Skip to content

autobahn/evmonlyapp: prepare block n+1 while block n executes - #4272

Open
bdchatham wants to merge 1 commit into
devin/1789943071-stack-2-pending-overlayfrom
devin/1789943071-stack-3-prepare-pipeline
Open

bdchatham wants to merge 1 commit into
devin/1789943071-stack-2-pending-overlayfrom
devin/1789943071-stack-3-prepare-pipeline

Conversation

@bdchatham

@bdchatham bdchatham commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Re-opens #4260 on top of #4271 (stack 3/4) for review; the code is the merged change, unmodified.

Block preparation — RLP decoding and ecrecover for the senders CheckTx did not see — sat on the FinalizeBlock critical path. This PR takes it off: the evmonly application gains an ABCI-side PrepareBlock that decodes a block and parks it in a single preparedBlock slot keyed by (height, req.Hash); FinalizeBlock takes the slot when both match (takePrepared) and otherwise prepares inline as before. Matching on the header hash is enough because the autobahn header commits to the payload hash, so the tx set is pinned. Hits and misses are counted by evmonly_finalize_prepared_blocks_total{prepared=true|false}, and the evmonly_prepare timer (#4267) wraps PrepareBlock.

The autobahn execute loop in gigaRouterCommon.runExecute becomes a two-stage pipeline under utils/scope: a fetcher goroutine blocks in r.data.GlobalBlock(n), calls app.PrepareBlock on it, and hands the block over an unbuffered channel, so block n+1 is fetched and prepared while the loop executes block n. The proxy forwards PrepareBlock when the application implements it. Storage-tail phases (#4263) are unchanged; evmonly_finalize's prepare phase now mostly measures the slot hit.

Determinism is unaffected: preparation is pure decoding, and a miss (wrong or stale preparation) only costs the inline decode. What a reviewer should weigh is the unsynchronized handoff — the fetcher can prepare n+1 before FinalizeBlock(n) has taken its slot, which is a silent miss rather than an error. Validated with go test -race on sei-tendermint/internal/{evmonlyapp,p2p,proxy} (prepared/unprepared parity, wrong-hash, concurrent prepare/finalize) plus make lint/fmtcheck.

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

Medium Risk
Concurrent prepare/finalize and a single-slot handoff can silently miss preparation (extra decode only), but changes core block execution ordering and Autobahn’s execute loop concurrency.

Overview
Moves stateless block prep (RLP decode + sender recovery) off the FinalizeBlock hot path by adding PrepareBlock on the EVM-only app and wiring it through the Autobahn execute loop.

The app keeps a single prepared slot keyed by (height, block hash). FinalizeBlock consumes it via takePrepared and runs ExecutePreparedBlock; on a miss it decodes inline as before. CheckTx sender cache gains peek/forget so prep can run without stealing entries. OTel adds evmonly_finalize_prepared_blocks_total{prepared=...} and an evmonly_prepare phase timer.

gigaRouterCommon.runExecute becomes a two-stage pipeline: a fetcher loads global block n, calls app.PrepareBlock, and hands {block, req} over an unbuffered channel while the main loop executes block n−1. finalizeRequest is split from proposerAddress, which is filled at execute time after the prior commit. The proxy forwards PrepareBlock when the wrapped app implements it.

Tests cover prepared vs unprepared parity, wrong-hash ignores, concurrent prepare/finalize, and router proposer checks on delivered blocks.

Reviewed by Cursor Bugbot for commit 453eb65. 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 85.81081% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.02%. Comparing base (313568b) to head (453eb65).

Files with missing lines Patch % Lines
sei-tendermint/internal/p2p/giga_router_common.go 74.50% 12 Missing and 1 partial ⚠️
sei-tendermint/internal/evmonlyapp/app.go 92.39% 7 Missing ⚠️
sei-tendermint/internal/proxy/proxy.go 80.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                             Coverage Diff                              @@
##           devin/1789943071-stack-2-pending-overlay    #4272      +/-   ##
============================================================================
- Coverage                                     90.90%   83.02%   -7.89%     
============================================================================
  Files                                            28       30       +2     
  Lines                                          4091     2992    -1099     
============================================================================
- Hits                                           3719     2484    -1235     
- Misses                                          372      507     +135     
- Partials                                          0        1       +1     
Flag Coverage Δ
sei-chain-pr 84.36% <85.81%> (-6.55%) ⬇️
sei-db 74.50% <ø> (?)

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

Files with missing lines Coverage Δ
sei-tendermint/internal/proxy/proxy.go 82.92% <80.00%> (ø)
sei-tendermint/internal/evmonlyapp/app.go 87.00% <92.39%> (+1.48%) ⬆️
sei-tendermint/internal/p2p/giga_router_common.go 78.41% <74.50%> (ø)

... and 52 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 two-stage fetch/prepare pipeline and the preparedBlock slot are correct — (height, hash) guarding means a wrong or stale preparation only falls back to inline decoding, and Executor.PrepareBlock is genuinely stateless so running it beside FinalizeBlock is safe. The main concern is that the handoff between the fetcher and the execute loop is unsynchronized, so the optimization can silently not apply.

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

Blockers

  • None at the file/PR level.

Non-blocking

  • [suggestion] No test pins that the pipeline actually produces prepared hits. TestEVMOnlyApplicationPreparesTheNextBlockWhileFinalizingTheCurrentOne asserts determinism but never reads evmonly_finalize_prepared_blocks_total, and in that test PrepareBlock(n+1) races FinalizeBlock(n) exactly as in production — so its own hit/miss split is nondeterministic and a regression to always-miss would stay green. The p2p router test (CheckBlocks) likewise only checks heights and proposers. Consider asserting the hit counter somewhere the handoff is deterministic.
  • [suggestion] The PR description does not match the implementation: it describes fetching n+1 with a non-blocking TryBlock over the contiguous prefix, but runExecute spawns a fetcher that blocks in r.data.GlobalBlock(n); it says FinalizeBlock matches "height and tx hashes" when the code matches height and req.Hash; and it names {hit|miss} labels where the counter uses prepared=true|false. Worth updating so the description describes the mechanism that landed.
  • 2 suggestion(s)/nit(s) flagged inline on specific lines.

return fmt.Errorf("r.data.GlobalBlock(%v): %w", n, err)
}
req := r.finalizeRequest(b)
if err := app.PrepareBlock(ctx, req); 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 comment above claims the fetcher "holds block n+1, already prepared, while the loop below executes block n", but the app keeps a single prepared slot and nothing synchronizes the fetcher's write to it against the execute loop's read.

Ordering is: prepare(n) → Send(n) → executor receives and starts executeBlock(n) → fetcher immediately does GlobalBlock(n+1) + PrepareBlock(n+1), which overwrites the slot. FinalizeBlock(n) only reaches takePrepared(n, hash) after proposerAddress(), parseFinalizeRequest, the executor lock and beginBlock. If the fetcher gets there first — likely when n+1 is empty or tiny and already local, i.e. during catch-up — block n misses and pays the full RLP decode + ecrecover inline, which is exactly the cost this PR removes. The loss is bounded (n+1 then hits) and correctness is unaffected, but it is silent: only the prepared counter shows it.

Either carry the preparation with the fetched block through the channel instead of a shared slot, or let the app hold the last two prepared blocks (dropping any at or below the last finalized height) so preparing n+1 cannot evict n.

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.

Not changed here by design: this stack re-opens the already-merged code unmodified so the team can review what actually runs on giga-1 (the stack top equals current giga-1). Noted as a follow-up. The PR description now describes the shared slot and the blocking fetcher as they are, including this miss window.

Txs: req.Txs,
Senders: senders,
})
prepared, hit := a.takePrepared(block.height, block.blockHash)

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 follow from keying the prepared block on (height, blockHash) alone.

First, on a hit req.Txs is never executed — result, and therefore TxResults, come entirely from prepared. That is safe only because the autobahn header hash commits to payloadHash, so (height, req.Hash) pins the tx set. Nothing in the app states that requirement, and Proxy.PrepareBlock is reachable for any ABCI application; a caller whose req.Hash does not commit to req.Txs (as TestEVMOnlyApplicationIgnoresAPreparedBlockForAnotherHash constructs by hand) would silently execute a different tx list than the one it passed.

Second, forgetSenders(req.Txs) on line 741 re-runs hashRawTxs over every raw transaction in the block on the critical path, purely to evict entries peekSenders already looked up during preparation.

One change covers both: have PrepareBlock keep the tx hashes it computed in preparedBlock, then verify them against the block's txs on the hit path and use them to drop the cached senders without hashing again.

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.

Not changed here by design: this stack re-opens the already-merged code unmodified so the team can review what actually runs on giga-1 (the stack top equals current giga-1). Noted as a follow-up.

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789943071-stack-3-prepare-pipeline branch from 120c763 to 1b530d3 Compare September 20, 2026 22:50
@bdchatham bdchatham changed the title [stack 3/4] autobahn/evmonlyapp: prepare block n+1 while block n executes autobahn/evmonlyapp: prepare block n+1 while block n executes Sep 20, 2026
@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 force-pushed the devin/1789943071-stack-3-prepare-pipeline branch from 1b530d3 to 453eb65 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

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