Persist the EVM-only execution cursor with each block's FlatKV state - #4190
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## giga-1 #4190 +/- ##
==========================================
- Coverage 65.55% 65.54% -0.02%
==========================================
Files 2081 2077 -4
Lines 157460 157117 -343
==========================================
- Hits 103222 102975 -247
+ Misses 54097 54001 -96
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
FlatKV state became durable in FinalizeBlock while the app's height, app hash and parent hash lived only in memory, so a restarted evmonly node reported height 0, re-ran InitChain and block 1, and crash-looped on existing nonces. The cursor is now written as an evmonly named changeset in the same CommitStateChanges call as the block's state, restored on construction, and a non-empty store refuses InitChain at any initial height.
665d830 to
273ea8b
Compare
PR SummaryHigh Risk Overview The executor now accepts an optional Tests cover restart resume, finalize-without-commit, and repeat Reviewed by Cursor Bugbot for commit 50c4f0f. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Persisting the execution cursor in the same CommitStateChanges batch as the block's EVM state is a sound fix for the restart crash-loop, and the restart/finalized-but-uncommitted paths are covered by new end-to-end tests. Two non-blocking cleanups: a duplicated seedInitialStateVersion call in InitChain, and an implicit assumption behind abandonPending.
Findings: 0 blocking | 3 non-blocking | 2 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- [suggestion]
evmOnlyCursor.encode/decodeEVMOnlyCursorintroduce a new hand-rolled 80-byte on-disk format, but its error branches (wrong length, height > int64) have no direct test — the restart tests only exercise the happy round trip. A small table test incursor.go's package would pin the layout so a future field addition can't silently change the byte order or size. - 2 suggestion(s)/nit(s) flagged inline on specific lines.
| block can be retried. A state failure can leave receipts behind, but retrying | ||
| the block overwrites them. `ResultSink` runs only after both stores succeed. | ||
|
|
||
| Because state becomes durable inside `ExecuteBlock`, before any ABCI `Commit`, |
There was a problem hiding this comment.
Not entirely accurate. The crash durability point is when we flush the block to BlockDB. The storage layer is intentionally asynchronous, and so at any point in time we may have dozens of executed blocks that are not crash durable at the DB layer. This is ok though, since we can re-execute blocks from the BlockDB.
There was a problem hiding this comment.
In theory we could make ExecuteBlock crash durable as this comment suggests. Cost of doing this is likely on the order of 50% to 80% reduction in max DB throughput.
There was a problem hiding this comment.
Thanks, reworded in 50c4f0f. It no longer claims crash durability at ExecuteBlock; it now says the store's version advances independently of ABCI Commit and that what survives a restart is decided by the storage layer (async flush, re-execution from BlockDB), which is why Info() has to come from storage rather than memory. Left unresolved in case you want to tweak the wording further.
…cursor tests, README wording
Fixes #4169. The EVM-only Autobahn app made FlatKV state durable inside
FinalizeBlockbut kept its height, app hash and parent hash only in memory, advanced byCommit. After a restartInfo()reported height 0, the Giga router took the fresh-genesis branch, andInitChainplus block 1 were replayed against state that already held those nonces, so the node crash-looped. TheinitialHeight == 1early return in the InitChain guard meant a non-empty store was never refused at the default initial height.The executor gains an optional
BlockChangeSetEncoderwhose named changesets are appended to the block's state changesets in the sameCommitStateChangescall. The app uses it to write anevmonly/cursorrecord (height ‖ appHash ‖ parentHash ‖ gasLimit) atomically with each block, andNewEVMOnlyApplicationreads it back on construction soInfo()reports the durable height and the router takes its restart branch. A crash betweenFinalizeBlock(N)andCommit(N)therefore restarts at N.InitChainis refused once an executor exists or the store is at any version other than 0 orinitialHeight-1. The chained SHA-256 app hash is unchanged.