fix(levm): keep system-call storage write when coinbase is the called contract#6751
Conversation
|
🤖 Kimi Code ReviewThis PR fixes a consensus-critical bug where system contract storage writes would be incorrectly reverted when the block's fee recipient (coinbase) equals the system contract address. SummaryThe change is correct and well-tested. The logic prevents the post-call coinbase restoration from clobbering legitimate storage mutations when the callee and fee recipient are the same account. Detailed Reviewcrates/vm/backends/levm/mod.rsLines 2586–2598: The fix correctly identifies that when The comment at lines 2586–2593 accurately explains the consensus requirement: other clients commit the system-call diff without restoring the callee, so restoring it here would cause a state root divergence. Suggestion: Consider adding a brief note about why this specifically affects consensus (state root mismatch) in the function-level documentation, as this is a subtle edge case in Ethereum's state transition rules. Test Coverage (Lines 3095–3236)The regression tests are comprehensive:
Lines 3112–3116: The Lines 3138–3146: The Lines 3180–3200: The Security/Consensus ConsiderationsThis fix is consensus-critical. The previous behavior would cause a state root mismatch with other clients (Geth, Nethermind, etc.) when processing blocks where The fix correctly handles:
All system calls that write to storage are protected by this change. Minor SuggestionsLine 3190: The Line 3112: Consider adding a source comment for the bytecode, e.g.: // EIP-2935 history contract runtime bytecode (see https://eips.ethereum.org/EIPS/eip-2935)DependenciesCargo.toml: Adding VerdictApprove. The fix is minimal, correct, and thoroughly tested. The regression test prevents future breakage of this specific consensus edge case. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
Greptile SummaryThis PR fixes a state-root divergence bug in
Confidence Score: 5/5Safe to merge — the change is a minimal, well-targeted guard that only affects the specific edge case where the block's fee recipient equals the system contract address. The fix is a single conditional wrapping the existing coinbase-restore logic, with no changes to the happy path for ordinary fee recipients. The comment explains exactly why the guard is correct, the tests verify both the in-memory cache and the emitted state updates, and the regression test was confirmed to fail on the pre-fix code. No files require special attention; crates/vm/backends/levm/mod.rs is the only substantive change and it is straightforward.
|
| Filename | Overview |
|---|---|
| crates/vm/backends/levm/mod.rs | Core fix: one-line guard skips coinbase restore when coinbase==contract_address; well-commented with thorough regression tests added in the same file. |
| crates/vm/Cargo.toml | Adds hex as a dev-dependency for decoding the EIP-2935 runtime bytecode constant in tests. |
| Cargo.lock | Lock file updated to record the new hex dev-dependency for ethrex-vm. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[generic_system_contract_levm called] --> B[Snapshot coinbase_backup and system_account_backup]
B --> C[Execute system contract e.g. EIP-2935 storage write]
C --> D{Execution succeeded?}
D -- No --> E[Return error]
D -- Yes --> F[Restore system_address from system_account_backup]
F --> G{block_header.coinbase == contract_address?}
G -- Yes coinbase IS the callee --> H[Skip coinbase restore Storage write persists]
G -- No ordinary fee recipient --> I[Restore coinbase account from coinbase_backup]
H --> J[Return Ok report]
I --> J
Reviews (1): Last reviewed commit: "fix(levm): keep system-call storage writ..." | Re-trigger Greptile
🤖 Codex Code Review
No production correctness bug stood out in the actual I could not run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Lines of code reportTotal lines added: Detailed view |
🤖 Claude Code ReviewI now have enough context to write the full review. Review:
|
JereSalo
left a comment
There was a problem hiding this comment.
Good news, we don't actually need a backup for any of the accounts. The only change that we have to revert when calling a system contract is the nonce incremental that is done to the SYSTEM_ADDRESS and happens on every transaction independently of its outcome, but there's nothing more that needs to be restored (thanks to gas price being set to zero).
I'd lean towards simplifying things here and forget about the backups entirely and after execution we can just decrement the nonce by one to the sender and that's it. Also, there's no need to remove something from the cache (current_accounts_state) if it wasn't there in the first place, it doesn't produce any harm not to do so, and in fact it may even be beneficial performance-wise (though probably negligible, so it's just a side-effect) if we reuse the same cached accounts for executing multiple blocks because we are not going to be removing accounts from it unnecessarily and reloading them on every block.
The fix I propose is simply removing the backup of both coinbase and system account and just add these lines at the end to decrease the nonce of the sender, that's it:
if let Some(account) = db.current_accounts_state.get_mut(&system_address) {
account.info.nonce = account.info.nonce.saturating_sub(1);
}I made the change in another branch in case you want to check it out. Feel free to incorporate it if you agree on the approach:
fix/syscall-coinbase-collision...refactor/syscall-nonce-decrement
|
Thanks Jere — great call, this is a much cleaner fix than my guard. Incorporated your commit as-is (kept your authorship): cherry-picked it on top after merging I confirmed it against the regression test I'd added ( One small thing I added on top ( |
JereSalo
left a comment
There was a problem hiding this comment.
Great, perhaps in the future we could drop the tests added here in case tests with same behavior are added to the execution-specs test suite. In the meantime it might be okay to keep them here.
| debug_assert!( | ||
| env.gas_price.is_zero() && env.base_fee_per_gas.is_zero(), | ||
| "system calls must run with a zero gas price" | ||
| ); |
There was a problem hiding this comment.
Tests make this redundant, I think
Summary
generic_system_contract_levmsnapshots the blockcoinbaseaccount before a system call (EIP-2935 / 4788 / 7002 / 7251) and restores it afterwards, so the call leaves no fee/value side effect on the fee recipient. That restore was unconditional.When a block's fee recipient (
coinbase) is set to the system contract being called, the restore reverts that contract's account to its pre-call snapshot — dropping the storage write the system call just made (e.g. the EIP-2935 parent-hash slot, or an EIP-7002/7251 request) from the emitted state updates. The result is a post-state root that diverges from other clients for an otherwise-valid block, sincefeeRecipientis freely chosen by the block producer.Fix
Skip the coinbase restore when
block_header.coinbase == contract_address. In that case the only change to the account is the system call's own storage write, which must persist; other clients commit the system-call diff and never restore the callee. Ordinary fee recipients are unaffected. A single guard covers all four system contracts, since they share this helper.Test
Adds
system_call_coinbase_tests, which drives the real EIP-2935 system call and asserts the history-contract storage write is emitted both for an ordinary fee recipient and when the fee recipient equals the history contract (the regression case). Verified the regression test fails on the pre-fix code.