Skip to content

fix(levm): skip nonce validation in eth_call simulations#6325

Open
ilitteri wants to merge 1 commit intomainfrom
fix/eth-call-nonce-validation
Open

fix(levm): skip nonce validation in eth_call simulations#6325
ilitteri wants to merge 1 commit intomainfrom
fix/eth-call-nonce-validation

Conversation

@ilitteri
Copy link
Collaborator

@ilitteri ilitteri commented Mar 5, 2026

Motivation

eth_call validates the transaction nonce against the sender account nonce. When the caller does not provide a nonce (the normal case for read-only calls), it defaults to 0. Any account with nonce > 0 gets a NonceMismatch error.

For reference, Geth skips nonce validation entirely for eth_call via a SkipNonceChecks flag in core/state_transition.go.

Closes #6312

Description

Add a disable_nonce_check flag to Environment (following the existing disable_balance_check pattern) and set it to true in simulate_tx_from_generic, which is the code path used by eth_call and eth_estimateGas.

Changes:

  • environment.rs: Added disable_nonce_check: bool field
  • backends/levm/mod.rs: Set flag to true in simulate_tx_from_generic; set to false in explicit Environment constructors
  • hooks/default_hook.rs, hooks/l2_hook.rs: Guard nonce mismatch check with !vm.env.disable_nonce_check

Add a `disable_nonce_check` flag to `Environment` (following the existing
`disable_balance_check` pattern) and set it to `true` in
`simulate_tx_from_generic`, which is the code path used by `eth_call` and
`eth_estimateGas`. This prevents `NonceMismatch` errors when the caller
does not provide a nonce (the normal case for read-only calls), matching
Geth's behavior of skipping nonce checks for `eth_call`.

Closes #6312
Copilot AI review requested due to automatic review settings March 5, 2026 20:45
@ilitteri ilitteri requested a review from ManuelBilbao as a code owner March 5, 2026 20:45
@ilitteri ilitteri added the levm Lambda EVM implementation label Mar 5, 2026
@ilitteri ilitteri requested review from a team and avilagaston9 as code owners March 5, 2026 20:45
@ilitteri ilitteri added the levm Lambda EVM implementation label Mar 5, 2026
@github-actions
Copy link

github-actions bot commented Mar 5, 2026

🤖 Kimi Code Review

Review Summary

The changes look correct and address the issue of nonce validation in eth_call simulations. The implementation follows a consistent pattern with the existing disable_balance_check flag.

Detailed Review

1. Code Correctness

  • The new disable_nonce_check field is properly added to the Environment struct
  • Both hooks (DefaultHook and L2Hook) correctly check this flag before validating nonces
  • The flag is set to true in eth_call contexts and false in regular transaction contexts

2. Security Considerations

  • This change is safe because:
    • eth_call is explicitly a simulation endpoint, not actual transaction execution
    • The flag is only set to true in eth_call contexts, never for real transaction processing
    • The default value is false, ensuring nonce checks are performed by default

3. Consistency

  • The implementation mirrors the existing disable_balance_check pattern
  • Both hooks apply the same logic, maintaining consistency across different VM configurations

4. Minor Suggestions

File: crates/vm/levm/src/environment.rs

  • Line 47: Consider adding a doc comment example or clarifying that this is specifically for eth_call and similar simulation RPC methods

File: crates/vm/backends/levm/mod.rs

  • Lines 1165, 1226, 1693: The flag initialization could be slightly more consistent. Consider grouping the "disable" flags together:
// Instead of spreading them across the struct initialization
disable_balance_check: false,
disable_nonce_check: false,

These are minor style preferences and don't affect correctness.

Conclusion

The PR correctly implements nonce check bypassing for eth_call simulations while maintaining security for actual transaction execution. The code is clean, follows established patterns, and addresses the intended use case appropriately.


Automated review by Kimi (Moonshot AI) · custom prompt

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Skip nonce validation during LEVM eth_call/simulation paths to avoid NonceMismatch errors when callers omit a nonce (defaulting to 0).

Changes:

  • Added disable_nonce_check to Environment to control nonce validation behavior.
  • Enabled disable_nonce_check for simulate_tx_from_generic (used by eth_call/eth_estimateGas).
  • Guarded nonce mismatch validation in both default and L2 hooks.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
crates/vm/levm/src/environment.rs Adds an environment flag to control nonce checking behavior during simulations.
crates/vm/backends/levm/mod.rs Initializes the new flag and enables it on the generic simulation path.
crates/vm/levm/src/hooks/default_hook.rs Skips nonce mismatch validation when the new flag is enabled.
crates/vm/levm/src/hooks/l2_hook.rs Skips nonce mismatch validation when the new flag is enabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 76 to 82
.map_err(|_| TxValidationError::NonceIsMax)?;

// check for nonce mismatch
if sender_info.nonce != vm.env.tx_nonce {
if !vm.env.disable_nonce_check && sender_info.nonce != vm.env.tx_nonce {
return Err(TxValidationError::NonceMismatch {
expected: sender_info.nonce,
actual: vm.env.tx_nonce,
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

disable_nonce_check currently only guards the NonceMismatch branch, but the NonceIsMax validation still runs even when nonce checks are disabled. If the intent is to “skip nonce validation entirely” (per PR description / Geth behavior), the NonceIsMax check should also be bypassed when vm.env.disable_nonce_check is true (e.g., wrap both validations under the same conditional).

Copilot uses AI. Check for mistakes.
Comment on lines 511 to 517
.map_err(|_| TxValidationError::NonceIsMax)?;

// check for nonce mismatch
if sender_info.nonce != vm.env.tx_nonce {
if !vm.env.disable_nonce_check && sender_info.nonce != vm.env.tx_nonce {
return Err(TxValidationError::NonceMismatch {
expected: sender_info.nonce,
actual: vm.env.tx_nonce,
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

Same issue as in DefaultHook: disable_nonce_check skips the mismatch check but not the preceding NonceIsMax validation. If simulations should bypass nonce validation entirely, consider gating the max-nonce validation with !vm.env.disable_nonce_check as well.

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +48
/// When true, skip nonce validation. Used by `eth_call` simulations
/// where the caller typically doesn't provide a nonce.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

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

The doc comment says the flag is used by eth_call simulations, but the PR description also states it’s enabled for eth_estimateGas (via simulate_tx_from_generic). Consider updating the comment to mention both call sites so future readers don’t assume it’s eth_call-only.

Suggested change
/// When true, skip nonce validation. Used by `eth_call` simulations
/// where the caller typically doesn't provide a nonce.
/// When true, skip nonce validation. Used by `eth_call` and `eth_estimateGas`
/// simulations where the caller typically doesn't provide a nonce.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Mar 5, 2026

Lines of code report

Total lines added: 4
Total lines removed: 0
Total lines changed: 4

Detailed view
+------------------------------------------+-------+------+
| File                                     | Lines | Diff |
+------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs    | 1510  | +3   |
+------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/environment.rs | 98    | +1   |
+------------------------------------------+-------+------+

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

🤖 Codex Code Review

No blocking findings. The change is scoped correctly: nonce validation remains enabled for consensus execution and is only disabled for generic simulation.

  • disable_nonce_check defaults to false in normal env construction (mod.rs, mod.rs).
  • It is enabled only in simulation path (mod.rs).
  • Both L1 and L2 nonce mismatch checks now gate on that flag (default_hook.rs, l2_hook.rs).

Residual risk / test gap:

  • I recommend adding regression tests for:
  1. eth_call without nonce succeeds when sender state nonce is non-zero (simulation path via transaction.rs).
  2. Regular tx execution still rejects nonce mismatch (consensus path via default_hook.rs).

I could not run cargo check in this environment due a rustup temp-file permission error (/home/runner/.rustup/tmp/...: Permission denied).


Automated review by OpenAI Codex · custom prompt

@greptile-apps
Copy link

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR fixes eth_call and eth_estimateGas failing with NonceMismatch for any account whose nonce is greater than zero by introducing a disable_nonce_check: bool flag on Environment, following the existing disable_balance_check pattern. The flag is set to true only in simulate_tx_from_generic (the shared eth_call/eth_estimateGas path) and guarded at the two nonce-mismatch check sites in DefaultHook and L2Hook.

Key findings:

  • Compilation errors (critical): Three files that build Environment with exhaustive struct literals (no ..Default::default() spread) were not updated to include the new disable_nonce_check field. This will prevent the crate from compiling:
    • tooling/ef_tests/state_v2/src/modules/runner.rs:151
    • tooling/ef_tests/state/runner/levm_runner.rs:231
    • test/tests/levm/eip7708_tests.rs:203
  • Minor divergence from Geth: Geth's SkipNonceChecks bypasses nonce validation entirely (including the NonceIsMax overflow guard). This implementation only skips the mismatch check; NonceIsMax is still enforced during simulations. For the overwhelming majority of real-world accounts this is irrelevant, but it is a semantic difference worth noting.

Confidence Score: 1/5

  • Not safe to merge — the PR introduces compilation errors in files outside the diff by omitting the new required struct field in three exhaustive Environment literals.
  • The approach and logic are sound, but three files with explicit (non-spread) Environment struct literals were not updated to include disable_nonce_check, which will cause compilation failures in the ef_tests and eip7708 test crates.
  • tooling/ef_tests/state_v2/src/modules/runner.rs, tooling/ef_tests/state/runner/levm_runner.rs, and test/tests/levm/eip7708_tests.rs all need disable_nonce_check: false added to their Environment struct literals.

Important Files Changed

Filename Overview
crates/vm/levm/src/environment.rs Adds disable_nonce_check: bool field to Environment, following the existing disable_balance_check pattern. Clean, well-documented addition.
crates/vm/backends/levm/mod.rs Correctly sets disable_nonce_check: false in the two explicit Environment constructors (setup_env, env_from_generic) and enables it with true only in simulate_tx_from_generic. The third env constructor in this file uses ..Default::default() so it is unaffected.
crates/vm/levm/src/hooks/default_hook.rs Guards the nonce-mismatch check with !vm.env.disable_nonce_check, but the unconditional increment_account_nonce call means NonceIsMax is still enforced during simulations — a minor divergence from Geth's full skip.
crates/vm/levm/src/hooks/l2_hook.rs Mirrors the same !vm.env.disable_nonce_check guard applied in default_hook.rs, consistent application of the fix for the L2 path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[eth_call / eth_estimateGas] --> B[simulate_tx_from_generic]
    B --> C[env_from_generic]
    C --> D[Environment\ndisable_nonce_check: false]
    D --> E[env.disable_nonce_check = true]
    E --> F[VM::execute]
    F --> G{Hook type?}
    G -->|L1| H[DefaultHook::prepare_execution]
    G -->|L2| I[L2Hook::prepare_execution_fee_token]
    H --> J[increment_account_nonce\nunconditional]
    I --> J
    J --> K{disable_nonce_check?}
    K -->|true| L[Skip nonce mismatch check ✓]
    K -->|false| M{nonce matches?}
    M -->|yes| L
    M -->|no| N[NonceMismatch error ✗]

    O[execute_tx\nnormal block execution] --> P[setup_env]
    P --> Q[Environment\ndisable_nonce_check: false]
    Q --> R[VM::execute → nonce validated normally]
Loading

Comments Outside Diff (2)

  1. tooling/ef_tests/state_v2/src/modules/runner.rs, line 151-152 (link)

    Missing disable_nonce_check field — compilation error

    The Environment struct literal is exhaustive (no ..Default::default() spread), but the newly added disable_nonce_check field is not present here. This will fail to compile.

    The same issue exists in:

    • test/tests/levm/eip7708_tests.rs:203
    • tooling/ef_tests/state/runner/levm_runner.rs:231

    All three sites need disable_nonce_check: false, added to the struct literal.

  2. crates/vm/levm/src/hooks/default_hook.rs, line 74-85 (link)

    NonceIsMax still enforced even when nonce check is disabled

    The NonceIsMax check (which increments the nonce and errors if it overflows u64::MAX) runs unconditionally, even when disable_nonce_check is true. Geth's SkipNonceChecks flag skips nonce validation entirely for eth_call, including the max-nonce guard.

    For the stated goal of matching Geth's behaviour, any account whose nonce is exactly u64::MAX will still fail eth_call with NonceIsMax, while Geth would succeed. This is an edge case, but it is a semantic divergence from the referenced Geth implementation. Consider guarding the increment (or at least the error path) with !vm.env.disable_nonce_check as well.

Last reviewed commit: b2e86f1

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

🤖 Claude Code Review

Here is my review of PR #6325:


Review: fix(levm): skip nonce validation in eth_call simulations

The fix is correct and addresses a real usability bug. The implementation is clean and follows the existing disable_balance_check pattern. A few points worth noting:

Correctness

NonceIsMax check still fires in simulation mode

increment_account_nonce is called unconditionally (step 7, line 75 in default_hook.rs), before the new guard. This means that for an account whose nonce is u64::MAX, eth_call will still fail with TxValidationError::NonceIsMax. In Geth, the NonceIsMax check lives inside the SkipNonceChecks block, so it is also skipped for eth_call. The current implementation diverges from Geth in this edge case.

In practice this is unreachable (a nonce of u64::MAX would require ~1.8×10¹⁹ transactions), but it's a semantic gap worth acknowledging.

Nonce increment in simulation DB

increment_account_nonce still mutates the ephemeral simulation DB even when disable_nonce_check = true. This is harmless because eth_call uses state that is never committed to persistent storage, and is consistent with how Geth handles it (nonce is incremented even with SkipNonceChecks). No issue here.

Documentation / Naming

environment.rs:47: The doc comment says "skip nonce validation", but only the mismatch check is actually skipped — NonceIsMax is not. A more precise description:

/// When true, skip the nonce mismatch check (sender nonce vs tx nonce).
/// Used by `eth_call`/`eth_estimateGas` simulations where the caller
/// typically omits the nonce (defaulting to 0).

Coverage gap

There are no tests added for this change. A test that calls simulate_tx_from_generic (or eth_call) with a sender whose account nonce is > 0 and whose GenericTransaction.nonce is None/0 would directly reproduce the issue from #6312 and protect against regressions.

Minor

The disable_nonce_check field follows the disable_balance_check pattern structurally, but there is an asymmetry worth noting:

  • disable_balance_check gates an early-return that short-circuits the entire deduction path (deduct_caller line 531).
  • disable_nonce_check only guards the mismatch comparison, not the preceding increment_account_nonce call.

This asymmetry is a documentation/comment concern rather than a bug, but it could confuse future readers who assume the two flags behave analogously.

Summary

The core change is correct and the default false value ensures nonce checks remain active for real transaction processing. The two actionable items are:


Automated review by Claude (Anthropic) · custom prompt

@github-actions
Copy link

github-actions bot commented Mar 5, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 3.267 ± 0.032 3.235 3.324 1.01 ± 0.01
main_levm_BubbleSort 3.427 ± 0.144 3.344 3.779 1.05 ± 0.05
pr_revm_BubbleSort 3.250 ± 0.032 3.203 3.303 1.00
pr_levm_BubbleSort 3.391 ± 0.079 3.343 3.592 1.04 ± 0.03

Benchmark Results: ERC20Approval

Command Mean [s] Min [s] Max [s] Relative
main_revm_ERC20Approval 1.056 ± 0.012 1.048 1.089 1.00 ± 0.02
main_levm_ERC20Approval 1.253 ± 0.037 1.236 1.357 1.19 ± 0.04
pr_revm_ERC20Approval 1.054 ± 0.015 1.038 1.085 1.00
pr_levm_ERC20Approval 1.259 ± 0.039 1.234 1.366 1.19 ± 0.04

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 141.1 ± 1.0 140.1 143.5 1.01 ± 0.01
main_levm_ERC20Mint 192.3 ± 3.4 189.8 200.7 1.37 ± 0.03
pr_revm_ERC20Mint 140.0 ± 1.8 138.7 144.8 1.00
pr_levm_ERC20Mint 201.5 ± 12.2 191.4 225.3 1.44 ± 0.09

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 248.5 ± 1.1 246.8 249.8 1.00
main_levm_ERC20Transfer 323.7 ± 13.2 314.6 360.0 1.30 ± 0.05
pr_revm_ERC20Transfer 249.0 ± 3.2 244.7 255.6 1.00 ± 0.01
pr_levm_ERC20Transfer 324.0 ± 2.9 319.4 327.1 1.30 ± 0.01

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 240.6 ± 5.9 232.1 248.3 1.00
main_levm_Factorial 385.2 ± 2.7 381.6 387.8 1.60 ± 0.04
pr_revm_Factorial 240.7 ± 5.1 231.5 249.2 1.00 ± 0.03
pr_levm_Factorial 393.6 ± 24.5 381.5 462.6 1.64 ± 0.11

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.778 ± 0.052 1.706 1.852 1.00
main_levm_FactorialRecursive 11.827 ± 0.048 11.751 11.880 6.65 ± 0.20
pr_revm_FactorialRecursive 1.782 ± 0.048 1.671 1.829 1.00 ± 0.04
pr_levm_FactorialRecursive 11.848 ± 0.096 11.723 12.041 6.66 ± 0.20

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 237.4 ± 5.2 228.2 243.9 1.02 ± 0.03
main_levm_Fibonacci 393.2 ± 42.4 370.5 479.7 1.68 ± 0.19
pr_revm_Fibonacci 233.7 ± 5.3 228.0 242.4 1.00
pr_levm_Fibonacci 373.4 ± 1.7 370.4 374.7 1.60 ± 0.04

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 913.9 ± 12.0 898.0 937.5 1.00
main_levm_FibonacciRecursive 1030.3 ± 41.1 1003.0 1142.6 1.13 ± 0.05
pr_revm_FibonacciRecursive 920.5 ± 11.0 902.4 938.5 1.01 ± 0.02
pr_levm_FibonacciRecursive 1018.3 ± 5.6 1011.2 1032.0 1.11 ± 0.02

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 9.2 ± 0.1 9.1 9.4 1.00 ± 0.02
main_levm_ManyHashes 12.0 ± 0.1 11.9 12.2 1.30 ± 0.02
pr_revm_ManyHashes 9.2 ± 0.1 9.1 9.5 1.00
pr_levm_ManyHashes 12.1 ± 0.4 11.8 13.0 1.31 ± 0.04

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 284.0 ± 2.9 280.2 289.1 1.00
main_levm_MstoreBench 446.6 ± 30.0 431.9 530.7 1.57 ± 0.11
pr_revm_MstoreBench 285.2 ± 3.0 280.4 289.2 1.00 ± 0.01
pr_levm_MstoreBench 435.5 ± 6.1 429.4 449.9 1.53 ± 0.03

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 319.9 ± 3.2 314.0 324.8 1.00
main_levm_Push 425.6 ± 42.7 395.2 508.2 1.33 ± 0.13
pr_revm_Push 322.7 ± 5.4 315.8 330.9 1.01 ± 0.02
pr_levm_Push 407.5 ± 7.4 395.2 421.0 1.27 ± 0.03

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 182.9 ± 7.7 177.3 201.2 1.46 ± 0.06
main_levm_SstoreBench_no_opt 125.2 ± 0.4 124.7 125.9 1.00
pr_revm_SstoreBench_no_opt 175.8 ± 1.6 174.1 179.9 1.40 ± 0.01
pr_levm_SstoreBench_no_opt 125.3 ± 0.9 124.6 127.9 1.00 ± 0.01

pub disable_balance_check: bool,
/// When true, skip nonce validation. Used by `eth_call` simulations
/// where the caller typically doesn't provide a nonce.
pub disable_nonce_check: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding the field here is correct, but since Environment doesn't use ..Default::default() at all construction sites, this will fail to compile. At minimum these files need disable_nonce_check: false added:

  • tooling/ef_tests/state_v2/src/modules/runner.rs (~line 145)
  • tooling/ef_tests/state/runner/levm_runner.rs (~line 225)
  • test/tests/levm/eip7708_tests.rs (~line 203)

@github-project-automation github-project-automation bot moved this to In Progress in ethrex_l1 Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

levm Lambda EVM implementation

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

eth_call validates nonce and fails when nonce is not provided and nonce(sender) > 0

3 participants