Skip to content

fix(l1): flush frame receipts with the storage codec - #7082

Open
AnkushinDaniil wants to merge 1 commit into
lambdaclass:mainfrom
AnkushinDaniil:daniil/frame-receipt-storage-codec
Open

fix(l1): flush frame receipts with the storage codec#7082
AnkushinDaniil wants to merge 1 commit into
lambdaclass:mainfrom
AnkushinDaniil:daniil/frame-receipt-storage-codec

Conversation

@AnkushinDaniil

@AnkushinDaniil AnkushinDaniil commented Aug 1, 2026

Copy link
Copy Markdown

Motivation

The block-data buffer flush writes receipts with the consensus codec (encode_to_vec), while add_receipt, add_receipts and the read path (get_receipt_by_block_hash) all use the storage codec. For non-frame receipts the two encodings coincide, so the mismatch never showed.

An EIP-8141 frame receipt is different: its consensus layout omits the top-level succeeded and leads with cumulative_gas_used. decode_storage therefore reads a gas value where it expects a bool and fails with MalformedBoolean. The receipt is served correctly from the buffer and becomes permanently unreadable the moment the block is flushed.

Description

One line: the flush path now uses encode_storage(), matching every other writer and the reader.

How was it found

On a Nethermind + ethrex devnet. Both clients agreed on block 69 byte for byte — same block hash, state root, receipts root and block access list hash — and both served eth_getTransactionByHash for the frame transaction in it, but eth_getTransactionReceipt returned null on ethrex. eth_getBlockReceipts surfaced the real cause:

Internal Error: Error decoding field 'succeeded' of type bool: MalformedBoolean

Tests

flushed_frame_receipt_survives_the_storage_codec stores a frame receipt through store_block_updates, forces a flush so the buffer can no longer answer, and reads it back. Verified in both directions: it fails with MalformedBoolean on the previous line and passes with the fix.

The fix is forward-only. Frame receipts already written by the old path stay on disk in the consensus encoding and keep failing to decode, so eth_getTransactionReceipt still returns null for those blocks until the node resyncs. Non-frame receipts encode identically under both codecs, so nothing before Hegota is affected.

@AnkushinDaniil
AnkushinDaniil requested a review from a team as a code owner August 1, 2026 10:29
Copilot AI review requested due to automatic review settings August 1, 2026 10:29
@github-actions github-actions Bot added the external-contributor PR opened by a contributor outside the team label Aug 1, 2026
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown

Greptile Summary

The PR corrects deferred receipt persistence by using the storage codec expected by RECEIPTS_V2 readers.

  • Changes buffered receipt flushing from the consensus encoding to encode_storage().
  • Adds an end-to-end regression test that flushes and reloads an EIP-8141 frame receipt after buffer eviction.

Confidence Score: 5/5

The PR appears safe to merge and fixes the frame-receipt persistence mismatch without introducing a conflicting storage path.

The changed writer now uses the codec consumed by every live RECEIPTS_V2 read path, while the regression test synchronously evicts the buffered receipt and verifies the persisted frame receipt round-trips from backend storage.

Important Files Changed

Filename Overview
crates/storage/store.rs Aligns deferred receipt writes with the storage decoder and the existing direct receipt writers.
test/tests/storage/deferred_persistence_tests.rs Adds a valid disk-path regression test that verifies a frame receipt survives flush, eviction, canonical lookup, and storage decoding.

Sequence Diagram

sequenceDiagram
  participant Worker as Persist worker
  participant Buffer as BlockDataBuffer
  participant DB as RECEIPTS_V2
  participant Reader as Receipt reader
  Worker->>Buffer: Select flushable block
  Worker->>DB: Write receipt.encode_storage()
  Worker->>Buffer: Evict after commit
  Reader->>Buffer: Lookup receipt
  Buffer-->>Reader: Miss after eviction
  Reader->>DB: Read persisted bytes
  DB-->>Reader: Receipt::decode_storage()
Loading

Reviews (1): Last reviewed commit: "fix(l1): flush frame receipts with the s..." | Re-trigger Greptile

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a receipt persistence bug in the storage flush path where receipts were written using the consensus/wire codec, while the read path (and other writers) expect the storage codec—causing EIP-8141 frame receipts to become undecodable after flush.

Changes:

  • Persist flushed receipts using Receipt::encode_storage() to match the read/other write paths.
  • Add a regression test covering a flushed frame receipt roundtrip through store_block_updates + forced flush + get_receipt.

Reviewed changes

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

File Description
test/tests/storage/deferred_persistence_tests.rs Adds a regression test ensuring frame receipts remain readable after the buffer flushes to disk.
crates/storage/store.rs Switches receipt encoding during block-data flush from consensus (encode_to_vec) to storage (encode_storage).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ElFantasma ElFantasma left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Correct and now consistent with all four other receipt codec sites; one non-blocking note about receipts already written by the old path.

Comment thread crates/storage/store.rs
RECEIPTS_V2,
&receipt_key(&hash, index as u64),
&receipt.encode_to_vec(),
&receipt.encode_storage(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified this was the only outlier — add_receipt (:836), add_receipts (:851) and both read paths (:883, :1385) already use the storage codec, so this line makes all five agree.

One thing the fix can't do by itself: frame receipts already flushed by the old path stay unreadable. They're on disk in the consensus encoding, and after this change the reader still calls decode_storage, so they keep failing with the same MalformedBoolean. Anyone running a Hegota devnet node from before this lands will still see eth_getTransactionReceipt return null for those blocks.

I don't think that warrants a migration or a STORE_SCHEMA_VERSION bump. Non-frame receipts encode identically under both codecs, so nothing pre-Hegota is affected, and frame receipts only exist on an unreleased fork — a resync is the proportionate remedy for the handful of devnet databases involved. But it's worth a line in the PR description so whoever runs those nodes knows the fix is forward-only, since the symptom (null receipts) is identical before and after and looks like the bug simply wasn't fixed.

Worth double-checking one thing I couldn't confirm from the diff: whether any devnet node is expected to keep its database across this change. If the answer is yes for a long-lived one, the calculus shifts toward a migration that re-encodes RECEIPTS_V2 entries whose tx_type is Frame.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No node keeps a database across this. The frame devnets are rebuilt from genesis on every run, so every affected database is throwaway and a resync costs nothing. The description now says the fix is forward-only. If a long-lived node appears later, re-encoding RECEIPTS_V2 entries whose tx_type is Frame is the migration, and it can land separately without touching this.

@AnkushinDaniil
AnkushinDaniil force-pushed the daniil/frame-receipt-storage-codec branch from 7a4ff51 to bea20c4 Compare August 4, 2026 15:18
The block-data buffer flush wrote receipts with the consensus codec while
every other writer and the read path use the storage codec. For non-frame
receipts the two agree, so this was invisible. A frame receipt's consensus
layout omits `succeeded` and leads with `cumulative_gas_used`, so
`decode_storage` reads a gas value as a bool and fails with
`MalformedBoolean`: once a block leaves the buffer, every frame transaction
in it loses its receipt.

Found on a two-client devnet, where `eth_getTransactionReceipt` returned
null on ethrex for a frame transaction both clients had in the same block.
@AnkushinDaniil
AnkushinDaniil force-pushed the daniil/frame-receipt-storage-codec branch from bea20c4 to 785bfe0 Compare August 6, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor PR opened by a contributor outside the team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants