Skip to content

mempool/evmonlyapp: read nonce/balance through the pending overlay; fetch first-seen accounts outside the store lock - #4271

Open
bdchatham wants to merge 1 commit into
devin/1789943071-stack-1-commit-behind-finalizefrom
devin/1789943071-stack-2-pending-overlay
Open

bdchatham wants to merge 1 commit into
devin/1789943071-stack-1-commit-behind-finalizefrom
devin/1789943071-stack-2-pending-overlay

Conversation

@bdchatham

Copy link
Copy Markdown
Contributor

Describe your changes and provide context

Re-opens #4258 on top of #4270 (stack 2/4). Depends on #4270's background commit: with the commit landing behind FinalizeBlock, CheckTx's nonce/balance reads would otherwise settle (wait for the commit) on every admission.

  • Executor.ReadLatestAccount(addr) returns the account as of the last block the executor ran, laying the in-flight block's pendingChanges over a store view without waiting for its commit. A pipelineGeneration counter detects a commit that started between opening the view and reading, in which case the read restarts (the view may hold a later block, and replaying the older overlay over it would go backwards). It reports the first failed commit rather than state that lacks that block.
  • evmonlyapp.EvmNonce/EvmBalance use it instead of openSettledView, so admission no longer settles the pipeline.
  • mempool/tx.go: the first-seen account lookup (accountReader.Account) moves out of the inner mempool mutex — lookup, lock, recheck that nobody inserted the account meanwhile — so the store read no longer serializes all CheckTx callers.

Testing performed to validate your change

  • scripts/ramtest.sh -race ./giga/evmonly/... (overlay/pipelined-read tests: sees in-flight block, reports failed commit, restarts when a block lands or retires underneath).
  • go test -race ./sei-tendermint/internal/evmonlyapp/... ./sei-tendermint/internal/mempool/....
  • 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 nonce/balance semantics for admission and concurrent mempool inserts; incorrect overlay/generation handling could admit bad txs or serve stale account state under pipelined commits.

Overview
Adds Executor.ReadLatestAccount so balance/nonce reflect the last executed block by layering in-flight pendingChanges over a store view without waiting for background commit. A pipelineGeneration counter forces the read to retry if another commit starts mid-flight (avoiding replaying a stale overlay over newer landed state), and failed commits surface as errors instead of partial state.

evmonlyapp routes EvmNonce / EvmBalance through this path so CheckTx admission sees the block FinalizeBlock just produced while its commit is still pipelined, rather than settling via openSettledView.

Mempool prefetches first-seen sender balance/nonce outside the store mutex; accountsEpoch invalidates prefetches taken before Clear / post-block Update so stale nonce checks are not installed after the chain advances.

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

@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

@codecov

codecov Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.33333% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.41%. Comparing base (4fefcc7) to head (313568b).

Files with missing lines Patch % Lines
sei-tendermint/internal/evmonlyapp/app.go 66.66% 4 Missing ⚠️
giga/evmonly/giga_store.go 91.66% 3 Missing ⚠️
sei-tendermint/internal/mempool/tx.go 95.23% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                                 Coverage Diff                                  @@
##           devin/1789943071-stack-1-commit-behind-finalize    #4271       +/-   ##
====================================================================================
+ Coverage                                            75.42%   89.41%   +13.99%     
====================================================================================
  Files                                                   10       33       +23     
  Lines                                                 1778     4499     +2721     
====================================================================================
+ Hits                                                  1341     4023     +2682     
- Misses                                                 436      476       +40     
+ Partials                                                 1        0        -1     
Flag Coverage Δ
sei-chain-pr 90.90% <89.33%> (+15.48%) ⬆️
sei-db 74.50% <ø> (?)

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

Files with missing lines Coverage Δ
giga/evmonly/executor.go 91.25% <ø> (ø)
giga/evmonly/pipeline_overlay.go 97.61% <100.00%> (ø)
sei-tendermint/internal/mempool/tx.go 95.04% <95.23%> (ø)
giga/evmonly/giga_store.go 92.00% <91.66%> (ø)
sei-tendermint/internal/evmonlyapp/app.go 85.51% <66.66%> (-0.64%) ⬇️

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

The pipelined latest-account read and the out-of-lock mempool prefetch both look correct: the pipelineGeneration guard is checked after the view is opened (so an unchanged generation proves the view holds nothing newer than the overlay), accountsEpoch is bumped at every accounts reset site, and LatestAccount.Balance can never be nil. Only non-blocking efficiency/observability suggestions.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] EvmNonce and EvmBalance each run a full latestAccount (view open + generation check + overlay), so txStore.prefetchAccount and txStore.insert each do two independent pipelined reads per first-seen account, and the balance/nonce pair they fold into one evmAccount can come from two different blocks. The new evmonly.LatestAccount already returns both together — a single combined method on the ABCI Application surface would halve the work and make the pair consistent. (The two-call shape is pre-existing; this PR adds the API that would let it collapse.)
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

}
reader := pending.overlay(gigaSnapshotStateReader{snapshot: snapshot, missingState: e.missingState})
if rowReader, ok := reader.(accountSnapshotReader); ok {
if row, ok := rowReader.ReadAccount(addr); ok {

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] ReadAccount is the full-row read: gigaSnapshotStateReader.ReadAccount additionally does r.snapshot.GetCode(addr) (plus a cloneBytes copy) whenever row.CodeHash != EmptyCodeHash, and LatestAccount throws that code away.

For EOAs — the mempool hot path this PR targets — that costs nothing. But EvmBalance/EvmNonce are also the backing for eth_getBalance and eth_getTransactionCount (rpc/core/mempool.go → Environment.EvmBalance/EvmTransactionCount → latestAccount), which accept arbitrary addresses. Against a contract that is a new per-call read and copy of up to 24 KiB of bytecode; the previous openSettledView path (AccountExists + GetBalance) never touched the code store.

Since only balance and nonce are wanted here, reading reader.GetBalance(addr) / reader.GetNonce(addr) directly (or adding a code-free row read) would avoid it.

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.

// has failed, it reads the settled store instead.
func (a *evmOnlyApplication) latestAccount(address common.Address) evmonly.LatestAccount {
if executor, ok := a.settler.Load().Get(); ok {
if account, err := executor.ReadLatestAccount(address); err == 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 error from ReadLatestAccount is discarded entirely. The two documented cases are fine — before InitChain the settler is empty, and a failed commit is logged once by openSettledView — but the remaining error (errors.New("giga store returned a nil snapshot")) falls through with no signal at all.

The fallback is openSettledView, which calls AwaitCommits() and therefore blocks. So if the store ever starts handing back nil views, every CheckTx admission silently reverts to settling the pipeline on each call — precisely the regression this PR removes — and nothing in the logs says so. A one-shot log on the non-nil, non-failure error (mirroring settleFailureLogged) would make that visible.

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-2-pending-overlay branch from 43c1a5c to c40a502 Compare September 20, 2026 22:50
@bdchatham bdchatham changed the title [stack 2/4] mempool/evmonlyapp: read nonce/balance through the pending overlay; fetch first-seen accounts outside the store lock mempool/evmonlyapp: read nonce/balance through the pending overlay; fetch first-seen accounts outside the store lock Sep 20, 2026
@devin-ai-integration
devin-ai-integration Bot removed this pull request from stack #4274 September 20, 2026 23:18
…nding overlay; fetch first-seen accounts outside the store lock (#4258)""

This reverts commit 69c86b0.
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789943071-stack-2-pending-overlay branch from c40a502 to 313568b 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
if moved {
return LatestAccount{}, false
}
reader := pending.overlay(gigaSnapshotStateReader{snapshot: snapshot, missingState: e.missingState})

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is it possible that N's changes haven't finished committing when N+2 has begun execution? In that case missingState would carry changes from N+1 but not from N unless missingState itself is stacked

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.

No — at most one block is ever uncommitted. executePreparedBlockWithStore calls awaitPipelineCommit() (lands N) right before startPipelineCommit for N+1, and it holds storeMu for the whole block, so N+2 cannot begin executing until N+1 has returned, i.e. until N is in the store. That is the invariant the pendingChanges overlay relies on: the view holds ≤ N, pending is exactly N+1, nothing in between can be missing. The pipelineGeneration recheck in readLatestAccount covers the one race that remains — a commit for N+2 starting between the read of pending and the view being opened — by retrying rather than replaying N+1 over a view that already contains N+2.

missingState is a different thing: it is not a per-block layer but the fallback StateReader for accounts the store has never seen (WithMissingAccountState, used for genesis-less funding in tests/loadtest), consulted only when snapshot.AccountExists(addr) is false. It never carries block changes, so there is nothing to stack — the overlay sits above it and above the snapshot alike.

This branch has not been deployed

No deployments
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