Skip to content

Receipt DB iteration - #4222

Merged
cody-littley merged 8 commits into
mainfrom
cjl/reciept-iteration
Sep 23, 2026
Merged

cody-littley merged 8 commits into
mainfrom
cjl/reciept-iteration

Conversation

@cody-littley

Copy link
Copy Markdown
Contributor

Adds the ability to iterate receipts in the RecieptDB.

@cursor

cursor Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes littidx write invariants and receipt-store config (littidx no longer selectable from app.toml); iteration touches retention/prune edge cases but is isolated to the Giga-managed backend.

Overview
Adds IterateReceipts and a ReceiptIterator to the receipt store API so callers can walk receipts in block order from a start height. Only the littidx backend implements the walk; pebble, in-memory Giga stores, and test fakes return ErrRangeQueryNotSupported.

littidx behavior is tightened for iteration: SetReceipts must cover every block (including blocks with no receipts, stored as empty parts), and writes that skip a block are rejected via requireNoSkippedBlock. The iterator positions on part keys, respects the retention floor, and handles empty-block stretches and post-prune keymap gaps via seek-from-oldest logic.

Configuration: littidx can no longer be set via app.toml (rs-backend); choosing it yields an explicit error that Giga opens that backend. Documented supported backend for app config is pebbledb only. Tests cover iterator walks, skipped-block refusal, and config rejection; the receipt bench simulator always commits each block.

Reviewed by Cursor Bugbot for commit 9561ac2. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 23, 2026, 5:06 PM

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bbe7e38. Configure here.

Comment thread sei-db/ledger_db/receipt/litt_receipt_iterator.go
seidroid[bot]
seidroid Bot previously requested changes Sep 17, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new IterateReceipts walk is a reasonable design, but adding it to the exported ReceiptStore interface breaks two evmrpc test fakes so ./evmrpc/... no longer builds, and the IteratorAt not-found fallback silently restarts the walk at the oldest key in the store, returning receipts below startBlock (and below the retention floor) on any store written by an earlier build.

Findings: 2 blocking | 2 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.
  • 2 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] The ReceiptIterator / IterateReceipts contract omits two properties a caller depends on: litt iterators capture a snapshot of the keys present at creation (blocks written afterwards are never yielded, so a follower cannot hold one open and expect it to track the head), and the documented "ascending block order" holds only because litt iterates in insertion order and blocks are normally written in ascending order — a part appended for an older block after a newer one (the legacy-migration shape the store doc already calls out) would be yielded out of order. Worth stating both in the interface godoc.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/ledger_db/receipt/receipt_store.go
Comment thread sei-db/ledger_db/receipt/litt_receipt_iterator.go
Comment thread sei-db/ledger_db/receipt/litt_receipt_store.go
@codecov

codecov Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.94737% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.98%. Comparing base (aa7d397) to head (9561ac2).
⚠️ Report is 42 commits behind head on main.

Files with missing lines Patch % Lines
sei-db/ledger_db/receipt/litt_receipt_iterator.go 75.67% 18 Missing ⚠️
giga/evmonly/receipt_store.go 0.00% 1 Missing ⚠️
sei-db/ledger_db/receipt/litt_receipt_store.go 93.75% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4222      +/-   ##
==========================================
- Coverage   67.01%   65.98%   -1.04%     
==========================================
  Files        2179     2060     -119     
  Lines      167367   156660   -10707     
==========================================
- Hits       112163   103370    -8793     
+ Misses      55063    53149    -1914     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 74.50% <78.94%> (?)
sei-db 74.50% <ø> (ø)
sei-db-state-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-db/config/receipt_config.go 86.27% <100.00%> (+0.56%) ⬆️
sei-db/ledger_db/receipt/receipt_store.go 73.07% <100.00%> (+0.14%) ⬆️
giga/evmonly/receipt_store.go 73.55% <0.00%> (+0.22%) ⬆️
sei-db/ledger_db/receipt/litt_receipt_store.go 86.85% <93.75%> (+0.32%) ⬆️
sei-db/ledger_db/receipt/litt_receipt_iterator.go 75.67% <75.67%> (ø)

... and 136 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cody-littley

Copy link
Copy Markdown
Contributor Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Sep 17, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The iterator design is sound and the previous round's two blockers (missing interface implementations, the not-found fallback walking below startBlock) are genuinely fixed by seekBlock. But the new write-side "no skipped block" invariant turns a legitimately lagging receipt head into a permanent, unrecoverable node panic loop on a path documented as must-not-error, and the iterator still never enforces its own lower bound.

Findings: 1 blocking | 3 non-blocking | 2 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] The ReceiptIterator godoc still omits the snapshot property it inherits from litt: the iterator captures the keys present at creation (litt.Table.Iterator, table.go:160), so blocks written after it is opened are never yielded. A caller that holds one open expecting it to track the head silently sees nothing. Worth stating on IterateReceipts alongside the ordering guarantee.
  • [suggestion] No test exercises the property seekBlock exists for — skipping receipts that sit below start on the whole-store fallback. TestReceiptIteratorStartsAtOldestStoredBlock reaches walkFromOldest, but with startBlock == 0 there is nothing below it to wrongly yield, so the seek would pass even if it returned the first primary key unconditionally. The new write guard makes that store shape unreachable through the public API, so pinning it needs an internal test that writes the part keys directly (the package already has *_internal_test.go files).
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/ledger_db/receipt/litt_receipt_store.go
Comment thread sei-db/ledger_db/receipt/litt_receipt_iterator.go
@cody-littley

Copy link
Copy Markdown
Contributor Author

@seidroid review

seidroid[bot]
seidroid Bot previously requested changes Sep 21, 2026

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The iterator design is sound and the empty-part write makes positioning work, but the new "no skipped block" write guard is still a latching, pre-commit-fatal refusal on a configuration the PR only documents as Giga-only rather than enforcing, and the iterator still never enforces its own lower bound.

Findings: 1 blocking | 4 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.
  • 1 blocking issue(s) flagged inline on specific lines.

Non-blocking

  • [suggestion] No test exercises the property seekBlock exists for — skipping part keys below start on the walkFromOldest fallback. TestReceiptIteratorStartsAtOldestStoredBlock reaches that path with startBlock == 0, so there is nothing below it to wrongly yield, and TestReceiptIteratorStartsAtRetentionFloor finds the part key at the clamped floor and never falls through. The new write guard makes the fall-through store shape unreachable through the public API, so pinning it needs an internal test that writes part keys directly (the package already has *_internal_test.go files).
  • [suggestion] writeReceipts derives the blocks it records from each record's own Receipt.BlockNumber, not from height, so the new "every block the store accepted has a key of its own" invariant only holds while the two agree — only the empty-batch path falls back to height. flushTransientReceipts explicitly documents that the transient store can hold receipts for a block other than the current height (x/evm/keeper/receipt.go:146-150); such a write leaves height unrecorded and the head short, and the next block then trips requireNoSkippedBlock. Recording height alongside the record-derived blocks, or checking that they agree, would make the guard measure what its doc claims.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-db/ledger_db/receipt/litt_receipt_store.go
Comment thread sei-db/ledger_db/receipt/litt_receipt_iterator.go
Comment thread sei-db/ledger_db/receipt/receipt_store.go
@cody-littley

Copy link
Copy Markdown
Contributor Author

@seidroid review

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The iterator design is sound, and refusing rs-backend = "littidx" from app.toml genuinely closes the previous round's blocker: the littidx store is now reachable only through GigaStorageManager, whose OpenDBWithRecovery converges every store onto min(block, state, receipt) before opening, so the fatal requireNoSkippedBlock refusal sits behind an invariant that is established by construction (evmonly's FinalizeBlock → giga_store.go writes every height in order, empty blocks included). Remaining findings are non-blocking: a stale rationale on the cryptosim write, an iterator contract still enforced only by write-ordering convention, an app.toml key that can no longer reach any store, and a missing CHANGELOG/compat note.

Findings: 0 blocking | 5 non-blocking | 3 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] No test exercises the property seekBlock exists for — skipping part keys below start on the walkFromOldest fallback. TestReceiptIteratorStartsAtOldestStoredBlock reaches that path with startBlock == 0 (nothing below it to wrongly yield), TestReceiptIteratorStartsAtRetentionFloor finds the part key at the clamped floor and never falls through, and TestReceiptIteratorStartsInAnEmptyStretch finds the empty-block part key. The write guard makes the fall-through shape unreachable through the public API, so pinning the GC-race case the walkFromOldest comment describes needs an internal test that writes part keys directly (the package already has *_internal_test.go files).
  • [suggestion] rs-backend = "littidx" was a previously accepted app.toml value (the old error advertised supported: pebbledb, littidx); it now fails ReadReceiptConfig, and app.go:653 panics on that error, so a node carrying the key cannot start until app.toml is edited — and after switching to pebbledb its receipt history under data/ledger/receipt/littidx is no longer served. That is the intended change, but there is no ## Unreleased CHANGELOG entry, even though the repo records exactly this class of config-compatibility change there (e.g. #4191's api.swagger no-op, #4032's telemetry default). Worth an Improvements/Upgrade-guide line so the startup panic is not an operator's first notice.
  • 3 suggestion(s)/nit(s) flagged inline on specific lines.

if len(records) > 0 {
sdkCtx := sdk.NewContext(nil, tmproto.Header{Height: int64(blockNumber)}, false) //nolint:gosec
// Every block is written, including one that produced no receipts: the store records each block
// it is given so a walk can position at any of them, and refuses a write that skips one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] This comment's rationale doesn't hold for this store. NewRecieptStoreSimulator opens the receipt store with Backend: "pebbledb" (same file, ~line 141), and the pebble backend has no part keys, no walk to position, and no skipped-block refusal — receiptStore.SetReceipts just applies the (here empty) changeset and stamps the version. Writing every block is still the right change, but for a different reason: production's flushTransientReceipts (x/evm/keeper/receipt.go:166) calls SetReceipts on every block whether or not it produced receipts, so the simulator now matches the production write cadence.

Worth noting the measurement effect too: RecordReceiptBlockWriteDuration and ReportReceiptsWritten(0) now fire for receipt-less blocks, so write-duration percentiles from this benchmark are no longer comparable with runs from before this change.

}
continue
}
r.txHash = common.BytesToHash(key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] Next still never checks the lower bound, so "at or above startBlock, in ascending block order" holds only while parts are appended in ascending block order — a write-ordering convention, not something the iterator establishes. requireNoSkippedBlock doesn't establish it either: it rejects only blockNumber > head+1, so any write at or below the head is accepted (TestLittIdxAcceptsRepeatedBlock pins that). Write blocks 1, 2, 3 then a second part for block 1 and, because litt iterates in insertion order, IterateReceipts(2) yields block 2, block 3, then block 1's receipts — below startBlock, out of order, and when 2 is the retention floor it re-exposes receipts GetReceiptFromStore refuses via belowRetentionFloor.

Not a blocker this round: with littidx now refused from app.toml, the only writer is the Giga path, which executes heights in order, has no legacy-migration multi-part flush (recovery.go:87), and rolls the receipt tail back with PruneAfter before reopening — so no live path produces that shape. It's still worth putting the guarantee on the iterator rather than on the callers, since there is no caller yet to learn the convention from: keep start on receiptIterator and skip in Next while r.block < start, which covers both the IteratorAt and walkFromOldest entry paths.

Comment thread sei-db/config/toml.go Outdated
# Backend defines the receipt store backend.
# Supported backends: pebble (aka pebbledb)
# The littidx backend is opened by Giga through its own storage config and is
# refused here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[suggestion] With littidx refused here, the log-filter-parallelism key further down this template (line ~218: "Applies only when rs-backend = littidx") can no longer reach any store that reads it: Giga builds its receipt config in DefaultGigaStorageConfig from DefaultReceiptStoreConfig() and never consults app.toml, and the pebble backend ignores the value. So this file now documents a tunable that is inert wherever it is set. Either adjust that key's comment to say receipt tuning for the Giga-opened store isn't read from app.toml, or wire Giga's ReceiptDBConfig from the app.toml-read config so the key keeps meaning.

@seidroid
seidroid Bot dismissed stale reviews from themself September 21, 2026 16:54

Superseded: latest AI review found no blocking issues.

Comment thread sei-db/config/toml.go Outdated

# Backend defines the receipt store backend.
# Supported backends: pebble (aka pebbledb)
# The littidx backend is opened by Giga through its own storage config and is

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.

We added that for migration purpose, so I dont think anyone has used littidx yet. We don't need to mention this comment since the above comment already state the supported backend is pebble

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

removed

@shemnon shemnon 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.

LGTM

@cody-littley
cody-littley added this pull request to the merge queue Sep 23, 2026
Merged via the queue into main with commit 96d0d6a Sep 23, 2026
74 checks passed
@cody-littley
cody-littley deleted the cjl/reciept-iteration branch September 23, 2026 17:37
shemnon added a commit that referenced this pull request Sep 23, 2026
Addresses a seidroid review finding: recomputeBlockStats took each
receipt by tx hash alone, trusting that the returned receipt actually
belonged to the block being recomputed. Now that #4222 has landed,
prefer ReceiptStore.IterateReceipts(height) — each receipt it yields
is scoped to height by the iterator's own BlockNumber(), not by a
tx-hash lookup that could answer from any block.

- recomputeBlockStats delegates to receiptRecordsForHeight, which
  tries IterateReceipts first and falls back to decoding the block and
  fetching receipts by hash (receiptRecordsFromBlock, the prior
  behavior) only on ErrRangeQueryNotSupported — the littidx backend
  supports iteration, but pebble and MemoryReceiptStore do not.
- Added stubIteratingReceiptStore/fakeReceiptIterator test doubles and
  a test proving the iterator path is used when available (backend.Block
  is left nil, so a fall-through to the old path would panic).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants