diff --git a/AGENTS.md b/AGENTS.md index 4024f7e0..8f003a0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Git submodules are required — clone with `--recursive` or run `git submodule u ### Spec System (`MegaSpecId`) -Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `REX3` → `REX4` → `REX5` → `REX6` → `REX7` +Progression: `EQUIVALENCE` → `MINI_REX` → `MINI_REX_1` → `MINI_REX_2` → `REX` → `REX1` → `REX2` → `REX3` → `REX4` → `REX5` → `REX6` → `REX7` (`MINI_REX_1`/`MINI_REX_2` are alias rungs executing `EQUIVALENCE` and `MINI_REX` behavior respectively) - **Spec** defines EVM behavior (what the EVM does). Defined in `crates/mega-evm/src/evm/spec.rs`. @@ -66,10 +66,10 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R - Frozen and activated are separate properties. `REX6` is frozen but has no activation timestamp on mainnet or testnet, so both chains still execute `REX5`. Freezing forbids further semantic change; scheduling is a later, separate decision. - - Specifications of each spec can be found in the upgrade pages under `docs/spec/upgrades/`. + - Specifications of each behavior-introducing spec can be found in the upgrade pages under `docs/spec/upgrades/`; alias rungs have no page of their own and are recorded in the upgrade overview and `docs/spec/hardfork-spec.md`. - **Hardfork** (`MegaHardfork`) defines network upgrade events (when specs activate). - Multiple hardforks can map to one spec. - `MiniRex1` and `MiniRex2` are hardforks that reuse `EQUIVALENCE` and `MINI_REX` respectively. + Every hardfork schedules a spec rung of its own — the fork→spec mapping is 1:1. + `MiniRex1` and `MiniRex2` schedule the alias specs `MINI_REX_1` and `MINI_REX_2`, whose `behavior()` projects to `EQUIVALENCE` and `MINI_REX` respectively. Defined in `crates/mega-evm/src/block/hardfork.rs`. - All specs use `OpSpecId::ISTHMUS` as the Optimism base layer. But this is subject to change in the future. @@ -81,6 +81,10 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R - **`block/`** — Block execution: executor, factory, hardfork-to-spec mapping, limit enforcement, and the canonical per-chain hardfork schedules. This module defines how a block in MegaETH block should be executed. `block/chain.rs` is the single source of truth for the mainnet/testnet chain IDs and activation-timestamp schedules (`hardfork_schedule(chain_id)`, `MAINNET_CHAIN_ID`, `TESTNET_CHAIN_ID`, `mainnet_hardforks()`, `testnet_hardforks()`); look there to find or change when a fork activates on a given chain. + Its unknown-chain fallback pins a named spec rather than following the latest one, so introducing a spec does not move chains that run from genesis; advancing that pin is a deliberate edit made when a spec is sealed. + Forks map 1:1 onto an ascending spec ladder, so the resolved `spec_id` is monotone; rollbacks are alias specs (`MINI_REX_1`, `MINI_REX_2`) whose `behavior()` projects to an earlier spec. + One value, two projections that must not be confused: `is_enabled` compares behavior and gates EVM semantics (rolls back in alias windows); `reaches` compares ladder position and gates one-way chain setup such as predeploys and pre-block rules (never rolls back). + The `is__active_at_timestamp` predicates are position projections for behavior-introducing forks and raw event queries for alias forks, and `MegaHardforks::validate_schedule` is the load-time check that a published schedule climbs the ladder without gaps. - **`limit/`** — Resource limit tracking: compute gas, data size, KV updates, state growth (each in its own module). MegaETH introduces additional resource metering mechanism and this module implements their logic as utility structs to be used by mega-evm. - **`access/`** — Block env access tracking and volatile data detection for parallel execution. @@ -99,7 +103,7 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R #### Backward Compatibility of Specs -The spec system (`MegaSpecId`) forms a linear progression where each newer spec includes all previous behaviors. +The spec system (`MegaSpecId`) forms a linear progression where each newer behavior-introducing spec includes all previous behaviors; the alias rungs are the exception, re-executing an earlier spec's behavior instead. The codebase **MUST** maintain backward-compatibility: EVM semantics must never change for existing (stable) specs. The only exception is the latest spec if explicitly marked as **unstable**. Consequently: @@ -315,6 +319,7 @@ When the agent is requested to implement a new feature or bug fix, it should con - **Override `HardforkParams::validate()` for every new params type.** The default implementation accepts any value silently. Override it with field-level invariant checks (e.g., non-zero addresses) so that `with_params()` panics loudly at chain-config load time rather than allowing the error to surface at the first block where the fork activates. + Also register the fork-requires-params rule in `MegaHardforks::validate_schedule` so a schedule that activates the fork without its params fails validation, not the fork's first block. - **Pre-block helpers must return state, not commit directly.** Any helper participating in `pre_execution_changes` (system contract deploys, pre-block system calls, etc.) MUST return `Option` and never call `db.commit(...)` directly. Full convention: `crates/mega-evm/src/system/AGENTS.md` → `PRE-BLOCK STATE CHANGE CONTRACT`. diff --git a/ARCH.md b/ARCH.md index d75d1be1..2d289518 100644 --- a/ARCH.md +++ b/ARCH.md @@ -18,7 +18,7 @@ This document provides detailed technical specifications and implementation deta The implementation exposes multiple EVM versions (`MegaSpecId`). String names and hardfork-to-spec mapping live in `crates/mega-evm/src/evm/spec.rs` and `crates/mega-evm/src/block/hardfork.rs`. -Available specs: `EQUIVALENCE`, `MINI_REX`, `REX`, `REX1`, `REX2`, `REX3`, `REX4`, `REX5`, `REX6`, `REX7`. +Available specs: `EQUIVALENCE`, `MINI_REX`, `MINI_REX_1`, `MINI_REX_2`, `REX`, `REX1`, `REX2`, `REX3`, `REX4`, `REX5`, `REX6`, `REX7` (`MINI_REX_1` and `MINI_REX_2` are alias rungs executing `EQUIVALENCE` and `MINI_REX` behavior respectively). This page details only the first few; the authoritative per-spec behavior is the specification under `docs/spec/`, whose upgrade pages cover every spec. diff --git a/bin/mega-evme/README.md b/bin/mega-evme/README.md index eb4eeb7e..61511298 100644 --- a/bin/mega-evme/README.md +++ b/bin/mega-evme/README.md @@ -252,7 +252,7 @@ These options are available across all commands. | Option | Default | Description | | ---------------------- | ------- | ----------------------------------------- | -| `--spec ` | Rex7 | Spec: `Equivalence`, `MiniRex`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` | +| `--spec ` | Rex7 | Spec: `Equivalence`, `MiniRex`, `MiniRex1`, `MiniRex2`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` (`MiniRex1`/`MiniRex2` are aliases executing `Equivalence`/`MiniRex` behavior) | | `--chain-id ` | 6342 | Chain ID | ### Block Environment diff --git a/bin/mega-evme/src/common/env.rs b/bin/mega-evme/src/common/env.rs index 24c11b96..1f6b7379 100644 --- a/bin/mega-evme/src/common/env.rs +++ b/bin/mega-evme/src/common/env.rs @@ -25,8 +25,9 @@ use super::{EvmeError, Result}; #[derive(Args, Debug, Clone)] #[command(next_help_heading = "Chain Options")] pub struct ChainArgs { - /// Name of spec to use, possible values: `MiniRex`, `Equivalence`, `Rex`, `Rex1`, `Rex2`, - /// `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` + /// Name of spec to use, possible values: `Equivalence`, `MiniRex`, `MiniRex1`, `MiniRex2`, + /// `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` (`MiniRex1`/`MiniRex2` + /// are alias specs executing `Equivalence`/`MiniRex` behavior) #[arg(long = "spec", default_value = "Rex7")] pub spec: String, diff --git a/bin/mega-evme/src/common/state.rs b/bin/mega-evme/src/common/state.rs index 58177f36..acc65518 100644 --- a/bin/mega-evme/src/common/state.rs +++ b/bin/mega-evme/src/common/state.rs @@ -566,8 +566,8 @@ where // Rex5+: SequencerRegistry (v1.0.0 pre-Rex6, v2.0.0 from Rex6). Only the bytecode // is installed here — a local run has no chain-config sequencer/admin to seed (the // registry's storage is otherwise read from forked state). - if spec >= MegaSpecId::REX5 { - let code = if spec >= MegaSpecId::REX6 { + if spec.reaches(MegaSpecId::REX5) { + let code = if spec.reaches(MegaSpecId::REX6) { SEQUENCER_REGISTRY_CODE_REX6 } else { SEQUENCER_REGISTRY_CODE diff --git a/bin/mega-t8n/src/cmd.rs b/bin/mega-t8n/src/cmd.rs index 46d47152..7117b59f 100644 --- a/bin/mega-t8n/src/cmd.rs +++ b/bin/mega-t8n/src/cmd.rs @@ -88,8 +88,9 @@ pub(crate) struct Cmd { #[arg(long = "input.txs", default_value = "stdin")] pub input_txs: String, - /// Name of hardfork to use, possible values: `Equivalence`, `MiniRex`, `Rex`, `Rex1`, `Rex2`, - /// `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` + /// Name of hardfork to use, possible values: `Equivalence`, `MiniRex`, `MiniRex1`, + /// `MiniRex2`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` + /// (`MiniRex1`/`MiniRex2` are alias specs executing `Equivalence`/`MiniRex` behavior) #[arg(long = "state.fork", default_value = "MiniRex")] pub fork: String, diff --git a/crates/mega-evm/README.md b/crates/mega-evm/README.md index 0daee52e..aaefbe1e 100644 --- a/crates/mega-evm/README.md +++ b/crates/mega-evm/README.md @@ -12,11 +12,11 @@ A specialized Ethereum Virtual Machine (EVM) implementation tailored for MegaETH This codebase distinguishes between two related concepts: -- **Spec (`MegaSpecId`)**: Defines EVM behavior - what the EVM does. Values: `EQUIVALENCE`, `MINI_REX`, `REX`, `REX1`, `REX2`, `REX3`, `REX4`, `REX5`, `REX6`, `REX7` +- **Spec (`MegaSpecId`)**: Defines EVM behavior - what the EVM does. Values: `EQUIVALENCE`, `MINI_REX`, `MINI_REX_1`, `MINI_REX_2`, `REX`, `REX1`, `REX2`, `REX3`, `REX4`, `REX5`, `REX6`, `REX7` - **Hardfork (`MegaHardfork`)**: Defines network upgrade events - when specs are activated. Values: `MiniRex`, `MiniRex1`, `MiniRex2`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` -Multiple hardforks can map to the same spec. -For example, both `MiniRex` and `MiniRex2` hardforks use the `MINI_REX` spec. +The mapping between hardforks and specs is one-to-one: every hardfork schedules a spec rung of its own. +Rollbacks are expressed by alias specs whose behavior projects to an earlier spec: `MiniRex1` schedules `MINI_REX_1` (behavior: `EQUIVALENCE`) and `MiniRex2` schedules `MINI_REX_2` (behavior: `MINI_REX`). ## Key Features diff --git a/crates/mega-evm/benches/block_bench.rs b/crates/mega-evm/benches/block_bench.rs index 674bbf19..d2b01974 100644 --- a/crates/mega-evm/benches/block_bench.rs +++ b/crates/mega-evm/benches/block_bench.rs @@ -9,15 +9,14 @@ use std::convert::Infallible; use alloy_consensus::{Signed, TxLegacy}; use alloy_evm::{block::BlockExecutor, EvmEnv, EvmFactory}; -use alloy_hardforks::ForkCondition; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - BlockLimits, MegaBlockExecutionCtx, MegaBlockExecutor, MegaEvmFactory, MegaHardfork, - MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, SequencerRegistryConfig, TestExternalEnvs, - ACCESS_CONTROL_ADDRESS, ACCESS_CONTROL_CODE, HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, + BlockLimits, MegaBlockExecutionCtx, MegaBlockExecutor, MegaEvmFactory, MegaHardforkConfig, + MegaSpecId, MegaTxEnvelope, SequencerRegistryConfig, TestExternalEnvs, ACCESS_CONTROL_ADDRESS, + ACCESS_CONTROL_CODE, HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, HIGH_PRECISION_TIMESTAMP_ORACLE_CODE, KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE, LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE, MEGA_SYSTEM_ADDRESS, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX5, SEQUENCER_REGISTRY_ADDRESS, SEQUENCER_REGISTRY_CODE, @@ -74,20 +73,20 @@ fn create_deploy_tx( alloy_consensus::transaction::Recovered::new_unchecked(tx, CALLER) } -/// Hardfork config activating all hardforks from genesis. -fn all_hardforks_config() -> MegaHardforkConfig { - MegaHardforkConfig::default() - .with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex1, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex2, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex3, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex4, ForkCondition::Timestamp(0)) - .with(MegaHardfork::Rex5, ForkCondition::Timestamp(0)) - .with_params(SequencerRegistryConfig { +/// Hardfork schedule coherent with `spec`: everything through the spec's rung is active from +/// genesis, with the params Rex5+ pre-block setup needs. Each benchmark row therefore measures +/// one complete per-spec world — pre-block setup included — matching the executor's rule that +/// setup follows the executing spec. (A REX6+ row would additionally need +/// `SequencerRegistryRex6Config` attached here.) +fn hardforks_for(spec: MegaSpecId) -> MegaHardforkConfig { + let mut config = MegaHardforkConfig::default().with_all_activated_through(spec); + if spec.reaches(MegaSpecId::REX5) { + config = config.with_params(SequencerRegistryConfig { rex5_initial_sequencer: MEGA_SYSTEM_ADDRESS, rex5_initial_admin: MEGA_SYSTEM_ADDRESS, - }) + }); + } + config } /// Create block EVM environment. @@ -190,7 +189,7 @@ fn bench_block_empty_txs(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor @@ -243,7 +242,7 @@ fn bench_block_mixed_txs(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor @@ -294,7 +293,7 @@ fn bench_block_deploy(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor @@ -316,10 +315,11 @@ fn bench_block_deploy(c: &mut Criterion) { /// Benchmark spec comparison for block execution. /// -/// NOTE: All specs run with `all_hardforks_config()`, so even `EQUIVALENCE` and `MINI_REX` -/// deploy Rex4 system contracts during `pre_execution_changes`. This is intentional — -/// the benchmark isolates EVM execution behavior differences across specs, not system -/// contract deployment overhead. +/// NOTE: Each spec runs against its own coherent world (`hardforks_for(spec)`), pre-block +/// setup included: `EQUIVALENCE` deploys nothing while `REX5` bootstraps its predeploys and +/// registry inside every iteration. Rows therefore compare whole-block costs per spec, not +/// isolated EVM execution deltas — for the latter, the cross-spec compute-gas snapshot +/// (`tests/compute_gas/`) is the tool. fn bench_block_spec_comparison(c: &mut Criterion) { let mut group = c.benchmark_group("block_executor_spec_comparison"); group.sample_size(10); @@ -353,7 +353,7 @@ fn bench_block_spec_comparison(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor @@ -407,7 +407,7 @@ fn bench_rex5_pre_block(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor.apply_pre_execution_changes().expect("pre-execution changes should succeed"); @@ -435,7 +435,7 @@ fn bench_rex5_pre_block(c: &mut Criterion) { let mut executor = MegaBlockExecutor::new( evm, block_ctx, - all_hardforks_config(), + hardforks_for(spec), OpAlloyReceiptBuilder::default(), ); executor.apply_pre_execution_changes().expect("pre-execution changes should succeed"); @@ -446,6 +446,32 @@ fn bench_rex5_pre_block(c: &mut Criterion) { group.finish(); } +/// Benchmark hardfork-config resolution on the real mainnet schedule. +/// +/// The position-projected predicates (`is_rex_5_active_at_timestamp`) are what downstream node +/// components call once per block; `spec_id` is the executor's per-block resolution; +/// `validate_schedule` is a once-per-config-load cost. Resolution's descending early-exit scan +/// keeps the per-query cost at one or two activation lookups for chains near the top of the +/// ladder, which this benchmark pins. +fn bench_hardfork_resolution(c: &mut Criterion) { + use mega_evm::MegaHardforks; + + let mut group = c.benchmark_group("hardfork_resolution"); + let schedule = mega_evm::mainnet_hardforks(); + // A timestamp after the last scheduled fork: every real query happens here. + let ts = 1_800_000_000u64; + + group.bench_function("is_rex_5_active_at_timestamp", |b| { + b.iter(|| black_box(schedule.is_rex_5_active_at_timestamp(black_box(ts)))) + }); + group.bench_function("spec_id", |b| b.iter(|| black_box(schedule.spec_id(black_box(ts))))); + group.bench_function("validate_schedule", |b| { + b.iter(|| black_box(schedule.validate_schedule())) + }); + + group.finish(); +} + criterion_group!( benches, bench_block_empty_txs, @@ -453,5 +479,6 @@ criterion_group!( bench_block_deploy, bench_block_spec_comparison, bench_rex5_pre_block, + bench_hardfork_resolution, ); criterion_main!(benches); diff --git a/crates/mega-evm/src/AGENTS.md b/crates/mega-evm/src/AGENTS.md index 4dd319bf..0eab71d1 100644 --- a/crates/mega-evm/src/AGENTS.md +++ b/crates/mega-evm/src/AGENTS.md @@ -17,7 +17,7 @@ Core MegaEVM crate implementation layer that composes execution, block processin ## KEY PATTERNS - `no_std` discipline is active for this crate. - Use `#[cfg(not(feature = "std"))] use alloc as std;` when std collections are required. -- Spec progression is additive. +- Spec progression is additive for behavior-introducing specs; alias rungs (`MINI_REX_1`, `MINI_REX_2`) re-execute an earlier behavior instead. - Keep behavior gates explicit via `spec.is_enabled(...)` at call sites. - Per-frame trackers must stay stack-aligned with EVM frame lifecycle hooks. - Intercepted synthetic frame results must keep tracker alignment via empty-frame pushes. diff --git a/crates/mega-evm/src/block/AGENTS.md b/crates/mega-evm/src/block/AGENTS.md index c969f6cf..090883e3 100644 --- a/crates/mega-evm/src/block/AGENTS.md +++ b/crates/mega-evm/src/block/AGENTS.md @@ -17,16 +17,47 @@ Block execution orchestration for MegaETH, including hardfork-to-spec resolution - Pre-execution and post-execution limits are intentionally separated. - Pre-checks reject/skip before execution. - Post-checks can drop outcomes before commit. -- System contract deployments are idempotent state patches and are hardfork-gated. +- System contract deployments are idempotent state patches, position-gated on the resolved spec. - Executor constructor asserts hardfork/spec coherence for non-test builds. - Block limiter state is cumulative and must be updated only on committed outcomes. - `pre_execution_changes` collects `Option` outcomes from each helper into a vector; `commit_system_call_outcomes` walks them and calls `system_caller.on_state(source, &state)` **before** `db.commit(state)` for every entry. The `on_state` hook feeds the stateless witness generator with the complete read/write set. Helpers must therefore return all accounts and slots they touched (including reads). See `crates/mega-evm/src/system/AGENTS.md` → `PRE-BLOCK STATE CHANGE CONTRACT` for the helper-side contract. +## INVARIANT MAP +The structural invariants of the spec ladder and fork schedule, indexed by where each is enforced and when it fires. +Guards live next to the tables they guard; this map is the index, not the home. + +Compile time (const assertions and exhaustive matches; a violation fails `cargo build`): +- `MegaSpecId::ALL` lists every spec in ladder order without gaps: `is_ladder_prefix` assertion in `evm/spec.rs`. +- The `behavior()` projection is flat — no alias chains or cycles, no upward targets: `is_flat_projection` assertion in `evm/spec.rs`. +- The fork→spec map is strictly ascending and therefore 1:1: `climbs_the_spec_ladder` assertion in `hardfork.rs`. +- Every new spec must be placed everywhere it matters: exhaustive matches in `ladder_index` (`evm/spec.rs`), the instruction table (`evm/instructions.rs`), precompiles (`evm/precompiles.rs`), and runtime limits (`evm/limit.rs`) fail compilation until the variant is wired; its fork is forced the same way by the matches on the fork enum in `MegaHardfork::spec_id` (`hardfork.rs`) and block limits (`limit.rs`). + +Chain-config load time: +- A published schedule climbs the ladder in activation order with required params attached: `MegaHardforks::validate_schedule` (`hardfork.rs`); `hardfork_schedule` (`chain.rs`) debug-asserts it, and node startup should call it. +- Per-fork params invariants hold: `HardforkParams::validate` runs inside `with_params` and panics at load time, not at the fork's first block. + +Block execution time: +- The cfg spec equals the schedule's resolution: executor constructor assert (compiled out under `test`/`test-utils`; pre-block setup reads the same cfg spec as `resolve_system_address`, so a tool overriding the cfg spec gets a coherent what-if rather than a hybrid block). +- A scheduled fork whose params are missing fails closed at its first block: registry checks in `executor.rs` and `system/sequencer_registry.rs`. + +Test time (`cargo test`): +- Alias dispatch grouping agrees with `behavior()`: reconciliation tests in `evm/instructions.rs`, `evm/precompiles.rs`, `evm/limit.rs`, and `limit.rs`. +- The resolved spec equals the maximum over activated forks on every schedule shape: `test_resolved_spec_is_the_maximum_over_activated_forks` (`hardfork.rs`). +- Position projections coincide with raw activation events: parity tests on every canonical schedule (`hardfork.rs`) and on a fully staged synthetic ladder (`tests/mutation/block.rs`). +- The const checkers themselves reject malformed inputs (the real tables can never exercise the negative path): `test_is_ladder_prefix_rejects_malformed_lists`, `test_is_flat_projection_rejects_malformed_tables`, `test_climbs_the_spec_ladder_rejects_malformed_lists`. +- Golden tables pin what derivation cannot: spec names and discriminant positions (`ALL_SPECS` in `evm/spec.rs`), the exact fork→spec pairs (`hardfork.rs`), and the pairing of load-time and pre-block params rules (`tests/block_executor/partial_ladder.rs`). + +CI: +- Spec-gate mutation testing probes boundary shifts and dispatch misroutes: `mutants/operators/spec-gate/` (behavior-introducing specs only; alias rungs are not on the behavior axis). + ## ANTI-PATTERNS - Do not apply post-execution limit counters before a tx outcome is commit-eligible. - Do not bypass `pre_execution_changes` in replay or simulation paths that aim for chain equivalence. - Do not infer spec from tx fields. -- Always derive spec from hardfork activation at block timestamp. +- Gate on the one resolved spec through the right projection: `spec.is_enabled(MegaSpecId::X)` compares BEHAVIOR (both sides project through `behavior()`, so alias windows roll semantics back), `spec.reaches(MegaSpecId::X)` compares POSITION (one-way setup; alias windows do not retract it). The `is__active_at_timestamp` predicates are position projections for behavior-introducing forks and raw event queries for alias forks (`is_mini_rex_1/2_active_at_timestamp`), whose occurrence is not recoverable from the ladder; `mega_fork_activation` answers raw scheduling for any fork. +- A published chain schedule must pass `MegaHardforks::validate_schedule` (rung gaps, activation ordering, required per-fork params). The execution layer stays tolerant of malformed schedules — position-compared setup stays additive — but that tolerance is the fail-safe, not permission to publish one; `hardfork_schedule` debug-asserts it and node startup should check it. A new `HardforkParams` type must be registered in `validate_schedule`. +- One spec, two projections. `spec_id` is monotone (forks map 1:1 onto an ascending ladder; rollbacks are alias rungs like `MINI_REX_1`). Behavior (`is_enabled`) gates execution semantics: EVM behavior, block limits, transaction classification — it rolls back inside an alias window. Position (`reaches`) gates one-way chain setup: system-contract predeploys, pre-block rules, expected installed bytecode versions — retracting it during a rollback window would drop the Oracle predeploys' read-only witness entries that the on-state hook feeds to stateless proofs and the state-sync transition shard. +- Do not express "a chain running spec N" as `with_all_activated().without(fork)`. Removing a middle rung leaves later forks active, so the resolved spec stays at the top of the ladder. Use `with_all_activated_through(MegaSpecId::N)`. - Do not hardcode gas-limit assumptions outside `BlockLimits` plumbing. - Do not commit outcomes without first firing `on_state`. The two-step `on_state` → `commit` ordering is the witness-recorder contract; swapping or skipping it corrupts stateless proofs. diff --git a/crates/mega-evm/src/block/chain.rs b/crates/mega-evm/src/block/chain.rs index 7168ce42..0414ce51 100644 --- a/crates/mega-evm/src/block/chain.rs +++ b/crates/mega-evm/src/block/chain.rs @@ -12,8 +12,8 @@ use alloy_hardforks::ForkCondition; use alloy_primitives::address; use crate::{ - MegaHardfork, MegaHardforkConfig, SequencerRegistryConfig, SequencerRegistryRex6Config, - MEGA_SYSTEM_ADDRESS, + MegaHardfork, MegaHardforkConfig, MegaHardforks, MegaSpecId, SequencerRegistryConfig, + SequencerRegistryRex6Config, MEGA_SYSTEM_ADDRESS, }; /// `MegaETH` mainnet chain ID. @@ -82,12 +82,18 @@ pub fn testnet_hardforks() -> MegaHardforkConfig { /// exercise rotations without friction; a real network must attach a /// governance-approved value in its published schedule when it schedules Rex6. /// -/// Rex7 is also active at genesis here. It is the unstable spec under active -/// development and carries no behavior of its own yet, so activating it costs -/// nothing on an unknown chain while keeping the fallback on the latest spec. +/// The rung is named explicitly rather than inherited from [`MegaSpecId::default`]. +/// These chains run at genesis, so the rung *is* their semantics from block zero, +/// with no fork boundary to separate an old rule from a new one. Introducing a spec +/// must therefore not move them: a devnet that produced history under this fallback +/// would otherwise replay differently — through [`hardfork_schedule`], which is what +/// `mega-evme replay` resolves an unknown chain ID with — against the same binary +/// that produced it. Advancing this rung is a deliberate edit; it currently names +/// `REX7`, the unstable development head, which carries no behavior of its own yet, +/// so dev chains track the newest semantics at no cost. pub fn all_activated_hardforks() -> MegaHardforkConfig { MegaHardforkConfig::new() - .with_all_activated() + .with_all_activated_through(MegaSpecId::REX7) .with_params(SequencerRegistryConfig { rex5_initial_sequencer: MEGA_SYSTEM_ADDRESS, rex5_initial_admin: MEGA_SYSTEM_ADDRESS, @@ -100,11 +106,15 @@ pub fn all_activated_hardforks() -> MegaHardforkConfig { /// Mainnet (`4326`) and testnet v2 (`6343`) use their published schedules; any /// other chain gets [`all_activated_hardforks`]. pub fn hardfork_schedule(chain_id: u64) -> MegaHardforkConfig { - match chain_id { + let schedule = match chain_id { MAINNET_CHAIN_ID => mainnet_hardforks(), TESTNET_CHAIN_ID => testnet_hardforks(), _ => all_activated_hardforks(), - } + }; + // The canonical schedules are edited by hand; a malformed edit must fail at the first + // resolution, not at the first block that happens to expose it. + debug_assert_eq!(schedule.validate_schedule(), Ok(())); + schedule } #[cfg(test)] @@ -139,7 +149,7 @@ mod tests { fn test_schedule_dispatch_by_chain_id() { assert_eq!(hardfork_schedule(MAINNET_CHAIN_ID).spec_id(1780632000), MegaSpecId::REX5); assert_eq!(hardfork_schedule(TESTNET_CHAIN_ID).spec_id(1780459200), MegaSpecId::REX5); - // Unknown chain: everything active at genesis, including the unstable REX7. + // Unknown chain: every fork up to the pinned rung, active at genesis. assert_eq!(hardfork_schedule(1).spec_id(0), MegaSpecId::REX7); } @@ -176,4 +186,34 @@ mod tests { .expect("fallback schedule must carry a SequencerRegistryRex6Config"); assert!(rex6_params.rex6_min_rotation_delay > 0); } + + /// The fallback rung is pinned, not inherited from [`MegaSpecId::default`]. + /// + /// Unknown chains run their rung from genesis, so it is their semantics from block zero with + /// no fork boundary. Introducing a spec must therefore leave them alone: registering a fork + /// above the pinned rung would rewrite what history they have already produced means, and + /// `mega-evme replay` resolves an unknown chain ID through this same schedule. + /// + /// A newly introduced spec leaves this test green — the pin holding still is the safe + /// direction, so nothing fails to force a decision. What the test pins is drift: the + /// fallback silently following `MegaSpecId::default` again (it fails here once the default + /// advances past the rung), and the rung advancing without this `RUNG` constant being edited + /// in the same change. + #[test] + fn test_unknown_chain_fallback_pins_its_rung() { + let hf = all_activated_hardforks(); + const RUNG: MegaSpecId = MegaSpecId::REX7; + + assert_eq!(hf.spec_id(0), RUNG, "the rung applies from genesis"); + assert_eq!(hf.spec_id(u64::MAX), RUNG, "and is terminal — no later fork is registered"); + + for fork in MegaHardfork::VARIANTS { + let registered = hf.mega_fork_activation(*fork) != ForkCondition::Never; + assert_eq!( + registered, + RUNG.is_enabled(fork.spec_id()), + "{fork:?} registration must follow the pinned rung, not MegaSpecId::default()" + ); + } + } } diff --git a/crates/mega-evm/src/block/eips.rs b/crates/mega-evm/src/block/eips.rs index 79ea6112..13056deb 100644 --- a/crates/mega-evm/src/block/eips.rs +++ b/crates/mega-evm/src/block/eips.rs @@ -16,6 +16,7 @@ use revm::{ use crate::{ block::hardfork::MegaHardforks, ExternalEnvTypes, MegaContext, MegaEvm, MegaHaltReason, + MegaSpecId, }; /// Applies the pre-block call to the [EIP-2935] blockhashes contract, using the given block, @@ -38,7 +39,8 @@ use crate::{ /// [EIP-2935]: https://eips.ethereum.org/EIPS/eip-2935 #[inline] pub(crate) fn transact_blockhashes_contract_call( - spec: H, + hardforks: H, + setup_spec: MegaSpecId, parent_block_hash: B256, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -49,7 +51,7 @@ where INSP: Inspector>, { let block_timestamp: u64 = evm.block().timestamp.saturating_to(); - if !spec.is_prague_active_at_timestamp(block_timestamp) { + if !hardforks.is_prague_active_at_timestamp(block_timestamp) { return Ok(None); } @@ -59,7 +61,7 @@ where return Ok(None); } - let res = if spec.is_rex_5_active_at_timestamp(block_timestamp) { + let res = if setup_spec.reaches(MegaSpecId::REX5) { let gas_limit = evm.block().gas_limit.max(crate::constants::rex5::SYSTEM_CALL_GAS_LIMIT_FLOOR); evm.transact_system_call_with_gas_limit( @@ -98,7 +100,8 @@ where /// [EIP-4788]: https://eips.ethereum.org/EIPS/eip-4788 #[inline] pub(crate) fn transact_beacon_root_contract_call( - spec: H, + hardforks: H, + setup_spec: MegaSpecId, parent_beacon_block_root: Option, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -109,7 +112,7 @@ where INSP: Inspector>, { let block_timestamp: u64 = evm.block().timestamp.saturating_to(); - if !spec.is_cancun_active_at_timestamp(block_timestamp) { + if !hardforks.is_cancun_active_at_timestamp(block_timestamp) { return Ok(None); } @@ -128,7 +131,7 @@ where return Ok(None); } - let res = if spec.is_rex_5_active_at_timestamp(block_timestamp) { + let res = if setup_spec.reaches(MegaSpecId::REX5) { let gas_limit = evm.block().gas_limit.max(crate::constants::rex5::SYSTEM_CALL_GAS_LIMIT_FLOOR); evm.transact_system_call_with_gas_limit( diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 43849a77..c4ddfea5 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -26,10 +26,12 @@ use revm::{ }; use crate::{ - block::eips, flat_system_contract_specs, is_apply_pending_changes_due, resolve_system_address, - transact_apply_pending_changes, transact_deploy, transact_deploy_sequencer_registry, - BlockLimiter, BlockMegaTransactionOutcome, BucketId, MegaBlockExecutionCtx, MegaHardforks, - MegaSystemCallOutcome, MegaTransaction, MegaTransactionExt, MegaTransactionOutcome, + block::eips, flat_system_contract_specs_for, is_apply_pending_changes_due, + resolve_system_address, transact_apply_pending_changes, transact_deploy, + transact_deploy_sequencer_registry_for, BlockLimiter, BlockMegaTransactionOutcome, BucketId, + MegaBlockExecutionCtx, MegaHardforks, MegaSpecId, MegaSystemCallOutcome, MegaTransaction, + MegaTransactionExt, MegaTransactionOutcome, SequencerRegistryConfig, + SequencerRegistryRex6Config, }; /// Block executor for the `MegaETH` chain. @@ -56,7 +58,6 @@ pub struct MegaBlockExecutor { receipt_builder: R, ctx: MegaBlockExecutionCtx, system_caller: SystemCaller, - /// The inner evm instance. pub evm: E, /// The block limiter for tracking the limit usage. @@ -175,12 +176,21 @@ where // clear flag to true. self.evm.db_mut().set_state_clear_flag(true); - let block_timestamp: u64 = self.evm.block().timestamp.saturating_to(); - let is_rex_5 = self.hardforks.is_rex_5_active_at_timestamp(block_timestamp); + // Every pre-block gate below derives from the executing spec in the EVM's cfg — the + // same source `resolve_system_address` reads — compared by POSITION (`reaches`): setup + // stays additive by construction — a config that schedules only a later fork still gets + // every earlier fork's predeploys and fail-closed checks, and an alias window + // (`MINI_REX_1`, live on mainnet) rolls back behavior without dropping the Oracle + // predeploys or their read-only witness entries. On node paths the constructor asserts + // this spec equals the schedule's resolution; tools that override the cfg spec get a + // coherent what-if (setup and execution move together) instead of a hybrid block. + let setup_spec = self.evm.ctx().mega_spec(); + let is_rex_5 = setup_spec.reaches(MegaSpecId::REX5); // EIP-2935 let result_and_state = eips::transact_blockhashes_contract_call( &self.hardforks, + setup_spec, self.ctx.parent_hash, &mut self.evm, )?; @@ -202,6 +212,7 @@ where // EIP-4788 let result_and_state = eips::transact_beacon_root_contract_call( &self.hardforks, + setup_spec, self.ctx.parent_beacon_block_root, &mut self.evm, )?; @@ -231,7 +242,7 @@ where // MegaAccessControl, MegaLimitControl) share one deploy path via the canonical // registry. We tentatively use `StateChangeSource::Transaction(0)` as the state // change source, as alloy defines no specific source for these predeploys. - for spec in flat_system_contract_specs(&self.hardforks, block_timestamp) { + for spec in flat_system_contract_specs_for(setup_spec) { let state = transact_deploy(self.evm.db_mut(), &spec).map_err(BlockExecutionError::other)?; outcomes @@ -244,14 +255,15 @@ where if is_rex_5 { // Deploy: seeds system address, sequencer, admin, and initialFromBlock // into storage on first deploy. - // Cloned so the helper below can take `&mut self`; two addresses, once per block. + // Cloned so the helper below can take `&mut self`; a few words, once per block. let params = self .hardforks - .fork_params::() + .fork_params::() .ok_or_else(|| BlockValidationError::BlockHashContractCall { message: "Rex5 active but SequencerRegistryConfig not configured".into(), })? .clone(); + let rex6_params = self.hardforks.fork_params::().cloned(); // The deploy and apply-pending-changes outcomes commit in push order, while the // apply system call always executes against the not-yet-committed state and thus @@ -264,11 +276,12 @@ where // `applyPendingChanges()` logic is identical in v1/v2 (v2 changes only rotation // scheduling), so its semantics do not depend on which side of the deploy it // executes. Pre-Rex6 blocks keep the original deploy-then-apply order untouched. - let is_rex_6 = self.hardforks.is_rex_6_active_at_timestamp(block_timestamp); + let is_rex_6 = setup_spec.reaches(MegaSpecId::REX6); if !is_rex_6 { self.push_deploy_sequencer_registry_outcome( - block_timestamp, + setup_spec, + rex6_params.as_ref(), block_number, ¶ms, &mut outcomes, @@ -298,7 +311,8 @@ where if is_rex_6 { self.push_deploy_sequencer_registry_outcome( - block_timestamp, + setup_spec, + rex6_params.as_ref(), block_number, ¶ms, &mut outcomes, @@ -313,14 +327,15 @@ where /// and pushes its outcome. fn push_deploy_sequencer_registry_outcome( &mut self, - block_timestamp: u64, + setup_spec: MegaSpecId, + rex6_params: Option<&SequencerRegistryRex6Config>, block_number: u64, - params: &crate::SequencerRegistryConfig, + params: &SequencerRegistryConfig, outcomes: &mut Vec, ) -> Result<(), BlockExecutionError> { - let result_and_state = transact_deploy_sequencer_registry( - &self.hardforks, - block_timestamp, + let result_and_state = transact_deploy_sequencer_registry_for( + setup_spec, + rex6_params, block_number, self.evm.db_mut(), params, @@ -665,7 +680,9 @@ where // After all pre-block outcomes are committed, resolve the system address for this block. // This reads _currentSystemAddress from the now-committed SequencerRegistry storage. - // The returned EvmState captures the read as a witness record. + // The returned EvmState captures the read as a witness record. Inside the resolver the + // behavior projection gates whether dynamic resolution applies, and the position + // projection selects the expected registry bytecode version. let spec = self.evm.ctx().mega_spec(); let (system_address, read_state) = resolve_system_address(&self.hardforks, spec, self.evm.db_mut())?; diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 98283482..82e02404 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -8,7 +8,7 @@ use auto_impl::auto_impl; use core::any::Any; use std::{boxed::Box, sync::Arc, vec::Vec}; -use crate::MegaSpecId; +use crate::{MegaSpecId, SequencerRegistryConfig, SequencerRegistryRex6Config}; hardfork! { /// The name of MegaETH hardforks. It is expected to mix with [`EthereumHardfork`] and @@ -17,9 +17,9 @@ hardfork! { MegaHardfork { /// The first hardfork. MiniRex, - /// The first patch hardfork to MiniRex. + /// The first alias hardfork: rolls behavior back to Equivalence on its own rung. MiniRex1, - /// The second patch hardfork to MiniRex. + /// The second alias hardfork: restores MiniRex behavior on its own rung. MiniRex2, /// The fourth hardfork. Rex, @@ -41,15 +41,21 @@ hardfork! { } impl MegaHardfork { + /// Whether this fork introduces new behavior — its spec is not an alias. Alias forks + /// (`MiniRex1`, `MiniRex2`) schedule a rung whose behavior belongs to an earlier spec. + pub(crate) fn introduces_behavior(self) -> bool { + !self.spec_id().is_alias() + } + /// Gets the `MegaSpecId` associated with this hardfork. #[allow(clippy::match_same_arms)] - pub fn spec_id(&self) -> MegaSpecId { - // Note: MiniRex1 and MiniRex2 are patch hardforks that intentionally reverted to - // previously released specs rather than introducing new EVM semantics. + pub const fn spec_id(&self) -> MegaSpecId { + // Note: MiniRex1 and MiniRex2 map to alias specs: rungs of their own whose + // behavior projects to previously released specs. match self { Self::MiniRex => MegaSpecId::MINI_REX, - Self::MiniRex1 => MegaSpecId::EQUIVALENCE, - Self::MiniRex2 => MegaSpecId::MINI_REX, + Self::MiniRex1 => MegaSpecId::MINI_REX_1, + Self::MiniRex2 => MegaSpecId::MINI_REX_2, Self::Rex => MegaSpecId::REX, Self::Rex1 => MegaSpecId::REX1, Self::Rex2 => MegaSpecId::REX2, @@ -62,6 +68,34 @@ impl MegaHardfork { } } +/// Whether each fork in `forks` maps to a strictly higher spec rung than the fork before it. +/// +/// This is the invariant the fork→spec map rests on: the reverse scan in +/// [`MegaHardforks::hardfork`] and the skipped-rung rule in +/// [`MegaHardforks::validate_schedule`] assume declaration order climbs the ladder, and +/// strictness pins injectivity — no two forks share a rung, so the map stays 1:1 and the +/// resolved spec is monotone on any ordered schedule. +/// +/// Shared by the compile-time assertion below and by the test that feeds it malformed lists, +/// so the checker itself is exercised — the real `VARIANTS` always satisfies the property it +/// checks. +const fn climbs_the_spec_ladder(forks: &[MegaHardfork]) -> bool { + let mut i = 1; + while i < forks.len() { + if forks[i - 1].spec_id() as u8 >= forks[i].spec_id() as u8 { + return false; + } + i += 1; + } + true +} + +const _: () = assert!( + climbs_the_spec_ladder(MegaHardfork::VARIANTS), + "every hardfork must map to a strictly higher spec rung than the fork declared before it — \ + express a rollback as a new alias rung, not by reusing an earlier spec" +); + /// Validation error returned by [`HardforkParams::validate`]. #[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)] #[display("{message}")] @@ -90,6 +124,23 @@ pub trait HardforkParams: Any + core::fmt::Debug + Send + Sync { } /// Extends [`OpHardforks`] with `MegaETH` helper methods. +/// +/// Forks map 1:1 onto specs, so [`spec_id`](Self::spec_id) — the latest activated fork's spec — +/// is monotone: it only ever climbs. Rollbacks are expressed by *alias specs* (`MINI_REX_1`, +/// `MINI_REX_2`): rungs of their own whose behavior projects to an earlier spec. One resolved +/// value is therefore read through two projections: +/// +/// - `spec.is_enabled(X)` — the BEHAVIOR gate: both sides project through [`MegaSpecId::behavior`], +/// so execution semantics roll back inside an alias window. +/// - `spec.reaches(X)` — the POSITION gate: raw rung comparison for one-way chain setup +/// (predeploys, bytecode versions, pre-block rules), which a rollback does not retract. +/// +/// The `is__active_at_timestamp` predicates are position projections for +/// behavior-introducing forks (additive on schedules that omit a predecessor) and raw event +/// queries for alias forks, whose occurrence is not recoverable from the ladder. To ask about +/// the raw scheduling event of any fork, use `mega_fork_activation` directly. To check a +/// schedule for well-formedness (rung gaps, ordering, required params), use +/// [`validate_schedule`](Self::validate_schedule). #[auto_impl(&, Box, Arc)] pub trait MegaHardforks: OpHardforks { /// Retrieves [`ForkCondition`] by a [`MegaHardfork`]. If `fork` is not present, returns @@ -111,32 +162,22 @@ pub trait MegaHardforks: OpHardforks { } /// Returns the current `MegaHardfork` active at the given timestamp. - fn hardfork(&self, timestamp: u64) -> Option { - if self.is_rex_7_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex7) - } else if self.is_rex_6_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex6) - } else if self.is_rex_5_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex5) - } else if self.is_rex_4_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex4) - } else if self.is_rex_3_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex3) - } else if self.is_rex_2_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex2) - } else if self.is_rex_1_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex1) - } else if self.is_rex_active_at_timestamp(timestamp) { - Some(MegaHardfork::Rex) - } else if self.is_mini_rex_2_active_at_timestamp(timestamp) { - Some(MegaHardfork::MiniRex2) - } else if self.is_mini_rex_1_active_at_timestamp(timestamp) { - Some(MegaHardfork::MiniRex1) - } else if self.is_mini_rex_active_at_timestamp(timestamp) { - Some(MegaHardfork::MiniRex) - } else { - None - } + /// + /// Resolution walks the declaration ladder from the top: the latest-declared fork whose + /// activation event has occurred wins. Declaration order is chronological, so at equal + /// activation timestamps the later-declared fork prevails. Driven off + /// [`MegaHardfork::VARIANTS`] so a newly declared fork joins resolution without a second + /// hand-written ladder here. + /// + /// Resolution reads raw activation events + /// ([`mega_fork_activation`](Self::mega_fork_activation)); with the 1:1 ascending fork->spec + /// map, the resolved spec is monotone in time on any ordered schedule. + fn hardfork(&self, timestamp: BlockTimestamp) -> Option { + MegaHardfork::VARIANTS + .iter() + .rev() + .find(|fork| self.mega_fork_activation(**fork).active_at_timestamp(timestamp)) + .copied() } /// Returns the current `MegaSpecId` for the given block timestamp. @@ -144,62 +185,216 @@ pub trait MegaHardforks: OpHardforks { self.hardfork(timestamp).map_or(MegaSpecId::EQUIVALENCE, |h| h.spec_id()) } - /// Returns `true` if [`MegaHardfork::MiniRex`] is active at given block timestamp. - fn is_mini_rex_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::MiniRex).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::MINI_REX`], the + /// spec introduced by [`MegaHardfork::MiniRex`]. Position-projected (`reaches`) — see the trait + /// docs; for the raw activation event use + /// [`mega_fork_activation`](Self::mega_fork_activation). + fn is_mini_rex_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::MINI_REX) } - /// Returns `true` if [`MegaHardfork::MiniRex1`] is active at given block timestamp. - fn is_mini_rex_1_active_at_timestamp(&self, timestamp: u64) -> bool { + /// Returns `true` if the [`MegaHardfork::MiniRex1`] activation event has occurred at the + /// given block timestamp. + /// + /// `MiniRex1` schedules the alias rung [`MegaSpecId::MINI_REX_1`], whose occurrence is not + /// recoverable from the ladder position (later rungs stand above it whether or not it was + /// ever scheduled), so this predicate stays a raw event query, unlike the + /// position-projected predicates of behavior-introducing forks. + fn is_mini_rex_1_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex1).active_at_timestamp(timestamp) } - /// Returns `true` if [`MegaHardfork::MiniRex2`] is active at given block timestamp. - fn is_mini_rex_2_active_at_timestamp(&self, timestamp: u64) -> bool { + /// Returns `true` if the [`MegaHardfork::MiniRex2`] activation event has occurred at the + /// given block timestamp. + /// + /// `MiniRex2` schedules the alias rung [`MegaSpecId::MINI_REX_2`], whose occurrence is not + /// recoverable from the ladder position, so this predicate stays a raw event query, unlike + /// the position-projected predicates of behavior-introducing forks. + fn is_mini_rex_2_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex2).active_at_timestamp(timestamp) } - /// Returns `true` if [`MegaHardfork::Rex`] is active at given block timestamp. - fn is_rex_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX`], the rung + /// introduced by [`MegaHardfork::Rex`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX) } - /// Returns `true` if [`MegaHardfork::Rex1`] is active at given block timestamp. - fn is_rex_1_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex1).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX1`], the rung + /// introduced by [`MegaHardfork::Rex1`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_1_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX1) } - /// Returns `true` if [`MegaHardfork::Rex2`] is active at given block timestamp. - fn is_rex_2_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex2).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX2`], the rung + /// introduced by [`MegaHardfork::Rex2`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_2_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX2) } - /// Returns `true` if [`MegaHardfork::Rex3`] is active at given block timestamp. - fn is_rex_3_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex3).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX3`], the rung + /// introduced by [`MegaHardfork::Rex3`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_3_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX3) } - /// Returns `true` if [`MegaHardfork::Rex4`] is active at given block timestamp. - fn is_rex_4_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex4).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX4`], the rung + /// introduced by [`MegaHardfork::Rex4`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_4_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX4) } - /// Returns `true` if [`MegaHardfork::Rex5`] is active at given block timestamp. - fn is_rex_5_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex5).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX5`], the rung + /// introduced by [`MegaHardfork::Rex5`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_5_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX5) } - /// Returns `true` if [`MegaHardfork::Rex6`] is active at given block timestamp. - fn is_rex_6_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex6).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX6`], the rung + /// introduced by [`MegaHardfork::Rex6`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_6_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX6) } - /// Returns `true` if [`MegaHardfork::Rex7`] is active at given block timestamp. - fn is_rex_7_active_at_timestamp(&self, timestamp: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex7).active_at_timestamp(timestamp) + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX7`], the rung + /// introduced by [`MegaHardfork::Rex7`]. Position-projected (`reaches`) — see the trait docs; + /// for the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + fn is_rex_7_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { + self.spec_id(timestamp).reaches(MegaSpecId::REX7) + } + + /// Checks the schedule for well-formedness, i.e., that it describes a chain climbing the + /// spec ladder rung by rung. + /// + /// This is the fail-fast complement to the fail-safe execution layer: block execution + /// tolerates a malformed schedule (setup gates compare rung positions, so a skipped rung's + /// setup still runs), but a real chain's published schedule with a skipped rung is almost + /// certainly a configuration mistake. Chain-config authors and node startup + /// paths should call this and refuse a schedule that does not validate. + /// + /// Checked, in order: + /// + /// - Every registered `MegaHardfork` activates by [`ForkCondition::Timestamp`] (or is + /// [`ForkCondition::Never`]). Block-number and TTD conditions never report active to the + /// timestamp-scoped resolution here and would silently deactivate the fork. + /// - Scheduled forks activate in declaration order: a later-declared fork must not activate + /// strictly before an earlier-declared one. + /// - No skipped rungs: a behavior-introducing fork may be unscheduled only if no scheduled fork + /// maps to an equal-or-higher rung. Alias forks are exempt as the *missing* side — testnet + /// legitimately never schedules `MiniRex1`/`MiniRex2` — but an alias scheduled without its + /// base is itself a skipped rung (the alias's rung sits above the base's). + /// - Per-fork parameters required by a scheduled fork are present (`Rex5` requires + /// [`SequencerRegistryConfig`](crate::SequencerRegistryConfig), `Rex6` requires + /// [`SequencerRegistryRex6Config`](crate::SequencerRegistryRex6Config)). This surfaces a + /// missing config at load time rather than at the first block of the fork. A new + /// [`HardforkParams`] type must be registered here. + fn validate_schedule(&self) -> Result<(), ScheduleError> { + let scheduled = + |fork: MegaHardfork| self.mega_fork_activation(fork) != ForkCondition::Never; + + let mut prev: Option<(MegaHardfork, u64)> = None; + for fork in MegaHardfork::VARIANTS { + match self.mega_fork_activation(*fork) { + ForkCondition::Never => {} + ForkCondition::Timestamp(timestamp) => { + if let Some((earlier, earlier_timestamp)) = prev { + if timestamp < earlier_timestamp { + return Err(ScheduleError::UnorderedForks { + earlier, + earlier_timestamp, + later: *fork, + later_timestamp: timestamp, + }); + } + } + prev = Some((*fork, timestamp)); + } + _ => return Err(ScheduleError::NonTimestampActivation { fork: *fork }), + } + } + + for fork in MegaHardfork::VARIANTS { + if !fork.introduces_behavior() || scheduled(*fork) { + continue; + } + if let Some(above) = MegaHardfork::VARIANTS + .iter() + .find(|g| scheduled(**g) && g.spec_id() >= fork.spec_id()) + { + return Err(ScheduleError::SkippedRung { missing: *fork, scheduled: *above }); + } + } + + if scheduled(MegaHardfork::Rex5) && self.fork_params::().is_none() + { + return Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex5, + params: "SequencerRegistryConfig", + }); + } + if scheduled(MegaHardfork::Rex6) && + self.fork_params::().is_none() + { + return Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex6, + params: "SequencerRegistryRex6Config", + }); + } + + Ok(()) } } +/// A malformed hardfork schedule, as reported by [`MegaHardforks::validate_schedule`]. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display, derive_more::Error)] +pub enum ScheduleError { + /// A later-declared fork activates strictly before an earlier-declared one. + #[display( + "hardfork {later:?} (t={later_timestamp}) activates before {earlier:?} (t={earlier_timestamp})" + )] + UnorderedForks { + /// The earlier-declared fork. + earlier: MegaHardfork, + /// Activation timestamp of the earlier-declared fork. + earlier_timestamp: u64, + /// The later-declared fork that activates too early. + later: MegaHardfork, + /// Activation timestamp of the later-declared fork. + later_timestamp: u64, + }, + /// A spec-introducing fork is unscheduled while a fork of an equal-or-higher spec is + /// scheduled — the ladder skips a rung. + #[display("hardfork {missing:?} is unscheduled but {scheduled:?} is scheduled above it")] + SkippedRung { + /// The unscheduled spec-introducing fork. + missing: MegaHardfork, + /// A scheduled fork whose spec is equal or higher. + scheduled: MegaHardfork, + }, + /// A `MegaHardfork` is registered with a block-number or TTD condition, which the + /// timestamp-scoped resolution never reports as active. + #[display("hardfork {fork:?} must activate by timestamp, not block number or TTD")] + NonTimestampActivation { + /// The fork with a non-timestamp activation condition. + fork: MegaHardfork, + }, + /// A scheduled fork requires per-fork parameters that are not attached. + #[display("hardfork {fork:?} is scheduled but its {params} params are not configured")] + MissingParams { + /// The scheduled fork whose params are missing. + fork: MegaHardfork, + /// The required params type. + params: &'static str, + }, +} + /// A single fork entry: identity, activation condition, and optional per-fork parameters. #[derive(Debug)] struct ForkEntry { @@ -300,22 +495,28 @@ impl MegaHardforkConfig { } /// Activates every `MegaHardfork` whose spec is enabled under `spec` at timestamp 0, and - /// unregisters every later fork. + /// leaves every later fork unregistered. /// /// This is how to express "a chain running spec N": the resulting config resolves to `spec` /// at any timestamp. Removing only the next fork up is not equivalent — the ladder runs past /// it, so the config would resolve to the newest fork still registered rather than to `spec`, - /// and it would silently drift again the next time a spec is introduced. + /// and it would silently drift again the next time a spec is introduced. On top of the wrong + /// resolved spec, the leftover later forks also keep the scheduled spec high, so every + /// pre-block setup gate below them stays open. /// - /// The result is a function of `spec` alone, not of what the config held before: a later fork - /// already registered is removed rather than left in place, so the resolved spec does not - /// depend on builder call order. + /// The *activations* in the result are a function of `spec` alone, not of what the config + /// held before: a later fork already registered is removed rather than left in place, so the + /// resolved spec does not depend on builder call order. Per-fork params are the exception: + /// params on a kept fork are preserved, while params on a removed fork are dropped with its + /// entry and are not restored by a later climb back up — re-attach them with + /// [`with_params`](Self::with_params). /// - /// Patch hardforks are included by their spec, not their position: `MiniRex1` maps back to - /// [`MegaSpecId::EQUIVALENCE`], so it is registered for every `spec`. + /// Alias forks sit on their own rungs above their base (`MINI_REX_1`/`MINI_REX_2` above + /// `MINI_REX`), so climbing through a rung includes everything below it — aliases and + /// bases alike — with no special casing. pub fn with_all_activated_through(mut self, spec: MegaSpecId) -> Self { for fork in MegaHardfork::VARIANTS { - if spec.is_enabled(fork.spec_id()) { + if spec.reaches(fork.spec_id()) { self.insert(*fork, ForkCondition::Timestamp(0)); } else { self = self.without(*fork); @@ -349,7 +550,18 @@ impl MegaHardforkConfig { /// Removes a `MegaHardfork` from the configuration, i.e., equivalent to setting the fork /// condition to [`ForkCondition::Never`]. - pub fn without(mut self, hardfork: MegaHardfork) -> Self { + /// + /// Deliberately private: `with_all_activated().without(fork)` reads as "a chain running the + /// spec below `fork`" but does not express it — the forks above `fork` stay registered, so + /// the config resolves above the intended rung and silently climbs again with the next spec. + /// Say it with [`with_all_activated_through`](Self::with_all_activated_through) instead. + /// + /// This narrows an idiom, not a capability: [`with`](Self::with) with + /// [`ForkCondition::Never`] produces the same unregistered fork, and must stay public because + /// the canonical schedules use it for exactly that (testnet's `MiniRex1` / `MiniRex2` never + /// activate). Writing a gap that way is at least visibly deliberate, and it keeps the entry + /// so [`with_params`](Self::with_params) can still attach to it. + fn without(mut self, hardfork: MegaHardfork) -> Self { self.entries.retain(|e| e.fork.name() != hardfork.name()); self } @@ -413,13 +625,35 @@ mod tests { use super::*; use crate::SequencerRegistryConfig; + #[test] + fn test_climbs_the_spec_ladder_rejects_malformed_lists() { + // The real `VARIANTS` is checked at compile time by the const assertion; only + // rejection cases can detect a weakened guard inside the checker. + assert!(climbs_the_spec_ladder(MegaHardfork::VARIANTS)); + assert!(climbs_the_spec_ladder(&[]), "the empty list climbs trivially"); + assert!(climbs_the_spec_ladder(&[MegaHardfork::Rex]), "a single fork climbs trivially"); + assert!( + climbs_the_spec_ladder(&[MegaHardfork::MiniRex, MegaHardfork::Rex5]), + "gaps are fine — strictly ascending is the property, contiguity is not" + ); + + assert!( + !climbs_the_spec_ladder(&[MegaHardfork::Rex, MegaHardfork::MiniRex]), + "a fork mapping below its predecessor must be rejected" + ); + assert!( + !climbs_the_spec_ladder(&[MegaHardfork::Rex, MegaHardfork::Rex]), + "two forks sharing a rung must be rejected — the map must stay 1:1" + ); + } + #[test] fn test_mega_hardfork_spec_ids_match_expected_specs() { - // Note: MiniRex1 and MiniRex2 are patch hardforks that reverted to earlier specs. + // Note: MiniRex1 and MiniRex2 map to alias rungs whose behavior reverts to earlier specs. let cases = [ (MegaHardfork::MiniRex, MegaSpecId::MINI_REX), - (MegaHardfork::MiniRex1, MegaSpecId::EQUIVALENCE), - (MegaHardfork::MiniRex2, MegaSpecId::MINI_REX), + (MegaHardfork::MiniRex1, MegaSpecId::MINI_REX_1), + (MegaHardfork::MiniRex2, MegaSpecId::MINI_REX_2), (MegaHardfork::Rex, MegaSpecId::REX), (MegaHardfork::Rex1, MegaSpecId::REX1), (MegaHardfork::Rex2, MegaSpecId::REX2), @@ -495,45 +729,6 @@ mod tests { } } - #[test] - fn test_with_all_activated_through_resolves_to_that_spec() { - // The contract callers rely on: the config resolves to exactly the spec asked for, at any - // timestamp. The list below is hand-written, so introducing a spec does not fail here on - // its own — pair it with a builder that derives from the fork ladder, and add the new - // spec here so the rung it names is covered too. - for spec in [ - MegaSpecId::EQUIVALENCE, - MegaSpecId::MINI_REX, - MegaSpecId::REX, - MegaSpecId::REX1, - MegaSpecId::REX2, - MegaSpecId::REX3, - MegaSpecId::REX4, - MegaSpecId::REX5, - MegaSpecId::REX6, - MegaSpecId::REX7, - ] { - let config = MegaHardforkConfig::default().with_all_activated_through(spec); - assert_eq!(config.spec_id(0), spec, "{spec:?} at genesis"); - assert_eq!(config.spec_id(u64::MAX), spec, "{spec:?} must be terminal"); - - // The same contract must hold when the config already carries later forks: the - // builder states the whole ladder, so it removes what it does not activate. Without - // that, the resolved spec would depend on which builder ran last. - let downgraded = - MegaHardforkConfig::default().with_all_activated().with_all_activated_through(spec); - assert_eq!(downgraded.spec_id(u64::MAX), spec, "{spec:?} from an activated config"); - } - - // `MegaSpecId::default()` tracks the latest spec, so the unqualified builder is the - // through-builder at the top of the ladder — and every fork is registered. - let all = MegaHardforkConfig::default().with_all_activated(); - assert_eq!(all.spec_id(0), MegaSpecId::default()); - for fork in MegaHardfork::VARIANTS { - assert_eq!(all.mega_fork_activation(*fork), ForkCondition::Timestamp(0), "{fork:?}"); - } - } - #[test] fn test_fork_params_typed_access() { let params = SequencerRegistryConfig { @@ -598,6 +793,355 @@ mod tests { MegaHardforkConfig::default().with_all_activated().with_params(AlwaysErrParams); } + /// Mainnet runs a real behavior rollback: inside the `MiniRex1` window the scheduled spec + /// is the alias rung `MINI_REX_1` — behavior projects to `EQUIVALENCE` while position keeps + /// one-way setup (the Oracle predeploys) enabled. + #[test] + fn test_mainnet_rollback_window_behavior_rolls_back_setup_stays() { + let hf = crate::mainnet_hardforks(); + let ForkCondition::Timestamp(rollback_start) = + hf.mega_fork_activation(MegaHardfork::MiniRex1) + else { + panic!("mainnet must schedule MiniRex1 by timestamp"); + }; + let ForkCondition::Timestamp(rollback_end) = + hf.mega_fork_activation(MegaHardfork::MiniRex2) + else { + panic!("mainnet must schedule MiniRex2 by timestamp"); + }; + assert!(rollback_start < rollback_end, "the rollback window must be non-empty"); + + for ts in [rollback_start, rollback_start + 1, rollback_end - 1] { + let spec = hf.spec_id(ts); + assert_eq!(spec, MegaSpecId::MINI_REX_1, "the scheduled spec is monotone"); + assert_eq!(spec.behavior(), MegaSpecId::EQUIVALENCE, "behavior rolls back"); + // Behavior gates turn MiniRex features off; position gates keep its setup. + assert!(!spec.is_enabled(MegaSpecId::MINI_REX)); + assert!(spec.reaches(MegaSpecId::MINI_REX)); + assert!(hf.is_mini_rex_active_at_timestamp(ts)); + } + } + + /// The scheduled spec's position reproduces every per-fork activation event exactly, for + /// every behavior-introducing fork, on every canonical schedule. This is what makes the + /// position-projected predicates a no-op switch on well-formed ladders. + /// + /// Forks that introduce no new behavior — `MiniRex1` (rollback to `EQUIVALENCE`) and + /// `MiniRex2` (restoration to `MINI_REX`) — may be omitted from a schedule whose ladder + /// climbs past their rungs, so their activation events are not recoverable from position and + /// nothing may gate on them this way. The predicate is derived rather than hardcoded so a + /// future rollback fork is classified automatically. + #[test] + fn test_position_matches_per_fork_activation_for_behavior_introducing_forks() { + for hf in [ + crate::mainnet_hardforks(), + crate::testnet_hardforks(), + crate::all_activated_hardforks(), + ] { + let mut stamps = std::vec![0u64, u64::MAX]; + for fork in MegaHardfork::VARIANTS { + if let ForkCondition::Timestamp(t) = hf.mega_fork_activation(*fork) { + stamps.extend([t.saturating_sub(1), t, t.saturating_add(1)]); + } + } + + for ts in stamps { + let resolved = hf.spec_id(ts); + for fork in MegaHardfork::VARIANTS { + if !fork.introduces_behavior() { + continue; + } + assert_eq!( + resolved.reaches(fork.spec_id()), + hf.mega_fork_activation(*fork).active_at_timestamp(ts), + "position disagrees with per-fork activation for {fork:?} at ts={ts}" + ); + } + } + } + } + + /// On a partial ladder — a config that schedules a later fork without its predecessors — the + /// resolved spec still enables every lower spec, and the spec-introducing predicates, being + /// position projections, report active for forks that were never scheduled. Gating on either + /// can therefore not silently skip a predecessor's behavior; only the raw activation events + /// distinguish the missing rungs. + #[test] + fn test_partial_ladder_resolved_spec_enables_unscheduled_predecessors() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::Rex5, ForkCondition::Never) + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); + + assert_eq!(hf.mega_fork_activation(MegaHardfork::Rex5), ForkCondition::Never); + assert_eq!(hf.mega_fork_activation(MegaHardfork::MiniRex), ForkCondition::Never); + + let resolved = hf.spec_id(0); + assert_eq!(resolved, MegaSpecId::REX6); + for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX2, MegaSpecId::REX4, MegaSpecId::REX5] { + assert!(resolved.is_enabled(spec), "{spec:?} must be enabled on a partial ladder"); + } + // The predicates project the position: unscheduled predecessors still report active. + assert!(hf.is_rex_5_active_at_timestamp(0), "Rex5 predicate projects the position"); + assert!(hf.is_mini_rex_active_at_timestamp(0), "MiniRex predicate projects the position"); + } + + /// `with_all_activated_through` is the well-formed way to express "a chain running spec N": + /// the resolved spec is exactly `N` at any timestamp. The specs under test come from + /// [`MegaSpecId::ALL`], so a newly introduced spec is covered here automatically instead of + /// depending on a hand-written list. + #[test] + fn test_with_all_activated_through_resolves_to_that_spec() { + for spec in MegaSpecId::ALL.iter().copied() { + let config = MegaHardforkConfig::default().with_all_activated_through(spec); + assert_eq!(config.spec_id(0), spec, "{spec:?} at genesis"); + assert_eq!(config.spec_id(u64::MAX), spec, "{spec:?} must be terminal"); + + // The same contract must hold when the config already carries later forks: the + // builder states the whole ladder, so it removes what it does not activate. Without + // that, the resolved spec would depend on which builder ran last. + let downgraded = + MegaHardforkConfig::default().with_all_activated().with_all_activated_through(spec); + assert_eq!(downgraded.spec_id(u64::MAX), spec, "{spec:?} from an activated config"); + } + } + + /// Removing a middle rung does NOT express "a chain running spec N". It is the partial-ladder + /// shape: the resolved spec follows the newest fork still registered, and — with the + /// predicate projected from its position — keeps every lower gate open. + #[test] + fn test_removing_a_middle_rung_does_not_lower_the_spec() { + let partial = + MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex4); + + assert_eq!( + partial.mega_fork_activation(MegaHardfork::Rex4), + ForkCondition::Never, + "Rex4 itself is unregistered" + ); + assert_ne!(partial.spec_id(0), MegaSpecId::REX4, "the resolved spec is not lowered"); + assert!( + partial.spec_id(0).is_enabled(MegaSpecId::REX4), + "the resolved spec still enables the removed fork's spec" + ); + assert!(partial.is_rex_4_active_at_timestamp(0), "the projected predicate stays open"); + } + + /// Documented domain limit: resolution is timestamp-scoped, so a `MegaHardfork` registered + /// by block number never contributes its own spec to it. `spec_id`/`hardfork` share this + /// limitation; every canonical schedule uses `Timestamp` or `Never`, and + /// `validate_schedule` rejects anything else. + #[test] + fn test_resolution_ignores_block_numbered_forks() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::MiniRex, ForkCondition::Block(0)) + .with(MegaHardfork::Rex, ForkCondition::Timestamp(0)); + + assert!( + !hf.mega_fork_activation(MegaHardfork::MiniRex).active_at_timestamp(0), + "block-numbered forks are not timestamped" + ); + // The resolved spec comes from Rex alone; it still covers MINI_REX by ordinal + // inclusion, so the projected predicate reports active even though the MiniRex event + // itself never fires. + assert_eq!(hf.spec_id(0), MegaSpecId::REX); + assert!(hf.spec_id(0).is_enabled(MegaSpecId::MINI_REX)); + assert!(hf.is_mini_rex_active_at_timestamp(0)); + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::NonTimestampActivation { fork: MegaHardfork::MiniRex }) + ); + } + + /// The resolved spec must equal the naive maximum over all activated forks, on every + /// schedule shape: canonical ladders, rollback windows, partial ladders, block-numbered + /// conditions, and the empty config. Under the 1:1 ascending fork→spec map the latest + /// activated fork IS the maximum; the const assertion on `climbs_the_spec_ladder` pins that + /// statically for declaration order, and this pins it dynamically across schedule shapes. + #[test] + fn test_resolved_spec_is_the_maximum_over_activated_forks() { + let configs = [ + crate::mainnet_hardforks(), + crate::testnet_hardforks(), + crate::all_activated_hardforks(), + MegaHardforkConfig::new(), + // Partial ladders: a lone top rung, and a lone alias fork. + MegaHardforkConfig::new().with(MegaHardfork::Rex6, ForkCondition::Timestamp(7)), + MegaHardforkConfig::new().with(MegaHardfork::MiniRex2, ForkCondition::Timestamp(7)), + // Non-timestamp conditions never contribute. + MegaHardforkConfig::new() + .with(MegaHardfork::MiniRex, ForkCondition::Block(0)) + .with(MegaHardfork::Rex2, ForkCondition::Timestamp(7)), + ]; + let rungs: Vec = MegaSpecId::ALL + .iter() + .map(|spec| MegaHardforkConfig::default().with_all_activated_through(*spec)) + .collect(); + + for hf in configs.iter().chain(rungs.iter()) { + let mut stamps = std::vec![0u64, u64::MAX]; + for fork in MegaHardfork::VARIANTS { + if let ForkCondition::Timestamp(t) = hf.mega_fork_activation(*fork) { + stamps.extend([t.saturating_sub(1), t, t.saturating_add(1)]); + } + } + for ts in stamps { + let naive = MegaHardfork::VARIANTS + .iter() + .filter(|fork| hf.mega_fork_activation(**fork).active_at_timestamp(ts)) + .map(|fork| fork.spec_id()) + .max() + .unwrap_or(MegaSpecId::EQUIVALENCE); + assert_eq!(hf.spec_id(ts), naive, "resolved spec diverges at ts={ts}"); + } + } + } + + /// Alias-fork predicates stay raw event queries: they are not recoverable from a spec + /// ordinal, so they must not be projected from the resolved spec. Testnet is the live case + /// — its resolved spec climbs the whole ladder while `MiniRex1`/`MiniRex2` are never + /// scheduled. + #[test] + fn test_alias_fork_predicates_stay_event_scoped() { + let hf = crate::testnet_hardforks(); + let ts = u64::MAX; + assert!(hf.spec_id(ts).is_enabled(MegaSpecId::REX5), "the resolved spec is high"); + assert!(!hf.is_mini_rex_1_active_at_timestamp(ts), "MiniRex1 never happened on testnet"); + assert!(!hf.is_mini_rex_2_active_at_timestamp(ts), "MiniRex2 never happened on testnet"); + + // On mainnet both events did happen, and the predicates report them. + let hf = crate::mainnet_hardforks(); + assert!(hf.is_mini_rex_1_active_at_timestamp(ts)); + assert!(hf.is_mini_rex_2_active_at_timestamp(ts)); + } + + /// Every canonical schedule and every `with_all_activated_through` rung is well-formed. + /// `with_all_activated_through(EQUIVALENCE)` is the edge worth pinning: it registers no + /// fork at all (a patch without its base would be an orphan) and resolves to + /// `EQUIVALENCE` by default. + #[test] + fn test_validate_schedule_accepts_well_formed_ladders() { + assert_eq!(crate::mainnet_hardforks().validate_schedule(), Ok(())); + assert_eq!(crate::testnet_hardforks().validate_schedule(), Ok(())); + assert_eq!(crate::all_activated_hardforks().validate_schedule(), Ok(())); + assert_eq!(MegaHardforkConfig::new().validate_schedule(), Ok(()), "no mega forks is fine"); + + for spec in MegaSpecId::ALL.iter().copied() { + let mut config = MegaHardforkConfig::default().with_all_activated_through(spec); + if spec.is_enabled(MegaSpecId::REX5) { + config = config.with_params(SequencerRegistryConfig { + rex5_initial_sequencer: crate::MEGA_SYSTEM_ADDRESS, + rex5_initial_admin: crate::MEGA_SYSTEM_ADDRESS, + }); + } + if spec.is_enabled(MegaSpecId::REX6) { + config = + config.with_params(SequencerRegistryRex6Config { rex6_min_rotation_delay: 1 }); + } + assert_eq!(config.validate_schedule(), Ok(()), "{spec:?} rung must validate"); + } + } + + /// A partial ladder is executable (position-gated setup stays additive) but not a valid + /// published schedule: `validate_schedule` is the fail-fast side of that split. + #[test] + fn test_validate_schedule_rejects_skipped_rungs() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)) + .with_params(SequencerRegistryRex6Config { rex6_min_rotation_delay: 1 }); + + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::SkippedRung { + missing: MegaHardfork::MiniRex, + scheduled: MegaHardfork::Rex6 + }) + ); + } + + /// A gap in the middle of the ladder is flagged too, naming the first missing rung. Unlike + /// the lowest rung (whose empty prefix makes it trivially spec-introducing), this exercises + /// the prefix classification for real: `Rex` counts as a rung only because every + /// earlier-declared fork maps strictly below it. + #[test] + fn test_validate_schedule_rejects_skipped_middle_rung() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0)) + .with(MegaHardfork::Rex2, ForkCondition::Timestamp(0)); + + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::SkippedRung { + missing: MegaHardfork::Rex, + scheduled: MegaHardfork::Rex2 + }) + ); + } + + /// Scheduling an alias fork without its base is the rollback of a fork that never + /// happened. Under the ascending 1:1 map this is just a skipped rung — the alias's spec + /// sits above its base's — so the general rung check catches it without a dedicated rule. + #[test] + fn test_validate_schedule_rejects_alias_without_base() { + let hf = + MegaHardforkConfig::new().with(MegaHardfork::MiniRex1, ForkCondition::Timestamp(0)); + + assert_eq!(hf.spec_id(0), MegaSpecId::MINI_REX_1); + assert_eq!(hf.spec_id(0).behavior(), MegaSpecId::EQUIVALENCE, "execution unaffected"); + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::SkippedRung { + missing: MegaHardfork::MiniRex, + scheduled: MegaHardfork::MiniRex1 + }) + ); + } + + #[test] + fn test_validate_schedule_rejects_unordered_forks() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::MiniRex, ForkCondition::Timestamp(100)) + .with(MegaHardfork::Rex, ForkCondition::Timestamp(50)); + + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::UnorderedForks { + earlier: MegaHardfork::MiniRex, + earlier_timestamp: 100, + later: MegaHardfork::Rex, + later_timestamp: 50, + }) + ); + } + + /// A scheduled fork whose required params are missing fails at validation time instead of at + /// the first block of the fork. + #[test] + fn test_validate_schedule_requires_scheduled_fork_params() { + let rex5_no_params = + MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5); + assert_eq!( + rex5_no_params.validate_schedule(), + Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex5, + params: "SequencerRegistryConfig" + }) + ); + + let rex6_no_params = MegaHardforkConfig::default() + .with_all_activated_through(MegaSpecId::REX6) + .with_params(SequencerRegistryConfig { + rex5_initial_sequencer: crate::MEGA_SYSTEM_ADDRESS, + rex5_initial_admin: crate::MEGA_SYSTEM_ADDRESS, + }); + assert_eq!( + rex6_no_params.validate_schedule(), + Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex6, + params: "SequencerRegistryRex6Config" + }) + ); + } + #[test] fn test_hardfork_and_spec_id_follow_latest_active_timestamp() { let config = MegaHardforkConfig::default() diff --git a/crates/mega-evm/src/block/limit.rs b/crates/mega-evm/src/block/limit.rs index 60b92c7a..28ee3adc 100644 --- a/crates/mega-evm/src/block/limit.rs +++ b/crates/mega-evm/src/block/limit.rs @@ -1026,6 +1026,36 @@ mod tests { use super::*; use alloy_primitives::B256; + /// Alias forks must configure exactly their behavior target's block limits: the + /// hand-written arm grouping in `from_hardfork_and_block_gas_limit` is reconciled with + /// `behavior()`, mirroring the instruction-table/precompile/runtime-limit reconciliation + /// tests. Driven off `VARIANTS` and `is_alias`, so a future alias fork joins automatically. + #[test] + fn test_alias_forks_use_their_behavior_targets_block_limits() { + const GAS: u64 = 1_000_000; + let alias_forks = + MegaHardfork::VARIANTS.iter().copied().filter(|fork| fork.spec_id().is_alias()); + let mut seen = 0; + for fork in alias_forks { + let limits = BlockLimits::from_hardfork_and_block_gas_limit(fork, GAS); + let target = fork.spec_id().behavior(); + let expected = match MegaHardfork::VARIANTS + .iter() + .copied() + .find(|base| base.spec_id() as u8 == target as u8) + { + Some(base_fork) => BlockLimits::from_hardfork_and_block_gas_limit(base_fork, GAS), + // The target predates every fork (EQUIVALENCE): no block-level limits apply. + None => BlockLimits::no_limits() + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(fork.spec_id())) + .with_block_gas_limit(GAS), + }; + assert_eq!(limits, expected, "{fork:?} must mirror its behavior target {target:?}"); + seen += 1; + } + assert_eq!(seen, 2, "MiniRex1 and MiniRex2 are the alias forks under reconciliation"); + } + fn limits_with_block_gas(block_gas_limit: u64) -> BlockLimits { let mut limits = BlockLimits::no_limits(); limits.block_gas_limit = block_gas_limit; diff --git a/crates/mega-evm/src/evm/factory.rs b/crates/mega-evm/src/evm/factory.rs index 1260cf47..bc2d8927 100644 --- a/crates/mega-evm/src/evm/factory.rs +++ b/crates/mega-evm/src/evm/factory.rs @@ -158,10 +158,14 @@ impl alloy_evm::EvmFactory .with_cfg(evm_env.cfg_env) .with_chain(L1BlockInfo::default()) .with_tx_runtime_limits(runtime_limits); + // The builder is an external closure with no exhaustive match over `MegaSpecId`, so it + // receives the behavior projection: dynamic precompiles are execution semantics, and a + // builder keyed on exact specs must not see an alias rung during a rollback window. The + // context above keeps the raw rung. MegaEvm::new(ctx).with_dyn_precompiles( self.dyn_precompiles_builder .as_ref() - .map_or_else(Default::default, |builder| builder(spec_id)), + .map_or_else(Default::default, |builder| builder(spec_id.behavior())), ) } @@ -188,4 +192,27 @@ mod tests { // Verify the getter returns a stable reference to the same field. assert!(core::ptr::eq(got, factory.external_env_factory())); } + + #[test] + fn test_dyn_precompiles_builder_receives_the_behavior_spec() { + use alloy_evm::EvmFactory as _; + use core::sync::atomic::{AtomicU8, Ordering}; + + // The builder must see the behavior projection, never an alias rung: an external + // builder keyed on exact specs would otherwise install a different precompile set + // during a rollback window. + static SEEN_SPEC: AtomicU8 = AtomicU8::new(u8::MAX); + + let factory = + MegaEvmFactory::new().with_dyn_precompiles_builder(std::sync::Arc::new(|spec| { + SEEN_SPEC.store(spec as u8, Ordering::SeqCst); + revm::primitives::HashMap::default() + })); + + let mut evm_env = EvmEnv::::default(); + evm_env.cfg_env.spec = MegaSpecId::MINI_REX_1; + let _evm = factory.create_evm(crate::test_utils::MemoryDatabase::default(), evm_env); + + assert_eq!(SEEN_SPEC.load(Ordering::SeqCst), MegaSpecId::EQUIVALENCE as u8); + } } diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index fb8b07cd..f9267532 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -113,7 +113,9 @@ use revm::{ /// /// ## Spec Progression and Opcode Overrides /// -/// Each spec builds on the previous one. Only the opcodes that change are listed: +/// Each behavior-introducing spec builds on the previous one; the alias specs have no table of +/// their own and select their behavior target's (see [`MegaInstructions::new`]). Only the +/// opcodes that change are listed: /// /// - **EQUIVALENCE**: Standard revm mainnet instruction table (no custom wrappers). /// - **`MINI_REX`** (base custom table): All 256 opcodes initialized from scratch. @@ -180,12 +182,17 @@ impl core::fmt::Debug for MegaInstructi impl MegaInstructions { /// Create a new `MegaethInstructions` with the given spec id. pub fn new(spec: MegaSpecId) -> Self { + // An alias spec is grouped with its `behavior()` target so it executes exactly the + // target's table; the grouping must agree with `behavior()`, pinned by + // `test_alias_specs_use_their_behavior_targets_table`. let instruction_table = match spec { - MegaSpecId::EQUIVALENCE => EthInstructions::new_mainnet(), - MegaSpecId::MINI_REX => EthInstructions::new(mini_rex::instruction_table::< - EthInterpreter, - MegaContext, - >()), + MegaSpecId::EQUIVALENCE | MegaSpecId::MINI_REX_1 => EthInstructions::new_mainnet(), + MegaSpecId::MINI_REX | MegaSpecId::MINI_REX_2 => { + EthInstructions::new(mini_rex::instruction_table::< + EthInterpreter, + MegaContext, + >()) + } MegaSpecId::REX | MegaSpecId::REX1 => EthInstructions::new(rex::instruction_table::< EthInterpreter, MegaContext, @@ -2345,3 +2352,34 @@ impl StackInspectTr for Stack { Some(unsafe { *self.data().get_unchecked(index) }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::MemoryDatabase, EmptyExternalEnv}; + + /// Every alias spec must select exactly its `behavior()` target's instruction + /// table — the match in `MegaInstructions::new` states the alias→target mapping a + /// second time, and this test is the reconciliation between the two. Both tables + /// come from the same monomorphization, so identical arms produce identical fn + /// pointers and `fn_addr_eq` compares them reliably. + #[test] + fn test_alias_specs_use_their_behavior_targets_table() { + for spec in MegaSpecId::ALL { + if !spec.is_alias() { + continue; + } + let alias = MegaInstructions::::new(*spec); + let target = MegaInstructions::::new(spec.behavior()); + let (alias_table, target_table) = + (alias.instruction_table(), target.instruction_table()); + for opcode in 0..=0xff_usize { + assert!( + core::ptr::fn_addr_eq(alias_table[opcode], target_table[opcode]), + "{spec:?} table diverges from its behavior target {:?} at opcode {opcode:#04x}", + spec.behavior(), + ); + } + } + } +} diff --git a/crates/mega-evm/src/evm/limit.rs b/crates/mega-evm/src/evm/limit.rs index a80b67c0..b9111abb 100644 --- a/crates/mega-evm/src/evm/limit.rs +++ b/crates/mega-evm/src/evm/limit.rs @@ -21,9 +21,12 @@ pub struct EvmTxRuntimeLimits { impl EvmTxRuntimeLimits { /// Creates a new `TxLimits` instance from the given `MegaSpecId`. pub fn from_spec(spec: MegaSpecId) -> Self { + // An alias spec is grouped with its `behavior()` target so it runs under exactly the + // target's limits; the grouping must agree with `behavior()`, pinned by + // `test_alias_specs_use_their_behavior_targets_limits`. match spec { - MegaSpecId::EQUIVALENCE => Self::equivalence(), - MegaSpecId::MINI_REX => Self::mini_rex(), + MegaSpecId::EQUIVALENCE | MegaSpecId::MINI_REX_1 => Self::equivalence(), + MegaSpecId::MINI_REX | MegaSpecId::MINI_REX_2 => Self::mini_rex(), MegaSpecId::REX | MegaSpecId::REX1 | MegaSpecId::REX2 => Self::rex(), MegaSpecId::REX3 => Self::rex3(), MegaSpecId::REX4 => Self::rex4(), @@ -157,3 +160,26 @@ impl EvmTxRuntimeLimits { self } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Every alias spec must run under exactly its `behavior()` target's limits — the + /// match in `from_spec` states the alias→target mapping a second time, and this + /// test is the reconciliation between the two. + #[test] + fn test_alias_specs_use_their_behavior_targets_limits() { + for spec in MegaSpecId::ALL { + if !spec.is_alias() { + continue; + } + assert_eq!( + EvmTxRuntimeLimits::from_spec(*spec), + EvmTxRuntimeLimits::from_spec(spec.behavior()), + "{spec:?} limits diverge from its behavior target {:?}", + spec.behavior(), + ); + } + } +} diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 37149f9f..d1bc95e4 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -37,10 +37,12 @@ impl MegaPrecompiles { /// Create a new precompile provider with the given `MegaETH` spec. #[inline] pub fn new_with_spec(spec: MegaSpecId) -> Self { - // Get base precompiles from op-revm + // Get base precompiles from op-revm. An alias spec is grouped with its `behavior()` + // target so it gets exactly the target's precompiles; the grouping must agree with + // `behavior()`, pinned by `test_alias_specs_use_their_behavior_targets_precompiles`. let inner = match spec { - MegaSpecId::EQUIVALENCE => op_revm::precompiles::isthmus(), - MegaSpecId::MINI_REX => mini_rex(), + MegaSpecId::EQUIVALENCE | MegaSpecId::MINI_REX_1 => op_revm::precompiles::isthmus(), + MegaSpecId::MINI_REX | MegaSpecId::MINI_REX_2 => mini_rex(), MegaSpecId::REX | MegaSpecId::REX1 | MegaSpecId::REX2 | @@ -269,6 +271,11 @@ impl PrecompileProvider HashMap + Send + Sync>; @@ -291,6 +298,26 @@ mod tests { }; use sha2::{Digest, Sha256}; + /// Every alias spec must get exactly its `behavior()` target's precompile set — the + /// match in `new_with_spec` states the alias→target mapping a second time, and this + /// test is the reconciliation between the two. The sets are `&'static`, so pointer + /// identity is the exact form of "same set". + #[test] + fn test_alias_specs_use_their_behavior_targets_precompiles() { + for spec in MegaSpecId::ALL { + if !spec.is_alias() { + continue; + } + let alias = MegaPrecompiles::new_with_spec(*spec); + let target = MegaPrecompiles::new_with_spec(spec.behavior()); + assert!( + core::ptr::eq(alias.precompiles(), target.precompiles()), + "{spec:?} precompiles diverge from its behavior target {:?}", + spec.behavior(), + ); + } + } + /// Generate valid KZG Point Evaluation test data from EIP-4844 test vectors. fn generate_kzg_test_input() -> InputsImpl { let commitment = hex::decode("8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca25f26936857bc3a7c2539ea8ec3a952b7").unwrap(); diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 6b01318e..92580422 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -16,6 +16,8 @@ use serde::{Deserialize, Serialize}; /// corresponding relations are as follows: /// - [`SpecId::EQUIVALENCE`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] /// - [`SpecId::MINI_REX`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] +/// - [`SpecId::MINI_REX_1`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] +/// - [`SpecId::MINI_REX_2`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] /// - [`SpecId::REX`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] /// - [`SpecId::REX1`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] /// - [`SpecId::REX2`] -> [`OpSpecId::ISTHMUS`] -> [`EthSpecId::PRAGUE`] @@ -40,6 +42,12 @@ pub enum MegaSpecId { EQUIVALENCE, /// The EVM version for the *Mini-Rex* hardfork of `MegaETH`. MINI_REX, + /// Alias spec for the `MiniRex1` hardfork: scheduled as its own rung, executes + /// [`MegaSpecId::EQUIVALENCE`] behavior (`behavior()` projects there). + MINI_REX_1, + /// Alias spec for the `MiniRex2` hardfork: scheduled as its own rung, executes + /// [`MegaSpecId::MINI_REX`] behavior (`behavior()` projects there). + MINI_REX_2, /// The EVM version for the *Rex* hardfork of `MegaETH`. REX, /// The EVM version for the *Rex1* hardfork of `MegaETH`. @@ -66,6 +74,10 @@ pub mod name { pub const EQUIVALENCE: &str = "Equivalence"; /// The string identifier for the *Mini-Rex* version of the `MegaETH` EVM. pub const MINI_REX: &str = "MiniRex"; + /// The string identifier for the `MiniRex1` alias spec. + pub const MINI_REX_1: &str = "MiniRex1"; + /// The string identifier for the `MiniRex2` alias spec. + pub const MINI_REX_2: &str = "MiniRex2"; /// The string identifier for the *Rex* version of the `MegaETH` EVM. pub const REX: &str = "Rex"; /// The string identifier for the *Rex1* version of the `MegaETH` EVM. @@ -85,6 +97,28 @@ pub mod name { } impl MegaSpecId { + /// Every spec in the progression, oldest first. + /// + /// The single point to enumerate specs from: sweeps and tables that list specs by hand + /// drift silently when a variant is added. Completeness is enforced in two steps — + /// introducing a variant is a compile error in `ladder_index`'s exhaustive match (and the + /// `is_ladder_prefix` const assertion ties each entry here to its ladder position), and + /// `test_all_ends_at_the_latest_spec` fails until the new spec is appended here. + pub const ALL: &'static [Self] = &[ + Self::EQUIVALENCE, + Self::MINI_REX, + Self::MINI_REX_1, + Self::MINI_REX_2, + Self::REX, + Self::REX1, + Self::REX2, + Self::REX3, + Self::REX4, + Self::REX5, + Self::REX6, + Self::REX7, + ]; + /// Converts the [`SpecId`] into its corresponding [`EthSpecId`]. pub const fn into_eth_spec(self) -> EthSpecId { self.into_op_spec().into_eth_spec() @@ -94,6 +128,8 @@ impl MegaSpecId { pub const fn into_op_spec(self) -> OpSpecId { match self { Self::MINI_REX | + Self::MINI_REX_1 | + Self::MINI_REX_2 | Self::EQUIVALENCE | Self::REX | Self::REX1 | @@ -106,22 +142,165 @@ impl MegaSpecId { } } - /// Returns `true` if `other` is enabled under `self` — i.e. `other` is at or below `self` - /// in [`SpecId`] order. + /// The behavior this spec executes: alias specs project to the spec whose behavior they + /// reuse; every other spec is its own behavior. /// - /// Evm versions are backward compatible: the current spec (`self`) enables every version at - /// or below it, so a lower-or-equal version is always enabled under a higher one. + /// The projection must stay flat — every target is a concrete spec at or below the + /// projecting rung, never another alias — which the `is_flat_projection` const assertion + /// below pins at compile time. + pub const fn behavior(self) -> Self { + match self { + Self::MINI_REX_1 => Self::EQUIVALENCE, + Self::MINI_REX_2 => Self::MINI_REX, + other => other, + } + } + + /// Whether this spec is an alias — a rung whose behavior belongs to another spec. + /// Derived from [`behavior`](Self::behavior), so a future alias needs only its projection + /// arm; there is no second list to extend. + pub const fn is_alias(self) -> bool { + self.behavior() as u8 != self as u8 + } + + /// Returns `true` if `other`'s BEHAVIOR is enabled under `self` — the gate for execution + /// semantics. Both sides project through [`behavior`](Self::behavior) first, so an alias + /// spec enables exactly what its behavior target enables (`MINI_REX_1` does NOT enable + /// `MINI_REX`). pub const fn is_enabled(self, other: Self) -> bool { + other.behavior() as u8 <= self.behavior() as u8 + } + + /// Returns `true` if the ladder has REACHED `other`'s rung — the gate for one-way chain + /// setup. Position comparison, no behavior projection: during an alias window the ladder + /// stands above the specs it rolled back from, so their setup stays in place. + pub const fn reaches(self, other: Self) -> bool { other as u8 <= self as u8 } } +/// Position on the spec progression, 0 = `EQUIVALENCE`. +/// +/// Not an API — the `u8` discriminant already carries the ordinal. This exists to be an +/// exhaustive match: introducing a spec fails compilation here until the variant is placed, +/// and the const assertion below fails until [`MegaSpecId::ALL`] lists it at that position. +const fn ladder_index(spec: MegaSpecId) -> usize { + match spec { + MegaSpecId::EQUIVALENCE => 0, + MegaSpecId::MINI_REX => 1, + MegaSpecId::MINI_REX_1 => 2, + MegaSpecId::MINI_REX_2 => 3, + MegaSpecId::REX => 4, + MegaSpecId::REX1 => 5, + MegaSpecId::REX2 => 6, + MegaSpecId::REX3 => 7, + MegaSpecId::REX4 => 8, + MegaSpecId::REX5 => 9, + MegaSpecId::REX6 => 10, + MegaSpecId::REX7 => 11, + } +} + +/// Whether `list` is a prefix of the spec ladder: entry `i` is exactly the spec at ladder +/// position `i` — in order, without gaps, starting from `EQUIVALENCE`. +/// +/// Shared by the compile-time assertion on [`MegaSpecId::ALL`] below and by the test that +/// feeds it malformed lists, so the checker itself is exercised — a weakened guard here would +/// otherwise pass silently, since the real `ALL` always satisfies the property it checks. +const fn is_ladder_prefix(list: &[MegaSpecId]) -> bool { + let mut i = 0; + while i < list.len() { + if ladder_index(list[i]) != i { + return false; + } + i += 1; + } + true +} + +const _: () = assert!( + is_ladder_prefix(MegaSpecId::ALL), + "MegaSpecId::ALL must list every spec in ladder order, without gaps" +); + +/// The target `spec` projects to under a behavior `table` of `(spec, target)` pairs; a spec +/// absent from the table is its own target, mirroring [`MegaSpecId::behavior`]'s identity arm. +const fn project(table: &[(MegaSpecId, MegaSpecId)], spec: MegaSpecId) -> MegaSpecId { + let mut i = 0; + while i < table.len() { + if table[i].0 as u8 == spec as u8 { + return table[i].1; + } + i += 1; + } + spec +} + +/// Whether a behavior table is flat: every target is a fixed point of the table and sits at +/// or below the spec projecting onto it. +/// +/// The fixed-point check rejects chains and cycles in one property — a chain `A→B→C` fails +/// because `B`'s own target is `C`, a cycle `A→B→A` because `B` projects back to `A` — either +/// way [`MegaSpecId::behavior`]'s single-step lookup would silently resolve half-way. The +/// downward check rejects an alias projecting upward, which would execute semantics whose +/// one-way setup (gated by [`MegaSpecId::reaches`], a position below the target's rung) never +/// ran. +/// +/// The table is passed as data so the checker can be fed malformed shapes: the compile-time +/// assertion below checks the real projection, and the test exercises the rejection cases the +/// real table can never produce — a weakened guard here would otherwise pass silently. +const fn is_flat_projection(table: &[(MegaSpecId, MegaSpecId)]) -> bool { + let mut i = 0; + while i < table.len() { + let (spec, target) = table[i]; + if project(table, target) as u8 != target as u8 { + return false; + } + if target as u8 > spec as u8 { + return false; + } + // A key that appears twice would make `project` ambiguous (first match wins); + // reject the table outright rather than trusting the lookup order. + let mut j = 0; + while j < i { + if table[j].0 as u8 == spec as u8 { + return false; + } + j += 1; + } + i += 1; + } + true +} + +/// [`MegaSpecId::behavior`] as data: every spec in [`MegaSpecId::ALL`] paired with its +/// projection target. +const fn behavior_table() -> [(MegaSpecId, MegaSpecId); MegaSpecId::ALL.len()] { + let mut table = [(MegaSpecId::EQUIVALENCE, MegaSpecId::EQUIVALENCE); MegaSpecId::ALL.len()]; + let mut i = 0; + while i < table.len() { + table[i] = (MegaSpecId::ALL[i], MegaSpecId::ALL[i].behavior()); + i += 1; + } + table +} + +const _: () = { + let table = behavior_table(); + assert!( + is_flat_projection(&table), + "behavior() targets must be concrete specs at or below the projecting rung" + ); +}; + impl From for &'static str { /// Converts the [`SpecId`] into its corresponding string identifier. fn from(spec_id: MegaSpecId) -> Self { match spec_id { MegaSpecId::EQUIVALENCE => name::EQUIVALENCE, MegaSpecId::MINI_REX => name::MINI_REX, + MegaSpecId::MINI_REX_1 => name::MINI_REX_1, + MegaSpecId::MINI_REX_2 => name::MINI_REX_2, MegaSpecId::REX => name::REX, MegaSpecId::REX1 => name::REX1, MegaSpecId::REX2 => name::REX2, @@ -142,6 +321,8 @@ impl FromStr for MegaSpecId { match s { name::EQUIVALENCE => Ok(Self::EQUIVALENCE), name::MINI_REX => Ok(Self::MINI_REX), + name::MINI_REX_1 => Ok(Self::MINI_REX_1), + name::MINI_REX_2 => Ok(Self::MINI_REX_2), name::REX => Ok(Self::REX), name::REX1 => Ok(Self::REX1), name::REX2 => Ok(Self::REX2), @@ -180,22 +361,32 @@ impl Display for MegaSpecId { mod tests { use super::*; - const ALL_SPECS: [(MegaSpecId, &str); 10] = [ - (MegaSpecId::EQUIVALENCE, name::EQUIVALENCE), - (MegaSpecId::MINI_REX, name::MINI_REX), - (MegaSpecId::REX, name::REX), - (MegaSpecId::REX1, name::REX1), - (MegaSpecId::REX2, name::REX2), - (MegaSpecId::REX3, name::REX3), - (MegaSpecId::REX4, name::REX4), - (MegaSpecId::REX5, name::REX5), - (MegaSpecId::REX6, name::REX6), - (MegaSpecId::REX7, name::REX7), + /// The one golden spec table: every spec with its string identifier and its pinned ladder + /// position. The spec column must be exactly [`MegaSpecId::ALL`] (asserted in the + /// round-trip test); the name and position columns stay hand-written — deriving either + /// from the code under test would make its check vacuous. + const ALL_SPECS: [(MegaSpecId, &str, u8); 12] = [ + (MegaSpecId::EQUIVALENCE, name::EQUIVALENCE, 0), + (MegaSpecId::MINI_REX, name::MINI_REX, 1), + (MegaSpecId::MINI_REX_1, name::MINI_REX_1, 2), + (MegaSpecId::MINI_REX_2, name::MINI_REX_2, 3), + (MegaSpecId::REX, name::REX, 4), + (MegaSpecId::REX1, name::REX1, 5), + (MegaSpecId::REX2, name::REX2, 6), + (MegaSpecId::REX3, name::REX3, 7), + (MegaSpecId::REX4, name::REX4, 8), + (MegaSpecId::REX5, name::REX5, 9), + (MegaSpecId::REX6, name::REX6, 10), + (MegaSpecId::REX7, name::REX7, 11), ]; #[test] fn test_spec_names_roundtrip_and_display() { - for (spec, expected_name) in ALL_SPECS { + // The spec column must be exactly `MegaSpecId::ALL`, so a newly introduced spec cannot + // be forgotten here. + assert!(ALL_SPECS.iter().map(|(spec, _, _)| *spec).eq(MegaSpecId::ALL.iter().copied())); + + for (spec, expected_name, _) in ALL_SPECS { assert_eq!(<&'static str>::from(spec), expected_name); assert_eq!(MegaSpecId::from_str(expected_name).unwrap(), spec); assert_eq!(spec.to_string(), expected_name); @@ -205,9 +396,86 @@ mod tests { assert_eq!(MegaSpecId::from_str("unknown"), Err(UnknownHardfork)); } + /// The completeness anchor for [`MegaSpecId::ALL`]: `Default` tracks the latest spec (its + /// own assertion above pins which), so a variant added without extending `ALL` fails here + /// once the default advances. The const assertion on `ladder_index` covers order and gaps; + /// this covers the tail. + #[test] + fn test_all_ends_at_the_latest_spec() { + assert_eq!(*MegaSpecId::ALL.last().unwrap(), MegaSpecId::default()); + } + + #[test] + fn test_ladder_positions_are_pinned() { + // A downstream variant-index codec (bincode-style) of `MegaSpecId` — or of a + // container holding it — silently misreads old data if discriminants renumber. + // Pinning every position turns any future renumbering into a loud diff on the + // golden table, where the review attention is. + assert_eq!(ALL_SPECS.len(), MegaSpecId::ALL.len()); + for (spec, _, position) in ALL_SPECS { + assert_eq!(spec as u8, position, "{spec:?} moved on the ladder"); + } + } + + #[test] + fn test_is_ladder_prefix_rejects_malformed_lists() { + assert!(is_ladder_prefix(MegaSpecId::ALL)); + assert!(is_ladder_prefix(&[]), "the empty prefix is a ladder prefix"); + assert!(is_ladder_prefix(&[MegaSpecId::EQUIVALENCE, MegaSpecId::MINI_REX])); + + assert!( + !is_ladder_prefix(&[MegaSpecId::MINI_REX, MegaSpecId::EQUIVALENCE]), + "reordered entries must be rejected" + ); + assert!( + !is_ladder_prefix(&[MegaSpecId::MINI_REX]), + "a list not starting at the ladder base must be rejected" + ); + assert!( + !is_ladder_prefix(&[MegaSpecId::EQUIVALENCE, MegaSpecId::REX]), + "a skipped rung must be rejected" + ); + } + + #[test] + fn test_is_flat_projection_rejects_malformed_tables() { + assert!(is_flat_projection(&behavior_table())); + assert!(is_flat_projection(&[]), "the empty table is flat"); + assert!( + is_flat_projection(&[(MegaSpecId::MINI_REX_1, MegaSpecId::EQUIVALENCE)]), + "a target absent from the table projects to itself" + ); + + assert!( + !is_flat_projection(&[ + (MegaSpecId::MINI_REX_1, MegaSpecId::EQUIVALENCE), + (MegaSpecId::MINI_REX_2, MegaSpecId::MINI_REX_1), + ]), + "a chain — an alias targeting another alias — must be rejected" + ); + assert!( + !is_flat_projection(&[ + (MegaSpecId::EQUIVALENCE, MegaSpecId::MINI_REX), + (MegaSpecId::MINI_REX, MegaSpecId::EQUIVALENCE), + ]), + "a projection cycle must be rejected" + ); + assert!( + !is_flat_projection(&[(MegaSpecId::MINI_REX, MegaSpecId::REX)]), + "an alias projecting to a higher rung must be rejected" + ); + assert!( + !is_flat_projection(&[ + (MegaSpecId::MINI_REX_1, MegaSpecId::EQUIVALENCE), + (MegaSpecId::MINI_REX_1, MegaSpecId::MINI_REX), + ]), + "a duplicate key must be rejected — it would make the projection ambiguous" + ); + } + #[test] fn test_all_specs_map_to_isthmus_and_prague() { - for (spec, _) in ALL_SPECS { + for spec in MegaSpecId::ALL.iter().copied() { assert_eq!(spec.into_op_spec(), OpSpecId::ISTHMUS); assert_eq!(spec.into_eth_spec(), EthSpecId::PRAGUE); assert_eq!(revm::primitives::hardfork::SpecId::from(spec), EthSpecId::PRAGUE); diff --git a/crates/mega-evm/src/system/AGENTS.md b/crates/mega-evm/src/system/AGENTS.md index 01740b90..dd2ad644 100644 --- a/crates/mega-evm/src/system/AGENTS.md +++ b/crates/mega-evm/src/system/AGENTS.md @@ -15,7 +15,8 @@ System contract integration layer with canonical addresses, deployment transacti ## KEY PATTERNS - Deployment helpers are idempotent and keyed by code hash equality. -- Hardfork gating happens in each deployment helper. +- Gating happens in each contract's `_spec()` builder, which takes the resolved `MegaSpecId` and gates on `spec.reaches(...)` — POSITION comparison, so alias (rollback) windows do not retract deploys. Nothing in the deploy layer takes a hardfork config — the spec builders and the crate-private `*_for` helpers receive the resolved spec and typed params, so a per-fork activation gate cannot be reintroduced. The `pub` `transact_deploy_*` wrappers keep their `(hardforks, block_timestamp)` shape for external callers and resolve everything themselves. +- Builders must never gate on `is_enabled` (behavior): predeploys are one-way, and a hardfork that rolls behavior back does not un-deploy them or change which bytecode version is expected to be installed. - Interceptors return `None` to fall through to on-chain bytecode on unknown selectors. - View/control interceptors reject non-zero transfer values with `NonZeroTransfer()`. - Synthetic interceptor results bypass normal child-frame init and require empty tracking frame push by caller. diff --git a/crates/mega-evm/src/system/control.rs b/crates/mega-evm/src/system/control.rs index 6773a4b7..7ac6bbf0 100644 --- a/crates/mega-evm/src/system/control.rs +++ b/crates/mega-evm/src/system/control.rs @@ -9,7 +9,7 @@ use alloy_primitives::{address, Address}; use alloy_sol_types::SolError; use revm::{database::State, state::EvmState}; -use crate::{MegaHardforks, SystemContractSpec}; +use crate::{MegaHardforks, MegaSpecId, SystemContractSpec}; /// The address of the access control system contract. pub const ACCESS_CONTROL_ADDRESS: Address = address!("0x6342000000000000000000000000000000000004"); @@ -50,18 +50,18 @@ pub fn transact_deploy_access_control_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - access_control_spec(&hardforks, block_timestamp) + access_control_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } -/// Builds the [`SystemContractSpec`] for the access-control contract active at -/// the given timestamp, or `None` if Rex4 is not yet active. -pub(crate) fn access_control_spec( - hardforks: &impl MegaHardforks, - block_timestamp: u64, -) -> Option { - hardforks.is_rex_4_active_at_timestamp(block_timestamp).then(|| { +/// Builds the [`SystemContractSpec`] for the access-control contract active under +/// `spec`, or `None` if `REX4` is not yet enabled. +/// +/// `spec` is the scheduled spec, compared by position — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn access_control_spec(spec: MegaSpecId) -> Option { + spec.reaches(MegaSpecId::REX4).then(|| { SystemContractSpec::new( ACCESS_CONTROL_ADDRESS, ACCESS_CONTROL_CODE, diff --git a/crates/mega-evm/src/system/deploy.rs b/crates/mega-evm/src/system/deploy.rs index 95f16f9b..1b9a753f 100644 --- a/crates/mega-evm/src/system/deploy.rs +++ b/crates/mega-evm/src/system/deploy.rs @@ -27,7 +27,7 @@ use revm::{ }; use std::vec::Vec; -use crate::MegaHardforks; +use crate::{MegaHardforks, MegaSpecId}; /// Declarative description of a single system-contract deployment. #[derive(Clone, Debug, PartialEq, Eq)] @@ -147,14 +147,22 @@ pub fn flat_system_contract_specs( hardforks: impl MegaHardforks, block_timestamp: u64, ) -> Vec { + flat_system_contract_specs_for(hardforks.spec_id(block_timestamp)) +} + +/// [`flat_system_contract_specs`] against an already-resolved spec. +/// +/// The block executor resolves the spec once per block and calls this directly; the public +/// wrapper above resolves it for callers that hold a hardfork config. +pub(crate) fn flat_system_contract_specs_for(spec: MegaSpecId) -> Vec { // Compose the per-contract spec builders (each its own single source of gate // + bytecode-version selection). `None` entries (inactive contracts) drop out. [ - super::oracle::oracle_spec(&hardforks, block_timestamp), - super::oracle::high_precision_timestamp_oracle_spec(&hardforks, block_timestamp), - super::keyless_deploy::keyless_deploy_spec(&hardforks, block_timestamp), - super::control::access_control_spec(&hardforks, block_timestamp), - super::limit_control::limit_control_spec(&hardforks, block_timestamp), + super::oracle::oracle_spec(spec), + super::oracle::high_precision_timestamp_oracle_spec(spec), + super::keyless_deploy::keyless_deploy_spec(spec), + super::control::access_control_spec(spec), + super::limit_control::limit_control_spec(spec), ] .into_iter() .flatten() @@ -165,8 +173,9 @@ pub fn flat_system_contract_specs( mod tests { use super::*; use crate::{ - MegaHardfork, MegaHardforkConfig, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_HASH, - ORACLE_CONTRACT_CODE_HASH_REX2, ORACLE_CONTRACT_CODE_HASH_REX5, + MegaHardfork, MegaHardforkConfig, HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, + ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_HASH, ORACLE_CONTRACT_CODE_HASH_REX2, + ORACLE_CONTRACT_CODE_HASH_REX5, }; use alloy_hardforks::ForkCondition; use alloy_primitives::address; @@ -308,6 +317,53 @@ mod tests { assert!(!addrs(&specs).contains(&crate::SEQUENCER_REGISTRY_ADDRESS)); } + /// Inside mainnet's `MiniRex1` rollback window the registry must still yield both `MiniRex` + /// predeploys. Gating on the executing spec (`EQUIVALENCE` there) would drop them, and with + /// them their read-only witness entries — a replay-observable change on frozen history. + #[test] + fn test_registry_survives_mainnet_rollback_window() { + let hf = crate::mainnet_hardforks(); + let ForkCondition::Timestamp(ts) = hf.mega_fork_activation(MegaHardfork::MiniRex1) else { + panic!("mainnet must schedule MiniRex1 by timestamp"); + }; + + let spec = hf.spec_id(ts); + assert_eq!(spec, crate::MegaSpecId::MINI_REX_1); + assert_eq!(spec.behavior(), crate::MegaSpecId::EQUIVALENCE); + + // The single scheduled spec keeps setup via position comparison even while its + // behavior projects back to EQUIVALENCE. + let specs = flat_system_contract_specs(&hf, ts); + assert_eq!( + addrs(&specs), + [ORACLE_CONTRACT_ADDRESS, HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS] + ); + // Still the pre-Rex2 bytecode line, with the pre-Rex5 upgrade semantics. + assert_eq!(specs[0].code_hash, ORACLE_CONTRACT_CODE_HASH); + assert!(specs[0].force_create_on_upgrade); + } + + /// A config that schedules Rex6 without Rex5 must still materialize every lower-spec + /// predeploy, at the Rex5 Oracle version. Under per-fork gating none of them deployed. + #[test] + fn test_registry_complete_on_partial_ladder() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::Rex5, ForkCondition::Never) + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); + + assert_eq!( + hf.mega_fork_activation(MegaHardfork::MiniRex), + ForkCondition::Never, + "no lower fork is scheduled" + ); + + let specs = flat_system_contract_specs(&hf, 0); + assert_eq!(specs.len(), 5, "the partial ladder must still deploy all five flat contracts"); + assert_eq!(specs[0].address, ORACLE_CONTRACT_ADDRESS); + assert_eq!(specs[0].code_hash, ORACLE_CONTRACT_CODE_HASH_REX5); + assert!(!specs[0].force_create_on_upgrade); + } + #[test] fn test_registry_oracle_version_by_spec() { // Only MiniRex active: Oracle v1.0.0, force-created on upgrade (pre-Rex5), diff --git a/crates/mega-evm/src/system/keyless_deploy.rs b/crates/mega-evm/src/system/keyless_deploy.rs index c7d7953f..cd1acf6a 100644 --- a/crates/mega-evm/src/system/keyless_deploy.rs +++ b/crates/mega-evm/src/system/keyless_deploy.rs @@ -21,7 +21,7 @@ use alloy_evm::Database; use alloy_primitives::{address, Address}; use revm::{database::State, state::EvmState}; -use crate::{MegaHardforks, SystemContractSpec}; +use crate::{MegaHardforks, MegaSpecId, SystemContractSpec}; // Re-export error types and transaction functions from sandbox pub use crate::sandbox::{ @@ -48,18 +48,18 @@ pub fn transact_deploy_keyless_deploy_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - keyless_deploy_spec(&hardforks, block_timestamp) + keyless_deploy_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } -/// Builds the [`SystemContractSpec`] for the keyless-deploy contract active at -/// the given timestamp, or `None` if Rex2 is not yet active. -pub(crate) fn keyless_deploy_spec( - hardforks: &impl MegaHardforks, - block_timestamp: u64, -) -> Option { - hardforks.is_rex_2_active_at_timestamp(block_timestamp).then(|| { +/// Builds the [`SystemContractSpec`] for the keyless-deploy contract active under +/// `spec`, or `None` if `REX2` is not yet enabled. +/// +/// `spec` is the scheduled spec, compared by position — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn keyless_deploy_spec(spec: MegaSpecId) -> Option { + spec.reaches(MegaSpecId::REX2).then(|| { SystemContractSpec::new( KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE, @@ -71,7 +71,7 @@ pub(crate) fn keyless_deploy_spec( #[cfg(test)] mod tests { use super::*; - use crate::{MegaHardfork, MegaHardforkConfig}; + use crate::MegaHardforkConfig; use revm::{ database::InMemoryDB, state::{AccountInfo, Bytecode}, @@ -131,8 +131,8 @@ mod tests { fn test_deploy_keyless_deploy_contract_requires_rex2() { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex2); + // A complete ladder topping out at Rex1: Rex2 is genuinely not reached. + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX1); let result = transact_deploy_keyless_deploy_contract(&hardforks, 0, &mut state) .expect("Should succeed"); diff --git a/crates/mega-evm/src/system/limit_control.rs b/crates/mega-evm/src/system/limit_control.rs index b904266e..43c38797 100644 --- a/crates/mega-evm/src/system/limit_control.rs +++ b/crates/mega-evm/src/system/limit_control.rs @@ -8,7 +8,7 @@ use alloy_evm::Database; use alloy_primitives::{address, Address}; use revm::{database::State, state::EvmState}; -use crate::{MegaHardforks, SystemContractSpec}; +use crate::{MegaHardforks, MegaSpecId, SystemContractSpec}; /// The address of the `MegaLimitControl` system contract. pub const LIMIT_CONTROL_ADDRESS: Address = address!("0x6342000000000000000000000000000000000005"); @@ -29,18 +29,18 @@ pub fn transact_deploy_limit_control_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - limit_control_spec(&hardforks, block_timestamp) + limit_control_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } /// Builds the [`SystemContractSpec`] for the `MegaLimitControl` contract active -/// at the given timestamp, or `None` if Rex4 is not yet active. -pub(crate) fn limit_control_spec( - hardforks: &impl MegaHardforks, - block_timestamp: u64, -) -> Option { - hardforks.is_rex_4_active_at_timestamp(block_timestamp).then(|| { +/// under `spec`, or `None` if `REX4` is not yet enabled. +/// +/// `spec` is the scheduled spec, compared by position — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn limit_control_spec(spec: MegaSpecId) -> Option { + spec.reaches(MegaSpecId::REX4).then(|| { SystemContractSpec::new(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE, LIMIT_CONTROL_CODE_HASH) }) } diff --git a/crates/mega-evm/src/system/oracle.rs b/crates/mega-evm/src/system/oracle.rs index 935175f5..fe9a9fa0 100644 --- a/crates/mega-evm/src/system/oracle.rs +++ b/crates/mega-evm/src/system/oracle.rs @@ -5,7 +5,7 @@ use alloy_evm::Database; use alloy_primitives::{address, b256, bytes, Address, Bytes, B256}; use revm::{database::State, state::EvmState}; -use crate::{MegaHardforks, SystemContractSpec}; +use crate::{MegaHardforks, MegaSpecId, SystemContractSpec}; /// The address of the oracle system contract. pub const ORACLE_CONTRACT_ADDRESS: Address = address!("0x6342000000000000000000000000000000000001"); @@ -37,39 +37,42 @@ pub use mega_system_contracts::oracle::IOracle; /// Note that the database `db` is not modified in this function. The caller is responsible to /// commit the changes to database. /// -/// The deployed bytecode depends on the active hardfork: +/// The deployed bytecode depends on the scheduled spec: /// - Pre-Rex2: v1.0.0 bytecode (without `sendHint` function) -/// - Rex2+: v1.1.0 bytecode (with `sendHint` function for oracle hints) +/// - Rex2 to Rex4: v1.1.0 bytecode (with `sendHint` function for oracle hints) +/// - Rex5+: v2.0.0 bytecode (reads the system address from the `SequencerRegistry`) pub fn transact_deploy_oracle_contract( hardforks: impl MegaHardforks, block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - oracle_spec(&hardforks, block_timestamp).map(|s| crate::transact_deploy(db, &s)).transpose() + oracle_spec(hardforks.spec_id(block_timestamp)) + .map(|s| crate::transact_deploy(db, &s)) + .transpose() } -/// Builds the [`SystemContractSpec`] for the Oracle contract active at the given -/// timestamp, or `None` if `MiniRex` is not yet active. +/// Builds the [`SystemContractSpec`] for the Oracle contract active under `spec`, +/// or `None` if `MINI_REX` is not yet enabled. /// /// Single source of the Oracle's gate, bytecode-version selection, and upgrade /// semantics — shared by [`transact_deploy_oracle_contract`] and the deploy /// registry ([`flat_system_contract_specs`](crate::flat_system_contract_specs)). -pub(crate) fn oracle_spec( - hardforks: &impl MegaHardforks, - block_timestamp: u64, -) -> Option { - if !hardforks.is_mini_rex_active_at_timestamp(block_timestamp) { +/// +/// `spec` is the scheduled spec, gated by position (`reaches`) rather than behavior: an Oracle +/// already installed under `MINI_REX` stays installed through an alias (rollback) window. +pub(crate) fn oracle_spec(spec: MegaSpecId) -> Option { + if !spec.reaches(MegaSpecId::MINI_REX) { return None; } - // Select the appropriate bytecode based on hardfork. + // Select the appropriate bytecode based on the spec. // - Pre-Rex2: v1.0.0 (without `sendHint`) // - Rex2-Rex4: v1.1.0 (with `sendHint`) // - Rex5+: v2.0.0 (reads system address from SequencerRegistry) - let rex5 = hardforks.is_rex_5_active_at_timestamp(block_timestamp); + let rex5 = spec.reaches(MegaSpecId::REX5); let (target_code, target_code_hash) = if rex5 { (ORACLE_CONTRACT_CODE_REX5, ORACLE_CONTRACT_CODE_HASH_REX5) - } else if hardforks.is_rex_2_active_at_timestamp(block_timestamp) { + } else if spec.reaches(MegaSpecId::REX2) { (ORACLE_CONTRACT_CODE_REX2, ORACLE_CONTRACT_CODE_HASH_REX2) } else { (ORACLE_CONTRACT_CODE, ORACLE_CONTRACT_CODE_HASH) @@ -105,18 +108,17 @@ pub fn transact_deploy_high_precision_timestamp_oracle( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - high_precision_timestamp_oracle_spec(&hardforks, block_timestamp) + high_precision_timestamp_oracle_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } /// Builds the [`SystemContractSpec`] for the high-precision timestamp Oracle -/// active at the given timestamp, or `None` if `MiniRex` is not yet active. -pub(crate) fn high_precision_timestamp_oracle_spec( - hardforks: &impl MegaHardforks, - block_timestamp: u64, -) -> Option { - hardforks.is_mini_rex_active_at_timestamp(block_timestamp).then(|| { +/// active under `spec`, or `None` if `MINI_REX` is not yet enabled. +/// +/// `spec` is the scheduled spec, compared by position — see [`oracle_spec`]. +pub(crate) fn high_precision_timestamp_oracle_spec(spec: MegaSpecId) -> Option { + spec.reaches(MegaSpecId::MINI_REX).then(|| { SystemContractSpec::new( HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, HIGH_PRECISION_TIMESTAMP_ORACLE_CODE, @@ -127,7 +129,7 @@ pub(crate) fn high_precision_timestamp_oracle_spec( #[cfg(test)] mod tests { - use crate::{MegaHardfork, MegaHardforkConfig}; + use crate::MegaHardforkConfig; use super::*; use alloy_primitives::keccak256; @@ -305,11 +307,8 @@ mod tests { fn test_deploy_oracle_contract_pre_rex2() { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); - // Activate MiniRex only (pre-Rex2, pre-Rex5) - let hardforks = MegaHardforkConfig::default() - .with_all_activated() - .without(MegaHardfork::Rex2) - .without(MegaHardfork::Rex5); + // A complete ladder topping out at Rex1 (pre-Rex2, pre-Rex5). + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX1); let result = transact_deploy_oracle_contract(&hardforks, 0, &mut state) .expect("Deployment should succeed") @@ -327,8 +326,8 @@ mod tests { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); // Activate Rex2 but not Rex5 - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex5); + // A complete ladder topping out at Rex4: Rex2 reached, Rex5 not. + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX4); let result = transact_deploy_oracle_contract(&hardforks, 0, &mut state) .expect("Deployment should succeed") @@ -374,8 +373,8 @@ mod tests { let mut state = State::builder().with_database(&mut db).build(); // Rex2 active, Rex5 not active - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex5); + // A complete ladder topping out at Rex4: Rex2 reached, Rex5 not. + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX4); let result = transact_deploy_oracle_contract(&hardforks, 0, &mut state) .expect("Deployment should succeed") @@ -499,8 +498,8 @@ mod tests { let mut state = State::builder().with_database(&mut db).build(); // Rex2 active, Rex5 NOT active → pre-Rex5 path → old behaviour preserved. - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex5); + // A complete ladder topping out at Rex4: Rex2 reached, Rex5 not. + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX4); let result = transact_deploy_oracle_contract(&hardforks, 0, &mut state) .expect("Deployment should succeed") diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 07f7e118..1b7a6475 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -192,7 +192,29 @@ pub fn transact_deploy_sequencer_registry( db: &mut State, config: &SequencerRegistryConfig, ) -> Result, BlockExecutionError> { - if !hardforks.is_rex_5_active_at_timestamp(block_timestamp) { + let spec = hardforks.spec_id(block_timestamp); + let rex6_config = hardforks.fork_params::(); + transact_deploy_sequencer_registry_for(spec, rex6_config, current_block_number, db, config) +} + +/// [`transact_deploy_sequencer_registry`] against an already-resolved spec. +/// +/// The block executor resolves the spec once per block and calls this directly; the public +/// wrapper above resolves it for callers that hold a hardfork config. Like the flat-registry +/// spec builders, this deliberately does not take a hardfork config — everything a deploy +/// depends on arrives resolved (the spec and the typed params), so a per-fork activation gate +/// cannot be reintroduced here. +pub(crate) fn transact_deploy_sequencer_registry_for( + spec: crate::MegaSpecId, + rex6_config: Option<&SequencerRegistryRex6Config>, + current_block_number: u64, + db: &mut State, + config: &SequencerRegistryConfig, +) -> Result, BlockExecutionError> { + // Gate and bytecode selection follow the scheduled spec, not per-fork registration: + // a registry once deployed stays deployed through a spec rollback, and a config that + // schedules only a later fork must still get the Rex5 bootstrap. + if !spec.reaches(crate::MegaSpecId::REX5) { return Ok(None); } @@ -206,17 +228,15 @@ pub fn transact_deploy_sequencer_registry( // Select the bytecode version and, for v2.0.0, resolve the seeded minimum rotation delay. // The Rex6 params are required as soon as Rex6 is active: failing fast here surfaces a // misconfigured chain at the activation block instead of deploying an unseeded registry. - let rex6 = hardforks.is_rex_6_active_at_timestamp(block_timestamp); + let rex6 = spec.reaches(crate::MegaSpecId::REX6); let (target_code, target_code_hash) = if rex6 { (SEQUENCER_REGISTRY_CODE_REX6, SEQUENCER_REGISTRY_CODE_HASH_REX6) } else { (SEQUENCER_REGISTRY_CODE, SEQUENCER_REGISTRY_CODE_HASH) }; let min_rotation_delay = if rex6 { - let params = hardforks.fork_params::().ok_or_else(|| { - BlockValidationError::BlockHashContractCall { - message: "Rex6 active but SequencerRegistryRex6Config not configured".into(), - } + let params = rex6_config.ok_or_else(|| BlockValidationError::BlockHashContractCall { + message: "Rex6 active but SequencerRegistryRex6Config not configured".into(), })?; debug_assert!( params.validate().is_ok(), @@ -390,6 +410,12 @@ where /// - Pre-REX5: returns `(MEGA_SYSTEM_ADDRESS, None)`. /// - REX5: reads `_currentSystemAddress` from committed registry storage. /// +/// The single `spec` is read through two projections that answer different questions: +/// `is_enabled` (behavior) gates whether dynamic system-address resolution applies — an alias +/// window whose behavior projects below REX5 returns [`MEGA_SYSTEM_ADDRESS`] again — while +/// `reaches` (position) selects which registry bytecode version the pre-block deploy installed, +/// which a rollback does not change. +/// /// The optional `EvmState` captures account + slot reads as a witness record. /// The executor MUST commit this via `system_caller.on_state()` + `db.commit()`. pub fn resolve_system_address( @@ -397,6 +423,8 @@ pub fn resolve_system_address( spec: crate::MegaSpecId, db: &mut State, ) -> Result<(Address, Option), BlockExecutionError> { + // One spec, two projections: `is_enabled` (behavior) gates whether dynamic resolution + // applies at all; `reaches` (position) selects the installed bytecode version below. if !spec.is_enabled(crate::MegaSpecId::REX5) { return Ok((MEGA_SYSTEM_ADDRESS, None)); } @@ -420,9 +448,10 @@ pub fn resolve_system_address( }; // Unreachable: deploy verifies the code hash before seeding storage. The expected version - // follows the spec: the pre-block deploy has already swapped the bytecode to v2.0.0 at the - // Rex6 activation block, so an exact per-spec match holds on every block. - let expected_code_hash = if spec.is_enabled(crate::MegaSpecId::REX6) { + // follows the scheduled spec, matching what the pre-block deploy installed: the + // bytecode was already swapped to v2.0.0 at the Rex6 activation block, so an exact match + // holds on every block. + let expected_code_hash = if spec.reaches(crate::MegaSpecId::REX6) { SEQUENCER_REGISTRY_CODE_HASH_REX6 } else { SEQUENCER_REGISTRY_CODE_HASH diff --git a/crates/mega-evm/tests/block_executor/main.rs b/crates/mega-evm/tests/block_executor/main.rs index 663e9301..e7b3ca8b 100644 --- a/crates/mega-evm/tests/block_executor/main.rs +++ b/crates/mega-evm/tests/block_executor/main.rs @@ -4,5 +4,6 @@ mod accessed_block_hashes; mod block_limits; mod deposit_da_exemption; mod inspector; +mod partial_ladder; mod sequencer_registry; mod trait_factory_runtime_limits; diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs new file mode 100644 index 00000000..0a49dd34 --- /dev/null +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -0,0 +1,404 @@ +//! End-to-end tests for pre-block setup on partial and rollback hardfork ladders. +//! +//! Pre-block setup (system-contract predeploys, the EIP-2935/EIP-4788 fail-closed checks, the +//! `SequencerRegistry` bootstrap) is gated on the resolved spec position-compared via +//! `reaches`, not on per-fork registration and not on the behavior projection. These tests pin +//! both directions of that choice: +//! +//! - A *partial* ladder — a config scheduling a later fork without its predecessors — must still +//! run every lower fork's setup, rather than silently skipping it. +//! - A *rollback* window — mainnet's `MiniRex1`, which maps back to `EQUIVALENCE` — must keep +//! running the setup that earlier forks already activated, rather than retracting it. + +use std::{ + convert::Infallible, + sync::{Arc, Mutex}, +}; + +use alloy_evm::{ + block::{BlockExecutor, BlockValidationError, OnStateHook, StateChangeSource}, + EvmEnv, +}; +use alloy_hardforks::ForkCondition; +use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; +use alloy_primitives::{address, bytes, Address, Bytes, B256, U256}; +use mega_evm::{ + test_utils::MemoryDatabase, BlockLimits, MegaBlockExecutionCtx, MegaBlockExecutorFactory, + MegaEvmFactory, MegaHardfork, MegaHardforkConfig, MegaHardforks, MegaSpecId, ScheduleError, + SequencerRegistryConfig, SequencerRegistryRex6Config, TestExternalEnvs, ACCESS_CONTROL_ADDRESS, + HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, KEYLESS_DEPLOY_ADDRESS, LIMIT_CONTROL_ADDRESS, + ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_HASH, ORACLE_CONTRACT_CODE_HASH_REX5, + SEQUENCER_REGISTRY_ADDRESS, SEQUENCER_REGISTRY_CODE_HASH_REX6, +}; +use mega_system_contracts::sequencer_registry::storage_slots::MIN_ROTATION_DELAY; +use revm::{ + context::BlockEnv, + database::{Database as _, State}, + state::{AccountInfo, Bytecode, EvmState}, +}; + +const BLOCK_NUMBER: u64 = 1000; +const BLOCK_GAS_LIMIT: u64 = 30_000_000; +const MIN_ROTATION_DELAY_BLOCKS: u64 = 100; + +const BOOTSTRAP_SEQUENCER: Address = address!("0x4000000000000000000000000000000000000004"); +const BOOTSTRAP_ADMIN: Address = address!("0x5000000000000000000000000000000000000005"); + +/// Runtime bytecode that unconditionally reverts (`PUSH0 PUSH0 REVERT`). Installed at the +/// EIP-2935 history-storage address to force a failing pre-block system call. +/// +/// A heavy SALT bucket — the mechanism `tests/rex5/pre_block_system_calls.rs` uses — cannot force +/// the failure here: the pre-block call's caller is `eip4788::SYSTEM_ADDRESS`, which counts as +/// system-originated, so from Rex6 the transaction is exempt from resource metering and its +/// storage writes are charged un-scaled. Bucket capacity no longer influences the outcome. +const ALWAYS_REVERT_CODE: Bytes = bytes!("0x5f5ffd"); + +fn sequencer_registry_config() -> SequencerRegistryConfig { + SequencerRegistryConfig { + rex5_initial_sequencer: BOOTSTRAP_SEQUENCER, + rex5_initial_admin: BOOTSTRAP_ADMIN, + } +} + +/// A partial ladder: Rex6 activates while Rex5 is registered but never activates, and no earlier +/// fork is registered at all. `Rex5` must be present as an entry to carry +/// [`SequencerRegistryConfig`] — parameter lookup is activation-independent, but `with_params` +/// requires the entry to exist. +fn rex6_only_chain_spec() -> MegaHardforkConfig { + MegaHardforkConfig::default() + .with(MegaHardfork::Rex5, ForkCondition::Never) + .with_params(sequencer_registry_config()) + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)) + .with_params(SequencerRegistryRex6Config { + rex6_min_rotation_delay: MIN_ROTATION_DELAY_BLOCKS, + }) +} + +fn create_evm_env(spec: MegaSpecId, timestamp: u64) -> EvmEnv { + let mut cfg_env = revm::context::CfgEnv::default(); + cfg_env.spec = spec; + let block_env = BlockEnv { + number: U256::from(BLOCK_NUMBER), + timestamp: U256::from(timestamp), + gas_limit: BLOCK_GAS_LIMIT, + ..Default::default() + }; + EvmEnv::new(cfg_env, block_env) +} + +/// Non-zero parent hash and beacon root on a non-genesis block, so both EIP helpers actually +/// issue their system call rather than taking the genesis-skip early return. +fn block_ctx() -> MegaBlockExecutionCtx { + MegaBlockExecutionCtx::new( + B256::from([0x29; 32]), + Some(B256::from([0x47; 32])), + Bytes::new(), + BlockLimits::no_limits(), + ) +} + +fn install_code(db: &mut MemoryDatabase, address: Address, code: Bytes) { + let bytecode = Bytecode::new_raw(code); + db.insert_account_info( + address, + AccountInfo { code_hash: bytecode.hash_slow(), code: Some(bytecode), ..Default::default() }, + ); +} + +fn install_eip_contracts(db: &mut MemoryDatabase) { + install_code( + db, + alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS, + alloy_eips::eip2935::HISTORY_STORAGE_CODE.clone(), + ); + install_code( + db, + alloy_eips::eip4788::BEACON_ROOTS_ADDRESS, + alloy_eips::eip4788::BEACON_ROOTS_CODE.clone(), + ); +} + +/// Records every account observed by the on-state hook, so a test can assert that a read-only +/// pre-block outcome reached the witness path. +#[derive(Debug, Default, Clone)] +struct RecordingStateHook { + accounts: Arc>>, +} + +impl OnStateHook for RecordingStateHook { + fn on_state(&mut self, _source: StateChangeSource, state: &EvmState) { + self.accounts.lock().unwrap().extend(state.keys().copied()); + } +} + +/// A config that activates Rex6 without ever activating Rex5 must still materialize every +/// lower-spec predeploy — at the Rex5 Oracle version — and bootstrap the `SequencerRegistry` at +/// v2.0.0. Under per-fork gating none of this ran. +#[test] +fn test_partial_ladder_runs_lower_fork_setup() { + let chain_spec = rex6_only_chain_spec(); + // Precondition: this really is a partial ladder (no activation event below Rex6), and the + // executing spec is Rex6. The `is_*_active_at_timestamp` predicates cannot state this — they + // project the scheduled spec, which a partial ladder keeps high by design. + assert_eq!(chain_spec.mega_fork_activation(MegaHardfork::Rex5), ForkCondition::Never); + assert_eq!(chain_spec.mega_fork_activation(MegaHardfork::MiniRex), ForkCondition::Never); + assert_eq!(chain_spec.spec_id(0), MegaSpecId::REX6); + + let mut db = MemoryDatabase::default(); + install_eip_contracts(&mut db); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let block_executor_factory = + MegaBlockExecutorFactory::new(chain_spec, evm_factory, OpAlloyReceiptBuilder::default()); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(MegaSpecId::REX6, 0), + ); + + executor.apply_pre_execution_changes().expect("partial-ladder pre-block setup must succeed"); + + for address in [ + ORACLE_CONTRACT_ADDRESS, + HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, + KEYLESS_DEPLOY_ADDRESS, + ACCESS_CONTROL_ADDRESS, + LIMIT_CONTROL_ADDRESS, + SEQUENCER_REGISTRY_ADDRESS, + ] { + let info = state.basic(address).unwrap(); + assert!( + info.is_some_and(|i| i.code_hash != revm::primitives::KECCAK_EMPTY), + "{address} must be deployed on a partial ladder" + ); + } + + // The Oracle takes its Rex5 bytecode, and the registry its Rex6 bytecode with the seeded + // rotation delay — i.e. the position drives version selection too, not just the on/off gate. + assert_eq!( + state.basic(ORACLE_CONTRACT_ADDRESS).unwrap().unwrap().code_hash, + ORACLE_CONTRACT_CODE_HASH_REX5 + ); + assert_eq!( + state.basic(SEQUENCER_REGISTRY_ADDRESS).unwrap().unwrap().code_hash, + SEQUENCER_REGISTRY_CODE_HASH_REX6 + ); + assert_eq!( + state.storage(SEQUENCER_REGISTRY_ADDRESS, MIN_ROTATION_DELAY).unwrap(), + U256::from(MIN_ROTATION_DELAY_BLOCKS) + ); +} + +/// The Rex5 fail-closed rule on pre-block system calls must be enforced on a partial ladder too. +/// Under per-fork gating the Rex5 check was skipped and a reverting EIP-2935 call was silently +/// accepted. +/// +/// Kept separate from the deployment test above: a pre-block EIP failure returns before the +/// predeploy loop is reached, so one block cannot assert both. +#[test] +fn test_partial_ladder_enforces_pre_block_fail_closed() { + let mut db = MemoryDatabase::default(); + install_eip_contracts(&mut db); + // Break EIP-2935 specifically. + install_code(&mut db, alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS, ALWAYS_REVERT_CODE); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let block_executor_factory = MegaBlockExecutorFactory::new( + rex6_only_chain_spec(), + evm_factory, + OpAlloyReceiptBuilder::default(), + ); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(MegaSpecId::REX6, 0), + ); + + let err = executor + .apply_pre_execution_changes() + .expect_err("a reverting EIP-2935 pre-block call must reject the block"); + let alloy_evm::block::BlockExecutionError::Validation(validation_err) = &err else { + panic!("expected a validation error, got: {err:?}"); + }; + assert!( + matches!(validation_err, BlockValidationError::BlockHashContractCall { .. }), + "expected BlockHashContractCall, got: {validation_err:?}" + ); + // Assert the *specific* failure. Both gating styles reject this block, but for different + // reasons: under per-fork gating the Rex5 check is skipped and the reverting call is + // accepted, only for `resolve_system_address` to later trip over the registry that was + // never deployed. Matching on the error variant alone would pass either way. + assert!( + format!("{validation_err:?}").contains("EIP-2935 pre-block system call did not succeed"), + "block must be rejected by the EIP-2935 fail-closed check itself, got: {validation_err:?}" + ); +} + +/// A config that activates Rex6 with no Rex5 entry at all cannot carry the +/// [`SequencerRegistryConfig`] the Rex5 bootstrap requires. Pre-block setup must fail closed +/// with a configuration error rather than silently skipping the registry. +/// +/// This drives `pre_execution_changes` directly rather than `apply_pre_execution_changes`: the +/// latter also runs `resolve_system_address`, which gates on the executing spec and so raises the +/// same error under either gating style. Only the pre-block setup step distinguishes them — +/// under per-fork gating it returns `Ok`, having quietly skipped the registry entirely. +#[test] +fn test_rex6_without_any_rex5_entry_fails_closed() { + let chain_spec = MegaHardforkConfig::default() + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)) + .with_params(SequencerRegistryRex6Config { + rex6_min_rotation_delay: MIN_ROTATION_DELAY_BLOCKS, + }); + + let mut db = MemoryDatabase::default(); + install_eip_contracts(&mut db); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let block_executor_factory = + MegaBlockExecutorFactory::new(chain_spec, evm_factory, OpAlloyReceiptBuilder::default()); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(MegaSpecId::REX6, 0), + ); + + let err = executor + .pre_execution_changes() + .expect_err("Rex5 setup without its config must fail rather than be skipped"); + assert!( + format!("{err:?}").contains("SequencerRegistryConfig not configured"), + "expected the missing-config error, got: {err:?}" + ); +} + +/// The load-time params roster (`validate_schedule` → `MissingParams`) and the block-time +/// fail-closed checks are written independently but must cover the same requirements: one +/// config missing a required params type has to fail BOTH surfaces. A params rule registered +/// on only one side — a schedule accepted at load that dies at the fork's first block, or a +/// load-time rule with no execution-time counterpart — fails this pairing. +#[test] +fn test_missing_params_fail_at_load_time_and_pre_block_alike() { + let ladder_through = |top: MegaHardfork| { + MegaHardfork::VARIANTS + .iter() + .copied() + .filter(|fork| { + !fork.spec_id().is_alias() && fork.spec_id() as u8 <= top.spec_id() as u8 + }) + .fold(MegaHardforkConfig::default(), |config, fork| { + config.with(fork, ForkCondition::Timestamp(0)) + }) + }; + let run_pre_block = |chain_spec: MegaHardforkConfig, spec: MegaSpecId| { + let mut db = MemoryDatabase::default(); + install_eip_contracts(&mut db); + let mut state = State::builder().with_database(&mut db).build(); + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let block_executor_factory = MegaBlockExecutorFactory::new( + chain_spec, + evm_factory, + OpAlloyReceiptBuilder::default(), + ); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(spec, 0), + ); + executor + .pre_execution_changes() + .err() + .map(|err| format!("{err:?}")) + .expect("a schedule missing required params must fail pre-block execution") + }; + + // Rex5 scheduled without `SequencerRegistryConfig`. + let rex5_without_params = ladder_through(MegaHardfork::Rex5); + assert_eq!( + rex5_without_params.validate_schedule(), + Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex5, + params: "SequencerRegistryConfig" + }) + ); + let err = run_pre_block(rex5_without_params, MegaSpecId::REX5); + assert!(err.contains("SequencerRegistryConfig not configured"), "got: {err}"); + + // Rex6 scheduled with the Rex5 params attached but without `SequencerRegistryRex6Config`. + let rex6_without_params = + ladder_through(MegaHardfork::Rex6).with_params(sequencer_registry_config()); + assert_eq!( + rex6_without_params.validate_schedule(), + Err(ScheduleError::MissingParams { + fork: MegaHardfork::Rex6, + params: "SequencerRegistryRex6Config" + }) + ); + let err = run_pre_block(rex6_without_params, MegaSpecId::REX6); + assert!(err.contains("SequencerRegistryRex6Config not configured"), "got: {err}"); +} + +/// Mainnet's `MiniRex1` window rolls the executing spec back to `EQUIVALENCE` while `MiniRex`'s +/// predeploys remain installed. Pre-block setup must keep emitting their read-only outcomes: +/// dropping them would remove the accounts from the on-state hook that feeds the stateless +/// witness and the state-sync transition shard — a replay-observable change on frozen history. +#[test] +fn test_rollback_window_still_emits_predeploy_witness_entries() { + let chain_spec = mega_evm::mainnet_hardforks(); + let ForkCondition::Timestamp(timestamp) = + chain_spec.mega_fork_activation(MegaHardfork::MiniRex1) + else { + panic!("mainnet must schedule MiniRex1 by timestamp"); + }; + // Precondition: inside the window the scheduled spec is the alias rung and its behavior + // projects back to EQUIVALENCE. + assert_eq!(chain_spec.spec_id(timestamp), MegaSpecId::MINI_REX_1); + assert_eq!(chain_spec.spec_id(timestamp).behavior(), MegaSpecId::EQUIVALENCE); + + let mut db = MemoryDatabase::default(); + install_eip_contracts(&mut db); + // Both MiniRex predeploys are already installed, as they would be on mainnet by then, so the + // deploy takes its idempotent read-only path. + install_code(&mut db, ORACLE_CONTRACT_ADDRESS, mega_evm::ORACLE_CONTRACT_CODE); + install_code( + &mut db, + HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, + mega_evm::HIGH_PRECISION_TIMESTAMP_ORACLE_CODE, + ); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let block_executor_factory = + MegaBlockExecutorFactory::new(chain_spec, evm_factory, OpAlloyReceiptBuilder::default()); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(MegaSpecId::MINI_REX_1, timestamp), + ); + + let recorder = RecordingStateHook::default(); + let accounts = recorder.accounts.clone(); + BlockExecutor::set_state_hook(&mut executor, Some(Box::new(recorder))); + executor.apply_pre_execution_changes().expect("rollback-window pre-block setup must succeed"); + + let observed = accounts.lock().unwrap(); + for address in [ORACLE_CONTRACT_ADDRESS, HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS] { + assert!( + observed.contains(&address), + "{address} must still reach the witness hook during the rollback window" + ); + } + + // The rollback window is pre-Rex2, so the Oracle keeps its v1.0.0 bytecode. + assert_eq!( + state.basic(ORACLE_CONTRACT_ADDRESS).unwrap().unwrap().code_hash, + ORACLE_CONTRACT_CODE_HASH + ); +} diff --git a/crates/mega-evm/tests/compute_gas/main.rs b/crates/mega-evm/tests/compute_gas/main.rs index 8bd085c8..44b9904b 100644 --- a/crates/mega-evm/tests/compute_gas/main.rs +++ b/crates/mega-evm/tests/compute_gas/main.rs @@ -73,10 +73,14 @@ const INITCODE_RETURNING_32_BYTES: [u8; 4] = [0x60, 0x20, 0x5f, 0xf3]; const ONE_ETH: u128 = 1_000_000_000_000_000_000; -/// Every spec in the progression, oldest first. -const ALL_SPECS: [(MegaSpecId, &str); 10] = [ +/// Every spec in the progression, oldest first. The alias rungs are included: their columns +/// must stay byte-identical to their behavior targets' (`MiniRex1` = `Equivalence`, +/// `MiniRex2` = `MiniRex`), so an alias diverging from its target surfaces as a snapshot diff. +const ALL_SPECS: [(MegaSpecId, &str); 12] = [ (MegaSpecId::EQUIVALENCE, "Equivalence"), (MegaSpecId::MINI_REX, "MiniRex"), + (MegaSpecId::MINI_REX_1, "MiniRex1"), + (MegaSpecId::MINI_REX_2, "MiniRex2"), (MegaSpecId::REX, "Rex"), (MegaSpecId::REX1, "Rex1"), (MegaSpecId::REX2, "Rex2"), @@ -913,8 +917,8 @@ fn test_compute_gas_survives_reverted_frame() { }; for (spec, spec_name) in ALL_SPECS { - if spec == MegaSpecId::EQUIVALENCE { - continue; // no metering + if spec.behavior() == MegaSpecId::EQUIVALENCE { + continue; // no metering (covers the `MiniRex1` alias, which executes Equivalence) } let with_revert = transact(spec, (program.build_db)()); let baseline = transact(spec, baseline_db()); diff --git a/crates/mega-evm/tests/compute_gas/snapshot.txt b/crates/mega-evm/tests/compute_gas/snapshot.txt index d6e976fb..000b0fef 100644 --- a/crates/mega-evm/tests/compute_gas/snapshot.txt +++ b/crates/mega-evm/tests/compute_gas/snapshot.txt @@ -14,6 +14,8 @@ intrinsic_only Equivalence 0 21000 success intrinsic_only MiniRex 21000 21000 success +intrinsic_only MiniRex1 0 21000 success +intrinsic_only MiniRex2 21000 21000 success intrinsic_only Rex 21000 60000 success intrinsic_only Rex1 21000 60000 success intrinsic_only Rex2 21000 60000 success @@ -25,6 +27,8 @@ intrinsic_only Rex7 21000 60000 success plain_arithmetic Equivalence 0 21352 success plain_arithmetic MiniRex 21352 21352 success +plain_arithmetic MiniRex1 0 21352 success +plain_arithmetic MiniRex2 21352 21352 success plain_arithmetic Rex 21352 60352 success plain_arithmetic Rex1 21352 60352 success plain_arithmetic Rex2 21352 60352 success @@ -36,6 +40,8 @@ plain_arithmetic Rex7 21352 60352 success plain_memory_expansion Equivalence 0 21437 success plain_memory_expansion MiniRex 21437 21437 success +plain_memory_expansion MiniRex1 0 21437 success +plain_memory_expansion MiniRex2 21437 21437 success plain_memory_expansion Rex 21437 60437 success plain_memory_expansion Rex1 21437 60437 success plain_memory_expansion Rex2 21437 60437 success @@ -47,6 +53,8 @@ plain_memory_expansion Rex7 21437 60437 success volatile_timestamp Equivalence 0 21004 success volatile_timestamp MiniRex 21004 21004 success +volatile_timestamp MiniRex1 0 21004 success +volatile_timestamp MiniRex2 21004 21004 success volatile_timestamp Rex 21004 60004 success volatile_timestamp Rex1 21004 60004 success volatile_timestamp Rex2 21004 60004 success @@ -58,6 +66,8 @@ volatile_timestamp Rex7 21004 60004 success sstore_zero_to_nonzero Equivalence 0 43106 success sstore_zero_to_nonzero MiniRex 43106 2043106 success +sstore_zero_to_nonzero MiniRex1 0 43106 success +sstore_zero_to_nonzero MiniRex2 43106 2043106 success sstore_zero_to_nonzero Rex 43106 82106 success sstore_zero_to_nonzero Rex1 43106 82106 success sstore_zero_to_nonzero Rex2 43106 82106 success @@ -69,6 +79,8 @@ sstore_zero_to_nonzero Rex7 43106 82106 success sstore_nonzero_to_nonzero Equivalence 0 26006 success sstore_nonzero_to_nonzero MiniRex 26006 26006 success +sstore_nonzero_to_nonzero MiniRex1 0 26006 success +sstore_nonzero_to_nonzero MiniRex2 26006 26006 success sstore_nonzero_to_nonzero Rex 26006 65006 success sstore_nonzero_to_nonzero Rex1 26006 65006 success sstore_nonzero_to_nonzero Rex2 26006 65006 success @@ -80,6 +92,8 @@ sstore_nonzero_to_nonzero Rex7 26006 65006 success sstore_nonzero_to_zero Equivalence 0 21206 success sstore_nonzero_to_zero MiniRex 26006 21206 success +sstore_nonzero_to_zero MiniRex1 0 21206 success +sstore_nonzero_to_zero MiniRex2 26006 21206 success sstore_nonzero_to_zero Rex 26006 60206 success sstore_nonzero_to_zero Rex1 26006 60206 success sstore_nonzero_to_zero Rex2 26006 60206 success @@ -91,6 +105,8 @@ sstore_nonzero_to_zero Rex7 26006 60206 success log0_empty Equivalence 0 21381 success log0_empty MiniRex 21381 21381 success +log0_empty MiniRex1 0 21381 success +log0_empty MiniRex2 21381 21381 success log0_empty Rex 21381 60381 success log0_empty Rex1 21381 60381 success log0_empty Rex2 21381 60381 success @@ -102,6 +118,8 @@ log0_empty Rex7 21381 60381 success log1_32bytes Equivalence 0 22027 success log1_32bytes MiniRex 22027 28337 success +log1_32bytes MiniRex1 0 22027 success +log1_32bytes MiniRex2 22027 28337 success log1_32bytes Rex 22027 67337 success log1_32bytes Rex1 22027 67337 success log1_32bytes Rex2 22027 67337 success @@ -113,6 +131,8 @@ log1_32bytes Rex7 22027 67337 success log4_128bytes Equivalence 0 23965 success log4_128bytes MiniRex 23965 49205 success +log4_128bytes MiniRex1 0 23965 success +log4_128bytes MiniRex2 23965 49205 success log4_128bytes Rex 23965 88205 success log4_128bytes Rex1 23965 88205 success log4_128bytes Rex2 23965 88205 success @@ -124,6 +144,8 @@ log4_128bytes Rex7 23965 88205 success log2_log3_sequence Equivalence 0 24176 success log2_log3_sequence MiniRex 24176 48046 success +log2_log3_sequence MiniRex1 0 24176 success +log2_log3_sequence MiniRex2 24176 48046 success log2_log3_sequence Rex 24176 87046 success log2_log3_sequence Rex1 24176 87046 success log2_log3_sequence Rex2 24176 87046 success @@ -135,6 +157,8 @@ log2_log3_sequence Rex7 24176 87046 success call_no_value_to_code Equivalence 0 23621 success call_no_value_to_code MiniRex 23621 23621 success +call_no_value_to_code MiniRex1 0 23621 success +call_no_value_to_code MiniRex2 23621 23621 success call_no_value_to_code Rex 23621 62621 success call_no_value_to_code Rex1 23621 62621 success call_no_value_to_code Rex2 23621 62621 success @@ -146,6 +170,8 @@ call_no_value_to_code Rex7 23621 62621 success call_value_to_empty Equivalence 0 55321 success call_value_to_empty MiniRex 55321 2055321 success +call_value_to_empty MiniRex1 0 55321 success +call_value_to_empty MiniRex2 55321 2055321 success call_value_to_empty Rex 55321 94321 success call_value_to_empty Rex1 55321 94321 success call_value_to_empty Rex2 55321 94321 success @@ -157,6 +183,8 @@ call_value_to_empty Rex7 57621 94321 success call_value_to_existing Equivalence 0 30321 success call_value_to_existing MiniRex 30321 30321 success +call_value_to_existing MiniRex1 0 30321 success +call_value_to_existing MiniRex2 30321 30321 success call_value_to_existing Rex 30321 69321 success call_value_to_existing Rex1 30321 69321 success call_value_to_existing Rex2 30321 69321 success @@ -168,6 +196,8 @@ call_value_to_existing Rex7 32621 69321 success callcode_value Equivalence 0 30321 success callcode_value MiniRex 30321 30321 success +callcode_value MiniRex1 0 30321 success +callcode_value MiniRex2 30321 30321 success callcode_value Rex 30321 69321 success callcode_value Rex1 30321 69321 success callcode_value Rex2 30321 69321 success @@ -179,6 +209,8 @@ callcode_value Rex7 32621 69321 success delegatecall Equivalence 0 23618 success delegatecall MiniRex 23618 23618 success +delegatecall MiniRex1 0 23618 success +delegatecall MiniRex2 23618 23618 success delegatecall Rex 23618 62618 success delegatecall Rex1 23618 62618 success delegatecall Rex2 23618 62618 success @@ -190,6 +222,8 @@ delegatecall Rex7 23618 62618 success staticcall Equivalence 0 23618 success staticcall MiniRex 23618 23618 success +staticcall MiniRex1 0 23618 success +staticcall MiniRex2 23618 23618 success staticcall Rex 23618 62618 success staticcall Rex1 23618 62618 success staticcall Rex2 23618 62618 success @@ -201,6 +235,8 @@ staticcall Rex7 23618 62618 success nested_calls_depth3 Equivalence 0 26236 success nested_calls_depth3 MiniRex 26236 26236 success +nested_calls_depth3 MiniRex1 0 26236 success +nested_calls_depth3 MiniRex2 26236 26236 success nested_calls_depth3 Rex 26236 65236 success nested_calls_depth3 Rex1 26236 65236 success nested_calls_depth3 Rex2 26236 65236 success @@ -212,6 +248,8 @@ nested_calls_depth3 Rex7 26236 65236 success create_empty_initcode Equivalence 0 53009 success create_empty_initcode MiniRex 53009 2053009 success +create_empty_initcode MiniRex1 0 53009 success +create_empty_initcode MiniRex2 53009 2053009 success create_empty_initcode Rex 53009 92009 success create_empty_initcode Rex1 53009 92009 success create_empty_initcode Rex2 53009 92009 success @@ -223,6 +261,8 @@ create_empty_initcode Rex7 53009 92009 success create2_empty_initcode Equivalence 0 53012 success create2_empty_initcode MiniRex 53012 2053012 success +create2_empty_initcode MiniRex1 0 53012 success +create2_empty_initcode MiniRex2 53012 2053012 success create2_empty_initcode Rex 53012 92012 success create2_empty_initcode Rex1 53012 92012 success create2_empty_initcode Rex2 53012 92012 success @@ -234,6 +274,8 @@ create2_empty_initcode Rex7 53012 92012 success create2_with_initcode Equivalence 0 53032 success create2_with_initcode MiniRex 53032 2053032 success +create2_with_initcode MiniRex1 0 53032 success +create2_with_initcode MiniRex2 53032 2053032 success create2_with_initcode Rex 53032 92032 success create2_with_initcode Rex1 53032 92032 success create2_with_initcode Rex2 53032 92032 success @@ -245,6 +287,8 @@ create2_with_initcode Rex7 53032 92032 success create_deploying_runtime_code Equivalence 0 59431 success create_deploying_runtime_code MiniRex 59431 2379431 success +create_deploying_runtime_code MiniRex1 0 59431 success +create_deploying_runtime_code MiniRex2 59431 2379431 success create_deploying_runtime_code Rex 59431 418431 success create_deploying_runtime_code Rex1 59431 418431 success create_deploying_runtime_code Rex2 59431 418431 success @@ -256,6 +300,8 @@ create_deploying_runtime_code Rex7 59431 418431 success create2_far_memory_offset Equivalence 0 67367 success create2_far_memory_offset MiniRex 67367 2067367 success +create2_far_memory_offset MiniRex1 0 67367 success +create2_far_memory_offset MiniRex2 67367 2067367 success create2_far_memory_offset Rex 67367 106367 success create2_far_memory_offset Rex1 67367 106367 success create2_far_memory_offset Rex2 67367 106367 success @@ -267,6 +313,8 @@ create2_far_memory_offset Rex7 67367 106367 success create2_oversized_initcode Equivalence 0 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode MiniRex 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) +create2_oversized_initcode MiniRex1 0 100000000 halt Base(Base(CreateInitCodeSizeLimit)) +create2_oversized_initcode MiniRex2 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode Rex 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode Rex1 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode Rex2 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) @@ -278,6 +326,8 @@ create2_oversized_initcode Rex7 21012 100000000 halt Ba selfdestruct_to_empty Equivalence 0 53603 success selfdestruct_to_empty MiniRex 21003 100000000 halt Base(Base(InvalidFEOpcode)) +selfdestruct_to_empty MiniRex1 0 53603 success +selfdestruct_to_empty MiniRex2 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_empty Rex 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_empty Rex1 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_empty Rex2 53603 92603 success @@ -289,6 +339,8 @@ selfdestruct_to_empty Rex7 53603 92603 success selfdestruct_to_existing Equivalence 0 28603 success selfdestruct_to_existing MiniRex 21003 100000000 halt Base(Base(InvalidFEOpcode)) +selfdestruct_to_existing MiniRex1 0 28603 success +selfdestruct_to_existing MiniRex2 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_existing Rex 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_existing Rex1 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_existing Rex2 28603 67603 success @@ -300,6 +352,8 @@ selfdestruct_to_existing Rex7 28603 67603 success selfdestruct_to_self Equivalence 0 26003 success selfdestruct_to_self MiniRex 21003 100000000 halt Base(Base(InvalidFEOpcode)) +selfdestruct_to_self MiniRex1 0 26003 success +selfdestruct_to_self MiniRex2 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_self Rex 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_self Rex1 21003 100000000 halt Base(Base(InvalidFEOpcode)) selfdestruct_to_self Rex2 26003 65003 success @@ -311,6 +365,8 @@ selfdestruct_to_self Rex7 26003 65003 success precompile_identity Equivalence 0 21157 success precompile_identity MiniRex 23657 23657 success +precompile_identity MiniRex1 0 21157 success +precompile_identity MiniRex2 23657 23657 success precompile_identity Rex 23657 62657 success precompile_identity Rex1 23657 62657 success precompile_identity Rex2 23657 62657 success @@ -322,6 +378,8 @@ precompile_identity Rex7 23657 62657 success precompile_sha256 Equivalence 0 21211 success precompile_sha256 MiniRex 23711 23711 success +precompile_sha256 MiniRex1 0 21211 success +precompile_sha256 MiniRex2 23711 23711 success precompile_sha256 Rex 23711 62711 success precompile_sha256 Rex1 23711 62711 success precompile_sha256 Rex2 23711 62711 success @@ -333,6 +391,8 @@ precompile_sha256 Rex7 23711 62711 success precompile_kzg_invalid_input Equivalence 0 221133 success precompile_kzg_invalid_input MiniRex 23633 223633 success +precompile_kzg_invalid_input MiniRex1 0 221133 success +precompile_kzg_invalid_input MiniRex2 23633 223633 success precompile_kzg_invalid_input Rex 23633 262633 success precompile_kzg_invalid_input Rex1 23633 262633 success precompile_kzg_invalid_input Rex2 23633 262633 success @@ -344,6 +404,8 @@ precompile_kzg_invalid_input Rex7 123633 262633 success precompile_underfunded Equivalence 0 21140 success precompile_underfunded MiniRex 23639 23640 success +precompile_underfunded MiniRex1 0 21140 success +precompile_underfunded MiniRex2 23639 23640 success precompile_underfunded Rex 23639 62640 success precompile_underfunded Rex1 23639 62640 success precompile_underfunded Rex2 23639 62640 success @@ -355,6 +417,8 @@ precompile_underfunded Rex7 23640 62640 success reverting_subcall Equivalence 0 24326 success reverting_subcall MiniRex 24326 24326 success +reverting_subcall MiniRex1 0 24326 success +reverting_subcall MiniRex2 24326 24326 success reverting_subcall Rex 24326 63326 success reverting_subcall Rex1 24326 63326 success reverting_subcall Rex2 24326 63326 success diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index cef5d674..0c55baac 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -211,36 +211,69 @@ fn staged_config() -> MegaHardforkConfig { .with(MegaHardfork::Rex3, ForkCondition::Timestamp(700)) .with(MegaHardfork::Rex4, ForkCondition::Timestamp(800)) .with(MegaHardfork::Rex5, ForkCondition::Timestamp(900)) + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(1000)) + .with(MegaHardfork::Rex7, ForkCondition::Timestamp(1100)) } -/// Each `is_*_active_at_timestamp` returns `true` at/after activation and `false` before. This -/// kills the `-> false` mutants on the MiniRex1/MiniRex2/Rex1/Rex3 predicates (and pins the rest). +/// Each `is_*_active_at_timestamp` returns `true` at/after activation and `false` before, on a +/// complete ordered ladder (where the position-projected predicates coincide with the activation +/// events). This kills the `-> false` mutants on the predicate bodies in +/// `crates/mega-evm/src/block/hardfork.rs` at the lines referenced below. +/// +/// Every predicate is probed on **both** sides of its own activation, which is what pins the +/// spec each one projects. A behavior-introducing predicate's body is +/// `spec_id(t).reaches(MegaSpecId::X)`, so shifting `X` one rung either way is a +/// live mutation: shifting down makes the predicate fire at the rung below (caught by the +/// `false` assertion one second early), shifting up makes it stop firing at its own rung (caught +/// by the `true` assertion). A one-sided probe kills neither direction. #[test] fn test_hardfork_activation_predicates_are_true_at_activation() { let cfg = staged_config(); - // MiniRex1 (line 144) — false strictly before, true at activation. + // MiniRex (hardfork.rs:192) — spec-introducing (MINI_REX). Shifting down lands on + // EQUIVALENCE, which is enabled at every timestamp, so only the `false` side catches it. + assert!(!cfg.is_mini_rex_active_at_timestamp(99)); + assert!(cfg.is_mini_rex_active_at_timestamp(100)); + + // MiniRex1 (hardfork.rs:203) — alias fork, raw event query. assert!(!cfg.is_mini_rex_1_active_at_timestamp(199)); assert!(cfg.is_mini_rex_1_active_at_timestamp(200)); - // MiniRex2 (line 149). + // MiniRex2 (hardfork.rs:213) — alias fork, raw event query. assert!(!cfg.is_mini_rex_2_active_at_timestamp(299)); assert!(cfg.is_mini_rex_2_active_at_timestamp(300)); - // Rex1 (line 159). + // Rex (hardfork.rs:220). + assert!(!cfg.is_rex_active_at_timestamp(399)); + assert!(cfg.is_rex_active_at_timestamp(400)); + + // Rex1 (hardfork.rs:227). assert!(!cfg.is_rex_1_active_at_timestamp(499)); assert!(cfg.is_rex_1_active_at_timestamp(500)); - // Rex3 (line 169). + // Rex2 (hardfork.rs:234). + assert!(!cfg.is_rex_2_active_at_timestamp(599)); + assert!(cfg.is_rex_2_active_at_timestamp(600)); + + // Rex3 (hardfork.rs:241). assert!(!cfg.is_rex_3_active_at_timestamp(699)); assert!(cfg.is_rex_3_active_at_timestamp(700)); - // Sanity on the neighbouring predicates so the staged config is self-consistent. - assert!(cfg.is_mini_rex_active_at_timestamp(100)); - assert!(cfg.is_rex_active_at_timestamp(400)); - assert!(cfg.is_rex_2_active_at_timestamp(600)); + // Rex4 (hardfork.rs:248). + assert!(!cfg.is_rex_4_active_at_timestamp(799)); assert!(cfg.is_rex_4_active_at_timestamp(800)); + + // Rex5 (hardfork.rs:255). + assert!(!cfg.is_rex_5_active_at_timestamp(899)); assert!(cfg.is_rex_5_active_at_timestamp(900)); + + // Rex6 (hardfork.rs:262). + assert!(!cfg.is_rex_6_active_at_timestamp(999)); + assert!(cfg.is_rex_6_active_at_timestamp(1000)); + + // Rex7 (hardfork.rs:269). + assert!(!cfg.is_rex_7_active_at_timestamp(1099)); + assert!(cfg.is_rex_7_active_at_timestamp(1100)); } // ============================================================================ diff --git a/crates/mega-evm/tests/rex4/deployment.rs b/crates/mega-evm/tests/rex4/deployment.rs index 685a37f7..0c339519 100644 --- a/crates/mega-evm/tests/rex4/deployment.rs +++ b/crates/mega-evm/tests/rex4/deployment.rs @@ -21,6 +21,9 @@ type TestEvm<'a, 'db> = MegaEvm<&'a mut TestState<'db>, NoOpInspector, EmptyExte type TestExecutor<'a, 'db> = MegaBlockExecutor, OpAlloyReceiptBuilder>; +// Complete ladders topping out at the target fork. Expressing these as +// `with_all_activated().without(Rex5)` instead would leave Rex6 activated, and pre-block setup +// gates compare rung positions — which would still report REX6 and open every lower gate. fn rex4_chain_spec() -> MegaHardforkConfig { MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX4) } diff --git a/crates/mega-evm/tests/rex5/pre_block_system_calls.rs b/crates/mega-evm/tests/rex5/pre_block_system_calls.rs index 8fdeff6b..2e7b1f1e 100644 --- a/crates/mega-evm/tests/rex5/pre_block_system_calls.rs +++ b/crates/mega-evm/tests/rex5/pre_block_system_calls.rs @@ -90,10 +90,10 @@ fn rex5_chain_spec() -> MegaHardforkConfig { } fn rex4_chain_spec() -> MegaHardforkConfig { - // Activate up to Rex4 only — Rex5 stays at `ForkCondition::Never`, so - // `is_rex_5_active_at_timestamp(_)` returns false and the new check is + // Activate up to Rex4 only — Rex5 stays at `ForkCondition::Never`, so the + // activated-spec floor stays below REX5 and the Rex5 fail-closed check is // skipped. Sequencer registry is not needed because its deploy is gated - // on Rex5 activation. + // on the same floor. MegaHardforkConfig::default().with(MegaHardfork::Rex4, ForkCondition::Timestamp(0)) } @@ -273,12 +273,15 @@ fn test_pre_rex5_preserves_silent_pre_block_call_failure() { .apply_pre_execution_changes() .expect("pre-REX5 must accept a halted pre-block system call"); - // The OOG'd SSTORE never wrote the parent hash to slot 0, so slot 0 - // must not equal the parent-hash value. + // The OOG'd SSTORE never wrote the parent hash to the ring-buffer slot the + // contract targets, `(block.number - 1) % 8191`. let storage_after = executor .evm_mut() .db_mut() - .storage(alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS, U256::ZERO) + .storage( + alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS, + U256::from((ACTIVATION_BLOCK - 1) % 8191), + ) .unwrap(); let parent_hash_word = U256::from_be_bytes(B256::from([0x29; 32]).0); assert_ne!( @@ -287,6 +290,66 @@ fn test_pre_rex5_preserves_silent_pre_block_call_failure() { ); } +/// The counterpart to `test_rex5_block_aware_budget_accepts_pre_block_call_above_30m`: on a +/// pre-REX5 chain the same ≈100M-per-SSTORE costs must still OOG, because pre-REX5 keeps +/// revm's upstream-fixed 30M budget for replay parity. The budget selection must follow the +/// `REX5` spec exactly — a gate at any earlier spec would run these historical blocks with +/// the block-aware budget and commit writes that mainnet history does not contain. The block +/// is still accepted (no fail-closed pre-REX5), but neither contract's slot may be written. +#[test] +fn test_pre_rex5_keeps_upstream_30m_budget_for_pre_block_calls() { + let mut db = MemoryDatabase::default(); + install_eip2935_history_storage(&mut db); + install_eip4788_beacon_roots(&mut db); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = MegaEvmFactory::new().with_external_env_factory(medium_external_envs()); + let block_executor_factory = MegaBlockExecutorFactory::new( + rex4_chain_spec(), + evm_factory, + OpAlloyReceiptBuilder::default(), + ); + let mut executor = block_executor_factory.create_executor( + &mut state, + block_ctx(), + create_evm_env(MegaSpecId::REX4, BLOCK_GAS_LIMIT), + ); + + executor + .apply_pre_execution_changes() + .expect("pre-REX5 must accept the halted pre-block system calls"); + + // EIP-2935: the ≈100M SSTORE OOG'd inside the 30M budget, so the ring-buffer slot the + // contract writes the parent hash to — `(block.number - 1) % 8191` — must stay empty. + let history_slot = executor + .evm_mut() + .db_mut() + .storage( + alloy_eips::eip2935::HISTORY_STORAGE_ADDRESS, + U256::from((ACTIVATION_BLOCK - 1) % 8191), + ) + .unwrap(); + assert_ne!( + history_slot, + U256::from_be_bytes(B256::from([0x29; 32]).0), + "EIP-2935 must stay on the 30M budget pre-REX5, so the ≈100M write cannot land", + ); + + // EIP-4788: same for the beacon-roots timestamp slot (the contract's ring buffer keys + // slots by `timestamp % 8191`). + let timestamp_slot = U256::from(1_800_000_000u64 % 8191); + let stored_timestamp = executor + .evm_mut() + .db_mut() + .storage(alloy_eips::eip4788::BEACON_ROOTS_ADDRESS, timestamp_slot) + .unwrap(); + assert_ne!( + stored_timestamp, + U256::from(1_800_000_000u64), + "EIP-4788 must stay on the 30M budget pre-REX5, so the ≈100M writes cannot land", + ); +} + /// Medium SALT makes each EIP-2935 / EIP-4788 SSTORE cost ≈ 100M of /// dynamic storage gas — above revm's upstream-fixed 30M default but /// within the REX5 `max(block.gas_limit, SYSTEM_CALL_GAS_LIMIT_FLOOR)` diff --git a/crates/mega-evm/tests/rex5/system_tx_replay.rs b/crates/mega-evm/tests/rex5/system_tx_replay.rs index beb2476c..9413a69c 100644 --- a/crates/mega-evm/tests/rex5/system_tx_replay.rs +++ b/crates/mega-evm/tests/rex5/system_tx_replay.rs @@ -63,6 +63,7 @@ type PocExecutor<'a> = MegaBlockExecutor< /// A chain running Rex5, matching the `CfgEnv.spec = REX5` these tests set. fn rex5_hardforks() -> MegaHardforkConfig { + // Rex6+ is excluded: this suite pins Rex5 semantics (v1.0.0 registry, REX5 spec). MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5).with_params( SequencerRegistryConfig { rex5_initial_sequencer: BOOTSTRAP_SEQUENCER, diff --git a/crates/mega-state-test/src/types/spec.rs b/crates/mega-state-test/src/types/spec.rs index fa59dbc2..4f5e672c 100644 --- a/crates/mega-state-test/src/types/spec.rs +++ b/crates/mega-state-test/src/types/spec.rs @@ -63,6 +63,10 @@ pub enum SpecName { Osaka, // SKIPPED /// `MegaETH` `MiniRex` hardfork MiniRex, + /// `MegaETH` `MiniRex1` alias spec (behavior: `Equivalence`) + MiniRex1, + /// `MegaETH` `MiniRex2` alias spec (behavior: `MiniRex`) + MiniRex2, /// `MegaETH` `Equivalence` spec (Ethereum-equivalent baseline) Equivalence, /// `MegaETH` `Rex` spec @@ -98,6 +102,8 @@ impl SpecName { pub fn to_spec_id(&self) -> Result { match self { Self::MiniRex => Ok(MegaSpecId::MINI_REX), + Self::MiniRex1 => Ok(MegaSpecId::MINI_REX_1), + Self::MiniRex2 => Ok(MegaSpecId::MINI_REX_2), Self::Rex => Ok(MegaSpecId::REX), Self::Rex1 => Ok(MegaSpecId::REX1), Self::Rex2 => Ok(MegaSpecId::REX2), @@ -119,6 +125,8 @@ impl SpecName { pub fn from_mega_spec(spec: MegaSpecId) -> Self { match spec { MegaSpecId::MINI_REX => Self::MiniRex, + MegaSpecId::MINI_REX_1 => Self::MiniRex1, + MegaSpecId::MINI_REX_2 => Self::MiniRex2, MegaSpecId::EQUIVALENCE => Self::Equivalence, MegaSpecId::REX => Self::Rex, MegaSpecId::REX1 => Self::Rex1, @@ -141,6 +149,8 @@ mod tests { fn test_to_spec_id_known_specs_succeed() { // MegaETH specs map to their own ids. assert_eq!(SpecName::MiniRex.to_spec_id(), Ok(MegaSpecId::MINI_REX)); + assert_eq!(SpecName::MiniRex1.to_spec_id(), Ok(MegaSpecId::MINI_REX_1)); + assert_eq!(SpecName::MiniRex2.to_spec_id(), Ok(MegaSpecId::MINI_REX_2)); assert_eq!(SpecName::Rex.to_spec_id(), Ok(MegaSpecId::REX)); assert_eq!(SpecName::Rex1.to_spec_id(), Ok(MegaSpecId::REX1)); assert_eq!(SpecName::Rex2.to_spec_id(), Ok(MegaSpecId::REX2)); @@ -180,6 +190,8 @@ mod tests { fn test_from_mega_spec_round_trips_known_specs() { for spec in [ MegaSpecId::MINI_REX, + MegaSpecId::MINI_REX_1, + MegaSpecId::MINI_REX_2, MegaSpecId::EQUIVALENCE, MegaSpecId::REX, MegaSpecId::REX1, @@ -194,6 +206,20 @@ mod tests { } } + #[test] + fn test_alias_spec_names_match_mega_evm_names() { + // A fixture dumped under an alias spec keys its `post` map by the + // serialized `SpecName`, and `--override.spec` parses the same string + // through `MegaSpecId`'s `FromStr` — the two name surfaces must agree. + for (spec_name, mega_name) in [ + (SpecName::MiniRex1, mega_evm::name::MINI_REX_1), + (SpecName::MiniRex2, mega_evm::name::MINI_REX_2), + ] { + let serialized = serde_json::to_string(&spec_name).expect("serialize"); + assert_eq!(serialized, format!("\"{mega_name}\"")); + } + } + #[test] fn test_from_mega_spec_maps_the_latest_spec() { // `MegaSpecId` is `#[non_exhaustive]`, so `from_mega_spec` needs a wildcard diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 0c7e0b1d..65b70acd 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -112,22 +112,20 @@ impl Cmd { self.bench_spec .as_deref() .map(|s| { + // Derived from `MegaSpecId::ALL` so the roster tracks the ladder (aliases and + // new specs included) instead of drifting as a hand-written list. let invalid_spec = || TestError { name: "spec".to_string(), path: s.to_string(), kind: TestErrorKind::FixtureError(format!( "invalid --bench-spec {s:?}; expected one of: {}", - [ - mega_evm::name::EQUIVALENCE, - mega_evm::name::MINI_REX, - mega_evm::name::REX, - mega_evm::name::REX1, - mega_evm::name::REX2, - mega_evm::name::REX3, - mega_evm::name::REX4, - mega_evm::name::REX5, - ] - .join(", ") + MegaSpecId::ALL + .iter() + .copied() + .filter(|spec| SpecName::from_mega_spec(*spec) != SpecName::Unknown) + .map(<&'static str>::from) + .collect::>() + .join(", ") )), }; let spec = MegaSpecId::from_str(s) @@ -246,20 +244,16 @@ mod tests { #[test] fn resolve_spec_accepts_every_known_spec() { - for (s, expected) in [ - (mega_evm::name::EQUIVALENCE, SpecName::Equivalence), - (mega_evm::name::MINI_REX, SpecName::MiniRex), - (mega_evm::name::REX, SpecName::Rex), - (mega_evm::name::REX1, SpecName::Rex1), - (mega_evm::name::REX2, SpecName::Rex2), - (mega_evm::name::REX3, SpecName::Rex3), - (mega_evm::name::REX4, SpecName::Rex4), - (mega_evm::name::REX5, SpecName::Rex5), - ] { - let spec = cmd_with_bench_spec(s).resolve_spec().expect("valid spec").expect("present"); - assert_eq!(spec, expected, "--bench-spec {s}"); + // Driven off `MegaSpecId::ALL`, so a newly introduced spec or alias joins here + // automatically — and a spec left without a fixture-facing `SpecName` fails loudly + // instead of being quietly absent from the roster. + for spec in MegaSpecId::ALL.iter().copied() { + let s: &str = spec.into(); + let resolved = + cmd_with_bench_spec(s).resolve_spec().expect("valid spec").expect("present"); + assert_eq!(resolved, SpecName::from_mega_spec(spec), "--bench-spec {s}"); // No accepted spec may slip through as Unknown and fail later. - assert_ne!(spec, SpecName::Unknown, "--bench-spec {s}"); + assert_ne!(resolved, SpecName::Unknown, "--bench-spec {s}"); } } diff --git a/docs/mega-evme/commands/replay.md b/docs/mega-evme/commands/replay.md index dd56ea11..b21c9a96 100644 --- a/docs/mega-evme/commands/replay.md +++ b/docs/mega-evme/commands/replay.md @@ -150,7 +150,9 @@ Hardcoded hardfork configs exist for: - **Chain 6343** — MegaETH testnet v2 - **Chain 4326** — MegaETH mainnet -For any other chain, `replay` enables all hardforks at genesis (currently equivalent to `Rex7`). +For any other chain, `replay` enables every hardfork up to a pinned spec at genesis — currently `Rex7`. +That pin does not follow the newest spec automatically: introducing a spec leaves unrecognized chains where they are, so a replay of history they already produced keeps its meaning. +Use `--override.spec` below to replay such a chain under a different spec. ### `--override.spec ` diff --git a/docs/mega-evme/commands/run.md b/docs/mega-evme/commands/run.md index 3204eb69..8fd2f1a1 100644 --- a/docs/mega-evme/commands/run.md +++ b/docs/mega-evme/commands/run.md @@ -327,7 +327,7 @@ RPC Options: Chain Options: --spec - Name of spec to use, possible values: `MiniRex`, `Equivalence`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` + Name of spec to use, possible values: `Equivalence`, `MiniRex`, `MiniRex1`, `MiniRex2`, `Rex`, `Rex1`, `Rex2`, `Rex3`, `Rex4`, `Rex5`, `Rex6`, `Rex7` (`MiniRex1`/`MiniRex2` are alias specs executing `Equivalence`/`MiniRex` behavior) [default: Rex7] diff --git a/docs/mega-evme/configuration/block-environment.md b/docs/mega-evme/configuration/block-environment.md index 3ab887a7..a1110ada 100644 --- a/docs/mega-evme/configuration/block-environment.md +++ b/docs/mega-evme/configuration/block-environment.md @@ -42,5 +42,6 @@ mega-evme run 0x41 --block.coinbase 0x1111111111111111111111111111111111111111 The block environment affects opcodes like `NUMBER`, `TIMESTAMP`, `COINBASE`, `BASEFEE`, `DIFFICULTY` / `PREVRANDAO`, and `BLOBBASEFEE`. -In MegaETH specs (MiniRex and above), accessing block environment fields triggers [gas detention](../../spec/evm/gas-detention.md) — the remaining compute gas is capped to reduce parallel execution conflicts. +From the MiniRex behavior onward, accessing block environment fields triggers [gas detention](../../spec/evm/gas-detention.md) — the remaining compute gas is capped to reduce parallel execution conflicts. +The alias spec `MiniRex1` executes `Equivalence` behavior, where this does not apply. This is normal MegaETH behavior and is reflected in `mega-evme` execution. diff --git a/docs/mega-evme/configuration/chain-and-spec.md b/docs/mega-evme/configuration/chain-and-spec.md index 6befb4d3..b3856126 100644 --- a/docs/mega-evme/configuration/chain-and-spec.md +++ b/docs/mega-evme/configuration/chain-and-spec.md @@ -23,6 +23,8 @@ Spec names are case-sensitive. | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `Equivalence` | Optimism Isthmus compatibility mode | | `MiniRex` | Initial MegaETH execution model with multidimensional gas | +| `MiniRex1` | Alias rung: executes `Equivalence` behavior (mainnet rollback window) | +| `MiniRex2` | Alias rung: executes `MiniRex` behavior (mainnet restoration) | | `Rex` | Revised storage gas economics and gas forwarding | | `Rex1` | Compute gas limit reset fix | | `Rex2` | SELFDESTRUCT restored (EIP-6780), KeylessDeploy system contract | diff --git a/docs/mega-evme/configuration/salt-buckets.md b/docs/mega-evme/configuration/salt-buckets.md index b41ae299..cdec3cd0 100644 --- a/docs/mega-evme/configuration/salt-buckets.md +++ b/docs/mega-evme/configuration/salt-buckets.md @@ -45,5 +45,5 @@ mega-evme tx \ - Without any `--bucket-capacity` flags, all buckets default to the minimum size, which means storage operations incur zero storage gas. - This option is available in all commands (`run`, `tx`, and `replay`). -- SALT-based dynamic gas pricing is active in MiniRex and later specs. - In `Equivalence` mode, bucket capacities have no effect. +- SALT-based dynamic gas pricing is active from the MiniRex behavior onward. + In `Equivalence` mode — including the alias spec `MiniRex1`, which executes `Equivalence` behavior — bucket capacities have no effect. diff --git a/docs/spec/AGENTS.md b/docs/spec/AGENTS.md index ae355e92..e31de7a5 100644 --- a/docs/spec/AGENTS.md +++ b/docs/spec/AGENTS.md @@ -161,6 +161,7 @@ A rule stated for "every X" is read as covering every X, so it must be checked a Two habits keep that honest: - **Declare the page's lower bound.** A page whose rules do not apply from Equivalence onward MUST say where they start, once, before stating them — as `evm/compute-gas.md` does with "Compute gas metering begins at MiniRex." Without it, every later sentence on the page silently claims to cover Equivalence, which runs the unmodified upstream instruction table and therefore has none of MegaETH's wrappers, storage-gas charges, or resource lanes. +- **Spec ranges are behavior ranges.** A range like "from MiniRex onward" or "MiniRex through Rex5" is read on behavior: an alias rung counts as its behavior target (`MINI_REX_1` counts as Equivalence), a convention `hardfork-spec.md` states once — do not add per-sentence alias caveats to range statements. - **Look for the counterexample before writing the quantifier.** For a claim spanning specs, resolve it through the per-spec instruction table selection; for one spanning opcodes or call schemes, check each named opcode's own wrapper. Where the implementation has an exception, name the exception in the rule rather than leaving the rule clean — an implementer following a rule mega-evm does not keep will diverge, and a halt reason or charge ordering is observable. ### Charging Lifecycle @@ -237,6 +238,10 @@ This section exists because EIP-1 makes Security Considerations a blocking requi Upgrade pages under `upgrades/` are the authoritative record of what changed at each spec. They complement concept pages: concept pages describe the current behavior, upgrade pages describe the delta. +Alias specs get no upgrade page of their own: an alias rung introduces no behavioral delta, so a dedicated page could only restate its target. +The authoritative record of an alias hardfork is its entry in `overview.md` (activation timestamps and a one-line description) plus the alias section of `hardfork-spec.md` (semantics). +Do not add per-alias pages such as `minirex1.md`. + ### Required Structure Every upgrade page MUST follow this structure: diff --git a/docs/spec/glossary.md b/docs/spec/glossary.md index c094258d..226a02b9 100644 --- a/docs/spec/glossary.md +++ b/docs/spec/glossary.md @@ -163,7 +163,14 @@ A set of MegaETH verifiable behaviors: the complete definition of what a correct Captures the execution-layer semantics that determine node correctness. -Progression: `EQUIVALENCE → MINI_REX → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7`. +Progression: `EQUIVALENCE → MINI_REX → MINI_REX_1 → MINI_REX_2 → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7`. +`MINI_REX_1` and `MINI_REX_2` are [alias rungs](#alias-spec) executing `EQUIVALENCE` and `MINI_REX` behavior respectively. + +## Alias Spec + +A spec rung whose behavior is defined to be identical to an earlier spec. +Alias specs express rollbacks while keeping the spec ladder monotone: `MINI_REX_1` (behavior: `EQUIVALENCE`) and `MINI_REX_2` (behavior: `MINI_REX`). +The rung's position governs one-way chain setup; its behavior governs execution semantics. See [Hardforks and Specs](hardfork-spec.md). @@ -173,7 +180,7 @@ A network upgrade event: when changes are activated on the chain. A hardfork may include protocol-level changes beyond MegaEVM (e.g., networking, state sync, RPC behavior). -Multiple hardforks can map to the same spec (e.g., MiniRex1 → EQUIVALENCE, MiniRex2 → MINI_REX). +Hardforks map one-to-one onto specs; a rollback hardfork schedules an [alias spec](#alias-spec) (e.g., MiniRex1 → MINI_REX_1, whose behavior is EQUIVALENCE). ## `MEGA_SYSTEM_ADDRESS` diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index ff10aee4..1fbd9623 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -15,9 +15,35 @@ The protocol distinguishes between two related concepts: - **[Hardfork](glossary.md#hardfork-megahardfork)** — A network upgrade event: _when_ changes are activated on the chain. A hardfork may include protocol-level changes beyond MegaEVM (e.g., networking, state sync, RPC behavior). - **[Spec](glossary.md#spec-megaspecid)** — A set of MegaETH verifiable behaviors: _what_ a correct node does. A spec captures the execution-layer semantics that determine node correctness. -Multiple hardforks can map to the same spec. -A hardfork can also map to an older spec. -For example: `MiniRex` → `MINI_REX`, `MiniRex1` → `EQUIVALENCE` (rollback), `MiniRex2` → `MINI_REX` (restoration). +Hardforks map one-to-one onto specs: every hardfork schedules a spec rung of its own. +A rollback is expressed by scheduling an alias spec — a rung whose behavior is identical to an earlier spec (see below). +For example: `MiniRex` → `MINI_REX`, `MiniRex1` → `MINI_REX_1` (behavior: `EQUIVALENCE`), `MiniRex2` → `MINI_REX_2` (behavior: `MINI_REX`). + +### Alias Specs: Behavior vs. Position + +Hardforks map one-to-one onto specs, so the spec resolved for a block — the latest activated hardfork's spec — only ever climbs. +A rollback is expressed by an **alias spec**: a rung of its own whose behavior is defined to be identical to an earlier spec. +`MiniRex1` maps to the alias spec `MINI_REX_1` (behavior: `EQUIVALENCE`), and `MiniRex2` maps to `MINI_REX_2` (behavior: `MINI_REX`). + +The one resolved spec is therefore read through two projections that answer different questions. + +- **Behavior** — which semantics the EVM applies in this block. + An alias spec executes exactly its target's semantics: during the `MiniRex1` window MegaEVM behaves as `EQUIVALENCE` again. +- **Position** — how far the ladder has climbed. + This is monotone and never decreases; an alias rung stands above the specs whose behavior it rolls back. + +The distinction matters for chain setup that is one-way. +System contracts predeployed under a hardfork remain deployed, and pre-block rules remain in effect, even while an alias rung rolls the executing semantics back. +A rollback changes how transactions execute; it does not un-deploy a contract or retract a system call. + +A node MUST determine pre-block setup — system-contract predeploys, their bytecode versions, and the fail-closed rules on the pre-block EIP-2935/EIP-4788 system calls — from the resolved spec's position. +A node MUST determine all other behavior — opcode behavior, gas costs, resource limits, transaction classification — from the resolved spec's behavior. + +Spec ranges elsewhere in this specification — "from MiniRex onward", "MiniRex through Rex5" — are stated on behavior. +An alias rung counts as its behavior target in such ranges: during the `MiniRex1` window, a rule stated "from MiniRex onward" does not apply, because the behavior in effect is `EQUIVALENCE`. + +A published hardfork schedule MUST climb the spec ladder rung by rung: a hardfork MUST NOT be scheduled unless every hardfork of a lower rung is scheduled, with one exception — a network MAY omit an alias hardfork, since an alias rung carries no setup of its own. +Execution is additionally robust to a malformed schedule: because setup derives from position, a scheduled hardfork implies its predecessors' setup even if they were never scheduled. This documentation covers specs — the verifiable behavioral definitions that determine correctness of a MegaETH node. Protocol-level changes outside the verifiable execution layer (e.g., networking, peer discovery) that are part of a hardfork are not covered here. @@ -25,10 +51,11 @@ Protocol-level changes outside the verifiable execution layer (e.g., networking, ## Spec Progression ``` -EQUIVALENCE → MINI_REX → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7 +EQUIVALENCE → MINI_REX → MINI_REX_1 → MINI_REX_2 → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7 ``` -Each newer spec includes all previous behaviors. +Each newer behavior-introducing spec includes all previous behaviors. +The alias rungs `MINI_REX_1` (behavior: `EQUIVALENCE`) and `MINI_REX_2` (behavior: `MINI_REX`) are the exception: an alias rung introduces no behavior of its own and instead executes exactly its target's earlier behavior (see [Alias Specs](#alias-specs-behavior-vs-position)). All specs build on Optimism Isthmus (Ethereum Prague) as the base layer. All specs through REX6 are frozen; REX7 is **unstable** and under active development. @@ -70,6 +97,15 @@ The first spec to introduce MegaETH-specific modifications: _See [MiniRex Network Upgrade](upgrades/minirex.md) for full details._ +### MINI_REX_1 and MINI_REX_2 + +Alias rungs with no behavior of their own. + +- `MINI_REX_1` (behavior: `EQUIVALENCE`) — scheduled by the `MiniRex1` hardfork; rolls execution semantics back to `EQUIVALENCE`. +- `MINI_REX_2` (behavior: `MINI_REX`) — scheduled by the `MiniRex2` hardfork; restores `MINI_REX` semantics. + +See [Alias Specs](#alias-specs-behavior-vs-position) for how behavior and position project from an alias rung. + ### REX Refines the [storage gas](glossary.md#storage-gas) economics introduced in MINI_REX: diff --git a/docs/spec/overview.md b/docs/spec/overview.md index bd84fd7e..6dbbe651 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -50,12 +50,14 @@ For the current stable behavior as a single reference, see the [MegaEVM Overview ## Spec Progression MegaETH uses a spec system to version its verifiable behavior at each stage of the protocol's evolution. -Each newer spec includes all previous behaviors: +Each newer behavior-introducing spec includes all previous behaviors: ``` -EQUIVALENCE → MINI_REX → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7 +EQUIVALENCE → MINI_REX → MINI_REX_1 → MINI_REX_2 → REX → REX1 → REX2 → REX3 → REX4 → REX5 → REX6 → REX7 ``` +`MINI_REX_1` and `MINI_REX_2` are [alias rungs](hardfork-spec.md#alias-specs-behavior-vs-position) with no behavior of their own: they execute `EQUIVALENCE` and `MINI_REX` behavior respectively, expressing a rollback while the spec ladder keeps climbing. + {% hint style="info" %} **Backward Compatibility** — EVM semantics for frozen specs are fixed. A new spec may add behavior, but it never changes what an existing frozen spec does. @@ -64,6 +66,7 @@ Contracts deployed under a given spec will continue to behave identically, regar - **EQUIVALENCE** — Baseline. Full Optimism Isthmus compatibility with block environment access tracking for parallel execution. - **MINI_REX** — Dual gas model, multidimensional resource limits, gas detention, 98/100 gas forwarding, SELFDESTRUCT disabled, Oracle and Timestamp system contracts. +- **MINI_REX_1 / MINI_REX_2** — Alias rungs with no behavior of their own; they execute EQUIVALENCE and MINI_REX behavior respectively. - **REX** — Revised storage gas economics (`base × (multiplier − 1)`), transaction intrinsic storage gas, state growth tracking, consistent CALL-like opcode behavior. - **REX1** — Fix: compute gas limit reset between transactions. - **REX2** — SELFDESTRUCT re-enabled (EIP-6780), KeylessDeploy system contract. diff --git a/docs/spec/upgrades/overview.md b/docs/spec/upgrades/overview.md index d544ca07..b79328b9 100644 --- a/docs/spec/upgrades/overview.md +++ b/docs/spec/upgrades/overview.md @@ -32,7 +32,7 @@ N/A {% endtab %} {% endtabs %} -Rollback: reverted to Equivalence spec (maps to `EQUIVALENCE`). +Rollback: schedules the alias spec `MINI_REX_1`, whose behavior is `EQUIVALENCE`. The MiniRex features were deactivated on the network; no chain reorg or state rollback occurred. Contracts deployed during MiniRex remained on-chain. @@ -47,7 +47,7 @@ N/A {% endtab %} {% endtabs %} -Restoration: re-activated MiniRex spec (maps to `MINI_REX`). +Restoration: schedules the alias spec `MINI_REX_2`, whose behavior is `MINI_REX`. All MiniRex features (dual gas model, resource limits, gas detention) were re-enabled. ### [Rex](rex.md) diff --git a/mutants/operators/spec-gate/generate.py b/mutants/operators/spec-gate/generate.py index 1ff7ab56..f6eb55f0 100755 --- a/mutants/operators/spec-gate/generate.py +++ b/mutants/operators/spec-gate/generate.py @@ -31,9 +31,14 @@ import argparse from pathlib import Path -# The full MegaSpecId progression, oldest -> newest, frozen and unstable alike -# (see crates/mega-evm/src/evm/spec.rs). Keep in spec order. This is the -# *adjacency* universe: it decides which spec sits next to which. +# The behavior-introducing MegaSpecId progression, oldest -> newest, frozen and +# unstable alike (see crates/mega-evm/src/evm/spec.rs). Keep in spec order. +# This is the *adjacency* universe: it decides which spec sits next to which. +# +# The alias rungs (MINI_REX_1, MINI_REX_2) are deliberately absent: is_enabled +# gates compare behavior projections, and an alias executes an earlier spec's +# behavior instead of introducing its own — on the behavior axis these rungs do +# not exist, so a boundary shift onto one would be a double shift in disguise. ALL_SPECS = [ "EQUIVALENCE", "MINI_REX",