Skip to content

Integrate evmonly executor with giga store - #3864

Merged
codchen merged 3 commits into
mainfrom
codex/integrate-evmonly-giga-store
Aug 31, 2026
Merged

codchen merged 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 31, 2026, 3:40 AM

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.95992% with 100 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.27%. Comparing base (a85dc9e) to head (83f8025).

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.69% 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   61.28%   60.27%   -1.02%     
==========================================
  Files        2155     2055     -100     
  Lines      188513   176894   -11619     
==========================================
- Hits       115526   106616    -8910     
+ Misses      62192    60439    -1753     
+ Partials    10795     9839     -956     
Flag Coverage Δ
sei-chain-pr 79.55% <79.95%> (?)
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%> (+1.68%) ⬆️
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.69% <90.69%> (ø)
giga/evmonly/cmd/evmonly-loadtest/state.go 70.10% <0.00%> (-18.21%) ⬇️
giga/evmonly/memory_store.go 81.29% <81.29%> (ø)

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

Medium Risk
Changes core execution persistence (mandatory giga commits, ordered blocks, encoder contract for storage clears); mistakes in production encoders or out-of-order callers could corrupt or reject state, though tests and fail-closed validation mitigate this.

Overview
The evmonly executor is now store-backed only: WithState and caller-owned StateWriter persistence are removed in favor of WithStore(giga.StateDB, NamedChangeSetEncoder). Each block opens a StateView, runs EVM/OCC against that snapshot, encodes StateChangeSet to protobuf NamedChangeSets, and calls CommitStateChanges under a mutex so snapshots and commits cannot overlap. Empty blocks still commit an empty changeset; encode/commit failures release the block result and skip ResultSink, which runs only after a successful commit.

A new in-memory MemoryStore implements giga.StateDB for tests and evmonly-loadtest: genesis comes from an immutable StateReader, commits use a direct typed KV format (including storage prefix clears), and versioned overlays keep current and historical views stable without cloning full base state.

Load harness and API tightening: the loadtest wires MemoryStore end-to-end, drops discard-path changeset application, and requires --workers=1 because block height must follow commit order. Tests use withTestState / withGeneratedState helpers; OCC paths take an explicit StateReader from the open snapshot instead of executor-held state.

Reviewed by Cursor Bugbot for commit 83f8025. 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.

Comment thread sei-db/state_db/giga/api.go Outdated
@@ -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

@codchen
codchen force-pushed the codex/integrate-evmonly-giga-store branch from 819a564 to 83f8025 Compare August 31, 2026 03:38

@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 83f8025. Configure here.

}
s.mu.RUnlock()
return &memoryStoreSnapshot{store: s, height: 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.

Genesis snapshot collides with block 0

Medium Severity

OpenView pins uncommitted genesis at height 0, and CommitStateChanges also accepts block 0. A snapshot opened before that commit then sees the new nodes, because latestMemoryStoreValue treats height-0 versions as visible. That breaks the StateView rule that an open view never observes later writes.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 83f8025. Configure here.

@codchen
codchen added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 95f6bfb Aug 31, 2026
150 of 154 checks passed
@codchen
codchen deleted the codex/integrate-evmonly-giga-store branch August 31, 2026 05:09
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 13, 2026
## Describe your changes and provide context

This PR builds on the EVM-only Giga store merged in sei-protocol#3864 and is rebased
onto that merge commit.

- Add a test-only top-level `evm-only-in-memory` node setting that
replaces the Cosmos ABCI application before `proxy.New`, so Autobahn and
the executor share the same application.
- Initialize the EVM-only executor from `InitChain`, accept protected
raw Ethereum transactions on chain ID 1337, and execute finalized blocks
with a deterministic app hash.
- Add a minimal EVM JSON-RPC listener on port 8545 with only
`eth_sendRawTransaction`. It validates Ethereum encoding, routes by
Autobahn EVM shard ownership, submits through the in-process mempool,
and returns the Ethereum transaction hash.
- Do not start Tendermint RPC for EVM-only nodes. The Docker harness
also disables the unused Cosmos REST, gRPC, and gRPC-web servers.
- Keep executor and Autobahn consensus state in memory; Docker disables
Autobahn persistence with `--persistent-state-dir=`.
- Add `make autobahn-evmonly-integration-test`, which submits 4,000
signed transfers through EVM JSON-RPC, asserts Tendermint RPC is
unavailable, and uses internal Prometheus execution counters to wait for
every validator.
- Add the `autobahn-e2e` cluster manager with `deploy`, `list`,
`forward`, and `teardown` subcommands for local Docker or a managed EC2
host. `list` obtains height from internal Prometheus metrics and does
not depend on Tendermint RPC.
- Keep the runtime deliberately ephemeral and load-test-only.
Persistence, restart recovery, state sync, receipts, additional EVM RPC
methods, and staking or other custom precompiles are out of scope.

## Testing performed to validate your change

- `make autobahn-evmonly-integration-test` (4,000 raw transfers through
EVM JSON-RPC; all four validators executed through height 8; 5,010 tx/s
observed locally; Tendermint RPC unavailable)
- Real `autobahn-e2e` local lifecycle: deploy four nodes, list live
status/Prometheus heights, forward node 2 to `127.0.0.1:18545`, call the
EVM RPC through the tunnel, confirm Tendermint `/status` resets, then
teardown all containers and the network
- `go test -count=1 ./cmd/autobahn-e2e`
- `go test -count=1 -run "^TestEVMOnlyRPC" ./sei-tendermint/node`
- `go test -c -tags autobahn_integration ./integration_test/autobahn`
- `go vet ./sei-tendermint/node ./cmd/autobahn-e2e`
- `go vet -tags autobahn_integration ./integration_test/autobahn`
- golangci-lint v2.8.0 on the changed packages with `--build-tags
autobahn_integration` (0 issues)
- golangci-lint v2.8.0 `fmt --diff` across the repository
- `sh -n docker/localnode/scripts/step4_config_override.sh
docker/localnode/scripts/step5_start_sei.sh`
- `git diff --check`
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