Fix typed receipt encoding in putSyncTransactionReceipts#10044
Fix typed receipt encoding in putSyncTransactionReceipts#10044macfarla wants to merge 1 commit intobesu-eth:mainfrom
Conversation
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>
There was a problem hiding this comment.
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
< 0x80using RLP-bytes encoding before list-encoding inputSyncTransactionReceipts. - 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. |
| 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())); |
|
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. |
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?
doc-change-requiredlabel to this PR if updates are required.Locally, you can run these tests to catch failures early:
./gradlew spotlessApply./gradlew build./gradlew acceptanceTest./gradlew integrationTest./gradlew ethereum:referenceTests:referenceTests