Skip to content

receipt decoder - handle both formats at read time#10079

Draft
macfarla wants to merge 1 commit intobesu-eth:mainfrom
macfarla:fix/receipt-storage-format
Draft

receipt decoder - handle both formats at read time#10079
macfarla wants to merge 1 commit intobesu-eth:mainfrom
macfarla:fix/receipt-storage-format

Conversation

@macfarla
Copy link
Contributor

@macfarla macfarla commented Mar 20, 2026

PR description

Fix receipt decode failure after snap sync

Problem

After snap syncing receipts from a peer (eth/69 or eth/70), subsequent reads of those receipts from local storage failed with:

RLPException: Cannot enter a lists, input is fully consumed (at bytes 0-0: [])

This manifested in two ways:

  1. Local reads — rlpDecodeTransactionReceipts (called by blockchain.getTxReceipts()) threw RLPException when processing previously snap-synced blocks
  2. Serving receipts to peers — EthServer.constructGetPaginatedReceiptsResponse threw RLPException, which bubbled up to EthProtocolManager and triggered a BREACH_OF_PROTOCOL disconnect against the requesting peer , incorrectly blaming them for a local storage problem

Root cause

Snap sync receives receipts in wire format. Compact receipts (eth/69/70, no bloom filter) encode log topics as raw bytes32. The read path used TransactionReceiptDecoder, which calls Log.readFrom(in, compacted=true) and expects canonical compacted storage format where topics are [leadingZeros, shortData] lists. This format mismatch caused the RLPException.

Additionally, for EIP-2718 typed receipts in wire format (type_byte || rlp_list, first byte 0x01–0x7F), TransactionReceiptDecoder reads the first byte as a 1-byte SHORT_ELEMENT, slices it off leaving Bytes.EMPTY, then fails on enterList() of the empty input - the "at bytes 0-0: []" error.

Changes:
Instead of converting sync receipts to canonical format at write time (which fails for wire-format typed receipts passed to TransactionReceiptDecoder), store raw wire bytes and handle both formats at read time:

  • putSyncTransactionReceipts: restored to store raw bytes with normalizeReceiptRlpElement wrapping raw type bytes (0x01-0x7F) as byte strings so the RLP list structure remains unambiguous.

  • rlpDecodeTransactionReceipts: now uses SyncTransactionReceiptDecoder + SyncTransactionReceiptConverter rather than TransactionReceiptDecoder directly, handling all 8 format combinations (legacy/typed × compacted/non-compacted × wire/canonical).

  • SyncTransactionReceiptDecoder.parseLogs: upgraded to handle both raw bytes32 topics (wire format) and [leadingZeros, shortData] lists (canonical compacted storage format) via per-item nextIsList() check and a new readTrimmedData helper mirroring Log.readFrom compacted logic.

  • SyncTransactionReceiptConverter.toTransactionReceipt: added storageFormatRlpInput helper to wrap wire-format typed receipts (first byte < 0x80) as byte strings before passing to TransactionReceiptDecoder, fixing the "Cannot enter a list, input is fully consumed (at bytes 0-0: [])" RLPException on fresh nodes.

Fixed Issue(s)

Thanks for sending a pull request! Have you done the following?

  • Checked out our contribution guidelines?
  • Considered documentation and added the doc-change-required label to this PR if updates are required.
  • Considered the changelog and included an update if required.
  • For database changes (e.g. KeyValueSegmentIdentifier) considered compatibility and performed forwards and backwards compatibility tests

Locally, you can run these tests to catch failures early:

  • spotless: ./gradlew spotlessApply
  • unit tests: ./gradlew build
  • acceptance tests: ./gradlew acceptanceTest
  • integration tests: ./gradlew integrationTest
  • reference tests: ./gradlew ethereum:referenceTests:referenceTests
  • hive tests: Engine or other RPCs modified?

…d time

Instead of converting sync receipts to canonical format at write time
(which fails for wire-format typed receipts passed to TransactionReceiptDecoder),
store raw wire bytes and handle both formats at read time:

- putSyncTransactionReceipts: restored to store raw bytes with
  normalizeReceiptRlpElement wrapping raw type bytes (0x01-0x7F) as
  byte strings so the RLP list structure remains unambiguous.

- rlpDecodeTransactionReceipts: now uses SyncTransactionReceiptDecoder +
  SyncTransactionReceiptConverter rather than TransactionReceiptDecoder
  directly, handling all 8 format combinations (legacy/typed ×
  compacted/non-compacted × wire/canonical).

- SyncTransactionReceiptDecoder.parseLogs: upgraded to handle both
  raw bytes32 topics (wire format) and [leadingZeros, shortData] lists
  (canonical compacted storage format) via per-item nextIsList() check
  and a new readTrimmedData helper mirroring Log.readFrom compacted logic.

- SyncTransactionReceiptConverter.toTransactionReceipt: added
  storageFormatRlpInput helper to wrap wire-format typed receipts
  (first byte < 0x80) as byte strings before passing to
  TransactionReceiptDecoder, fixing the "Cannot enter a list, input
  is fully consumed (at bytes 0-0: [])" RLPException on fresh nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com>
Comment on lines +145 to +154
final List<Bytes> topics =
logInput.readList(
topicIn ->
topicIn.nextIsList()
? Bytes32.wrap(readTrimmedData(topicIn))
: topicIn.readBytes32());

// Data may also be compacted [leadingZeros, shortData] in canonical storage format.
final Bytes data =
logInput.nextIsList() ? readTrimmedData(logInput) : logInput.readBytes();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it possible to just do this change in the TransactionReceiptDecoder to avoid the need to use SyncTransactionReceiptDecoder and SyncTransactionReceiptConverter when loading from the DB?

@macfarla macfarla self-assigned this Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants