Skip to content

feat: replay preceding block txs for accurate gas estimation#96

Merged
rubydusa merged 21 commits into
mainfrom
RonTuretzky/issue-95-spec
May 5, 2026
Merged

feat: replay preceding block txs for accurate gas estimation#96
rubydusa merged 21 commits into
mainfrom
RonTuretzky/issue-95-spec

Conversation

@RonTuretzky
Copy link
Copy Markdown
Contributor

Summary

Closes #95

  • Adds PrecedingTx struct and replay_preceding_transactions() to the gas-estimator crate — replays transactions via revm's transact_commit to advance CacheDB state
  • Adds get_preceding_transactions() to the rpc crate — fetches block with full tx objects via a single eth_getBlockByNumber(N, true) call, converts to PrecedingTx
  • Adds estimate_state_changes_gas_with_preceding() to GasKillerEvmSketchDefault — creates one CacheDB, replays preceding txs, then runs gas estimation on the same DB
  • Wires it into the CLI: extracts transaction_index from receipt, fetches preceding txs when index > 0, gracefully falls back to current behavior on RPC failure

Decisions (from spec):

  • Single RPC call (eth_getBlockByNumber with full tx objects) for fetching preceding txs
  • Direct revm replay (not debug_traceCall per preceding tx)
  • EIP-4788 beacon root handling is out of scope
  • WASM path unchanged (no RPC access)

Test plan

  • Test with a first-in-block transaction (should skip replay, no behavior change)
  • Test with a mid-block transaction (should replay preceding txs and show improved accuracy)
  • Test with --anvil flag (Anvil path is unchanged, no regression)
  • Verify graceful fallback when RPC doesn't support eth_getBlockByNumber

…#95)

When analyzing a transaction that isn't the first in its block, the gas
estimation previously used state from block N-1, ignoring state changes
from preceding transactions. This adds replay of preceding transactions
via revm's transact_commit before running gas estimation, ensuring the
simulated state matches the actual on-chain state at execution time.
Copy link
Copy Markdown

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

Adds mid-block state awareness to gas estimation by replaying transactions that precede the target transaction within the same block, improving estimation accuracy for state-dependent executions.

Changes:

  • Introduces a PrecedingTx representation and replay_preceding_transactions() in gas-estimator to advance a shared CacheDB via revm replay.
  • Adds get_preceding_transactions() in rpc to fetch a block with full transactions and convert 0..tx_index into replayable transactions.
  • Integrates preceding-tx replay into the EvmSketch estimator path and CLI (with graceful fallback on RPC failure).

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
crates/rpc/src/lib.rs Fetches full block txs via eth_getBlockByNumber(..., true) and converts preceding txs into PrecedingTx.
crates/rpc/Cargo.toml Adds dependencies needed for block tx fetching/conversion (gas-analyzer-estimator, alloy-rpc-types, revm).
crates/gas-estimator/src/lib.rs Adds PrecedingTx and a revm-based replay routine that commits preceding tx state into CacheDB.
crates/evmsketch/src/lib.rs Adds estimate_state_changes_gas_with_preceding() to replay into the same CacheDB before estimating.
crates/cli/src/main.rs Wires receipt transaction_index -> preceding tx fetch -> replayed estimation, with fallback to prior behavior.
Cargo.lock Updates lockfile for added dependencies/versions.

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

Comment thread crates/rpc/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/evmsketch/src/lib.rs Outdated
Comment thread crates/cli/src/main.rs Outdated
Comment thread crates/evmsketch/src/lib.rs Outdated
Comment thread crates/rpc/src/lib.rs Outdated
Foundry v1.7.0 (released 2026-04-28) bumped revm-inspectors to v0.39.0,
which since v0.38.1 emits StructLog.memory entries as 0x-prefixed hex.
The previous parser fed entries straight into u8::from_str_radix and
panicked on the 'x' with InvalidDigit. Strip an optional 0x per entry
so geth/erigon, old Anvil, and new Anvil all work.
@rubydusa
Copy link
Copy Markdown
Collaborator

CI failure diagnosis & fix

The test-native failure on this PR is an external regression, not caused by anything on this branch. Pushed c8e4495 to fix it.

Symptom

test tests::test_compute_state_update_simulate_call ... FAILED

thread 'tests::test_compute_state_update_simulate_call' panicked at crates/core/src/trace.rs:38:45:
invalid hex: ParseIntError { kind: InvalidDigit }

Why it's not the PR's fault

git diff main..HEAD --stat shows the PR touches cli, evmsketch, gas-estimator, rpc, and wasmnot crates/core/src/trace.rs or crates/anvil/. The failing test (test_compute_state_update_simulate_call) and the panicking parser (parse_trace_memory) are both untouched by this branch.

Branch CI history confirms this:

Date Foundry stable at the time Result
2026-04-02 (last green on this branch, after merging main) v1.5.1
2026-04-06 (last green on main) v1.5.1
2026-04-29 (this PR's CI runs) v1.7.0 (released 2026-04-28)

The CI workflow installs Foundry via foundry-rs/foundry-toolchain@v1, which always pulls the latest stable. The toolchain rolled from 1.5.1 → 1.7.0 between green and red, with no code change on our side.

Root cause

Foundry v1.7.0 bumped revm-inspectors from 0.33.00.39.0. Since revm-inspectors v0.38.1, PR #425 ("align memory encoding and returnData gating") changed convert_memory():

-        memory.push(hex::encode(chunk));
+        memory.push(hex::encode_prefixed(chunk));

So Anvil's debug_traceTransaction StructLog.memory entries now look like:

  • Old: "0000000000000000000000000000000000000000000000000000000000000001" (64 hex chars, bare)
  • New: "0x0000000000000000000000000000000000000000000000000000000000000001" (66 chars, prefixed)

The motivation for the upstream change is alignment with the opcode tracer spec (execution-apis #762).

Our parse_trace_memory (crates/core/src/trace.rs:35) joined the entries and parsed every 2 chars as a hex byte. With the new format, the second chunk is \"0x\", and u8::from_str_radix(\"0x\", 16) returns InvalidDigit on the 'x' — exactly the panic in the log. Geth/erigon and pre-1.7 Anvil still emit bare hex, so it's a forward-incompatibility, not a bug we introduced.

This affects only the Anvil backend; the EvmSketch path doesn't go through Anvil's structlogger, which is why test-native's first step (cargo test, default = EvmSketch) passed and only the second step (cargo test --features anvil) failed.

The fix (c8e4495)

In parse_trace_memory, strip an optional 0x per entry before joining:

pub fn parse_trace_memory(memory: Vec<String>) -> Vec<u8> {
    memory
        .iter()
        .map(|s| s.strip_prefix(\"0x\").unwrap_or(s))
        .collect::<String>()
        .chars()
        .collect::<Vec<char>>()
        .chunks(2)
        .map(|c| c.iter().collect::<String>())
        .map(|s| u8::from_str_radix(&s, 16).expect(\"invalid hex\"))
        .collect::<Vec<u8>>()
}

This is backward-compatible: bare hex passes through strip_prefix unchanged, and prefixed hex gets the prefix removed. Both old and new Anvil, plus geth/erigon, now work.

I also added a #[cfg(test)] unit test in crates/core/src/trace.rs covering bare, prefixed, and mixed inputs. This locks in both formats so the same regression — or anything similar — gets caught in core without needing a live RPC + Anvil round-trip.

Verification

  • cargo test -p gas-analyzer-core ✅ (new unit test passes)
  • cargo clippy --workspace --exclude gas-killer-wasm --exclude gas-analyzer-anvil --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • The original test_compute_state_update_simulate_call integration test could not be reproduced locally (my Anvil is v1.5.1, pre-regression), but it goes through parse_trace_memory which now handles both formats. CI will confirm.

Optional follow-up (not in this PR)

foundry-rs/foundry-toolchain@v1 floats to latest stable, so any future Foundry release can break CI the same way. If we want determinism, pin the version:

- uses: foundry-rs/foundry-toolchain@v1
  with:
    version: stable  # or a specific tag like v1.7.0

Happy to do that as a separate PR if desired.

🤖 Generated with Claude Code

The Rust SimEnvOpts struct gained `basefee` to fix replay-time BASEFEE
opcode values, but the Solidity fixture used to verify env propagation
(SimEnvOptsStructs.SimEnvOpts) didn't have a corresponding field, so a
regression in basefee wiring would not be caught by the env tests.

Add `blockBaseFee` to the Solidity struct, both constructors, and the
env-mismatch check, regenerate the abis/, and add a wrong-basefee test
mirroring the existing wrong-timestamp one. Existing tests now run with
a non-zero basefee so the new field is actually exercised.
…spec

# Conflicts:
#	crates/gas-estimator/src/lib.rs
`estimate_state_changes_gas_with_preceding` was reading storage from
`sketch.rpc_db`, which is anchored at the tx's own block number. Per
JSON-RPC semantics, that returns state at the *end* of block N — after
every tx in the block, including the one being analyzed, has executed.

For txs whose state updates re-invoke functions that consume in-block
nonces (EIP-2612 USDC permit, etc.), this caused the simulated call to
revert because the recovered signer no longer matched `owner`: the
signature was valid for nonce N but the simulator saw nonce N+1.
The CLI then silently fell back to the heuristic estimator.

Switch to a `SimpleRpcDb` anchored at `block_number - 1` so storage
reads observe pre-tx state. `replay_preceding_transactions` then
brings the DB to the correct mid-block state before estimation. This
matches the no-preceding path on line 253 which already uses
`anchor_block_number().saturating_sub(1)`. `sim_env` continues to
use the real block's header for basefee/timestamp/coinbase/etc.
Reframe the comment so it explains the general property — RPC reads at
`block_number` see post-block state, and replaying preceding txs on top
of that double-applies their effects — and treat EIP-2612 permit as one
example of how the breakage surfaces, rather than the cause.
revm's `Context::mainnet()` defaults to `SpecId::PRAGUE`. Mainnet has
since activated Osaka (Fusaka), and contracts deployed at or after that
fork can use opcodes that PRAGUE doesn't recognize. When revm hits one
under the wrong spec it halts the frame with `InstructionResult::NotActivated`,
which propagates to the caller as "all forwarded gas consumed, 0 bytes
returndata". 0x's `AllowanceHolder.exec` interprets that signature as an
out-of-gas griefing attempt and re-emits it via the `INVALID` opcode,
producing an opaque empty revert in the outer estimator.

Concretely, repro tx 0x71c48ec2…b1ba16e03d hits this path: the 0x
settler calls Ekubo Core, whose log2 implementation uses inline-asm
`clz(...)` (EIP-7939, Osaka-only opcode 0x1E). Switching `cfg.spec` to
`SpecId::OSAKA` activates the CLZ handler, the call returns normally,
and the analyzer reports a measured estimate (2.79% savings) instead
of falling back to the heuristic.

Osaka also activates EIP-7825, which caps individual transactions at
2^24 gas regardless of the block gas limit. We were forwarding the
full block gas_limit (60M on this chain) into the simulated tx, which
revm now rejects with `TxGasLimitGreaterThanCap`. Clamp the tx-level
`gas_limit(...)` at every site (`estimate_gas_raw` and
`replay_preceding_transactions`) to `min(sim_env.gas_limit,
EIP7825_TX_GAS_CAP)`. Block-level `gas_limit` is left untouched.

Trade-off: hard-coding `OSAKA` is incorrect for historical txs from
blocks before Osaka activated. Acceptable for now because this tool is
run on recent txs; a follow-up should derive `SpecId` from the block
number.
Copy link
Copy Markdown

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

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.


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

Comment thread crates/cli/src/main.rs
Comment thread crates/evmsketch/src/lib.rs Outdated
rubydusa and others added 2 commits May 4, 2026 09:19
fix: anchor preceding-tx replay state to block N-1
The repo was renamed and transferred from BreadchainCoop/gas-killer-avs-sol
to gas-killer/solidity-sdk. Update the submodule URL and bump the pinned
SHA to upstream main. The diff between SHAs is natspec-only on
StateChangeHandlerLib.sol; ABI and deployed bytecode of the wrapper
contract are byte-identical (only the IPFS metadata hash changes).
Copy link
Copy Markdown
Contributor

@bagelface bagelface left a comment

Choose a reason for hiding this comment

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

Correctness review. Six issues below, ranging from latent bugs to cases that actively corrupt the mid-block CacheDB state used for gas estimation.

Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs
Comment thread crates/gas-estimator/src/lib.rs Outdated
cfg.disable_balance_check = true;
cfg.disable_base_fee = true;
cfg.disable_fee_charge = true;
cfg.spec = revm::primitives::hardfork::SpecId::OSAKA;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same SpecId::OSAKA problem as line 186, but with a more direct correctness consequence here. In the replay path, a preceding transaction from a pre-Osaka block may behave differently under Osaka semantics: it might revert when it originally succeeded, or succeed when it originally reverted, resulting in different storage writes being committed to the CacheDB. Any subsequent read from those slots — including by the analyzed transaction's simulation — will see wrong state.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same fix as the comment on line 186 — replay_preceding_transactions now uses sim_env.spec (derived from the anchored header against EthSpec::mainnet()) instead of a hardcoded OSAKA. See 3e0f025 and test_sim_env_spec_derivation_against_mainnet in crates/evmsketch/src/lib.rs.

Comment thread crates/gas-estimator/src/lib.rs Outdated
Comment thread crates/gas-estimator/src/lib.rs Outdated
///
/// If `preceding_txs` is empty (first-in-block), this behaves identically
/// to `estimate_state_changes_gas`.
pub fn estimate_state_changes_gas_with_preceding(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The public estimate_state_changes_gas_raw at line 142 was not updated by this PR. It still creates CacheDB::new(&self.sketch.rpc_db)BasicRpcDb anchored to block N — so eth_getStorageAt calls return post-block state. This is the exact bug PR #122 fixed for estimate_state_changes_gas (line 246) and this function. Any external caller of estimate_state_changes_gas_raw gets subtly wrong results. It should be updated to use SimpleRpcDb at block_number - 1, consistent with its sibling methods.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Addressed in 3e0f025. estimate_state_changes_gas_raw now constructs SimpleRpcDb { provider, block_number: anchor - 1 } and wraps it in a CacheDB, mirroring the fix already applied to its sibling estimate_state_changes_gas.

Tested by test_simple_rpc_db_queries_at_configured_block in crates/evmsketch/src/lib.rs. It uses a custom recording tower::Service that captures every JSON-RPC request, then constructs a SimpleRpcDb with block_number = 99 and asserts the resulting eth_getStorageAt call carries block tag "0x63" — so the underlying primitive cannot silently ignore its configured block.

Comment thread crates/rpc/src/lib.rs
Copy link
Copy Markdown
Contributor

@bagelface bagelface left a comment

Choose a reason for hiding this comment

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

see comments

@bagelface bagelface mentioned this pull request May 4, 2026
6 tasks
…as anchor

Addresses bagelface's review on PR #96.

- SimEnvOpts gains `spec: SpecId` and `difficulty: U256`. Both replace
  hardcoded values in `estimate_gas_raw` and `replay_preceding_transactions`,
  so historical (pre-Osaka, pre-Merge) blocks no longer get the wrong gas
  schedule, opcode set, or DIFFICULTY value.
- evmsketch's `sim_env()` derives `spec` via `alloy_evm::spec` against
  `EthSpec::mainnet()` and reads `difficulty` from the header.
- `PrecedingTx` gains `access_list` (EIP-2930) and `authorization_list`
  (EIP-7702); `get_preceding_transactions` populates both, and the replay
  TxEnv builder threads them through. Without these, replay can OOG mid-tx
  and silently drop storage writes from the CacheDB.
- The EIP-7825 per-tx cap is dropped from the replay path entirely (with
  `disable_fee_charge` it serves no correctness purpose) and gated on
  `spec >= OSAKA` in the analyzed-tx path.
- `estimate_state_changes_gas_raw` switched from `BasicRpcDb` (anchored
  at block N) to `SimpleRpcDb` at `block_number - 1`, mirroring the fix
  PR #122 already applied to its sibling.

The wasm path keeps `OSAKA` + zero difficulty since it runs against
EmptyDB with no real chain state.
rubydusa added a commit that referenced this pull request May 5, 2026
…as anchor

Addresses bagelface's review on PR #96.

- SimEnvOpts gains `spec: SpecId` and `difficulty: U256`. Both replace
  hardcoded values in `estimate_gas_raw` and `replay_preceding_transactions`,
  so historical (pre-Osaka, pre-Merge) blocks no longer get the wrong gas
  schedule, opcode set, or DIFFICULTY value.
- evmsketch's `sim_env()` derives `spec` via `alloy_evm::spec` against
  `EthSpec::mainnet()` and reads `difficulty` from the header.
- `PrecedingTx` gains `access_list` (EIP-2930) and `authorization_list`
  (EIP-7702); `get_preceding_transactions` populates both, and the replay
  TxEnv builder threads them through. Without these, replay can OOG mid-tx
  and silently drop storage writes from the CacheDB.
- The EIP-7825 per-tx cap is dropped from the replay path entirely (with
  `disable_fee_charge` it serves no correctness purpose) and gated on
  `spec >= OSAKA` in the analyzed-tx path.
- `estimate_state_changes_gas_raw` switched from `BasicRpcDb` (anchored
  at block N) to `SimpleRpcDb` at `block_number - 1`, mirroring the fix
  PR #122 already applied to its sibling.

The wasm path keeps `OSAKA` + zero difficulty since it runs against
EmptyDB with no real chain state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds regression tests for each issue flagged in bagelface's review.
Each test is written to fail under the pre-fix code so future regressions
surface immediately.

gas-estimator:
- effective_tx_gas_limit unit test (Issue 2): cap fires under Osaka,
  pre-Osaka specs left untouched.
- replay-no-cap test (Issue 3): a >16.7M-gas burner under Shanghai
  records gas_used > 2^24, proving the EIP-7825 cap is not applied
  to the replay path.
- difficulty propagation test (Issue 4): handwritten bytecode
  `DIFFICULTY PUSH1 0 SSTORE` under GRAY_GLACIER spec; storage slot 0
  must equal sim_env.difficulty.
- access_list test (Issue 5): same SLOAD tx replayed with vs without
  AccessList entry; gas_used delta of exactly 2300 confirms the entry
  is being threaded into the TxEnv (intrinsic 4300 minus warm savings 2000).
- authorization_list test (Issue 6): signed EIP-7702 authorization
  applied during replay; CacheDB account for the authority gains the
  23-byte 0xef0100 || target delegation indicator.
- replay_preceding_transactions now returns Vec<ExecutionResult> so
  tests can inspect per-tx gas_used / halt reasons.
- effective_tx_gas_limit extracted as a small helper so the cap-gating
  logic has a direct test surface.
- PrecedingTx derives Clone/Debug for test ergonomics.

evmsketch:
- spec derivation test (Issue 1): synthesized headers at known mainnet
  Berlin/London/Paris/Shanghai/Cancun heights map to the right SpecId
  via alloy_evm::spec(&EthSpec::mainnet(), header). Catches accidental
  chainspec swaps (e.g. to Sepolia).
- SimpleRpcDb mock-provider test (Issue 7): a custom recording
  tower::Service captures the JSON-RPC request issued by storage_ref
  and asserts the block tag matches the configured block_number, so
  the N-1 anchoring in estimate_state_changes_gas_raw cannot be
  defeated by the underlying primitive.
rubydusa added a commit that referenced this pull request May 5, 2026
Adds regression tests for each issue flagged in bagelface's review.
Each test is written to fail under the pre-fix code so future regressions
surface immediately.

gas-estimator:
- effective_tx_gas_limit unit test (Issue 2): cap fires under Osaka,
  pre-Osaka specs left untouched.
- replay-no-cap test (Issue 3): a >16.7M-gas burner under Shanghai
  records gas_used > 2^24, proving the EIP-7825 cap is not applied
  to the replay path.
- difficulty propagation test (Issue 4): handwritten bytecode
  `DIFFICULTY PUSH1 0 SSTORE` under GRAY_GLACIER spec; storage slot 0
  must equal sim_env.difficulty.
- access_list test (Issue 5): same SLOAD tx replayed with vs without
  AccessList entry; gas_used delta of exactly 2300 confirms the entry
  is being threaded into the TxEnv (intrinsic 4300 minus warm savings 2000).
- authorization_list test (Issue 6): signed EIP-7702 authorization
  applied during replay; CacheDB account for the authority gains the
  23-byte 0xef0100 || target delegation indicator.
- replay_preceding_transactions now returns Vec<ExecutionResult> so
  tests can inspect per-tx gas_used / halt reasons.
- effective_tx_gas_limit extracted as a small helper so the cap-gating
  logic has a direct test surface.
- PrecedingTx derives Clone/Debug for test ergonomics.

evmsketch:
- spec derivation test (Issue 1): synthesized headers at known mainnet
  Berlin/London/Paris/Shanghai/Cancun heights map to the right SpecId
  via alloy_evm::spec(&EthSpec::mainnet(), header). Catches accidental
  chainspec swaps (e.g. to Sepolia).
- SimpleRpcDb mock-provider test (Issue 7): a custom recording
  tower::Service captures the JSON-RPC request issued by storage_ref
  and asserts the block tag matches the configured block_number, so
  the N-1 anchoring in estimate_state_changes_gas_raw cannot be
  defeated by the underlying primitive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rubydusa added 2 commits May 5, 2026 11:51
The previous docstrings led with 'Issue N — ...' references to a code
review that future readers won't have context on. Replaced each with a
self-contained statement of the invariant or property the test enforces.
@rubydusa rubydusa requested a review from bagelface May 5, 2026 16:43
rubydusa and others added 2 commits May 5, 2026 12:50
chore(submodule): retarget gas-killer-avs-sol to renamed solidity-sdk
Comment thread crates/evmsketch/src/lib.rs Outdated
/// value (zero post-Merge).
pub fn sim_env(&self) -> SimEnvOpts {
let header = self.sketch.anchor.header();
let spec = alloy_evm::spec(&alloy_evm::eth::spec::EthSpec::mainnet(), header);
Copy link
Copy Markdown
Contributor

@bagelface bagelface May 5, 2026

Choose a reason for hiding this comment

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

we can't assume mainnet spec. we will be running Sepolia transactions as well, so should detect from chain ID

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Addressed in 6fdfdfc.

Chain detection now happens in EvmSketchExecutorBuilder::build: it queries eth_chainId, maps it through chain_id_to_genesis_and_spec, threads Genesis::Mainnet | Genesis::Sepolia into EvmSketch::builder().with_genesis(...), and stores the chain ID on the executor. sim_env() then picks EthSpec::mainnet() or EthSpec::sepolia() based on the stored chain ID instead of hardcoding mainnet.

Unsupported chain IDs (Holesky, Anvil, etc.) error explicitly rather than silently defaulting to mainnet — defaulting would corrupt hardfork derivation whenever the target chain disagrees with mainnet at the same height/timestamp.

Two tests in crates/evmsketch/src/lib.rs:

  • test_chain_id_to_genesis_and_spec_supported_and_rejected_chains — pins mainnet→Genesis::Mainnet + Cancun ts 1_710_338_135, sepolia→Genesis::Sepolia + Cancun ts 1_706_655_072, and asserts that 17000/31337/0 all error.
  • test_sepolia_spec_diverges_from_mainnet — synthesizes a header at timestamp 1_708_000_000 (between the two Cancun activations) and asserts mainnet maps to SHANGHAI while Sepolia maps to CANCUN. Hardcoding mainnet would silently misclassify the Sepolia case as Shanghai.

Copy link
Copy Markdown
Contributor

@bagelface bagelface left a comment

Choose a reason for hiding this comment

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

We don't want to assume mainnet specs, so need to detect Sepolia or Mainnet.

Hardcoding `EthSpec::mainnet()` produces wrong hardfork activation when
analyzing Sepolia transactions (e.g. Cancun activates ~3 days earlier on
Sepolia by timestamp). Query `eth_chainId` in `EvmSketchExecutorBuilder::build`,
map to `Genesis` and `EthSpec`, and pick the matching spec in `sim_env`.

Unsupported chains error explicitly rather than silently degrading.
@rubydusa rubydusa force-pushed the RonTuretzky/issue-95-spec branch from 8711082 to 251b2ce Compare May 5, 2026 21:10
Copy link
Copy Markdown
Contributor

@bagelface bagelface left a comment

Choose a reason for hiding this comment

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

lgtm

@rubydusa rubydusa merged commit 25dafc3 into main May 5, 2026
3 checks passed
@rubydusa rubydusa deleted the RonTuretzky/issue-95-spec branch May 5, 2026 21:53
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.

Transaction simulations don't include preceding transactions from the same block

4 participants