Skip to content

feat(evmonly): persist executor receipts - #4088

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

feat(evmonly): persist executor receipts#4088
codchen wants to merge 5 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()
  • persist receipts before the height-advancing state commit so receipt failures leave the block retryable
  • make the node own and close the manager only after giga.Run exits, including construction and startup failure cleanup
  • reject EVM-only execution in seed mode at node configuration validation

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-tendermint/node
  • golangci-lint run --timeout 10m — 0 issues
  • golangci-lint fmt --diff
  • gofmt and goimports on every touched Go file

Notes

  • 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 4, 2026, 3:29 AM

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.92405% with 114 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.33%. Comparing base (67ade5e) to head (5de8bc1).

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/public.go 70.27% 8 Missing and 3 partials ⚠️
sei-tendermint/node/node.go 44.44% 9 Missing and 1 partial ⚠️
giga/evmonly/giga_store.go 86.66% 1 Missing and 1 partial ⚠️
giga/evmonly/receipt.go 94.59% 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.26%   60.33%   -0.94%     
==========================================
  Files        2188     2083     -105     
  Lines      192258   179709   -12549     
==========================================
- Hits       117793   108427    -9366     
+ Misses      63301    61149    -2152     
+ Partials    11164    10133    -1031     
Flag Coverage Δ
sei-chain-pr 74.89% <63.92%> (?)
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/recovery.go 65.33% <100.00%> (+0.46%) ⬆️
sei-db/bootstrap/storage_manager.go 69.69% <100.00%> (+3.03%) ⬆️
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 94.59% <94.59%> (ø)
sei-tendermint/node/node.go 65.42% <44.44%> (-0.21%) ⬇️
... and 3 more

... and 110 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
Changes block commit ordering and shared storage lifecycle for Autobahn/EVM-only paths; receipt-before-state semantics affect retry behavior if state commit fails after receipts are written.

Overview
The EVM-only executor now wires through bootstrap.GigaStorageManager instead of a bare StateDB: WithStore is removed in favor of WithStorageManager, which must supply both state and receipt.ReceiptStore. After each block, geth receipts are converted to receipt.ReceiptRecord and written with SetReceipts before the height-advancing CommitStateChanges, so receipt failures leave state retryable; ResultSink runs only after both succeed.

A concurrency-safe MemoryReceiptStore backs tests, loadtest, and the in-memory Autobahn app. GigaStorageManager gains NewGigaStorageManagerWithStores, a StateStore() abstraction (injectable vs production *giga.StateDB), and closes via stateStore.

Tendermint node startup centralizes Autobahn storage: the configured block DB lives on the manager, buildGigaRouter consumes manager.BlockStore() instead of opening its own store, and the node owns and closes the manager after giga.Run (including failed construction). The EVM-only in-memory app moves to evmonlyapp, returns the manager from NewEVMOnlyInMemoryApplication, and seed mode rejects evm-only-in-memory.

Reviewed by Cursor Bugbot for commit 5de8bc1. 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 Outdated
Comment thread giga/evmonly/giga_store.go
Comment thread giga/evmonly/storage_manager.go
Comment thread giga/evmonly/receipt_store.go
Comment thread sei-tendermint/node/public.go
@codchen

codchen commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator 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 three fixes from the last round landed correctly: receipts are now written before the height-advancing state commit (with tests covering both failure directions), seed mode rejects EVMOnlyInMemory at config validation, and the README no longer references a nonexistent type. Storage ownership through NewmakeNodecloseGigaStorageManager is leak-free on every path I traced; the remaining items are the two unresolved design/retention suggestions plus two new ones on the injected-manager contract.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] newReceiptContext (giga/evmonly/receipt.go) still builds an sdk.Context with a nil MultiStore and carries no comment recording that. It is safe for both current backends (receiptStore.SetReceipts never touches the multistore, and MemoryReceiptStore ignores it), but the executor now accepts any receipt.ReceiptStore via WithStorageManager, so a one-line note that the returned context has no store would keep the next backend from hitting a nil deref through ctx.KVStore.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

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] Still unaddressed from the last round, and Codex flags it independently: MemoryReceiptStore grows without bound. blocks and byTxHash retain every receipt (with its logs) for the process lifetime, and nothing ever calls PruneHistoryNewGigaStorageManagerWithStores does not run startGarbageCollector, so the store joins no prune cycle even though ExternalPruning() reports true.

Before this PR the EVM-only runtime dropped receipts with the block result, so a long Autobahn Docker load run now carries a steady-state memory cost proportional to total transactions rather than to live state. Either cap retention inside SetReceipts (a KeepRecent-style floor) or have the EVM-only app call PruneHistory on commit. Failing that, a sentence on this godoc saying retention is unbounded absent an external collector would at least make the constraint visible where the store is constructed.

}

// NewGigaStorageManagerWithStores returns a manager that owns the supplied stores.
func NewGigaStorageManagerWithStores(

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] A manager built here is a different animal from one built by NewGigaStorageManager, and the godoc says none of it: cfg is nil, no recovery runs, no garbage collector starts, and stateDB stays nil while stateStore is set — so StateDB(), StateWAL(), SC() and SS() all return nil, prunableStores() skips the injected state store because it gates on stateDB, and recoverStores would nil-deref on m.stateDB.RollbackTo(target).

Nothing reaches those paths today (only OpenDBWithRecovery calls recoverStores, and only the config-driven constructor calls that), but GigaStorageManager is the production storage owner, so the next caller has no way to know which half of its surface is live. Worth stating the contract on this godoc — which accessors are valid, and that retention/recovery are the caller's problem — or guarding recoverStores on a nil stateDB.


// 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] Repeating from the last round, unchanged: taking the concrete *bootstrap.GigaStorageManager pulls sei-db/bootstrap (littblock, flatkv, controller, the checkpoint scheduler, autobahn/blockstore) into giga/evmonly, which previously depended only on sei-db/state_db/giga and sei-db/proto — including for the standalone evmonly-loadtest binary.

The executor uses exactly two methods. Declaring them here — type StorageManager interface { StateStore() gigatypes.StateDB; ReceiptDB() receipt.ReceiptStore } — keeps *bootstrap.GigaStorageManager a valid argument, lets withTestStores and the load harness pass a two-field struct instead of routing through NewGigaStorageManagerWithStores(nil, ...), and drops the bootstrap dependency. (The part of the earlier comment about the constructor being test-shaped no longer holds — openAutobahnStorageManager is a production caller now — but the dependency direction is the same.)

func NewEVMOnlyInMemoryApplication(
chainID uint64,
validators []abci.ValidatorUpdate,
blockStore *blockstore.Store,

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] All three call sites in app_test.go pass nil here, but the godoc does not say nil is allowed. sei-tendermint/AGENTS.md requires utils.Option for optional values and disallows passing nil as a function value unless explicitly documented. A nil block store is also semantically load-bearing — it produces a manager that createRouter then rejects with "autobahn requires a storage manager with a block store" — so either take utils.Option[*blockstore.Store] or state on the godoc that nil means "no block store, tests only".

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