From d1549979acce9868cb63a3236d9de845a20ed496 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 31 Jul 2026 16:37:45 +0800 Subject: [PATCH 01/37] refactor(block): derive pre-block setup gates from the activated-spec floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-block setup — system-contract predeploys, their bytecode versions, and the EIP-2935/EIP-4788 pre-block system calls — was gated on per-fork `is__active_at_timestamp` predicates. That reads the hardfork config as a set of independent switches when the domain is a point on a linear ladder, and it makes two shapes behave wrongly. A config that schedules a later fork without its predecessors resolves the executing spec to that fork, yet every predicate below it reports inactive, so the lower forks' setup silently never runs. And a rollback hardfork — `MiniRex1`, live on mainnet, mapping back to EQUIVALENCE — would retract setup that earlier forks already performed if setup were instead gated on the executing spec, dropping the Oracle predeploys and their read-only witness entries from every block in that window. Separate the two questions. `spec_id` stays the reversible executing spec and keeps gating EVM behavior, block limits, and transaction classification. The new `max_activated_spec_id` is the monotone activated-spec floor — the highest spec any activated fork introduced — and gates one-way chain setup. Each contract's `_spec()` builder now takes a resolved `MegaSpecId` rather than a hardfork config, so a per-fork gate cannot be reintroduced, and the executor resolves the floor once per block. `with_all_activated_through(spec)` replaces `with_all_activated().without(f)` as the way to say "a chain running spec N": removing a middle rung leaves later forks active, so both the executing spec and the floor stay at the top of the ladder. The unknown-chain fallback now names its rung instead of inheriting `MegaSpecId::default()`. These chains run their rung from genesis, so it is their semantics from block zero with no fork boundary; introducing a spec must not move them, or history they already produced replays differently through `mega-evme replay`, which resolves unknown chain IDs here. --- crates/mega-evm/src/block/AGENTS.md | 4 +- crates/mega-evm/src/block/chain.rs | 45 ++- crates/mega-evm/src/block/eips.rs | 14 +- crates/mega-evm/src/block/executor.rs | 54 ++- crates/mega-evm/src/block/hardfork.rs | 288 +++++++++++++-- crates/mega-evm/src/system/AGENTS.md | 3 +- crates/mega-evm/src/system/control.rs | 18 +- crates/mega-evm/src/system/deploy.rs | 69 +++- crates/mega-evm/src/system/keyless_deploy.rs | 24 +- crates/mega-evm/src/system/limit_control.rs | 16 +- crates/mega-evm/src/system/oracle.rs | 61 ++-- .../mega-evm/src/system/sequencer_registry.rs | 106 ++++-- crates/mega-evm/tests/block_executor/main.rs | 1 + .../tests/block_executor/partial_ladder.rs | 333 ++++++++++++++++++ crates/mega-evm/tests/rex4/deployment.rs | 13 +- docs/mega-evme/commands/replay.md | 4 +- docs/spec/hardfork-spec.md | 16 + 17 files changed, 922 insertions(+), 147 deletions(-) create mode 100644 crates/mega-evm/tests/block_executor/partial_ladder.rs diff --git a/crates/mega-evm/src/block/AGENTS.md b/crates/mega-evm/src/block/AGENTS.md index c969f6cf..e33a6efe 100644 --- a/crates/mega-evm/src/block/AGENTS.md +++ b/crates/mega-evm/src/block/AGENTS.md @@ -26,7 +26,9 @@ Block execution orchestration for MegaETH, including hardfork-to-spec resolution - 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. +- Do not gate anything on a per-fork `is__active_at_timestamp` predicate. Resolve one spec value from the block timestamp and gate on `spec.is_enabled(MegaSpecId::X)`, so behavior stays additive on a config that schedules a later fork without its predecessors. +- Pick the right one of the two resolved specs. `spec_id` is reversible (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates execution semantics: EVM behavior, block limits, the executor's spec-coherence assert, transaction classification. `max_activated_spec_id` is monotone and gates one-way chain setup: system-contract predeploys, pre-block system calls, expected installed bytecode versions. A spec rollback does not un-deploy a predeploy, so gating setup on `spec_id` would retract it for the duration of the rollback window — and with it the read-only witness entries 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 both the executing spec and the activated-spec floor stay 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 043864d7..e5b19292 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, MegaSpecId, SequencerRegistryConfig, + SequencerRegistryRex6Config, MEGA_SYSTEM_ADDRESS, }; /// `MegaETH` mainnet chain ID. @@ -81,9 +81,19 @@ pub fn testnet_hardforks() -> MegaHardforkConfig { /// seeds the smallest valid rotation delay (1 block) so local and dev chains can /// exercise rotations without friction; a real network must attach a /// governance-approved value in its published schedule when it schedules Rex6. +/// +/// 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 made when a spec is +/// sealed, and forgetting it leaves these chains where they are, which is the safe +/// direction. pub fn all_activated_hardforks() -> MegaHardforkConfig { MegaHardforkConfig::new() - .with_all_activated() + .with_all_activated_through(MegaSpecId::REX6) .with_params(SequencerRegistryConfig { rex5_initial_sequencer: MEGA_SYSTEM_ADDRESS, rex5_initial_admin: MEGA_SYSTEM_ADDRESS, @@ -135,7 +145,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 REX6. + // Unknown chain: every fork up to the pinned rung, active at genesis. assert_eq!(hardfork_schedule(1).spec_id(0), MegaSpecId::REX6); } @@ -158,4 +168,31 @@ 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. + /// + /// Advancing the rung is a deliberate edit made when a spec is sealed. This test is what + /// makes the edit deliberate: a new spec fails here until someone decides. + #[test] + fn test_unknown_chain_fallback_pins_its_rung() { + let hf = all_activated_hardforks(); + const RUNG: MegaSpecId = MegaSpecId::REX6; + + 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..675f88ed 100644 --- a/crates/mega-evm/src/block/eips.rs +++ b/crates/mega-evm/src/block/eips.rs @@ -38,7 +38,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: crate::MegaSpecId, parent_block_hash: B256, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -49,7 +50,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 +60,7 @@ where return Ok(None); } - let res = if spec.is_rex_5_active_at_timestamp(block_timestamp) { + let res = if setup_spec.is_enabled(crate::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 +99,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: crate::MegaSpecId, parent_beacon_block_root: Option, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -109,7 +111,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 +130,7 @@ where return Ok(None); } - let res = if spec.is_rex_5_active_at_timestamp(block_timestamp) { + let res = if setup_spec.is_enabled(crate::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..2dea56a4 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -26,10 +26,10 @@ 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, is_apply_pending_changes_due, resolve_system_address, + transact_apply_pending_changes, transact_deploy, BlockLimiter, BlockMegaTransactionOutcome, + BucketId, MegaBlockExecutionCtx, MegaHardforks, MegaSystemCallOutcome, MegaTransaction, + MegaTransactionExt, MegaTransactionOutcome, }; /// Block executor for the `MegaETH` chain. @@ -56,6 +56,15 @@ pub struct MegaBlockExecutor { receipt_builder: R, ctx: MegaBlockExecutionCtx, system_caller: SystemCaller, + /// The activated-spec floor for this block's timestamp, resolved once at construction. + /// + /// Every pre-block setup gate derives from this single value, which is what keeps setup + /// additive by construction. It is deliberately NOT the executing spec: see + /// [`MegaHardforks::max_activated_spec_id`]. + /// + /// Cached because the block env is fixed for an executor's lifetime — the constructor + /// already reads `block().timestamp` for its hardfork-coherence asserts. + setup_spec: crate::MegaSpecId, /// The inner evm instance. pub evm: E, @@ -131,6 +140,7 @@ where ); Self { + setup_spec: hardforks.max_activated_spec_id(block_timestamp), hardforks: hardforks.clone(), receipt_builder, receipts: Vec::new(), @@ -175,12 +185,22 @@ 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 one floor resolved at construction, so + // setup stays additive by construction: a config that schedules only a later fork still + // gets every earlier fork's predeploys and fail-closed checks. + // + // This is the activated-spec floor, NOT `spec_id(block_timestamp)`. The two differ + // whenever a patch hardfork rolls the spec back (`MiniRex1` -> `EQUIVALENCE`, live on + // mainnet), and pre-block setup is one-way: a rollback does not un-deploy a predeploy. + // Gating setup on the reversible spec would drop the Oracle predeploys — and their + // read-only witness entries — from every block in such a window. + let setup_spec = self.setup_spec; + let is_rex_5 = setup_spec.is_enabled(crate::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 +222,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 +252,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 crate::flat_system_contract_specs_for(setup_spec) { let state = transact_deploy(self.evm.db_mut(), &spec).map_err(BlockExecutionError::other)?; outcomes @@ -264,11 +285,11 @@ 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.is_enabled(crate::MegaSpecId::REX6); if !is_rex_6 { self.push_deploy_sequencer_registry_outcome( - block_timestamp, + setup_spec, block_number, ¶ms, &mut outcomes, @@ -298,7 +319,7 @@ where if is_rex_6 { self.push_deploy_sequencer_registry_outcome( - block_timestamp, + setup_spec, block_number, ¶ms, &mut outcomes, @@ -313,14 +334,14 @@ where /// and pushes its outcome. fn push_deploy_sequencer_registry_outcome( &mut self, - block_timestamp: u64, + setup_spec: crate::MegaSpecId, block_number: u64, params: &crate::SequencerRegistryConfig, outcomes: &mut Vec, ) -> Result<(), BlockExecutionError> { - let result_and_state = transact_deploy_sequencer_registry( + let result_and_state = crate::transact_deploy_sequencer_registry_for( &self.hardforks, - block_timestamp, + setup_spec, block_number, self.evm.db_mut(), params, @@ -666,9 +687,12 @@ 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. - let spec = self.evm.ctx().mega_spec(); + // The executing spec gates whether dynamic resolution applies (semantics); the + // activated-spec floor selects the expected registry bytecode version, matching what + // the floor-gated pre-block deploy installed. + let exec_spec = self.evm.ctx().mega_spec(); let (system_address, read_state) = - resolve_system_address(&self.hardforks, spec, self.evm.db_mut())?; + resolve_system_address(&self.hardforks, exec_spec, self.setup_spec, self.evm.db_mut())?; if let Some(state) = read_state { self.system_caller.on_state(StateChangeSource::Transaction(0), &state); self.evm.db_mut().commit(state); diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 34452530..a9bd4962 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -139,6 +139,40 @@ pub trait MegaHardforks: OpHardforks { self.hardfork(timestamp).map_or(MegaSpecId::EQUIVALENCE, |h| h.spec_id()) } + /// Returns the highest [`MegaSpecId`] among all [`MegaHardfork`]s activated at or before + /// `timestamp`. + /// + /// This differs from [`spec_id`](Self::spec_id) only when a patch hardfork maps to an earlier + /// spec, as `MiniRex1` does (it rolls back to `EQUIVALENCE`). The two answer different + /// questions: + /// + /// - `spec_id` — *which EVM semantics execute in this block*. Reversible: a rollback hardfork + /// moves it back down, and it must stay the gate for execution behavior and block limits. + /// - This method — *which chain-setup features have ever been activated*. Monotone: a spec + /// rollback does not un-deploy a predeploy or retract a pre-block system call, so one-way + /// setup must be gated on this instead. + /// + /// Deriving setup gates from this single value keeps them additive by construction: a config + /// that schedules only a late fork still gets every earlier fork's setup, matching the ordinal + /// inclusion the EVM layer already relies on. + /// + /// The equivalence with the per-fork `is_*_active_at_timestamp` predicates holds for + /// *spec-introducing* forks — those whose spec is strictly higher than every earlier fork's. + /// `MiniRex1` (rollback) and `MiniRex2` (restoration) introduce no new spec and are therefore + /// not recoverable from a spec ordinal; nothing gates on them. + /// + /// Like `spec_id` and [`hardfork`](Self::hardfork), this is timestamp-scoped: a `MegaHardfork` + /// registered with [`ForkCondition::Block`] or [`ForkCondition::TTD`] never reports active + /// here. Every `MegaHardfork` in the canonical schedules uses `Timestamp` or `Never`. + fn max_activated_spec_id(&self, timestamp: BlockTimestamp) -> MegaSpecId { + MegaHardfork::VARIANTS + .iter() + .filter(|fork| self.mega_fork_activation(**fork).active_at_timestamp(timestamp)) + .map(|fork| fork.spec_id()) + .max() + .unwrap_or(MegaSpecId::EQUIVALENCE) + } + /// 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) @@ -285,17 +319,35 @@ impl MegaHardforkConfig { } /// Sets all `MegaHardfork` to be activated at timestamp 0. - pub fn with_all_activated(mut self) -> Self { - self.insert(MegaHardfork::MiniRex, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::MiniRex1, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::MiniRex2, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex1, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex2, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex3, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex4, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex5, ForkCondition::Timestamp(0)); - self.insert(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); + pub fn with_all_activated(self) -> Self { + self.with_all_activated_through(MegaSpecId::default()) + } + + /// Activates every `MegaHardfork` whose spec is enabled under `spec` at timestamp 0, and + /// 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. On top of the wrong + /// executing spec, the leftover later forks also keep the activated-spec floor + /// ([`max_activated_spec_id`](MegaHardforks::max_activated_spec_id)) 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. + /// + /// Patch hardforks are included by their spec, not their position: `MiniRex1` maps back to + /// [`MegaSpecId::EQUIVALENCE`], so it is registered for every `spec`. + pub fn with_all_activated_through(mut self, spec: MegaSpecId) -> Self { + for fork in MegaHardfork::VARIANTS { + if spec.is_enabled(fork.spec_id()) { + self.insert(*fork, ForkCondition::Timestamp(0)); + } else { + self = self.without(*fork); + } + } self } @@ -456,19 +508,16 @@ mod tests { fn test_with_all_activated_enables_all_mega_hardforks() { let config = MegaHardforkConfig::default().with_all_activated(); - for hardfork in [ - MegaHardfork::MiniRex, - MegaHardfork::MiniRex1, - MegaHardfork::MiniRex2, - MegaHardfork::Rex, - MegaHardfork::Rex1, - MegaHardfork::Rex2, - MegaHardfork::Rex3, - MegaHardfork::Rex4, - MegaHardfork::Rex5, - MegaHardfork::Rex6, - ] { - assert_eq!(config.mega_fork_activation(hardfork), ForkCondition::Timestamp(0)); + // Driven off `VARIANTS`, which the `hardfork!` macro generates from the same variant list + // that declares the enum. A second hand-written list here would assert only that the + // forks someone remembered to name are activated — a new fork missing from both the + // builder and the list would fail neither. + for hardfork in MegaHardfork::VARIANTS { + assert_eq!( + config.mega_fork_activation(*hardfork), + ForkCondition::Timestamp(0), + "{hardfork:?}" + ); } } @@ -536,6 +585,197 @@ mod tests { MegaHardforkConfig::default().with_all_activated().with_params(AlwaysErrParams); } + /// Mainnet runs a real spec rollback: `MiniRex1` maps to `EQUIVALENCE` while `MiniRex`'s + /// timestamp has already passed. Inside that window the resolved spec and the activated-spec + /// floor disagree, and only the floor keeps one-way setup (the Oracle predeploys) enabled. + #[test] + fn test_mainnet_rollback_window_separates_resolved_spec_from_floor() { + 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] { + assert_eq!(hf.spec_id(ts), MegaSpecId::EQUIVALENCE, "executing spec rolls back"); + assert_eq!(hf.max_activated_spec_id(ts), MegaSpecId::MINI_REX, "floor stays monotone"); + // Gating setup on the executing spec would drop the MiniRex predeploys here. + assert!(!hf.spec_id(ts).is_enabled(MegaSpecId::MINI_REX)); + assert!(hf.max_activated_spec_id(ts).is_enabled(MegaSpecId::MINI_REX)); + assert!(hf.is_mini_rex_active_at_timestamp(ts)); + } + } + + /// The floor reproduces every per-fork activation predicate exactly, for every + /// *spec-introducing* fork, on every canonical schedule. This is what makes the switch a + /// no-op on well-formed ladders. + /// + /// Forks that introduce no new spec — `MiniRex1` (rollback to `EQUIVALENCE`) and `MiniRex2` + /// (restoration to `MINI_REX`) — are not recoverable from a spec ordinal by construction, so + /// nothing may gate on them. The predicate is derived rather than hardcoded so a future + /// rollback fork is classified automatically. + #[test] + fn test_floor_matches_per_fork_activation_for_spec_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 floor = hf.max_activated_spec_id(ts); + for (i, fork) in MegaHardfork::VARIANTS.iter().enumerate() { + let introduces_spec = + MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); + if !introduces_spec { + continue; + } + assert_eq!( + floor.is_enabled(fork.spec_id()), + hf.mega_fork_activation(*fork).active_at_timestamp(ts), + "floor disagrees with per-fork activation for {fork:?} at ts={ts}" + ); + } + } + } + } + + /// The gap this change closes: a config that schedules a later fork without its predecessor + /// resolves to that fork's spec, yet every per-fork predicate below it reports inactive. + /// The floor makes the lower gates additive again. + #[test] + fn test_partial_ladder_floor_enables_unscheduled_predecessors() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::Rex5, ForkCondition::Never) + .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); + + assert!(!hf.is_rex_5_active_at_timestamp(0), "Rex5 is not scheduled"); + assert!(!hf.is_mini_rex_active_at_timestamp(0), "MiniRex is not scheduled"); + assert_eq!(hf.spec_id(0), MegaSpecId::REX6); + + let floor = hf.max_activated_spec_id(0); + assert_eq!(floor, MegaSpecId::REX6); + for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX2, MegaSpecId::REX4, MegaSpecId::REX5] { + assert!(floor.is_enabled(spec), "floor must enable {spec:?} on a partial ladder"); + } + } + + /// `with_all_activated_through` is the well-formed way to express "a chain running spec N": + /// both the executing spec and the activated-spec floor resolve to exactly `N`, at any + /// timestamp. Driven off `MegaSpecId`'s own progression rather than a second hand-written + /// list, so a newly introduced spec fails here once instead of silently widening every + /// "chain running spec N" config in the suite. + #[test] + fn test_with_all_activated_through_resolves_to_that_spec() { + for spec in [ + MegaSpecId::EQUIVALENCE, + MegaSpecId::MINI_REX, + MegaSpecId::REX, + MegaSpecId::REX1, + MegaSpecId::REX2, + MegaSpecId::REX3, + MegaSpecId::REX4, + MegaSpecId::REX5, + MegaSpecId::REX6, + ] { + 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"); + assert_eq!(config.max_activated_spec_id(0), spec, "{spec:?} floor at genesis"); + assert_eq!( + config.max_activated_spec_id(u64::MAX), + spec, + "{spec:?} floor 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"); + assert_eq!( + downgraded.max_activated_spec_id(u64::MAX), + spec, + "{spec:?} floor from an activated config" + ); + } + } + + /// Removing a middle rung does NOT express "a chain running spec N". It is the partial-ladder + /// shape: the executing spec follows the newest fork still registered, and the floor keeps + /// every lower setup 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!(!partial.is_rex_4_active_at_timestamp(0), "Rex4 itself is unregistered"); + assert_ne!(partial.spec_id(0), MegaSpecId::REX4, "the executing spec is not lowered"); + assert!( + partial.max_activated_spec_id(0).is_enabled(MegaSpecId::REX4), + "the floor still enables the removed fork's spec" + ); + } + + /// The floor and the executing spec agree across the Rex5/Rex6 boundary on every canonical + /// schedule, which is what makes the two-spec split in `resolve_system_address` inert today. + /// Adding a hardfork that maps below `REX5` would break this and must be caught here. + #[test] + fn test_floor_and_executing_spec_agree_across_rex5_rex6_boundary() { + 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 (exec, floor) = (hf.spec_id(ts), hf.max_activated_spec_id(ts)); + for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + assert_eq!( + exec.is_enabled(spec), + floor.is_enabled(spec), + "executing spec and floor disagree on {spec:?} at ts={ts}" + ); + } + } + } + } + + /// Documented domain limit: the floor is timestamp-scoped, so a `MegaHardfork` registered by + /// block number never contributes to it and the equivalence with the per-fork predicates does + /// not hold. `spec_id`/`hardfork` share this limitation; every canonical schedule uses + /// `Timestamp` or `Never`. + #[test] + fn test_floor_ignores_block_numbered_forks() { + let hf = MegaHardforkConfig::new() + .with(MegaHardfork::MiniRex, ForkCondition::Block(0)) + .with(MegaHardfork::Rex, ForkCondition::Timestamp(0)); + + assert!(!hf.is_mini_rex_active_at_timestamp(0), "block-numbered forks are not timestamped"); + assert_eq!(hf.max_activated_spec_id(0), MegaSpecId::REX); + assert!(hf.max_activated_spec_id(0).is_enabled(MegaSpecId::MINI_REX)); + } + #[test] fn test_hardfork_and_spec_id_follow_latest_active_timestamp() { let config = MegaHardforkConfig::default() diff --git a/crates/mega-evm/src/system/AGENTS.md b/crates/mega-evm/src/system/AGENTS.md index 01740b90..7fa53cc9 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 a resolved `MegaSpecId` and gates on `spec.is_enabled(...)`. The builders deliberately do not take a hardfork config, 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 the spec themselves. +- The spec passed to a builder is the **activated-spec floor** (`MegaHardforks::max_activated_spec_id`), not the executing spec. Predeploys are one-way: a hardfork that rolls the spec back does not un-deploy them, and it must not 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..4479d274 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.max_activated_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 activated-spec floor — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn access_control_spec(spec: MegaSpecId) -> Option { + spec.is_enabled(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..7e7e4fa1 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,23 @@ pub fn flat_system_contract_specs( hardforks: impl MegaHardforks, block_timestamp: u64, ) -> Vec { + flat_system_contract_specs_for(hardforks.max_activated_spec_id(block_timestamp)) +} + +/// [`flat_system_contract_specs`] against an already-resolved activated-spec floor. +/// +/// The block executor resolves the floor once per block +/// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)) 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 +174,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 +318,49 @@ 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"); + }; + + assert_eq!(hf.spec_id(ts), crate::MegaSpecId::EQUIVALENCE); + assert!( + flat_system_contract_specs_for(hf.spec_id(ts)).is_empty(), + "executing spec would drop both predeploys in the rollback window" + ); + + 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!(!hf.is_mini_rex_active_at_timestamp(0), "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..d5e35615 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.max_activated_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 activated-spec floor — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn keyless_deploy_spec(spec: MegaSpecId) -> Option { + spec.is_enabled(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..24094601 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.max_activated_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 activated-spec floor — see +/// [`oracle_spec`](crate::system::oracle::oracle_spec). +pub(crate) fn limit_control_spec(spec: MegaSpecId) -> Option { + spec.is_enabled(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..94b51ffd 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"); @@ -45,31 +45,34 @@ pub fn transact_deploy_oracle_contract( 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.max_activated_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 activated-spec floor +/// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)), not the executing +/// spec: an Oracle already installed under `MINI_REX` stays installed through a spec rollback. +pub(crate) fn oracle_spec(spec: MegaSpecId) -> Option { + if !spec.is_enabled(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.is_enabled(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.is_enabled(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.max_activated_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 activated-spec floor — see [`oracle_spec`]. +pub(crate) fn high_precision_timestamp_oracle_spec(spec: MegaSpecId) -> Option { + spec.is_enabled(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 196f879d..5a8b307c 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -192,7 +192,26 @@ 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.max_activated_spec_id(block_timestamp); + transact_deploy_sequencer_registry_for(hardforks, spec, current_block_number, db, config) +} + +/// [`transact_deploy_sequencer_registry`] against an already-resolved activated-spec floor. +/// +/// The block executor resolves the floor once per block and calls this directly; the public +/// wrapper above resolves it for callers that hold a hardfork config. `hardforks` is still +/// needed for the Rex6 parameter lookup, which is per-fork rather than per-spec. +pub(crate) fn transact_deploy_sequencer_registry_for( + hardforks: impl MegaHardforks, + spec: crate::MegaSpecId, + current_block_number: u64, + db: &mut State, + config: &SequencerRegistryConfig, +) -> Result, BlockExecutionError> { + // Gate and bytecode selection follow the activated-spec floor, 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.is_enabled(crate::MegaSpecId::REX5) { return Ok(None); } @@ -206,7 +225,7 @@ 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.is_enabled(crate::MegaSpecId::REX6); let (target_code, target_code_hash) = if rex6 { (SEQUENCER_REGISTRY_CODE_REX6, SEQUENCER_REGISTRY_CODE_HASH_REX6) } else { @@ -390,14 +409,28 @@ where /// - Pre-REX5: returns `(MEGA_SYSTEM_ADDRESS, None)`. /// - REX5: reads `_currentSystemAddress` from committed registry storage. /// +/// The two spec arguments answer different questions and must not be collapsed into one: +/// +/// - `exec_spec` — the executing spec, gating whether dynamic system-address resolution applies at +/// all. This is execution semantics: it decides how a transaction is classified, so a spec +/// rollback below REX5 must return [`MEGA_SYSTEM_ADDRESS`] again. +/// - `setup_spec` — the activated-spec floor, selecting which registry bytecode version is expected +/// to be installed. This must match what [`transact_deploy_sequencer_registry`] installed, which +/// is floor-gated because a deployed contract is not un-deployed by a rollback. +/// +/// The two agree on every canonical schedule (no hardfork maps below REX5), so this split is +/// currently inert; it exists so that adding a rollback hardfork cannot silently change +/// transaction classification or produce a spurious code-hash mismatch. +/// /// 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( hardforks: impl MegaHardforks, - spec: crate::MegaSpecId, + exec_spec: crate::MegaSpecId, + setup_spec: crate::MegaSpecId, db: &mut State, ) -> Result<(Address, Option), BlockExecutionError> { - if !spec.is_enabled(crate::MegaSpecId::REX5) { + if !exec_spec.is_enabled(crate::MegaSpecId::REX5) { return Ok((MEGA_SYSTEM_ADDRESS, None)); } @@ -420,9 +453,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 activated-spec floor, 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 setup_spec.is_enabled(crate::MegaSpecId::REX6) { SEQUENCER_REGISTRY_CODE_HASH_REX6 } else { SEQUENCER_REGISTRY_CODE_HASH @@ -960,8 +994,13 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, witness) = - resolve_system_address(&rex6_hardforks(), MegaSpecId::REX6, &mut state).unwrap(); + let (addr, witness) = resolve_system_address( + &rex6_hardforks(), + MegaSpecId::REX6, + MegaSpecId::REX6, + &mut state, + ) + .unwrap(); assert_eq!(addr, TEST_SYSTEM_ADDRESS); assert!(witness.is_some()); } @@ -987,8 +1026,13 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let err = resolve_system_address(&rex6_hardforks(), MegaSpecId::REX6, &mut state) - .expect_err("V1 code hash at REX6 must fail closed"); + let err = resolve_system_address( + &rex6_hardforks(), + MegaSpecId::REX6, + MegaSpecId::REX6, + &mut state, + ) + .expect_err("V1 code hash at REX6 must fail closed"); assert!(err.to_string().contains("code hash mismatch")); } @@ -997,9 +1041,13 @@ mod tests { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, _) = - resolve_system_address(MegaHardforkConfig::default(), MegaSpecId::REX4, &mut state) - .unwrap(); + let (addr, _) = resolve_system_address( + MegaHardforkConfig::default(), + MegaSpecId::REX4, + MegaSpecId::REX4, + &mut state, + ) + .unwrap(); assert_eq!(addr, MEGA_SYSTEM_ADDRESS); } @@ -1022,8 +1070,13 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, witness) = - resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state).unwrap(); + let (addr, witness) = resolve_system_address( + &rex5_hardforks(), + MegaSpecId::REX5, + MegaSpecId::REX5, + &mut state, + ) + .unwrap(); assert_eq!(addr, TEST_SYSTEM_ADDRESS); // Witness must capture the registry account and the CURRENT_SYSTEM_ADDRESS slot. @@ -1053,7 +1106,12 @@ mod tests { ); let mut state = State::builder().with_database(&mut db).build(); - let result = resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state); + let result = resolve_system_address( + &rex5_hardforks(), + MegaSpecId::REX5, + MegaSpecId::REX5, + &mut state, + ); assert!(result.is_err(), "zero _currentSystemAddress should be an error"); } @@ -1063,8 +1121,9 @@ mod tests { let mut state = State::builder().with_database(&mut db).build(); let hardforks = rex5_hardforks(); - let err = resolve_system_address(&hardforks, MegaSpecId::REX5, &mut state) - .expect_err("missing registry at Rex5 must fail closed"); + let err = + resolve_system_address(&hardforks, MegaSpecId::REX5, MegaSpecId::REX5, &mut state) + .expect_err("missing registry at Rex5 must fail closed"); assert!(err.to_string().contains("does not exist")); } @@ -1081,8 +1140,13 @@ mod tests { ); let mut state = State::builder().with_database(&mut db).build(); - let err = resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state) - .expect_err("wrong code hash must fail closed"); + let err = resolve_system_address( + &rex5_hardforks(), + MegaSpecId::REX5, + MegaSpecId::REX5, + &mut state, + ) + .expect_err("wrong code hash must fail closed"); assert!(err.to_string().contains("code hash mismatch")); } 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..a0d6cb0b --- /dev/null +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -0,0 +1,333 @@ +//! 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 **activated-spec floor** +//! (`MegaHardforks::max_activated_spec_id`), not on per-fork registration and not on the +//! reversible executing spec. 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, + 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, and the executing spec is Rex6. + assert!(!chain_spec.is_rex_5_active_at_timestamp(0)); + assert!(!chain_spec.is_mini_rex_active_at_timestamp(0)); + 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 floor 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:?}" + ); +} + +/// 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 executing spec really has rolled back. + assert_eq!(chain_spec.spec_id(timestamp), 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::EQUIVALENCE, 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/rex4/deployment.rs b/crates/mega-evm/tests/rex4/deployment.rs index 803931aa..dc983d48 100644 --- a/crates/mega-evm/tests/rex4/deployment.rs +++ b/crates/mega-evm/tests/rex4/deployment.rs @@ -5,7 +5,7 @@ use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{Address, Bytes, B256}; use mega_evm::{ test_utils::MemoryDatabase, BlockLimits, EmptyExternalEnv, MegaBlockExecutionCtx, - MegaBlockExecutor, MegaEvm, MegaEvmFactory, MegaHardfork, MegaHardforkConfig, MegaSpecId, + MegaBlockExecutor, MegaEvm, MegaEvmFactory, MegaHardforkConfig, MegaSpecId, ACCESS_CONTROL_ADDRESS, ACCESS_CONTROL_CODE, ACCESS_CONTROL_CODE_HASH, LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE, LIMIT_CONTROL_CODE_HASH, }; @@ -21,15 +21,16 @@ 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 derive from the activated-spec floor — which would still report REX6 and open every +// lower gate. fn rex4_chain_spec() -> MegaHardforkConfig { - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex5) + MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX4) } fn rex3_chain_spec() -> MegaHardforkConfig { - MegaHardforkConfig::default() - .with_all_activated() - .without(MegaHardfork::Rex4) - .without(MegaHardfork::Rex5) + MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX3) } fn make_block_env(timestamp: u64) -> BlockEnv { diff --git a/docs/mega-evme/commands/replay.md b/docs/mega-evme/commands/replay.md index 260f450b..144c318c 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 `Rex6`). +For any other chain, `replay` enables every hardfork up to a pinned spec at genesis — currently `Rex6`. +That pin does not follow the newest spec: 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/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index 190ff056..551abc02 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -19,6 +19,22 @@ 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). +### Executing Spec vs. Highest Spec Reached + +Because a hardfork may roll the spec back, two distinct questions have to be answered separately at each block. + +- **Executing spec** — which semantics the EVM applies in this block: the spec of the most recently activated hardfork. + This is reversible: during the `MiniRex1` window the executing spec is `EQUIVALENCE` again, and MegaEVM behaves accordingly. +- **Highest spec reached** — the greatest spec among all hardforks activated at or before this block. + This is monotone and never decreases, even across a rollback. + +The distinction matters for chain setup that is one-way. +System contracts predeployed under a hardfork remain deployed, and pre-block system calls remain in effect, even while a later hardfork rolls the executing semantics back to an earlier spec. +A rollback changes how transactions execute; it does not un-deploy a contract or retract a system call. + +Pre-block setup — system-contract predeploys, their bytecode versions, and the pre-block EIP-2935/EIP-4788 system calls — is therefore determined by the highest spec reached. +Everything else — opcode behavior, gas costs, resource limits, transaction classification — is determined by the executing spec. + 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. From c3614ef6c79ce4358e6d5628c6e0db4d0997fe9e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 31 Jul 2026 16:46:21 +0800 Subject: [PATCH 02/37] refactor(system): state the Rex5 registry test chains as a rung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six configs still wrote "a chain running Rex5" as `with_all_activated().without(Rex6)` — the shape this change's own guidance now forbids, and the one that silently widens: the next spec leaves the removed fork's successor registered, so the config resolves above the rung its comment claims. --- crates/mega-evm/src/system/sequencer_registry.rs | 15 +++++---------- crates/mega-evm/tests/rex5/system_tx_replay.rs | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 5a8b307c..6f110a8a 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -527,8 +527,7 @@ mod tests { /// registry runs v1.0.0. fn rex5_hardforks() -> MegaHardforkConfig { MegaHardforkConfig::default() - .with_all_activated() - .without(MegaHardfork::Rex6) + .with_all_activated_through(MegaSpecId::REX5) .with_params(test_config()) } @@ -619,8 +618,7 @@ mod tests { fn test_deploy_seeds_storage() { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex6); + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5); let result = transact_deploy_sequencer_registry(&hardforks, 0, 1000, &mut state, &test_config()) @@ -668,8 +666,7 @@ mod tests { }, ); let mut state = State::builder().with_database(&mut db).build(); - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex6); + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5); let result = transact_deploy_sequencer_registry(&hardforks, 0, 2000, &mut state, &test_config()) @@ -694,8 +691,7 @@ mod tests { }, ); let mut state = State::builder().with_database(&mut db).build(); - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex6); + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5); let err = transact_deploy_sequencer_registry(&hardforks, 0, 2000, &mut state, &test_config()) @@ -718,8 +714,7 @@ mod tests { }, ); let mut state = State::builder().with_database(&mut db).build(); - let hardforks = - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex6); + let hardforks = MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5); let result = transact_deploy_sequencer_registry(&hardforks, 0, 1000, &mut state, &test_config()) diff --git a/crates/mega-evm/tests/rex5/system_tx_replay.rs b/crates/mega-evm/tests/rex5/system_tx_replay.rs index 183e119d..0c105de3 100644 --- a/crates/mega-evm/tests/rex5/system_tx_replay.rs +++ b/crates/mega-evm/tests/rex5/system_tx_replay.rs @@ -63,7 +63,7 @@ type PocExecutor<'a> = MegaBlockExecutor< fn rex5_hardforks() -> MegaHardforkConfig { // Rex6 is excluded: this suite pins Rex5 semantics (v1.0.0 registry, REX5 spec). - MegaHardforkConfig::default().with_all_activated().without(MegaHardfork::Rex6).with_params( + MegaHardforkConfig::default().with_all_activated_through(MegaSpecId::REX5).with_params( SequencerRegistryConfig { rex5_initial_sequencer: BOOTSTRAP_SEQUENCER, rex5_initial_admin: BOOTSTRAP_ADMIN, From 696cc3668484dd05b67618fbd71f47fc2cdcaeb3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 31 Jul 2026 16:55:44 +0800 Subject: [PATCH 03/37] refactor(hardfork): make `without` crate-private MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every external use was `with_all_activated().without(fork)` meaning "a chain running the spec below `fork`", which it does not express: the forks above `fork` stay registered, so the config resolves above the intended rung and climbs again with the next spec. `with_all_activated_through` says it. This narrows an idiom, not a capability — `with(fork, ForkCondition::Never)` still unregisters a fork and must stay public, since the canonical testnet schedule uses it for `MiniRex1` / `MiniRex2`. Writing a gap that way is at least visibly deliberate. --- AGENTS.md | 2 ++ crates/mega-evm/src/block/hardfork.rs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f3661267..51081471 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,6 +78,8 @@ 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. + Two resolved specs come out of a config and must not be confused: `spec_id` is the reversible executing spec (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates EVM behavior; `max_activated_spec_id` is the monotone activated-spec floor and gates one-way chain setup such as predeploys and pre-block system calls. - **`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. diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index a9bd4962..24f32c08 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -376,7 +376,19 @@ 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 not public. Every external use it had was + /// `with_all_activated().without(fork)` meaning "a chain running the spec below `fork`", which + /// it does not express — 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. + pub(crate) fn without(mut self, hardfork: MegaHardfork) -> Self { self.entries.retain(|e| e.fork.name() != hardfork.name()); self } From a460fc17e621d783f54494d7294e6b9a2fde582e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 31 Jul 2026 18:01:15 +0800 Subject: [PATCH 04/37] refactor(hardfork): single-source spec gating with floor-projected predicates Spec-introducing per-fork predicates now project the activated-spec floor (cascading, kona-style), so gating on them stays additive on partial ladders; patch forks keep raw event semantics. Hardfork resolution derives from the variant ladder instead of a hand-written chain, the floor scan early-exits at the first activated spec-introducing fork, and validate_schedule() rejects malformed schedules (skipped rungs, ordering, missing params) at load time. The deploy layer no longer sees a hardfork config, and resolve_system_address asserts the floor-above-exec invariant. Costs are pinned by a hardfork_resolution benchmark group. --- AGENTS.md | 2 + crates/mega-evm/benches/block_bench.rs | 30 ++ crates/mega-evm/src/block/AGENTS.md | 3 +- crates/mega-evm/src/block/chain.rs | 17 +- crates/mega-evm/src/block/eips.rs | 9 +- crates/mega-evm/src/block/executor.rs | 34 +- crates/mega-evm/src/block/hardfork.rs | 460 ++++++++++++++---- crates/mega-evm/src/system/AGENTS.md | 2 +- crates/mega-evm/src/system/deploy.rs | 6 +- crates/mega-evm/src/system/oracle.rs | 5 +- .../mega-evm/src/system/sequencer_registry.rs | 32 +- .../tests/block_executor/partial_ladder.rs | 8 +- crates/mega-evm/tests/mutation/block.rs | 47 +- .../tests/rex5/pre_block_system_calls.rs | 6 +- docs/spec/hardfork-spec.md | 5 +- 15 files changed, 526 insertions(+), 140 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 51081471..305072e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,7 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R `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. Two resolved specs come out of a config and must not be confused: `spec_id` is the reversible executing spec (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates EVM behavior; `max_activated_spec_id` is the monotone activated-spec floor and gates one-way chain setup such as predeploys and pre-block system calls. + The `is__active_at_timestamp` predicates for spec-introducing forks are projections of that floor (raw activation events are answered by `mega_fork_activation`), 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. @@ -313,6 +314,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/crates/mega-evm/benches/block_bench.rs b/crates/mega-evm/benches/block_bench.rs index 674bbf19..76277c9d 100644 --- a/crates/mega-evm/benches/block_bench.rs +++ b/crates/mega-evm/benches/block_bench.rs @@ -446,6 +446,35 @@ fn bench_rex5_pre_block(c: &mut Criterion) { group.finish(); } +/// Benchmark hardfork-config resolution on the real mainnet schedule. +/// +/// The floor-projected predicates (`is_rex_5_active_at_timestamp`) and +/// `max_activated_spec_id` 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. +/// The floor'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("max_activated_spec_id", |b| { + b.iter(|| black_box(schedule.max_activated_spec_id(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 +482,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/block/AGENTS.md b/crates/mega-evm/src/block/AGENTS.md index e33a6efe..7215d66e 100644 --- a/crates/mega-evm/src/block/AGENTS.md +++ b/crates/mega-evm/src/block/AGENTS.md @@ -26,7 +26,8 @@ Block execution orchestration for MegaETH, including hardfork-to-spec resolution - 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. -- Do not gate anything on a per-fork `is__active_at_timestamp` predicate. Resolve one spec value from the block timestamp and gate on `spec.is_enabled(MegaSpecId::X)`, so behavior stays additive on a config that schedules a later fork without its predecessors. +- Gate on a resolved spec value (`spec.is_enabled(MegaSpecId::X)`), resolved once from the block timestamp. The `is__active_at_timestamp` predicates for spec-introducing forks are projections of the activated-spec floor — gating on them is therefore additive-by-construction too, but they cannot express the executing spec, and only `mega_fork_activation` answers whether a fork's activation event itself was scheduled (the patch-fork predicates `is_mini_rex_1/2_active_at_timestamp` stay event queries for exactly that reason). +- 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 — the floor keeps setup 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`. - Pick the right one of the two resolved specs. `spec_id` is reversible (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates execution semantics: EVM behavior, block limits, the executor's spec-coherence assert, transaction classification. `max_activated_spec_id` is monotone and gates one-way chain setup: system-contract predeploys, pre-block system calls, expected installed bytecode versions. A spec rollback does not un-deploy a predeploy, so gating setup on `spec_id` would retract it for the duration of the rollback window — and with it the read-only witness entries 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 both the executing spec and the activated-spec floor stay at the top of the ladder. Use `with_all_activated_through(MegaSpecId::N)`. - Do not hardcode gas-limit assumptions outside `BlockLimits` plumbing. diff --git a/crates/mega-evm/src/block/chain.rs b/crates/mega-evm/src/block/chain.rs index e5b19292..1fa8df32 100644 --- a/crates/mega-evm/src/block/chain.rs +++ b/crates/mega-evm/src/block/chain.rs @@ -12,7 +12,7 @@ use alloy_hardforks::ForkCondition; use alloy_primitives::address; use crate::{ - MegaHardfork, MegaHardforkConfig, MegaSpecId, SequencerRegistryConfig, + MegaHardfork, MegaHardforkConfig, MegaHardforks, MegaSpecId, SequencerRegistryConfig, SequencerRegistryRex6Config, MEGA_SYSTEM_ADDRESS, }; @@ -106,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)] @@ -176,8 +180,11 @@ mod tests { /// 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. /// - /// Advancing the rung is a deliberate edit made when a spec is sealed. This test is what - /// makes the edit deliberate: a new spec fails here until someone decides. + /// 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(); diff --git a/crates/mega-evm/src/block/eips.rs b/crates/mega-evm/src/block/eips.rs index 675f88ed..41468b18 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, @@ -39,7 +40,7 @@ use crate::{ #[inline] pub(crate) fn transact_blockhashes_contract_call( hardforks: H, - setup_spec: crate::MegaSpecId, + setup_spec: MegaSpecId, parent_block_hash: B256, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -60,7 +61,7 @@ where return Ok(None); } - let res = if setup_spec.is_enabled(crate::MegaSpecId::REX5) { + let res = if setup_spec.is_enabled(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( @@ -100,7 +101,7 @@ where #[inline] pub(crate) fn transact_beacon_root_contract_call( hardforks: H, - setup_spec: crate::MegaSpecId, + setup_spec: MegaSpecId, parent_beacon_block_root: Option, evm: &mut MegaEvm, ) -> Result>, BlockExecutionError> @@ -130,7 +131,7 @@ where return Ok(None); } - let res = if setup_spec.is_enabled(crate::MegaSpecId::REX5) { + let res = if setup_spec.is_enabled(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 2dea56a4..db229a72 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, is_apply_pending_changes_due, resolve_system_address, - transact_apply_pending_changes, transact_deploy, 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. @@ -64,7 +66,7 @@ pub struct MegaBlockExecutor { /// /// Cached because the block env is fixed for an executor's lifetime — the constructor /// already reads `block().timestamp` for its hardfork-coherence asserts. - setup_spec: crate::MegaSpecId, + setup_spec: MegaSpecId, /// The inner evm instance. pub evm: E, @@ -195,7 +197,7 @@ where // Gating setup on the reversible spec would drop the Oracle predeploys — and their // read-only witness entries — from every block in such a window. let setup_spec = self.setup_spec; - let is_rex_5 = setup_spec.is_enabled(crate::MegaSpecId::REX5); + let is_rex_5 = setup_spec.is_enabled(MegaSpecId::REX5); // EIP-2935 let result_and_state = eips::transact_blockhashes_contract_call( @@ -252,7 +254,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 crate::flat_system_contract_specs_for(setup_spec) { + for spec in flat_system_contract_specs_for(setup_spec) { let state = transact_deploy(self.evm.db_mut(), &spec).map_err(BlockExecutionError::other)?; outcomes @@ -265,14 +267,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 @@ -285,11 +288,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 = setup_spec.is_enabled(crate::MegaSpecId::REX6); + let is_rex_6 = setup_spec.is_enabled(MegaSpecId::REX6); if !is_rex_6 { self.push_deploy_sequencer_registry_outcome( setup_spec, + rex6_params.as_ref(), block_number, ¶ms, &mut outcomes, @@ -320,6 +324,7 @@ where if is_rex_6 { self.push_deploy_sequencer_registry_outcome( setup_spec, + rex6_params.as_ref(), block_number, ¶ms, &mut outcomes, @@ -334,14 +339,15 @@ where /// and pushes its outcome. fn push_deploy_sequencer_registry_outcome( &mut self, - setup_spec: crate::MegaSpecId, + setup_spec: MegaSpecId, + rex6_params: Option<&SequencerRegistryRex6Config>, block_number: u64, - params: &crate::SequencerRegistryConfig, + params: &SequencerRegistryConfig, outcomes: &mut Vec, ) -> Result<(), BlockExecutionError> { - let result_and_state = crate::transact_deploy_sequencer_registry_for( - &self.hardforks, + let result_and_state = transact_deploy_sequencer_registry_for( setup_spec, + rex6_params, block_number, self.evm.db_mut(), params, diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 24f32c08..a89f9f44 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 @@ -87,6 +87,25 @@ pub trait HardforkParams: Any + core::fmt::Debug + Send + Sync { } /// Extends [`OpHardforks`] with `MegaETH` helper methods. +/// +/// Everything derives from one source of truth — the raw activation events reported by +/// [`mega_fork_activation`](Self::mega_fork_activation) — in three layers: +/// +/// - [`hardfork`](Self::hardfork) / [`spec_id`](Self::spec_id) — the *executing* resolution: the +/// latest-declared activated fork and its spec. Reversible: a rollback patch fork moves the +/// executing spec back down. +/// - [`max_activated_spec_id`](Self::max_activated_spec_id) — the *activated-spec floor*: the +/// highest spec any activated fork introduced. Monotone across rollbacks. +/// - The `is__active_at_timestamp` convenience predicates. For **spec-introducing** forks +/// these are projections of the floor: they answer "has the chain reached this fork's spec", not +/// "was this fork itself scheduled", so gating on them stays additive on a schedule that omits a +/// predecessor and monotone across spec rollbacks. For **patch** forks (`MiniRex1`, `MiniRex2`), +/// which introduce no new spec and are not recoverable from a spec ordinal, the predicates remain +/// raw event queries. +/// +/// 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 @@ -108,30 +127,22 @@ pub trait MegaHardforks: OpHardforks { } /// Returns the current `MegaHardfork` active at the given timestamp. + /// + /// 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. + /// + /// This is the *executing* resolution: it reads raw activation events + /// ([`mega_fork_activation`](Self::mega_fork_activation)), not the activated-spec floor, so a + /// rollback patch fork like `MiniRex1` correctly takes over from the fork it patches. fn hardfork(&self, timestamp: u64) -> Option { - 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 - } + 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. @@ -156,10 +167,11 @@ pub trait MegaHardforks: OpHardforks { /// that schedules only a late fork still gets every earlier fork's setup, matching the ordinal /// inclusion the EVM layer already relies on. /// - /// The equivalence with the per-fork `is_*_active_at_timestamp` predicates holds for - /// *spec-introducing* forks — those whose spec is strictly higher than every earlier fork's. + /// For *spec-introducing* forks — those whose spec is strictly higher than every earlier + /// fork's — the `is_*_active_at_timestamp` predicates are projections of this floor, and on + /// well-formed ladders the floor coincides with each such fork's raw activation event. /// `MiniRex1` (rollback) and `MiniRex2` (restoration) introduce no new spec and are therefore - /// not recoverable from a spec ordinal; nothing gates on them. + /// not recoverable from a spec ordinal; their predicates stay raw event queries. /// /// Like `spec_id` and [`hardfork`](Self::hardfork), this is timestamp-scoped: a `MegaHardfork` /// registered with [`ForkCondition::Block`] or [`ForkCondition::TTD`] never reports active @@ -173,57 +185,210 @@ pub trait MegaHardforks: OpHardforks { .unwrap_or(MegaSpecId::EQUIVALENCE) } - /// Returns `true` if [`MegaHardfork::MiniRex`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::MINI_REX`], the + /// spec introduced by [`MegaHardfork::MiniRex`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::MiniRex).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::MINI_REX) } - /// Returns `true` if [`MegaHardfork::MiniRex1`] is active at given block timestamp. + /// Returns `true` if the [`MegaHardfork::MiniRex1`] activation event has occurred at the + /// given block timestamp. + /// + /// `MiniRex1` is a patch hardfork: it introduces no new spec (it rolls the executing spec + /// back to [`MegaSpecId::EQUIVALENCE`]), so it is not recoverable from a spec ordinal and + /// this predicate stays a raw event query, unlike the floor-projected spec-introducing + /// predicates. fn is_mini_rex_1_active_at_timestamp(&self, timestamp: u64) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex1).active_at_timestamp(timestamp) } - /// Returns `true` if [`MegaHardfork::MiniRex2`] is active at given block timestamp. + /// Returns `true` if the [`MegaHardfork::MiniRex2`] activation event has occurred at the + /// given block timestamp. + /// + /// `MiniRex2` is a patch hardfork: it introduces no new spec (it restores + /// [`MegaSpecId::MINI_REX`] after the `MiniRex1` rollback), so it is not recoverable from a + /// spec ordinal and this predicate stays a raw event query, unlike the floor-projected + /// spec-introducing predicates. fn is_mini_rex_2_active_at_timestamp(&self, timestamp: u64) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex2).active_at_timestamp(timestamp) } - /// Returns `true` if [`MegaHardfork::Rex`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX`], the spec + /// introduced by [`MegaHardfork::Rex`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX) } - /// Returns `true` if [`MegaHardfork::Rex1`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX1`], the spec + /// introduced by [`MegaHardfork::Rex1`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex1).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX1) } - /// Returns `true` if [`MegaHardfork::Rex2`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX2`], the spec + /// introduced by [`MegaHardfork::Rex2`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex2).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX2) } - /// Returns `true` if [`MegaHardfork::Rex3`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX3`], the spec + /// introduced by [`MegaHardfork::Rex3`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex3).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX3) } - /// Returns `true` if [`MegaHardfork::Rex4`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX4`], the spec + /// introduced by [`MegaHardfork::Rex4`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex4).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX4) } - /// Returns `true` if [`MegaHardfork::Rex5`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX5`], the spec + /// introduced by [`MegaHardfork::Rex5`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex5).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX5) } - /// Returns `true` if [`MegaHardfork::Rex6`] is active at given block timestamp. + /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX6`], the spec + /// introduced by [`MegaHardfork::Rex6`]. Floor-projected — 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: u64) -> bool { - self.mega_fork_activation(MegaHardfork::Rex6).active_at_timestamp(timestamp) + self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX6) + } + + /// 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 derive from the activated-spec floor, 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 spec-introducing fork may be unscheduled only if no scheduled fork + /// maps to an equal-or-higher spec. Patch forks are exempt — testnet legitimately never + /// schedules `MiniRex1`/`MiniRex2`. + /// - 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 (i, fork) in MegaHardfork::VARIANTS.iter().enumerate() { + let introduces_spec = + MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); + if !introduces_spec || 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 { @@ -334,9 +499,12 @@ impl MegaHardforkConfig { /// ([`max_activated_spec_id`](MegaHardforks::max_activated_spec_id)) 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`. @@ -377,18 +545,17 @@ impl MegaHardforkConfig { /// Removes a `MegaHardfork` from the configuration, i.e., equivalent to setting the fork /// condition to [`ForkCondition::Never`]. /// - /// Deliberately not public. Every external use it had was - /// `with_all_activated().without(fork)` meaning "a chain running the spec below `fork`", which - /// it does not express — 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. + /// 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. - pub(crate) fn without(mut self, hardfork: MegaHardfork) -> Self { + fn without(mut self, hardfork: MegaHardfork) -> Self { self.entries.retain(|e| e.fork.name() != hardfork.name()); self } @@ -665,17 +832,19 @@ mod tests { } } - /// The gap this change closes: a config that schedules a later fork without its predecessor - /// resolves to that fork's spec, yet every per-fork predicate below it reports inactive. - /// The floor makes the lower gates additive again. + /// On a partial ladder — a config that schedules a later fork without its predecessors — the + /// floor still enables every lower spec, and the spec-introducing predicates, being floor + /// 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_floor_enables_unscheduled_predecessors() { let hf = MegaHardforkConfig::new() .with(MegaHardfork::Rex5, ForkCondition::Never) .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); - assert!(!hf.is_rex_5_active_at_timestamp(0), "Rex5 is not scheduled"); - assert!(!hf.is_mini_rex_active_at_timestamp(0), "MiniRex is not scheduled"); + assert_eq!(hf.mega_fork_activation(MegaHardfork::Rex5), ForkCondition::Never); + assert_eq!(hf.mega_fork_activation(MegaHardfork::MiniRex), ForkCondition::Never); assert_eq!(hf.spec_id(0), MegaSpecId::REX6); let floor = hf.max_activated_spec_id(0); @@ -683,26 +852,19 @@ mod tests { for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX2, MegaSpecId::REX4, MegaSpecId::REX5] { assert!(floor.is_enabled(spec), "floor must enable {spec:?} on a partial ladder"); } + // The predicates project the floor: unscheduled predecessors still report active. + assert!(hf.is_rex_5_active_at_timestamp(0), "Rex5 predicate follows the floor"); + assert!(hf.is_mini_rex_active_at_timestamp(0), "MiniRex predicate follows the floor"); } /// `with_all_activated_through` is the well-formed way to express "a chain running spec N": /// both the executing spec and the activated-spec floor resolve to exactly `N`, at any - /// timestamp. Driven off `MegaSpecId`'s own progression rather than a second hand-written - /// list, so a newly introduced spec fails here once instead of silently widening every - /// "chain running spec N" config in the suite. + /// timestamp. The specs under test are derived from the fork ladder itself (every spec some + /// fork maps to, which covers the whole progression), 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::EQUIVALENCE, - MegaSpecId::MINI_REX, - MegaSpecId::REX, - MegaSpecId::REX1, - MegaSpecId::REX2, - MegaSpecId::REX3, - MegaSpecId::REX4, - MegaSpecId::REX5, - MegaSpecId::REX6, - ] { + for spec in MegaHardfork::VARIANTS.iter().map(|fork| fork.spec_id()) { 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"); @@ -728,24 +890,34 @@ mod tests { } /// Removing a middle rung does NOT express "a chain running spec N". It is the partial-ladder - /// shape: the executing spec follows the newest fork still registered, and the floor keeps - /// every lower setup gate open. + /// shape: the executing spec follows the newest fork still registered, and the floor — with + /// the predicate projected from it — 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!(!partial.is_rex_4_active_at_timestamp(0), "Rex4 itself is unregistered"); + assert_eq!( + partial.mega_fork_activation(MegaHardfork::Rex4), + ForkCondition::Never, + "Rex4 itself is unregistered" + ); assert_ne!(partial.spec_id(0), MegaSpecId::REX4, "the executing spec is not lowered"); assert!( partial.max_activated_spec_id(0).is_enabled(MegaSpecId::REX4), "the floor still enables the removed fork's spec" ); + assert!( + partial.is_rex_4_active_at_timestamp(0), + "the projected predicate stays open with the floor" + ); } - /// The floor and the executing spec agree across the Rex5/Rex6 boundary on every canonical - /// schedule, which is what makes the two-spec split in `resolve_system_address` inert today. - /// Adding a hardfork that maps below `REX5` would break this and must be caught here. + /// The floor and the executing spec agree on every spec at or above `REX5` across every + /// canonical schedule, which is what makes the two-spec split in `resolve_system_address` + /// inert today. A rollback hardfork scheduled *after* Rex5's activation that maps below + /// `REX5` would break the agreement and must be caught here. (A rollback scheduled before + /// Rex5 — mainnet's `MiniRex1` — keeps both sides below `REX5` and does not.) #[test] fn test_floor_and_executing_spec_agree_across_rex5_rex6_boundary() { for hf in [ @@ -762,7 +934,11 @@ mod tests { for ts in stamps { let (exec, floor) = (hf.spec_id(ts), hf.max_activated_spec_id(ts)); - for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + for spec in MegaHardfork::VARIANTS + .iter() + .map(|fork| fork.spec_id()) + .filter(|spec| spec.is_enabled(MegaSpecId::REX5)) + { assert_eq!( exec.is_enabled(spec), floor.is_enabled(spec), @@ -774,18 +950,134 @@ mod tests { } /// Documented domain limit: the floor is timestamp-scoped, so a `MegaHardfork` registered by - /// block number never contributes to it and the equivalence with the per-fork predicates does - /// not hold. `spec_id`/`hardfork` share this limitation; every canonical schedule uses - /// `Timestamp` or `Never`. + /// 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_floor_ignores_block_numbered_forks() { let hf = MegaHardforkConfig::new() .with(MegaHardfork::MiniRex, ForkCondition::Block(0)) .with(MegaHardfork::Rex, ForkCondition::Timestamp(0)); - assert!(!hf.is_mini_rex_active_at_timestamp(0), "block-numbered forks are not timestamped"); + assert!( + !hf.mega_fork_activation(MegaHardfork::MiniRex).active_at_timestamp(0), + "block-numbered forks are not timestamped" + ); + // The floor 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.max_activated_spec_id(0), MegaSpecId::REX); assert!(hf.max_activated_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 }) + ); + } + + /// Patch-fork predicates stay raw event queries: they are not recoverable from a spec + /// ordinal, so they must not follow the floor. Testnet is the live case — its floor climbs + /// the whole ladder while `MiniRex1`/`MiniRex2` are never scheduled. + #[test] + fn test_patch_fork_predicates_stay_event_scoped() { + let hf = crate::testnet_hardforks(); + let ts = u64::MAX; + assert!(hf.max_activated_spec_id(ts).is_enabled(MegaSpecId::REX5), "floor 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 only + /// `MiniRex1` (a patch fork), which must not count as a skipped rung. + #[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 MegaHardfork::VARIANTS.iter().map(|fork| fork.spec_id()) { + 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 (the floor keeps setup 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 + }) + ); + } + + #[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] diff --git a/crates/mega-evm/src/system/AGENTS.md b/crates/mega-evm/src/system/AGENTS.md index 7fa53cc9..ff789a8d 100644 --- a/crates/mega-evm/src/system/AGENTS.md +++ b/crates/mega-evm/src/system/AGENTS.md @@ -15,7 +15,7 @@ System contract integration layer with canonical addresses, deployment transacti ## KEY PATTERNS - Deployment helpers are idempotent and keyed by code hash equality. -- Gating happens in each contract's `_spec()` builder, which takes a resolved `MegaSpecId` and gates on `spec.is_enabled(...)`. The builders deliberately do not take a hardfork config, 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 the spec themselves. +- Gating happens in each contract's `_spec()` builder, which takes a resolved `MegaSpecId` and gates on `spec.is_enabled(...)`. Nothing in the deploy layer takes a hardfork config — the spec builders and the crate-private `*_for` helpers receive the resolved floor 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. - The spec passed to a builder is the **activated-spec floor** (`MegaHardforks::max_activated_spec_id`), not the executing spec. Predeploys are one-way: a hardfork that rolls the spec back does not un-deploy them, and it must not 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()`. diff --git a/crates/mega-evm/src/system/deploy.rs b/crates/mega-evm/src/system/deploy.rs index 7e7e4fa1..12a69c87 100644 --- a/crates/mega-evm/src/system/deploy.rs +++ b/crates/mega-evm/src/system/deploy.rs @@ -352,7 +352,11 @@ mod tests { .with(MegaHardfork::Rex5, ForkCondition::Never) .with(MegaHardfork::Rex6, ForkCondition::Timestamp(0)); - assert!(!hf.is_mini_rex_active_at_timestamp(0), "no lower fork is scheduled"); + 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"); diff --git a/crates/mega-evm/src/system/oracle.rs b/crates/mega-evm/src/system/oracle.rs index 94b51ffd..d18f4e30 100644 --- a/crates/mega-evm/src/system/oracle.rs +++ b/crates/mega-evm/src/system/oracle.rs @@ -37,9 +37,10 @@ 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 activated-spec floor: /// - 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, diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 6f110a8a..9088fb75 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -193,17 +193,20 @@ pub fn transact_deploy_sequencer_registry( config: &SequencerRegistryConfig, ) -> Result, BlockExecutionError> { let spec = hardforks.max_activated_spec_id(block_timestamp); - transact_deploy_sequencer_registry_for(hardforks, spec, current_block_number, db, config) + 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 activated-spec floor. /// /// The block executor resolves the floor once per block and calls this directly; the public -/// wrapper above resolves it for callers that hold a hardfork config. `hardforks` is still -/// needed for the Rex6 parameter lookup, which is per-fork rather than per-spec. +/// 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 floor and the typed params), so a per-fork activation gate +/// cannot be reintroduced here. pub(crate) fn transact_deploy_sequencer_registry_for( - hardforks: impl MegaHardforks, spec: crate::MegaSpecId, + rex6_config: Option<&SequencerRegistryRex6Config>, current_block_number: u64, db: &mut State, config: &SequencerRegistryConfig, @@ -232,10 +235,8 @@ pub(crate) fn transact_deploy_sequencer_registry_for( (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(), @@ -418,9 +419,11 @@ where /// to be installed. This must match what [`transact_deploy_sequencer_registry`] installed, which /// is floor-gated because a deployed contract is not un-deployed by a rollback. /// -/// The two agree on every canonical schedule (no hardfork maps below REX5), so this split is -/// currently inert; it exists so that adding a rollback hardfork cannot silently change -/// transaction classification or produce a spurious code-hash mismatch. +/// The two agree on every canonical schedule — no hardfork scheduled after Rex5's activation +/// rolls the spec back below `REX5` (mainnet's `MiniRex1` maps below it, but activates before +/// Rex5, keeping both sides below `REX5`) — so this split is currently inert; it exists so that +/// adding such a rollback hardfork cannot silently change transaction classification or produce +/// a spurious code-hash mismatch. /// /// The optional `EvmState` captures account + slot reads as a witness record. /// The executor MUST commit this via `system_caller.on_state()` + `db.commit()`. @@ -430,6 +433,13 @@ pub fn resolve_system_address( setup_spec: crate::MegaSpecId, db: &mut State, ) -> Result<(Address, Option), BlockExecutionError> { + // The floor is a maximum over activated forks and the executing spec is one of them, so + // `setup_spec >= exec_spec` always. A swapped argument pair satisfies the type system but + // trips this in every rollback-window test. + debug_assert!( + setup_spec.is_enabled(exec_spec), + "setup_spec ({setup_spec:?}) below exec_spec ({exec_spec:?}) — arguments swapped?" + ); if !exec_spec.is_enabled(crate::MegaSpecId::REX5) { return Ok((MEGA_SYSTEM_ADDRESS, None)); } diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs index a0d6cb0b..0141af9f 100644 --- a/crates/mega-evm/tests/block_executor/partial_ladder.rs +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -137,9 +137,11 @@ impl OnStateHook for RecordingStateHook { #[test] fn test_partial_ladder_runs_lower_fork_setup() { let chain_spec = rex6_only_chain_spec(); - // Precondition: this really is a partial ladder, and the executing spec is Rex6. - assert!(!chain_spec.is_rex_5_active_at_timestamp(0)); - assert!(!chain_spec.is_mini_rex_active_at_timestamp(0)); + // 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 activated-spec floor, 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(); diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index cef5d674..d969d8c9 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -211,36 +211,63 @@ 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)) } -/// 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 floor-projected predicates coincide with the activation +/// events). This kills the `-> false` mutants on the predicates. +/// +/// Every predicate is probed on **both** sides of its own activation, which is what pins the +/// spec each one projects. A spec-introducing predicate's body is +/// `max_activated_spec_id(t).is_enabled(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 — 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 — patch 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 — patch 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. + assert!(!cfg.is_rex_active_at_timestamp(399)); + assert!(cfg.is_rex_active_at_timestamp(400)); + + // Rex1. assert!(!cfg.is_rex_1_active_at_timestamp(499)); assert!(cfg.is_rex_1_active_at_timestamp(500)); - // Rex3 (line 169). + // Rex2. + assert!(!cfg.is_rex_2_active_at_timestamp(599)); + assert!(cfg.is_rex_2_active_at_timestamp(600)); + + // Rex3. 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. + assert!(!cfg.is_rex_4_active_at_timestamp(799)); assert!(cfg.is_rex_4_active_at_timestamp(800)); + + // Rex5. + assert!(!cfg.is_rex_5_active_at_timestamp(899)); assert!(cfg.is_rex_5_active_at_timestamp(900)); + + // Rex6. + assert!(!cfg.is_rex_6_active_at_timestamp(999)); + assert!(cfg.is_rex_6_active_at_timestamp(1000)); } // ============================================================================ 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..a4a6592c 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)) } diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index 551abc02..d48ddf32 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -32,9 +32,12 @@ The distinction matters for chain setup that is one-way. System contracts predeployed under a hardfork remain deployed, and pre-block system calls remain in effect, even while a later hardfork rolls the executing semantics back to an earlier spec. A rollback changes how transactions execute; it does not un-deploy a contract or retract a system call. -Pre-block setup — system-contract predeploys, their bytecode versions, and the pre-block EIP-2935/EIP-4788 system calls — is therefore determined by the highest spec reached. +Pre-block setup — system-contract predeploys, their bytecode versions, and the fail-closed rules on the pre-block EIP-2935/EIP-4788 system calls — is therefore determined by the highest spec reached. Everything else — opcode behavior, gas costs, resource limits, transaction classification — is determined by the executing spec. +A published hardfork schedule climbs the spec ladder rung by rung: a hardfork is scheduled only after every hardfork of a lower spec, with patch hardforks as the only ones a network may skip. +Execution is additionally robust to a malformed schedule: because setup derives from the highest spec reached, 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. From 0f92b9595577245dd18df8dd7f8350f21ed40e89 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 31 Jul 2026 18:22:30 +0800 Subject: [PATCH 05/37] test(hardfork): cover a skipped middle rung in validate_schedule The only skipped-rung test used the lowest rung, whose empty prefix makes the spec-introducing classification degenerate. A middle-rung gap pins the prefix comparison itself, killing the surviving replace-<-with-== mutant. --- crates/mega-evm/src/block/hardfork.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index a89f9f44..f693b2d0 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -1034,6 +1034,25 @@ mod tests { ); } + /// 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 + }) + ); + } + #[test] fn test_validate_schedule_rejects_unordered_forks() { let hf = MegaHardforkConfig::new() From e2539683836b1b14adcbf23d423da7d10db1dad4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 10:53:59 +0800 Subject: [PATCH 06/37] refactor(spec): declare MegaSpecId::ALL as the single spec enumeration The exhaustive ladder_index match makes introducing a spec a compile error until the variant is placed, a const assertion ties each ALL entry to its ladder position, and the latest-spec anchor test fails until ALL carries the new spec. Golden name pairs stay hand-written but their spec column must equal ALL. --- crates/mega-evm/src/evm/spec.rs | 65 ++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 9fff9bba..0437a2ad 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -80,6 +80,25 @@ 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 (whose + /// 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::REX, + Self::REX1, + Self::REX2, + Self::REX3, + Self::REX4, + Self::REX5, + Self::REX6, + ]; + /// Converts the [`SpecId`] into its corresponding [`EthSpecId`]. pub const fn into_eth_spec(self) -> EthSpecId { self.into_op_spec().into_eth_spec() @@ -110,6 +129,36 @@ impl MegaSpecId { } } +/// 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::REX => 2, + MegaSpecId::REX1 => 3, + MegaSpecId::REX2 => 4, + MegaSpecId::REX3 => 5, + MegaSpecId::REX4 => 6, + MegaSpecId::REX5 => 7, + MegaSpecId::REX6 => 8, + } +} + +const _: () = { + let mut i = 0; + while i < MegaSpecId::ALL.len() { + assert!( + ladder_index(MegaSpecId::ALL[i]) == i, + "MegaSpecId::ALL must list every spec in ladder order, without gaps" + ); + i += 1; + } +}; + impl From for &'static str { /// Converts the [`SpecId`] into its corresponding string identifier. fn from(spec_id: MegaSpecId) -> Self { @@ -186,6 +235,11 @@ mod tests { #[test] fn test_spec_names_roundtrip_and_display() { + // The golden pairs stay hand-written — deriving the expected names from the code under + // test would make the round-trip vacuous — but 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); @@ -196,9 +250,18 @@ 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_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); From fa9264881054d07ab6268f9a3647e24af183dd82 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 10:53:59 +0800 Subject: [PATCH 07/37] perf(hardfork): early-exit the activated-spec floor scan The descending scan stops at the first activated spec-introducing fork, so floor queries near the top of the ladder cost one or two activation lookups instead of one per fork (~83ns vs ~350ns on the mainnet schedule). A reference test pins the scan against the naive max-over-activated-forks formula across every schedule shape. Spec sweeps in the hardfork tests now come from MegaSpecId::ALL. --- crates/mega-evm/src/block/hardfork.rs | 87 ++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index f693b2d0..d1aea272 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -177,12 +177,29 @@ pub trait MegaHardforks: OpHardforks { /// registered with [`ForkCondition::Block`] or [`ForkCondition::TTD`] never reports active /// here. Every `MegaHardfork` in the canonical schedules uses `Timestamp` or `Never`. fn max_activated_spec_id(&self, timestamp: BlockTimestamp) -> MegaSpecId { - MegaHardfork::VARIANTS - .iter() - .filter(|fork| self.mega_fork_activation(**fork).active_at_timestamp(timestamp)) - .map(|fork| fork.spec_id()) - .max() - .unwrap_or(MegaSpecId::EQUIVALENCE) + // Descending scan with early exit: a spec-introducing fork's spec is the highest any + // fork declared at or before it maps to, so the scan stops at the first activated + // spec-introducing fork (or once the running floor already covers everything earlier). + // The floor for a chain near the top of the ladder — every real query — thus costs one + // or two activation lookups, not one per fork; only the `mega_fork_activation` calls + // are bounded, the `introduces_spec` prefix check is enum-ordinal arithmetic. + // `test_floor_early_exit_matches_naive_reference` pins this against the plain + // max-over-activated-forks formula. + let mut floor = MegaSpecId::EQUIVALENCE; + for (i, fork) in MegaHardfork::VARIANTS.iter().enumerate().rev() { + let introduces_spec = + MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); + if introduces_spec && floor.is_enabled(fork.spec_id()) { + break; + } + if self.mega_fork_activation(*fork).active_at_timestamp(timestamp) { + floor = floor.max(fork.spec_id()); + if introduces_spec { + break; + } + } + } + floor } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::MINI_REX`], the @@ -859,12 +876,11 @@ mod tests { /// `with_all_activated_through` is the well-formed way to express "a chain running spec N": /// both the executing spec and the activated-spec floor resolve to exactly `N`, at any - /// timestamp. The specs under test are derived from the fork ladder itself (every spec some - /// fork maps to, which covers the whole progression), so a newly introduced spec is covered - /// here automatically instead of depending on a hand-written list. + /// 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 MegaHardfork::VARIANTS.iter().map(|fork| fork.spec_id()) { + 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"); @@ -934,10 +950,8 @@ mod tests { for ts in stamps { let (exec, floor) = (hf.spec_id(ts), hf.max_activated_spec_id(ts)); - for spec in MegaHardfork::VARIANTS - .iter() - .map(|fork| fork.spec_id()) - .filter(|spec| spec.is_enabled(MegaSpecId::REX5)) + for spec in + MegaSpecId::ALL.iter().copied().filter(|spec| spec.is_enabled(MegaSpecId::REX5)) { assert_eq!( exec.is_enabled(spec), @@ -974,6 +988,49 @@ mod tests { ); } + /// The early-exit descending scan in `max_activated_spec_id` must be observationally + /// identical to the naive maximum over all activated forks, on every schedule shape: + /// canonical ladders, rollback windows, partial ladders, block-numbered conditions, and the + /// empty config. + #[test] + fn test_floor_early_exit_matches_naive_reference() { + let configs = [ + crate::mainnet_hardforks(), + crate::testnet_hardforks(), + crate::all_activated_hardforks(), + MegaHardforkConfig::new(), + // Partial ladders: a lone top rung, and a lone patch 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.max_activated_spec_id(ts), naive, "floor diverges at ts={ts}"); + } + } + } + /// Patch-fork predicates stay raw event queries: they are not recoverable from a spec /// ordinal, so they must not follow the floor. Testnet is the live case — its floor climbs /// the whole ladder while `MiniRex1`/`MiniRex2` are never scheduled. @@ -1001,7 +1058,7 @@ mod tests { assert_eq!(crate::all_activated_hardforks().validate_schedule(), Ok(())); assert_eq!(MegaHardforkConfig::new().validate_schedule(), Ok(()), "no mega forks is fine"); - for spec in MegaHardfork::VARIANTS.iter().map(|fork| fork.spec_id()) { + 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 { From d74f53370bbf0d2dc6162889a7133430c9fc9f65 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 10:53:59 +0800 Subject: [PATCH 08/37] test(rex5): pin the pre-REX5 30M pre-block system call budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Medium-bucket costs (~100M per SSTORE) must still OOG on a REX4 chain: pre-REX5 keeps revm's upstream 30M budget for replay parity, so the budget selection must follow the REX5 spec exactly. Also point the EIP-2935 assertions at the ring-buffer slot the contract actually writes, (number-1) % 8191 — slot 0 was vacuously empty. --- .../tests/rex5/pre_block_system_calls.rs | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) 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 a4a6592c..2e7b1f1e 100644 --- a/crates/mega-evm/tests/rex5/pre_block_system_calls.rs +++ b/crates/mega-evm/tests/rex5/pre_block_system_calls.rs @@ -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)` From f3b0e3c079b326b3780e822fb59dc0b735e0dcc4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 11:26:19 +0800 Subject: [PATCH 09/37] fix(hardfork): reject orphan patch forks and align timestamp types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scheduled patch fork now requires its base — the nearest earlier-declared spec-introducing fork — to be scheduled (ScheduleError::OrphanPatch), and with_all_activated_through no longer registers a patch without its base, so through(EQUIVALENCE) registers nothing and resolves by default. The spec-introducing classification and base lookup are factored into MegaHardfork helpers shared by the floor scan, validation, and the builder. All trait timestamp parameters now spell BlockTimestamp. --- crates/mega-evm/src/block/hardfork.rs | 115 ++++++++++++++++++++------ 1 file changed, 88 insertions(+), 27 deletions(-) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index d1aea272..dbad2cef 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -39,6 +39,27 @@ hardfork! { } impl MegaHardfork { + /// Whether this fork introduces a new spec — its spec is strictly higher than every + /// earlier-declared fork's. Patch forks (`MiniRex1`, `MiniRex2`) do not. + pub(crate) fn introduces_spec(self) -> bool { + let declared = self.declaration_index(); + Self::VARIANTS[..declared].iter().all(|fork| fork.spec_id() < self.spec_id()) + } + + /// The nearest earlier-declared spec-introducing fork — for a patch fork, the fork whose + /// behavior it patches. `None` only for the first declared fork. + pub(crate) fn base_fork(self) -> Option { + let declared = self.declaration_index(); + Self::VARIANTS[..declared].iter().rev().find(|fork| fork.introduces_spec()).copied() + } + + fn declaration_index(self) -> usize { + Self::VARIANTS + .iter() + .position(|fork| *fork == self) + .expect("every MegaHardfork is in VARIANTS") + } + /// Gets the `MegaSpecId` associated with this hardfork. #[allow(clippy::match_same_arms)] pub fn spec_id(&self) -> MegaSpecId { @@ -137,7 +158,7 @@ pub trait MegaHardforks: OpHardforks { /// This is the *executing* resolution: it reads raw activation events /// ([`mega_fork_activation`](Self::mega_fork_activation)), not the activated-spec floor, so a /// rollback patch fork like `MiniRex1` correctly takes over from the fork it patches. - fn hardfork(&self, timestamp: u64) -> Option { + fn hardfork(&self, timestamp: BlockTimestamp) -> Option { MegaHardfork::VARIANTS .iter() .rev() @@ -186,9 +207,8 @@ pub trait MegaHardforks: OpHardforks { // `test_floor_early_exit_matches_naive_reference` pins this against the plain // max-over-activated-forks formula. let mut floor = MegaSpecId::EQUIVALENCE; - for (i, fork) in MegaHardfork::VARIANTS.iter().enumerate().rev() { - let introduces_spec = - MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); + for fork in MegaHardfork::VARIANTS.iter().rev() { + let introduces_spec = fork.introduces_spec(); if introduces_spec && floor.is_enabled(fork.spec_id()) { break; } @@ -205,7 +225,7 @@ pub trait MegaHardforks: OpHardforks { /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::MINI_REX`], the /// spec introduced by [`MegaHardfork::MiniRex`]. Floor-projected — 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: u64) -> bool { + fn is_mini_rex_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::MINI_REX) } @@ -216,7 +236,7 @@ pub trait MegaHardforks: OpHardforks { /// back to [`MegaSpecId::EQUIVALENCE`]), so it is not recoverable from a spec ordinal and /// this predicate stays a raw event query, unlike the floor-projected spec-introducing /// predicates. - fn is_mini_rex_1_active_at_timestamp(&self, timestamp: u64) -> bool { + fn is_mini_rex_1_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex1).active_at_timestamp(timestamp) } @@ -227,56 +247,56 @@ pub trait MegaHardforks: OpHardforks { /// [`MegaSpecId::MINI_REX`] after the `MiniRex1` rollback), so it is not recoverable from a /// spec ordinal and this predicate stays a raw event query, unlike the floor-projected /// spec-introducing predicates. - fn is_mini_rex_2_active_at_timestamp(&self, timestamp: u64) -> bool { + fn is_mini_rex_2_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.mega_fork_activation(MegaHardfork::MiniRex2).active_at_timestamp(timestamp) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX`], the spec /// introduced by [`MegaHardfork::Rex`]. Floor-projected — 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: u64) -> bool { + fn is_rex_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX1`], the spec /// introduced by [`MegaHardfork::Rex1`]. Floor-projected — 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: u64) -> bool { + fn is_rex_1_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX1) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX2`], the spec /// introduced by [`MegaHardfork::Rex2`]. Floor-projected — 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: u64) -> bool { + fn is_rex_2_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX2) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX3`], the spec /// introduced by [`MegaHardfork::Rex3`]. Floor-projected — 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: u64) -> bool { + fn is_rex_3_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX3) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX4`], the spec /// introduced by [`MegaHardfork::Rex4`]. Floor-projected — 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: u64) -> bool { + fn is_rex_4_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX4) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX5`], the spec /// introduced by [`MegaHardfork::Rex5`]. Floor-projected — 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: u64) -> bool { + fn is_rex_5_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX5) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX6`], the spec /// introduced by [`MegaHardfork::Rex6`]. Floor-projected — 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: u64) -> bool { + fn is_rex_6_active_at_timestamp(&self, timestamp: BlockTimestamp) -> bool { self.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX6) } @@ -299,6 +319,9 @@ pub trait MegaHardforks: OpHardforks { /// - No skipped rungs: a spec-introducing fork may be unscheduled only if no scheduled fork /// maps to an equal-or-higher spec. Patch forks are exempt — testnet legitimately never /// schedules `MiniRex1`/`MiniRex2`. + /// - No orphan patches: a scheduled patch fork requires the spec-introducing fork it patches to + /// be scheduled. Rolling back or restoring a fork that never happened is a configuration + /// mistake even though execution resolves it harmlessly. /// - Per-fork parameters required by a scheduled fork are present (`Rex5` requires /// [`SequencerRegistryConfig`](crate::SequencerRegistryConfig), `Rex6` requires /// [`SequencerRegistryRex6Config`](crate::SequencerRegistryRex6Config)). This surfaces a @@ -329,10 +352,8 @@ pub trait MegaHardforks: OpHardforks { } } - for (i, fork) in MegaHardfork::VARIANTS.iter().enumerate() { - let introduces_spec = - MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); - if !introduces_spec || scheduled(*fork) { + for fork in MegaHardfork::VARIANTS { + if !fork.introduces_spec() || scheduled(*fork) { continue; } if let Some(above) = MegaHardfork::VARIANTS @@ -343,6 +364,16 @@ pub trait MegaHardforks: OpHardforks { } } + for fork in MegaHardfork::VARIANTS { + if fork.introduces_spec() || !scheduled(*fork) { + continue; + } + let base = fork.base_fork().expect("a patch fork always has an earlier base"); + if !scheduled(base) { + return Err(ScheduleError::OrphanPatch { patch: *fork, base }); + } + } + if scheduled(MegaHardfork::Rex5) && self.fork_params::().is_none() { return Err(ScheduleError::MissingParams { @@ -389,6 +420,15 @@ pub enum ScheduleError { /// A scheduled fork whose spec is equal or higher. scheduled: MegaHardfork, }, + /// A patch fork is scheduled while the spec-introducing fork it patches is not — a + /// rollback or restoration of a fork that never happened. + #[display("patch hardfork {patch:?} is scheduled but its base {base:?} is not")] + OrphanPatch { + /// The scheduled patch fork. + patch: MegaHardfork, + /// The unscheduled spec-introducing fork it patches. + base: 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")] @@ -523,11 +563,15 @@ impl MegaHardforkConfig { /// 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`. + /// Patch hardforks ride along with the fork they patch: `MiniRex1`/`MiniRex2` are + /// registered exactly when `MiniRex` is. A patch without its base would schedule the + /// rollback of a fork that never happened — the shape `validate_schedule` rejects as + /// [`ScheduleError::OrphanPatch`] — so `with_all_activated_through(EQUIVALENCE)` registers + /// no fork at all and resolves to `EQUIVALENCE` by default. pub fn with_all_activated_through(mut self, spec: MegaSpecId) -> Self { for fork in MegaHardfork::VARIANTS { - if spec.is_enabled(fork.spec_id()) { + let base_included = fork.base_fork().is_none_or(|base| spec.is_enabled(base.spec_id())); + if spec.is_enabled(fork.spec_id()) && base_included { self.insert(*fork, ForkCondition::Timestamp(0)); } else { self = self.without(*fork); @@ -833,10 +877,8 @@ mod tests { for ts in stamps { let floor = hf.max_activated_spec_id(ts); - for (i, fork) in MegaHardfork::VARIANTS.iter().enumerate() { - let introduces_spec = - MegaHardfork::VARIANTS[..i].iter().all(|e| e.spec_id() < fork.spec_id()); - if !introduces_spec { + for fork in MegaHardfork::VARIANTS { + if !fork.introduces_spec() { continue; } assert_eq!( @@ -1049,8 +1091,9 @@ mod tests { } /// 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 only - /// `MiniRex1` (a patch fork), which must not count as a skipped rung. + /// `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(())); @@ -1110,6 +1153,24 @@ mod tests { ); } + /// Scheduling a patch fork without the fork it patches is the rollback of a fork that + /// never happened. Execution resolves it harmlessly (the spec stays `EQUIVALENCE`), but a + /// published schedule with this shape is a configuration mistake. + #[test] + fn test_validate_schedule_rejects_orphan_patch() { + let hf = + MegaHardforkConfig::new().with(MegaHardfork::MiniRex1, ForkCondition::Timestamp(0)); + + assert_eq!(hf.spec_id(0), MegaSpecId::EQUIVALENCE, "execution itself is unaffected"); + assert_eq!( + hf.validate_schedule(), + Err(ScheduleError::OrphanPatch { + patch: MegaHardfork::MiniRex1, + base: MegaHardfork::MiniRex + }) + ); + } + #[test] fn test_validate_schedule_rejects_unordered_forks() { let hf = MegaHardforkConfig::new() From 568d51c98f76e37424e163ee3230937575e6d814 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 11:26:19 +0800 Subject: [PATCH 10/37] docs(spec): state hardfork schedule rules normatively The executing-spec/highest-spec-reached split and the schedule-climbing rules now use RFC-2119 language, including the orphan-patch prohibition. Mutation probe comments regain their hardfork.rs line references per the REVIEW.md linkage rule. --- crates/mega-evm/tests/mutation/block.rs | 25 +++++++++++++------------ docs/spec/hardfork-spec.md | 6 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index d969d8c9..a7fbe17e 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -216,7 +216,8 @@ fn staged_config() -> MegaHardforkConfig { /// Each `is_*_active_at_timestamp` returns `true` at/after activation and `false` before, on a /// complete ordered ladder (where the floor-projected predicates coincide with the activation -/// events). This kills the `-> false` mutants on the predicates. +/// 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 spec-introducing predicate's body is @@ -228,44 +229,44 @@ fn staged_config() -> MegaHardforkConfig { fn test_hardfork_activation_predicates_are_true_at_activation() { let cfg = staged_config(); - // MiniRex — spec-introducing (MINI_REX). Shifting down lands on EQUIVALENCE, which is - // enabled at every timestamp, so only the `false` side catches it. + // MiniRex (hardfork.rs:229) — 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 — patch fork, raw event query. + // MiniRex1 (hardfork.rs:240) — patch 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 — patch fork, raw event query. + // MiniRex2 (hardfork.rs:251) — patch fork, raw event query. assert!(!cfg.is_mini_rex_2_active_at_timestamp(299)); assert!(cfg.is_mini_rex_2_active_at_timestamp(300)); - // Rex. + // Rex (hardfork.rs:258). assert!(!cfg.is_rex_active_at_timestamp(399)); assert!(cfg.is_rex_active_at_timestamp(400)); - // Rex1. + // Rex1 (hardfork.rs:265). assert!(!cfg.is_rex_1_active_at_timestamp(499)); assert!(cfg.is_rex_1_active_at_timestamp(500)); - // Rex2. + // Rex2 (hardfork.rs:272). assert!(!cfg.is_rex_2_active_at_timestamp(599)); assert!(cfg.is_rex_2_active_at_timestamp(600)); - // Rex3. + // Rex3 (hardfork.rs:279). assert!(!cfg.is_rex_3_active_at_timestamp(699)); assert!(cfg.is_rex_3_active_at_timestamp(700)); - // Rex4. + // Rex4 (hardfork.rs:286). assert!(!cfg.is_rex_4_active_at_timestamp(799)); assert!(cfg.is_rex_4_active_at_timestamp(800)); - // Rex5. + // Rex5 (hardfork.rs:293). assert!(!cfg.is_rex_5_active_at_timestamp(899)); assert!(cfg.is_rex_5_active_at_timestamp(900)); - // Rex6. + // Rex6 (hardfork.rs:300). assert!(!cfg.is_rex_6_active_at_timestamp(999)); assert!(cfg.is_rex_6_active_at_timestamp(1000)); } diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index d48ddf32..61d06caf 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -32,10 +32,10 @@ The distinction matters for chain setup that is one-way. System contracts predeployed under a hardfork remain deployed, and pre-block system calls remain in effect, even while a later hardfork rolls the executing semantics back to an earlier spec. A rollback changes how transactions execute; it does not un-deploy a contract or retract a system call. -Pre-block setup — system-contract predeploys, their bytecode versions, and the fail-closed rules on the pre-block EIP-2935/EIP-4788 system calls — is therefore determined by the highest spec reached. -Everything else — opcode behavior, gas costs, resource limits, transaction classification — is determined by the executing spec. +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 highest spec reached. +A node MUST determine all other behavior — opcode behavior, gas costs, resource limits, transaction classification — from the executing spec. -A published hardfork schedule climbs the spec ladder rung by rung: a hardfork is scheduled only after every hardfork of a lower spec, with patch hardforks as the only ones a network may skip. +A published hardfork schedule MUST climb the spec ladder rung by rung: a hardfork MUST NOT be scheduled unless every hardfork of a lower spec is scheduled, with two rules for patch hardforks — a network MAY omit a patch hardfork, but MUST NOT schedule a patch hardfork whose base hardfork is not scheduled. Execution is additionally robust to a malformed schedule: because setup derives from the highest spec reached, 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. From a510e5cc9faf3e7d4ef9b15d509ac54672a95bf8 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 1 Aug 2026 12:03:24 +0800 Subject: [PATCH 11/37] test(spec): make the ALL ladder checker itself testable The const assertion's loop guard was invisible to tests: the real ALL always satisfies the property, so a weakened guard passed silently. The check is now a const fn over any list, const-asserted on ALL and fed malformed lists by a test, so the rejection paths exercise the guard. --- crates/mega-evm/src/evm/spec.rs | 52 +++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 0437a2ad..c9cb03c3 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -84,8 +84,8 @@ impl MegaSpecId { /// /// 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 (whose - /// const assertion ties each entry here to its ladder position), and + /// 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, @@ -148,16 +148,27 @@ const fn ladder_index(spec: MegaSpecId) -> usize { } } -const _: () = { +/// 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 < MegaSpecId::ALL.len() { - assert!( - ladder_index(MegaSpecId::ALL[i]) == i, - "MegaSpecId::ALL must list every spec in ladder order, without gaps" - ); + 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" +); impl From for &'static str { /// Converts the [`SpecId`] into its corresponding string identifier. @@ -259,6 +270,29 @@ mod tests { assert_eq!(*MegaSpecId::ALL.last().unwrap(), MegaSpecId::default()); } + /// The compile-time checker is itself exercised with malformed lists: the real `ALL` + /// always satisfies the property, so only rejection cases can detect a weakened guard + /// inside the checker. + #[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_all_specs_map_to_isthmus_and_prague() { for spec in MegaSpecId::ALL.iter().copied() { From ce6ea8f1e501f2c0a15cb7bef3344c391a007f9b Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 11:13:48 +0800 Subject: [PATCH 12/37] =?UTF-8?q?prototype:=20B=20design=20=E2=80=94=201:1?= =?UTF-8?q?=20fork/spec=20with=20alias=20specs=20and=20behavior=20projecti?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/mega-evm/src/block/eips.rs | 4 +- crates/mega-evm/src/block/executor.rs | 8 +- crates/mega-evm/src/block/hardfork.rs | 116 ++++++------------ crates/mega-evm/src/evm/instructions.rs | 7 +- crates/mega-evm/src/evm/limit.rs | 5 +- crates/mega-evm/src/evm/precompiles.rs | 5 +- crates/mega-evm/src/evm/spec.rs | 69 +++++++++-- crates/mega-evm/src/system/control.rs | 2 +- crates/mega-evm/src/system/deploy.rs | 10 +- crates/mega-evm/src/system/keyless_deploy.rs | 2 +- crates/mega-evm/src/system/limit_control.rs | 2 +- crates/mega-evm/src/system/oracle.rs | 8 +- .../mega-evm/src/system/sequencer_registry.rs | 97 ++++----------- .../tests/block_executor/partial_ladder.rs | 8 +- 14 files changed, 158 insertions(+), 185 deletions(-) diff --git a/crates/mega-evm/src/block/eips.rs b/crates/mega-evm/src/block/eips.rs index 41468b18..13056deb 100644 --- a/crates/mega-evm/src/block/eips.rs +++ b/crates/mega-evm/src/block/eips.rs @@ -61,7 +61,7 @@ where return Ok(None); } - let res = if setup_spec.is_enabled(MegaSpecId::REX5) { + 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( @@ -131,7 +131,7 @@ where return Ok(None); } - let res = if setup_spec.is_enabled(MegaSpecId::REX5) { + 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 db229a72..75891eca 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -197,7 +197,7 @@ where // Gating setup on the reversible spec would drop the Oracle predeploys — and their // read-only witness entries — from every block in such a window. let setup_spec = self.setup_spec; - let is_rex_5 = setup_spec.is_enabled(MegaSpecId::REX5); + let is_rex_5 = setup_spec.reaches(MegaSpecId::REX5); // EIP-2935 let result_and_state = eips::transact_blockhashes_contract_call( @@ -288,7 +288,7 @@ 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 = setup_spec.is_enabled(MegaSpecId::REX6); + let is_rex_6 = setup_spec.reaches(MegaSpecId::REX6); if !is_rex_6 { self.push_deploy_sequencer_registry_outcome( @@ -696,9 +696,9 @@ where // The executing spec gates whether dynamic resolution applies (semantics); the // activated-spec floor selects the expected registry bytecode version, matching what // the floor-gated pre-block deploy installed. - let exec_spec = self.evm.ctx().mega_spec(); + let spec = self.evm.ctx().mega_spec(); let (system_address, read_state) = - resolve_system_address(&self.hardforks, exec_spec, self.setup_spec, self.evm.db_mut())?; + resolve_system_address(&self.hardforks, spec, self.evm.db_mut())?; if let Some(state) = read_state { self.system_caller.on_state(StateChangeSource::Transaction(0), &state); self.evm.db_mut().commit(state); diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index dbad2cef..2e4fd0a0 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -39,18 +39,10 @@ hardfork! { } impl MegaHardfork { - /// Whether this fork introduces a new spec — its spec is strictly higher than every - /// earlier-declared fork's. Patch forks (`MiniRex1`, `MiniRex2`) do not. + /// 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_spec(self) -> bool { - let declared = self.declaration_index(); - Self::VARIANTS[..declared].iter().all(|fork| fork.spec_id() < self.spec_id()) - } - - /// The nearest earlier-declared spec-introducing fork — for a patch fork, the fork whose - /// behavior it patches. `None` only for the first declared fork. - pub(crate) fn base_fork(self) -> Option { - let declared = self.declaration_index(); - Self::VARIANTS[..declared].iter().rev().find(|fork| fork.introduces_spec()).copied() + !self.spec_id().is_alias() } fn declaration_index(self) -> usize { @@ -67,8 +59,8 @@ impl MegaHardfork { // previously released specs rather than introducing new EVM semantics. 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, @@ -206,27 +198,17 @@ pub trait MegaHardforks: OpHardforks { // are bounded, the `introduces_spec` prefix check is enum-ordinal arithmetic. // `test_floor_early_exit_matches_naive_reference` pins this against the plain // max-over-activated-forks formula. - let mut floor = MegaSpecId::EQUIVALENCE; - for fork in MegaHardfork::VARIANTS.iter().rev() { - let introduces_spec = fork.introduces_spec(); - if introduces_spec && floor.is_enabled(fork.spec_id()) { - break; - } - if self.mega_fork_activation(*fork).active_at_timestamp(timestamp) { - floor = floor.max(fork.spec_id()); - if introduces_spec { - break; - } - } - } - floor + // With the 1:1 ascending fork->spec map, the latest activated fork IS the maximum: + // the floor coincides with `spec_id` on every schedule. Kept as an alias for the + // transition; call sites can migrate to `spec_id` + `reaches`. + self.spec_id(timestamp) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::MINI_REX`], the /// spec introduced by [`MegaHardfork::MiniRex`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::MINI_REX) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::MINI_REX) } /// Returns `true` if the [`MegaHardfork::MiniRex1`] activation event has occurred at the @@ -255,49 +237,49 @@ pub trait MegaHardforks: OpHardforks { /// introduced by [`MegaHardfork::Rex`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX1`], the spec /// introduced by [`MegaHardfork::Rex1`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX1) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX1) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX2`], the spec /// introduced by [`MegaHardfork::Rex2`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX2) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX2) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX3`], the spec /// introduced by [`MegaHardfork::Rex3`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX3) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX3) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX4`], the spec /// introduced by [`MegaHardfork::Rex4`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX4) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX4) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX5`], the spec /// introduced by [`MegaHardfork::Rex5`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX5) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX5) } /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX6`], the spec /// introduced by [`MegaHardfork::Rex6`]. Floor-projected — 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.max_activated_spec_id(timestamp).is_enabled(MegaSpecId::REX6) + self.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX6) } /// Checks the schedule for well-formedness, i.e., that it describes a chain climbing the @@ -364,16 +346,6 @@ pub trait MegaHardforks: OpHardforks { } } - for fork in MegaHardfork::VARIANTS { - if fork.introduces_spec() || !scheduled(*fork) { - continue; - } - let base = fork.base_fork().expect("a patch fork always has an earlier base"); - if !scheduled(base) { - return Err(ScheduleError::OrphanPatch { patch: *fork, base }); - } - } - if scheduled(MegaHardfork::Rex5) && self.fork_params::().is_none() { return Err(ScheduleError::MissingParams { @@ -420,15 +392,6 @@ pub enum ScheduleError { /// A scheduled fork whose spec is equal or higher. scheduled: MegaHardfork, }, - /// A patch fork is scheduled while the spec-introducing fork it patches is not — a - /// rollback or restoration of a fork that never happened. - #[display("patch hardfork {patch:?} is scheduled but its base {base:?} is not")] - OrphanPatch { - /// The scheduled patch fork. - patch: MegaHardfork, - /// The unscheduled spec-introducing fork it patches. - base: 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")] @@ -563,15 +526,12 @@ impl MegaHardforkConfig { /// entry and are not restored by a later climb back up — re-attach them with /// [`with_params`](Self::with_params). /// - /// Patch hardforks ride along with the fork they patch: `MiniRex1`/`MiniRex2` are - /// registered exactly when `MiniRex` is. A patch without its base would schedule the - /// rollback of a fork that never happened — the shape `validate_schedule` rejects as - /// [`ScheduleError::OrphanPatch`] — so `with_all_activated_through(EQUIVALENCE)` registers - /// no fork at all and resolves to `EQUIVALENCE` by default. + /// 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 { - let base_included = fork.base_fork().is_none_or(|base| spec.is_enabled(base.spec_id())); - if spec.is_enabled(fork.spec_id()) && base_included { + if spec.reaches(fork.spec_id()) { self.insert(*fork, ForkCondition::Timestamp(0)); } else { self = self.without(*fork); @@ -685,8 +645,8 @@ mod tests { // Note: MiniRex1 and MiniRex2 are patch hardforks that reverted 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), @@ -844,11 +804,12 @@ mod tests { assert!(rollback_start < rollback_end, "the rollback window must be non-empty"); for ts in [rollback_start, rollback_start + 1, rollback_end - 1] { - assert_eq!(hf.spec_id(ts), MegaSpecId::EQUIVALENCE, "executing spec rolls back"); - assert_eq!(hf.max_activated_spec_id(ts), MegaSpecId::MINI_REX, "floor stays monotone"); - // Gating setup on the executing spec would drop the MiniRex predeploys here. - assert!(!hf.spec_id(ts).is_enabled(MegaSpecId::MINI_REX)); - assert!(hf.max_activated_spec_id(ts).is_enabled(MegaSpecId::MINI_REX)); + 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)); } } @@ -882,7 +843,7 @@ mod tests { continue; } assert_eq!( - floor.is_enabled(fork.spec_id()), + floor.reaches(fork.spec_id()), hf.mega_fork_activation(*fork).active_at_timestamp(ts), "floor disagrees with per-fork activation for {fork:?} at ts={ts}" ); @@ -1153,20 +1114,21 @@ mod tests { ); } - /// Scheduling a patch fork without the fork it patches is the rollback of a fork that - /// never happened. Execution resolves it harmlessly (the spec stays `EQUIVALENCE`), but a - /// published schedule with this shape is a configuration mistake. + /// 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_orphan_patch() { + 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::EQUIVALENCE, "execution itself is unaffected"); + 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::OrphanPatch { - patch: MegaHardfork::MiniRex1, - base: MegaHardfork::MiniRex + Err(ScheduleError::SkippedRung { + missing: MegaHardfork::MiniRex, + scheduled: MegaHardfork::MiniRex1 }) ); } diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 03c35baf..3bc4b277 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -180,7 +180,12 @@ impl core::fmt::Debug for MegaInstructi impl MegaInstructions { /// Create a new `MegaethInstructions` with the given spec id. pub fn new(spec: MegaSpecId) -> Self { - let instruction_table = match spec { + // Dispatch on the BEHAVIOR: alias specs execute their target's tables. `behavior()` + // never returns an alias, so the alias arms below are unreachable by construction. + let instruction_table = match spec.behavior() { + MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { + unreachable!("behavior() projects aliases to their targets") + } MegaSpecId::EQUIVALENCE => EthInstructions::new_mainnet(), MegaSpecId::MINI_REX => EthInstructions::new(mini_rex::instruction_table::< EthInterpreter, diff --git a/crates/mega-evm/src/evm/limit.rs b/crates/mega-evm/src/evm/limit.rs index 581f46fa..b0f11b27 100644 --- a/crates/mega-evm/src/evm/limit.rs +++ b/crates/mega-evm/src/evm/limit.rs @@ -21,7 +21,10 @@ pub struct EvmTxRuntimeLimits { impl EvmTxRuntimeLimits { /// Creates a new `TxLimits` instance from the given `MegaSpecId`. pub fn from_spec(spec: MegaSpecId) -> Self { - match spec { + match spec.behavior() { + MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { + unreachable!("behavior() projects aliases to their targets") + } MegaSpecId::EQUIVALENCE => Self::equivalence(), MegaSpecId::MINI_REX => Self::mini_rex(), MegaSpecId::REX | MegaSpecId::REX1 | MegaSpecId::REX2 => Self::rex(), diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index cf112153..e77070fe 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -38,7 +38,10 @@ impl MegaPrecompiles { #[inline] pub fn new_with_spec(spec: MegaSpecId) -> Self { // Get base precompiles from op-revm - let inner = match spec { + let inner = match spec.behavior() { + MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { + unreachable!("behavior() projects aliases to their targets") + } MegaSpecId::EQUIVALENCE => op_revm::precompiles::isthmus(), MegaSpecId::MINI_REX => mini_rex(), MegaSpecId::REX | diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index c9cb03c3..e24ada2d 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -39,6 +39,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`. @@ -63,6 +69,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. @@ -90,6 +100,8 @@ impl MegaSpecId { pub const ALL: &'static [Self] = &[ Self::EQUIVALENCE, Self::MINI_REX, + Self::MINI_REX_1, + Self::MINI_REX_2, Self::REX, Self::REX1, Self::REX2, @@ -108,6 +120,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 | @@ -119,12 +133,33 @@ impl MegaSpecId { } } - /// Returns `true` if `other` is enabled under `self` — i.e. `other` is at or below `self` - /// in [`SpecId`] order. - /// - /// 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 behavior this spec executes: alias specs project to the spec whose behavior they + /// reuse; every other spec is its own behavior. + 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. + pub const fn is_alias(self) -> bool { + matches!(self, Self::MINI_REX_1 | Self::MINI_REX_2) + } + + /// 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 } } @@ -138,13 +173,15 @@ const fn ladder_index(spec: MegaSpecId) -> usize { match spec { MegaSpecId::EQUIVALENCE => 0, MegaSpecId::MINI_REX => 1, - MegaSpecId::REX => 2, - MegaSpecId::REX1 => 3, - MegaSpecId::REX2 => 4, - MegaSpecId::REX3 => 5, - MegaSpecId::REX4 => 6, - MegaSpecId::REX5 => 7, - MegaSpecId::REX6 => 8, + 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, } } @@ -176,6 +213,8 @@ impl From for &'static str { 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, @@ -195,6 +234,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), @@ -232,9 +273,11 @@ impl Display for MegaSpecId { mod tests { use super::*; - const ALL_SPECS: [(MegaSpecId, &str); 9] = [ + const ALL_SPECS: [(MegaSpecId, &str); 11] = [ (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), diff --git a/crates/mega-evm/src/system/control.rs b/crates/mega-evm/src/system/control.rs index 4479d274..2b6f1979 100644 --- a/crates/mega-evm/src/system/control.rs +++ b/crates/mega-evm/src/system/control.rs @@ -61,7 +61,7 @@ pub fn transact_deploy_access_control_contract( /// `spec` is the activated-spec floor — see /// [`oracle_spec`](crate::system::oracle::oracle_spec). pub(crate) fn access_control_spec(spec: MegaSpecId) -> Option { - spec.is_enabled(MegaSpecId::REX4).then(|| { + 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 12a69c87..4a6eaf9c 100644 --- a/crates/mega-evm/src/system/deploy.rs +++ b/crates/mega-evm/src/system/deploy.rs @@ -328,12 +328,12 @@ mod tests { panic!("mainnet must schedule MiniRex1 by timestamp"); }; - assert_eq!(hf.spec_id(ts), crate::MegaSpecId::EQUIVALENCE); - assert!( - flat_system_contract_specs_for(hf.spec_id(ts)).is_empty(), - "executing spec would drop both predeploys in the rollback window" - ); + 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), diff --git a/crates/mega-evm/src/system/keyless_deploy.rs b/crates/mega-evm/src/system/keyless_deploy.rs index d5e35615..b2edc389 100644 --- a/crates/mega-evm/src/system/keyless_deploy.rs +++ b/crates/mega-evm/src/system/keyless_deploy.rs @@ -59,7 +59,7 @@ pub fn transact_deploy_keyless_deploy_contract( /// `spec` is the activated-spec floor — see /// [`oracle_spec`](crate::system::oracle::oracle_spec). pub(crate) fn keyless_deploy_spec(spec: MegaSpecId) -> Option { - spec.is_enabled(MegaSpecId::REX2).then(|| { + spec.reaches(MegaSpecId::REX2).then(|| { SystemContractSpec::new( KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE, diff --git a/crates/mega-evm/src/system/limit_control.rs b/crates/mega-evm/src/system/limit_control.rs index 24094601..9d72338f 100644 --- a/crates/mega-evm/src/system/limit_control.rs +++ b/crates/mega-evm/src/system/limit_control.rs @@ -40,7 +40,7 @@ pub fn transact_deploy_limit_control_contract( /// `spec` is the activated-spec floor — see /// [`oracle_spec`](crate::system::oracle::oracle_spec). pub(crate) fn limit_control_spec(spec: MegaSpecId) -> Option { - spec.is_enabled(MegaSpecId::REX4).then(|| { + 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 d18f4e30..d8c201e8 100644 --- a/crates/mega-evm/src/system/oracle.rs +++ b/crates/mega-evm/src/system/oracle.rs @@ -62,7 +62,7 @@ pub fn transact_deploy_oracle_contract( /// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)), not the executing /// spec: an Oracle already installed under `MINI_REX` stays installed through a spec rollback. pub(crate) fn oracle_spec(spec: MegaSpecId) -> Option { - if !spec.is_enabled(MegaSpecId::MINI_REX) { + if !spec.reaches(MegaSpecId::MINI_REX) { return None; } @@ -70,10 +70,10 @@ pub(crate) fn oracle_spec(spec: MegaSpecId) -> Option { // - 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 = spec.is_enabled(MegaSpecId::REX5); + 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 spec.is_enabled(MegaSpecId::REX2) { + } else if spec.reaches(MegaSpecId::REX2) { (ORACLE_CONTRACT_CODE_REX2, ORACLE_CONTRACT_CODE_HASH_REX2) } else { (ORACLE_CONTRACT_CODE, ORACLE_CONTRACT_CODE_HASH) @@ -119,7 +119,7 @@ pub fn transact_deploy_high_precision_timestamp_oracle( /// /// `spec` is the activated-spec floor — see [`oracle_spec`]. pub(crate) fn high_precision_timestamp_oracle_spec(spec: MegaSpecId) -> Option { - spec.is_enabled(MegaSpecId::MINI_REX).then(|| { + spec.reaches(MegaSpecId::MINI_REX).then(|| { SystemContractSpec::new( HIGH_PRECISION_TIMESTAMP_ORACLE_ADDRESS, HIGH_PRECISION_TIMESTAMP_ORACLE_CODE, diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 9088fb75..a9f3cba1 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -214,7 +214,7 @@ pub(crate) fn transact_deploy_sequencer_registry_for( // Gate and bytecode selection follow the activated-spec floor, 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.is_enabled(crate::MegaSpecId::REX5) { + if !spec.reaches(crate::MegaSpecId::REX5) { return Ok(None); } @@ -228,7 +228,7 @@ pub(crate) fn transact_deploy_sequencer_registry_for( // 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 = spec.is_enabled(crate::MegaSpecId::REX6); + 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 { @@ -410,37 +410,22 @@ where /// - Pre-REX5: returns `(MEGA_SYSTEM_ADDRESS, None)`. /// - REX5: reads `_currentSystemAddress` from committed registry storage. /// -/// The two spec arguments answer different questions and must not be collapsed into one: -/// -/// - `exec_spec` — the executing spec, gating whether dynamic system-address resolution applies at -/// all. This is execution semantics: it decides how a transaction is classified, so a spec -/// rollback below REX5 must return [`MEGA_SYSTEM_ADDRESS`] again. -/// - `setup_spec` — the activated-spec floor, selecting which registry bytecode version is expected -/// to be installed. This must match what [`transact_deploy_sequencer_registry`] installed, which -/// is floor-gated because a deployed contract is not un-deployed by a rollback. -/// -/// The two agree on every canonical schedule — no hardfork scheduled after Rex5's activation -/// rolls the spec back below `REX5` (mainnet's `MiniRex1` maps below it, but activates before -/// Rex5, keeping both sides below `REX5`) — so this split is currently inert; it exists so that -/// adding such a rollback hardfork cannot silently change transaction classification or produce -/// a spurious code-hash mismatch. +/// 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( hardforks: impl MegaHardforks, - exec_spec: crate::MegaSpecId, - setup_spec: crate::MegaSpecId, + spec: crate::MegaSpecId, db: &mut State, ) -> Result<(Address, Option), BlockExecutionError> { - // The floor is a maximum over activated forks and the executing spec is one of them, so - // `setup_spec >= exec_spec` always. A swapped argument pair satisfies the type system but - // trips this in every rollback-window test. - debug_assert!( - setup_spec.is_enabled(exec_spec), - "setup_spec ({setup_spec:?}) below exec_spec ({exec_spec:?}) — arguments swapped?" - ); - if !exec_spec.is_enabled(crate::MegaSpecId::REX5) { + // 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)); } @@ -466,7 +451,7 @@ pub fn resolve_system_address( // follows the activated-spec floor, 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 setup_spec.is_enabled(crate::MegaSpecId::REX6) { + let expected_code_hash = if spec.reaches(crate::MegaSpecId::REX6) { SEQUENCER_REGISTRY_CODE_HASH_REX6 } else { SEQUENCER_REGISTRY_CODE_HASH @@ -999,13 +984,8 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, witness) = resolve_system_address( - &rex6_hardforks(), - MegaSpecId::REX6, - MegaSpecId::REX6, - &mut state, - ) - .unwrap(); + let (addr, witness) = + resolve_system_address(&rex6_hardforks(), MegaSpecId::REX6, &mut state).unwrap(); assert_eq!(addr, TEST_SYSTEM_ADDRESS); assert!(witness.is_some()); } @@ -1031,13 +1011,8 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let err = resolve_system_address( - &rex6_hardforks(), - MegaSpecId::REX6, - MegaSpecId::REX6, - &mut state, - ) - .expect_err("V1 code hash at REX6 must fail closed"); + let err = resolve_system_address(&rex6_hardforks(), MegaSpecId::REX6, &mut state) + .expect_err("V1 code hash at REX6 must fail closed"); assert!(err.to_string().contains("code hash mismatch")); } @@ -1046,13 +1021,9 @@ mod tests { let mut db = InMemoryDB::default(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, _) = resolve_system_address( - MegaHardforkConfig::default(), - MegaSpecId::REX4, - MegaSpecId::REX4, - &mut state, - ) - .unwrap(); + let (addr, _) = + resolve_system_address(MegaHardforkConfig::default(), MegaSpecId::REX4, &mut state) + .unwrap(); assert_eq!(addr, MEGA_SYSTEM_ADDRESS); } @@ -1075,13 +1046,8 @@ mod tests { .unwrap(); let mut state = State::builder().with_database(&mut db).build(); - let (addr, witness) = resolve_system_address( - &rex5_hardforks(), - MegaSpecId::REX5, - MegaSpecId::REX5, - &mut state, - ) - .unwrap(); + let (addr, witness) = + resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state).unwrap(); assert_eq!(addr, TEST_SYSTEM_ADDRESS); // Witness must capture the registry account and the CURRENT_SYSTEM_ADDRESS slot. @@ -1111,12 +1077,7 @@ mod tests { ); let mut state = State::builder().with_database(&mut db).build(); - let result = resolve_system_address( - &rex5_hardforks(), - MegaSpecId::REX5, - MegaSpecId::REX5, - &mut state, - ); + let result = resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state); assert!(result.is_err(), "zero _currentSystemAddress should be an error"); } @@ -1126,9 +1087,8 @@ mod tests { let mut state = State::builder().with_database(&mut db).build(); let hardforks = rex5_hardforks(); - let err = - resolve_system_address(&hardforks, MegaSpecId::REX5, MegaSpecId::REX5, &mut state) - .expect_err("missing registry at Rex5 must fail closed"); + let err = resolve_system_address(&hardforks, MegaSpecId::REX5, &mut state) + .expect_err("missing registry at Rex5 must fail closed"); assert!(err.to_string().contains("does not exist")); } @@ -1145,13 +1105,8 @@ mod tests { ); let mut state = State::builder().with_database(&mut db).build(); - let err = resolve_system_address( - &rex5_hardforks(), - MegaSpecId::REX5, - MegaSpecId::REX5, - &mut state, - ) - .expect_err("wrong code hash must fail closed"); + let err = resolve_system_address(&rex5_hardforks(), MegaSpecId::REX5, &mut state) + .expect_err("wrong code hash must fail closed"); assert!(err.to_string().contains("code hash mismatch")); } diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs index 0141af9f..0468c3d3 100644 --- a/crates/mega-evm/tests/block_executor/partial_ladder.rs +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -289,8 +289,10 @@ fn test_rollback_window_still_emits_predeploy_witness_entries() { else { panic!("mainnet must schedule MiniRex1 by timestamp"); }; - // Precondition: inside the window the executing spec really has rolled back. - assert_eq!(chain_spec.spec_id(timestamp), MegaSpecId::EQUIVALENCE); + // 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); @@ -311,7 +313,7 @@ fn test_rollback_window_still_emits_predeploy_witness_entries() { let mut executor = block_executor_factory.create_executor( &mut state, block_ctx(), - create_evm_env(MegaSpecId::EQUIVALENCE, timestamp), + create_evm_env(MegaSpecId::MINI_REX_1, timestamp), ); let recorder = RecordingStateHook::default(); From 3e895d2336d3df53c31e8befa0fd9136ac6378db Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 11:13:48 +0800 Subject: [PATCH 13/37] prototype: cleanup --- crates/mega-evm/src/block/hardfork.rs | 7 ------- crates/mega-evm/src/evm/spec.rs | 8 ++++---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 2e4fd0a0..02368e52 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -45,13 +45,6 @@ impl MegaHardfork { !self.spec_id().is_alias() } - fn declaration_index(self) -> usize { - Self::VARIANTS - .iter() - .position(|fork| *fork == self) - .expect("every MegaHardfork is in VARIANTS") - } - /// Gets the `MegaSpecId` associated with this hardfork. #[allow(clippy::match_same_arms)] pub fn spec_id(&self) -> MegaSpecId { diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index e24ada2d..2a181e0a 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -39,10 +39,10 @@ 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 + /// 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 + /// 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`. @@ -69,9 +69,9 @@ 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. + /// The string identifier for the `MiniRex1` alias spec. pub const MINI_REX_1: &str = "MiniRex1"; - /// The string identifier for the *MiniRex2* alias spec. + /// 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"; From f5ceaebc8b36d1914707eee2ea60ea96db27e5a1 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 11:13:48 +0800 Subject: [PATCH 14/37] docs: align vocabulary with alias specs and dual projections behavior()/is_enabled gates semantics, reaches gates one-way setup; alias rungs MINI_REX_1/MINI_REX_2 replace the patch-fork mapping. Adds the Rex7 predicate probe and repoints mutation line refs. --- AGENTS.md | 5 +- crates/mega-evm/src/block/AGENTS.md | 8 +- crates/mega-evm/src/block/executor.rs | 30 ++-- crates/mega-evm/src/block/hardfork.rs | 155 +++++++----------- crates/mega-evm/src/system/AGENTS.md | 4 +- crates/mega-evm/src/system/control.rs | 2 +- crates/mega-evm/src/system/deploy.rs | 2 +- crates/mega-evm/src/system/keyless_deploy.rs | 2 +- crates/mega-evm/src/system/limit_control.rs | 2 +- crates/mega-evm/src/system/oracle.rs | 9 +- .../mega-evm/src/system/sequencer_registry.rs | 6 +- .../tests/block_executor/partial_ladder.rs | 9 +- crates/mega-evm/tests/mutation/block.rs | 31 ++-- crates/mega-evm/tests/rex4/deployment.rs | 3 +- docs/spec/hardfork-spec.md | 26 +-- 15 files changed, 134 insertions(+), 160 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 12ae116e..bd394e9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,8 +82,9 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R 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. - Two resolved specs come out of a config and must not be confused: `spec_id` is the reversible executing spec (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates EVM behavior; `max_activated_spec_id` is the monotone activated-spec floor and gates one-way chain setup such as predeploys and pre-block system calls. - The `is__active_at_timestamp` predicates for spec-introducing forks are projections of that floor (raw activation events are answered by `mega_fork_activation`), and `MegaHardforks::validate_schedule` is the load-time check that a published schedule climbs the ladder without gaps. + 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. diff --git a/crates/mega-evm/src/block/AGENTS.md b/crates/mega-evm/src/block/AGENTS.md index 7215d66e..86d8d896 100644 --- a/crates/mega-evm/src/block/AGENTS.md +++ b/crates/mega-evm/src/block/AGENTS.md @@ -26,10 +26,10 @@ Block execution orchestration for MegaETH, including hardfork-to-spec resolution - 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. -- Gate on a resolved spec value (`spec.is_enabled(MegaSpecId::X)`), resolved once from the block timestamp. The `is__active_at_timestamp` predicates for spec-introducing forks are projections of the activated-spec floor — gating on them is therefore additive-by-construction too, but they cannot express the executing spec, and only `mega_fork_activation` answers whether a fork's activation event itself was scheduled (the patch-fork predicates `is_mini_rex_1/2_active_at_timestamp` stay event queries for exactly that reason). -- 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 — the floor keeps setup 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`. -- Pick the right one of the two resolved specs. `spec_id` is reversible (a patch hardfork may map back to an earlier spec, as `MiniRex1` does) and gates execution semantics: EVM behavior, block limits, the executor's spec-coherence assert, transaction classification. `max_activated_spec_id` is monotone and gates one-way chain setup: system-contract predeploys, pre-block system calls, expected installed bytecode versions. A spec rollback does not un-deploy a predeploy, so gating setup on `spec_id` would retract it for the duration of the rollback window — and with it the read-only witness entries 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 both the executing spec and the activated-spec floor stay at the top of the ladder. Use `with_all_activated_through(MegaSpecId::N)`. +- 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/executor.rs b/crates/mega-evm/src/block/executor.rs index 75891eca..9d08aa2b 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -58,11 +58,11 @@ pub struct MegaBlockExecutor { receipt_builder: R, ctx: MegaBlockExecutionCtx, system_caller: SystemCaller, - /// The activated-spec floor for this block's timestamp, resolved once at construction. + /// The scheduled spec for this block's timestamp, resolved once at construction. /// - /// Every pre-block setup gate derives from this single value, which is what keeps setup - /// additive by construction. It is deliberately NOT the executing spec: see - /// [`MegaHardforks::max_activated_spec_id`]. + /// Every pre-block setup gate derives from this single value through `reaches` (position), + /// which keeps setup additive by construction and immune to alias windows — an alias rung + /// rolls back behavior, not the setup below it. /// /// Cached because the block env is fixed for an executor's lifetime — the constructor /// already reads `block().timestamp` for its hardfork-coherence asserts. @@ -187,15 +187,12 @@ where // clear flag to true. self.evm.db_mut().set_state_clear_flag(true); - // Every pre-block gate below derives from the one floor resolved at construction, so - // setup stays additive by construction: a config that schedules only a later fork still - // gets every earlier fork's predeploys and fail-closed checks. - // - // This is the activated-spec floor, NOT `spec_id(block_timestamp)`. The two differ - // whenever a patch hardfork rolls the spec back (`MiniRex1` -> `EQUIVALENCE`, live on - // mainnet), and pre-block setup is one-way: a rollback does not un-deploy a predeploy. - // Gating setup on the reversible spec would drop the Oracle predeploys — and their - // read-only witness entries — from every block in such a window. + // Every pre-block gate below derives from the one scheduled spec resolved at + // construction, 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. let setup_spec = self.setup_spec; let is_rex_5 = setup_spec.reaches(MegaSpecId::REX5); @@ -692,10 +689,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 executing spec gates whether dynamic resolution applies (semantics); the - // activated-spec floor selects the expected registry bytecode version, matching what - // the floor-gated pre-block deploy installed. + // 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 bfcb7a94..9525d51d 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -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, @@ -50,8 +50,8 @@ impl MegaHardfork { /// 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. + // 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::MINI_REX_1, @@ -97,23 +97,21 @@ pub trait HardforkParams: Any + core::fmt::Debug + Send + Sync { /// Extends [`OpHardforks`] with `MegaETH` helper methods. /// -/// Everything derives from one source of truth — the raw activation events reported by -/// [`mega_fork_activation`](Self::mega_fork_activation) — in three layers: +/// 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: /// -/// - [`hardfork`](Self::hardfork) / [`spec_id`](Self::spec_id) — the *executing* resolution: the -/// latest-declared activated fork and its spec. Reversible: a rollback patch fork moves the -/// executing spec back down. -/// - [`max_activated_spec_id`](Self::max_activated_spec_id) — the *activated-spec floor*: the -/// highest spec any activated fork introduced. Monotone across rollbacks. -/// - The `is__active_at_timestamp` convenience predicates. For **spec-introducing** forks -/// these are projections of the floor: they answer "has the chain reached this fork's spec", not -/// "was this fork itself scheduled", so gating on them stays additive on a schedule that omits a -/// predecessor and monotone across spec rollbacks. For **patch** forks (`MiniRex1`, `MiniRex2`), -/// which introduce no new spec and are not recoverable from a spec ordinal, the predicates remain -/// raw event queries. +/// - `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. /// -/// 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 +/// 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 { @@ -143,9 +141,9 @@ pub trait MegaHardforks: OpHardforks { /// [`MegaHardfork::VARIANTS`] so a newly declared fork joins resolution without a second /// hand-written ladder here. /// - /// This is the *executing* resolution: it reads raw activation events - /// ([`mega_fork_activation`](Self::mega_fork_activation)), not the activated-spec floor, so a - /// rollback patch fork like `MiniRex1` correctly takes over from the fork it patches. + /// 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() @@ -160,49 +158,22 @@ pub trait MegaHardforks: OpHardforks { } /// Returns the highest [`MegaSpecId`] among all [`MegaHardfork`]s activated at or before - /// `timestamp`. + /// `timestamp` — which, under the 1:1 ascending fork->spec map, is exactly + /// [`spec_id`](Self::spec_id). /// - /// This differs from [`spec_id`](Self::spec_id) only when a patch hardfork maps to an earlier - /// spec, as `MiniRex1` does (it rolls back to `EQUIVALENCE`). The two answer different - /// questions: - /// - /// - `spec_id` — *which EVM semantics execute in this block*. Reversible: a rollback hardfork - /// moves it back down, and it must stay the gate for execution behavior and block limits. - /// - This method — *which chain-setup features have ever been activated*. Monotone: a spec - /// rollback does not un-deploy a predeploy or retract a pre-block system call, so one-way - /// setup must be gated on this instead. - /// - /// Deriving setup gates from this single value keeps them additive by construction: a config - /// that schedules only a late fork still gets every earlier fork's setup, matching the ordinal - /// inclusion the EVM layer already relies on. - /// - /// For *spec-introducing* forks — those whose spec is strictly higher than every earlier - /// fork's — the `is_*_active_at_timestamp` predicates are projections of this floor, and on - /// well-formed ladders the floor coincides with each such fork's raw activation event. - /// `MiniRex1` (rollback) and `MiniRex2` (restoration) introduce no new spec and are therefore - /// not recoverable from a spec ordinal; their predicates stay raw event queries. - /// - /// Like `spec_id` and [`hardfork`](Self::hardfork), this is timestamp-scoped: a `MegaHardfork` - /// registered with [`ForkCondition::Block`] or [`ForkCondition::TTD`] never reports active - /// here. Every `MegaHardfork` in the canonical schedules uses `Timestamp` or `Never`. + /// Kept as an alias so call sites can state "one-way setup" intent explicitly; pair it with + /// [`MegaSpecId::reaches`] (position) rather than `is_enabled` (behavior). fn max_activated_spec_id(&self, timestamp: BlockTimestamp) -> MegaSpecId { - // Descending scan with early exit: a spec-introducing fork's spec is the highest any - // fork declared at or before it maps to, so the scan stops at the first activated - // spec-introducing fork (or once the running floor already covers everything earlier). - // The floor for a chain near the top of the ladder — every real query — thus costs one - // or two activation lookups, not one per fork; only the `mega_fork_activation` calls - // are bounded, the `introduces_spec` prefix check is enum-ordinal arithmetic. - // `test_floor_early_exit_matches_naive_reference` pins this against the plain - // max-over-activated-forks formula. // With the 1:1 ascending fork->spec map, the latest activated fork IS the maximum: // the floor coincides with `spec_id` on every schedule. Kept as an alias for the // transition; call sites can migrate to `spec_id` + `reaches`. self.spec_id(timestamp) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::MINI_REX`], the - /// spec introduced by [`MegaHardfork::MiniRex`]. Floor-projected — see the trait docs; for - /// the raw activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::MINI_REX) } @@ -210,10 +181,10 @@ pub trait MegaHardforks: OpHardforks { /// Returns `true` if the [`MegaHardfork::MiniRex1`] activation event has occurred at the /// given block timestamp. /// - /// `MiniRex1` is a patch hardfork: it introduces no new spec (it rolls the executing spec - /// back to [`MegaSpecId::EQUIVALENCE`]), so it is not recoverable from a spec ordinal and - /// this predicate stays a raw event query, unlike the floor-projected spec-introducing - /// predicates. + /// `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) } @@ -221,64 +192,63 @@ pub trait MegaHardforks: OpHardforks { /// Returns `true` if the [`MegaHardfork::MiniRex2`] activation event has occurred at the /// given block timestamp. /// - /// `MiniRex2` is a patch hardfork: it introduces no new spec (it restores - /// [`MegaSpecId::MINI_REX`] after the `MiniRex1` rollback), so it is not recoverable from a - /// spec ordinal and this predicate stays a raw event query, unlike the floor-projected - /// spec-introducing predicates. + /// `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` once the activated-spec floor has reached [`MegaSpecId::REX`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX`], the rung /// introduced by [`MegaHardfork::Rex`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX1`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX1`], the rung /// introduced by [`MegaHardfork::Rex1`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX1) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX2`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX2`], the rung /// introduced by [`MegaHardfork::Rex2`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX2) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX3`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX3`], the rung /// introduced by [`MegaHardfork::Rex3`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX3) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX4`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX4`], the rung /// introduced by [`MegaHardfork::Rex4`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX4) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX5`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX5`], the rung /// introduced by [`MegaHardfork::Rex5`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX5) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX6`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX6`], the rung /// introduced by [`MegaHardfork::Rex6`]. Floor-projected — 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX6) } - /// Returns `true` once the activated-spec floor has reached [`MegaSpecId::REX7`], the spec + /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX7`], the rung /// introduced by [`MegaHardfork::Rex7`]. Floor-projected — 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 { @@ -289,9 +259,9 @@ pub trait MegaHardforks: OpHardforks { /// 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 derive from the activated-spec floor, 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 + /// 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: @@ -301,10 +271,10 @@ pub trait MegaHardforks: OpHardforks { /// 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). + /// - 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 @@ -516,9 +486,8 @@ impl MegaHardforkConfig { /// 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. On top of the wrong - /// executing spec, the leftover later forks also keep the activated-spec floor - /// ([`max_activated_spec_id`](MegaHardforks::max_activated_spec_id)) high, so every pre-block - /// setup gate below them stays open. + /// resolved spec, the leftover later forks also keep the scheduled spec high, so every + /// pre-block setup gate below them stays open. /// /// 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 @@ -643,7 +612,7 @@ mod tests { #[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::MINI_REX_1), @@ -787,11 +756,11 @@ mod tests { MegaHardforkConfig::default().with_all_activated().with_params(AlwaysErrParams); } - /// Mainnet runs a real spec rollback: `MiniRex1` maps to `EQUIVALENCE` while `MiniRex`'s - /// timestamp has already passed. Inside that window the resolved spec and the activated-spec - /// floor disagree, and only the floor keeps one-way setup (the Oracle predeploys) enabled. + /// 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_separates_resolved_spec_from_floor() { + 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) @@ -816,9 +785,9 @@ mod tests { } } - /// The floor reproduces every per-fork activation predicate exactly, for every - /// *spec-introducing* fork, on every canonical schedule. This is what makes the switch a - /// no-op on well-formed ladders. + /// 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 spec — `MiniRex1` (rollback to `EQUIVALENCE`) and `MiniRex2` /// (restoration to `MINI_REX`) — are not recoverable from a spec ordinal by construction, so @@ -880,7 +849,7 @@ mod tests { } /// `with_all_activated_through` is the well-formed way to express "a chain running spec N": - /// both the executing spec and the activated-spec floor resolve to exactly `N`, at any + /// the resolved spec is exactly `N` under both projections, 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] @@ -1004,7 +973,7 @@ mod tests { crate::testnet_hardforks(), crate::all_activated_hardforks(), MegaHardforkConfig::new(), - // Partial ladders: a lone top rung, and a lone patch fork. + // 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. diff --git a/crates/mega-evm/src/system/AGENTS.md b/crates/mega-evm/src/system/AGENTS.md index ff789a8d..dd2ad644 100644 --- a/crates/mega-evm/src/system/AGENTS.md +++ b/crates/mega-evm/src/system/AGENTS.md @@ -15,8 +15,8 @@ System contract integration layer with canonical addresses, deployment transacti ## KEY PATTERNS - Deployment helpers are idempotent and keyed by code hash equality. -- Gating happens in each contract's `_spec()` builder, which takes a resolved `MegaSpecId` and gates on `spec.is_enabled(...)`. Nothing in the deploy layer takes a hardfork config — the spec builders and the crate-private `*_for` helpers receive the resolved floor 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. -- The spec passed to a builder is the **activated-spec floor** (`MegaHardforks::max_activated_spec_id`), not the executing spec. Predeploys are one-way: a hardfork that rolls the spec back does not un-deploy them, and it must not change which bytecode version is expected to be installed. +- 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 2b6f1979..8e80c92b 100644 --- a/crates/mega-evm/src/system/control.rs +++ b/crates/mega-evm/src/system/control.rs @@ -58,7 +58,7 @@ pub fn transact_deploy_access_control_contract( /// Builds the [`SystemContractSpec`] for the access-control contract active under /// `spec`, or `None` if `REX4` is not yet enabled. /// -/// `spec` is the activated-spec floor — see +/// `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(|| { diff --git a/crates/mega-evm/src/system/deploy.rs b/crates/mega-evm/src/system/deploy.rs index 4a6eaf9c..29fbfc87 100644 --- a/crates/mega-evm/src/system/deploy.rs +++ b/crates/mega-evm/src/system/deploy.rs @@ -150,7 +150,7 @@ pub fn flat_system_contract_specs( flat_system_contract_specs_for(hardforks.max_activated_spec_id(block_timestamp)) } -/// [`flat_system_contract_specs`] against an already-resolved activated-spec floor. +/// [`flat_system_contract_specs`] against an already-resolved scheduled spec. /// /// The block executor resolves the floor once per block /// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)) and calls this diff --git a/crates/mega-evm/src/system/keyless_deploy.rs b/crates/mega-evm/src/system/keyless_deploy.rs index b2edc389..37f820e6 100644 --- a/crates/mega-evm/src/system/keyless_deploy.rs +++ b/crates/mega-evm/src/system/keyless_deploy.rs @@ -56,7 +56,7 @@ pub fn transact_deploy_keyless_deploy_contract( /// Builds the [`SystemContractSpec`] for the keyless-deploy contract active under /// `spec`, or `None` if `REX2` is not yet enabled. /// -/// `spec` is the activated-spec floor — see +/// `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(|| { diff --git a/crates/mega-evm/src/system/limit_control.rs b/crates/mega-evm/src/system/limit_control.rs index 9d72338f..9ee96c03 100644 --- a/crates/mega-evm/src/system/limit_control.rs +++ b/crates/mega-evm/src/system/limit_control.rs @@ -37,7 +37,7 @@ pub fn transact_deploy_limit_control_contract( /// Builds the [`SystemContractSpec`] for the `MegaLimitControl` contract active /// under `spec`, or `None` if `REX4` is not yet enabled. /// -/// `spec` is the activated-spec floor — see +/// `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(|| { diff --git a/crates/mega-evm/src/system/oracle.rs b/crates/mega-evm/src/system/oracle.rs index d8c201e8..a3366090 100644 --- a/crates/mega-evm/src/system/oracle.rs +++ b/crates/mega-evm/src/system/oracle.rs @@ -37,7 +37,7 @@ 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 activated-spec floor: +/// The deployed bytecode depends on the scheduled spec: /// - Pre-Rex2: v1.0.0 bytecode (without `sendHint` function) /// - 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`) @@ -58,9 +58,8 @@ pub fn transact_deploy_oracle_contract( /// semantics — shared by [`transact_deploy_oracle_contract`] and the deploy /// registry ([`flat_system_contract_specs`](crate::flat_system_contract_specs)). /// -/// `spec` is the activated-spec floor -/// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)), not the executing -/// spec: an Oracle already installed under `MINI_REX` stays installed through a spec rollback. +/// `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; @@ -117,7 +116,7 @@ pub fn transact_deploy_high_precision_timestamp_oracle( /// Builds the [`SystemContractSpec`] for the high-precision timestamp Oracle /// active under `spec`, or `None` if `MINI_REX` is not yet enabled. /// -/// `spec` is the activated-spec floor — see [`oracle_spec`]. +/// `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( diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 80540396..266750e9 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -197,7 +197,7 @@ pub fn transact_deploy_sequencer_registry( transact_deploy_sequencer_registry_for(spec, rex6_config, current_block_number, db, config) } -/// [`transact_deploy_sequencer_registry`] against an already-resolved activated-spec floor. +/// [`transact_deploy_sequencer_registry`] against an already-resolved scheduled spec. /// /// The block executor resolves the floor once per block and calls this directly; the public /// wrapper above resolves it for callers that hold a hardfork config. Like the flat-registry @@ -211,7 +211,7 @@ pub(crate) fn transact_deploy_sequencer_registry_for( db: &mut State, config: &SequencerRegistryConfig, ) -> Result, BlockExecutionError> { - // Gate and bytecode selection follow the activated-spec floor, not per-fork registration: + // 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) { @@ -448,7 +448,7 @@ pub fn resolve_system_address( }; // Unreachable: deploy verifies the code hash before seeding storage. The expected version - // follows the activated-spec floor, matching what the pre-block deploy installed: the + // 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) { diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs index 0468c3d3..c26fb704 100644 --- a/crates/mega-evm/tests/block_executor/partial_ladder.rs +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -1,9 +1,10 @@ //! 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 **activated-spec floor** -//! (`MegaHardforks::max_activated_spec_id`), not on per-fork registration and not on the -//! reversible executing spec. These tests pin both directions of that choice: +//! `SequencerRegistry` bootstrap) is gated on the **scheduled spec** +//! (`MegaHardforks::max_activated_spec_id`, 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. @@ -139,7 +140,7 @@ 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 activated-spec floor, which a partial ladder keeps high by design. + // 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); diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index a7fbe17e..830a1bbe 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -212,16 +212,17 @@ fn staged_config() -> MegaHardforkConfig { .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, on a -/// complete ordered ladder (where the floor-projected predicates coincide with the activation +/// 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 spec-introducing predicate's body is -/// `max_activated_spec_id(t).is_enabled(MegaSpecId::X)`, so shifting `X` one rung either way is a +/// spec each one projects. A behavior-introducing predicate's body is +/// `max_activated_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. @@ -229,46 +230,50 @@ fn staged_config() -> MegaHardforkConfig { fn test_hardfork_activation_predicates_are_true_at_activation() { let cfg = staged_config(); - // MiniRex (hardfork.rs:229) — spec-introducing (MINI_REX). Shifting down lands on + // MiniRex (hardfork.rs:178) — 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:240) — patch fork, raw event query. + // MiniRex1 (hardfork.rs:189) — 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 (hardfork.rs:251) — patch fork, raw event query. + // MiniRex2 (hardfork.rs:199) — 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)); - // Rex (hardfork.rs:258). + // Rex (hardfork.rs:206). assert!(!cfg.is_rex_active_at_timestamp(399)); assert!(cfg.is_rex_active_at_timestamp(400)); - // Rex1 (hardfork.rs:265). + // Rex1 (hardfork.rs:213). assert!(!cfg.is_rex_1_active_at_timestamp(499)); assert!(cfg.is_rex_1_active_at_timestamp(500)); - // Rex2 (hardfork.rs:272). + // Rex2 (hardfork.rs:220). assert!(!cfg.is_rex_2_active_at_timestamp(599)); assert!(cfg.is_rex_2_active_at_timestamp(600)); - // Rex3 (hardfork.rs:279). + // Rex3 (hardfork.rs:227). assert!(!cfg.is_rex_3_active_at_timestamp(699)); assert!(cfg.is_rex_3_active_at_timestamp(700)); - // Rex4 (hardfork.rs:286). + // Rex4 (hardfork.rs:234). assert!(!cfg.is_rex_4_active_at_timestamp(799)); assert!(cfg.is_rex_4_active_at_timestamp(800)); - // Rex5 (hardfork.rs:293). + // Rex5 (hardfork.rs:241). assert!(!cfg.is_rex_5_active_at_timestamp(899)); assert!(cfg.is_rex_5_active_at_timestamp(900)); - // Rex6 (hardfork.rs:300). + // Rex6 (hardfork.rs:248). assert!(!cfg.is_rex_6_active_at_timestamp(999)); assert!(cfg.is_rex_6_active_at_timestamp(1000)); + + // Rex7 (hardfork.rs:255). + 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 dc983d48..0c339519 100644 --- a/crates/mega-evm/tests/rex4/deployment.rs +++ b/crates/mega-evm/tests/rex4/deployment.rs @@ -23,8 +23,7 @@ type TestExecutor<'a, 'db> = // 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 derive from the activated-spec floor — which would still report REX6 and open every -// lower gate. +// 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/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index fab65476..85dccbb4 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -19,24 +19,28 @@ 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). -### Executing Spec vs. Highest Spec Reached +### Alias Specs: Behavior vs. Position -Because a hardfork may roll the spec back, two distinct questions have to be answered separately at each block. +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`). -- **Executing spec** — which semantics the EVM applies in this block: the spec of the most recently activated hardfork. - This is reversible: during the `MiniRex1` window the executing spec is `EQUIVALENCE` again, and MegaEVM behaves accordingly. -- **Highest spec reached** — the greatest spec among all hardforks activated at or before this block. - This is monotone and never decreases, even across a rollback. +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 system calls remain in effect, even while a later hardfork rolls the executing semantics back to an earlier spec. +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 highest spec reached. -A node MUST determine all other behavior — opcode behavior, gas costs, resource limits, transaction classification — from the executing spec. +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. -A published hardfork schedule MUST climb the spec ladder rung by rung: a hardfork MUST NOT be scheduled unless every hardfork of a lower spec is scheduled, with two rules for patch hardforks — a network MAY omit a patch hardfork, but MUST NOT schedule a patch hardfork whose base hardfork is not scheduled. -Execution is additionally robust to a malformed schedule: because setup derives from the highest spec reached, a scheduled hardfork implies its predecessors' setup even if they were never scheduled. +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. From 04f1a31d29f508e6fa61a598f8a20fc514a8b075 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 11:13:48 +0800 Subject: [PATCH 15/37] refactor(spec): derive is_alias from the behavior projection --- crates/mega-evm/src/evm/spec.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 49f477a1..1a41f5ba 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -151,8 +151,10 @@ impl MegaSpecId { } /// 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 { - matches!(self, Self::MINI_REX_1 | Self::MINI_REX_2) + self.behavior() as u8 != self as u8 } /// Returns `true` if `other`'s BEHAVIOR is enabled under `self` — the gate for execution From 670590ef1b87b7cd9e59a553e666858835337491 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 11:18:26 +0800 Subject: [PATCH 16/37] docs: cover alias specs across glossary, spec pages, and mega-evme The 1:1 hardfork->spec statement replaces the many-to-one mapping in the hardfork-spec intro, glossary, and upgrade overview; the glossary gains an Alias Spec entry; the mega-evme spec table lists the two alias rungs now accepted by --spec. --- AGENTS.md | 4 ++-- docs/mega-evme/configuration/chain-and-spec.md | 2 ++ docs/spec/glossary.md | 9 ++++++++- docs/spec/hardfork-spec.md | 6 +++--- docs/spec/upgrades/overview.md | 4 ++-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bd394e9a..82bb3ce5 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` → `REX` → `REX1` → `REX2` → `REX3` → `REX4` → `REX5` → `REX6` → `REX7` (alias rungs `MINI_REX_1`/`MINI_REX_2` sit between `MINI_REX` and `REX`, executing earlier behaviors) - **Spec** defines EVM behavior (what the EVM does). Defined in `crates/mega-evm/src/evm/spec.rs`. @@ -69,7 +69,7 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R - Specifications of each spec can be found in the upgrade pages under `docs/spec/upgrades/`. - **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. + `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. 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/spec/glossary.md b/docs/spec/glossary.md index c094258d..4ec35452 100644 --- a/docs/spec/glossary.md +++ b/docs/spec/glossary.md @@ -164,6 +164,13 @@ 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`. +The [alias rungs](#alias-spec) `MINI_REX_1` and `MINI_REX_2` sit between `MINI_REX` and `REX`, 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 85dccbb4..677a47c7 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -15,9 +15,9 @@ 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 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) From 172d89e5aa79baa898e55eed0ab2661c6409a381 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 12:00:19 +0800 Subject: [PATCH 17/37] fix(state-test): add fixture name mappings for the alias specs --- crates/mega-state-test/src/types/spec.rs | 26 ++++++++++++++++++++++++ crates/state-test/src/main.rs | 2 ++ 2 files changed, 28 insertions(+) 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..3505dc62 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -249,6 +249,8 @@ mod tests { for (s, expected) in [ (mega_evm::name::EQUIVALENCE, SpecName::Equivalence), (mega_evm::name::MINI_REX, SpecName::MiniRex), + (mega_evm::name::MINI_REX_1, SpecName::MiniRex1), + (mega_evm::name::MINI_REX_2, SpecName::MiniRex2), (mega_evm::name::REX, SpecName::Rex), (mega_evm::name::REX1, SpecName::Rex1), (mega_evm::name::REX2, SpecName::Rex2), From 26f1e09cb4a83537ec97d6cd2b6aed5fd3f1c6be Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 12:00:19 +0800 Subject: [PATCH 18/37] docs(spec): show the alias rungs in the spec progression --- docs/spec/hardfork-spec.md | 14 ++++++++++++-- docs/spec/overview.md | 7 +++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index 677a47c7..d71828d3 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -48,10 +48,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. @@ -93,6 +94,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. From 4c580eb619ca47c9f91775cefe2f15c0859ab38e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 12:30:26 +0800 Subject: [PATCH 19/37] refactor(evm): dispatch alias specs by direct arm grouping, reconciled with behavior() by test --- crates/mega-evm/src/evm/instructions.rs | 51 ++++++++++++++++++++----- crates/mega-evm/src/evm/limit.rs | 35 ++++++++++++++--- crates/mega-evm/src/evm/precompiles.rs | 33 ++++++++++++---- 3 files changed, 96 insertions(+), 23 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index af89e4b0..40896d07 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -180,17 +180,17 @@ impl core::fmt::Debug for MegaInstructi impl MegaInstructions { /// Create a new `MegaethInstructions` with the given spec id. pub fn new(spec: MegaSpecId) -> Self { - // Dispatch on the BEHAVIOR: alias specs execute their target's tables. `behavior()` - // never returns an alias, so the alias arms below are unreachable by construction. - let instruction_table = match spec.behavior() { - MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { - unreachable!("behavior() projects aliases to their targets") + // 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 | MegaSpecId::MINI_REX_1 => EthInstructions::new_mainnet(), + MegaSpecId::MINI_REX | MegaSpecId::MINI_REX_2 => { + EthInstructions::new(mini_rex::instruction_table::< + EthInterpreter, + MegaContext, + >()) } - MegaSpecId::EQUIVALENCE => EthInstructions::new_mainnet(), - MegaSpecId::MINI_REX => EthInstructions::new(mini_rex::instruction_table::< - EthInterpreter, - MegaContext, - >()), MegaSpecId::REX | MegaSpecId::REX1 => EthInstructions::new(rex::instruction_table::< EthInterpreter, MegaContext, @@ -2350,3 +2350,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 da225e5e..b9111abb 100644 --- a/crates/mega-evm/src/evm/limit.rs +++ b/crates/mega-evm/src/evm/limit.rs @@ -21,12 +21,12 @@ pub struct EvmTxRuntimeLimits { impl EvmTxRuntimeLimits { /// Creates a new `TxLimits` instance from the given `MegaSpecId`. pub fn from_spec(spec: MegaSpecId) -> Self { - match spec.behavior() { - MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { - unreachable!("behavior() projects aliases to their targets") - } - MegaSpecId::EQUIVALENCE => Self::equivalence(), - MegaSpecId::MINI_REX => Self::mini_rex(), + // 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 | 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(), @@ -160,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 7bba7c60..5768ac14 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -37,13 +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 - let inner = match spec.behavior() { - MegaSpecId::MINI_REX_1 | MegaSpecId::MINI_REX_2 => { - unreachable!("behavior() projects aliases to their targets") - } - MegaSpecId::EQUIVALENCE => op_revm::precompiles::isthmus(), - MegaSpecId::MINI_REX => mini_rex(), + // 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 | MegaSpecId::MINI_REX_1 => op_revm::precompiles::isthmus(), + MegaSpecId::MINI_REX | MegaSpecId::MINI_REX_2 => mini_rex(), MegaSpecId::REX | MegaSpecId::REX1 | MegaSpecId::REX2 | @@ -294,6 +293,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(); From a711a5bdb486035ebb6fb5b4b21550b3dfc27fa0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 15:02:38 +0800 Subject: [PATCH 20/37] fix(evm): pass the behavior spec to the dyn precompiles builder --- crates/mega-evm/src/evm/factory.rs | 29 +++++++++++++++++++++++++- crates/mega-evm/src/evm/precompiles.rs | 5 +++++ 2 files changed, 33 insertions(+), 1 deletion(-) 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/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 5768ac14..d1bc95e4 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -271,6 +271,11 @@ impl PrecompileProvider HashMap + Send + Sync>; From 2db133f479288668518e835bc0a4cb5d3489e4e4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 15:02:39 +0800 Subject: [PATCH 21/37] test: pin the alias specs in the cross-spec compute-gas snapshot --- crates/mega-evm/tests/compute_gas/main.rs | 12 ++-- .../mega-evm/tests/compute_gas/snapshot.txt | 64 +++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) 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 From b420d60c3ec764cfb684e35e655fa489506eb26a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 15:02:39 +0800 Subject: [PATCH 22/37] refactor(hardfork): rename introduces_spec to introduces_behavior --- crates/mega-evm/src/block/hardfork.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 9525d51d..2d23184b 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -43,7 +43,7 @@ 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_spec(self) -> bool { + pub(crate) fn introduces_behavior(self) -> bool { !self.spec_id().is_alias() } @@ -306,7 +306,7 @@ pub trait MegaHardforks: OpHardforks { } for fork in MegaHardfork::VARIANTS { - if !fork.introduces_spec() || scheduled(*fork) { + if !fork.introduces_behavior() || scheduled(*fork) { continue; } if let Some(above) = MegaHardfork::VARIANTS @@ -789,12 +789,13 @@ mod tests { /// 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 spec — `MiniRex1` (rollback to `EQUIVALENCE`) and `MiniRex2` - /// (restoration to `MINI_REX`) — are not recoverable from a spec ordinal by construction, so - /// nothing may gate on them. The predicate is derived rather than hardcoded so a future - /// rollback fork is classified automatically. + /// 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_floor_matches_per_fork_activation_for_spec_introducing_forks() { + fn test_position_matches_per_fork_activation_for_behavior_introducing_forks() { for hf in [ crate::mainnet_hardforks(), crate::testnet_hardforks(), @@ -810,7 +811,7 @@ mod tests { for ts in stamps { let floor = hf.max_activated_spec_id(ts); for fork in MegaHardfork::VARIANTS { - if !fork.introduces_spec() { + if !fork.introduces_behavior() { continue; } assert_eq!( From bfc6420ba191f11b32867f3cb61ba3df46cf9a99 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 15:02:39 +0800 Subject: [PATCH 23/37] docs: complete alias-spec coverage across rosters, ranges, and CLI value lists --- AGENTS.md | 6 +++--- ARCH.md | 2 +- bin/mega-evme/README.md | 2 +- bin/mega-evme/src/common/env.rs | 5 +++-- bin/mega-t8n/src/cmd.rs | 5 +++-- crates/mega-evm/README.md | 6 +++--- crates/mega-evm/src/AGENTS.md | 2 +- crates/mega-evm/src/evm/instructions.rs | 4 +++- crates/mega-evm/src/evm/spec.rs | 2 ++ docs/mega-evme/commands/run.md | 2 +- docs/mega-evme/configuration/block-environment.md | 3 ++- docs/mega-evme/configuration/salt-buckets.md | 4 ++-- docs/spec/AGENTS.md | 1 + docs/spec/glossary.md | 4 ++-- docs/spec/hardfork-spec.md | 3 +++ 15 files changed, 31 insertions(+), 20 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 82bb3ce5..0cc33b0c 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` (alias rungs `MINI_REX_1`/`MINI_REX_2` sit between `MINI_REX` and `REX`, executing earlier behaviors) +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`. @@ -68,7 +68,7 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `REX` → `REX1` → `REX2` → `R 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/`. - **Hardfork** (`MegaHardfork`) defines network upgrade events (when specs activate). - Multiple hardforks can map to one spec. + 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. @@ -103,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: 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-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/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/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 40896d07..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. diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 1a41f5ba..2b5e17fa 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`] 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/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..de8f7489 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 diff --git a/docs/spec/glossary.md b/docs/spec/glossary.md index 4ec35452..226a02b9 100644 --- a/docs/spec/glossary.md +++ b/docs/spec/glossary.md @@ -163,8 +163,8 @@ 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`. -The [alias rungs](#alias-spec) `MINI_REX_1` and `MINI_REX_2` sit between `MINI_REX` and `REX`, executing `EQUIVALENCE` and `MINI_REX` behavior respectively. +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 diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index d71828d3..1fbd9623 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -39,6 +39,9 @@ A rollback changes how transactions execute; it does not un-deploy a contract or 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. From 2609c26ae83ac0a75ba155f91d49e808b1e6d9b3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 15:46:25 +0800 Subject: [PATCH 24/37] docs(spec): record that alias specs get no dedicated upgrade page --- AGENTS.md | 2 +- docs/spec/AGENTS.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0cc33b0c..8f003a0f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,7 @@ Progression: `EQUIVALENCE` → `MINI_REX` → `MINI_REX_1` → `MINI_REX_2` → - 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). 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. diff --git a/docs/spec/AGENTS.md b/docs/spec/AGENTS.md index de8f7489..e31de7a5 100644 --- a/docs/spec/AGENTS.md +++ b/docs/spec/AGENTS.md @@ -238,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: From 2c56c2d8c3a3c54ce48fc72a6a5a1acbf7c7c6fe Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 17:10:43 +0800 Subject: [PATCH 25/37] test: pin ladder positions and hardfork declaration order --- crates/mega-evm/src/block/hardfork.rs | 15 +++++++++++++++ crates/mega-evm/src/evm/spec.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 2d23184b..08667042 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -610,6 +610,21 @@ mod tests { use super::*; use crate::SequencerRegistryConfig; + #[test] + fn test_variants_declaration_order_climbs_the_ladder() { + // The reverse scan in `hardfork()` and the skipped-rung rule in `validate_schedule` + // assume declaration order maps to strictly ascending spec rungs; this fails at a + // misplaced variant instead of two derived tests away. + for pair in MegaHardfork::VARIANTS.windows(2) { + assert!( + (pair[0].spec_id() as u8) < (pair[1].spec_id() as u8), + "{:?} -> {:?} must climb the spec ladder", + pair[0], + pair[1], + ); + } + } + #[test] fn test_mega_hardfork_spec_ids_match_expected_specs() { // Note: MiniRex1 and MiniRex2 map to alias rungs whose behavior reverts to earlier specs. diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index 2b5e17fa..cfc40141 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -331,6 +331,32 @@ mod tests { /// The compile-time checker is itself exercised with malformed lists: the real `ALL` /// always satisfies the property, so only rejection cases can detect a weakened guard /// inside the checker. + #[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 here, + // where the review attention is. + let pinned: [(MegaSpecId, u8); 12] = [ + (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), + ]; + assert_eq!(pinned.len(), MegaSpecId::ALL.len()); + for (spec, position) in pinned { + 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)); From f7bee86616e354264c3eb409d8e4308d885dfa2f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 4 Aug 2026 17:10:43 +0800 Subject: [PATCH 26/37] refactor(evme): gate predeploy bytecode selection via reaches --- bin/mega-evme/src/common/state.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 40ed2c1001175aaa257ed34f39693fbff388b212 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:12:53 +0800 Subject: [PATCH 27/37] test(spec): pin behavior() flatness with a const assertion --- crates/mega-evm/src/evm/spec.rs | 110 ++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index cfc40141..a1b2f775 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -144,6 +144,10 @@ impl MegaSpecId { /// The behavior this spec executes: alias specs project to the spec whose behavior they /// reuse; every other spec is its own behavior. + /// + /// 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, @@ -219,6 +223,76 @@ const _: () = assert!( "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 { @@ -377,6 +451,42 @@ mod tests { ); } + #[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 MegaSpecId::ALL.iter().copied() { From 158fe7a5397b994c6e9c3ffbd7c5006ed7af71ca Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:12:53 +0800 Subject: [PATCH 28/37] test(hardfork): promote the ladder-climb check to a const assertion --- crates/mega-evm/src/block/hardfork.rs | 61 +++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index 08667042..d5dba5ff 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -49,7 +49,7 @@ impl MegaHardfork { /// Gets the `MegaSpecId` associated with this hardfork. #[allow(clippy::match_same_arms)] - pub fn spec_id(&self) -> MegaSpecId { + 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 { @@ -68,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}")] @@ -611,18 +639,25 @@ mod tests { use crate::SequencerRegistryConfig; #[test] - fn test_variants_declaration_order_climbs_the_ladder() { - // The reverse scan in `hardfork()` and the skipped-rung rule in `validate_schedule` - // assume declaration order maps to strictly ascending spec rungs; this fails at a - // misplaced variant instead of two derived tests away. - for pair in MegaHardfork::VARIANTS.windows(2) { - assert!( - (pair[0].spec_id() as u8) < (pair[1].spec_id() as u8), - "{:?} -> {:?} must climb the spec ladder", - pair[0], - pair[1], - ); - } + 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] From 2a6219848ef382eb1c31a57ad74b8991c7bda6fc Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:53:37 +0800 Subject: [PATCH 29/37] refactor(hardfork): delete max_activated_spec_id in favor of spec_id --- crates/mega-evm/benches/block_bench.rs | 13 +- crates/mega-evm/src/block/executor.rs | 2 +- crates/mega-evm/src/block/hardfork.rs | 199 ++++++------------ crates/mega-evm/src/system/control.rs | 2 +- crates/mega-evm/src/system/deploy.rs | 9 +- crates/mega-evm/src/system/keyless_deploy.rs | 2 +- crates/mega-evm/src/system/limit_control.rs | 2 +- crates/mega-evm/src/system/oracle.rs | 4 +- .../mega-evm/src/system/sequencer_registry.rs | 8 +- .../tests/block_executor/partial_ladder.rs | 9 +- crates/mega-evm/tests/mutation/block.rs | 24 +-- 11 files changed, 105 insertions(+), 169 deletions(-) diff --git a/crates/mega-evm/benches/block_bench.rs b/crates/mega-evm/benches/block_bench.rs index 76277c9d..c6ee3bde 100644 --- a/crates/mega-evm/benches/block_bench.rs +++ b/crates/mega-evm/benches/block_bench.rs @@ -448,11 +448,11 @@ fn bench_rex5_pre_block(c: &mut Criterion) { /// Benchmark hardfork-config resolution on the real mainnet schedule. /// -/// The floor-projected predicates (`is_rex_5_active_at_timestamp`) and -/// `max_activated_spec_id` 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. -/// The floor'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. +/// 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; @@ -464,9 +464,6 @@ fn bench_hardfork_resolution(c: &mut Criterion) { 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("max_activated_spec_id", |b| { - b.iter(|| black_box(schedule.max_activated_spec_id(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())) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 9d08aa2b..2668922b 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -142,7 +142,7 @@ where ); Self { - setup_spec: hardforks.max_activated_spec_id(block_timestamp), + setup_spec: hardforks.spec_id(block_timestamp), hardforks: hardforks.clone(), receipt_builder, receipts: Vec::new(), diff --git a/crates/mega-evm/src/block/hardfork.rs b/crates/mega-evm/src/block/hardfork.rs index d5dba5ff..82e02404 100644 --- a/crates/mega-evm/src/block/hardfork.rs +++ b/crates/mega-evm/src/block/hardfork.rs @@ -185,25 +185,12 @@ pub trait MegaHardforks: OpHardforks { self.hardfork(timestamp).map_or(MegaSpecId::EQUIVALENCE, |h| h.spec_id()) } - /// Returns the highest [`MegaSpecId`] among all [`MegaHardfork`]s activated at or before - /// `timestamp` — which, under the 1:1 ascending fork->spec map, is exactly - /// [`spec_id`](Self::spec_id). - /// - /// Kept as an alias so call sites can state "one-way setup" intent explicitly; pair it with - /// [`MegaSpecId::reaches`] (position) rather than `is_enabled` (behavior). - fn max_activated_spec_id(&self, timestamp: BlockTimestamp) -> MegaSpecId { - // With the 1:1 ascending fork->spec map, the latest activated fork IS the maximum: - // the floor coincides with `spec_id` on every schedule. Kept as an alias for the - // transition; call sites can migrate to `spec_id` + `reaches`. - self.spec_id(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.max_activated_spec_id(timestamp).reaches(MegaSpecId::MINI_REX) + self.spec_id(timestamp).reaches(MegaSpecId::MINI_REX) } /// Returns `true` if the [`MegaHardfork::MiniRex1`] activation event has occurred at the @@ -228,59 +215,59 @@ pub trait MegaHardforks: OpHardforks { } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX`], the rung - /// introduced by [`MegaHardfork::Rex`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX) + self.spec_id(timestamp).reaches(MegaSpecId::REX) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX1`], the rung - /// introduced by [`MegaHardfork::Rex1`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX1) + self.spec_id(timestamp).reaches(MegaSpecId::REX1) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX2`], the rung - /// introduced by [`MegaHardfork::Rex2`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX2) + self.spec_id(timestamp).reaches(MegaSpecId::REX2) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX3`], the rung - /// introduced by [`MegaHardfork::Rex3`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX3) + self.spec_id(timestamp).reaches(MegaSpecId::REX3) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX4`], the rung - /// introduced by [`MegaHardfork::Rex4`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX4) + self.spec_id(timestamp).reaches(MegaSpecId::REX4) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX5`], the rung - /// introduced by [`MegaHardfork::Rex5`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX5) + self.spec_id(timestamp).reaches(MegaSpecId::REX5) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX6`], the rung - /// introduced by [`MegaHardfork::Rex6`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX6) + self.spec_id(timestamp).reaches(MegaSpecId::REX6) } /// Returns `true` once the scheduled spec has reached [`MegaSpecId::REX7`], the rung - /// introduced by [`MegaHardfork::Rex7`]. Floor-projected — see the trait docs; for the raw - /// activation event use [`mega_fork_activation`](Self::mega_fork_activation). + /// 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.max_activated_spec_id(timestamp).reaches(MegaSpecId::REX7) + self.spec_id(timestamp).reaches(MegaSpecId::REX7) } /// Checks the schedule for well-formedness, i.e., that it describes a chain climbing the @@ -859,15 +846,15 @@ mod tests { } for ts in stamps { - let floor = hf.max_activated_spec_id(ts); + let resolved = hf.spec_id(ts); for fork in MegaHardfork::VARIANTS { if !fork.introduces_behavior() { continue; } assert_eq!( - floor.reaches(fork.spec_id()), + resolved.reaches(fork.spec_id()), hf.mega_fork_activation(*fork).active_at_timestamp(ts), - "floor disagrees with per-fork activation for {fork:?} at ts={ts}" + "position disagrees with per-fork activation for {fork:?} at ts={ts}" ); } } @@ -875,46 +862,39 @@ mod tests { } /// On a partial ladder — a config that schedules a later fork without its predecessors — the - /// floor still enables every lower spec, and the spec-introducing predicates, being floor - /// 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 + /// 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_floor_enables_unscheduled_predecessors() { + 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); - assert_eq!(hf.spec_id(0), MegaSpecId::REX6); - let floor = hf.max_activated_spec_id(0); - assert_eq!(floor, MegaSpecId::REX6); + let resolved = hf.spec_id(0); + assert_eq!(resolved, MegaSpecId::REX6); for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX2, MegaSpecId::REX4, MegaSpecId::REX5] { - assert!(floor.is_enabled(spec), "floor must enable {spec:?} on a partial ladder"); + assert!(resolved.is_enabled(spec), "{spec:?} must be enabled on a partial ladder"); } - // The predicates project the floor: unscheduled predecessors still report active. - assert!(hf.is_rex_5_active_at_timestamp(0), "Rex5 predicate follows the floor"); - assert!(hf.is_mini_rex_active_at_timestamp(0), "MiniRex predicate follows the floor"); + // 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` under both projections, 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. + /// 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"); - assert_eq!(config.max_activated_spec_id(0), spec, "{spec:?} floor at genesis"); - assert_eq!( - config.max_activated_spec_id(u64::MAX), - spec, - "{spec:?} floor 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 @@ -922,17 +902,12 @@ mod tests { 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"); - assert_eq!( - downgraded.max_activated_spec_id(u64::MAX), - spec, - "{spec:?} floor from an activated config" - ); } } /// Removing a middle rung does NOT express "a chain running spec N". It is the partial-ladder - /// shape: the executing spec follows the newest fork still registered, and the floor — with - /// the predicate projected from it — keeps every lower gate open. + /// 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 = @@ -943,57 +918,20 @@ mod tests { ForkCondition::Never, "Rex4 itself is unregistered" ); - assert_ne!(partial.spec_id(0), MegaSpecId::REX4, "the executing spec is not lowered"); + assert_ne!(partial.spec_id(0), MegaSpecId::REX4, "the resolved spec is not lowered"); assert!( - partial.max_activated_spec_id(0).is_enabled(MegaSpecId::REX4), - "the floor still enables the removed fork's spec" + 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 with the floor" - ); - } - - /// The floor and the executing spec agree on every spec at or above `REX5` across every - /// canonical schedule, which is what makes the two-spec split in `resolve_system_address` - /// inert today. A rollback hardfork scheduled *after* Rex5's activation that maps below - /// `REX5` would break the agreement and must be caught here. (A rollback scheduled before - /// Rex5 — mainnet's `MiniRex1` — keeps both sides below `REX5` and does not.) - #[test] - fn test_floor_and_executing_spec_agree_across_rex5_rex6_boundary() { - 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 (exec, floor) = (hf.spec_id(ts), hf.max_activated_spec_id(ts)); - for spec in - MegaSpecId::ALL.iter().copied().filter(|spec| spec.is_enabled(MegaSpecId::REX5)) - { - assert_eq!( - exec.is_enabled(spec), - floor.is_enabled(spec), - "executing spec and floor disagree on {spec:?} at ts={ts}" - ); - } - } - } + assert!(partial.is_rex_4_active_at_timestamp(0), "the projected predicate stays open"); } - /// Documented domain limit: the floor is timestamp-scoped, so a `MegaHardfork` registered by - /// block number never contributes its own spec to it. `spec_id`/`hardfork` share this + /// 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_floor_ignores_block_numbered_forks() { + fn test_resolution_ignores_block_numbered_forks() { let hf = MegaHardforkConfig::new() .with(MegaHardfork::MiniRex, ForkCondition::Block(0)) .with(MegaHardfork::Rex, ForkCondition::Timestamp(0)); @@ -1002,10 +940,11 @@ mod tests { !hf.mega_fork_activation(MegaHardfork::MiniRex).active_at_timestamp(0), "block-numbered forks are not timestamped" ); - // The floor 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.max_activated_spec_id(0), MegaSpecId::REX); - assert!(hf.max_activated_spec_id(0).is_enabled(MegaSpecId::MINI_REX)); + // 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(), @@ -1013,12 +952,13 @@ mod tests { ); } - /// The early-exit descending scan in `max_activated_spec_id` must be observationally - /// identical to the naive maximum over all activated forks, on every schedule shape: - /// canonical ladders, rollback windows, partial ladders, block-numbered conditions, and the - /// empty config. + /// 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_floor_early_exit_matches_naive_reference() { + fn test_resolved_spec_is_the_maximum_over_activated_forks() { let configs = [ crate::mainnet_hardforks(), crate::testnet_hardforks(), @@ -1051,19 +991,20 @@ mod tests { .map(|fork| fork.spec_id()) .max() .unwrap_or(MegaSpecId::EQUIVALENCE); - assert_eq!(hf.max_activated_spec_id(ts), naive, "floor diverges at ts={ts}"); + assert_eq!(hf.spec_id(ts), naive, "resolved spec diverges at ts={ts}"); } } } - /// Patch-fork predicates stay raw event queries: they are not recoverable from a spec - /// ordinal, so they must not follow the floor. Testnet is the live case — its floor climbs - /// the whole ladder while `MiniRex1`/`MiniRex2` are never scheduled. + /// 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_patch_fork_predicates_stay_event_scoped() { + fn test_alias_fork_predicates_stay_event_scoped() { let hf = crate::testnet_hardforks(); let ts = u64::MAX; - assert!(hf.max_activated_spec_id(ts).is_enabled(MegaSpecId::REX5), "floor is high"); + 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"); @@ -1100,8 +1041,8 @@ mod tests { } } - /// A partial ladder is executable (the floor keeps setup additive) but not a valid published - /// schedule: `validate_schedule` is the fail-fast side of that split. + /// 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() diff --git a/crates/mega-evm/src/system/control.rs b/crates/mega-evm/src/system/control.rs index 8e80c92b..7ac6bbf0 100644 --- a/crates/mega-evm/src/system/control.rs +++ b/crates/mega-evm/src/system/control.rs @@ -50,7 +50,7 @@ pub fn transact_deploy_access_control_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - access_control_spec(hardforks.max_activated_spec_id(block_timestamp)) + access_control_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } diff --git a/crates/mega-evm/src/system/deploy.rs b/crates/mega-evm/src/system/deploy.rs index 29fbfc87..1b9a753f 100644 --- a/crates/mega-evm/src/system/deploy.rs +++ b/crates/mega-evm/src/system/deploy.rs @@ -147,14 +147,13 @@ pub fn flat_system_contract_specs( hardforks: impl MegaHardforks, block_timestamp: u64, ) -> Vec { - flat_system_contract_specs_for(hardforks.max_activated_spec_id(block_timestamp)) + flat_system_contract_specs_for(hardforks.spec_id(block_timestamp)) } -/// [`flat_system_contract_specs`] against an already-resolved scheduled spec. +/// [`flat_system_contract_specs`] against an already-resolved spec. /// -/// The block executor resolves the floor once per block -/// ([`max_activated_spec_id`](crate::MegaHardforks::max_activated_spec_id)) and calls this -/// directly; the public wrapper above resolves it for callers that hold a hardfork config. +/// 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. diff --git a/crates/mega-evm/src/system/keyless_deploy.rs b/crates/mega-evm/src/system/keyless_deploy.rs index 37f820e6..cd1acf6a 100644 --- a/crates/mega-evm/src/system/keyless_deploy.rs +++ b/crates/mega-evm/src/system/keyless_deploy.rs @@ -48,7 +48,7 @@ pub fn transact_deploy_keyless_deploy_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - keyless_deploy_spec(hardforks.max_activated_spec_id(block_timestamp)) + keyless_deploy_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } diff --git a/crates/mega-evm/src/system/limit_control.rs b/crates/mega-evm/src/system/limit_control.rs index 9ee96c03..43c38797 100644 --- a/crates/mega-evm/src/system/limit_control.rs +++ b/crates/mega-evm/src/system/limit_control.rs @@ -29,7 +29,7 @@ pub fn transact_deploy_limit_control_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - limit_control_spec(hardforks.max_activated_spec_id(block_timestamp)) + limit_control_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } diff --git a/crates/mega-evm/src/system/oracle.rs b/crates/mega-evm/src/system/oracle.rs index a3366090..fe9a9fa0 100644 --- a/crates/mega-evm/src/system/oracle.rs +++ b/crates/mega-evm/src/system/oracle.rs @@ -46,7 +46,7 @@ pub fn transact_deploy_oracle_contract( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - oracle_spec(hardforks.max_activated_spec_id(block_timestamp)) + oracle_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } @@ -108,7 +108,7 @@ pub fn transact_deploy_high_precision_timestamp_oracle( block_timestamp: u64, db: &mut State, ) -> Result, DB::Error> { - high_precision_timestamp_oracle_spec(hardforks.max_activated_spec_id(block_timestamp)) + high_precision_timestamp_oracle_spec(hardforks.spec_id(block_timestamp)) .map(|s| crate::transact_deploy(db, &s)) .transpose() } diff --git a/crates/mega-evm/src/system/sequencer_registry.rs b/crates/mega-evm/src/system/sequencer_registry.rs index 266750e9..1b7a6475 100644 --- a/crates/mega-evm/src/system/sequencer_registry.rs +++ b/crates/mega-evm/src/system/sequencer_registry.rs @@ -192,17 +192,17 @@ pub fn transact_deploy_sequencer_registry( db: &mut State, config: &SequencerRegistryConfig, ) -> Result, BlockExecutionError> { - let spec = hardforks.max_activated_spec_id(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 scheduled spec. +/// [`transact_deploy_sequencer_registry`] against an already-resolved spec. /// -/// The block executor resolves the floor once per block and calls this directly; the public +/// 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 floor and the typed params), so a per-fork activation gate +/// 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, diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs index c26fb704..731f39d8 100644 --- a/crates/mega-evm/tests/block_executor/partial_ladder.rs +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -1,10 +1,9 @@ //! 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 **scheduled spec** -//! (`MegaHardforks::max_activated_spec_id`, position-compared via `reaches`), not on -//! per-fork registration and not on the behavior projection. These tests pin both directions of -//! that choice: +//! `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. @@ -177,7 +176,7 @@ fn test_partial_ladder_runs_lower_fork_setup() { } // The Oracle takes its Rex5 bytecode, and the registry its Rex6 bytecode with the seeded - // rotation delay — i.e. the floor drives version selection too, not just the on/off gate. + // 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 diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index 830a1bbe..0c55baac 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -222,7 +222,7 @@ fn staged_config() -> MegaHardforkConfig { /// /// 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 -/// `max_activated_spec_id(t).reaches(MegaSpecId::X)`, so shifting `X` one rung either way is a +/// `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. @@ -230,48 +230,48 @@ fn staged_config() -> MegaHardforkConfig { fn test_hardfork_activation_predicates_are_true_at_activation() { let cfg = staged_config(); - // MiniRex (hardfork.rs:178) — spec-introducing (MINI_REX). Shifting down lands on + // 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:189) — alias fork, raw event query. + // 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 (hardfork.rs:199) — alias fork, raw event query. + // 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)); - // Rex (hardfork.rs:206). + // Rex (hardfork.rs:220). assert!(!cfg.is_rex_active_at_timestamp(399)); assert!(cfg.is_rex_active_at_timestamp(400)); - // Rex1 (hardfork.rs:213). + // Rex1 (hardfork.rs:227). assert!(!cfg.is_rex_1_active_at_timestamp(499)); assert!(cfg.is_rex_1_active_at_timestamp(500)); - // Rex2 (hardfork.rs:220). + // 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:227). + // Rex3 (hardfork.rs:241). assert!(!cfg.is_rex_3_active_at_timestamp(699)); assert!(cfg.is_rex_3_active_at_timestamp(700)); - // Rex4 (hardfork.rs:234). + // 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:241). + // 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:248). + // 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:255). + // Rex7 (hardfork.rs:269). assert!(!cfg.is_rex_7_active_at_timestamp(1099)); assert!(cfg.is_rex_7_active_at_timestamp(1100)); } From abe9124ff9b19d46ac5576f670316ef0addbf2f9 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:57:04 +0800 Subject: [PATCH 30/37] refactor(block): derive pre-block setup from the executing spec --- crates/mega-evm/src/block/executor.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 2668922b..c4ddfea5 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -58,16 +58,6 @@ pub struct MegaBlockExecutor { receipt_builder: R, ctx: MegaBlockExecutionCtx, system_caller: SystemCaller, - /// The scheduled spec for this block's timestamp, resolved once at construction. - /// - /// Every pre-block setup gate derives from this single value through `reaches` (position), - /// which keeps setup additive by construction and immune to alias windows — an alias rung - /// rolls back behavior, not the setup below it. - /// - /// Cached because the block env is fixed for an executor's lifetime — the constructor - /// already reads `block().timestamp` for its hardfork-coherence asserts. - setup_spec: MegaSpecId, - /// The inner evm instance. pub evm: E, /// The block limiter for tracking the limit usage. @@ -142,7 +132,6 @@ where ); Self { - setup_spec: hardforks.spec_id(block_timestamp), hardforks: hardforks.clone(), receipt_builder, receipts: Vec::new(), @@ -187,13 +176,15 @@ where // clear flag to true. self.evm.db_mut().set_state_clear_flag(true); - // Every pre-block gate below derives from the one scheduled spec resolved at - // construction, 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. - let setup_spec = self.setup_spec; + // 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 From baed076a082f6f11500d88b94dc437adee55aa2a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:57:58 +0800 Subject: [PATCH 31/37] test(block): reconcile BlockLimits alias grouping with behavior() --- crates/mega-evm/src/block/limit.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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; From cb432843bbb0c0ad73e725eee657de2d3885260c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:58:58 +0800 Subject: [PATCH 32/37] test(spec): merge the name and position golden tables --- crates/mega-evm/src/evm/spec.rs | 64 +++++++++++++-------------------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/crates/mega-evm/src/evm/spec.rs b/crates/mega-evm/src/evm/spec.rs index a1b2f775..92580422 100644 --- a/crates/mega-evm/src/evm/spec.rs +++ b/crates/mega-evm/src/evm/spec.rs @@ -361,29 +361,32 @@ impl Display for MegaSpecId { mod tests { use super::*; - const ALL_SPECS: [(MegaSpecId, &str); 12] = [ - (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), - (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() { - // The golden pairs stay hand-written — deriving the expected names from the code under - // test would make the round-trip vacuous — but 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())); + // 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 { + 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); @@ -402,31 +405,14 @@ mod tests { assert_eq!(*MegaSpecId::ALL.last().unwrap(), MegaSpecId::default()); } - /// The compile-time checker is itself exercised with malformed lists: the real `ALL` - /// always satisfies the property, so only rejection cases can detect a weakened guard - /// inside the checker. #[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 here, - // where the review attention is. - let pinned: [(MegaSpecId, u8); 12] = [ - (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), - ]; - assert_eq!(pinned.len(), MegaSpecId::ALL.len()); - for (spec, position) in pinned { + // 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"); } } From cbc12c6c85ae32ae0d819fa5d48fb9a996c51d22 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:00:45 +0800 Subject: [PATCH 33/37] fix(state-test): derive the --bench-spec roster from MegaSpecId::ALL --- crates/state-test/src/main.rs | 44 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 3505dc62..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,22 +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::MINI_REX_1, SpecName::MiniRex1), - (mega_evm::name::MINI_REX_2, SpecName::MiniRex2), - (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}"); } } From 291db6b67993e732ea9d70dbc361bf94e6022513 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:01:12 +0800 Subject: [PATCH 34/37] docs(mutants): state the behavior-axis alias omission in the spec roster --- mutants/operators/spec-gate/generate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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", From 2720baf309adf6e249426c492b453cf6f730cbe6 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:03:16 +0800 Subject: [PATCH 35/37] test(block): pin the load-time and pre-block params rules together --- .../tests/block_executor/partial_ladder.rs | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/tests/block_executor/partial_ladder.rs b/crates/mega-evm/tests/block_executor/partial_ladder.rs index 731f39d8..0a49dd34 100644 --- a/crates/mega-evm/tests/block_executor/partial_ladder.rs +++ b/crates/mega-evm/tests/block_executor/partial_ladder.rs @@ -24,7 +24,7 @@ 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, + 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, @@ -277,6 +277,73 @@ fn test_rex6_without_any_rex5_entry_fails_closed() { ); } +/// 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 From 45f2a4080d615551ea7ab6f3fd2e7477f38e89e3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:04:12 +0800 Subject: [PATCH 36/37] docs(block): add the invariant map for the spec ladder and fork schedule --- crates/mega-evm/src/block/AGENTS.md | 30 ++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/block/AGENTS.md b/crates/mega-evm/src/block/AGENTS.md index 86d8d896..090883e3 100644 --- a/crates/mega-evm/src/block/AGENTS.md +++ b/crates/mega-evm/src/block/AGENTS.md @@ -17,11 +17,39 @@ 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. From 574bd50137880b017c2417738bf6f4835fbf2dd4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:42:28 +0800 Subject: [PATCH 37/37] bench(block): pair each block benchmark with its spec-coherent schedule Pre-block setup now follows the executing spec, so the rex4 rows previously running Rex5-scheduled setup measure a REX4 world from here on; the affected CodSpeed baselines shift accordingly. --- crates/mega-evm/benches/block_bench.rs | 52 +++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/crates/mega-evm/benches/block_bench.rs b/crates/mega-evm/benches/block_bench.rs index c6ee3bde..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");