fix(levm): revert doesn't unmark the account as existing#6592
Conversation
A reverted transaction that touched a non-existent account via mark_modified leaves a stale exists=true in the cache. A subsequent EIP-7702 SetCode tx whose authority is that address then sees exists=true and applies the spurious REFUND_AUTH_PER_EXISTING_ACCOUNT. The test compares the gas charged by an EIP-7702 SetCode tx with a fresh authority in two scenarios within a single block: only the EIP-7702 tx, and a reverting helper-call that touches the authority (via a 1-wei sub-call) followed by the EIP-7702 tx. Both must charge the same gas.
restore_cache_state restored info, status, and has_storage from the backup but omitted exists. mark_modified() unconditionally sets exists=true, so a reverted transaction that touched a non-existent account leaves stale exists=true in the cache. A subsequent EIP-7702 SetCode tx whose authority is that address then sees exists=true and applies the spurious REFUND_AUTH_PER_EXISTING_ACCOUNT (or the EIP-8037 STATE_GAS_NEW_ACCOUNT refund on Amsterdam+). Restoring exists is only effective if the backup carries the pre-modification state. VM::get_account_mut and the SELFDESTRUCT hook both backed up via &mut returned from db.get_account_mut, which runs mark_modified before the backup is taken. Reorder so the backup captures the immutable account state and mark_modified runs after.
🤖 Kimi Code ReviewThis PR fixes a consensus-critical bug in the LEVM implementation regarding EIP-7702 authority account state handling during transaction reverts. Summary of ChangesThe fix ensures that the Detailed Review1.
|
Lines of code reportTotal lines added: Detailed view |
Greptile SummaryThis PR fixes a bug where a reverted account creation incorrectly left the
Confidence Score: 5/5Safe to merge. The change is minimal and targeted — only the callframe backup/restore path for the The change is small and well-understood: one extra field restored on revert, and two sites where the backup snapshot is now taken before No files require special attention beyond the five changed here.
|
| Filename | Overview |
|---|---|
| crates/vm/levm/src/utils.rs | Adds current_account.exists = account.exists to restore_cache_state, completing the backup/restore cycle for the exists field — the core fix. |
| crates/vm/levm/src/db/gen_db.rs | Reorders backup capture in VM::get_account_mut to use the immutable get_account snapshot before get_account_mut calls mark_modified (which sets exists = true), ensuring the backup always holds the pre-modification state. |
| crates/vm/levm/src/hooks/default_hook.rs | Applies the same backup-before-mark_modified pattern to the SELFDESTRUCT account removal path, consistent with the fix in gen_db.rs. |
| test/tests/blockchain/eip7702_revert_authority_tests.rs | New regression test that constructs two scenarios (with and without a preceding reverted touch to the authority) and asserts equal gas usage for the EIP-7702 transaction; correctly captures the bug described in the PR. |
| test/tests/blockchain/mod.rs | Adds the new test module declaration — trivial change. |
Reviews (1): Last reviewed commit: "fix(levm): restore exists flag on transa..." | Re-trigger Greptile
🤖 Claude Code ReviewNow I have a thorough understanding of the changes. Let me write the review. PR Review: fix(levm): revert doesn't unmark the account as existingOverviewThis PR fixes a two-part bug in LEVM's EIP-7702 gas accounting during reverts:
Both fixes together are necessary: without the ordering fix, the backup would capture CorrectnessFix 1 — Fix 2 — Both fixes are necessary and complementary. The logic is sound. Potential Concern:
|
🤖 Codex Code ReviewNo findings. The fix is coherent with the underlying bug: Residual risk: the new regression test in Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Benchmark Results ComparisonNo significant difference was registered for any benchmark run. Detailed ResultsBenchmark Results: BubbleSort
Benchmark Results: ERC20Approval
Benchmark Results: ERC20Mint
Benchmark Results: ERC20Transfer
Benchmark Results: Factorial
Benchmark Results: FactorialRecursive
Benchmark Results: Fibonacci
Benchmark Results: FibonacciRecursive
Benchmark Results: ManyHashes
Benchmark Results: MstoreBench
Benchmark Results: Push
Benchmark Results: SstoreBench_no_opt
|
|
Reviewed the patch and ran a merge-tree test against a long-lived branch (no conflicts). Fix is clean and the test exercises the exact path. Two follow-up observations: 1. One more raw Grepping the levm tree turns up a third caller that bypasses // crates/vm/levm/src/hooks/l2_hook.rs:780
vm.db.get_account_mut(fee_token)?.storage = new_storage;
Either way it keeps the antipattern from proliferating to a future site where the account isn't guaranteed to pre-exist. 2. Worth auditing the storage-slot side separately The Otherwise the fix looks correct:
|
**Motivation** Prepare the v12.0.0 release by bumping the workspace version and merging the release branch back to main. **Description** - Bump workspace version from 11.0.0 to 12.0.0 in the root `Cargo.toml`, the guest-program `Cargo.toml` files (sp1, risc0, openvm, zisk) and `crates/l2/tee/quote-gen/Cargo.toml`; refresh lockfiles via `make update-cargo-lock`. - Update the `--builder.extra-data` default in `docs/CLI.md` to `ethrex 12.0.0` in both ethrex and ethrex L2 sections. - Merge `main` into the release branch, picking up the two levm fixes that landed during release stabilization (lambdaclass#6591, lambdaclass#6592) along with their tests: - `fix(levm): account erroneously considered as existing after zero-value transfer` (lambdaclass#6591) — avoids marking an account as modified when a balance increase/decrease is zero. - `fix(levm): revert doesn't unmark the account as existing` (lambdaclass#6592) — `restore_cache_state` now restores `exists` alongside `info`, `status`, and `has_storage` so a reverted transaction no longer leaves a stale `exists = true` in the cache (which would cause a later EIP-7702 SetCode tx to apply a spurious `REFUND_AUTH_PER_EXISTING_ACCOUNT` / EIP-8037 `STATE_GAS_NEW_ACCOUNT` refund). Also reorders the backup in `VM::get_account_mut` and the SELFDESTRUCT hook so the backup captures pre-modification state. These fixes were also cherry-picked directly onto the release branch before the merge; the merge from `main` adds their tests. **Checklist** - [x] Updated `STORE_SCHEMA_VERSION` — N/A, no storage schema change. --------- Co-authored-by: Lucas Fiegl <iovoid@users.noreply.github.com>
**Motivation** When an account creation reverts, we incorrectly mark it as existing for the purposes of EIP7702, leading to charging gas incorrectly. **Description** Currently we track accounts that exist in the trie for purposes of charging EIP7702 gas (the price is different if the authority already exists in the trie). This is saved in the `exists` field of the account, which is currently not saved in the callframe backup. This means when an account creation is reverted, we don't unmark it as existing. This causes an incorrect amount of gas to be charged. The PR contains a regression test, that should fail on the first commit and pass on the second.
**Motivation** When an account creation reverts, we incorrectly mark it as existing for the purposes of EIP7702, leading to charging gas incorrectly. **Description** Currently we track accounts that exist in the trie for purposes of charging EIP7702 gas (the price is different if the authority already exists in the trie). This is saved in the `exists` field of the account, which is currently not saved in the callframe backup. This means when an account creation is reverted, we don't unmark it as existing. This causes an incorrect amount of gas to be charged. The PR contains a regression test, that should fail on the first commit and pass on the second.
**Motivation** When an account creation reverts, we incorrectly mark it as existing for the purposes of EIP7702, leading to charging gas incorrectly. **Description** Currently we track accounts that exist in the trie for purposes of charging EIP7702 gas (the price is different if the authority already exists in the trie). This is saved in the `exists` field of the account, which is currently not saved in the callframe backup. This means when an account creation is reverted, we don't unmark it as existing. This causes an incorrect amount of gas to be charged. The PR contains a regression test, that should fail on the first commit and pass on the second.
**Motivation** Bring ethrex up to bal-devnet-7 (BAL fixtures `bal@v7.1.1`). Stacked on top of #bal-devnet-6-pr (now in main). **Description** Aligns EIP-8037 state-gas accounting with bal-devnet-7 spec progression (EELS PRs lambdaclass#2815 / lambdaclass#2816 / lambdaclass#2823 / lambdaclass#2827 / lambdaclass#2828 / lambdaclass#2836 / lambdaclass#2845 / lambdaclass#2848), bumps Amsterdam fixtures from `snobal-devnet-6@v1.1.0` to `bal@v7.1.1`, and bumps the pinned hive version past the ethrex `--http.api` fix. Main changes: - EIP-8037 state-gas alignment with bal-devnet-7: - System-call state-gas reservoir. - Halt refunds spilled state gas (Policy A). - Tx-level CREATE failure refunds intrinsic `NEW_ACCOUNT`; `intrinsic_state_gas_charged` preserved across the failure path. - Tx-CREATE collision refund with regular-gas burn; billing matches EELS. - Cross-frame revert leaks inline credits. - Cross-frame revert reservoir formula fix. - Block-level `state_gas_used` subtracts `state_refund`. - Remove same-tx SELFDESTRUCT state-gas refund (EELS PR lambdaclass#2845, v7.1.0). - EIP-7702: - `set_delegation` refund via dedicated `state_refund` channel. - `set_delegation` refunds `AUTH_BASE` on existing delegation (EELS PR lambdaclass#2836). - `set_delegation` refunds `AUTH_BASE` on delegation clear (EELS PR lambdaclass#2848, v7.1.1). - levm fixes pulled from main: - `revert` doesn't unmark the account as existing (lambdaclass#6592). - Account erroneously considered as existing after zero-value transfer (lambdaclass#6591). - Tooling / tests: - Per-tx gas-dimension dump on block `gas_used` mismatch. - Bump Amsterdam fixtures to `bal@v7.1.1`. - Annotate BAL balance-mismatch errors with gas-equivalent diff and recognised state-gas constant multiples. - Unskip 74 bal-devnet-6 Amsterdam fixtures now passing. - Skip 21 stale EIP-8025 fixtures pinned at `bal@v5.7.0` (zkevm@v0.3.3 bundle, pre-bal-7). - Drop stale bal-devnet-6 known-issues entries from `docs/known_issues.md` and hive `KNOWN_EXCLUDED_TESTS`. - CI: - Bump pinned hive version past the ethrex `--http.api` flag feature-detect fix (`c4d839b3`, hive lambdaclass#1485). Without this, hive starts ethrex with the default HTTP namespace allowlist (`eth,net,web3`) and tests touching `admin_*`/`debug_*`/`txpool_*` fail. **Local test run** `./run_test.sh` against `tests-bal@v7.1.1`: 2,145 / 2,145 pass. `cargo test -p ethrex-test --tests`: 453 / 453 pass. **Checklist** - [ ] Updated `STORE_SCHEMA_VERSION` (crates/storage/lib.rs) if the PR includes breaking changes to the `Store` requiring a re-sync. --------- Co-authored-by: Lucas Fiegl <iovoid@users.noreply.github.com> Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com>
Motivation
When an account creation reverts, we incorrectly mark it as existing for the purposes of EIP7702, leading to charging gas incorrectly.
Description
Currently we track accounts that exist in the trie for purposes of charging EIP7702 gas (the price is different if the authority already exists in the trie). This is saved in the
existsfield of the account, which is currently not saved in the callframe backup.This means when an account creation is reverted, we don't unmark it as existing. This causes an incorrect amount of gas to be charged.
The PR contains a regression test, that should fail on the first commit and pass on the second.