Skip to content

evmonlyapp: land block state commits behind FinalizeBlock - #4270

Closed
bdchatham wants to merge 1 commit into
giga-1from
devin/1789943071-stack-1-commit-behind-finalize
Closed

bdchatham wants to merge 1 commit into
giga-1from
devin/1789943071-stack-1-commit-behind-finalize

Conversation

@bdchatham

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Re-opens #4254 on top of the revert in #4269 so it can be reviewed on its own. Bottom of a four-PR stack (#4254 → #4258 → #4260 → #4261); the top of the stack is byte-identical to today's giga-1.

FinalizeBlock stops waiting for the block's FlatKV commit. It calls executor.PrepareBlock + ExecutePreparedBlock (timed as the prepare and execute phases of evmonly_finalize) and returns once the commit has been started; the executor lands block N's commit while N+1 executes, reading N's changes through an overlay in the meantime.

Anything that must observe committed state settles first through the new settler (AwaitCommits): EvmCall, Query, RPC views (openSettledView), and node.closeGigaStorage before the stores are closed. A failed background commit is latched by the executor and surfaces on the next FinalizeBlock (halting the node) and to committed-state readers; abandonPending takes the executor so it can settle before dropping the cursor.

FinalizeBlock(N):  beginBlock → PrepareBlock → ExecutePreparedBlock → stage cursor → return
                                                            └── commit(N) runs in background
FinalizeBlock(N+1): executor awaits commit(N) before starting commit(N+1)
Query/EvmCall/RPC: settler.AwaitCommits() → open view

Measured on giga-testnet-2 as the first step of ~72k → ~96k tx/s.

Testing performed to validate your change

  • go test -race ./sei-tendermint/internal/evmonlyapp/... ./sei-tendermint/node/... (includes the failed-background-commit surfacing and commit-lands-behind-Finalize tests).
  • make fmtcheck, make lint.

Link to Devin session: https://app.devin.ai/sessions/ff612badcded4aa5914ea408dbb41888
Open in Devin Desktop: https://app.devin.ai/desktop/session/ff612badcded4aa5914ea408dbb41888?variant=devin
Requested by: @bdchatham

@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@cursor

cursor Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

PR Summary

High Risk
Changes core block execution and commit ordering for EVM-only/Autobahn; incorrect settling or error handling could serve stale state, lose durability on shutdown, or mask failed commits until later blocks.

Overview
FinalizeBlock no longer blocks on FlatKV state commit. It runs PrepareBlock then ExecutePreparedBlock (new prepare OTel phase alongside execute) and returns once the commit is started, so block N’s commit can finish while N+1 executes.

A settler publishes the executor for paths that need durable store state: AwaitCommits on the app (and proxy.AwaitCommits), openSettledView for EvmNonce / EvmBalance, a retry loop in EvmCall that settles then re-checks block context, and closeGigaStorage before closing Giga storage. Failed background commits are latched and surface on the next FinalizeBlock, EvmCall, and AwaitCommits; abandonPending settles first. Commit still only advances the cursor (latest height for RPC) without waiting on the store write.

Tests add closeEVMOnlyTestApp / AwaitCommits on teardown and cover reads after finalize-before-commit and failed-commit behavior.

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

@devin-ai-integration
devin-ai-integration Bot added this pull request to stack #4274 September 20, 2026 22:34
@github-actions

github-actions Bot commented Sep 20, 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 20, 2026, 11:20 PM

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

Comment thread sei-tendermint/internal/evmonlyapp/app.go
@codecov

codecov Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.56250% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.42%. Comparing base (7e3ca24) to head (4fefcc7).
⚠️ Report is 50 commits behind head on giga-1.

Files with missing lines Patch % Lines
sei-tendermint/internal/evmonlyapp/app.go 85.96% 8 Missing ⚠️
sei-tendermint/internal/proxy/proxy.go 0.00% 5 Missing ⚠️
sei-tendermint/node/node.go 0.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           giga-1    #4270       +/-   ##
===========================================
+ Coverage   65.55%   75.42%    +9.86%     
===========================================
  Files        2081       10     -2071     
  Lines      157460     1778   -155682     
===========================================
- Hits       103222     1341   -101881     
+ Misses      54097      436    -53661     
+ Partials      141        1      -140     
Flag Coverage Δ
sei-chain ?
sei-chain-pr 75.42% <76.56%> (?)
sei-db ?

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

Files with missing lines Coverage Δ
sei-tendermint/node/node.go 73.13% <0.00%> (-1.60%) ⬇️
sei-tendermint/internal/proxy/proxy.go 83.11% <0.00%> (-8.00%) ⬇️
sei-tendermint/internal/evmonlyapp/app.go 86.14% <85.96%> (+4.37%) ⬆️

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

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

Moving the block state commit behind FinalizeBlock is implemented carefully: the settler covers every committed-state reader, changeset encoding and the pipeline overlay are alias-free, lock order is preserved, and shutdown settles before closing the stores. Two non-blocking notes concern the new EvmCall settle-and-recheck loop and the swallowed commit failure in openSettledView.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] The new coverage is valuable but entirely sequential. TestEVMOnlyApplicationReadsSettleBehindFinalizeBlock finalizes, then reads, on one goroutine, so it never exercises the concurrency the PR introduces — a reader calling AwaitCommits/openSettledView while FinalizeBlock is mid-execution and retiring the previous commit. A -race test that drives EvmNonce/EvmCall from a goroutine while blocks are finalized would pin the interaction between awaitPipelineCommit retiring pipelineChanges and executePreparedBlockWithStore reading it before opening its view.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.
  • 1 non-blocking pre-existing issue(s) listed below under pre-existing issues.

Pre-existing issues

  • [suggestion] EvmCall (sei-tendermint/internal/evmonlyapp/app.go) releases the cursor lock before executor.Call opens its own store view, so a block can commit in between and pair block N's block context with block N+1's state. This predates the PR; the new settle-and-recheck loop narrows the window but does not close it.

if err := executor.AwaitCommits(); err != nil {
return nil, err
}
settled, err := a.callBlockContext()

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] Two things about the recheck.

First, the comment above claims that after the recheck "the store holds the advertised block and no later one has been committed to the cursor." That only holds at the instant of this call: executor.Call opens its own view (giga/evmonly/call.go:33) after this returns, so a block committed between here and that OpenView is still visible to the call while blockCtx advertises the older NUMBER/TIMESTAMP/BLOCKHASH. The loop genuinely guarantees the store is at least at blockCtx.Number, which is the useful part — worth stating that instead, so a later reader does not take the stronger guarantee as given.

Second, this second callBlockContext() can return the "EVM-only call attempted before committing the finalized block" error for a state that is transient by construction. Concretely: an eth_call arrives with cursor at N and nothing pending, so the first callBlockContext succeeds; AwaitCommits blocks while commit(N) lands; during that wait FinalizeBlock(N+1) returns and stages its cursor; the recheck now sees pending and fails the call. Before this change the pending check ran once at an instant, so the same call would have succeeded. Since this is already a retry loop, a staged-but-uncommitted cursor is arguably a retry condition rather than a failure — though a plain continue would busy-spin here (AwaitCommits returns immediately once settled), so it needs a backoff or a bound rather than just falling through.

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.

Deliberately not changed here: this stack re-opens the already-merged code byte-for-byte so the team can review what is actually running on giga-1 (the top of the stack equals current giga-1). Agreed this is a real improvement; tracking it as a follow-up to land on top once the stack has been reviewed, unless the reviewers prefer it folded in.

// consistent version, and the failure halts the node through the next
// FinalizeBlock.
func (a *evmOnlyApplication) openSettledView() gigatypes.StateView {
if err := a.AwaitCommits(); err != nil && !a.settleFailureLogged.Swap(true) {

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] Swallowing the settle error means committed-state readers silently serve state from before the failed block. TestEVMOnlyApplicationSurfacesAFailedCommitFromTheNextBlock codifies the reachable state: commit(1) fails, Commit still advances the cursor, so LastBlockHeight() reports 1 while GetLatestVersion() is 0. In that window eth_getBalance/eth_getTransactionCount at "latest" answer from genesis state with no error to the caller and one log line for the process lifetime, while eth_blockNumber reports 1 — and EvmCall on the same app does return the error, so the RPC surface is internally inconsistent. Pre-change a failed commit failed FinalizeBlock, so the cursor could never advance past it.

The halt-on-next-FinalizeBlock argument bounds this for a producing node, but the doc comment's "the failure halts the node through the next FinalizeBlock" is doing a lot of work for a node that is already halting and still serving RPC. Consider surfacing it to the reader (as EvmCall does) rather than only logging.

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.

Deliberately not changed here: this stack re-opens the already-merged code byte-for-byte so the team can review what is actually running on giga-1 (the top of the stack equals current giga-1). Agreed this is a real improvement; tracking it as a follow-up to land on top once the stack has been reviewed, unless the reviewers prefer it folded in.

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789943071-stack-1-commit-behind-finalize branch from 46d3fc2 to aec5c3b Compare September 20, 2026 22:50
@bdchatham bdchatham changed the title [stack 1/4] evmonlyapp: land block state commits behind FinalizeBlock evmonlyapp: land block state commits behind FinalizeBlock Sep 20, 2026
An error occurred while trying to automatically change base from devin/1789943071-revert-exec-opts to giga-1 September 20, 2026 23:17
@devin-ai-integration
devin-ai-integration Bot removed this pull request from stack #4274 September 20, 2026 23:18
@devin-ai-integration
devin-ai-integration Bot changed the base branch from devin/1789943071-revert-exec-opts to giga-1 September 20, 2026 23:18
…lock (pipelined executor path) (#4254)""

This reverts commit c0c4c4e.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789943071-stack-1-commit-behind-finalize branch from aec5c3b to 4fefcc7 Compare September 20, 2026 23:18
@devin-ai-integration
devin-ai-integration Bot added this pull request to stack #4275 September 20, 2026 23:18
@masih
masih marked this pull request as draft September 25, 2026 21:07
@masih

masih commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

@bdchatham needs to be based on main

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Superseded by #4366, which is based on main (PLT-1312). Main already has the executor-side pipeline from #4317, so #4366 only ports the application layer onto main's evmOnlyState. Relative to this PR it changes two things:

  • EvmCall / EvmEstimateGas settle while holding state. FinalizeBlock can't stage a block during the wait, so the transient refusal the review flagged can't happen, and a commit failure comes back as an error.
  • A -race test drives EvmNonce / EvmCall from a goroutine while blocks are finalized.

Closing this PR so nothing targets giga-1.

revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Sep 29, 2026
…tocol#4366)

The EVM-only executor on main can already pipeline commits
(`PrepareBlock`, `ExecutePreparedBlock`, `AwaitCommits`, from sei-protocol#4317),
but `evmOnlyApplication.FinalizeBlock` still calls the synchronous
`ExecuteBlock`. That call waits for the FlatKV commit of block N before
it returns, so the node can't start N+1 until the write lands. This PR
brings the application-layer half of sei-protocol#4270 over from `giga-1`
(PLT-1312), adapted to main's `evmOnlyState` / sender-cache layout
instead of the older cursor/executor split that sei-protocol#4270 assumes.

`FinalizeBlock` now runs `PrepareBlock` and then `ExecutePreparedBlock`
(in `executeBlockPipelined`), and returns once the state commit has
started. The next block still waits on the previous commit inside the
executor before it starts its own. Committed-state readers settle the
in-flight commit first. `EvmNonce`, `EvmBalance` and `EvmCode` go
through `openSettledView`, which calls `AwaitCommits` on the executor
published through a lock-free `settler`, so they never block on
`FinalizeBlock`. `currentExecutionContext`, used by `EvmCall` and
`EvmEstimateGas`, awaits while it holds `state`, so no new block can
start between the settle and the snapshot, and it returns a failed
commit as an error. `openSettledView` logs a failed commit once and
serves the last version that landed. Its return values have no error
channel, and the failure halts the node through the next
`FinalizeBlock`. `Proxy.AwaitCommits` exposes the settle, and
`nodeImpl.closeGigaStorage` calls it before `manager.Close()` so
shutdown never closes the store under an in-flight write.

Consensus and app hash are unchanged: the app hash is still computed
from the execution result, not the store write. The behaviour change is
that a failed store commit now surfaces one block later, from the next
`FinalizeBlock`, rather than from its own. Between `FinalizeBlock` and
`Commit`, `EvmNonce` and friends already see the finalized block's
state; before this PR they could not, because the write had already
landed by then anyway. New tests cover reads settling across four
pipelined blocks and a commit failure surfacing from the next block,
from `EvmCall` and from `AwaitCommits`, and a `-race` test that drives
`EvmNonce`/`EvmCall` from a goroutine while blocks are finalized and
committed, asserting the nonce never goes backwards. The test helpers
now settle before closing storage. I validated with `scripts/ramtest.sh
-race` over `evmonlyapp`, `proxy`, `node` and `giga/evmonly`, plus `make
fmtcheck` and `make lint`.

Link to Devin session:
https://app.devin.ai/sessions/d808db90106341278344623de10b92c0
Open in Devin Desktop:
https://app.devin.ai/desktop/session/d808db90106341278344623de10b92c0?variant=devin
Requested by: @bdchatham

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
revofusion pushed a commit to revofusion/sei-chain that referenced this pull request Oct 6, 2026
… acceptance barrier (sei-protocol#4457)

Backport: this ports sei-protocol#4273, the reviewable version of sei-protocol#4261, to main
from the giga-1 PR stack sei-protocol#4270–sei-protocol#4273. The stack targeted stacked giga-1
branches and never reached main; only its first PR landed on main, as
sei-protocol#4366. @shemnon approved sei-protocol#4273 at 1143167. Since that commit, the
changes are main's renames (`baseAccount`, the `phaseOCC*` constants,
`takeAccessSets`) and two review fixes: merge fragments are cleared
before they return to the pool, and each parallel pass's look-ahead is
bounded.

On giga-testnet-2, `occ_validate` and `occ_merge` took about a third of
the executor main loop, all of it on the calling goroutine. Most of that
time went to transactions that the frontier accepts as they stand. A
block of about 1,800 transactions holds less than one conflict. On
`giga-1`, sei-protocol#4261 moved this work onto the OCC pool. It cut validate and
merge from about 8.5 ms to 4.1 ms per block. That gave about 20% more
blocks per second during the 95k tx/s window. sei-protocol#4269 reverted it for the
reviewable stack, and that stack (sei-protocol#4273) never reached main. This PR
ports sei-protocol#4261 and the sei-protocol#4273 follow-up to main (PLT-1377).

Acceptance stays a serial barrier in block order. `stateAccessIndex` and
`blockSTMState` split into 64 address shards. `indexResults` records the
writes of every speculative result up front, and `conflictsWithin(key,
sourcePrefix, txIndex)` replaces `conflictsWithAfter`.
`acceptValidatedPrefix` finds the first result that the serial frontier
would not accept, and `applyRange` folds the run before it into the
prefix shard by shard. `validateBlockSTMFrontier` then handles only that
one result, and `serialBackoff` keeps dependency chains on the calling
goroutine. Each pass looks ahead at most twice as far as the previous
pass accepted, and at least 2,048 results, so the work of a block's
passes stays linear in its size. `changeSetIntoParallel` builds the
changeset of each shard on the pool and joins the shards in address
order. Its per-shard base-row cache replaces `prefetchBaseAccounts`. The
port needs nothing from sei-protocol#4258 or sei-protocol#4260; the only conflicts came from the
`baseAccount` and phase-constant renames on main.

Non-app-hash-breaking: changesets, receipts, and tx results stay
byte-identical to main. Shards are contiguous ranges of the first
address byte, so shard order is canonical address order. Each address
lives in one shard, so the apply order in a shard is block order. The
new index can only add conflicts, from stale writes of an earlier
incarnation. An extra rerun executes against the exact accepted prefix,
so it gives the sequential result, and the extra reruns show only in OCC
metrics. Review `stmFrontierAccepts` most closely, because it must
mirror `needsSTMRerun`, and `touchedShards`, because it must cover every
address that `applyOwned` and `indexResults` touch.
`TestOCCRandomizedConflictingBlocksMatchSequential` checks seeded dense
and sparse blocks at 3 and 8 workers against the sequential executor,
down to the encoded FlatKV pairs. A seeded digest run gave identical
output on main and this branch for 864 blocks, including 3,000- and
6,000-transaction blocks that cross a pass's look-ahead.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants