Skip to content

Integrate evmonly executor with giga store - #3864

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

Integrate evmonly executor with giga store#3864
codchen wants to merge 3 commits into
mainfrom
codex/integrate-evmonly-giga-store

Conversation

@codchen

@codchen codchen commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • make the evmonly executor store-only: every block opens a giga snapshot and commits through CommitStateChanges
  • require a store-specific NamedChangeSetEncoder and preserve storage-prefix clears
  • add the loadtest MemoryStore implementation over the existing immutable StateReader
  • encode state changes directly into typed, fixed-width NamedChangeSet key/value pairs with contiguous backing allocations
  • apply direct pairs without an RLP encode/decode round trip
  • retain historical state through per-block version slabs and linked version nodes, avoiding independently growing slices and duplicate account-touch indexes
  • keep result sinks post-commit and preserve cleanup/error behavior across sequential and OCC execution

Why

The evmonly executor now has one persistence model: a giga Store. The concrete store implementation can vary, but execution no longer has a separate non-giga state path.

The first loadtest adapter wrapped the complete native changeset in RLP and decoded it immediately inside CommitStateChanges. The direct format removes that redundant work while continuing to exercise the real giga interface.

Loadtest

Configuration: 400 blocks, 1,000 transactions/block, one ordered block worker, 12 executor workers, zero gas price, and discard result sink. Values are three-run medians in tx/s.

Workload Pre-MemoryStore Giga MemoryStore Difference OCC reruns/block
Transfer, unique 254,684 227,601 -10.6% 0
Transfer, 10% recipient conflicts 224,630 198,971 -11.4% 50
Transfer, hot recipient 74,961 71,220 -5.0% 999
Transfer, same sender 84,601 81,371 -3.8% 999
ERC20 transfer, unique 194,834 175,273 -10.0% 0
ERC20 transfer, 10% recipient conflicts 163,868 146,445 -10.6% 50
ERC20 transfer, hot recipient 34,256 32,986 -3.7% 999
Snapshot/revert 262,522 233,799 -10.9% 0

Every run completed 400,000/400,000 transactions successfully with zero execution errors and zero OCC fallbacks.

The pre-MemoryStore comparison is not an equivalent persistence implementation: it executes against WithState and discards block state changes, while the Giga run encodes, commits, and retains current and historical state for later snapshots. It is therefore a useful lower bound on commit overhead, not evidence that the Giga interface itself costs 10% in production.

In an 800-block snapshot/revert profile, EncodeMemoryStoreChangeSet and CommitStateChanges each represented about 0.1% of sampled CPU. Most MemoryStore-specific allocation was the retained versioned storage map. A pointer-free indexed-history experiment did not improve end-to-end throughput and was reverted.

Validation

  • go test ./giga/... ./sei-db/state_db/giga
  • go test -race ./giga/evmonly/...
  • go vet ./giga/evmonly/... ./sei-db/state_db/giga
  • touched Go files pass gofmt -s -l and goimports -l
  • git diff --check

The full-tree goimports -l . reports pre-existing untouched generated and test files.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 27, 2026, 2:12 AM

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.91968% with 100 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.03%. Comparing base (d13df57) to head (819a564).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
giga/evmonly/memory_store.go 81.29% 44 Missing and 31 partials ⚠️
giga/evmonly/cmd/evmonly-loadtest/state.go 0.00% 20 Missing ⚠️
giga/evmonly/giga_store.go 90.47% 2 Missing and 2 partials ⚠️
giga/evmonly/occ.go 87.50% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3864      +/-   ##
==========================================
- Coverage   59.07%   58.03%   -1.05%     
==========================================
  Files        2305     2208      -97     
  Lines      196778   185680   -11098     
==========================================
- Hits       116249   107761    -8488     
+ Misses      69790    68086    -1704     
+ Partials    10739     9833     -906     
Flag Coverage Δ
sei-chain-pr 79.49% <79.91%> (?)
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/config.go 67.14% <100.00%> (+0.32%) ⬆️
giga/evmonly/cmd/evmonly-loadtest/pipeline.go 71.91% <100.00%> (+0.53%) ⬆️
giga/evmonly/cmd/evmonly-loadtest/sinks.go 75.71% <100.00%> (-0.23%) ⬇️
giga/evmonly/executor.go 87.68% <100.00%> (-0.05%) ⬇️
giga/evmonly/state.go 89.00% <100.00%> (+0.57%) ⬆️
giga/evmonly/types.go 81.81% <ø> (ø)
giga/evmonly/occ.go 81.72% <87.50%> (ø)
giga/evmonly/giga_store.go 90.47% <90.47%> (ø)
giga/evmonly/cmd/evmonly-loadtest/state.go 70.10% <0.00%> (-18.21%) ⬇️
giga/evmonly/memory_store.go 81.29% <81.29%> (ø)

... and 99 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 force-pushed the codex/integrate-evmonly-giga-store branch from a80537b to 66a304c Compare August 6, 2026 09:19
@codchen
codchen force-pushed the codex/integrate-evmonly-giga-store branch from 1a66532 to 819a564 Compare August 27, 2026 02:10
@codchen
codchen marked this pull request as ready for review August 27, 2026 02:11
@cursor

cursor Bot commented Aug 27, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes core block execution and state persistence: every block now commits through giga, with ordered commits and encoder semantics (especially storage prefix clears) affecting correctness if misconfigured.

Overview
Makes the evmonly executor giga-store-only: WithState and caller-owned StateWriter persistence are removed in favor of WithStore(giga.StateDB, NamedChangeSetEncoder). Each block opens a snapshot, runs sequential or OCC execution against it, encodes StateChangeSet to protobuf NamedChangeSets, and calls CommitStateChanges under a mutex so snapshots and commits do not overlap. Missing store/encoder, encode/commit failures, and canceled contexts fail closed without invoking ResultSink; sinks run only after a successful commit.

Adds MemoryStore, a test/load harness giga.StateDB over an immutable StateReader: direct typed KV encoding (including StorageClears), versioned in-memory overlays for current and historical snapshots, and EncodeMemoryStoreChangeSet without an RLP round trip.

Updates evmonly-loadtest to wire MemoryStore, drops discard-sink state application, and requires --workers=1 because each block depends on the prior commit’s snapshot. Docs and StateSnapshot note concurrent-read safety for OCC.

Reviewed by Cursor Bugbot for commit 819a564. 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 making the evmonly executor store-only over the giga StateDB interface; the encoder slab arithmetic, storage-clear versioning, commit validation ordering, and error/release paths all check out. One suggestion: MemoryStore's touch-based AccountExists diverges from the account-existence semantics sei-db/state_db/giga/api.go documents.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] --workers in the loadtest now rejects every value except its default of 1 (cmd/evmonly-loadtest/config.go), leaving a CLI flag with a single legal value. Either drop the flag until the harness can order commits across workers, or keep it and have the harness serialize CommitStateChanges itself so the knob stays meaningful.
  • 1 suggestion(s)/nit(s) flagged inline on specific lines.

_, codeTouched := latestMemoryStoreValue(s.store.code[addr], s.height)
firstStorageTouch, storageTouched := s.store.storageTouch[addr]
s.store.mu.RUnlock()
if balanceTouched || nonceTouched || codeTouched || storageTouched && firstStorageTouch <= s.height {

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] AccountExists is touch-based rather than value-based: once any balance/nonce/code node or storage touch is recorded for an address, it reports true at every height from that point on. Two consequences diverge from the contract in sei-db/state_db/giga/api.go:

  • A block that self-destructs a contract emits BalanceChange{Balance: 0}, a code delete, and a StorageClear. All three create nodes, and touchStorageAccountLocked records the clear as a storage touch, so the destroyed account reports AccountExists() == true forever — not just "in the current block" as the interface doc allows. GetCodeHash (line 451) then returns keccak256("") where the doc specifies the zero hash for non-existent accounts, i.e. the opposite of EXTCODEHASH / keeper.GetCodeHash behaviour.
  • TestMemoryStoreTracksZeroValueAndStorageOnlyAccounts pins the same shape for an account whose only write is Balance: 0, which a real SC/SS store would not materialize as an existing account.

Nothing consumes this today — gigaSnapshotStateReader only implements GetBalance/GetNonce/GetCode/GetState, and nativeStateDB.Exist derives existence itself — so this is not a correctness bug in the executor path. But MemoryStore is the first and only implementation of gigastore.StateDB and is positioned as the test double for it, so tests written against these semantics will pass here and fail against the production store. Consider resolving existence from the effective values at s.height (non-zero balance, non-zero nonce, non-empty code, or a live storage slot) instead of from touch records, and dropping storageTouch in favour of a height-versioned marker — or, if the touch-based approximation is deliberate, say so in the MemoryStore godoc and note the GetCodeHash deviation.

Comment thread giga/evmonly/executor.go
}

func (e *Executor) executePreparedBlock(ctx context.Context, req PreparedBlock) (*BlockResult, error) {
func (e *Executor) executePreparedBlock(ctx context.Context, req PreparedBlock, source StateReader) (*BlockResult, error) {

@yzang2019 yzang2019 Aug 27, 2026

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.

Is source properly closed after the execution is done? The way how StateSnapshot works require the underlying goroutine to close the reader after usage is done

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.

Yes. source is only an adapter around the snapshot opened in executePreparedBlockWithStore, and that function registers defer snapshot.Close() immediately after the nil check. Sequential execution is synchronous; on the OCC path, occWorkerPool.Run returns only after errgroup.Wait() has joined every worker. Each worker also releases its nativeStateDB and resets its source before executePreparedBlock returns. Therefore the opening goroutine closes the snapshot only after all executor goroutines have stopped using it, including error paths, and before ExecutePreparedBlock proceeds to the result sink. TestExecutorGigaStoreSnapshotFeedsOCCExecution covers closure on the OCC path.

@@ -45,6 +45,8 @@ type StateDB interface {
//
// Until Close, the underlying resources (e.g. an ephemeral SC snapshot or a
// pinned SS version) stay alive, even concurrently with later writes/commits.
// All read methods must be safe for concurrent calls because EVM executor
// workers may share one snapshot while executing a block.

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 will rename this to StateView in the future PR, just FYI, no action needed for now

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.

2 participants