Skip to content

feat(evmonly): run load tests on Giga storage - #4088

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

feat(evmonly): run load tests on Giga storage#4088
codchen wants to merge 11 commits into
mainfrom
codex/evmonly-receipt-store

Conversation

@codchen

@codchen codchen commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • persist EVM-only execution receipts through the shared receipt.ReceiptStore owned by the existing bootstrap.GigaStorageManager
  • serve eth_getTransactionReceipt from the manager-owned receipt store, with finalized block hashes supplied by the Autobahn-aware block reader
  • return Ethereum-compatible receipt fields and null for unknown or not-yet-committed transactions
  • run standalone and Autobahn EVM-only load tests against disk-backed FlatKV state and littidx receipts instead of the in-memory test stores
  • use the same manager-owned littblock store for Autobahn EVM-only consensus blocks
  • configure EVM-only runtimes as validators with GigaSS disabled while explicitly retaining ReceiptDB persistence
  • use the FlatKV balance read/write support from Add balance support to flatKV #4101 and encode balance, nonce, code, and storage changes, including storage-prefix clears
  • seed standalone generated genesis state at height 1 and execute measured blocks from height 2
  • supply initial funded state only for synthetic Autobahn accounts absent from FlatKV; subsequent balance reads use their persisted account rows
  • retain MemoryStore and MemoryReceiptStore as documented unit-test implementations only
  • rename the load-test mode from evm-only-in-memory to evm-only

Storage boundary

This branch is rebased onto the current main, including #4101, and has no net changes under sei-db. It consumes the existing manager APIs: StateDB(), ReceiptDB(), and BlockStore(). EVM-only uses a shared validator storage configuration: FlatKV, littidx receipts, and littblock remain enabled, while GigaSS is disabled because EVM-only execution does not use it. The standalone harness opens the block store but has no consensus layer to populate blocks.

Testing

  • scripts/ramtest.sh ./giga/evmonly/... ./sei-tendermint/internal/evmonlyapp/... -count=1
  • scripts/ramtest.sh ./giga/evmonly/... ./sei-tendermint/internal/evmonlyapp/... -race -count=1
  • go test -race ./giga/evmonly/rpc -count=1
  • scripts/ramtest.sh ./sei-tendermint/node/... -run TestSelectAutobahnBlockStoreOwnership\|TestPrepareApplicationEVMOnly\|TestValidateNodeSetupConfig -count=1
  • scripts/ramtest.sh ./sei-tendermint/config/... ./cmd/autobahn-e2e/... -count=1
  • GOWORK=off go test -c -tags autobahn_integration ./integration_test/autobahn
  • make autobahn-evmonly-integration-test — four local Docker validators finalized 4,000 raw EVM transfers in 1.192 s (3,356 tx/s), every validator reached at least height 12, and every validator served a persisted receipt with validated block and transaction fields
  • golangci-lint run --timeout 10m — 0 issues
  • golangci-lint fmt --diff
  • gofmt and goimports on every touched Go file
  • bash -n docker/localnode/scripts/step4_config_override.sh
  • git diff --quiet origin/main -- sei-db

make fmtcheck invokes a cached go run linter binary built with Go 1.24 and rejects the repository Go 1.25.6 target locally; the equivalent installed golangci-lint fmt --diff check passes.

@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 10, 2026, 3:12 AM

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.89404% with 206 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.28%. Comparing base (c9c9549) to head (8dab03b).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
giga/evmonly/receipt_store.go 63.43% 37 Missing and 12 partials ⚠️
giga/evmonly/flatkv_changeset.go 66.66% 15 Missing and 15 partials ⚠️
sei-tendermint/node/setup.go 30.23% 25 Missing and 5 partials ⚠️
giga/evmonly/cmd/evmonly-loadtest/state.go 46.51% 14 Missing and 9 partials ⚠️
sei-tendermint/node/node.go 28.00% 16 Missing and 2 partials ⚠️
giga/evmonly/rpc/receipt.go 76.92% 8 Missing and 7 partials ⚠️
sei-tendermint/internal/evmonlyapp/app.go 76.47% 10 Missing and 2 partials ⚠️
giga/evmonly/cmd/evmonly-loadtest/pipeline.go 56.52% 5 Missing and 5 partials ⚠️
sei-tendermint/node/public.go 83.78% 4 Missing and 2 partials ⚠️
giga/evmonly/giga_store.go 84.00% 2 Missing and 2 partials ⚠️
... and 4 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4088      +/-   ##
==========================================
- Coverage   61.24%   60.28%   -0.97%     
==========================================
  Files        2190     2079     -111     
  Lines      191985   179206   -12779     
==========================================
- Hits       117587   108032    -9555     
+ Misses      62978    60821    -2157     
+ Partials    11420    10353    -1067     
Flag Coverage Δ
sei-chain-pr 69.57% <65.89%> (?)
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 Δ
cmd/autobahn-e2e/aws.go 53.01% <100.00%> (ø)
giga/evmonly/executor.go 86.89% <100.00%> (-0.05%) ⬇️
giga/evmonly/memory_store.go 80.45% <ø> (ø)
sei-tendermint/config/autobahn.go 50.00% <ø> (ø)
sei-tendermint/config/config.go 83.67% <ø> (ø)
sei-tendermint/config/toml.go 57.62% <ø> (ø)
sei-tendermint/node/seed.go 52.13% <ø> (ø)
cmd/autobahn-e2e/deploy.go 4.34% <0.00%> (ø)
giga/evmonly/receipt.go 94.59% <94.59%> (ø)
giga/evmonly/storage_manager.go 90.00% <90.00%> (ø)
... and 11 more

... and 117 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 persistence ordering (receipts then state), FlatKV encoding, and Autobahn EVM-only node wiring; scoped to test/load paths but touches shared storage and RPC surfaces.

Overview
Autobahn EVM-only load testing moves from the ephemeral in-memory path to disk-backed Giga storage, and the toggle is renamed from AUTOBAHN_EVMONLY_IN_MEMORY / evm-only-in-memory to AUTOBAHN_EVMONLY / evm-only across Makefile, Docker, and autobahn-e2e.

The EVM-only executor now requires both a state store and a ReceiptStore: receipts are written before the height-advancing FlatKV commit, with a new FlatKV changeset encoder (including storage-prefix clears) and WithMissingAccountState for synthetic funded accounts not yet in FlatKV. WithStorageManager wires the shared GigaStorageManager (FlatKV, littidx receipts, littblock; GigaSS off). Unit tests keep MemoryStore / MemoryReceiptStore.

evmonly-loadtest commits generated genesis at height 1 and runs measured blocks through a temp manager; the Autobahn ABCI app lives under evmonlyapp and uses the same stack. Tendermint opens manager-owned storage for EVM-only nodes and reuses its block store in the Giga router. Minimal JSON-RPC on 8545 adds eth_getTransactionReceipt (receipt store required); integration tests assert receipts after load.

Reviewed by Cursor Bugbot for commit 8dab03b. 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 Outdated
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.

Comment thread giga/evmonly/receipt_store.go Outdated
Comment thread sei-db/bootstrap/storage_manager.go Outdated
Comment thread giga/evmonly/storage_manager.go
Comment thread sei-tendermint/internal/evmonlyapp/app.go Outdated
@codchen
codchen marked this pull request as draft September 7, 2026 04:46
@codchen codchen changed the title feat(evmonly): persist executor receipts feat(evmonly): run load tests on Giga storage Sep 7, 2026

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 93687db. Configure here.

Comment thread sei-tendermint/internal/evmonlyapp/app.go
@codchen
codchen marked this pull request as ready for review September 7, 2026 07:21
@codchen

codchen commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

4 similar comments
@codchen

codchen commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@seidroid review

@masih

masih commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@seidroid review

@alexander-sei

Copy link
Copy Markdown
Contributor

@seidroid review

@masih

masih commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

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

Solid follow-up: the EVM-only executor now persists non-balance state through FlatKV and receipts through the manager-owned littidx store, MemoryStore/MemoryReceiptStore are demoted to unit-test doubles, and the NewGigaStorageManagerWithStores test-shaped constructor is gone (the PR has no net sei-db diff). No blockers; the remaining items are restart semantics of the now-persistent EVM-only app, a fail-closed gap when a FlatKV state store is used without a balance store, and two convention/dependency points.

Findings: 0 blocking | 6 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 saying so. It is safe for the two backends in play — littReceiptStore.SetReceipts never touches the multistore and its GetReceipt fallback guards on a nil storeKey — but the executor accepts any receipt.ReceiptStore, 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.
  • [suggestion] giga/evmonly/cmd/evmonly-loadtest/pipeline.go commits generated genesis at height 1, but state.changeSet() emits only nonce/code/storage pairs, so a pure-transfer run with all-zero nonces produces nil changesets and the genesis commit is an empty block. That works (FlatKV accepts an empty contiguous block), but neither flatkv_changeset_test.go nor the loadtest tests cover the empty-changeset encode → commit path, which is the default workload's shape.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

func evmOnlyInMemoryGasLimit(req *abci.RequestInitChain) (uint64, error) {
func (a *evmOnlyApplication) seedInitialStateVersion(initialHeight int64) error {
stateStore := a.storage.SC()
if stateStore == nil || initialHeight == 1 {

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 initialHeight == 1 short-circuit skips the guard in exactly the case where it is needed. State, receipts and blocks now live in data/autobahn (step4 no longer passes --persistent-state-dir=), so that directory survives a container/process restart, while evmOnlyStatecommittedHeight, appHash, parentHash — and the whole PlaceholderBalanceStore are rebuilt empty by NewEVMOnlyApplication. On a restart of a genesis-height-1 chain, Info() reports height 0, InitChain runs again, this function returns early without noticing that SC().GetLatestVersion() is already N, and the first FinalizeBlock fails at CommitStateChanges(1, ...) because FlatKV requires contiguous versions (store_apply.go:45). Codex flags the same path.

It fails closed rather than diverging, but the diagnostic lands two layers away from the cause. Dropping the initialHeight == 1 clause makes the existing "EVM-only state is already at height %d before InitChain" error fire for the common case too (a fresh store returns 0, so height 1 still passes), and SetInitialVersion is already equivalent to a no-op there. A close/reopen test would pin whichever behaviour you choose — refuse to start on a populated directory, or recover committedHeight/appHash from 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.

step4 no longer passes --persistent-state-dir=, so data/autobahn survives a restart, while committedHeight, appHash and the whole PlaceholderBalanceStore are rebuilt empty by NewEVMOnlyApplication. On restart, storage recovers itself correctly and comes back on height N; the app reports height 0, InitChain reruns, seedInitialStateVersion returns early, and FinalizeBlock(1) executes. executePreparedBlockWithStore writes receipts before the height-advancing state commit.

Block 1 now holds a receipt from an execution that was never committed, produced against a fresh zero-balance placeholder.

Comment thread giga/evmonly/giga_store.go Outdated
}

func (r gigaSnapshotStateReader) GetBalance(addr common.Address) *big.Int {
if r.balances != 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] The balance store is optional here, but for the FlatKV-backed state store it is mandatory: flatKVStateView.GetBalance panics (sei-db/state_db/sc/flatkv/state_view.go:107), and encodeFlatKVChangeSet silently drops changes.Balances. So WithStorageManager(manager, NewFlatKVChangeSetEncoder(...)) without a matching WithBalanceStore panics inside the state view on the first balance read, rather than failing with an executor-level error like the sibling errMissingStateStore / errMissingReceiptStore checks.

Both production call sites pass WithBalanceStore today, so this is a latent misconfiguration rather than a live bug — but it is a guard that belongs at the choke point. Either have executePreparedBlockWithStore reject a nil balanceStore when the state view cannot serve balances, or bundle the encoder and the placeholder store into one option so they cannot be supplied apart.

return giga, ownedBlockStore, nil
}

func selectAutobahnBlockStore(

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 second return value uses a bare nil as "not owned by the caller", which buildGigaRouter propagates and createRouter then tests with if blockStore != nil, plus a closeAutobahnBlockStore helper that exists only to nil-check. sei-tendermint/AGENTS.md asks for utils.Option for optional values and says not to add defensive nil-checks in internal logic — and this file already uses utils.Option[atypes.BlockStore] for exactly this value on the way in (managed) and on the way out of createRouter.

Returning utils.Option[atypes.BlockStore] for the owned store would make ownership explicit at every hop and let closeAutobahnBlockStore collapse into if store, ok := owned.Get(); ok.


// 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, with the case now narrower: WithStore and WithReceiptStore are exported here, so WithStorageManager is a two-line convenience wrapper — and it is the only reason giga/evmonly imports sei-db/bootstrap, pulling littblock, flatkv, controller, the checkpoint scheduler and autobahn/blockstore into the package (and into the standalone evmonly-loadtest binary).

Both production call sites already have the manager in hand and could call WithStore(manager.StateDB(), encoder) plus WithReceiptStore(manager.ReceiptDB()) directly, or pass a locally-declared two-method interface. That drops the import and removes the if manager != nil branch, which currently swallows a nil manager into a much later errMissingStateStore.

if err != nil {
return fmt.Errorf("configure storage manager: %w", err)
}
storage, err := bootstrap.NewGigaStorageManager(ctx, storageConfig.WithFullNodeMode())

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 should start with ValidatorMode since GigaSS hasn't really been wired up yet

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. EVM-only now starts from WithValidatorMode() so GigaSS stays disabled. Because validator mode also disables ReceiptDB, the shared EVM-only validator config explicitly re-enables it so executor receipts remain disk-backed.

@codchen
codchen force-pushed the codex/evmonly-receipt-store branch from b930957 to 547ee42 Compare September 9, 2026 04:04
@codchen
codchen force-pushed the codex/evmonly-receipt-store branch from 9c13e70 to 8dab03b Compare September 10, 2026 03:10
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.

4 participants