Skip to content

feat!: restart mega-evm as the Satin skeleton on the revm 40 forks - #385

Draft
RealiCZ wants to merge 87 commits into
satinfrom
cz/feat/satin-t2-skeleton
Draft

RealiCZ wants to merge 87 commits into
satinfrom
cz/feat/satin-t2-skeleton

Conversation

@RealiCZ

@RealiCZ RealiCZ commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is the first commit series of the Satin wave-1 pull request, which lands the Satin skeleton, the test gates and the common execution layer in that order and then takes the differential harness out of the repository: the workspace moves to the MegaETH forks of revm 40.0.3 and op-revm 20.0.0, the legacy core leaves, and crates/mega-evm restarts from an empty skeleton that runs a single spec, SATIN, through op-revm's OpEvm, which the common execution layer replaces with its own frame lifecycle over op-revm's handler.

  • Dependencies. revm 27.1 → 40.0.3 (all twelve crates patched to the revm fork), op-revm 8.1 → 20.0.0 (declared from the OP monorepo revision the node locks, patched to the op-revm fork), alloy-evm 0.36, alloy-op-evm 0.32, revm-inspectors 0.40, alloy 2.x. cargo tree -i revm shows one revm, from the fork; revm 27 is gone from the lockfile. The workspace becomes 2.0.0-alpha.1 (path-dependency versions in lockstep), MSRV 1.94.1 (the minimum of the locked alloy 2.4.2 crates; cargo metadata --locked reports no dependency above it, and the workspace checks on the 1.94.1 toolchain).
  • Workspace. The four tool crates (bin/mega-evme, bin/mega-t8n, crates/mega-state-test, crates/state-test) leave members / default-members; their sources are untouched and they rejoin when they are ported to Satin.
  • Core. MegaSpecId::SATIN (Karst / Osaka) and MegaHardfork::Satin; legacy spec names are rejected, never mapped. MegaContext wraps op-revm's context and fixes the Satin switches: EIP-8037 and EIP-2780 on, tx_gas_limit_cap = 200,000,000, EIP-7708 and the system-call reservoir margin off, Osaka gas table. MegaEvm wraps OpEvm (the common execution layer below replaces that with revm's Evm and its own handler) and implements revm's execution traits and alloy-evm's Evm; MegaEvmFactory implements alloy-evm's EvmFactory. external/ is carried over unchanged; limit/ (filled by the common execution layer) and access/ are empty placeholders; system/keyless holds the keyless data helpers; constants.rs holds the provisional numbers; block/ has the chain activation table (Satin unscheduled) and an executor skeleton.
  • Tests. The test inventory made for the engine rewrite is applied: 381 legacy-only / retired tests deleted (274 integration, 107 unit), 836 surviving tests parked under crates/mega-evm/tests/_pending/ with a README naming the owner of each file, 36 unit tests ported in place. New: an empty transaction, a value transfer and one SSTORE run through MegaEvm and through op-revm's OpEvm on the same CfgEnv and agree on every ResultGas field, the logs and the state; spec round-trip and legacy-name rejection tests.
  • Test gates (the second series, section "Test gates" below). A differential harness ran 352 scenarios through MegaEvm and stock revm 43 with no unexplained difference (the fourth series takes it out of the repository; the scenario format stays); the corpus and factory benches join transact under CodSpeed with a written policy; an execution-spec workflow runs the fork's own runner at the pinned tag with pinned counts; the mutation rules gain a reference point, a sharding rule, a shard mode and a second scope over the gates' own infrastructure, and the parked legacy mutant killers are retired.
  • Common execution layer (the third series, section "Common execution layer" below). MegaEvm runs MegaHandler over op-revm's handler with its own frame lifecycle; the Host stages what SSTORE, LOG and SELFDESTRUCT observe and wrappers commit it after the opcode; data-size bytes and write records are counted per frame; a limit stops a transaction with a latched revert (MegaLimitExceeded), never a halt; synthetic frame results carry the inherited reservoir; block execution admits only declared read-only inspectors; execute_transaction reports the regular, state and history ledgers. No limit is enforced by default, and with none configured MegaEvm still equals op-revm; the layer was certified against stock revm 43 (0 unexplained differences over 352 scenarios) before the harness left the repository. All 41 parked rows of this layer and 4 depth-guard rows are ported, and the six owed static-callee halt reasons are asserted again.
  • Differential harness removal (the fourth series, section "Differential harness removal" below). By decision the harness (runner, registry and scenario corpus) is maintained outside this repository: crates/mega-differential, its differential check and the 35 lockfile packages only it used are gone, and nothing else in the lockfile moves. The scenario format stays as a bench input (the corpus bench keeps its 13 scenarios), the infrastructure mutation scope keeps the scenario runner, and the 21 CREATE / CREATE2 rows the harness held are parked again under the common execution layer.
  • CI. replay-bench's bench job is off (if: false) until the tool crates are ported; the benchmark matrix was transact only (the test gates series below makes it corpus, factory, transact); the suppression-hygiene step no longer fails on an empty spec-gate plan.

The revm and op-revm forks are pinned by release tag: v40.0.3-mega.1 (revm 43485bf7de73b6ad92e9b5de4318eb96e2de2182) and v20.0.0-mega.2 (op-revm 260f65329f1f47b7412a1b2c1fda85453f66521c). Moving the pins from rev = to tag = changed only the source lines of the twelve revm packages and of op-revm in Cargo.lock.

Answers to the review questions

  • op-revm 817e36e9 vs 260f6532. 817e36e9 is the branch commit of the op-revm pin pull request; 260f6532 is its squash merge onto op-revm main. git diff 817e36e9 260f6532 in the op-revm checkout is empty and both commits have tree d3f2e5cb: same tree, different commit. The tag v20.0.0-mega.2 is on the merge commit, and the merge commit is the acceptance baseline; every check below ran on the tagged graph.
  • Consumers of MegaEvm::into_inner() and the implementation aliases. None today. MegaInnerEvm, MegaInstructions, MegaInnerContext and into_inner() are now crate-private.

Commits

Commit Concern
build: take the four tool crates out of the workspace members workspace
80d13bff, applying the test inventory to the integration tests 274 deleted, 691 parked
build!: move to the revm and op-revm forks and take out the legacy core dependency migration; the legacy src/, examples and bench targets leave; 107 unit tests deleted, 181 parked; 11 limit/* mutants suppressions dropped
feat: add MegaSpecId::SATIN, MegaHardfork::Satin and the chain activation table spec, fork, activation table
feat: carry the external environment interfaces over unchanged external/ (byte-identical to a8f8c7c9 except mod.rs without gas.rs)
feat: add the provisional constants table and the shared types constants.rs, types.rs
test: carry the test utilities over test_utils/
build: align clippy's MSRV with the workspace (1.94) clippy.toml
feat: run Satin through op-revm's OpEvm MegaContext, MegaEvm, MegaEvmFactory, equivalence tests
feat: move the keyless deployment helpers to system/keyless as data-only modules keyless RLP / signer / address / error ABI
feat: add the limit and access placeholders and the block executor skeleton placeholders
feat: define the crate's public surface re-exports, aliases, unused_crate_dependencies
test: add a transact bench skeleton for Satin bench
test: close the mutation survivors of the Satin skeleton two tests, one line suppression
docs: rewrite AGENTS.md for the Satin engine AGENTS.md, crate README, REVIEW.md
ci: keep the satin workflows runnable without the tool crates CI
fix: keep ParentBudgetExceeded in the keyless error ABI review fix
test: share the zero-fee transaction setup and tighten the Satin tests review fix
docs: fix review findings in the Satin docs review fix
ci: fail the hygiene step on a grep error, not only on a planner error review fix
build: pin the revm and op-revm forks by release tag review fix: rev =tag =
build: raise the MSRV to 1.94.1, the locked alloy 2.4 minimum review fix: workspace and clippy MSRV
refactor: keep the op-revm implementation types crate-private review fix: public surface; Precompiles documented as provisional
style: format the crate-private instruction table alias rustfmt of the previous commit
docs: name the owning mechanisms in comments and docs review fix: source comments, AGENTS.md, manifests, workflow comments and _pending/ name the owning mechanism
test: record the reviewer sign-off for the EthSpecId default suppression review fix: suppression signed
style: drop the trailing blank lines of the parked test files git diff --check clean
docs: say what lands on top of the Satin EVM wrapper wording
docs: make the parked-test ledger English-only and drop the internal pointers _pending/README.md: English disposition labels, a legend for the decision ids, decision notes without private pointers; counts and owners unchanged
docs: reword two comments that point at later changes wording of two comments
test: add the JSON transaction scenario format and its MegaEvm runner test gates: test_utils::Scenario
test: add the differential harness against stock revm 43 test gates: crates/mega-differential, 23 hand-written scenarios, fee-vault registry entries
test: carry the execution-spec-test-derived corpus over to the differential harness test gates: 263 scenarios
test: decode delegation designators in scenario pre-states and let scenarios describe themselves test gates: scenario format
test: group accepted differences by mechanism in the deviation registry test gates: registry entries carry their effects
test: cover EIP-7702, access lists, SELFDESTRUCT to existing accounts, logs and precompiles in the differential corpus test gates: 37 scenarios, the BN254 pairing entry
ci: run the differential harness as the differential check test gates: differential job
bench: add a differential-corpus slice and EVM construction to the Satin bench set test gates: corpus, factory benches
test: retire the parked legacy mutant killers test gates: 5 files, 44 tests
test: port the test gates' parked CREATE and CREATE2 tests into the differential corpus test gates: 21 rows, 20 scenarios
ci: let the mutation driver run one shard of a large mutant set test gates: MUTANTS_SHARD
docs: write the test-gate policy into REVIEW.md and AGENTS.md test gates: policy
ci: run the execution-spec fixtures through the fork's runner at the pinned tag test gates: exec-spec.yml with pinned counts
docs: keep what the retired mutant killers cited in the pending ledger review fix
test: share the arms' record code and tighten the scenario format review fix
test: cover gas prices in the differential corpus review fix: 7 scenarios
ci: fail the execution-spec count when its runner fails review fix
docs: keep the corpus size out of REVIEW.md and qualify the execution-spec coverage review fix
test: verify a KZG proof in the differential corpus review fix: 2 scenarios, 2 claim tests
ci: fail the execution-spec count when a counting command fails review fix
docs: keep the static-callee halt reasons owed in the pending ledger review fix
docs: make the mutation reference numbers reproducible review fix
test: add a mutation scope for the test gates' own infrastructure review fix: infra scope, its job, the hygiene universe
test: close the infrastructure mutation survivors review fix: 6 tests, 3 suppressions
feat: define the Satin gas ledgers, block counters and limit verdict types common execution layer: MegaGasUsage, BlockGasCounters, LimitKind, LimitCheck, MegaLimitExceeded; 7 rows ported
feat: run Satin through MegaHandler and MegaEvm's own frame lifecycle common execution layer: MegaEvm over revm's Evm, MegaHandler over op-revm's
feat: stage Host observations and commit write records after the opcode common execution layer: staging Host, commit wrappers, per-frame lanes, JournalInspectTr; 33 rows ported
feat: carry the inherited reservoir on synthetic frame results and guard the call depth common execution layer: synthetic-result contract, depth guard, interceptor and keyless extension points; 3 rows ported
feat: stop transactions that cross a limit with a latched revert common execution layer: abort protocol, EvmTxRuntimeLimits; 1 row ported
feat: gate block execution on declared observers and refuse creation revivals common execution layer: inspector entry
feat: report the transaction outcome with its ledgers, usage and stop common execution layer: execute_transaction; 1 row ported
test: assert the halt reason of a creation in a static callee common execution layer: the six owed static-callee assertions
bench: add deep calls, storage writes and logs to the transact bench common execution layer: bench
docs: write the common execution layer's contracts into AGENTS.md and the crate README common execution layer: docs
fix: keep the sender's account and stopped creations out of the write records review fix
test: reach the creator-nonce check through an inspector and a collision review fix
refactor: reset the layer at the EVM's entry points and share the pre-frame answer review fix
fix: keep a selfdestruct to the sender out of the write records review fix
fix: enforce a limit stop before the writes it guards review fix: stopped creations bump the nonce, authorities are enforced before they apply
test: pin the limit verdicts, the Host's staging and the lane merge rule review fix (mutation survivors)
test: cover which frame's account counts as recorded and the limit boundary review fix (mutation survivors)
test: draw the reservoir before a stop, report history gas, and rewrite results under the latch review fix
fix: check a declared observer's inputs, stack, memory and journal writes in debug builds review fix
test: pin fees, refunds, system calls and rejections against op-revm review fix (mutation survivors)
test: pin the synthetic-result settlement to revm's frame return review fix
test: name the ported tests after what they check review fix
docs: record the review round's rules in AGENTS.md and the frame init order review fix
style: drop the trailing blank line of a parked test file git diff --check clean
test: pin the authority records at the data-size cap review fix (mutation survivor)
refactor: check every creation's nonce bump, the outermost one included review fix (equivalent mutant removed)
test: suppress the equivalent and dead mutants of the common execution layer 5 line suppressions
bench: give the corpus bench its own scenario inputs harness removal: the 13 scenarios the corpus bench reads, under crates/mega-evm/benches/scenarios
test: park the CREATE and CREATE2 tests again under the common execution layer harness removal: 5 parked files restored, 21 rows owed to the common execution layer
test: pin the nonce and balance a scenario's database holds harness removal: the two infrastructure survivors only the corpus had killed
test: mutate the scenario runner alone in the infrastructure scope harness removal: infra scope, its job, mutants/README.md, REVIEW.md numbers
build: take the differential harness out of the repository harness removal: the crate, the workspace member, the differential check, the test step's --exclude, 35 lockfile packages
docs: say the differential harness is maintained outside this repository harness removal: AGENTS.md, REVIEW.md, the scenario module's comments

The legacy core cannot build on revm 40, so build! moves the manifests and removes the legacy sources in one commit. Every commit of every series passes cargo check --workspace --all-targets --locked.

Dependency matrix

Crate Before (a8f8c7c9) After Source after
revm (+ 11 sub-crates) 27.1.0 (and 38.0.0 for the revm-latest bench alias) 40.0.3 megaeth-labs/revm tag v40.0.3-mega.1 (43485bf7) via [patch.crates-io]
op-revm 8.1.0 (and 20.0.0 crates.io for the op-revm-latest alias) 20.0.0 declared from ethereum-optimism/optimism f67d87cd, patched to megaeth-labs/op-revm tag v20.0.0-mega.2 (260f6532)
alloy-evm 0.15.0 0.36.0 crates.io
alloy-op-evm 0.15.0 0.32.0 ethereum-optimism/optimism f67d87cd; its op-revm edge resolves to the fork
alloy-op-hardforks 0.2.13 0.5.0 ethereum-optimism/optimism f67d87cd (declared; not a core dependency yet)
op-alloy-consensus 0.18.14 2.0.0 ethereum-optimism/optimism f67d87cd
revm-inspectors 0.27.3 0.40.4 crates.io (dev-dependency; 0.40.1, the node's version, does not build against alloy 2.4)
alloy-primitives 1.6.0 1.6.0 crates.io
alloy-consensus / alloy-eips 1.1.0 / 1.8.3 2.4.2 crates.io
alloy-hardforks 0.2.13 0.4.8 crates.io
alloy-sol-types 1.4.1 1.6.0 crates.io; the alloy-sol-macro* family is held at 1.6.0 in the lockfile (1.7.3 macros against 1.6.0 types break mega-system-contracts)
mega-evm, mega-system-contracts, mega-state-test 1.7.1 2.0.0-alpha.1 path

The tool-only alloy crates (alloy-provider, alloy-rpc-*, alloy-transport*, op-alloy-network, op-alloy-rpc-types) are declared at the node's 2.x versions but not resolved: no member uses them until the tool crates are ported and rejoin the workspace.

Bench targets

Target / alias Disposition
transact kept as a skeleton: ExecuteEvm::transact for an empty transaction and an ether transfer through MegaEvm and through OpEvm on the same CfgEnv; the common execution layer adds deep calls, storage writes and logs
attack_replay, block_bench, comp_cost, ctt, enriched_tx, mega_bench, revm_bench deleted with the legacy core; the test gates rebuild the suite
benches/common/ deleted (legacy harness)
revm-latest, op-revm-latest aliases deleted; the main tree is the fork line, and the patch would redirect the aliases anyway
benches/data/*.hex (revm_bench inputs), benches/fixtures/known_attack_deploy.json (attack_replay input) kept, unread, for the rebuilt benchmark suite; the manifest says so

Exported API sketch

What a consumer (the node, the tools) sees from mega_evm:

Item Shape
MegaSpecId #[repr(u8)] #[non_exhaustive] enum { SATIN = 12 }, Default = SATIN; ALL, into_op_spec() (KARST), into_eth_spec() (OSAKA), name(); Display / FromStr "Satin"; serde "SATIN"; From<MegaSpecId> for OpSpecId, EthSpecId, &'static str
ParseMegaSpecError Legacy(String) for EquivalenceRex7 (LEGACY_SPEC_NAMES), Unknown otherwise
MegaHardfork alloy-hardforks hardfork! enum { Satin }, spec_id()
MAINNET_CHAIN_ID, TESTNET_CHAIN_ID, ChainActivation { chain_id, satin: Option<BlockTimestamp> }, CHAIN_ACTIVATIONS, chain_activation(chain_id) activation table; satin is None on both chains
MegaContext<DB, ExtEnvs = EmptyExternalEnv> new(db, spec), new_with_external_envs(db, spec, envs), with_cfg(CfgEnv<MegaSpecId>), with_block, with_tx, with_chain, modify_chain, spec(), mega_cfg(), external_envs(), into_parts(); from the common execution layer, with_tx_runtime_limits(EvmTxRuntimeLimits), additional_limit() and, with test-utils, additional_limit_mut(); implements revm ContextTr (Cfg = CfgEnv<OpSpecId>), ContextSetters, Host
MegaEvm<DB, INSP, ExtEnvs = EmptyExternalEnv> new(ctx), with_inspector(i), ctx(), ctx_mut(), is_inspecting(); from the common execution layer, with_trusted_inspector(i), has_rewriting_inspector(), inspector(), execute_transaction(tx), and revm's EvmTr / InspectorEvmTr; implements ExecuteEvm, ExecuteCommitEvm, InspectEvm, InspectCommitEvm, SystemCallEvm, InspectSystemCallEvm (revm) and alloy_evm::Evm (Spec = MegaSpecId, Precompiles = OpPrecompiles, documented as provisional); the revm traits report EVMError<DB::Error, OpTransactionError>, the alloy-evm interface EVMError<DB::Error, MegaTransactionError>
MegaEvmFactory<ExtEnvFactory = EmptyExternalEnv> new(), external_env_factory(), with_external_env_factory(f); implements alloy_evm::EvmFactory (Precompiles = OpPrecompiles, documented as provisional)
MegaTransaction = alloy_op_evm::OpTx, MegaTransactionError = alloy_op_evm::OpTxError, MegaHaltReason = op_revm::OpHaltReason, MegaTxType, MegaTxEnvelope aliases; the common execution layer keeps all five (see its section) and adds the result and gas types as new names
MegaHandler, MegaGasUsage, MegaTransactionOutcome, BlockGasCounters, LimitKind, LimitCheck, LimitUsage, MegaLimitExceeded, EvmTxRuntimeLimits, AdditionalLimit, StagedRecord, JournalInspectTr, TrustedObserver, DeclaredObserver, FORBIDDEN_CREATE_REVIVAL, untouched_call_gas, untouched_create_gas, with_pools_of, synthetic_frame_result, settle_frame_result, WRITE_RECORD_SIZE, LOG_BASE_SIZE, LOG_TOPIC_SIZE the common execution layer's contracts; signatures in its section
MegaBlockExecutor<E> skeleton: new, evm, evm_mut, into_evm; block execution lands with the block executor
ExternalEnvFactory, ExternalEnvTypes, ExternalEnvs, SaltEnv, OracleEnv, EmptyExternalEnv, BucketId, MIN_BUCKET_SIZE unchanged from the legacy core
constants::{COST_PER_STATE_BYTE, SLOT_STATE_GAS, ACCOUNT_STATE_GAS, COST_PER_HISTORY_BYTE, TX_GAS_LIMIT_CAP, TX_DATA_LIMIT, BLOCK_DATA_LIMIT} 1,530 / 97,920 / 183,600 / 88 / 200,000,000 / 13,107,200 / 13,107,200, all provisional
system::keyless::{decode_keyless_tx, recover_signer, calculate_keyless_deploy_address, KeylessDeployError, encode_error_result, decode_error_result} data only; decode_keyless_tx takes no spec and always rejects trailing bytes; KeylessDeployError covers every IKeylessDeploy error, with ParentBudgetExceeded.kind as the raw ABI u8
test_utils (feature test-utils) MemoryDatabase, ErrorInjectingDatabase, BytecodeBuilder, GasInspector, right_pad_bytes, transact(spec, db, caller, callee, data, value, gas_limit), zero_fee_l1_block_info(), op_transaction(tx), TestExternalEnvs; from the test gates series, the JSON scenario format (Scenario, PreAccount, ScenarioTx, ScenarioTxKind, AccessListEntry, AuthorizationEntry, ScenarioTxOutcome) with validate, database, block and run on MegaEvm
re-exports revm, op_revm, alloy_evm, alloy_op_evm, alloy_primitives, alloy_hardforks, alloy_consensus, alloy_sol_types, op_alloy_consensus; aliases SpecId, Transaction, HaltReason, TransactionError, TxType, Evm, EvmFactory, Context, BlockExecutor

The op-revm types a MegaContext / MegaEvm wraps (MegaInnerContext, MegaInnerEvm, MegaInstructions) and MegaEvm::into_inner() are crate-private, so no consumer can reach the concrete OP engine or its instruction layout. The one place the OP implementation stays visible is the Precompiles = OpPrecompiles associated type that alloy-evm's Evm and EvmFactory require: its doc comment says it is provisional and that code naming OpPrecompiles through it has no source-compatibility promise when the Satin precompile provider replaces it.

The inspector entry is revm's InspectEvm / InspectSystemCallEvm on MegaEvm plus alloy-evm's set_inspector_enabled; a test runs revm-inspectors' TracingInspector through it. The common execution layer adds the admission gate (with_trusted_inspector, has_rewriting_inspector, DeclaredObserver).

Measured on this tree (Osaka table, no MegaETH pricing yet): empty call 15,000 gas, value transfer to an empty account 21,000, one SSTORE 37,106; state gas 0 in all three because the Osaka table has no state-gas entries (the Satin gas table adds them). A 201,000,000-gas empty call leaves exactly 1,000,000 in the reservoir.

Test inventory reconciliation

Corpus Before Deleted (legacy-only + retired) Parked in _pending Ported in place
crates/mega-evm/tests/ 965 274 (125 + 149) 691 0
crates/mega-evm/src/ 288 107 (54 + 53) 145 36
tool crates 126 0 untouched in place

The 36 ported unit tests: 17 of the 21 the inventory assigns to the Satin skeleton, 5 bucket-hasher tests (owned by SALT pricing, carried with external/), 14 keyless helper tests (owned by native keyless deployment, carried with system/keyless). The other 4 skeleton rows are parked under the mechanism they need (the common execution layer, the Satin gas table, SALT pricing, the block executor), with the reason in the README. 18 new tests were added. The classification is the inventory's own rule table applied to every test name; the per-mechanism counts match the inventory's totals exactly before the ports.

What _pending/ holds

Decision ids (Dnn) index the Satin decision table, the numbered list of design decisions behind this engine; it is published with the engine's specification.

As of the test gates series (the skeleton series left 836: the test gates retired the 44 parked legacy mutant killers and ported their own 21 rows into the differential corpus):

Owning mechanism Tests From tests/ From src/
the common execution layer 41 19 22
the Satin gas table 34 27 7
SALT pricing 75 64 11
history gas 28 24 4
compute gas 26 26 0
the data-size limit 47 45 2
detention 81 76 5
the state-growth and KV limits 56 55 1
revert-class aborts 17 17 0
the pre-block system calls 18 15 3
the system contract interceptors 65 57 8
system contract deployment 67 17 50
the oracle and control contracts 77 77 0
native keyless deployment 75 73 2
the block executor 53 23 30
inspector support 4 4 0
undecided (D57, D58) 7 7 0
Total 771 626 145

The common execution layer then ports its 41 rows and 4 depth-guard rows of the system contract interceptors: 726 remain, and the README lists the ported rows. The harness removal parks the test gates' 21 CREATE / CREATE2 rows again, under the common execution layer: 747 remain. _pending/ has no main.rs, so Cargo never builds it. It no longer has a mutation/ directory: the legacy mutant killers were retired (the README keeps what they cited), since the Satin sources leave no survivor to regenerate one for.

Decisions taken in this series

  • MegaSpecId::SATIN = 12: the discriminant continues after the legacy ladder, so a raw as u8 never aliases a legacy spec.
  • ParseMegaSpecError::Legacy is distinct from Unknown, so the ported tools can route a legacy fixture to the legacy engine instead of failing generically.
  • The spec owns its configuration switches: MegaContext::with_cfg overwrites the gas table, the 8037/2780/7708 switches, the cap and the margin whatever the caller passes (reth's Osaka env builder would otherwise set the EIP-7825 cap).
  • MegaTransaction is alloy-op-evm's OpTx (orphan rules keep alloy-evm's traits off OpTransaction; OpTx already carries the signed-tx conversions the executor needs), so alloy-op-evm is a core dependency.
  • Precompiles are op-revm's OpPrecompiles: alloy-evm's PrecompilesMap only implements PrecompileProvider for revm's concrete Context, not for a wrapping context. The Satin gas table brings the Satin precompile provider (KZG 100k) and decides the dynamic-precompile shape the node's RPC needs; the associated type is documented as provisional, with no source-compatibility promise for consumers that name it.
  • KeylessDeployError::ParentBudgetExceeded keeps its ABI shape with the raw u8 kind: the legacy LimitKind is gone, and the Satin resource dimensions are defined by the limit mechanisms that follow. (The common execution layer brings LimitKind back with the legacy discriminants; the error keeps its raw u8.)
  • The Osaka gas table prices state gas at zero, so EIP-8037 is on (reservoir, cap, accounting) but no transaction draws state gas until the Satin gas table is installed; the SSTORE equivalence test pins the zero so the Satin gas table changes it on purpose.
  • derive_more became an optional dependency of test-utils.

Test plan

Command Result
cargo metadata --locked --no-deps exit 0
cargo metadata --locked (resolved graph) highest dependency rust_version is 1.94.1 (alloy 2.4.2), equal to the workspace's
cargo +1.94 check --workspace --all-targets --all-features --locked (rustc 1.94.1) exit 0, no warnings
cargo tree -i revm --locked one revm 40.0.3 from megaeth-labs/revm?tag=v40.0.3-mega.1#43485bf7; op-revm 20.0.0 from megaeth-labs/op-revm?tag=v20.0.0-mega.2#260f6532; all 12 revm crates and op-revm from the tags; revm@27.1.0 matches no package
cargo fmt --all --check exit 0
cargo clippy --workspace --lib --examples --tests --benches --all-features --locked exit 0, no warnings
cargo sort --check --workspace --grouped --order … exit 0
cargo test --workspace --locked exit 0; 60 passed, 0 failed, 1 ignored doctest (mega-evm 51 unit + 3 equivalence, system-contracts 6)
cargo check -p mega-evm --target riscv64imac-unknown-none-elf --no-default-features --locked exit 0
forge build in crates/system-contracts; cargo build -p mega-system-contracts exit 0
git diff origin/satin -- bin crates/mega-state-test crates/state-test empty
git diff --check origin/satin..HEAD exit 0
grep -rnP '[\x{4e00}-\x{9fff}]' over the tree no hits
grep -rnP '\bT[0-9]+(\.[0-9]+)?\b' over *.rs, *.md, *.toml, *.yml, *.yaml, *.py, *.sh, *.json no hits
npx prettier --check '*.md' 'docs/**/*.md' clean
cargo bench -p mega-evm --bench transact exit 0; all four workloads run, 1.7–2.2 µs per transaction for both engines (local wall-clock, noisy)
cargo check --workspace --all-targets --locked at every commit of the series 30/30 ok
cargo mutants --list --package mega-evm + umutate.py plan --packs spec-gate + mutation_gate.py orphans (the hygiene job's steps, with comby and universalmutator installed) 251-mutant universe, empty spec-gate plan, all 11 suppressions live
scripts/mutation_test.sh diff a8f8c7c9 + mutation_gate.py report --suppressions mutants/suppressions.toml PASS; 115 mutants: 49 caught, 0 survived, 1 suppressed (the signed EthSpecId default entry), 65 unviable

The last two commits change only crates/mega-evm/tests/_pending/README.md, which no Cargo target, formatter or linter reads, and the wording of two comments. fmt, test, git diff --check, the two greps and cargo check ran on the head, prettier ran on the README commit, and the other rows ran on the commit before the two.

Open items for review

  • Publishing. mega-evm now depends on git-only crates (op-revm, alloy-op-evm, op-alloy-consensus) and on [patch] redirects, so cargo publish of 2.0.0-alpha.1 would fail. Crate identity and publishability under patches belong to the crate identity and publishing work; nothing here sets publish = false.
  • Re-owned inventory rows. Four rows the inventory assigns to the Satin skeleton are parked under the common execution layer, the Satin gas table, SALT pricing and the block executor (reasons in the pending README).
  • revm-inspectors. The lockfile has 0.40.4; the node locks 0.40.1, which does not build against the alloy 2.4 this lockfile resolves.

CI notes

  • Scheduled and comment-triggered workflows (/benchmark, /replay-bench, nightly mutation) run the default branch's copy of the workflow file. On satin PRs, /replay-bench and /benchmark therefore still try the legacy targets and fail at build until the tool crates are ported and the test gates rebuild the benchmark suite; main is not touched from here.
  • mutation.yml's PR-diff cargo-mutants gate covers the whole new core on this PR; the local run above takes about three minutes.

Test gates

The second commit series lands four gates: the differential harness, the instruction-count baseline, the interim execution-spec run and the mutation-testing rules. The fourth series takes the differential harness out of the repository again (section "Differential harness removal"); the other three stay, and the numbers below are updated where it moved them.

Facts in the sections above that this series changed: the Summary (its test-gates bullet and the benchmark matrix in its CI bullet), the test_utils row of the exported API sketch, and the table under "What _pending/ holds" with the sentence after it.

Differential harness

Removed from this repository by decision: the harness (runner, registry and scenario corpus) is maintained outside mega-evm and is not a check on its pull requests, and the scenario format (test_utils::Scenario) stays as the corpus bench's input.

Instruction-count baseline (CodSpeed)

  • Benches. transact (kept), corpus (13 JSON scenarios through MegaEvm, since the harness removal under crates/mega-evm/benches/scenarios and described as a bench input, not a conformance suite: SSTORE, create, nested revert, two transactions, system call, calldata floor, nested refund at depth 10, EIP-7702 delegation, access list, SELFDESTRUCT from init code, logs, ecrecover, modexp; each checks that no transaction is rejected before it is measured) and factory (MegaEvmFactory::create_evm and create_evm_with_inspector). codspeed.yml builds every bench target of mega-evm, so it picks them up unchanged; benchmark.yml's matrix is [corpus, factory, transact].
  • Local runs. cargo bench -p mega-evm --bench corpus, --bench factory and --bench transact exit 0 (wall-clock, noisy: corpus 1.4–20 µs per scenario, factory 0.8 µs, transact 1.7–1.8 µs).
  • Policy (REVIEW.md, "Test gates"). The baseline is the latest satin push; regressions are read from the CodSpeed report on the pull request. The regression threshold is a setting of the repository's CodSpeed project: it is not in the repository (no .codspeed file, nothing in codspeed.yml) or in the check output, which reports "Performance Gate Passed" or "Performance Regression: <overall %>" without a threshold, so the policy does not state a number. The CodSpeed check is not required on satin (the required checks are lint, test, no-std and require-label).

Execution-spec tests

.github/workflows/exec-spec.yml reads the fork tag from Cargo.lock (and fails unless the lockfile pins exactly one fork revision and the tag names it), checks out megaeth-labs/revm at that tag with the fork's own toolchain, runs ./scripts/run-tests.sh clean cargo release, then counts the Osaka and Amsterdam state tests the runner executes (from its per-test JSON outcome) against the post-states the fixtures define, and compares them with the pinned numbers. Triggers: workflow_dispatch, a weekly schedule (inert until satin is the default branch), and pull requests and pushes to satin that touch Cargo.toml or Cargo.lock. 60-minute bound; not required. The script prints no counts, so the counting step runs the two state-test trees once more with the runner's per-test JSON outcome (about a minute, on the script's build). The comparison runs under ||, where errexit is off, so each of the three commands behind it is guarded on its own: the runner, the count of the state tests the fixtures define, and the count of those the runner executed. Probed by sourcing the workflow's check with stubs: it returns 0 with all three succeeding, and 1 when any one of them prints the expected number and then exits non-zero, or when a count is off by one.

First run (local, at v40.0.3-mega.1, fixtures main v5.4.0 develop and tests-glamsterdam-devnet@v8.1.0): the script exits 0 (every state-test suite passes; main blockchain tests 61,606 passed, 76 skipped), 22 minutes wall, most of it downloading 12 GB of fixtures.

Fixtures Executed Skipped Defined
Osaka (main develop state tests) 19,465 52 19,517
Amsterdam (glamsterdam devnet state tests) 15,754 72 15,826

The skipped ones are the create-collision fixtures with storage the runner skips by name, and expected-exception transactions it cannot build (bad signatures, blob-hash creates). REVIEW.md says that no check in this repository runs execution-spec tests on MegaEvm itself until the state-test tool is ported.

Mutation testing

  • Reference point. The skeleton series' diff gate: 115 mutants, 49 caught, 0 survived, 1 suppressed, 65 unviable, 7m41s on CI. Reproduced locally at this head with scripts/mutation_test.sh diff a8f8c7c9: the same 115 / 49 / 0 / 1 / 65, PASS, 4m12s (JOBS=8). REVIEW.md now names that base commit and the driver-plus-gate command pair, because cargo mutants on its own generates a different population (the driver excludes function-scoped suppressions before generation, the gate filters line-scoped ones after the run).
  • This series, production scope. scripts/mutation_test.sh diff 04a16eec mutates nothing: the only src/ change is test_utils/, which .cargo/mutants.toml excludes, so the gate reports no mutatable change.
  • Infrastructure scope. That exclusion, and a test_package of mega-evm alone, are why the code implementing the gates was never mutated. .cargo/mutants-infra.toml plus scripts/mutation_test.sh infra mutate the scenario runner (crates/mega-evm/src/test_utils/scenario.rs) and the harness's record, diff and registry modules against both packages' tests, so the whole corpus is among the killing tests; the production scope is unchanged and the two are disjoint. First run: 80 mutants, 58 caught, 18 survived, 4 unviable. Fifteen survivors were real gaps — the rendering of a log and of an account (what the comparison reports and what a registry effect is a pattern over), the glob's middle segments, that an entry names both a mechanism and a reason, the per-kind field rules and the system-call shape of a scenario transaction, and the fixed block — and are closed by six tests. Three are equivalent (Scenario::block writes a timestamp, a base fee and a gas limit that revm's BlockEnv::default() already holds) and are suppressed as line entries, killable again the moment an upstream default moves, since the new test pins all five values. The run is now 80 mutants, 73 caught, 0 survived, 3 suppressed, 4 unviable, PASS, 7m46s (JOBS=8). Job cargo-mutants infrastructure runs it on a pull request touching that code, either configuration, the driver or the gate, and nightly; 90-minute bound, not required. A suppression can belong to either scope, so the suppression-hygiene job lists both into its universe (331 mutants then; all 14 suppressions matched a live mutant). With the harness gone (section "Differential harness removal"), the scope is the scenario runner alone, tested by mega-evm: 39 mutants. Two of them survived, Scenario::database dropping a pre-state account's nonce or its balance, which only the corpus had killed; a unit test kills both, and the run is 39 mutants, 34 caught, 0 survived, 3 suppressed, 2 unviable, PASS, 5m45s (JOBS=8). The hygiene universe is now 551 mutants, and all 18 suppressions match a live mutant.
  • Sharding. Rule in REVIEW.md: shard a series whose diff lists more than 1,000 mutants (about an hour at the reference rate; the job bound is 330 minutes). scripts/mutation_test.sh gains MUTANTS_SHARD=k/n (cargo-mutants' --shard), because file <glob> is not diff-scoped; checked on constants.rs: 20 mutants split into two disjoint shards of 10, each gated PASS. The pull request that first needs it adds a shard matrix to the job.
  • Spec-gate pack. Kept dormant. On a single-spec engine it finds no gate, the hygiene job accepts the empty plan, and the pack, its workflow jobs and the suppression wiring are exactly what the first spec after Satin needs; removing them now means restoring them verbatim then. It is not misleading as long as REVIEW.md says it finds nothing on Satin, which it now does.
  • Parked mutant killers. The parked mutation/ directory held 5 files (4 test files and main.rs), 44 tests, all keyed to surviving mutants of the legacy sources and owned by mechanisms Satin does not have yet. The Satin sources have no survivor (the whole-core run above), so nothing was regenerated; the files are deleted, and the pending README counts them in one line instead of listing them.
  • Parked rows owned by the test gates. The 21 rows the inventory assigns to the test gates (canonical CREATE / CREATE2 behavior) were ported as 20 differential scenarios; with the harness gone they are parked again, under the common execution layer (section "Differential harness removal").

The pending README is still generated from the test inventory, now with the retired mutation rows and the rows ported into the harness as inputs, and regenerating it reproduces the committed file. _pending/ now holds 771 tests (from 836).

Scheduled workflows

GitHub fires a schedule only from the default branch's copy of a workflow, so satin's schedules (nightly mutation, weekly benchmark, replay-bench, doc-audit, the weekly execution-spec run) stay inert until satin is the default branch, and a satin schedule cannot be added from here. All five have workflow_dispatch, and gh workflow run <workflow>.yml --ref satin runs satin's copy on satin's head, so no ref input was added; REVIEW.md lists what each does when dispatched on satin (replay-bench does nothing until the state-test tool is ported).

Test gates test plan

As run at the head of the test gates series; the mega-differential rows describe the crate the fourth series removes, and the final head's checks are under "Differential harness removal".

Command Result
cargo fmt --all --check exit 0
cargo clippy --workspace --lib --examples --tests --benches --all-features --locked exit 0, no warnings
cargo sort --check --workspace --grouped --order … exit 0
cargo test --workspace --locked exit 0: mega-differential 15 unit + 6 claims + 1 corpus; mega-evm 59 unit + 3 satin; system-contracts 0 unit + 5 + 1; 1 ignored doctest
cargo test -p mega-differential --locked -- --nocapture the summary line above
cargo check -p mega-evm --target riscv64imac-unknown-none-elf --no-default-features --locked exit 0
cargo bench -p mega-evm --bench corpus / factory / transact exit 0 each
cargo tree -i revm --locked -p mega-evm one revm, 40.0.3 from megaeth-labs/revm?tag=v40.0.3-mega.1#43485bf7
cargo tree -p mega-differential -i revm --locked ambiguous: revm@40.0.3, revm@43.0.0 (both sources)
scripts/mutation_test.sh diff 04a16eec + mutation_gate.py report nothing to mutate (only the excluded test_utils/ changed)
scripts/mutation_test.sh diff a8f8c7c9 + mutation_gate.py report PASS; 115 mutants: 49 caught, 0 survived, 1 suppressed, 65 unviable; 4m12s
scripts/mutation_test.sh infra + mutation_gate.py report PASS; 80 mutants: 73 caught, 0 survived, 3 suppressed, 4 unviable; 7m46s (JOBS=8)
mutation_gate.py orphans over both scopes' universe universe 331 mutants; all 14 suppressions match a live mutant
the execution-spec check function, sourced with stub counting commands 0 with all three succeeding; 1 when the runner fails, when the defined count prints 19517 and then fails, when the executed count prints 19465 and then fails, and on a count off by one; the previous revision returns 0 for the second of those
actionlint on differential.yml, exec-spec.yml, mutation.yml, build-and-test.yml, benchmark.yml clean apart from the custom blacksmith-* runner labels, which satin's benchmark.yml already has
the execution-spec workflow's pin and counting steps, run locally on the fork checkout pin v40.0.3-mega.1 = 43485bf7; counts as in the table above; a wrong pinned count and a failing runner each fail the step with an error line
npx prettier --check '*.md' 'docs/**/*.md' clean
git diff --check origin/satin..HEAD exit 0
CJK, work-item-code and internal-vocabulary sweeps over the tree, the commit messages and this description no hits
regenerating crates/mega-evm/tests/_pending/README.md from the test inventory identical to the committed file
cargo check --workspace --all-targets --locked and cargo test -p mega-differential --locked at every commit of the series 24/24 ok

Review round

A two-axis review (standards, spec) of the series found no breach of a documented standard; the follow-up commits:

  • The scenario runner no longer defaults a missing gas limit to zero (it panics with the reason), transaction types are named, and TxSpec became ScenarioTx.
  • Scenarios can set a gas price.
  • The execution-spec counting step fails when its own runner run fails, and says why it runs the two trees again.
  • The pending README keeps what the retired mutant killers cited (dispositions and decision notes per owning mechanism).

A second, zero-context review of the same series returned three findings, all confirmed; the follow-up commits of the two that did not concern the harness alone:

  • The execution-spec count could pass on a failed counting command. check runs under ||, where errexit is off, so the two command substitutions behind the comparison were unguarded: a pipeline printing the expected number and then exiting non-zero still passed. Both are guarded now, and the fix is probed by sourcing the workflow's own check with stub counting commands.
  • The gate's infrastructure was outside the gate. A second mutation scope covers it; see "Mutation testing" above for the scope, its job and its numbers.

The reviewer also asked whether the six static-callee halt-reason assertions dropped by the ported CREATE tests are discharged. They were not: _pending/README.md recorded them as owed, and the common execution layer re-adds them (tests/satin/static_callee.rs). They stay when the harness leaves and the CREATE rows are parked again.

Common execution layer

The third commit series defines the contracts every later mechanism plugs into: the frame lifecycle, the result and gas types, the observe-stage-commit layer with its write records, the abort protocol, the inspector entry and the synthetic-result reservoir contract. It adds no pricing: with no limit configured, MegaEvm still equals op-revm (the equivalence tests). The layer was certified against stock revm 43 before the differential harness left the repository: at the head of this series the harness ran 352 scenarios with 0 unexplained differences and 0 stale registry effects, the same line as before the series, and the registry unchanged.

Facts in the sections above that this series changed: the Summary (its opening sentence, the Core bullet and a new bullet for this series), the transact row of "Bench targets", the MegaContext, MegaEvm and alias rows of the exported API sketch with the new rows under it and the inspector paragraph after it, the sentence after the table under "What _pending/ holds", the KeylessDeployError line of "Decisions taken in this series", and the last paragraph of the test gates' review round.

The contracts

Contract Signature Where
Frame lifecycle MegaHandler<EVM, ERROR, FRAME> wraps op-revm's OpHandler and overrides pre_execution (applied EIP-7702 authorities) and last_frame_result (the outermost lane, the latch translation, op-revm's settlement, the history gas); MegaEvm implements EvmTr / InspectorEvmTr itself: frame_init (latch → depth guard → MegaEvm::interceptMegaEvm::rewrite_keyless → lane and records → revm), frame_run / inspect_frame_run (before_frame_run: a latched frame returns the stop without an instruction), frame_return_result (lane pop, latch rewrite), inspect_frame_init (paired frame_start / frame_end on every early return) evm/execution.rs
Result and gas types MegaGasUsage { regular, state, history, reservoir_remaining, floor, gas_used } with new(&ResultGas, history) and block_execution_gas(); MegaTransactionOutcome { result_and_state, gas, usage, limit_exceeded } (derefs to ResultAndState) from MegaEvm::execute_transaction; BlockGasCounters { execution, state, history } with record(&MegaGasUsage) evm/result.rs, block/result.rs
Observe, stage, commit StagedRecord::{Sstore(SStoreResult), Log { topics, data_len }, SelfDestruct { had_value, target_exists, to_other_account, beneficiary }}; AdditionalLimit::{stage_record, commit_staged_record, discard_staged_record, discard_stale_record} (crate-internal: the Host and the wrappers are their only callers) and the public staged_record(); wrappers for SSTORE, LOG0..LOG4, SELFDESTRUCT in mega_instructions; LimitUsage { data_size, write_records }, WRITE_RECORD_SIZE = 40, LOG_BASE_SIZE = LOG_TOPIC_SIZE = 32; JournalInspectTr::{inspect_account, inspect_account_code_hash, inspect_storage} for reads that do not warm evm/host.rs, evm/instructions.rs, limit/
Abort protocol LimitKind::{DataSize, KVUpdate, ComputeGas, StateGrowth} (discriminants 0–3, as the legacy engine encoded them); LimitCheck::{WithinLimit, ExceedsLimit { kind, limit, used, frame_local }, Exempt} with revert_data(); MegaLimitExceeded(uint8 kind, uint64 limit); AdditionalLimit::latch(kind, limit, used) and latched(); EvmTxRuntimeLimits { tx_data_size_limit, frame_data_size_limit } (both unlimited by default) via MegaContext::with_tx_runtime_limits limit/
Inspector entry MegaEvm::with_inspector (tools), with_trusted_inspector<I: TrustedObserver>, has_rewriting_inspector(); TrustedObserver (implemented for NoOpInspector, &mut T, DeclaredObserver<I>); DeclaredObserver<I> forwards and, in debug builds, checks every callback; FORBIDDEN_CREATE_REVIVAL evm/inspector.rs, evm/mod.rs
Synthetic results untouched_call_gas(&CallInputs), untouched_create_gas(&CreateInputs), with_pools_of(gas_limit, &Gas), synthetic_frame_result(&FrameInput, InstructionResult, Bytes), settle_frame_result(ctx, &mut GasTracker, &mut FrameResult) evm/frame.rs

Invariants

  • The Host only observes. sstore, log and selfdestruct stage what revm hands them and record nothing; every other Host method delegates.
  • Commit after the opcode. A wrapper discards a stale record on entry, runs revm's instruction, and commits the staged record when the opcode completed (including SELFDESTRUCT's own SelfDestruct result) or discards it when the opcode failed. An SSTORE that runs out of gas after its Host call wrote the slot counts nothing. Every other opcode, and the static gas table, is revm's.
  • Write records. One 40-byte record per account or slot write, at the data-size limit's sites: a slot's first change in the transaction (taken back on write-back), a value transfer's sender and recipient, a creation's creator nonce and created account, a SELFDESTRUCT moving value to another account, an applied EIP-7702 authority, the transaction's value recipient or created account. The sender's account is part of the transaction body and never a record. Deduplicated per frame, on the frame's lane: a success merges the lane into the caller's, a failure discards it; a creator's nonce record survives a failed creation once the nonce was bumped. The KV count is the write-record count.
  • The latch. A transaction-level limit stops the transaction with a revert whose output is MegaLimitExceeded: no caller resumes, no frame starts, every returned result is rewritten to the stop, and the outermost frame settles like an EIP-8037 revert (unspent regular gas and the whole reservoir back). A frame budget reverts its frame alone. A limit is enforced before the writes it guards: a frame whose start would cross it is answered before revm builds it (a creation still bumps its creator's nonce, so a stopped creation transaction cannot be replayed), and EIP-7702 authorities whose records would cross it are taken back with their gas. Real out-of-gas, precompile out-of-gas and invalid opcodes still halt and burn; an out-of-gas before the first frame takes back what pre-execution counted.
  • Lanes stay aligned. One lane per frame result revm returns, an empty one for a frame answered without running (the latch, the depth guard, an interceptor, an inspector).
  • Synthetic results carry the caller's pools. Never Gas::new(limit); they carry the calling opcode's upfront state-gas flags and settle exactly like revm's own. The depth guard and the latch exit use them here; interceptors, precompile normalisation and native keyless deployment later.
  • The admission gate. A rewriting inspector is a tool feature; block execution admits only with_trusted_inspector. The one refused rewrite is a failed creation turned into a success.
  • One transaction. Every entry point of MegaEvm resets the layer before it runs the handler.

Halt-reason set

MegaHaltReason stays op_revm::OpHaltReason: Base(HaltReason) for the EVM's own halts and FailedDeposit. A resource limit never halts a transaction; it reverts with MegaLimitExceeded, and MegaTransactionOutcome::limit_exceeded says which limit (a contract can revert with the same bytes).

What types.rs still aliases

All five, because Satin adds nothing to them: MegaTransaction (OpTx), MegaHaltReason (OpHaltReason), MegaTransactionError (OpTxError), MegaTxType, MegaTxEnvelope. The types Satin does add are new names (MegaGasUsage, MegaTransactionOutcome, BlockGasCounters, LimitCheck, MegaLimitExceeded).

Ported rows

All 41 rows the pending README assigned to this layer, plus the 4 depth-guard rows of rex5/call_too_deep_guard.rs that the README assigned to the system contract interceptors and that need no interceptor (the synthetic-result contract they pin lands here). _pending/ holds 726 tests (from 771), and the README lists the ported rows with where each went.

Legacy file Tests Now in
src/evm/result.rs 4 src/evm/result.rs (rewritten for the Satin halt set)
src/limit/mod.rs 3 src/limit/mod.rs
src/limit/frame_limit.rs 4 src/limit/frame_limit.rs (the data-size and write-record lanes)
src/evm/host.rs 10 src/evm/host.rs (JournalInspectTr)
src/evm/mod.rs 1 tests/satin/outcome.rs
equivalence/evm_state.rs 3 tests/satin/state.rs
rex4/eip7702_delegation_cycle.rs 1 tests/satin/state.rs
mini_rex/db_error.rs 4 tests/satin/db_error.rs (a database error now surfaces as EVMError::Database, not Custom)
rex5/frame_target_updated_dedup.rs 5 tests/satin/write_records.rs
rex6/create_frame_accounting.rs 2 tests/satin/write_records.rs
rex6/self_transfer_account_dedup.rs 4 tests/satin/write_records.rs
rex5/call_too_deep_guard.rs 4 tests/satin/synthetic_frame_gas.rs (the depth guard answers without an interceptor; a latched transaction's stop wins over CallTooDeep)

The six static-callee halt-reason assertions the test gates left owed are back in tests/satin/static_callee.rs: they read the callee's frame outcome as its caller receives it, and every one is StateChangeDuringStaticCall, as in canonical revm.

When the harness left the repository, the 21 CREATE / CREATE2 rows it had held as scenarios were parked again under this layer (section "Differential harness removal"), so _pending/ holds 747 tests on the final head; the six assertions above stay.

Bench

transact gains deep_calls (a contract calling itself 64 deep, each frame writing a slot and logging), storage_writes (200 first writes, then 200 write-backs) and logs (200 two-topic logs), each through MegaEvm and through OpEvm, so CodSpeed reports the frame lifecycle's and the wrappers' cost on this pull request. Local wall-clock (noisy, for orientation only): deep calls 25.5 µs vs 25.2 µs, storage writes 20.4 vs 19.2, logs 7.6 vs 7.2, empty transaction 1.80 vs 1.57.

Mutation testing

Production scope, scripts/mutation_test.sh diff daed59c8 + scripts/mutation_gate.py report --suppressions mutants/suppressions.toml:

At Mutants Caught Missed Unviable Gate
docs: write the common execution layer's contracts into AGENTS.md and the crate README (the ten feature commits) 280 112 56 112
docs: record the review round's rules in AGENTS.md and the frame init order (the review round) 289 163 8 118
test: suppress the equivalent and dead mutants of the common execution layer (the head of the series) 285 161 5, all suppressed 119 PASS, 161/161 viable mutants killed
  • The 56 survivors of the first run (20 in evm/execution.rs, 16 in limit/limit.rs, 10 in evm/inspector.rs, the rest in the Host, the frame lanes, the verdicts, the context, the synthetic results and the result types) are closed by the review round's pins.
  • Of the 8 left after the review round, one was a real gap: record_applied_authorities' >>= (authority records landing exactly on the cap), closed by test: pin the authority records at the data-size cap. Two were equivalent: the creator-nonce check's depth > 0 guard (the outermost creation has no creator record to take back); refactor: check every creation's nonce bump, the outermost one included removes the guard. The other five are suppressed.
  • Five signed line suppressions in mutants/suppressions.toml, each saying when it becomes killable. Dead while EIP-7708 is off (frame init journals no log, and op-revm's precompiles journal none): the two logs().len() != logs_i comparisons of inspect_frame_init, inspect_logs, and DeclaredObserver::log (an opcode's log reaches log_full, which a test covers); they become live when EIP-7708 is switched on. Equivalent: AdditionalLimit::discard_stale_record — each wrapped opcode stages its own record through the Host whenever it completes and a failing opcode's wrapper discards what is staged, so a stale record can never be committed; the entry discard guards a future wrapped opcode that completes without a Host call.

Shapes chosen where another was plausible

  • MegaEvm owns revm's Evm rather than op-revm's OpEvm: OpEvm's frame methods cannot be overridden, and the frame lifecycle is the extension point.
  • MegaHaltReason stays an alias: stops are reverts, so a Satin halt enum would be op-revm's with a new name.
  • LimitKind keeps the legacy discriminants, so contracts decoding MegaLimitExceeded keep working.
  • Three staged-record variants: the Host sees only SSTORE, LOG and SELFDESTRUCT; value-transfer and creation records are counted at frame init, which runs after the opcode completed.
  • A transfer's and a creation's records sit on the child's lane, so the child's failure takes them back; history gas (a later series) charges them to the caller at the opcode.
  • Per-frame deduplication, not per-transaction: O(1) and allocation-free per frame; a per-transaction set would allocate per distinct account.
  • The frame budget knob is flat and capped by the caller's remaining budget, so a child can never push its caller over; the data-size limit replaces it with its own rule.
  • A frame stopped at init is answered before revm builds it, not built and reverted: a value transfer answered by revm itself (empty code, a precompile) commits before a revert could be applied.
  • The latch rewrite leaves halts as halts: only an inspector can produce one under the latch, and a real out-of-gas must stay one.
  • The depth guard covers CALL and STATICCALL only: those are what an interceptor answers; revm checks the depth of every frame it builds.
  • The reset lives in MegaEvm's entry points, not in copies of revm's handler defaults.
  • DeclaredObserver's debug check covers what it can see cheaply (gas, action, stack depth, memory size, inputs, results, journal entries and logs); it is documented as a tripwire.

Review round

A standards review and a spec review ran over the series once its ten feature commits were in, and two mutation runs followed; the later commits are their fixes.

  • The sender's account is never a write record. It is part of the transaction body: a frame running as the sender (reached through an EIP-7702 delegation) counts it as recorded, a value transfer to the sender records no recipient, and a SELFDESTRUCT whose beneficiary is the sender records nothing.
  • A limit is enforced before the writes it guards. A frame the limit stops at init is answered before revm builds it, and a creation answered that way still bumps its creator's nonce, as one that starts and reverts does (a stopped creation transaction cannot be replayed). EIP-7702 authorities whose records would cross the limit are taken back with the gas they charged, and the first frame is answered with the stop. An out-of-gas in the runtime phase before the first frame takes back what pre-execution counted, and any latch with it.
  • The creator-nonce check covers every creation. It is reached through an inspector's answer and through an address collision, and the outermost creation runs the same check (it has no creator record to take back, so the old depth guard was an equivalent mutant).
  • One reset per transaction, at the entry points. The layer is reset in MegaEvm's entry points (transact_one, replay, inspect_one_tx, both system-call paths) rather than in copies of revm's handler defaults, and the latch and the depth guard share one pre-frame answer, so an inspector's answer can neither start a frame of a stopped transaction nor reach past the call-stack limit.
  • DeclaredObserver checks more in debug builds: the inputs, the stack, the memory size and the journal writes each callback leaves, besides gas, action and results.
  • Pins added for review findings and mutation survivors: the limit verdicts, the Host's staging and the lane merge rule; which frame's account counts as recorded and the limit boundary; the reservoir drawn before a stop coming back, the history gas reported, results rewritten under the latch; fees, refunds, system calls and rejections equal to op-revm; the synthetic-result settlement equal to revm's frame return; the authority records at the data-size cap. The ported tests are renamed after what they check, and AGENTS.md records the review round's rules and the frame init order.
  • Five signed suppressions for the survivors no test can kill (see "Mutation testing" above).

Common execution layer test plan

Command Result
cargo check --workspace --all-targets --locked at every commit of the series 27/27 ok
the certification against stock revm 43, before the harness left the repository: cargo test -p mega-differential --locked at every commit of the series that touches crates/mega-evm/src 16/16 ok
the same certification at the head of the series (-- --nocapture) differential: 352 scenarios, 13296 fields compared, 1088 deviations matched (op-fee-vault-touch x1080, op-karst-bn254-pairing-input-bound x8), 0 unexplained, 0 stale registry effects, the line of the test gates series
scripts/mutation_test.sh diff daed59c8 + mutation_gate.py report --suppressions mutants/suppressions.toml at the head of the series PASS; 285 mutants: 161 caught, 0 survived, 5 suppressed, 119 unviable
cargo bench -p mega-evm --bench transact, corpus, factory exit 0 each

The workspace-wide checks (fmt, clippy, sort, the test suites, the no_std check, prettier, git diff --check, the sweeps, the pending README) ran on the final head of this pull request; see "Differential harness removal".

Differential harness removal

The fourth commit series takes the differential harness out of this repository: by decision the harness (runner, registry and scenario corpus) is maintained outside mega-evm, and nothing here decides where it goes. The common execution layer was certified against it before it left (section "Common execution layer").

Facts in the sections above that this series changed: the Summary (its opening sentence, the test-gates and common-execution-layer bullets, and a new bullet for this series), the commits table and the sentence after it, the sentence after the table under "What _pending/ holds", the test gates' introduction, its "Differential harness", "Instruction-count baseline", "Execution-spec tests" and "Mutation testing" subsections, the note above its test plan and its review round, and the common execution layer's opening paragraph, ported rows and test plan.

  • Removed. crates/mega-differential (the runner, the 352-scenario corpus, deviations.json, the claim tests), its workspace membership, .github/workflows/differential.yml (the differential check), and the --exclude mega-differential of build-and-test.yml's test step, which is cargo test --workspace again. AGENTS.md loses the harness's commands, its workspace row, its test-organization entry and its rule, and the cargo tree rule is back to "exactly one revm". REVIEW.md says in three lines that the harness is maintained outside this repository and is not a check on its pull requests, and no longer counts derived scenarios as execution-spec coverage. Nothing in docs/spec/ changes.
  • Kept. The scenario format (test_utils::Scenario). The corpus bench reads its 13 scenarios from crates/mega-evm/benches/scenarios/, copied unchanged from the corpus, and its doc says they are a bench input, not a conformance suite.
  • Parked tests. The 21 CREATE / CREATE2 rows the test gates had ported as 20 scenarios are parked again: the five parked source files are restored as they were before that port (four whole files, and the two CREATE2 operand-underflow tests of rex6/error_paths.rs), and the pending README lists the rows under the common execution layer, marking the six whose static-callee halt reason tests/satin/static_callee.rs already asserts. _pending/ holds 747 tests (from 726).
  • Mutation scopes. The infrastructure scope is the scenario runner alone, tested by mega-evm (.cargo/mutants-infra.toml, scripts/mutation_test.sh infra, the cargo-mutants infrastructure job and its path filter, mutants/README.md, REVIEW.md); its three signed BlockEnv suppressions stay. Its first run found two survivors that only the corpus had killed, Scenario::database dropping a pre-state account's nonce or its balance, and a unit test kills both. On the final head: 39 mutants, 34 caught, 0 survived, 3 suppressed, 2 unviable, PASS, 5m44s (JOBS=8). The production scope is unchanged. Suppression hygiene over both scopes: 551 mutants, all 18 suppressions match a live mutant.
  • Lockfile. Removing the only dependent of revm 43 drops 35 packages and adds none: mega-differential; revm 43.0.0 and its eleven 43.0.x sub-crates; the arkworks 0.6 crates (twelve); foldhash 0.1.5, phf_generator and phf_macros 0.14.0, ripemd 0.2.0 and sha2 0.11.0; tracing-subscriber with nu-ansi-term, sharded-slab, thread_local and tracing-log. No remaining package changes version: c-kzg stays at 2.1.8 and tracing-core at 0.1.36, the releases the harness's resolve had moved them to, so the core's KZG backend is still c-kzg 2.1.8. cargo tree -i revm --locked shows one revm, the fork, and Cargo.lock has one name = "revm".

Final test plan

On the final head of this pull request:

Command Result
cargo fmt --all --check exit 0
cargo clippy --workspace --lib --examples --tests --benches --all-features --locked exit 0, no warnings
cargo sort --check --workspace --grouped --order … exit 0
cargo test --workspace --locked exit 0: mega-evm 99 unit + 94 satin; system-contracts 0 unit + 5 + 1; 1 ignored doctest
cargo check -p mega-evm --target riscv64imac-unknown-none-elf --no-default-features --locked exit 0
cargo bench -p mega-evm --bench corpus / transact exit 0 each
cargo tree -i revm --locked; grep -c 'name = "revm"' Cargo.lock one revm, 40.0.3 from megaeth-labs/revm?tag=v40.0.3-mega.1#43485bf7; 1
npx prettier --check '*.md' 'docs/**/*.md' clean
git diff --check origin/satin..HEAD exit 0
actionlint on build-and-test.yml, exec-spec.yml, mutation.yml clean
scripts/mutation_test.sh infra + mutation_gate.py report PASS; 39 mutants: 34 caught, 0 survived, 3 suppressed, 2 unviable; 5m44s (JOBS=8)
cargo mutants --list over both scopes + mutation_gate.py orphans universe 551 mutants; all 18 suppressions match a live mutant (the spec-gate plan needs comby and universalmutator, which were not installed locally; on Satin it finds nothing)
CJK, work-item-code and internal-vocabulary sweeps over the tree, the 87 commit messages and this description no hits
regenerating crates/mega-evm/tests/_pending/README.md from the test inventory identical to the committed file; 747 pending
cargo check --workspace --all-targets --locked at every commit of the series 6/6 ok

Labels

spec:new, api:breaking, comp:core, comp:misc, dependencies, agent

bin/mega-evme, bin/mega-t8n, crates/mega-state-test and crates/state-test
still target the legacy engine. Their sources stay untouched; they rejoin
the workspace when they are ported to Satin.
Delete the 274 integration tests the test inventory marks legacy-only or
retired (125 + 149) and move the 691 that survive into Satin under
tests/_pending/, unchanged apart from the deletions. _pending has no
main.rs, so Cargo does not build it; its README names the owning
mechanism for every file. compute_gas/snapshot.txt goes with the
per-opcode counter it pinned (D40).
revm 27.1 -> 40.0.3 with all twelve revm crates patched to the megaeth
fork (rev 43485bf7), op-revm 8.1 -> 20.0.0 from the OP monorepo revision
the node locks (f67d87cd) patched to the megaeth op-revm fork (rev
817e36e9), alloy-evm 0.15 -> 0.36, alloy-op-evm 0.15 -> 0.32,
revm-inspectors 0.27 -> 0.40, alloy 1.x -> 2.x, alloy-primitives 1.6.
The workspace becomes 2.0.0-alpha.1 (path-dependency versions in
lockstep) and MSRV 1.94 (op-revm).

The legacy core cannot build on revm 40, so its sources leave in the
same commit: src/, the two examples and every bench target (the test
gates rebuild them; the bench inputs under benches/data and
benches/fixtures stay). The 181 inline unit tests the test inventory
keeps move to tests/_pending/src/; the 107 it marks legacy-only or
retired go with the code. The 11 mutants suppressions on limit/* go
with limit/.
…tion table

Satin is a single-spec engine: MegaSpecId has one rung, SATIN, running on
Karst / Osaka. Display and FromStr use "Satin" and serde the variant name
"SATIN", the same forms the legacy specs use. Legacy names (Equivalence ..
Rex7) fail to parse with ParseMegaSpecError::Legacy and are never mapped to
Satin. The discriminant continues after the legacy ladder (12).

MegaHardfork has the single fork Satin. The activation table carries the
mainnet and testnet chain ids with the Satin timestamp unset.

The three spec tests the test inventory assigns to the Satin skeleton are
ported here (the Isthmus/Prague mapping test now pins Karst/Osaka).
ExternalEnvFactory, ExternalEnvTypes, ExternalEnvs, SaltEnv, OracleEnv,
EmptyExternalEnv, the bucket hasher and TestExternalEnvs come back from the
legacy core as they were, with their seven unit tests. gas.rs
(DynamicGasCost) stays out: it belongs to SALT pricing.
constants.rs holds the Satin placeholders in one table, each marked
provisional: CPSB 1,530 (slot 97,920, account 183,600), CPHB 88, the
200,000,000 execution cap and the Rex6 data-size limits.

types.rs names the transaction, halt reason, error and envelope types.
The transaction is alloy-op-evm's OpTx, which carries the signed-tx
conversions the node's block executor uses, so the core now depends on
alloy-op-evm (its op-revm edge resolves to the op-revm fork).
MemoryDatabase, ErrorInjectingDatabase, BytecodeBuilder, GasInspector and
right_pad_bytes come back from the legacy core; the only change is the
DatabaseCommit signature, which takes EvmState on revm 40. transact() and
the BytecodeBuilder execution tests follow with the engine.
MegaContext wraps op-revm's context shape (MegaTransaction, L1BlockInfo)
and holds the MegaSpecId configuration next to the OpSpecId one op-revm
executes on; both are written together. The spec fixes Karst on the Osaka
gas table, EIP-8037 and EIP-2780 on, a 200M tx_gas_limit_cap, and EIP-7708
and the system-call reservoir margin off, whatever configuration the caller
passes. Every Host and context method delegates to the wrapped context.

MegaEvm wraps OpEvm over MegaContext with op-revm's precompiles and
implements revm's execution, inspection and system-call traits and
alloy-evm's Evm; MegaEvmFactory implements alloy-evm's EvmFactory and
feeds each EVM the external environments of its block.

Tests: an empty transaction, a value transfer and one SSTORE run through
MegaEvm and through op-revm's OpEvm on the same CfgEnv and agree on every
ResultGas field, the logs and the state; the empty transaction leaves
exactly the gas above the 200M cap in the reservoir. Twelve of the
unit tests the test inventory assigns to the Satin skeleton are ported;
four that need later work are parked under the common execution layer,
the Satin gas table, SALT pricing and the block executor with the reason
in the pending README. test_utils::transact now takes the spec and the
gas limit.
…nly modules

The RLP decoding, signer recovery, deploy-address derivation and the
IKeylessDeploy error ABI move from the retired sandbox module with their
fourteen unit tests. Satin has one spec, so decode_keyless_tx drops its
spec argument and always rejects trailing bytes (the Rex5 rule); the
pre-Rex5 acceptance test goes. ParentBudgetExceeded goes too: it named the
sandbox preflight, which the native CREATE sub-frame (native keyless
deployment) retires.
…eleton

limit/ and access/ are empty modules whose doc line names the mechanisms
that fill them (the observe-stage-commit layer and the abort protocol, the
data-size limit, detention, the state-gas limits and the KV count).
MegaBlockExecutor only owns its EVM for now; block execution lands on it
with the block executor.
Re-export the upstream crates consumers must build against (revm,
op-revm, alloy-evm, alloy-op-evm, alloy-primitives, alloy-hardforks,
alloy-consensus, alloy-sol-types, op-alloy-consensus), keep the short
aliases (SpecId, Evm, EvmFactory, Context, BlockExecutor, ...), and warn
on unused crate dependencies again. derive_more is only used by the test
utilities, so it becomes an optional dependency of the test-utils feature.
Times ExecuteEvm::transact for an empty transaction and an ether transfer
through MegaEvm and through op-revm's OpEvm on the same CfgEnv. It keeps
one bench target alive until the test gates rebuild the suite.
The diff-scoped cargo-mutants run over this series left three survivors.
Two were test gaps: InspectEvm::set_inspector and MegaContext::modify_chain
now have tests that observe them. The third, From<MegaSpecId> for
EthSpecId -> Default::default(), is equivalent while revm's default spec
is Osaka, the spec SATIN maps to; it gets a line suppression, which stays
live and turns into a no-op when a revm upgrade moves the default.
AGENTS.md now describes the satin branch: one spec (SATIN on Karst), the
fork patches, the module table with the mechanism that fills each module,
the _pending test layout, and the rule that today's main is the legacy
engine and Satin is the only active spec. The unchanged conventions
(test_ prefix, no_std, determinism, cargo sort, lint and format, one
sentence per line, Prettier) stay as they were. The crate README follows,
and REVIEW.md points at the parked mutation tests.
replay-bench builds state-test, which is no longer a workspace member:
its bench job is disabled with if: false until state-test is ported to
Satin (compare needs it and is skipped with it). The benchmark matrix
shrinks to transact, the only bench target until the test gates rebuild
the suite.

The suppression-hygiene step grepped the spec-gate plan in a pipeline; on
a single-spec engine the pack finds no gates, grep exits 1 and pipefail
failed the step. The plan now runs on its own, so a planner failure still
fails, and only the grep tolerates an empty result.
The data-only keyless module dropped the variant because its kind field
was the legacy LimitKind. It comes back with the raw kind code of the
IKeylessDeploy error, so every error the interface declares still encodes
and decodes; the Satin meaning of the code is left to the limit
mechanisms that define the resource dimensions.
test_utils gains zero_fee_l1_block_info() and op_transaction(), which
the unit tests, the equivalence tests, the bench and transact() now
share instead of four copies. The equivalence tests also check the gas
table, EIP-7708 and the reservoir margin, and pin that the SSTORE draws no
state gas on the Osaka table. The external-env getter test now observes
the factory it was given, and the with_cfg test is named for what it
checks.
AGENTS.md gets back two rules later mechanisms need (only CALL and
STATICCALL reach interceptor dispatch; per-fork params are validated at
load time) and says plainly that state gas is priced at zero until the
Satin gas table lands; the crate README says the same and keeps one
sentence per line. REVIEW.md's two pointers to moved or deleted files
name both branches. The constants docs no longer cite an outside
document. The pending README lists the 36 tests ported in place, which
is why its counts sit below the test inventory's, and the manifest says
why the bench inputs stay.
The spec-gate grep accepted any exit status; it now accepts only 1 (no
match), so a real grep failure (exit 2) still fails the step.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Generated by AI agents api:breaking Crate interface change — downstream users must update comp:core Changes to the `mega-evm` core crate comp:doc Changes in the documentation comp:misc Changes to the miscellaneous part of this repo dependencies Pull requests that update a dependency file spec:new Introduces a new MegaSpecId variant

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant