Skip to content

Fix typed receipt encoding in putSyncTransactionReceipts#10044

Open
macfarla wants to merge 1 commit intobesu-eth:mainfrom
macfarla:fix/typed-receipt-storage-encoding
Open

Fix typed receipt encoding in putSyncTransactionReceipts#10044
macfarla wants to merge 1 commit intobesu-eth:mainfrom
macfarla:fix/typed-receipt-storage-encoding

Conversation

@macfarla
Copy link
Contributor

@macfarla macfarla commented Mar 13, 2026

Receipts received via snap sync (eth/68) arrive through ReceiptsMessage.deserializeReceiptLists() which calls readBytes(), stripping the outer RLP bytes-element wrapper and leaving raw typeCode||rlp_body bytes (first byte in 0x01-0x7f).

When these were passed directly to SimpleNoCopyRlpEncoder.encodeList(), the single-byte type code (e.g. 0x02 for EIP-1559) was stored as a standalone RLP item and the rlp_body as another, splitting one receipt into two items. On read-back, TransactionReceiptDecoder.readFrom() saw only the 1-byte type code, called slice(1) producing Bytes.EMPTY, and enterList() threw RLPException -- causing the server to disconnect peers with BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED.

Fix by checking if the raw bytes start with a byte < 0x80 (raw EIP-2718 type code with no RLP wrapper) and, if so, encoding them as a proper RLP bytes element via NO_COPY_RLP_ENCODER.encode(). Receipts that already carry a valid RLP element header (>= 0x80: either a string prefix 0x80-0xbf or list prefix 0xc0-0xff) are stored unchanged.

Adds a test that simulates the network receive path (readBytes() stripping the outer wrapper) for EIP-1559 and ACCESS_LIST receipts.

Fixed Issue(s)

fixes #10043

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?

Receipts received via snap sync (eth/68) arrive through
ReceiptsMessage.deserializeReceiptLists() which calls readBytes(),
stripping the outer RLP bytes-element wrapper and leaving raw
typeCode||rlp_body bytes (first byte in 0x01-0x7f).

When these were passed directly to SimpleNoCopyRlpEncoder.encodeList(),
the single-byte type code (e.g. 0x02 for EIP-1559) was stored as a
standalone RLP item and the rlp_body as another, splitting one receipt
into two items. On read-back, TransactionReceiptDecoder.readFrom() saw
only the 1-byte type code, called slice(1) producing Bytes.EMPTY, and
enterList() threw RLPException -- causing the server to disconnect
peers with BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED.

Fix by checking if the raw bytes start with a byte < 0x80 (raw EIP-2718
type code with no RLP wrapper) and, if so, encoding them as a proper
RLP bytes element via NO_COPY_RLP_ENCODER.encode(). Receipts that
already carry a valid RLP element header (>= 0x80: either a string
prefix 0x80-0xbf or list prefix 0xc0-0xff) are stored unchanged.

Adds a test that simulates the network receive path (readBytes()
stripping the outer wrapper) for EIP-1559 and ACCESS_LIST receipts.

Signed-off-by: Sally MacFarlane <macfarla.github@gmail.com>
Copilot AI review requested due to automatic review settings March 13, 2026 08:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes persistence of EIP-2718 typed transaction receipts received via snap sync (eth/68) by ensuring “raw typeByte || rlpBody” payloads are stored as a single RLP bytes element, preventing receipt splitting and decode failures on read-back.

Changes:

  • Re-wraps receipt bytes starting with < 0x80 using RLP-bytes encoding before list-encoding in putSyncTransactionReceipts.
  • Adds a regression test that simulates the eth/68 receive path (readBytes() stripping the outer wrapper) for typed receipts (EIP-1559 + access list).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorage.java Ensures typed receipts arriving as raw typeByte
ethereum/core/src/test/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStoragePrefixedKeyBlockchainStorageTest.java Adds a regression test simulating snap-sync receipt decoding path and verifying round-trip storage/retrieval.

You can also share your feedback on Copilot code review. Take the survey.

// them (it expects readBytes() to return the full typeCode||body).
// Receipts that are already valid RLP elements (first byte >= 0x80:
// either an RLP string prefix 0x80-0xbf, or list prefix 0xc0-0xff)
// are stored as-is.
Comment on lines +368 to +385
transactionReceipts.stream()
.map(
r -> {
final Bytes rawBytes = r.getRlpBytes();
// EIP-2718 typed receipts received from the network arrive via
// readBytes() which strips the outer RLP bytes-element wrapper,
// leaving raw typeCode||rlp_body (first byte in 0x01-0x7f).
// These must be re-wrapped as a single RLP bytes element so that
// TransactionReceiptDecoder.decodeTypedReceiptComponents can decode
// them (it expects readBytes() to return the full typeCode||body).
// Receipts that are already valid RLP elements (first byte >= 0x80:
// either an RLP string prefix 0x80-0xbf, or list prefix 0xc0-0xff)
// are stored as-is.
return Byte.toUnsignedInt(rawBytes.get(0)) < 0x80
? NO_COPY_RLP_ENCODER.encode(rawBytes)
: rawBytes;
})
.toList()));
@macfarla
Copy link
Contributor Author

The bug has two sides, and affects eth/68 and eth/70:

Storage (write) - happens during snap sync receipt download regardless of protocol version. Whenever putSyncTransactionReceipts is called with receipts decoded from an eth/68-format ReceiptsMessage, typed receipts come in with their outer RLP wrapper stripped (via readBytes()), and get stored incorrectly. eth/69 is not affected because it encodes typed receipts as flat lists (nextIsList() = true → currentListAsBytes() → full list bytes preserved in getRlpBytes()).

Serving (read) - getTxReceipts() → rlpDecodeTransactionReceipts() fails on the corrupted data. This would crash whether the request came in via eth/68 GetReceiptsMessage or eth/70 GetPaginatedReceiptsMessage — both paths call the same getTxReceipts().

So: any node that snap-synced against an eth/68 peer (which is the common case today) and then tries to serve receipt requests will crash. The eth/70 context just happened to be when it was caught because that's what was being tested.

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.

snap - server disconnecting client for breach of protocol

3 participants