Skip to content

feat(evmonly): persist executor receipts - #4088

Open
codchen wants to merge 3 commits into
mainfrom
codex/evmonly-receipt-store
Open

feat(evmonly): persist executor receipts#4088
codchen wants to merge 3 commits into
mainfrom
codex/evmonly-receipt-store

Conversation

@codchen

@codchen codchen commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • convert EVM-only geth receipts into the shared receipt.ReceiptRecord format and persist them through the existing receipt.ReceiptStore interface
  • configure the executor with the concrete bootstrap.GigaStorageManager and remove the EVM-only in-memory manager implementation
  • keep the EVM-only state and receipt backends in memory while installing them into the shared manager
  • open Autobahn’s configured memory or LittDB block store under the same manager and build Autobahn data state from manager.BlockStore()
  • make the node own and close the manager only after giga.Run exits, including construction and startup failure cleanup
  • keep result sinks behind successful state and receipt persistence, including empty-block receipt versions

Testing

  • go test ./giga/evmonly/...
  • go test ./sei-db/bootstrap
  • go test ./sei-db/ledger_db/receipt
  • go test ./sei-tendermint/internal/evmonlyapp
  • go test ./sei-tendermint/internal/p2p
  • go test ./sei-tendermint/node -run "TestPrepareApplication|TestValidateNodeSetupConfig|TestBuildGigaConfig|TestPreparePersistentStateDir" -count=1
  • go test -race ./giga/evmonly ./sei-tendermint/internal/evmonlyapp
  • go vet ./giga/evmonly/... ./sei-db/bootstrap ./sei-tendermint/internal/evmonlyapp ./sei-tendermint/node
  • gofmt and goimports on every touched Go file

Notes

  • make fmtcheck cannot start locally because the pinned golangci-lint executable was built with Go 1.24, below the repository target of Go 1.25.6.
  • The full sei-tendermint/node test package reaches an unrelated local-port collision in TestFreezeModeDisablesMempoolTraffic; the targeted node setup tests pass independently.

@github-actions

github-actions Bot commented Sep 3, 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 3, 2026, 1:08 PM

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.07190% with 113 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.27%. Comparing base (7273a43) to head (af5a2d7).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
giga/evmonly/receipt_store.go 63.43% 36 Missing and 13 partials ⚠️
sei-tendermint/node/setup.go 13.33% 38 Missing and 1 partial ⚠️
sei-tendermint/node/node.go 44.44% 9 Missing and 1 partial ⚠️
sei-tendermint/node/public.go 71.42% 8 Missing and 2 partials ⚠️
giga/evmonly/giga_store.go 86.66% 1 Missing and 1 partial ⚠️
giga/evmonly/receipt.go 93.93% 1 Missing and 1 partial ⚠️
sei-tendermint/internal/evmonlyapp/app.go 92.30% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4088      +/-   ##
==========================================
- Coverage   61.30%   60.27%   -1.04%     
==========================================
  Files        2178     2074     -104     
  Lines      190788   178692   -12096     
==========================================
- Hits       116968   107707    -9261     
+ Misses      62796    60957    -1839     
+ Partials    11024    10028     -996     
Flag Coverage Δ
sei-chain-pr 74.99% <63.07%> (?)
sei-db 69.80% <ø> (ø)
sei-db-state-db ?

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

Files with missing lines Coverage Δ
giga/evmonly/cmd/evmonly-loadtest/pipeline.go 72.01% <100.00%> (+1.22%) ⬆️
giga/evmonly/executor.go 86.74% <ø> (-0.20%) ⬇️
giga/evmonly/storage_manager.go 100.00% <100.00%> (ø)
sei-db/bootstrap/storage_manager.go 70.09% <100.00%> (+1.46%) ⬆️
sei-tendermint/node/seed.go 52.13% <100.00%> (ø)
sei-tendermint/internal/evmonlyapp/app.go 71.93% <92.30%> (ø)
giga/evmonly/giga_store.go 88.88% <86.66%> (-1.81%) ⬇️
giga/evmonly/receipt.go 93.93% <93.93%> (ø)
sei-tendermint/node/node.go 65.42% <44.44%> (-0.21%) ⬇️
sei-tendermint/node/public.go 72.56% <71.42%> (-4.36%) ⬇️
... and 2 more

... and 109 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.

@codchen
codchen marked this pull request as ready for review September 3, 2026 13:06
@cursor

cursor Bot commented Sep 3, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches ordered block commit, receipt persistence after state (no rollback on receipt failure), and Autobahn storage lifecycle; mis-wiring could leave state advanced without receipts or leak/close stores at the wrong time.

Overview
The EVM-only executor now commits state and receipts together through bootstrap.GigaStorageManager: WithStore is replaced by WithStorageManager, which must supply both giga.StateDB and receipt.ReceiptStore plus the named changeset encoder. After each block’s state commit, geth receipts are converted to receipt.ReceiptRecord and written via the shared ledger interface (including empty blocks); ResultSink runs only after both succeed.

Tests and the load harness wire MemoryStore + NewMemoryReceiptStore into the manager via NewGigaStorageManagerWithStores. A concurrency-safe MemoryReceiptStore implements the production receipt API for ephemeral runtimes.

Tendermint / Autobahn centralizes storage: the node opens the configured block DB under one manager, buildGigaRouter uses manager.BlockStore() instead of opening its own store, and the manager is closed after giga.Run (with startup/failure cleanup). The in-memory EVM-only ABCI app moves to internal/evmonlyapp and returns the shared manager from construction.

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

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

Solid, well-tested refactor that routes EVM-only executor receipts through the shared receipt.ReceiptStore and moves Autobahn block-store ownership into a GigaStorageManager that the node closes after giga.Run. No blockers; the notable items are a README reference to a type that doesn't exist, an unrecoverable state/receipt ordering on the persistence path, and the executor now depending on the concrete bootstrap manager.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] newReceiptContext builds an sdk.Context with a nil MultiStore. That is safe for both current ReceiptStore backends (the write path never touches the store, and legacyReceiptFromKVStore guards on a nil storeKey), but nothing records the assumption. A one-line comment on newReceiptContext stating that the returned context carries no store and is only valid for backends constructed without a legacy storeKey would keep the next caller from hitting a nil deref via ctx.KVStore.
  • 5 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread giga/evmonly/README.md
blocks. `ResultSink` runs after both stores succeed; a persistence error does
not roll back the state commit.

`MemoryStorageManager` supplies the non-persistent state and receipt

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] MemoryStorageManager does not exist anywhere in the tree — grep -r MemoryStorageManager matches only this line. The PR removed the EVM-only in-memory manager in favour of bootstrap.NewGigaStorageManagerWithStores, so this paragraph documents a type readers cannot find. Reword to describe MemoryStore and MemoryReceiptStore as the two non-persistent implementations installed into a bootstrap.GigaStorageManager.

Relatedly, "Neither is a production implementation" is a bit strong now that MemoryReceiptStore backs the Autobahn EVM-only app rather than only tests.

if err := stateStore.CommitStateChanges(blockNumber, changesets); err != nil {
return nil, fmt.Errorf("commit state changes for block %d: %w", req.Context.Number, err)
}
if err := receiptStore.SetReceipts(newReceiptContext(ctx, blockNumber), records); err != nil {

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] Receipts are written after CommitStateChanges, and that ordering makes a receipt failure unrecoverable rather than retryable: the state store has already advanced its height, so re-executing the same block fails (MemoryStore.CommitStateChanges rejects blockNum <= currentHeight at memory_store.go:231, and the giga store is height-ordered too). A cancelled context during shutdown reaches this exact path, since MemoryReceiptStore.SetReceipts returns ctx.Err() after the commit already landed.

Swapping the two writes removes the trap: receipt writes are keyed by tx hash and idempotent, so writing them first and leaving CommitStateChanges as the last (height-advancing) step means a receipt failure leaves the block fully retryable, and a state-commit failure leaves only receipts that the retry overwrites. If the current order is deliberate, the README sentence "a persistence error does not roll back the state commit" should also say the block cannot then be re-executed.


// WithStorageManager selects the stores used for state and receipt persistence.
// The encoder converts executor-native state changes into the state store's format.
func WithStorageManager(manager *bootstrap.GigaStorageManager, encoder NamedChangeSetEncoder) Option {

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] Taking the concrete *bootstrap.GigaStorageManager pulls sei-db/bootstrap (littblock, flatkv, controller, the checkpoint scheduler) into giga/evmonly, which previously depended only on sei-db/state_db/giga and sei-db/proto. It also forced a test-shaped constructor, NewGigaStorageManagerWithStores, into the production bootstrap package, where every current caller passes a nil block store just to hold two in-memory stores.

The executor only needs two methods. Declaring them in evmonly — e.g. type StorageManager interface { StateDB() gigastore.StateDB; ReceiptDB() receipt.ReceiptStore } — keeps *bootstrap.GigaStorageManager a valid argument (it already satisfies that shape), lets tests and the load harness pass a two-field struct, and drops the bootstrap dependency along with the extra constructor. It would also make the README's "satisfies this contract" wording literally true.

receipt *evmtypes.Receipt
}

// MemoryReceiptStore retains receipts in memory by transaction hash and block.

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] MemoryReceiptStore grows without bound: blocks and byTxHash retain every receipt (with its logs) for the process lifetime, and nothing calls PruneHistoryNewGigaStorageManagerWithStores never starts a StorageGarbageCollector, so the store joins no prune cycle. Before this PR the EVM-only runtime dropped receipts with the block result, so a long Autobahn Docker load run now carries a new steady-state memory cost proportional to total transactions rather than to live state.

Worth either capping retention in the store (a KeepRecent-style floor applied inside SetReceipts) or having the EVM-only app call PruneHistory on commit. A note on this godoc that retention is unbounded absent an external collector would at least make the constraint visible at the call site.

storageManager,
nodeOptions...,
)
case config.ModeSeed:

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] The seed branch never sets storageManagerTransferred and never closes the manager, so a successful makeSeedNode return would leak the block store that prepareApplication opened (holding the littDB directory lock for the node's lifetime).

This is not reachable today: EVMOnlyInMemory requires AutobahnConfigFile (validateNodeSetupConfig), and makeSeedNode passes utils.None[*proxy.Proxy]() to createRouter, which fails with "autobahn requires app" whenever AutobahnConfigFile != "" — so the error path always runs the deferred close. But the ownership protocol this PR introduces then depends on an invariant two functions away. Rejecting EVMOnlyInMemory in seed mode inside validateNodeSetupConfig, or closing the manager before returning the seed node, makes it hold locally.

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.

1 participant