Skip to content

feat(l1): implement eth_simulateV1 RPC method#6951

Draft
ilitteri wants to merge 8 commits into
feat/state-override-setfrom
feat/eth-simulate-v1
Draft

feat(l1): implement eth_simulateV1 RPC method#6951
ilitteri wants to merge 8 commits into
feat/state-override-setfrom
feat/eth-simulate-v1

Conversation

@ilitteri

@ilitteri ilitteri commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Motivation

eth_simulateV1 is the multi-block successor to eth_call batching (execution-apis ethSimulate); wallets, simulators and MEV tooling increasingly depend on it. Builds on the State/Block Override Sets from #6660 (this PR is stacked on that branch).

Description

Simulates up to 256 chained blocks on top of a base block without committing anything to the store:

  • Per-block overrides and calls: each blockStateCalls entry carries block overrides (number/time/gasLimit/feeRecipient/prevRandao/baseFeePerGas/blobBaseFee/difficulty/withdrawals), state overrides (reusing feat(l1,l2): add State Override Set and Block Override Set to RPC simulation methods #6660's StateOverrideSet), and a list of call objects executed as real transactions (variant chosen by fields, EIP-1559 default; auto-filled nonces from the live simulated state; remaining-block-gas defaults; zeroed signatures with senders carried out-of-band).
  • State chaining: a cumulative SimulationOverlay is the single source of truth for both reads (SimulationVmDatabase) and per-block state roots (re-applying the cumulative account updates onto the base block's trie), so block N+1 observes block N's execution results. BLOCKHASH resolves real ancestors and previously simulated blocks; EIP-4788/EIP-2935 system calls run per simulated block; EIP-7685 requests are extracted per block.
  • Modes: validation: true enforces nonces and a real EIP-1559 base fee; the default behaves like eth_call (zero base fee, relaxed checks). traceTransfers emits synthetic ERC-20 Transfer logs from 0xeeee…eeee for every ETH transfer, kept out of receipts and the logs bloom. returnFullTransactions switches between tx hashes and full objects.
  • Responses: full block objects matching eth_getBlockByHash output with real state/transactions/receipts roots, extended with per-call results (status, returnData, gasUsed, maxUsedGas, logs with per-block logIndex and blockTimestamp, error with revert data). Request-level failures use the spec error-code table (-38010..-38026); tx validation failures abort the request while reverts/halts are per-call results with the tx included in the block, matching geth.
  • Semantics verified against geth master internal/ethapi/simulate.go and the execution-apis eth_simulateV1 test corpus (91 .io files); ~11 scenarios are ported as integration tests against the in-memory store. A 50M cumulative gas budget (geth's RPCGasCap default) bounds the request.

Known gaps, documented in docs/roadmaps/forks-roadmap.md: movePrecompileToAddress is parsed but not yet enforced (follow-up); Amsterdam EIP-8037 2D gas / EIP-7928 BAL headers are not modeled; maxUsedGas reports pre-refund gas rather than geth's in-flight peak. The hive rpc-compat CI pin predates the eth_simulateV1 corpus (newer fixtures use a pre-merge genesis), so CI does not exercise the method yet — conformance is covered by the ported scenarios.

Also fixes test compilation on the base branch (Code::bytecode became private after the merge with main; the vm.rs test helpers now use the public accessors).

How to Test

cargo test -p ethrex-rpc --lib simulate
cargo test -p ethrex-blockchain --lib simulate

Or against a live node:

ethrex --dev --datadir devdata &
curl -s -X POST http://localhost:8545 -H 'Content-Type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"eth_simulateV1",
  "params":[{
    "traceTransfers": true,
    "blockStateCalls": [
      {"stateOverrides": {"0xc000000000000000000000000000000000000000": {"balance": "0xde0b6b3a7640000"}},
       "calls": [{"from": "0xc000000000000000000000000000000000000000", "to": "0xc100000000000000000000000000000000000000", "value": "0x7b"}]},
      {"blockOverrides": {"feeRecipient": "0x000000000000000000000000000000000000beef"},
       "calls": [{"from": "0xc100000000000000000000000000000000000000", "to": "0xc200000000000000000000000000000000000000", "value": "0x1"}]}
    ]
  }, "latest"]
}'

Expected: two simulated blocks chained by parentHash, both calls "status": "0x1", a Transfer log from 0xeeee…eeee per call, and the second block's miner set to 0x…beef.

Checklist

  • Updated STORE_SCHEMA_VERSION (crates/storage/lib.rs) if the PR includes breaking changes to the Store requiring a re-sync. — N/A, no Store schema changes.

Closes #6212

ilitteri added 7 commits July 2, 2026 10:31
…e ETH-transfer logs.

Environment gains disable_nonce_check (eth_simulateV1 with validation=false
behaves like eth_call; the account nonce still increments), disable_eoa_check
(simulations allow contract senders; the EIP-3607 check is skipped
unconditionally in simulation contexts), and trace_eth_transfers (emit
informational ETH-transfer logs from the execution-apis sentinel address
0xeeee...eeee when tracing pre-Amsterdam).

The five EIP-7708 transfer-log emission sites now route through
VM::eth_transfer_log_address, which encodes the precedence: Amsterdam+
consensus logs from SYSTEM_ADDRESS (self-transfers excluded per the EIP),
otherwise traceTransfers logs from TRACE_TRANSFER_ADDRESS (self-transfers
included; CALLCODE excluded since its value never leaves the caller).
create_eth_transfer_log takes the emitter address explicitly. Pre-Cancun
selfdestructs gain a trace-only emission site.

Consensus behavior is unchanged: all sites still emit from SYSTEM_ADDRESS
with identical conditions on Amsterdam+.
…ulateV1.

Evm::execute_tx_simulate mirrors execute_tx but returns SimulationTxError,
which keeps TxValidationError variants structured instead of stringifying
them through From<VMError> for EvmError — the RPC layer needs the variant to
map nonce/basefee/funds/intrinsic-gas failures to their spec error codes and
abort the whole request (validation failures are request-level errors in
eth_simulateV1, unlike reverts which are per-call results). It applies the
simulation relaxations: EOA-sender check always skipped, nonce check skipped
unless validation:true, unpriced blob fees zeroed, traceTransfers flag.

setup_env/setup_env_with_config now return VMError (callers auto-convert), so
the base-fee-too-low validation raised while computing the effective gas
price survives structurally. Evm::get_account_nonce reads through the live
EVM cache for per-block nonce auto-fill. SimTxConfig, SimulationTxError,
TxValidationError, TxResult, ExecutionReport and TRACE_TRANSFER_ADDRESS are
re-exported from ethrex_vm for the blockchain-crate engine.
…te chaining.

The overlay accumulates every change since the base block (state overrides
materialized as AccountUpdates plus each simulated block's execution results)
and is the single source of truth for both reads and per-block state roots.
SimulationVmDatabase serves reads overlay-first with fallback to a
StoreVmDatabase opened at the base block, which is how simulated block N+1
observes block N's results without committing anything. BLOCKHASH resolves
real ancestors through the inner database and previously simulated blocks
from the overlay's hash map.

The overlay needs its own merge (AccountUpdate::merge is not enough):
an incoming removal voids everything accumulated for the address, and
re-creation over a tombstone folds into removed_storage + info because the
trie application (apply_account_updates_from_trie_batch) short-circuits on
removed and would drop the new account info.

Also fixes test compilation on the base branch: Code::bytecode became
private after the merge with main, so the mock database and the overlay
tests now use the public accessors. The shared test mock moved to a
test_mock_db module reused by both wrapper test suites.
Simulates a chain of up to 256 blocks on top of a base block without
committing anything, mirroring geth's simulator semantics (verified against
internal/ethapi/simulate.go and the execution-apis eth_simulateV1 test
corpus):

- sanitize_blocks resolves block numbers/timestamps (defaults parent+1 and
  parent+12), validates strict ordering, fills numbering gaps with empty
  blocks that are part of the response, and bounds the simulated height at
  base+256.
- Headers inherit coinbase/gasLimit from the (post-override) parent;
  prevRandao/difficulty default to zero; baseFeePerGas is EIP-1559-derived
  only with validation:true and zero otherwise, so zero-priced calls run
  like eth_call; the blob-fee market rolls forward in both modes.
- Calls become real transactions (variant chosen by fields, EIP-1559 by
  default) with zeroed signatures, auto-filled nonces read from the live
  simulated state and remaining-block-gas defaults; senders travel
  out-of-band. An explicit gas above the remaining block gas aborts with
  the block-gas-limit error; a 50M cumulative request budget (geth's
  RPCGasCap default) clamps call gas otherwise.
- Tx validation failures abort the whole request (structured, for spec
  error codes); reverts/halts are per-call results and the tx stays in the
  block, with revert data separated from returnData.
- Pre-block system calls (EIP-4788/EIP-2935) run for every simulated block
  including gap-filled ones, which is also what makes BLOCKHASH observable
  in-EVM; EIP-7685 requests are extracted per block; withdrawals overrides
  credit balances and land in the body/withdrawalsRoot.
- State chains through SimulationOverlay; per-block state roots re-apply
  the cumulative updates onto the base block's trie (storage tries must
  open at committed roots). traceTransfers sentinel logs stay out of
  receipts and the logs bloom but are kept in per-call results.

Not modeled yet (documented): EIP-8037 2D gas accounting and EIP-7928 BAL
headers on Amsterdam chains, and maxUsedGas reports pre-refund gas rather
than geth's in-flight peak.
EthSimulateRequest parses the {blockStateCalls, traceTransfers, validation,
returnFullTransactions} payload (strict: unknown keys and malformed payloads
map to -32602, which ethrex's generic BadParams would report as -32000) with
an optional block parameter defaulting to latest, converts overrides into
engine types, and runs Blockchain::simulate_v1 inside spawn_blocking with a
5s wall-clock cap, following the debug_traceCall scaffolding.

Each result block serializes exactly like eth_getBlockByHash (RpcBlock is
flattened; returnFullTransactions switches the untagged body representation)
extended with calls: status/returnData/gasUsed/maxUsedGas plus logs carrying
per-block logIndex, tx hash/index, block hash/number and blockTimestamp —
maxUsedGas and blockTimestamp are required by the execution-apis response
schema. Failed calls keep logs: [] and carry revert data in error.data with
code 3 (-32015 for other halts). Request-level failures map to the spec
error-code table (-38010..-38026) through the new code-agnostic
RpcErr::EthSimulate variant.

BlockOverrideSet gains the eth_simulateV1 field spellings as serde aliases
(feeRecipient/prevRandao/blobBaseFee) — the derived Deserialize silently
dropped them before — plus the spec's withdrawals list, and exposes the
blobBaseFee inversion so the engine receives excess_blob_gas directly.
Ports eleven scenarios from the execution-apis eth_simulateV1 test corpus to
the in-memory fixtures/genesis/l1.json chain, asserting response JSON paths
(the hive fixtures use a pre-merge genesis ethrex cannot import, which is
also why the CI rpc-compat pin predates the corpus): simple transfer with a
balance override, empty block entry, block-number order (-38020), timestamp
order (-38021), explicit gas above block limit (-38015), missing base fee
under validation (-38012), insufficient funds (-38014), traceTransfers
synthetic log shape (address/topics/data/blockTimestamp, excluded from the
logs bloom), cross-block state carry-over with chained parent hashes, gap
blocks in the response with 12s timestamp steps, full transaction objects,
and block overrides reflected in the result header.

RpcTransaction::build_with_sender constructs full transaction objects from
the engine's out-of-band senders, since simulated transactions carry zeroed
signatures that cannot be recovered.
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Stateless blockchain EF-tests (zkevm bundle) skipped under Amsterdam v6.1.0

make -C tooling/ef_tests/blockchain test-stateless runs against the
tests-zkevm@v0.4.1 fixture bundle — currently the only published zkevm test
release. Those fixtures were filled against an older glamsterdam devnet and
re-execute every case under the for_amsterdam fork, so they lag the
glamsterdam-devnet v6.1.0 gas accounting this client now implements
(EIP-8037 / EIP-8038 / EIP-2780 / EIP-7976 / EIP-7981 …). Re-executing them
yields ~2790/2864 stale-gas failures ("Transaction execution unexpectedly
failed"), spread pervasively across every fork and through the EIP-8025 proof
suite, so there is no clean passing subset to keep.

Until a v6.1.0-aligned zkevm bundle is published, the entire bundle is skipped
for the stateless run via the fork_Amsterdam entry in the stateless-only
EXTRA_SKIPS (tooling/ef_tests/blockchain/tests/all.rs) — every test in this
Amsterdam-only bundle carries the [fork_Amsterdam-…] parametrization in its
test key. The skip is #[cfg(feature = "stateless")], so it does not touch
the non-stateless test-levm run. Coverage of these EIPs is retained by
test-levm, the engine EF-tests, and the state EF-tests, all of which execute
against the live v6.1.0 fixtures and pass.

Re-enable by removing the "fork_Amsterdam" skip once .fixtures_url_zkevm
points at a zkevm bundle filled for glamsterdam-devnet v6.1.0.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 2068
Total lines removed: 0
Total lines changed: 2068

Detailed view
+------------------------------------------------------+-------+------+
| File                                                 | Lines | Diff |
+------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/blockchain.rs               | 2954  | +1   |
+------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/simulate.rs                 | 692   | +692 |
+------------------------------------------------------+-------+------+
| ethrex/crates/blockchain/vm.rs                       | 895   | +311 |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/eth/mod.rs              | 12    | +1   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/eth/simulate.rs         | 881   | +881 |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/rpc.rs                  | 1384  | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/types/block_override.rs | 179   | +20  |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/types/transaction.rs    | 119   | +17  |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/utils.rs                | 363   | +15  |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs                | 2707  | +28  |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/backends/mod.rs                     | 372   | +35  |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/errors.rs                           | 65    | +22  |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/constants.rs               | 66    | +1   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/environment.rs             | 101   | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/execution_handlers.rs      | 183   | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/hooks/default_hook.rs      | 519   | +2   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/opcode_handlers/system.rs  | 1118  | +7   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/utils.rs                   | 744   | +4   |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/vm.rs                      | 1128  | +17  |
+------------------------------------------------------+-------+------+
| ethrex/crates/vm/lib.rs                              | 21    | +5   |
+------------------------------------------------------+-------+------+

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

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.018 ± 0.016 2.990 3.042 1.16 ± 0.01
main_levm_BubbleSort 2.593 ± 0.015 2.572 2.619 1.00 ± 0.01
pr_revm_BubbleSort 2.999 ± 0.040 2.967 3.096 1.16 ± 0.02
pr_levm_BubbleSort 2.592 ± 0.006 2.581 2.601 1.00

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 989.4 ± 7.1 982.9 1008.1 1.02 ± 0.01
main_levm_ERC20Approval 969.7 ± 8.2 960.9 990.1 1.00
pr_revm_ERC20Approval 1005.3 ± 13.1 990.8 1030.9 1.04 ± 0.02
pr_levm_ERC20Approval 981.5 ± 24.6 968.7 1050.2 1.01 ± 0.03

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 136.4 ± 5.4 133.9 151.6 1.00
main_levm_ERC20Mint 149.5 ± 0.9 148.4 151.2 1.10 ± 0.04
pr_revm_ERC20Mint 138.0 ± 2.1 136.7 143.2 1.01 ± 0.04
pr_levm_ERC20Mint 149.8 ± 0.8 148.7 151.2 1.10 ± 0.04

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 237.6 ± 2.7 234.1 242.0 1.00 ± 0.01
main_levm_ERC20Transfer 242.4 ± 2.8 239.3 247.0 1.02 ± 0.01
pr_revm_ERC20Transfer 237.6 ± 1.0 236.6 239.3 1.00
pr_levm_ERC20Transfer 240.5 ± 1.2 238.8 243.1 1.01 ± 0.01

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 226.3 ± 2.7 224.0 233.5 1.00
main_levm_Factorial 259.5 ± 7.9 254.0 280.7 1.15 ± 0.04
pr_revm_Factorial 227.8 ± 1.3 226.2 230.2 1.01 ± 0.01
pr_levm_Factorial 256.1 ± 1.2 254.7 258.4 1.13 ± 0.01

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.734 ± 0.042 1.654 1.791 1.01 ± 0.03
main_levm_FactorialRecursive 9.436 ± 0.088 9.305 9.592 5.51 ± 0.08
pr_revm_FactorialRecursive 1.712 ± 0.021 1.683 1.737 1.00
pr_levm_FactorialRecursive 9.384 ± 0.032 9.342 9.448 5.48 ± 0.07

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 203.4 ± 3.9 198.8 213.4 1.00
main_levm_Fibonacci 218.6 ± 4.5 213.5 225.9 1.07 ± 0.03
pr_revm_Fibonacci 206.8 ± 0.7 205.6 208.0 1.02 ± 0.02
pr_levm_Fibonacci 217.8 ± 4.0 214.8 225.9 1.07 ± 0.03

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 908.9 ± 9.5 890.6 923.5 1.28 ± 0.01
main_levm_FibonacciRecursive 709.2 ± 2.7 706.1 713.3 1.00
pr_revm_FibonacciRecursive 906.8 ± 16.5 892.4 946.5 1.28 ± 0.02
pr_levm_FibonacciRecursive 718.2 ± 10.6 704.4 735.1 1.01 ± 0.02

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.8 ± 0.1 8.7 8.9 1.00
main_levm_ManyHashes 9.4 ± 0.1 9.3 9.6 1.08 ± 0.02
pr_revm_ManyHashes 8.8 ± 0.1 8.6 9.1 1.00 ± 0.02
pr_levm_ManyHashes 9.3 ± 0.4 9.1 10.3 1.06 ± 0.05

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 259.2 ± 7.6 255.7 280.7 1.37 ± 0.04
main_levm_MstoreBench 195.3 ± 3.1 192.3 201.0 1.03 ± 0.02
pr_revm_MstoreBench 283.3 ± 80.7 255.1 512.5 1.50 ± 0.43
pr_levm_MstoreBench 189.2 ± 1.0 187.9 190.8 1.00

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 291.4 ± 4.2 289.1 303.4 1.22 ± 0.02
main_levm_Push 240.2 ± 1.4 238.6 242.4 1.01 ± 0.01
pr_revm_Push 291.6 ± 1.7 289.5 294.3 1.22 ± 0.01
pr_levm_Push 238.4 ± 1.3 237.0 240.9 1.00

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 167.4 ± 2.2 162.5 169.3 1.69 ± 0.02
main_levm_SstoreBench_no_opt 99.3 ± 0.6 98.7 100.8 1.00
pr_revm_SstoreBench_no_opt 169.4 ± 6.2 162.5 184.1 1.71 ± 0.06
pr_levm_SstoreBench_no_opt 99.9 ± 3.2 98.8 109.1 1.01 ± 0.03

Some simulators (anvil among them) skip nonce enforcement even with
validation: true, so a nonce-gapped call passes and only reverts get caught.
ethrex follows geth: with validation on, an explicit nonce above the account
nonce aborts the request with -38011 and a stale nonce with -38010, while
the same gapped call without validation succeeds (the reference corpus'
transaction-too-high-nonce case — explicit nonces are ignored there, like
eth_call). The tests pin all four behaviors, with fees covering the real
base fee that validation mode derives so the nonce check is the one that
fires.

Known precedence divergence, deliberately untouched: when a call violates
both fees/balance and nonce, ethrex's pre-execution hook reports the fee or
balance error first while geth checks the nonce first; the hook order is
shared with consensus execution and is not worth forking for an error-code
tiebreak.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant