From 1e7025591733832433c4b18666a12bf322e71ea1 Mon Sep 17 00:00:00 2001 From: mega-putin Date: Fri, 17 Jul 2026 01:38:51 +0000 Subject: [PATCH 01/11] =?UTF-8?q?perf:=20ARO=20pruned=2011-edit=20terminal?= =?UTF-8?q?=20set=20=E2=80=94=20execution,=20instructions,=20precompiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal r4: TERMINAL_CONFIRMED_WITH_TRADE 105 improved, 1 regressed (eip7702_authlist/rex5/400 +0.3368%) 55 control rows max|Δ%|=3.35%, all within 5.0% bound 3 files, +265/-72 Baseline: 7d409ae (current main) Toolchain: codspeed=4.18.3;cargo-codspeed=5.0.1;valgrind=3.26.0.codspeed5;rustc=1.96.0 Stamp: /tmp/terminal-r4.json sha256=1ecbcaddec7eb130c8c3eea05cfe88a31e71260b3292c7e46cc6bffbcfd96998 --- crates/mega-evm/src/evm/execution.rs | 157 +++++++++++++++++------- crates/mega-evm/src/evm/instructions.rs | 135 ++++++++++++++++++-- crates/mega-evm/src/evm/precompiles.rs | 45 ++++--- 3 files changed, 265 insertions(+), 72 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index a2e28751..38fc14ce 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -8,7 +8,7 @@ use delegate::delegate; use op_revm::{ handler::{IsTxError, OpHandler}, transaction::deposit::DEPOSIT_TRANSACTION_TYPE, - OpHaltReason, OpTransactionError, + OpHaltReason, OpSpecId, OpTransactionError, }; use revm::{ context::{ @@ -33,7 +33,7 @@ use revm::{ CallOutcome, CallScheme, CreateOutcome, FrameInput, Gas, InitialAndFloorGas, InstructionResult, InterpreterAction, InterpreterResult, }, - primitives::CALL_STACK_LIMIT, + primitives::{hardfork::SpecId, CALL_STACK_LIMIT}, Inspector, Journal, }; @@ -257,6 +257,43 @@ where } } +impl + MegaHandler +where + EVM: EvmTr, Frame = FRAME>, + ERROR: EvmTrError + + From + + From + + FromStringError + + IsTxError + + core::fmt::Debug, + FRAME: FrameTr, +{ + #[inline] + fn post_execution_without_eip7702_refund_work( + &self, + evm: &mut EVM, + exec_result: &mut FRAME::FrameResult, + init_and_floor_gas: InitialAndFloorGas, + ) -> Result<(), ERROR> { + let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE; + let spec = evm.ctx().cfg().spec(); + let is_regolith = spec.is_enabled_in(OpSpecId::REGOLITH); + + // Match `OpHandler::refund` except for the no-op EIP-7702 refund addition. + if !is_deposit || is_regolith { + exec_result + .gas_mut() + .set_final_refund(spec.into_eth_spec().is_enabled_in(SpecId::LONDON)); + } + + self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas); + self.reimburse_caller(evm, exec_result)?; + self.reward_beneficiary(evm, exec_result)?; + Ok(()) + } +} + impl MegaEvm { /// This is the hook to be called in the beginning of the `frame_run` and `inspect_frame_run` /// functions. This function checks if the additional limit is already exceeded, if so, we @@ -471,8 +508,27 @@ where let init_and_floor_gas = self.validate(evm)?; let eip7702_refund = self.pre_execution(evm)? as i64; - let mut exec_result = self.execution(evm, &init_and_floor_gas)?; - self.post_execution(evm, &mut exec_result, init_and_floor_gas, eip7702_refund)?; + let mut exec_result = if evm.ctx().spec.is_enabled(MegaSpecId::REX5) { + debug_assert!(evm.ctx().tx().gas_limit() >= init_and_floor_gas.initial_gas); + + let gas_limit = evm.ctx().tx().gas_limit() - init_and_floor_gas.initial_gas; + let first_frame_input = self.first_frame_input(evm, gas_limit)?; + let mut frame_result = self.run_exec_loop(evm, first_frame_input)?; + self.last_frame_result(evm, &mut frame_result)?; + frame_result + } else { + self.execution(evm, &init_and_floor_gas)? + }; + if evm.ctx().tx().tx_type() == TransactionType::Eip7702 { + self.post_execution(evm, &mut exec_result, init_and_floor_gas, eip7702_refund)?; + } else { + debug_assert_eq!(eip7702_refund, 0); + self.post_execution_without_eip7702_refund_work( + evm, + &mut exec_result, + init_and_floor_gas, + )?; + } // Prepare the output self.execution_result(evm, exec_result) @@ -896,13 +952,63 @@ where &mut self, mut frame_init: ::FrameInit, ) -> Result, ContextDbError> { - let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); - let is_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX); - let is_rex3_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX3); let is_rex4_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX4); let is_rex5_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX5); let additional_limit = self.ctx().additional_limit.clone(); + // REX4+: If a TX-level limit is already exceeded (e.g., intrinsic DataSize/KVUpdate + // overflow from before_tx_start), abort before interceptor dispatch. Interceptors + // return synthetic results that skip before_frame_init(), which would otherwise + // catch the exceeded limit. + // + // Gated to REX4 only: pre-REX4 specs use TX-global check_limit() which catches + // intrinsic overflow during execution. Changing pre-REX4 behavior would break replay. + if is_rex4_enabled { + // Separate borrow scope: the RefMut must be dropped before push_empty_frame + // borrows again. + let exceeded = additional_limit + .borrow_mut() + .frame_result_if_exceeding_limit(&frame_init.frame_input); + if let Some(frame_result) = exceeded { + additional_limit.borrow_mut().push_empty_frame(); + return Ok(FrameInitResult::Result(frame_result)); + } + } + + // REX5+: enforce `CALL_STACK_LIMIT` before interceptor dispatch. Interceptors + // short-circuit before revm's `make_call_frame` runs its own depth check, so + // without this guard a system contract could be invoked at unbounded depth. + // Revm's depth check is scheme-independent and runs before any call-frame side effect, + // so all call schemes can share this early result. + if is_rex5_enabled { + debug_assert!( + self.ctx().spec.is_enabled(MegaSpecId::REX3), + "REX5 must imply REX3, so Rex5 calls cannot need CALL-based oracle detection" + ); + + if let FrameInput::Call(call_inputs) = &frame_init.frame_input { + if frame_init.depth > CALL_STACK_LIMIT as usize { + debug_assert!( + matches!( + call_inputs.scheme, + CallScheme::Call | + CallScheme::StaticCall | + CallScheme::CallCode | + CallScheme::DelegateCall + ), + "all FrameInput::Call schemes must share revm's too-deep call result" + ); + + let frame_result = gen_call_too_deep_result(call_inputs); + additional_limit.borrow_mut().push_empty_frame(); + return Ok(FrameInitResult::Result(frame_result)); + } + } + } + + let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); + let is_rex3_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX3); + // Check if this is a call to the oracle contract and mark it as accessed. // This handles both direct transaction calls and internal CALL operations. // Rex3+: Oracle access gas detention is triggered by SLOAD (not CALL), so skip this @@ -917,6 +1023,7 @@ where // DELEGATECALL bypass it. // Rex: STATICCALL is added to oracle access detection (unifying CALL-like behavior). if is_mini_rex_enabled && !is_rex3_enabled { + let is_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX); if let FrameInput::Call(call_inputs) = &frame_init.frame_input { let detect_oracle = match call_inputs.scheme { CallScheme::Call => true, @@ -940,42 +1047,6 @@ where } } - // REX4+: If a TX-level limit is already exceeded (e.g., intrinsic DataSize/KVUpdate - // overflow from before_tx_start), abort before interceptor dispatch. Interceptors - // return synthetic results that skip before_frame_init(), which would otherwise - // catch the exceeded limit. - // - // Gated to REX4 only: pre-REX4 specs use TX-global check_limit() which catches - // intrinsic overflow during execution. Changing pre-REX4 behavior would break replay. - if is_rex4_enabled { - // Separate borrow scope: the RefMut must be dropped before push_empty_frame - // borrows again. - let exceeded = additional_limit - .borrow_mut() - .frame_result_if_exceeding_limit(&frame_init.frame_input); - if let Some(frame_result) = exceeded { - additional_limit.borrow_mut().push_empty_frame(); - return Ok(FrameInitResult::Result(frame_result)); - } - } - - // REX5+: enforce `CALL_STACK_LIMIT` before interceptor dispatch. Interceptors - // short-circuit before revm's `make_call_frame` runs its own depth check, so - // without this guard a system contract could be invoked at unbounded depth. - // Scope mirrors interceptor dispatch (Call/StaticCall only); other schemes still - // flow into revm where its own depth check applies. - if is_rex5_enabled { - if let FrameInput::Call(call_inputs) = &frame_init.frame_input { - if matches!(call_inputs.scheme, CallScheme::Call | CallScheme::StaticCall) && - frame_init.depth > CALL_STACK_LIMIT as usize - { - let frame_result = gen_call_too_deep_result(call_inputs); - additional_limit.borrow_mut().push_empty_frame(); - return Ok(FrameInitResult::Result(frame_result)); - } - } - } - // System contract interception dispatch. // Each interceptor checks target address and ABI-decodes function selectors. // Side-effect interceptors (oracle hint) usually return None. diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 91956221..75941b43 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -5,7 +5,7 @@ use crate::{ ExternalEnvTypes, HostExt, JournalInspectTr, MegaContext, MegaSpecId, }; use alloy_evm::Database; -use alloy_primitives::{keccak256, Bytes, U256}; +use alloy_primitives::{keccak256, B256, Bytes, Log, U256}; use revm::{ context::ContextTr, handler::instructions::{EthInstructions, InstructionProvider}, @@ -13,7 +13,9 @@ use revm::{ as_usize_or_fail, gas, gas_or_fail, instructions::{self, control, utility::IntoAddress}, interpreter::EthInterpreter, - interpreter_types::{InputsTr, LoopControl, MemoryTr, RuntimeFlag}, + interpreter_types::{ + Immediates, InputsTr, Jumps, LoopControl, MemoryTr, RuntimeFlag, StackTr, + }, resize_memory, CallScheme, FrameInput, Instruction, InstructionContext, InstructionResult, InstructionTable, InterpreterAction, InterpreterTypes, SStoreResult, Stack, }, @@ -1654,6 +1656,53 @@ pub mod storage_gas_ext { pub mod compute_gas_ext { use super::*; + /// LOG opcode leaf with compute gas charged by the surrounding wrapper. + /// + /// This mirrors revm's LOG implementation, but uses `Log::new_unchecked` because this module + /// only wires it for LOG0..LOG4. + #[inline] + fn log_unchecked< + const N: usize, + WIRE: InterpreterTypes, + H: HostExt + ?Sized, + >( + context: InstructionContext<'_, H, WIRE>, + ) { + debug_assert!(N <= 4); + if context.interpreter.runtime_flag.is_static() { + context.interpreter.halt(InstructionResult::StateChangeDuringStaticCall); + return; + } + + let Some([offset, len]) = context.interpreter.stack.popn::<2>() else { + context.interpreter.halt(InstructionResult::StackUnderflow); + return; + }; + let len = as_usize_or_fail!(context.interpreter, len); + gas_or_fail!(context.interpreter, gas::log_cost(N as u8, len as u64)); + let data = if len == 0 { + Bytes::new() + } else { + let offset = as_usize_or_fail!(context.interpreter, offset); + resize_memory!(context.interpreter, offset, len); + Bytes::copy_from_slice(context.interpreter.memory.slice_len(offset, len).as_ref()) + }; + if context.interpreter.stack.len() < N { + context.interpreter.halt(InstructionResult::StackUnderflow); + return; + } + let Some(topics) = context.interpreter.stack.popn::() else { + context.interpreter.halt(InstructionResult::StackUnderflow); + return; + }; + + context.host.log(Log::new_unchecked( + context.interpreter.input.target_address(), + topics.into_iter().map(B256::from).collect(), + data, + )); + } + /// Macro to wrap the original instruction implementation with compute gas tracking. /// /// Two variants: @@ -1726,7 +1775,22 @@ pub mod compute_gas_ext { } wrap_op_compute_gas!(stop, "STOP", instructions::control::stop); - wrap_op_compute_gas!(add, "ADD", instructions::arithmetic::add); + /// `ADD` opcode with compute gas tracking. + #[inline] + pub fn add( + context: InstructionContext<'_, H, WIRE>, + ) { + gas!(context.interpreter, gas::VERYLOW); + + let Some(([op1], op2)) = context.interpreter.stack.popn_top() else { + context.interpreter.halt(InstructionResult::StackUnderflow); + return; + }; + *op2 = op1.wrapping_add(*op2); + + let mut additional_limit = context.host.additional_limit().borrow_mut(); + compute_gas!(context.interpreter, additional_limit, gas::VERYLOW); + } wrap_op_compute_gas!(mul, "MUL", instructions::arithmetic::mul); wrap_op_compute_gas!(sub, "SUB", instructions::arithmetic::sub); wrap_op_compute_gas!(div, "DIV", instructions::arithmetic::div); @@ -1785,7 +1849,24 @@ pub mod compute_gas_ext { wrap_op_compute_gas!(blobhash, "BLOBHASH", instructions::tx_info::blob_hash); wrap_op_compute_gas!(blobbasefee, "BLOBBASEFEE", instructions::block_info::blob_basefee); - wrap_op_compute_gas!(pop, "POP", instructions::stack::pop); + /// `POP` opcode with compute gas tracking. + #[inline] + pub fn pop, H: HostExt + ?Sized>( + context: InstructionContext<'_, H, WIRE>, + ) { + let gas_before = context.interpreter.gas.remaining(); + + gas!(context.interpreter, gas::BASE); + if !context.interpreter.stack.discard_top() { + context.interpreter.halt(InstructionResult::StackUnderflow); + return; + } + + let gas_used = gas::BASE; + debug_assert_eq!(gas_before.saturating_sub(context.interpreter.gas.remaining()), gas_used); + let mut additional_limit = context.host.additional_limit().borrow_mut(); + compute_gas!(context.interpreter, additional_limit, gas_used); + } wrap_op_compute_gas!(mload, "MLOAD", instructions::memory::mload); wrap_op_compute_gas!(mstore, "MSTORE", instructions::memory::mstore); wrap_op_compute_gas!(mstore8, "MSTORE8", instructions::memory::mstore8); @@ -1802,7 +1883,24 @@ pub mod compute_gas_ext { wrap_op_compute_gas!(mcopy, "MCOPY", instructions::memory::mcopy); wrap_op_compute_gas!(push0, "PUSH0", instructions::stack::push0); - wrap_op_compute_gas!(push1, "PUSH1", instructions::stack::push::<1, _, _>); + /// `PUSH1` opcode with compute gas tracking. + #[inline] + pub fn push1( + context: InstructionContext<'_, H, WIRE>, + ) { + gas!(context.interpreter, gas::VERYLOW); + + let value = U256::from(context.interpreter.bytecode.read_u8()); + if !context.interpreter.stack.push(value) { + context.interpreter.halt(InstructionResult::StackOverflow); + return; + } + + context.interpreter.bytecode.relative_jump(1); + + let mut additional_limit = context.host.additional_limit().borrow_mut(); + compute_gas!(context.interpreter, additional_limit, gas::VERYLOW); + } wrap_op_compute_gas!(push2, "PUSH2", instructions::stack::push::<2, _, _>); wrap_op_compute_gas!(push3, "PUSH3", instructions::stack::push::<3, _, _>); wrap_op_compute_gas!(push4, "PUSH4", instructions::stack::push::<4, _, _>); @@ -1869,11 +1967,11 @@ pub mod compute_gas_ext { wrap_op_compute_gas!(swap15, "SWAP15", instructions::stack::swap::<15, _, _>); wrap_op_compute_gas!(swap16, "SWAP16", instructions::stack::swap::<16, _, _>); - wrap_op_compute_gas!(log0, "LOG0", instructions::host::log::<0, _>); - wrap_op_compute_gas!(log1, "LOG1", instructions::host::log::<1, _>); - wrap_op_compute_gas!(log2, "LOG2", instructions::host::log::<2, _>); - wrap_op_compute_gas!(log3, "LOG3", instructions::host::log::<3, _>); - wrap_op_compute_gas!(log4, "LOG4", instructions::host::log::<4, _>); + wrap_op_compute_gas!(log0, "LOG0", log_unchecked::<0, _, _>); + wrap_op_compute_gas!(log1, "LOG1", log_unchecked::<1, _, _>); + wrap_op_compute_gas!(log2, "LOG2", log_unchecked::<2, _, _>); + wrap_op_compute_gas!(log3, "LOG3", log_unchecked::<3, _, _>); + wrap_op_compute_gas!(log4, "LOG4", log_unchecked::<4, _, _>); wrap_op_compute_gas!(@frame create, "CREATE", instructions::contract::create::<_, false, _>); wrap_op_compute_gas!(@frame call, "CALL", instructions::contract::call); @@ -1916,6 +2014,10 @@ pub trait StackInspectTr { /// Inspect the N-th element of the stack. The top of the stack is the 0-th element. /// If the stack is too short, return None. fn inspect(&self) -> Option; + + /// Discard the top element of the stack. + /// Returns false if the stack is empty. + fn discard_top(&mut self) -> bool; } impl StackInspectTr for Stack { @@ -1927,4 +2029,17 @@ impl StackInspectTr for Stack { // SAFETY: the index must be within the bounds of the stack Some(unsafe { *self.data().get_unchecked(index) }) } + + fn discard_top(&mut self) -> bool { + let len = self.len(); + if len == 0 { + return false; + } + debug_assert!(self.len() > 0); + // SAFETY: the stack is non-empty and U256 does not need drop glue. + unsafe { + self.data_mut().set_len(len - 1); + } + true + } } diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index bcbfc726..9c21e1da 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -19,7 +19,7 @@ use revm::{ context::Cfg, context_interface::ContextTr, handler::{EthPrecompiles, PrecompileProvider}, - interpreter::{Gas, InputsImpl, InterpreterResult}, + interpreter::{Gas, InputsImpl, InstructionResult, InterpreterResult}, precompile::Precompiles, primitives::{Address, HashMap}, }; @@ -210,7 +210,30 @@ impl PrecompileProvider= kzg_point_evaluation::GAS_COST + { + // KZG with the wrapper's `gas_limit < GAS_COST` pre-check passed: upstream + // verification ran and returned a non-OOG error + // (`BlobInvalidInputLength` / `BlobMismatchedVersion` / + // `BlobVerifyKzgProofFailed`). Charge the fixed cost regardless of which + // error variant fired. Using the structural predicate + // (`limit() >= GAS_COST`) instead of an error-variant match keeps this arm + // robust against upstream KZG adding new non-OOG variants. + kzg_point_evaluation::GAS_COST + } else { + output.gas.limit() + }; + context.additional_limit.borrow_mut().record_compute_gas(compute_gas); + return output; + } + + if is_rex5_enabled { + debug_assert!(matches!( + output.result, + InstructionResult::Return | InstructionResult::Revert + )); let spent = output.gas.spent(); let refunded = output.gas.refunded(); let mut normalized = Gas::new(gas_limit); @@ -231,23 +254,7 @@ impl PrecompileProvider= kzg_point_evaluation::GAS_COST - { - // KZG with the wrapper's `gas_limit < GAS_COST` pre-check passed: upstream - // verification ran and returned a non-OOG error - // (`BlobInvalidInputLength` / `BlobMismatchedVersion` / - // `BlobVerifyKzgProofFailed`). Charge the fixed cost regardless of which - // error variant fired. Using the structural predicate - // (`limit() >= GAS_COST`) instead of an error-variant match keeps this arm - // robust against upstream KZG adding new non-OOG variants. - kzg_point_evaluation::GAS_COST - } else { - output.gas.limit() - }; - context.additional_limit.borrow_mut().record_compute_gas(compute_gas); + context.additional_limit.borrow_mut().record_compute_gas(output.gas.spent()); } else if context.spec.is_enabled(MegaSpecId::MINI_REX) { context.additional_limit.borrow_mut().record_compute_gas(output.gas.spent()); } From 366ebaef7b2e690deb15e5686bcb7f2835fe30c6 Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 02:16:47 +0000 Subject: [PATCH 02/11] address review feedback: tighten depth-guard coverage\n\nGenerated-by: engineer-agent --- crates/mega-evm/src/evm/execution.rs | 9 +- crates/mega-evm/src/evm/instructions.rs | 10 +- crates/mega-evm/tests/mini_rex/main.rs | 1 + .../tests/mini_rex/opcode_wrappers.rs | 185 ++++++++++++++++++ .../tests/rex5/call_too_deep_guard.rs | 113 +++++++++-- docs/spec/system-contracts/interception.md | 2 +- docs/spec/upgrades/rex5.md | 5 +- mutants/suppressions.toml | 25 +++ 8 files changed, 322 insertions(+), 28 deletions(-) create mode 100644 crates/mega-evm/tests/mini_rex/opcode_wrappers.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 38fc14ce..91f8dae3 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -257,8 +257,7 @@ where } } -impl - MegaHandler +impl MegaHandler where EVM: EvmTr, Frame = FRAME>, ERROR: EvmTrError @@ -1250,14 +1249,12 @@ where return Ok(ItemOrResult::Result(frame_result)); } } - // (2) REX5+: enforce CALL_STACK_LIMIT for Call/StaticCall so an inspector + // (2) REX5+: enforce CALL_STACK_LIMIT for all call schemes so an inspector // cannot deliver a synthetic call result at unbounded depth, mirroring the // protection added to `frame_init` before interceptor dispatch. if is_rex5_enabled { if let FrameInput::Call(call_inputs) = &frame_init.frame_input { - if matches!(call_inputs.scheme, CallScheme::Call | CallScheme::StaticCall) && - frame_init.depth > CALL_STACK_LIMIT as usize - { + if frame_init.depth > CALL_STACK_LIMIT as usize { let mut frame_result = gen_call_too_deep_result(call_inputs); ctx.additional_limit.borrow_mut().push_empty_frame(); frame_end(ctx, inspector, &frame_init.frame_input, &mut frame_result); diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 75941b43..0339cd3c 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -5,7 +5,7 @@ use crate::{ ExternalEnvTypes, HostExt, JournalInspectTr, MegaContext, MegaSpecId, }; use alloy_evm::Database; -use alloy_primitives::{keccak256, B256, Bytes, Log, U256}; +use alloy_primitives::{keccak256, Bytes, Log, B256, U256}; use revm::{ context::ContextTr, handler::instructions::{EthInstructions, InstructionProvider}, @@ -1661,11 +1661,7 @@ pub mod compute_gas_ext { /// This mirrors revm's LOG implementation, but uses `Log::new_unchecked` because this module /// only wires it for LOG0..LOG4. #[inline] - fn log_unchecked< - const N: usize, - WIRE: InterpreterTypes, - H: HostExt + ?Sized, - >( + fn log_unchecked( context: InstructionContext<'_, H, WIRE>, ) { debug_assert!(N <= 4); @@ -2035,7 +2031,7 @@ impl StackInspectTr for Stack { if len == 0 { return false; } - debug_assert!(self.len() > 0); + debug_assert!(!self.is_empty()); // SAFETY: the stack is non-empty and U256 does not need drop glue. unsafe { self.data_mut().set_len(len - 1); diff --git a/crates/mega-evm/tests/mini_rex/main.rs b/crates/mega-evm/tests/mini_rex/main.rs index aee27a70..53d0dc3a 100644 --- a/crates/mega-evm/tests/mini_rex/main.rs +++ b/crates/mega-evm/tests/mini_rex/main.rs @@ -9,6 +9,7 @@ mod db_error; mod disallow_selfdestruct; mod gas; mod mega_system_transaction; +mod opcode_wrappers; mod oracle; mod state_growth_limit; mod tx_data_and_kv_update_limit; diff --git a/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs new file mode 100644 index 00000000..3aeea93f --- /dev/null +++ b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs @@ -0,0 +1,185 @@ +//! Coverage tests for handwritten compute-gas opcode wrappers in `evm/instructions.rs`. + +use std::convert::Infallible; + +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EVMError, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionError, +}; +use revm::{ + bytecode::opcode::*, + context::{ + result::{ExecutionResult, ResultAndState}, + tx::TxEnvBuilder, + }, +}; + +const CALLER: Address = address!("0000000000000000000000000000000000200000"); +const CONTRACT: Address = address!("0000000000000000000000000000000000200001"); +const TARGET: Address = address!("0000000000000000000000000000000000200002"); + +fn transact_code( + db: &mut MemoryDatabase, + target: Address, +) -> Result, EVMError> { + let mut context = MegaContext::new(db, MegaSpecId::MINI_REX); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut evm = MegaEvm::new(context); + let tx = TxEnvBuilder::new().caller(CALLER).call(target).gas_limit(5_000_000).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + alloy_evm::Evm::transact_raw(&mut evm, tx) +} + +fn assert_halt(result: &ExecutionResult) { + assert!(result.is_halt(), "expected halt, got {result:?}"); +} + +fn build_log_contract(topic_count: usize) -> Bytes { + let mut builder = BytecodeBuilder::default() + .push_bytes(0xdead_beef_u32.to_be_bytes()) + .push_number(0u8) + .append(MSTORE); + + for i in 0..topic_count { + builder = builder.push_number((0x10 + i) as u8); + } + + builder + .push_number(4u8) + .push_number(0x1c_u8) + .append(LOG0 + topic_count as u8) + .append(STOP) + .build() +} + +#[test] +fn test_push1_add_pop_success_paths_execute() { + let bytecode = BytecodeBuilder::default() + .append(PUSH1) + .append(1) + .append(PUSH1) + .append(2) + .append(ADD) + .append(POP) + .append(STOP) + .build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert!(result.result.is_success(), "expected success, got {:?}", result.result); +} + +#[test] +fn test_add_stack_underflow_halts() { + let bytecode = + BytecodeBuilder::default().append(PUSH1).append(1).append(ADD).append(STOP).build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + +#[test] +fn test_pop_stack_underflow_halts() { + let bytecode = BytecodeBuilder::default().append(POP).append(STOP).build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + +#[test] +fn test_push1_stack_overflow_halts() { + let mut builder = BytecodeBuilder::default(); + for _ in 0..1024 { + builder = builder.append(PUSH0); + } + let bytecode = builder.append(PUSH1).append(1).append(STOP).build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + +#[test] +fn test_log_variants_emit_expected_topic_counts() { + for topic_count in 0..=4 { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, build_log_contract(topic_count)); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert!( + result.result.is_success(), + "expected success for LOG{topic_count}, got {:?}", + result.result + ); + + let logs = result.result.logs(); + assert_eq!(logs.len(), 1, "LOG{topic_count} should emit exactly one log"); + let log = &logs[0]; + assert_eq!(log.address, CONTRACT, "LOG{topic_count} emitter mismatch"); + assert_eq!(log.data.topics().len(), topic_count, "LOG{topic_count} topic count mismatch",); + assert_eq!( + log.data.data.as_ref(), + &0xdead_beef_u32.to_be_bytes(), + "LOG{topic_count} data mismatch", + ); + } +} + +#[test] +fn test_log_stack_underflow_halts() { + let bytecode = BytecodeBuilder::default().append(PUSH0).append(LOG1).append(STOP).build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + +#[test] +fn test_log_staticcall_halts_without_emitting_logs() { + let target_code = build_log_contract(0); + let caller_code = BytecodeBuilder::default() + .push_number(0u8) + .push_number(0u8) + .push_number(0u8) + .push_number(0u8) + .push_number(0u8) + .push_address(TARGET) + .push_number(100_000u32) + .append(STATICCALL) + .append(POP) + .append(STOP) + .build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, caller_code) + .account_code(TARGET, target_code); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert!(result.result.is_success(), "parent STATICCALL wrapper should succeed"); + assert!(result.result.logs().is_empty(), "static LOG must not emit logs"); +} diff --git a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs index bb4d0193..c96003c4 100644 --- a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs +++ b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs @@ -36,7 +36,12 @@ use revm::{ const CALLER: Address = address!("0000000000000000000000000000000000300010"); const GAS_LIMIT: u64 = 100_000; -fn make_call_frame_init(target: Address, selector: [u8; 4], depth: usize) -> FrameInit { +fn make_call_frame_init( + target: Address, + selector: [u8; 4], + depth: usize, + scheme: CallScheme, +) -> FrameInit { FrameInit { depth, memory: SharedMemory::new(), @@ -48,7 +53,7 @@ fn make_call_frame_init(target: Address, selector: [u8; 4], depth: usize) -> Fra target_address: target, caller: CALLER, value: CallValue::Transfer(U256::ZERO), - scheme: CallScheme::Call, + scheme, is_static: false, })), } @@ -82,8 +87,12 @@ fn test_rex5_depth_guard_returns_call_too_deep_for_system_contract() { let mut evm = MegaEvm::new(context); let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; - let frame_init = - make_call_frame_init(ACCESS_CONTROL_ADDRESS, selector, CALL_STACK_LIMIT as usize + 1); + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::Call, + ); let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); let ItemOrResult::Result(frame_result) = result else { @@ -110,8 +119,12 @@ fn test_rex5_depth_boundary_allows_call_at_limit() { let mut evm = MegaEvm::new(context); let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; - let frame_init = - make_call_frame_init(ACCESS_CONTROL_ADDRESS, selector, CALL_STACK_LIMIT as usize); + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize, + CallScheme::Call, + ); let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { @@ -132,8 +145,12 @@ fn test_pre_rex5_depth_guard_disabled() { let mut evm = MegaEvm::new(context); let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; - let frame_init = - make_call_frame_init(ACCESS_CONTROL_ADDRESS, selector, CALL_STACK_LIMIT as usize + 1); + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::Call, + ); let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { @@ -174,8 +191,12 @@ fn test_rex5_exceeded_tx_limit_wins_over_call_too_deep() { let mut evm = MegaEvm::new(context); let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; - let frame_init = - make_call_frame_init(ACCESS_CONTROL_ADDRESS, selector, CALL_STACK_LIMIT as usize + 1); + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::Call, + ); let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { @@ -231,7 +252,8 @@ fn test_rex5_inspect_frame_init_depth_guard_overrides_inspector() { // Target a regular address (not a system contract). The inspector will intercept; // without the depth guard, the inspector's synthetic Stop result would survive. let target = address!("0000000000000000000000000000000000300001"); - let frame_init = make_call_frame_init(target, [0u8; 4], CALL_STACK_LIMIT as usize + 1); + let frame_init = + make_call_frame_init(target, [0u8; 4], CALL_STACK_LIMIT as usize + 1, CallScheme::Call); let result = InspectorEvmTr::inspect_frame_init(&mut evm, frame_init) .expect("inspect_frame_init should not error"); @@ -251,3 +273,72 @@ fn test_rex5_inspect_frame_init_depth_guard_overrides_inspector() { assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); } + +#[test] +fn test_rex5_depth_guard_returns_call_too_deep_for_callcode() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context); + + let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::CallCode, + ); + + let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); + let ItemOrResult::Result(frame_result) = result else { + panic!("expected Result variant, got Item"); + }; + assert_call_too_deep(&frame_result); +} + +#[test] +fn test_rex5_depth_guard_returns_call_too_deep_for_delegatecall() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context); + + let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::DelegateCall, + ); + + let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); + let ItemOrResult::Result(frame_result) = result else { + panic!("expected Result variant, got Item"); + }; + assert_call_too_deep(&frame_result); +} + +#[test] +fn test_rex5_inspect_frame_init_depth_guard_overrides_callcode_inspector() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let evm = MegaEvm::new(context); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + + let target = address!("0000000000000000000000000000000000300002"); + let frame_init = + make_call_frame_init(target, [0u8; 4], CALL_STACK_LIMIT as usize + 1, CallScheme::CallCode); + + let result = InspectorEvmTr::inspect_frame_init(&mut evm, frame_init) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + assert_eq!( + outcome.result.result, + InstructionResult::CallTooDeep, + "depth guard must override inspector output for CALLCODE too", + ); + + let insp = evm.inspector(); + assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); + assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); +} diff --git a/docs/spec/system-contracts/interception.md b/docs/spec/system-contracts/interception.md index 9cc0b8cb..0a63381c 100644 --- a/docs/spec/system-contracts/interception.md +++ b/docs/spec/system-contracts/interception.md @@ -30,7 +30,7 @@ A system contract MAY intercept `CALL` or `STATICCALL`, but MUST NOT intercept ` ### Call-Depth Gate Interceptor dispatch MUST respect the EVM call-stack depth limit. -For a `CALL` or `STATICCALL` whose call depth exceeds `CALL_STACK_LIMIT`, a node MUST NOT consult any interceptor. +For any call-frame initialization whose call depth exceeds `CALL_STACK_LIMIT`, a node MUST NOT consult any interceptor. Such a call MUST follow the same depth-failure path an ordinary over-deep call follows: the node MUST produce a synthetic too-deep call result that consumes no gas (the forwarded gas limit is fully refundable to the caller) and MUST NOT perform any interceptor side effect. When both a transaction-level resource-limit overflow and the depth limit apply to the same call, the resource-limit overflow result takes priority over the depth-gate result. diff --git a/docs/spec/upgrades/rex5.md b/docs/spec/upgrades/rex5.md index 21838b8a..d1383ea3 100644 --- a/docs/spec/upgrades/rex5.md +++ b/docs/spec/upgrades/rex5.md @@ -320,14 +320,13 @@ A call to a system contract from a frame whose depth exceeded the limit could th #### New behavior -For any `CALL` or `STATICCALL` whose call depth exceeds `CALL_STACK_LIMIT`, a node MUST short-circuit frame initialization with a synthetic call-too-deep result that spends no gas (the full call gas limit is refundable to the caller) BEFORE consulting any system contract interceptor. +For any call-frame initialization whose call depth exceeds `CALL_STACK_LIMIT`, a node MUST short-circuit frame initialization with a synthetic call-too-deep result that spends no gas (the full call gas limit is refundable to the caller) BEFORE consulting any system contract interceptor. No interceptor side effects (volatile-data tracker mutation, oracle hint forwarding, keyless deploy) MUST be performed in the too-deep case. A transaction-level additional-limit exceed takes priority over this depth gate. The inspector-driven frame-initialization path mirrors the same ordering: exceeded-limit check, then depth gate, then any inspector-provided synthetic output. -`CALLCODE`, `DELEGATECALL`, and the `CREATE` / `CREATE2` paths are out of scope for this gate. -Their depth checks remain in the standard EVM frame-construction path. +`CREATE` and `CREATE2` remain out of scope for this gate. ### 19. Oracle Hint Admission and Metering diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index 8894c225..76240990 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -218,6 +218,31 @@ mutant = "replace ::write_u32 w justification = "Dead/unreachable from the public API: same as write_u8 — write_u32 is reached only via trait dispatch on the non-re-exported pub(crate) hasher; never on any bucket_id path. Stubbing it to () changes no observable output." reviewer = "improve-mutation-score (William Aaron Cheung)" +# --- REX5 implies every earlier MegaSpecId gate (equivalent) ---------------------- +# +# The debug assertion in `frame_init` intentionally pins the architectural invariant that REX5 +# implies REX3, because Rex3 introduced the SLOAD-based oracle detection that makes the old +# CALL-based path unreachable. Replacing that check with REX2 or REX4 cannot change observable +# behavior: `MegaSpecId::is_enabled` is monotonic across the linear progression +# EQUIVALENCE -> MINI_REX -> REX -> REX1 -> REX2 -> REX3 -> REX4 -> REX5, so a REX5 context also +# satisfies both earlier predicates. These mutants therefore only weaken or strengthen a debug-only +# proof without affecting any reachable runtime path. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "spec-gate self.ctx().spec.is_enabled(MegaSpecId::REX3), -> self.ctx().spec.is_enabled(MegaSpecId::REX2)," +justification = "Equivalent: MegaSpecId gates are monotonic in the linear spec progression, so every REX5 context also satisfies is_enabled(REX2). The mutated debug_assert checks a weaker predicate but cannot change any runtime behavior or observable outcome." +reviewer = "address-review-feedback" + +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "spec-gate self.ctx().spec.is_enabled(MegaSpecId::REX3), -> self.ctx().spec.is_enabled(MegaSpecId::REX4)," +justification = "Equivalent: MegaSpecId gates are monotonic in the linear spec progression, so every REX5 context also satisfies is_enabled(REX4). The mutated debug_assert checks a different-but-still-always-true invariant under REX5 and cannot affect runtime behavior." +reviewer = "address-review-feedback" + [[suppress]] kind = "line" category = "dead" From d283e5bdef9c28fcccb2e51c94540157aa456f66 Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 02:21:44 +0000 Subject: [PATCH 03/11] fix CI: collapse duplicate precompile gas branch\n\nGenerated-by: engineer-agent --- crates/mega-evm/src/evm/precompiles.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 9c21e1da..65320d51 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -253,9 +253,7 @@ impl PrecompileProvider Date: Fri, 17 Jul 2026 02:25:26 +0000 Subject: [PATCH 04/11] fix CI: extend coverage for depth guard and log wrappers\n\nGenerated-by: engineer-agent --- .../tests/mini_rex/opcode_wrappers.rs | 41 +++++++++++ .../tests/rex5/call_too_deep_guard.rs | 73 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs index 3aeea93f..4b3e7078 100644 --- a/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs +++ b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs @@ -58,6 +58,15 @@ fn build_log_contract(topic_count: usize) -> Bytes { .build() } +fn build_zero_length_log_contract(topic_count: usize) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for i in 0..topic_count { + builder = builder.push_number((0x20 + i) as u8); + } + + builder.push_number(0u8).push_number(0u8).append(LOG0 + topic_count as u8).append(STOP).build() +} + #[test] fn test_push1_add_pop_success_paths_execute() { let bytecode = BytecodeBuilder::default() @@ -146,6 +155,21 @@ fn test_log_variants_emit_expected_topic_counts() { } } +#[test] +fn test_log_zero_length_uses_empty_data_branch() { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, build_zero_length_log_contract(1)); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert!(result.result.is_success(), "expected success, got {:?}", result.result); + + let logs = result.result.logs(); + assert_eq!(logs.len(), 1, "expected exactly one zero-length log"); + assert_eq!(logs[0].data.data.as_ref(), &[0u8; 0], "zero-length log should have empty data",); + assert_eq!(logs[0].data.topics().len(), 1, "LOG1 should retain its topic"); +} + #[test] fn test_log_stack_underflow_halts() { let bytecode = BytecodeBuilder::default().append(PUSH0).append(LOG1).append(STOP).build(); @@ -158,6 +182,23 @@ fn test_log_stack_underflow_halts() { assert_halt(&result.result); } +#[test] +fn test_log_topic_underflow_halts_after_offset_and_len_are_present() { + let bytecode = BytecodeBuilder::default() + .push_number(0u8) + .push_number(0u8) + .append(LOG1) + .append(STOP) + .build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + #[test] fn test_log_staticcall_halts_without_emitting_logs() { let target_code = build_log_contract(0); diff --git a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs index c96003c4..bf476d5a 100644 --- a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs +++ b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs @@ -342,3 +342,76 @@ fn test_rex5_inspect_frame_init_depth_guard_overrides_callcode_inspector() { assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); } + +#[test] +fn test_rex5_inspect_frame_init_depth_guard_overrides_delegatecall_inspector() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let evm = MegaEvm::new(context); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + + let target = address!("0000000000000000000000000000000000300003"); + let frame_init = make_call_frame_init( + target, + [0u8; 4], + CALL_STACK_LIMIT as usize + 1, + CallScheme::DelegateCall, + ); + + let result = InspectorEvmTr::inspect_frame_init(&mut evm, frame_init) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + assert_eq!( + outcome.result.result, + InstructionResult::CallTooDeep, + "depth guard must override inspector output for DELEGATECALL too", + ); + + let insp = evm.inspector(); + assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); + assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); +} + +#[test] +fn test_rex5_inspect_frame_init_exceeded_tx_limit_wins_over_call_too_deep() { + use mega_evm::{AdditionalLimit, EvmTxRuntimeLimits, LimitCheck, LimitKind}; + use std::{cell::RefCell, rc::Rc}; + + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX5); + let mut additional = + AdditionalLimit::new(MegaSpecId::REX5, EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5)); + additional.has_exceeded_limit = LimitCheck::ExceedsLimit { + kind: LimitKind::KVUpdate, + limit: 0, + used: 1, + frame_local: false, + }; + context.additional_limit = Rc::new(RefCell::new(additional)); + + let evm = MegaEvm::new(context); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + let frame_init = make_call_frame_init( + address!("0000000000000000000000000000000000300004"), + [0u8; 4], + CALL_STACK_LIMIT as usize + 1, + CallScheme::Call, + ); + + let result = InspectorEvmTr::inspect_frame_init(&mut evm, frame_init) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + assert_eq!( + outcome.result.result, + InstructionResult::OutOfGas, + "inspect_frame_init must preserve exceeded-limit priority over CallTooDeep", + ); + + let insp = evm.inspector(); + assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); + assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); +} From f0637bf8064e44e61ce3e7e9018caa9c4017a5b0 Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 02:33:37 +0000 Subject: [PATCH 05/11] fix CI: improve patch coverage for guard paths Generated-by: engineer-agent --- crates/mega-evm/src/evm/precompiles.rs | 32 +++++++++++- .../tests/mini_rex/opcode_wrappers.rs | 34 ++++++++++++ .../tests/rex5/call_too_deep_guard.rs | 52 +++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 65320d51..97964a6c 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -286,7 +286,7 @@ mod tests { test_utils::MemoryDatabase, AdditionalLimit, EvmTxRuntimeLimits, MegaContext, MegaSpecId, }; use alloy_evm::precompiles::PrecompilesMap; - use alloy_primitives::Bytes; + use alloy_primitives::{address, Bytes}; use core::cell::RefCell; use revm::{ handler::PrecompileProvider, @@ -338,6 +338,17 @@ mod tests { inputs } + fn generate_invalid_blake2f_input() -> InputsImpl { + let address = address!("0000000000000000000000000000000000000009"); + InputsImpl { + target_address: address, + bytecode_address: Some(address), + caller_address: address, + input: revm::interpreter::CallInput::Bytes(Bytes::from(vec![0xff])), + call_value: Default::default(), + } + } + fn set_spec_for_context( precompiles_map: &mut PrecompilesMap, _context: &MegaContext, @@ -635,6 +646,25 @@ mod tests { ); } + #[test] + fn test_rex5_non_kzg_precompile_error_records_forwarded_limit() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX5); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX5).precompiles(), + ); + let inputs = generate_invalid_blake2f_input(); + let address = address!("0000000000000000000000000000000000000009"); + let forwarded_gas = 50_000u64; + + let result = precompiles_map.run(&mut context, &address, &inputs, true, forwarded_gas); + let output = result.expect("run ok").expect("Some output"); + assert!(matches!(output.result, InstructionResult::PrecompileError)); + assert_eq!(output.gas.limit(), forwarded_gas); + assert_eq!(output.gas.spent(), 0); + assert_eq!(context.additional_limit.borrow().get_usage().compute_gas, forwarded_gas); + } + /// End-to-end verification that the REX5+ `Gas` normalization on `PrecompileOOG` does NOT /// affect caller gas accounting. /// diff --git a/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs index 4b3e7078..ae1de09a 100644 --- a/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs +++ b/crates/mega-evm/tests/mini_rex/opcode_wrappers.rs @@ -199,6 +199,40 @@ fn test_log_topic_underflow_halts_after_offset_and_len_are_present() { assert_halt(&result.result); } +#[test] +fn test_log_len_overflow_halts_with_invalid_operand_oog() { + let bytecode = BytecodeBuilder::default() + .push_u256(U256::MAX) + .push_number(0u8) + .append(LOG0) + .append(STOP) + .build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + +#[test] +fn test_log_offset_overflow_halts_with_invalid_operand_oog() { + let bytecode = BytecodeBuilder::default() + .push_number(1u8) + .push_u256(U256::MAX) + .append(LOG0) + .append(STOP) + .build(); + + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(CONTRACT, bytecode); + + let result = transact_code(&mut db, CONTRACT).unwrap(); + assert_halt(&result.result); +} + #[test] fn test_log_staticcall_halts_without_emitting_logs() { let target_code = build_log_contract(0); diff --git a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs index bf476d5a..e9515c41 100644 --- a/crates/mega-evm/tests/rex5/call_too_deep_guard.rs +++ b/crates/mega-evm/tests/rex5/call_too_deep_guard.rs @@ -316,6 +316,27 @@ fn test_rex5_depth_guard_returns_call_too_deep_for_delegatecall() { assert_call_too_deep(&frame_result); } +#[test] +fn test_rex5_depth_guard_returns_call_too_deep_for_staticcall() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context); + + let selector = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; + let frame_init = make_call_frame_init( + ACCESS_CONTROL_ADDRESS, + selector, + CALL_STACK_LIMIT as usize + 1, + CallScheme::StaticCall, + ); + + let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init should not error"); + let ItemOrResult::Result(frame_result) = result else { + panic!("expected Result variant, got Item"); + }; + assert_call_too_deep(&frame_result); +} + #[test] fn test_rex5_inspect_frame_init_depth_guard_overrides_callcode_inspector() { let mut db = MemoryDatabase::default(); @@ -374,6 +395,37 @@ fn test_rex5_inspect_frame_init_depth_guard_overrides_delegatecall_inspector() { assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); } +#[test] +fn test_rex5_inspect_frame_init_depth_guard_overrides_staticcall_inspector() { + let mut db = MemoryDatabase::default(); + let context = MegaContext::new(&mut db, MegaSpecId::REX5); + let evm = MegaEvm::new(context); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + + let target = address!("0000000000000000000000000000000000300005"); + let frame_init = make_call_frame_init( + target, + [0u8; 4], + CALL_STACK_LIMIT as usize + 1, + CallScheme::StaticCall, + ); + + let result = InspectorEvmTr::inspect_frame_init(&mut evm, frame_init) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + assert_eq!( + outcome.result.result, + InstructionResult::CallTooDeep, + "depth guard must override inspector output for STATICCALL too", + ); + + let insp = evm.inspector(); + assert_eq!(insp.call_count, 1, "inspector should see exactly one call_start"); + assert_eq!(insp.call_end_count, 1, "inspector's call_end must be paired"); +} + #[test] fn test_rex5_inspect_frame_init_exceeded_tx_limit_wins_over_call_too_deep() { use mega_evm::{AdditionalLimit, EvmTxRuntimeLimits, LimitCheck, LimitKind}; From 481cb55d15a64a9a478f8d6cdeb0bcfadf61d2cc Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 02:50:14 +0000 Subject: [PATCH 06/11] fix CI: kill precompile spec-gate mutant Generated-by: engineer-agent --- crates/mega-evm/src/evm/precompiles.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 97964a6c..061d910d 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -399,6 +399,28 @@ mod tests { assert_eq!(output.gas.spent(), GAS_COST); } + #[test] + fn test_kzg_precompile_equivalence_does_not_record_compute_gas() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::EQUIVALENCE); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::EQUIVALENCE).precompiles(), + ); + let inputs = generate_kzg_test_input(); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + + let result = precompiles_map.run(&mut context, &address, &inputs, true, 200_000); + assert!(result.is_ok(), "Precompile should succeed with sufficient gas"); + let output = result.unwrap().unwrap(); + assert!(matches!(output.result, InstructionResult::Return), "Result should be Return"); + assert!(output.gas.spent() > 0, "successful EQUIVALENCE precompile should consume gas"); + assert_eq!( + context.additional_limit.borrow().get_usage().compute_gas, + 0, + "EQUIVALENCE must not record precompile compute gas; widening the MINI_REX gate would break replay", + ); + } + #[test] fn test_set_spec_preserves_mega_kzg_override() { let mut db = MemoryDatabase::default(); From 7afd416dac039f83df9cfa7ff7469e1fbf78a2ac Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 03:21:56 +0000 Subject: [PATCH 07/11] fix CI: tighten execution mutation coverage Generated-by: engineer-agent --- crates/mega-evm/tests/execution_mutation.rs | 181 ++++++++++++++++++++ mutants/suppressions.toml | 29 ++++ 2 files changed, 210 insertions(+) create mode 100644 crates/mega-evm/tests/execution_mutation.rs diff --git a/crates/mega-evm/tests/execution_mutation.rs b/crates/mega-evm/tests/execution_mutation.rs new file mode 100644 index 00000000..622aea4d --- /dev/null +++ b/crates/mega-evm/tests/execution_mutation.rs @@ -0,0 +1,181 @@ +//! Focused mutation-killing regression tests for `evm/execution.rs`. + +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, +}; +use revm::{ + context::{result::ResultAndState, tx::TxEnvBuilder, ContextTr}, + handler::{FrameResult, ItemOrResult}, + inspector::InspectorEvmTr, + interpreter::{ + interpreter::SharedMemory, interpreter_action::FrameInit, + interpreter_types::InterpreterTypes, CallInput, CallInputs, CallOutcome, CallScheme, + CallValue, FrameInput, Gas, InstructionResult, InterpreterResult, + }, + primitives::CALL_STACK_LIMIT, + Inspector, +}; + +const CALLER: Address = address!("0000000000000000000000000000000000700000"); +const CONTRACT: Address = address!("0000000000000000000000000000000000700001"); +const INSPECT_TARGET: Address = address!("0000000000000000000000000000000000700002"); +const GAS_LIMIT: u64 = 100_000; + +#[derive(Default)] +struct AlwaysInterceptInspector { + call_count: usize, + call_end_count: usize, +} + +impl Inspector for AlwaysInterceptInspector { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.call_count += 1; + Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Stop, + output: Bytes::new(), + gas: Gas::new(inputs.gas_limit), + }, + memory_offset: inputs.return_memory_offset.clone(), + }) + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.call_end_count += 1; + } +} + +fn build_evm( + spec: MegaSpecId, + db: &mut MemoryDatabase, +) -> MegaEvm<&mut MemoryDatabase, revm::inspector::NoOpInspector, EmptyExternalEnv> { + let mut context = MegaContext::new(db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + MegaEvm::new(context) +} + +fn transact( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx: MegaTransaction, +) -> ResultAndState { + let mut evm = build_evm(spec, db); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transaction should not error") +} + +fn refund_contract_code() -> Bytes { + BytecodeBuilder::default().sstore(U256::ZERO, U256::ZERO).stop().build() +} + +fn make_refund_tx(is_deposit: bool) -> MegaTransaction { + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(200_000) + .gas_price(0) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + if is_deposit { + tx.deposit.source_hash = MEGA_SYSTEM_TRANSACTION_SOURCE_HASH; + } + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn make_call_frame_init(depth: usize) -> FrameInit { + FrameInit { + depth, + memory: SharedMemory::new(), + frame_input: FrameInput::Call(Box::new(CallInputs { + input: CallInput::Bytes(Bytes::new()), + return_memory_offset: 0..0, + gas_limit: GAS_LIMIT, + bytecode_address: INSPECT_TARGET, + target_address: INSPECT_TARGET, + caller: CALLER, + value: CallValue::Transfer(U256::ZERO), + scheme: CallScheme::Call, + is_static: false, + })), + } +} + +#[test] +fn test_deposit_refund_matches_regular_tx_under_isthmus() { + let mut normal_db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_u64)) + .account_code(CONTRACT, refund_contract_code()) + .account_storage(CONTRACT, U256::ZERO, U256::from(1_u64)); + let normal = transact(MegaSpecId::REX4, &mut normal_db, make_refund_tx(false)); + assert!(normal.result.is_success(), "regular transaction should succeed: {:?}", normal.result); + + let mut deposit_db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_u64)) + .account_code(CONTRACT, refund_contract_code()) + .account_storage(CONTRACT, U256::ZERO, U256::from(1_u64)); + let deposit = transact(MegaSpecId::REX4, &mut deposit_db, make_refund_tx(true)); + assert!( + deposit.result.is_success(), + "deposit transaction should succeed: {:?}", + deposit.result + ); + + assert_eq!( + normal.result.gas_used(), + deposit.result.gas_used(), + "all Mega specs map to OpSpecId::ISTHMUS, so REGOLITH refund rules must apply equally to deposit and regular transactions", + ); +} + +#[test] +fn test_inspect_frame_init_depth_equal_limit_preserves_inspector_result() { + let mut db = MemoryDatabase::default(); + let evm = build_evm(MegaSpecId::REX5, &mut db); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + + let result = InspectorEvmTr::inspect_frame_init( + &mut evm, + make_call_frame_init(CALL_STACK_LIMIT as usize), + ) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + + assert_eq!( + outcome.result.result, + InstructionResult::Stop, + "depth == CALL_STACK_LIMIT is still permitted; the inspector result must survive", + ); + let inspector = evm.inspector(); + assert_eq!(inspector.call_count, 1); + assert_eq!(inspector.call_end_count, 1); +} + +#[test] +fn test_inspect_frame_init_low_depth_preserves_inspector_result() { + let mut db = MemoryDatabase::default(); + let evm = build_evm(MegaSpecId::REX5, &mut db); + let mut evm = evm.with_inspector(AlwaysInterceptInspector::default()); + + let result = InspectorEvmTr::inspect_frame_init(&mut evm, make_call_frame_init(0)) + .expect("inspect_frame_init should not error"); + let ItemOrResult::Result(FrameResult::Call(outcome)) = result else { + panic!("expected Call result"); + }; + + assert_eq!( + outcome.result.result, + InstructionResult::Stop, + "the depth guard must not fire below CALL_STACK_LIMIT", + ); + let inspector = evm.inspector(); + assert_eq!(inspector.call_count, 1); + assert_eq!(inspector.call_end_count, 1); +} diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index 76240990..bcc248ac 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -70,6 +70,35 @@ pattern = "::tx_limit" justification = "Dead/equivalent: this trait method has zero call sites; all callers use the inherent frame_tracker.tx_limit(). Mutating it (-> 0 / -> 1) changes no observable behavior (full suite green with the mutant). Required only to satisfy the TxRuntimeLimit trait." reviewer = "improve-mutation-score (William Aaron Cheung)" +# --- execution.rs post_execution_without_eip7702_refund_work equivalents ---------- +# +# Every reachable `MegaSpecId` maps to `OpSpecId::ISTHMUS` (`evm/spec.rs`), and ISTHMUS +# already enables REGOLITH. At this call site: +# +# let is_regolith = spec.is_enabled_in(OpSpecId::REGOLITH); +# if !is_deposit || is_regolith { ... } +# +# `is_regolith` is therefore always `true` for every reachable MegaEVM execution, so the +# branch condition collapses to `true` regardless of the deposit bit. Mutating the +# `tx_type() == DEPOSIT_TRANSACTION_TYPE` comparison or deleting the `!` cannot change +# observable behavior today; the non-equivalent `|| -> &&` mutant remains unsuppressed and +# is pinned by `tests/execution_mutation.rs`. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "replace == with != in MegaHandler::post_execution_without_eip7702_refund_work" +justification = "Equivalent for all reachable MegaEVM specs: every MegaSpecId maps to OpSpecId::ISTHMUS, and ISTHMUS enables REGOLITH. That makes `is_regolith` always true here, so the surrounding `if !is_deposit || is_regolith` branch is taken regardless of whether `tx_type() == DEPOSIT_TRANSACTION_TYPE` is true or false." +reviewer = "engineer-agent" + +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "delete ! in MegaHandler::post_execution_without_eip7702_refund_work" +justification = "Equivalent for all reachable MegaEVM specs: every MegaSpecId maps to OpSpecId::ISTHMUS, and ISTHMUS enables REGOLITH. With `is_regolith` always true, both `!is_deposit || is_regolith` and `is_deposit || is_regolith` reduce to `true`, so deleting the negation cannot change behavior." +reviewer = "engineer-agent" + [[suppress]] kind = "function" category = "dead" From ba8c0da575421825d6c615591015b3bd56e7403b Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 03:52:23 +0000 Subject: [PATCH 08/11] fix CI: make deposit refund mutant observable Generated-by: engineer-agent --- crates/mega-evm/tests/execution_mutation.rs | 51 +++++++++++++++++---- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/tests/execution_mutation.rs b/crates/mega-evm/tests/execution_mutation.rs index 622aea4d..40b9d99b 100644 --- a/crates/mega-evm/tests/execution_mutation.rs +++ b/crates/mega-evm/tests/execution_mutation.rs @@ -23,6 +23,7 @@ const CALLER: Address = address!("0000000000000000000000000000000000700000"); const CONTRACT: Address = address!("0000000000000000000000000000000000700001"); const INSPECT_TARGET: Address = address!("0000000000000000000000000000000000700002"); const GAS_LIMIT: u64 = 100_000; +const REFUND_SLOTS: u64 = 4; #[derive(Default)] struct AlwaysInterceptInspector { @@ -70,7 +71,11 @@ fn transact( } fn refund_contract_code() -> Bytes { - BytecodeBuilder::default().sstore(U256::ZERO, U256::ZERO).stop().build() + let mut builder = BytecodeBuilder::default(); + for slot in 0..REFUND_SLOTS { + builder = builder.sstore(U256::from(slot), U256::ZERO); + } + builder.stop().build() } fn make_refund_tx(is_deposit: bool) -> MegaTransaction { @@ -110,27 +115,57 @@ fn make_call_frame_init(depth: usize) -> FrameInit { fn test_deposit_refund_matches_regular_tx_under_isthmus() { let mut normal_db = MemoryDatabase::default() .account_balance(CALLER, U256::from(1_000_000_u64)) - .account_code(CONTRACT, refund_contract_code()) - .account_storage(CONTRACT, U256::ZERO, U256::from(1_u64)); + .account_code(CONTRACT, refund_contract_code()); + for slot in 0..REFUND_SLOTS { + normal_db = normal_db.account_storage(CONTRACT, U256::from(slot), U256::from(1_u64)); + } let normal = transact(MegaSpecId::REX4, &mut normal_db, make_refund_tx(false)); assert!(normal.result.is_success(), "regular transaction should succeed: {:?}", normal.result); + let revm::context::result::ExecutionResult::Success { + gas_refunded: normal_refunded, + gas_used: normal_used, + .. + } = &normal.result + else { + panic!("expected success result"); + }; let mut deposit_db = MemoryDatabase::default() .account_balance(CALLER, U256::from(1_000_000_u64)) - .account_code(CONTRACT, refund_contract_code()) - .account_storage(CONTRACT, U256::ZERO, U256::from(1_u64)); + .account_code(CONTRACT, refund_contract_code()); + for slot in 0..REFUND_SLOTS { + deposit_db = deposit_db.account_storage(CONTRACT, U256::from(slot), U256::from(1_u64)); + } let deposit = transact(MegaSpecId::REX4, &mut deposit_db, make_refund_tx(true)); assert!( deposit.result.is_success(), "deposit transaction should succeed: {:?}", deposit.result ); - + let revm::context::result::ExecutionResult::Success { + gas_refunded: deposit_refunded, + gas_used: deposit_used, + .. + } = &deposit.result + else { + panic!("expected success result"); + }; assert_eq!( - normal.result.gas_used(), - deposit.result.gas_used(), + normal_used, deposit_used, "all Mega specs map to OpSpecId::ISTHMUS, so REGOLITH refund rules must apply equally to deposit and regular transactions", ); + assert!( + *normal_refunded > 0, + "the SSTORE clear path must produce a non-zero refund so the branch stays observable", + ); + assert!( + *normal_refunded < REFUND_SLOTS * 4_800, + "the multi-slot clear must hit the London refund cap; otherwise skipping set_final_refund stays observationally identical", + ); + assert_eq!( + normal_refunded, deposit_refunded, + "under ISTHMUS/REGOLITH, deposit transactions must preserve the same final gas refund as regular transactions", + ); } #[test] From cff42be3e9e20679e1a8f2b963f29bcda6ce349e Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 06:56:38 +0000 Subject: [PATCH 09/11] address review feedback: tighten mutation coverage and log cleanup Generated-by: engineer-agent --- crates/mega-evm/src/evm/instructions.rs | 4 -- crates/mega-evm/tests/execution_mutation.rs | 50 +++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 0339cd3c..61611d18 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1683,10 +1683,6 @@ pub mod compute_gas_ext { resize_memory!(context.interpreter, offset, len); Bytes::copy_from_slice(context.interpreter.memory.slice_len(offset, len).as_ref()) }; - if context.interpreter.stack.len() < N { - context.interpreter.halt(InstructionResult::StackUnderflow); - return; - } let Some(topics) = context.interpreter.stack.popn::() else { context.interpreter.halt(InstructionResult::StackUnderflow); return; diff --git a/crates/mega-evm/tests/execution_mutation.rs b/crates/mega-evm/tests/execution_mutation.rs index 40b9d99b..01cd6069 100644 --- a/crates/mega-evm/tests/execution_mutation.rs +++ b/crates/mega-evm/tests/execution_mutation.rs @@ -1,5 +1,6 @@ //! Focused mutation-killing regression tests for `evm/execution.rs`. +use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization}; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, @@ -22,6 +23,8 @@ use revm::{ const CALLER: Address = address!("0000000000000000000000000000000000700000"); const CONTRACT: Address = address!("0000000000000000000000000000000000700001"); const INSPECT_TARGET: Address = address!("0000000000000000000000000000000000700002"); +const EIP7702_AUTHORITY: Address = address!("0000000000000000000000000000000000700010"); +const EIP7702_DELEGATE: Address = address!("0000000000000000000000000000000000700011"); const GAS_LIMIT: u64 = 100_000; const REFUND_SLOTS: u64 = 4; @@ -93,6 +96,23 @@ fn make_refund_tx(is_deposit: bool) -> MegaTransaction { tx } +fn make_eip7702_tx() -> MegaTransaction { + let authorization_list = vec![RecoveredAuthorization::new_unchecked( + Authorization { chain_id: U256::from(1_u64), address: EIP7702_DELEGATE, nonce: 0 }, + RecoveredAuthority::Valid(EIP7702_AUTHORITY), + )]; + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(200_000) + .gas_price(0) + .authorization_list_recovered(authorization_list) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + fn make_call_frame_init(depth: usize) -> FrameInit { FrameInit { depth, @@ -168,6 +188,36 @@ fn test_deposit_refund_matches_regular_tx_under_isthmus() { ); } +#[test] +fn test_eip7702_existing_authority_records_final_refund() { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_u64)) + .account_balance(CONTRACT, U256::from(1_u64)) + .account_balance(EIP7702_AUTHORITY, U256::from(1_u64)); + + let result = transact(MegaSpecId::REX5, &mut db, make_eip7702_tx()); + assert!(result.result.is_success(), "EIP-7702 transaction should succeed: {:?}", result.result); + + let revm::context::result::ExecutionResult::Success { gas_refunded, .. } = &result.result + else { + panic!("expected success result"); + }; + assert!( + *gas_refunded > 0, + "existing authorities in a valid EIP-7702 auth list must contribute a non-zero final refund; otherwise the post_execution refund branch becomes unobservable", + ); + + let authority_account = result + .state + .get(&EIP7702_AUTHORITY) + .expect("authority should be updated by auth processing"); + assert_eq!(authority_account.info.nonce, 1, "successful auth should increment authority nonce"); + assert!( + authority_account.info.code.as_ref().is_some_and(|code| code.is_eip7702()), + "successful auth should install EIP-7702 bytecode", + ); +} + #[test] fn test_inspect_frame_init_depth_equal_limit_preserves_inspector_result() { let mut db = MemoryDatabase::default(); From 042cf909f784c052735fa0cffccded6063b46b04 Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 07:14:10 +0000 Subject: [PATCH 10/11] address review feedback: collapse dead refund branch Generated-by: engineer-agent --- crates/mega-evm/src/evm/execution.rs | 13 +++++-------- mutants/suppressions.toml | 29 ---------------------------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 91f8dae3..ccd9b16d 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -275,16 +275,13 @@ where exec_result: &mut FRAME::FrameResult, init_and_floor_gas: InitialAndFloorGas, ) -> Result<(), ERROR> { - let is_deposit = evm.ctx().tx().tx_type() == DEPOSIT_TRANSACTION_TYPE; let spec = evm.ctx().cfg().spec(); - let is_regolith = spec.is_enabled_in(OpSpecId::REGOLITH); - // Match `OpHandler::refund` except for the no-op EIP-7702 refund addition. - if !is_deposit || is_regolith { - exec_result - .gas_mut() - .set_final_refund(spec.into_eth_spec().is_enabled_in(SpecId::LONDON)); - } + // All reachable Mega specs map to ISTHMUS, which already includes REGOLITH. + // That makes the deposit special-case in OpHandler::refund unreachable here, so the + // only behavior difference from the full refund path is skipping record_refund(0). + debug_assert!(spec.is_enabled_in(OpSpecId::REGOLITH)); + exec_result.gas_mut().set_final_refund(spec.into_eth_spec().is_enabled_in(SpecId::LONDON)); self.eip7623_check_gas_floor(evm, exec_result, init_and_floor_gas); self.reimburse_caller(evm, exec_result)?; diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index bcc248ac..76240990 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -70,35 +70,6 @@ pattern = "::tx_limit" justification = "Dead/equivalent: this trait method has zero call sites; all callers use the inherent frame_tracker.tx_limit(). Mutating it (-> 0 / -> 1) changes no observable behavior (full suite green with the mutant). Required only to satisfy the TxRuntimeLimit trait." reviewer = "improve-mutation-score (William Aaron Cheung)" -# --- execution.rs post_execution_without_eip7702_refund_work equivalents ---------- -# -# Every reachable `MegaSpecId` maps to `OpSpecId::ISTHMUS` (`evm/spec.rs`), and ISTHMUS -# already enables REGOLITH. At this call site: -# -# let is_regolith = spec.is_enabled_in(OpSpecId::REGOLITH); -# if !is_deposit || is_regolith { ... } -# -# `is_regolith` is therefore always `true` for every reachable MegaEVM execution, so the -# branch condition collapses to `true` regardless of the deposit bit. Mutating the -# `tx_type() == DEPOSIT_TRANSACTION_TYPE` comparison or deleting the `!` cannot change -# observable behavior today; the non-equivalent `|| -> &&` mutant remains unsuppressed and -# is pinned by `tests/execution_mutation.rs`. -[[suppress]] -kind = "line" -category = "equivalent" -file = "crates/mega-evm/src/evm/execution.rs" -mutant = "replace == with != in MegaHandler::post_execution_without_eip7702_refund_work" -justification = "Equivalent for all reachable MegaEVM specs: every MegaSpecId maps to OpSpecId::ISTHMUS, and ISTHMUS enables REGOLITH. That makes `is_regolith` always true here, so the surrounding `if !is_deposit || is_regolith` branch is taken regardless of whether `tx_type() == DEPOSIT_TRANSACTION_TYPE` is true or false." -reviewer = "engineer-agent" - -[[suppress]] -kind = "line" -category = "equivalent" -file = "crates/mega-evm/src/evm/execution.rs" -mutant = "delete ! in MegaHandler::post_execution_without_eip7702_refund_work" -justification = "Equivalent for all reachable MegaEVM specs: every MegaSpecId maps to OpSpecId::ISTHMUS, and ISTHMUS enables REGOLITH. With `is_regolith` always true, both `!is_deposit || is_regolith` and `is_deposit || is_regolith` reduce to `true`, so deleting the negation cannot change behavior." -reviewer = "engineer-agent" - [[suppress]] kind = "function" category = "dead" From 0fd06a20a011f893f3ddc4d76c61304a80d65b20 Mon Sep 17 00:00:00 2001 From: "mega-putin[bot]" Date: Fri, 17 Jul 2026 07:32:44 +0000 Subject: [PATCH 11/11] address review feedback: realign precompile comments Generated-by: engineer-agent --- crates/mega-evm/src/evm/precompiles.rs | 67 ++++++++++++++------------ 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 061d910d..e9d3dd00 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -190,27 +190,18 @@ impl PrecompileProvider= GAS_COST`): record the declared fixed cost. revm's + // `PrecompileError` halt still consumes the parent's forwarded `gas_limit` from + // the EVM-gas meter, so compute-gas here is intentionally a separate number from + // the EVM-gas burn. + // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the + // wrapper's pre-check itself OOG'd before verification could run): revm did not + // call `record_cost`, so `spent() == 0`. The parent still permanently loses the + // forwarded amount, so record `limit()` to match the EVM-gas burn. let compute_gas = if address == &kzg_point_evaluation::ADDRESS && output.gas.limit() >= kzg_point_evaluation::GAS_COST { @@ -230,6 +221,26 @@ impl PrecompileProvider PrecompileProvider= GAS_COST`): record the declared fixed cost. revm's `PrecompileError` - // halt still consumes the parent's forwarded `gas_limit` from the EVM-gas meter, so - // compute-gas here is intentionally a separate number from the EVM-gas burn. - // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the - // wrapper's pre-check itself OOG'd before verification could run): revm did not call - // `record_cost`, so `spent() == 0`. The parent still permanently loses the forwarded - // amount, so record `limit()` to match the EVM-gas burn. if context.spec.is_enabled(MegaSpecId::MINI_REX) { + // Compute-gas recording on success / revert: + // + // revm already called `record_cost`, so `spent()` reflects the actual + // consumption for the finalized precompile result. Use it directly. context.additional_limit.borrow_mut().record_compute_gas(output.gas.spent()); } output