From 375a5c704742e5235fc9c9ada49e08b5c71bc4fb Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 01:31:45 +0800 Subject: [PATCH 01/28] feat: add a read-only observer channel for keyless sandbox execution --- crates/mega-evm/src/block/executor.rs | 14 +- crates/mega-evm/src/evm/context.rs | 35 +- crates/mega-evm/src/evm/mod.rs | 16 +- crates/mega-evm/src/sandbox/execution.rs | 256 +++++++++++-- crates/mega-evm/src/sandbox/mod.rs | 17 + crates/mega-evm/src/sandbox/observer.rs | 468 +++++++++++++++++++++++ 6 files changed, 777 insertions(+), 29 deletions(-) create mode 100644 crates/mega-evm/src/sandbox/observer.rs diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 43849a77..2121904f 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -1,6 +1,7 @@ #[cfg(not(feature = "std"))] use alloc as std; -use std::{boxed::Box, collections::BTreeMap, vec::Vec}; +use core::cell::RefCell; +use std::{boxed::Box, collections::BTreeMap, rc::Rc, vec::Vec}; use alloy_consensus::{Eip658Value, Header, Transaction, TxReceipt}; use alloy_eips::{Encodable2718, Typed2718}; @@ -150,6 +151,17 @@ where pub fn inspector(&self) -> &INSP { self.evm.inspector() } + + /// Attaches an observer for nested sandbox execution. + /// + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. `None` + /// restores the no-observer sandbox path. + pub fn set_keyless_sandbox_observer( + &mut self, + observer: Option>>>, + ) { + self.evm.set_keyless_sandbox_observer(observer); + } } impl<'db, DB, C, R, INSP, ExtEnvs> diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 886022cf..c2a95040 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -29,14 +29,15 @@ use revm::{ }; use crate::{ - constants, is_system_originated, AdditionalLimit, BucketId, DynamicGasCost, EmptyExternalEnv, - EvmTxRuntimeLimits, ExternalEnvTypes, ExternalEnvs, MegaSpecId, TxRuntimeLimit, - VolatileDataAccess, VolatileDataAccessTracker, VolatileDataAccessType, + constants, is_system_originated, sandbox::SandboxObserver, AdditionalLimit, BucketId, + DynamicGasCost, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvTypes, ExternalEnvs, + MegaSpecId, TxRuntimeLimit, VolatileDataAccess, VolatileDataAccessTracker, + VolatileDataAccessType, }; /// `MegaETH` EVM context type. This struct wraps [`OpContext`] and implements the [`ContextTr`] /// trait to be used as the context for the [`crate::Evm`]. -#[derive(Debug, derive_more::Deref, derive_more::DerefMut)] +#[derive(derive_more::Debug, derive_more::Deref, derive_more::DerefMut)] pub struct MegaContext { /// The inner context. #[deref] @@ -75,6 +76,14 @@ pub struct MegaContext { /// before the sandbox runs). pub(crate) inside_sandbox: Rc>, + /// Observer for nested sandbox execution. + /// + /// `None` keeps the historical no-inspector sandbox path. The observer is + /// read-only: mutating interpreter or context state through its hooks is + /// undefined and may diverge consensus. + #[debug(ignore)] + pub(crate) keyless_sandbox_observer: Option>>>, + /// The system address for the current block. /// Pre-REX5: always `MEGA_SYSTEM_ADDRESS` (the legacy hardcoded constant). /// REX5+: resolved from `SequencerRegistry` storage in `apply_pre_execution_changes`. @@ -174,6 +183,7 @@ impl MegaContext { tx_limits.oracle_access_compute_gas_limit, ))), inside_sandbox: Rc::new(RefCell::new(false)), + keyless_sandbox_observer: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -237,6 +247,7 @@ impl MegaContext { tx_limits.oracle_access_compute_gas_limit, ))), inside_sandbox: Rc::new(RefCell::new(false)), + keyless_sandbox_observer: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -265,6 +276,7 @@ impl MegaContext { oracle_env: self.oracle_env, volatile_data_tracker: self.volatile_data_tracker, inside_sandbox: self.inside_sandbox, + keyless_sandbox_observer: self.keyless_sandbox_observer, system_address: self.system_address, } } @@ -369,6 +381,8 @@ impl MegaContext { oracle_env: Rc::new(RefCell::new(external_envs.oracle_env)), volatile_data_tracker: self.volatile_data_tracker, inside_sandbox: self.inside_sandbox, + // Observer is parameterized by `ExtEnvs`; a type change cannot keep it. + keyless_sandbox_observer: None, system_address: self.system_address, } } @@ -452,6 +466,19 @@ impl MegaContext { self } + /// Attaches an observer for nested sandbox execution. + /// + /// `None` restores the no-observer sandbox path. Observation is read-only: + /// mutating interpreter or context state through the hooks is undefined + /// and may diverge consensus. There is no take/drain API; recorded data + /// stays in the caller's observer. + pub fn set_keyless_sandbox_observer( + &mut self, + observer: Option>>>, + ) { + self.keyless_sandbox_observer = observer; + } + /// Gets the current total data size generated from transaction execution. /// /// # Returns diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 5ddf94f2..fd90940a 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -39,7 +39,8 @@ mod state; #[cfg(not(feature = "std"))] use alloc as std; -use std::{collections::BTreeMap, vec::Vec}; +use core::cell::RefCell; +use std::{collections::BTreeMap, rc::Rc, vec::Vec}; use alloy_primitives::{Address, B256}; pub use context::*; @@ -68,7 +69,7 @@ use revm::{ ExecuteEvm, InspectEvm, Inspector, Journal, }; -use crate::{BucketId, ExternalEnvTypes, LimitUsage, MegaTransaction}; +use crate::{sandbox::SandboxObserver, BucketId, ExternalEnvTypes, LimitUsage, MegaTransaction}; /// The main EVM implementation for the `MegaETH` chain. /// @@ -233,6 +234,17 @@ impl MegaEvm { } impl MegaEvm { + /// Attaches an observer for nested sandbox execution. + /// + /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. `None` + /// restores the no-observer sandbox path. + pub fn set_keyless_sandbox_observer( + &mut self, + observer: Option>>>, + ) { + self.inner.ctx.set_keyless_sandbox_observer(observer); + } + /// Provides a reference to the block environment. /// /// The block environment contains information about the current block being processed, diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 2782bbf7..43978e9a 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -32,6 +32,7 @@ #[cfg(not(feature = "std"))] use alloc as std; +use core::cell::RefCell; use std::{rc::Rc, vec::Vec}; use alloy_consensus::{Signed, Transaction as AlloyTransaction, TxLegacy}; @@ -68,6 +69,10 @@ use super::{ use super::{ error::{encode_error_result, KeylessDeployError}, + observer::{ + ObserverBridge, SandboxCompletionKind, SandboxEndOutcome, SandboxObserver, + SandboxRejectKind, SandboxStartInfo, + }, state::SandboxDb, }; @@ -441,6 +446,20 @@ pub fn execute_keyless_deploy_call } // Step 9: Execute sandbox and apply state changes. + // + // INVARIANT: when an observer is attached, `sandbox_start` and `sandbox_end` + // fire exactly once on this path, covering every terminal arm below. + let observer = ctx.keyless_sandbox_observer.clone(); + if let Some(obs) = &observer { + obs.borrow_mut().sandbox_start(&SandboxStartInfo { + spec: ctx.spec, + signer: deploy_signer, + deploy_address, + gas_limit_override: gas_limit_override_u64, + tx_gas_limit, + }); + } + match execute_keyless_deploy_sandbox(ctx, sandbox_tx, sandbox_tx_limits) { SandboxOutcome::Completed { state, completion, limit_usage, volatile_accesses } => { let gas_used = completion.gas_used(); @@ -455,11 +474,21 @@ pub fn execute_keyless_deploy_call gas_used, &return_memory_offset, ) { + notify_sandbox_end( + &observer, + SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::PostAccountingHalt, + }, + ); return halt; } } if let Err(e) = apply_sandbox_state(ctx, state, deploy_signer) { + notify_sandbox_end( + &observer, + SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::ApplyFailed }, + ); return make_error!(e); } @@ -470,17 +499,37 @@ pub fn execute_keyless_deploy_call match completion { SandboxCompletion::Deployed { gas_used, deploy_address: deployed, logs } => { if deployed != deploy_address { + notify_sandbox_end( + &observer, + SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::AddressMismatch, + }, + ); return make_error!(KeylessDeployError::AddressMismatch); } for log in logs { ctx.log(log); } + notify_sandbox_end( + &observer, + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::Deployed, + gas_used, + }, + ); make_success!(gas_used, deployed) } SandboxCompletion::EmptyCode { gas_used, logs } => { for log in logs { ctx.log(log); } + notify_sandbox_end( + &observer, + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::EmptyCode, + gas_used, + }, + ); make_execution_failure!( gas_used, KeylessDeployError::EmptyCodeDeployed { gas_used } @@ -491,6 +540,13 @@ pub fn execute_keyless_deploy_call // nonce bump from `make_create_frame`, plus any committed sandbox // writes) persists; outer caller sees the failure reason in // `errorData`. + notify_sandbox_end( + &observer, + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, + gas_used, + }, + ); make_execution_failure!(gas_used, error) } } @@ -504,6 +560,10 @@ pub fn execute_keyless_deploy_call if ctx.spec.is_enabled(MegaSpecId::REX5) { gas.erase_cost(gas_limit_override_u64); } + notify_sandbox_end( + &observer, + SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }, + ); make_error!(e) } } @@ -562,11 +622,15 @@ pub(crate) fn execute_keyless_deploy_sandbox::new_with_shared_ext_envs( sandbox_db, mega_spec, salt_env, oracle_env, ); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, observer) } else { let sandbox_ctx = MegaContext::new(sandbox_db, mega_spec); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, None) } } /// Applies the shared sandbox-context configuration, runs the sandbox tx, and returns the /// processed outcome. Factored out of `execute_keyless_deploy_sandbox` so the REX4+ and /// pre-REX4 branches only differ in the `MegaContext` constructor they call. -fn run_sandbox_ctx( - sandbox_ctx: MegaContext, +fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( + sandbox_ctx: MegaContext, ExtEnvs>, sandbox_tx: MegaTransaction, sandbox_tx_limits: Option, block: BlockEnv, chain: L1BlockInfo, + observer: Option>>>, ) -> SandboxOutcome { let sandbox_ctx = match sandbox_tx_limits { Some(limits) => sandbox_ctx.with_tx_runtime_limits(limits), @@ -646,18 +711,46 @@ fn run_sandbox_ctx( let sandbox_ctx = sandbox_ctx.with_block(block).with_chain(chain).with_inside_sandbox(true); let is_rex5_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX5); let is_rex6_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX6); - let mut sandbox_evm = MegaEvm::new(sandbox_ctx); - let result = sandbox_evm.transact_raw(sandbox_tx); - let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); - let volatile_accesses = - sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); - process_sandbox_transact_result( - result, - limit_usage, - volatile_accesses, - is_rex5_enabled, - is_rex6_enabled, - ) + if let Some(observer) = observer { + let mut sandbox_evm = + MegaEvm::new(sandbox_ctx).with_inspector(ObserverBridge::new(observer)); + let result = sandbox_evm.transact_raw(sandbox_tx); + let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); + let volatile_accesses = + sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); + process_sandbox_transact_result( + result, + limit_usage, + volatile_accesses, + is_rex5_enabled, + is_rex6_enabled, + ) + } else { + // Preserve the existing zero-observation path for every caller which + // has not attached a sandbox observer. + let mut sandbox_evm = MegaEvm::new(sandbox_ctx); + let result = sandbox_evm.transact_raw(sandbox_tx); + let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); + let volatile_accesses = + sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); + process_sandbox_transact_result( + result, + limit_usage, + volatile_accesses, + is_rex5_enabled, + is_rex6_enabled, + ) + } +} + +/// Delivers [`SandboxObserver::sandbox_end`] when an observer is attached. +fn notify_sandbox_end( + observer: &Option>>>, + outcome: SandboxEndOutcome, +) { + if let Some(obs) = observer { + obs.borrow_mut().sandbox_end(&outcome); + } } /// Outcome of sandbox execution. @@ -1613,4 +1706,123 @@ mod tests { "deposit-caller state-growth must not be recorded when the storage gas computation fails", ); } + + const SPLIT_CREATE_CALLER: Address = Address::repeat_byte(0x20); + const SPLIT_CREATE_SLOT: U256 = U256::from_limbs([2, 0, 0, 0]); + const SPLIT_CREATE_CODE_LEN: u32 = 8_000; + const SPLIT_CREATE_COMPUTE_BUDGET: u64 = 1_000_000; + + fn split_create_initcode() -> Bytes { + let code_len = SPLIT_CREATE_CODE_LEN.to_be_bytes(); + Bytes::from(vec![ + revm::bytecode::opcode::PUSH1, + 0x2a, + revm::bytecode::opcode::PUSH1, + 0x02, + revm::bytecode::opcode::SSTORE, + revm::bytecode::opcode::PUSH1, + 0x16, + revm::bytecode::opcode::PUSH1, + 0x00, + revm::bytecode::opcode::KECCAK256, + revm::bytecode::opcode::POP, + revm::bytecode::opcode::PUSH3, + code_len[1], + code_len[2], + code_len[3], + revm::bytecode::opcode::PUSH1, + 0x00, + revm::bytecode::opcode::RETURN, + ]) + } + + struct SplitNopObserver; + + impl SandboxObserver for SplitNopObserver {} + + fn run_split_create_fixture( + spec: MegaSpecId, + observer: Option>>>, + ) -> SandboxOutcome { + use crate::test_utils::MemoryDatabase; + use revm::state::EvmState; + let mut db = MemoryDatabase::default(); + db.set_account_balance(SPLIT_CREATE_CALLER, U256::from(10_000_000_000_000_000_000u128)); + let parent_state = EvmState::default(); + let sandbox_db = SandboxDb::new(&parent_state, &mut db); + let mut context = MegaContext::new(sandbox_db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let block = context.block().clone(); + let chain = context.chain().clone(); + + let tx_env = TxEnv { + caller: SPLIT_CREATE_CALLER, + kind: TxKind::Create, + gas_limit: 100_000_000, + gas_price: 0, + data: split_create_initcode(), + value: U256::ZERO, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx_env); + tx.enveloped_tx = Some(Bytes::new()); + if spec.is_enabled(MegaSpecId::REX5) { + tx.deposit.source_hash = SANDBOX_TX_SOURCE_HASH; + } + + let limits = + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(SPLIT_CREATE_COMPUTE_BUDGET); + run_sandbox_ctx(context, tx, Some(limits), block, chain, observer) + } + + fn split_outcome_fingerprint(outcome: &SandboxOutcome) -> (bool, Option, u64) { + match outcome { + SandboxOutcome::Completed { state, completion, .. } => { + let deployed = SPLIT_CREATE_CALLER.create(0); + let created = state.get(&deployed).is_some_and(|account| account.is_created()); + let slot = state.get(&deployed).and_then(|account| { + account.storage.get(&SPLIT_CREATE_SLOT).map(|slot| slot.present_value()) + }); + (created, slot, completion.gas_used()) + } + SandboxOutcome::Rejected(_) => (false, None, 0), + } + } + + #[test] + fn test_observer_parity_pre_rex5_split_create_commits_sstore() { + let baseline = run_split_create_fixture(MegaSpecId::REX4, None); + let observer: Rc>> = + Rc::new(RefCell::new(SplitNopObserver)); + let observed = run_split_create_fixture(MegaSpecId::REX4, Some(observer)); + + let (created, slot, gas) = split_outcome_fingerprint(&baseline); + assert!(created, "pre-REX5 split CREATE must leave the account created"); + assert_eq!(slot, Some(U256::from(0x2a)), "SSTORE must survive the failed deploy"); + assert_eq!( + split_outcome_fingerprint(&observed), + (created, slot, gas), + "observer must not change split-CREATE sandbox outcome" + ); + } + + #[test] + fn test_observer_parity_rex5_atomic_create_failure() { + let baseline = run_split_create_fixture(MegaSpecId::REX5, None); + let observer: Rc>> = + Rc::new(RefCell::new(SplitNopObserver)); + let observed = run_split_create_fixture(MegaSpecId::REX5, Some(observer)); + + let (created, slot, gas) = split_outcome_fingerprint(&baseline); + assert!(!created, "REX5 atomic CREATE failure must not leave a created account"); + assert!(slot.is_none() || slot == Some(U256::ZERO)); + assert_eq!( + split_outcome_fingerprint(&observed), + (created, slot, gas), + "observer must not change atomic CREATE-failure outcome" + ); + } } diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index c59c9889..7d5e9570 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -59,11 +59,26 @@ //! //! - `execution` - Core sandbox execution logic and the main entry point //! [`execute_keyless_deploy_call`] +//! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal //! - `tx` - Transaction decoding and validation for pre-EIP-155 transactions //! - `error` - Error types ([`KeylessDeployError`]) that map to Solidity errors in `IKeylessDeploy` //! +//! # Sandbox observation +//! +//! Nested sandbox execution is otherwise invisible to a parent inspector. Callers may +//! attach a [`SandboxObserver`] via [`crate::MegaContext::set_keyless_sandbox_observer`] +//! (also forwarded from [`crate::MegaEvm`] and [`crate::MegaBlockExecutor`]). The observer +//! sees interpreter hooks inside the sandbox plus a paired `sandbox_start` / `sandbox_end` +//! lifecycle. Observation is read-only and cannot short-circuit `CALL`/`CREATE`. +//! Reverted inner frames still emit their events; whether sandbox state was applied to +//! the parent is reported by [`SandboxEndOutcome::state_applied`]. With no observer +//! attached the sandbox path is unchanged. +//! +//! [`SandboxObserver`]: observer::SandboxObserver +//! [`SandboxEndOutcome::state_applied`]: observer::SandboxEndOutcome::state_applied +//! //! # Type Erasure Strategy //! //! A key challenge in implementing sandbox execution is preventing infinite type instantiation. @@ -95,11 +110,13 @@ mod error; mod execution; +mod observer; mod state; mod state_merge; mod tx; pub use error::*; pub use execution::*; +pub use observer::*; pub use state::*; pub use tx::*; diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs new file mode 100644 index 00000000..9888c357 --- /dev/null +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -0,0 +1,468 @@ +//! Read-only observation channel for nested sandbox execution. +//! +//! A [`SandboxObserver`] sees every interpreter hook that revm's [`Inspector`] +//! would see inside a sandbox, plus a paired [`SandboxObserver::sandbox_start`] / +//! [`SandboxObserver::sandbox_end`] lifecycle. Observation cannot short-circuit +//! `CALL`/`CREATE`: those hooks have no return value, and [`ObserverBridge`] +//! always forwards `None` to the EVM. +//! +//! # Read-only contract +//! +//! Implementations must not mutate interpreter or context state through the +//! `&mut` hook arguments. Doing so is not structurally prevented for every +//! argument (the signatures match revm so generic inspectors can adapt) and +//! may cause consensus divergence; debug builds already trip conservation +//! asserts on many such mutations. +//! +//! # Lifecycle +//! +//! [`sandbox_start`](SandboxObserver::sandbox_start) and +//! [`sandbox_end`](SandboxObserver::sandbox_end) fire exactly once per sandbox +//! that begins execution, and only when an observer is attached. Reverted +//! inner frames still emit their events; whether sandbox state was applied to +//! the parent is reported by [`SandboxEndOutcome::state_applied`]. + +#[cfg(not(feature = "std"))] +use alloc as std; +use core::cell::RefCell; +use std::rc::Rc; + +use alloy_primitives::{Address, Log, U256}; +use revm::{ + interpreter::{ + interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + Interpreter, + }, + Inspector, +}; + +use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; + +use super::state::SandboxDb; + +/// Context available when a sandbox is about to execute. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxStartInfo { + /// Spec used by the parent context that started the sandbox. + pub spec: MegaSpecId, + /// Recovered signer of the keyless deployment transaction. + pub signer: Address, + /// Deterministic deploy address derived from the signer. + pub deploy_address: Address, + /// Gas limit override actually supplied to the sandbox (after any cap). + pub gas_limit_override: u64, + /// Gas limit carried by the signed keyless transaction itself. + pub tx_gas_limit: u64, +} + +/// Terminal outcome of one sandbox execution, delivered exactly once. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SandboxEndOutcome { + /// Sandbox completed and its state was applied to the parent journal. + Applied { + /// Wire-shape completion reported to the outer caller. + completion: SandboxCompletionKind, + /// Gas consumed by the sandbox EVM. + gas_used: u64, + }, + /// Sandbox ran but its state was not applied. + NotApplied { + /// Why the sandbox state was discarded. + reason: SandboxRejectKind, + }, +} + +/// Completion kind for a sandbox whose state was applied to the parent. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxCompletionKind { + /// Inner CREATE succeeded with non-empty runtime bytecode. + Deployed, + /// Inner CREATE succeeded but returned empty runtime bytecode. + EmptyCode, + /// Sandbox EVM execution failed (revert or halt) after producing mergeable state. + ExecutionFailed, +} + +/// Reason a completed or aborted sandbox did not apply state to the parent. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxRejectKind { + /// Validate-reject or internal error before a mergeable frame was produced. + Rejected, + /// REX5+ post-execution resource accounting rejected the sandbox. + PostAccountingHalt, + /// Applying sandbox state to the parent journal failed. + ApplyFailed, + /// Deployed address did not match the derived address. + AddressMismatch, +} + +impl SandboxEndOutcome { + /// Returns `true` when sandbox state was applied to the parent journal. + pub fn state_applied(&self) -> bool { + matches!(self, Self::Applied { .. }) + } +} + +/// Object-safe observer for nested sandbox execution. +/// +/// All hooks have empty default implementations so adding a hook is not a +/// breaking change. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` +/// keep the trait object-safe. +/// +/// INVARIANT: hooks observe; they must not mutate execution state. [`ObserverBridge`] +/// structurally drops `call`/`create` override outcomes. +pub trait SandboxObserver { + /// Called once after the sandbox interpreter is created, before the first opcode. + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called before each opcode executes. + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called after each opcode executes. + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called when a log is emitted inside the sandbox. + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + log: Log, + ) { + let _ = interp; + let _ = context; + let _ = log; + } + + /// Called when a call frame is about to start. Cannot override the call. + #[inline] + fn call(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &mut CallInputs) { + let _ = context; + let _ = inputs; + } + + /// Called when a call frame has concluded. + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &CallInputs, + outcome: &CallOutcome, + ) { + let _ = context; + let _ = inputs; + let _ = outcome; + } + + /// Called when a create frame is about to start. Cannot override the create. + #[inline] + fn create( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &mut CreateInputs, + ) { + let _ = context; + let _ = inputs; + } + + /// Called when a create frame has concluded. + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &CreateInputs, + outcome: &CreateOutcome, + ) { + let _ = context; + let _ = inputs; + let _ = outcome; + } + + /// Called when a contract self-destructs inside the sandbox. + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + let _ = contract; + let _ = target; + let _ = value; + } + + /// Called once immediately before sandbox execution starts. + #[inline] + fn sandbox_start(&mut self, info: &SandboxStartInfo) { + let _ = info; + } + + /// Called once with the terminal sandbox outcome. Paired with [`Self::sandbox_start`]. + #[inline] + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + let _ = outcome; + } +} + +/// Adapts any HRTB-compatible revm [`Inspector`] into a [`SandboxObserver`]. +/// +/// `call`/`create` override outcomes from the inner inspector are discarded. +/// [`SandboxObserver::sandbox_start`] and [`SandboxObserver::sandbox_end`] keep +/// their default empty implementations. +pub struct InspectorSandboxObserver(pub I); + +impl core::fmt::Debug for InspectorSandboxObserver { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("InspectorSandboxObserver").finish_non_exhaustive() + } +} + +impl SandboxObserver for InspectorSandboxObserver +where + E: ExternalEnvTypes, + I: for<'a> Inspector, E>, EthInterpreter>, +{ + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.0.initialize_interp(interp, context); + } + + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.0.step(interp, context); + } + + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.0.step_end(interp, context); + } + + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + log: Log, + ) { + self.0.log(interp, context, log); + } + + #[inline] + fn call(&mut self, context: &mut MegaContext, E>, inputs: &mut CallInputs) { + let _ = self.0.call(context, inputs); + } + + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &CallOutcome, + ) { + // Clone so a mutating inner inspector cannot write back into execution. + let mut outcome = outcome.clone(); + self.0.call_end(context, inputs, &mut outcome); + } + + #[inline] + fn create(&mut self, context: &mut MegaContext, E>, inputs: &mut CreateInputs) { + let _ = self.0.create(context, inputs); + } + + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CreateInputs, + outcome: &CreateOutcome, + ) { + // Clone so a mutating inner inspector cannot write back into execution. + let mut outcome = outcome.clone(); + self.0.create_end(context, inputs, &mut outcome); + } + + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.0.selfdestruct(contract, target, value); + } +} + +/// Inspector that forwards sandbox frames to a [`SandboxObserver`]. +/// +/// `call` and `create` always return `None`, so the observer has no channel +/// that can override execution. +pub(crate) struct ObserverBridge { + observer: Rc>>, +} + +impl core::fmt::Debug for ObserverBridge { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ObserverBridge").finish_non_exhaustive() + } +} + +impl ObserverBridge { + /// Wraps a shared observer handle as a revm inspector. + pub(crate) fn new(observer: Rc>>) -> Self { + Self { observer } + } +} + +impl Inspector, E>, EthInterpreter> + for ObserverBridge +{ + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.observer.borrow_mut().initialize_interp(interp, context); + } + + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.observer.borrow_mut().step(interp, context); + } + + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.observer.borrow_mut().step_end(interp, context); + } + + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + log: Log, + ) { + self.observer.borrow_mut().log(interp, context, log); + } + + #[inline] + fn call( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + self.observer.borrow_mut().call(context, inputs); + None + } + + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + self.observer.borrow_mut().call_end(context, inputs, outcome); + } + + #[inline] + fn create( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { + self.observer.borrow_mut().create(context, inputs); + None + } + + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.observer.borrow_mut().create_end(context, inputs, outcome); + } + + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.observer.borrow_mut().selfdestruct(contract, target, value); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::EmptyExternalEnv; + use std::boxed::Box; + + struct NopObserver; + + impl SandboxObserver for NopObserver {} + + struct GenericInspector; + + impl Inspector for GenericInspector {} + + #[test] + fn test_sandbox_observer_is_object_safe() { + let _boxed: Box> = Box::new(NopObserver); + let _rc: Rc>> = + Rc::new(RefCell::new(NopObserver)); + } + + #[test] + fn test_sandbox_end_outcome_reports_whether_state_was_applied() { + let applied = + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, gas_used: 1 }; + assert!(applied.state_applied()); + + let not_applied = SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }; + assert!(!not_applied.state_applied()); + } + + #[test] + fn test_inspector_adapter_accepts_generic_inspector() { + fn assert_observer>(_: T) {} + assert_observer::(InspectorSandboxObserver(GenericInspector)); + } +} From df5f2c443ce5fdad7c12843aa8a017a71d54b02d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 01:31:45 +0800 Subject: [PATCH 02/28] test: cover sandbox observer parity, event streams, and inspector adapter --- .../tests/rex2/keyless_sandbox_observer.rs | 703 ++++++++++++++++++ crates/mega-evm/tests/rex2/main.rs | 1 + 2 files changed, 704 insertions(+) create mode 100644 crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs new file mode 100644 index 00000000..ce7ee579 --- /dev/null +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -0,0 +1,703 @@ +//! Sandbox observer: parity with the no-observer path, event-stream contracts, +//! and HRTB adapter coverage for generic inspectors. + +use std::{cell::RefCell, rc::Rc}; + +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + revm::context::result::{ExecutionResult, ResultAndState}, + sandbox::{ + decode_error_result, InspectorSandboxObserver, SandboxCompletionKind, SandboxEndOutcome, + SandboxObserver, SandboxRejectKind, SandboxStartInfo, + }, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, + MegaSpecId, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{CALL, POP, PUSH0, RETURN, STATICCALL}, + context::TxEnv, + handler::EvmTr, + inspector::NoOpInspector, + interpreter::{ + interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, + CreateInputs, CreateOutcome, Interpreter, + }, + Inspector, +}; + +const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); +const LARGE_GAS_LIMIT_OVERRIDE: u64 = 10_000_000_000; +const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; +const SIGNED_TX_GAS_LIMIT: u64 = 1_000_000; +const REVERTER: Address = address!("0000000000000000000000000000000000aaaaaa"); +const IDENTITY_PRECOMPILE: Address = address!("0000000000000000000000000000000000000004"); + +const SPECS: [MegaSpecId; 5] = + [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4, MegaSpecId::REX5, MegaSpecId::REX6]; + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ObservedEvent { + Start { + spec: MegaSpecId, + signer: Address, + deploy_address: Address, + gas_limit_override: u64, + tx_gas_limit: u64, + }, + End(SandboxEndOutcome), + InitializeInterp, + Step(u8), + StepEnd(u8), + Call { + target: Address, + }, + CallEnd { + target: Address, + }, + Create, + CreateEnd, + Log, + Selfdestruct, +} + +#[derive(Default)] +struct RecordingObserver { + events: Vec, +} + +impl SandboxObserver for RecordingObserver { + fn initialize_interp( + &mut self, + _interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + self.events.push(ObservedEvent::InitializeInterp); + } + + fn step( + &mut self, + interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + self.events.push(ObservedEvent::Step(interp.bytecode.opcode())); + } + + fn step_end( + &mut self, + interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + self.events.push(ObservedEvent::StepEnd(interp.bytecode.opcode())); + } + + fn call( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CallInputs, + ) { + self.events.push(ObservedEvent::Call { target: inputs.target_address }); + } + + fn call_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &CallInputs, + _outcome: &CallOutcome, + ) { + self.events.push(ObservedEvent::CallEnd { target: inputs.target_address }); + } + + fn create( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + _inputs: &mut CreateInputs, + ) { + self.events.push(ObservedEvent::Create); + } + + fn create_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + _inputs: &CreateInputs, + _outcome: &CreateOutcome, + ) { + self.events.push(ObservedEvent::CreateEnd); + } + + fn log( + &mut self, + _interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + _log: alloy_primitives::Log, + ) { + self.events.push(ObservedEvent::Log); + } + + fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) { + self.events.push(ObservedEvent::Selfdestruct); + } + + fn sandbox_start(&mut self, info: &SandboxStartInfo) { + self.events.push(ObservedEvent::Start { + spec: info.spec, + signer: info.signer, + deploy_address: info.deploy_address, + gas_limit_override: info.gas_limit_override, + tx_gas_limit: info.tx_gas_limit, + }); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(ObservedEvent::End(outcome.clone())); + } +} + +#[derive(Clone, Default)] +struct GenericCreateCounter { + creates: Rc>, +} + +impl Inspector for GenericCreateCounter { + fn create(&mut self, _context: &mut CTX, _inputs: &mut CreateInputs) -> Option { + *self.creates.borrow_mut() += 1; + None + } +} + +fn success_constructor() -> Bytes { + BytecodeBuilder::default() + .sstore(U256::from(0), U256::from(1)) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +fn revert_constructor() -> Bytes { + Bytes::from_static(&hex!("60006000fd")) +} + +fn empty_code_constructor() -> Bytes { + BytecodeBuilder::default().append_many([PUSH0, PUSH0, RETURN]).build() +} + +fn constructor_calls_reverter() -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(REVERTER) + .push_number(50_000_u32) + .append(CALL) + .append(POP) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +fn constructor_calls_identity_precompile() -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(IDENTITY_PRECOMPILE) + .push_number(50_000_u32) + .append(STATICCALL) + .append(POP) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +fn create_pre_eip155_deploy_tx_with_value(init_code: Bytes, value: U256) -> (Bytes, Address) { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: SIGNED_TX_GAS_LIMIT, + to: TxKind::Create, + value, + input: init_code, + chain_id: None, + }; + let r = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let s = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let sig = Signature::new(r, s, false); + let signed = Signed::new_unchecked(tx, sig, B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + let tx_bytes = Bytes::from(buf); + let signer = signed.recover_signer().expect("should recover signer"); + (tx_bytes, signer) +} + +fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { + create_pre_eip155_deploy_tx_with_value(init_code, U256::ZERO) +} + +fn keyless_deploy_call_tx( + keyless_deployment_tx: Bytes, + gas_limit_override: u64, +) -> MegaTransaction { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_deployment_tx, + gasLimitOverride: U256::from(gas_limit_override), + } + .abi_encode(); + let tx = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + data: call_data.into(), + value: U256::ZERO, + gas_limit: 1_000_000_000_000, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn funded_db(signer: Address) -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_balance(signer, U256::from(LARGE_SIGNER_BALANCE)); + db +} + +struct RunConfig<'a> { + spec: MegaSpecId, + db: &'a mut MemoryDatabase, + tx_bytes: Bytes, + gas_limit_override: u64, + observer: Option>>>, + tx_limits: Option, +} + +fn run_keyless(config: RunConfig<'_>) -> ResultAndState { + let mut context = MegaContext::new(config.db, config.spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + if let Some(limits) = config.tx_limits { + context = context.with_tx_runtime_limits(limits); + } + context.set_keyless_sandbox_observer(config.observer); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(config.tx_bytes, config.gas_limit_override); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") +} + +fn assert_result_and_state_eq( + with_observer: &ResultAndState, + without_observer: &ResultAndState, + case: &str, +) { + assert_eq!( + with_observer.result, without_observer.result, + "{case}: execution result must match" + ); + assert_eq!( + with_observer.result.gas_used(), + without_observer.result.gas_used(), + "{case}: gas_used must match" + ); + assert_eq!( + with_observer.state.len(), + without_observer.state.len(), + "{case}: state account count must match" + ); + for (addr, account) in &with_observer.state { + let other = without_observer + .state + .get(addr) + .unwrap_or_else(|| panic!("{case}: missing account {addr:?} in no-observer state")); + assert_eq!(account, other, "{case}: account {addr:?}"); + } +} + +fn parity_pair( + spec: MegaSpecId, + init_code: Bytes, + fund_signer: bool, + tx_limits: Option, + case: &str, +) { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); + let mut db_obs = if fund_signer { funded_db(signer) } else { MemoryDatabase::default() }; + let mut db_base = db_obs.clone(); + let observer: Rc>> = + Rc::new(RefCell::new(RecordingObserver::default())); + + let observed = run_keyless(RunConfig { + spec, + db: &mut db_obs, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(observer), + tx_limits, + }); + let baseline = run_keyless(RunConfig { + spec, + db: &mut db_base, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None, + tx_limits, + }); + assert_result_and_state_eq(&observed, &baseline, case); +} + +fn run_with_recorder( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + tx_limits: Option, +) -> (ResultAndState, Vec) { + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let observer: Rc>> = recorder.clone(); + let result = run_keyless(RunConfig { + spec, + db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(observer), + tx_limits, + }); + let events = recorder.borrow().events.clone(); + (result, events) +} + +fn start_and_end(events: &[ObservedEvent]) -> (&ObservedEvent, &ObservedEvent) { + let start = + events.iter().find(|e| matches!(e, ObservedEvent::Start { .. })).expect("sandbox_start"); + let end = + events.iter().rev().find(|e| matches!(e, ObservedEvent::End(_))).expect("sandbox_end"); + (start, end) +} + +fn assert_single_start_end_pair(events: &[ObservedEvent]) { + let starts = events.iter().filter(|e| matches!(e, ObservedEvent::Start { .. })).count(); + let ends = events.iter().filter(|e| matches!(e, ObservedEvent::End(_))).count(); + assert_eq!(starts, 1, "sandbox_start once, got {starts}"); + assert_eq!(ends, 1, "sandbox_end once, got {ends}"); + let start_idx = events.iter().position(|e| matches!(e, ObservedEvent::Start { .. })).unwrap(); + let end_idx = events.iter().position(|e| matches!(e, ObservedEvent::End(_))).unwrap(); + assert!(start_idx < end_idx, "sandbox_start before sandbox_end"); +} + +fn assert_call_create_balanced(events: &[ObservedEvent]) { + let mut call_depth = 0isize; + let mut create_depth = 0isize; + for event in events { + match event { + ObservedEvent::Call { .. } => call_depth += 1, + ObservedEvent::CallEnd { .. } => call_depth -= 1, + ObservedEvent::Create => create_depth += 1, + ObservedEvent::CreateEnd => create_depth -= 1, + _ => {} + } + assert!(call_depth >= 0, "call_end without call: {events:?}"); + assert!(create_depth >= 0, "create_end without create: {events:?}"); + } + assert_eq!(call_depth, 0, "unbalanced call frames: {events:?}"); + assert_eq!(create_depth, 0, "unbalanced create frames: {events:?}"); +} + +#[test] +fn test_observer_parity_success_across_specs() { + for spec in SPECS { + parity_pair(spec, success_constructor(), true, None, &format!("success {spec:?}")); + } +} + +#[test] +fn test_observer_parity_constructor_revert_across_specs() { + for spec in SPECS { + parity_pair(spec, revert_constructor(), true, None, &format!("revert {spec:?}")); + } +} + +fn parent_compute_gas_used(spec: MegaSpecId, signer: Address, tx_bytes: Bytes) -> u64 { + let mut usage_db = funded_db(signer); + let mut context = MegaContext::new(&mut usage_db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap(); + let used = evm.ctx_ref().additional_limit.borrow().get_usage().compute_gas; + used +} + +#[test] +fn test_observer_parity_rex5_resource_limit_halt() { + for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let used = parent_compute_gas_used(spec, signer, tx_bytes); + assert!(used > 1, "baseline compute gas"); + let limits = + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(used.saturating_sub(1)); + parity_pair( + spec, + success_constructor(), + true, + Some(limits), + &format!("resource halt {spec:?}"), + ); + } +} + +#[test] +fn test_sandbox_start_end_pair_on_successful_deploy() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let deploy_address = signer.create(0); + let mut db = funded_db(signer); + let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + + assert!(result.result.is_success(), "{spec:?} should succeed: {:?}", result.result); + assert_single_start_end_pair(&events); + assert_call_create_balanced(&events); + + let (start, end) = start_and_end(&events); + match start { + ObservedEvent::Start { + spec: start_spec, + signer: start_signer, + deploy_address: start_deploy, + gas_limit_override, + tx_gas_limit, + } => { + assert_eq!(*start_spec, spec); + assert_eq!(*start_signer, signer); + assert_eq!(*start_deploy, deploy_address); + assert_eq!(*gas_limit_override, LARGE_GAS_LIMIT_OVERRIDE); + assert_eq!(*tx_gas_limit, SIGNED_TX_GAS_LIMIT); + } + other => panic!("expected Start, got {other:?}"), + } + match end { + ObservedEvent::End(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::Deployed, + gas_used, + }) => { + assert!(*gas_used > 0, "{spec:?} applied gas"); + } + other => panic!("{spec:?}: expected Applied(Deployed), got {other:?}"), + } + } +} + +#[test] +fn test_sandbox_end_applied_execution_failed_on_constructor_revert() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_constructor()); + let mut db = funded_db(signer); + let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + assert!(result.result.is_success(), "{spec:?} outer success-style revert"); + assert_single_start_end_pair(&events); + match start_and_end(&events).1 { + ObservedEvent::End(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, + .. + }) => {} + other => panic!("{spec:?}: expected Applied(ExecutionFailed), got {other:?}"), + } + } +} + +#[test] +fn test_sandbox_end_applied_empty_code_on_rex5() { + for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(empty_code_constructor()); + let mut db = funded_db(signer); + let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + assert!(result.result.is_success(), "{spec:?} empty-code outer success"); + match start_and_end(&events).1 { + ObservedEvent::End(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::EmptyCode, + .. + }) => {} + other => panic!("{spec:?}: expected Applied(EmptyCode), got {other:?}"), + } + } +} + +#[test] +fn test_sandbox_end_not_applied_rejected_on_insufficient_balance() { + for spec in SPECS { + let (tx_bytes, _signer) = + create_pre_eip155_deploy_tx_with_value(success_constructor(), U256::from(1)); + let mut db = MemoryDatabase::default(); + let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + assert!( + matches!(result.result, ExecutionResult::Revert { .. }), + "{spec:?} insufficient balance should revert the outer call: {:?}", + result.result + ); + assert_single_start_end_pair(&events); + match start_and_end(&events).1 { + ObservedEvent::End(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::Rejected, + }) => {} + other => panic!("{spec:?}: expected NotApplied(Rejected), got {other:?}"), + } + let error = match &result.result { + ExecutionResult::Revert { output, .. } => decode_error_result(output), + other => panic!("expected revert, got {other:?}"), + }; + assert!( + matches!(error, Some(mega_evm::sandbox::KeylessDeployError::InsufficientBalance)), + "{spec:?}: expected InsufficientBalance, got {error:?}" + ); + } +} + +#[test] +fn test_sandbox_end_not_applied_post_accounting_halt_on_rex5() { + for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let used = parent_compute_gas_used(spec, signer, tx_bytes.clone()); + let limits = + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(used.saturating_sub(1)); + + let mut db = funded_db(signer); + let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, Some(limits)); + assert!(result.result.is_halt(), "{spec:?} residual overflow should halt"); + assert_single_start_end_pair(&events); + match start_and_end(&events).1 { + ObservedEvent::End(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::PostAccountingHalt, + }) => {} + other => panic!("{spec:?}: expected PostAccountingHalt, got {other:?}"), + } + } +} + +#[test] +fn test_reverted_subframe_events_still_stream() { + let spec = MegaSpecId::REX5; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_calls_reverter()); + let mut db = funded_db(signer); + db.set_account_code(REVERTER, Bytes::from_static(&hex!("60006000fd"))); + let (_result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + + assert_single_start_end_pair(&events); + assert_call_create_balanced(&events); + assert!( + events.iter().any(|e| matches!(e, ObservedEvent::Call { target } if *target == REVERTER)), + "reverted call must still emit call: {events:?}" + ); + assert!( + events + .iter() + .any(|e| matches!(e, ObservedEvent::CallEnd { target } if *target == REVERTER)), + "reverted call must still emit call_end: {events:?}" + ); +} + +#[test] +fn test_short_circuit_precompile_call_is_balanced() { + for spec in SPECS { + let (tx_bytes, signer) = + create_pre_eip155_deploy_tx(constructor_calls_identity_precompile()); + let mut db = funded_db(signer); + let (_result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + assert_call_create_balanced(&events); + assert!( + events.iter().any(|e| { + matches!(e, ObservedEvent::Call { target } if *target == IDENTITY_PRECOMPILE) + }), + "{spec:?}: missing identity precompile call" + ); + assert!( + events.iter().any(|e| { + matches!(e, ObservedEvent::CallEnd { target } if *target == IDENTITY_PRECOMPILE) + }), + "{spec:?}: missing identity precompile call_end" + ); + } +} + +#[test] +fn test_event_order_create_wraps_steps() { + let spec = MegaSpecId::REX5; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let (_result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); + + let create = events.iter().position(|e| matches!(e, ObservedEvent::Create)).unwrap(); + let create_end = events.iter().position(|e| matches!(e, ObservedEvent::CreateEnd)).unwrap(); + let first_step = events.iter().position(|e| matches!(e, ObservedEvent::Step(_))).unwrap(); + let last_step_end = + events.iter().rposition(|e| matches!(e, ObservedEvent::StepEnd(_))).unwrap(); + assert!(create < first_step, "create before steps"); + assert!(last_step_end < create_end, "step_end before create_end"); +} + +#[test] +fn test_inspector_adapter_records_sandbox_create_for_generic_inspector() { + let tracer = GenericCreateCounter::default(); + let creates = Rc::clone(&tracer.creates); + let adapter = InspectorSandboxObserver(tracer); + let observer: Rc>> = + Rc::new(RefCell::new(adapter)); + + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let result = run_keyless(RunConfig { + spec: MegaSpecId::REX5, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(observer), + tx_limits: None, + }); + assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); + assert!( + *creates.borrow() >= 1, + "generic inspector must see the sandbox CREATE frame, got {}", + creates.borrow() + ); +} + +#[test] +fn test_no_observer_skips_lifecycle_hooks() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let result = run_keyless(RunConfig { + spec: MegaSpecId::REX5, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None, + tx_limits: None, + }); + assert!(result.result.is_success(), "default path must still succeed"); +} diff --git a/crates/mega-evm/tests/rex2/main.rs b/crates/mega-evm/tests/rex2/main.rs index 4a20cfef..c734edfe 100644 --- a/crates/mega-evm/tests/rex2/main.rs +++ b/crates/mega-evm/tests/rex2/main.rs @@ -1,5 +1,6 @@ //! Tests for Rex2 hardfork features. mod keyless_deploy; +mod keyless_sandbox_observer; mod oracle_hint; mod selfdestruct; From a60ff179738919c1e788384d936e4197f8897c7c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 01:54:54 +0800 Subject: [PATCH 03/28] fix: keep pre-REX4 sandbox EmptyExternalEnv when an observer is attached Observer attachment now uses dual-slot type erasure so pre-REX4 sandboxes keep EmptyExternalEnv instead of sharing the parent env. Opcode-level hooks still fire on that path when the observer implements both env types. --- crates/mega-evm/src/block/executor.rs | 26 +++- crates/mega-evm/src/evm/context.rs | 102 ++++++++++-- crates/mega-evm/src/evm/mod.rs | 28 +++- crates/mega-evm/src/sandbox/execution.rs | 34 ++-- crates/mega-evm/src/sandbox/mod.rs | 9 ++ crates/mega-evm/src/sandbox/observer.rs | 25 +++ .../tests/rex2/keyless_sandbox_observer.rs | 145 ++++++++++++++++-- 7 files changed, 319 insertions(+), 50 deletions(-) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 2121904f..6599d0b6 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -152,15 +152,31 @@ where self.evm.inspector() } - /// Attaches an observer for nested sandbox execution. + /// Attaches an observer for nested sandbox execution on every spec. /// - /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. `None` - /// restores the no-observer sandbox path. - pub fn set_keyless_sandbox_observer( + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. The observer + /// must implement [`crate::sandbox::SandboxObserver`] for both this executor's + /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. `None` restores the no-observer + /// sandbox path. + pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: crate::sandbox::SandboxObserver + + crate::sandbox::SandboxObserver + + 'static, + { + self.evm.set_keyless_sandbox_observer(observer); + } + + /// Attaches an observer that implements [`crate::sandbox::SandboxObserver`] + /// only for this executor's `ExtEnvs`. + /// + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer_for_parent_env`]. + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, ) { - self.evm.set_keyless_sandbox_observer(observer); + self.evm.set_keyless_sandbox_observer_for_parent_env(observer); } } diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index c2a95040..62fb0141 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -76,14 +76,29 @@ pub struct MegaContext { /// before the sandbox runs). pub(crate) inside_sandbox: Rc>, - /// Observer for nested sandbox execution. - /// - /// `None` keeps the historical no-inspector sandbox path. The observer is - /// read-only: mutating interpreter or context state through its hooks is - /// undefined and may diverge consensus. + /// Observer for nested sandbox execution, typed against this context's [`ExtEnvs`]. + /// + /// `None` keeps the historical no-inspector sandbox path. REX4+ sandboxes + /// share the parent env and deliver opcode-level hooks through this slot. + /// Changing [`ExtEnvs`] via [`Self::with_external_envs`] resets this field + /// and the [`EmptyExternalEnv`] observer slot: the observer cannot be + /// carried across an env-type change and must be attached after external + /// environments are assembled. The observer is read-only: mutating + /// interpreter or context state through its hooks is undefined and may + /// diverge consensus. #[debug(ignore)] pub(crate) keyless_sandbox_observer: Option>>>, + /// Observer handle typed against [`EmptyExternalEnv`]. + /// + /// Pre-REX4 sandboxes always execute with [`EmptyExternalEnv`]. Opcode-level + /// hooks on that path use this slot so attaching an observer cannot change + /// sandbox env semantics. `None` when no observer is attached, or when the + /// caller used [`Self::set_keyless_sandbox_observer_for_parent_env`]. + #[debug(ignore)] + pub(crate) keyless_sandbox_observer_empty: + Option>>>, + /// The system address for the current block. /// Pre-REX5: always `MEGA_SYSTEM_ADDRESS` (the legacy hardcoded constant). /// REX5+: resolved from `SequencerRegistry` storage in `apply_pre_execution_changes`. @@ -184,6 +199,7 @@ impl MegaContext { ))), inside_sandbox: Rc::new(RefCell::new(false)), keyless_sandbox_observer: None, + keyless_sandbox_observer_empty: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -248,6 +264,7 @@ impl MegaContext { ))), inside_sandbox: Rc::new(RefCell::new(false)), keyless_sandbox_observer: None, + keyless_sandbox_observer_empty: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -277,6 +294,7 @@ impl MegaContext { volatile_data_tracker: self.volatile_data_tracker, inside_sandbox: self.inside_sandbox, keyless_sandbox_observer: self.keyless_sandbox_observer, + keyless_sandbox_observer_empty: self.keyless_sandbox_observer_empty, system_address: self.system_address, } } @@ -353,6 +371,11 @@ impl MegaContext { /// external environments, the dynamic gas cost calculator and oracle environment /// are reinitialized with the new configurations. /// + /// Changing `ExtEnvs` resets the sandbox observer (both the parent-env slot + /// and the [`EmptyExternalEnv`] slot): the observer is parameterized by env + /// type and cannot be carried across this conversion. Attach the observer + /// after external environments are assembled. + /// /// # Arguments /// /// * `external_envs` - The new external environments configuration @@ -383,6 +406,7 @@ impl MegaContext { inside_sandbox: self.inside_sandbox, // Observer is parameterized by `ExtEnvs`; a type change cannot keep it. keyless_sandbox_observer: None, + keyless_sandbox_observer_empty: None, system_address: self.system_address, } } @@ -466,17 +490,56 @@ impl MegaContext { self } - /// Attaches an observer for nested sandbox execution. + /// Attaches an observer for nested sandbox execution on every spec. + /// + /// The observer must implement [`SandboxObserver`] for both this context's + /// [`ExtEnvs`] and [`EmptyExternalEnv`]. The same handle is stored as two + /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes + /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent + /// env). [`crate::sandbox::InspectorSandboxObserver`] with a fully generic + /// inspector satisfies both bounds. + /// + /// Attaching an observer does not change sandbox external-env semantics at + /// any spec. + /// + /// `None` restores the no-observer sandbox path and clears both slots. + /// Observation is read-only: mutating interpreter or context state through + /// the hooks is undefined and may diverge consensus. There is no take/drain + /// API; recorded data stays in the caller's observer. + pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static, + { + match observer { + Some(obs) => { + let cloned = Rc::clone(&obs); + let parent: Rc>> = cloned; + let empty: Rc>> = obs; + self.keyless_sandbox_observer = Some(parent); + self.keyless_sandbox_observer_empty = Some(empty); + } + None => { + self.keyless_sandbox_observer = None; + self.keyless_sandbox_observer_empty = None; + } + } + } + + /// Attaches an observer that implements [`SandboxObserver`] only for this + /// context's [`ExtEnvs`]. + /// + /// Opcode-level observation is available on REX4+ sandboxes, which share + /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit + /// only `sandbox_start` / `sandbox_end` through this handle. /// - /// `None` restores the no-observer sandbox path. Observation is read-only: - /// mutating interpreter or context state through the hooks is undefined - /// and may diverge consensus. There is no take/drain API; recorded data - /// stays in the caller's observer. - pub fn set_keyless_sandbox_observer( + /// Prefer [`Self::set_keyless_sandbox_observer`] when the observer + /// implements both env types. `None` clears both slots. + pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, ) { self.keyless_sandbox_observer = observer; + self.keyless_sandbox_observer_empty = None; } /// Gets the current total data size generated from transaction execution. @@ -809,6 +872,10 @@ mod tests { use crate::TestExternalEnvs; + struct NopObserver; + + impl SandboxObserver for NopObserver {} + #[test] fn test_with_cfg_updates_spec() { // Create context with initial spec @@ -906,4 +973,17 @@ mod tests { .new_account_gas(address!("0000000000000000000000000000000000100003")) .expect("bucket lookup against the supplied env should succeed"); } + + #[test] + fn test_with_external_envs_resets_sandbox_observer() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + assert!(context.keyless_sandbox_observer.is_some()); + assert!(context.keyless_sandbox_observer_empty.is_some()); + + let context = + context.with_external_envs(TestExternalEnvs::::new().into()); + assert!(context.keyless_sandbox_observer.is_none()); + assert!(context.keyless_sandbox_observer_empty.is_none()); + } } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index fd90940a..e54d45df 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -69,7 +69,10 @@ use revm::{ ExecuteEvm, InspectEvm, Inspector, Journal, }; -use crate::{sandbox::SandboxObserver, BucketId, ExternalEnvTypes, LimitUsage, MegaTransaction}; +use crate::{ + sandbox::SandboxObserver, BucketId, EmptyExternalEnv, ExternalEnvTypes, LimitUsage, + MegaTransaction, +}; /// The main EVM implementation for the `MegaETH` chain. /// @@ -234,15 +237,28 @@ impl MegaEvm { } impl MegaEvm { - /// Attaches an observer for nested sandbox execution. + /// Attaches an observer for nested sandbox execution on every spec. + /// + /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. The observer + /// must implement [`SandboxObserver`] for both this EVM's [`ExtEnvs`] and + /// [`EmptyExternalEnv`]. `None` restores the no-observer sandbox path. + pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static, + { + self.inner.ctx.set_keyless_sandbox_observer(observer); + } + + /// Attaches an observer that implements [`SandboxObserver`] only for this + /// EVM's [`ExtEnvs`]. /// - /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. `None` - /// restores the no-observer sandbox path. - pub fn set_keyless_sandbox_observer( + /// Forwards to [`MegaContext::set_keyless_sandbox_observer_for_parent_env`]. + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, ) { - self.inner.ctx.set_keyless_sandbox_observer(observer); + self.inner.ctx.set_keyless_sandbox_observer_for_parent_env(observer); } /// Provides a reference to the block environment. diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 43978e9a..24be4433 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -622,15 +622,17 @@ pub(crate) fn execute_keyless_deploy_sandbox::new_with_shared_ext_envs( sandbox_db, mega_spec, salt_env, oracle_env, ); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, observer) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, parent_observer) } else { let sandbox_ctx = MegaContext::new(sandbox_db, mega_spec); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, None) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, empty_observer) } } /// Applies the shared sandbox-context configuration, runs the sandbox tx, and returns the /// processed outcome. Factored out of `execute_keyless_deploy_sandbox` so the REX4+ and -/// pre-REX4 branches only differ in the `MegaContext` constructor they call. +/// pre-REX4 branches only differ in the `MegaContext` constructor and observer slot. fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( sandbox_ctx: MegaContext, ExtEnvs>, sandbox_tx: MegaTransaction, diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 7d5e9570..58615ebd 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -76,6 +76,15 @@ //! the parent is reported by [`SandboxEndOutcome::state_applied`]. With no observer //! attached the sandbox path is unchanged. //! +//! Observation does not change sandbox external-env semantics: pre-REX4 sandboxes +//! always run with [`crate::EmptyExternalEnv`] (minimum bucket capacity, no oracle +//! data), and REX4+ sandboxes always share the parent env. Opcode-level hooks on +//! pre-REX4 are delivered through a second observer slot typed against +//! [`crate::EmptyExternalEnv`]. Observers that only implement [`SandboxObserver`] +//! for the parent env type can use +//! [`crate::MegaContext::set_keyless_sandbox_observer_for_parent_env`]; pre-REX4 +//! then emits only `sandbox_start` / `sandbox_end`. +//! //! [`SandboxObserver`]: observer::SandboxObserver //! [`SandboxEndOutcome::state_applied`]: observer::SandboxEndOutcome::state_applied //! diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 9888c357..42e51dfc 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -21,6 +21,20 @@ //! that begins execution, and only when an observer is attached. Reverted //! inner frames still emit their events; whether sandbox state was applied to //! the parent is reported by [`SandboxEndOutcome::state_applied`]. +//! +//! # External-environment invariance +//! +//! Attaching an observer must not change sandbox env semantics at any spec. +//! Pre-REX4 sandboxes always run with [`crate::EmptyExternalEnv`]; REX4+ +//! sandboxes always share the parent env. Callers that implement +//! [`SandboxObserver`] for both the parent env type and +//! [`crate::EmptyExternalEnv`] attach via +//! [`crate::MegaContext::set_keyless_sandbox_observer`], which stores two +//! type-erased handles so opcode-level hooks fire on both paths. +//! [`InspectorSandboxObserver`] with a fully generic inspector satisfies both +//! bounds. An observer that only implements the parent env type can attach +//! via [`crate::MegaContext::set_keyless_sandbox_observer_for_parent_env`]; +//! pre-REX4 then emits only `sandbox_start` / `sandbox_end`. #[cfg(not(feature = "std"))] use alloc as std; @@ -465,4 +479,15 @@ mod tests { fn assert_observer>(_: T) {} assert_observer::(InspectorSandboxObserver(GenericInspector)); } + + #[test] + fn test_inspector_adapter_satisfies_empty_and_parent_env_observer_bounds() { + use crate::TestExternalEnvs; + fn assert_dual + SandboxObserver>( + _: T, + ) { + } + assert_dual(InspectorSandboxObserver(GenericInspector)); + assert_dual(NopObserver); + } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index ce7ee579..033ea11c 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -14,7 +14,7 @@ use mega_evm::{ }, test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, - MegaSpecId, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, + MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, MIN_BUCKET_SIZE, }; use revm::{ bytecode::opcode::{CALL, POP, PUSH0, RETURN, STATICCALL}, @@ -287,16 +287,19 @@ fn funded_db(signer: Address) -> MemoryDatabase { db } -struct RunConfig<'a> { +struct RunConfig<'a, O> { spec: MegaSpecId, db: &'a mut MemoryDatabase, tx_bytes: Bytes, gas_limit_override: u64, - observer: Option>>>, + observer: Option>>, tx_limits: Option, } -fn run_keyless(config: RunConfig<'_>) -> ResultAndState { +fn run_keyless(config: RunConfig<'_, O>) -> ResultAndState +where + O: SandboxObserver + 'static, +{ let mut context = MegaContext::new(config.db, config.spec); context.modify_chain(|chain| { chain.operator_fee_scalar = Some(U256::ZERO); @@ -349,8 +352,7 @@ fn parity_pair( let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); let mut db_obs = if fund_signer { funded_db(signer) } else { MemoryDatabase::default() }; let mut db_base = db_obs.clone(); - let observer: Rc>> = - Rc::new(RefCell::new(RecordingObserver::default())); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); let observed = run_keyless(RunConfig { spec, @@ -365,7 +367,7 @@ fn parity_pair( db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None, + observer: None::>>, tx_limits, }); assert_result_and_state_eq(&observed, &baseline, case); @@ -378,7 +380,7 @@ fn run_with_recorder( tx_limits: Option, ) -> (ResultAndState, Vec) { let recorder = Rc::new(RefCell::new(RecordingObserver::default())); - let observer: Rc>> = recorder.clone(); + let observer = Rc::clone(&recorder); let result = run_keyless(RunConfig { spec, db, @@ -427,6 +429,127 @@ fn assert_call_create_balanced(events: &[ObservedEvent]) { assert_eq!(create_depth, 0, "unbalanced create frames: {events:?}"); } +fn crowded_parent_env() -> TestExternalEnvs { + TestExternalEnvs::new() + .with_default_bucket_capacity((MIN_BUCKET_SIZE as u64) * 8) + .with_oracle_storage(U256::ZERO, U256::from(0x42)) +} + +fn run_keyless_with_parent_env( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + env: TestExternalEnvs, + observer: Option>>, +) -> ResultAndState +where + O: SandboxObserver + SandboxObserver + 'static, +{ + let mut context = MegaContext::new(db, spec).with_external_envs(env.into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer(observer); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") +} + +#[test] +fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { + for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db_obs = funded_db(signer); + let mut db_base = db_obs.clone(); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let env = crowded_parent_env(); + + let observed = run_keyless_with_parent_env( + spec, + &mut db_obs, + tx_bytes.clone(), + env.clone(), + Some(observer), + ); + let baseline = run_keyless_with_parent_env( + spec, + &mut db_base, + tx_bytes, + env, + None::>>, + ); + + assert!( + baseline.result.is_success(), + "{spec:?} baseline must succeed: {:?}", + baseline.result + ); + assert_result_and_state_eq( + &observed, + &baseline, + &format!("pre-REX4 nonempty parent env {spec:?}"), + ); + } +} + +#[test] +fn test_opcode_events_flow_on_pre_rex4_with_nonempty_parent_env() { + for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let result = run_keyless_with_parent_env( + spec, + &mut db, + tx_bytes, + crowded_parent_env(), + Some(Rc::clone(&recorder)), + ); + let events = recorder.borrow().events.clone(); + + assert!(result.result.is_success(), "{spec:?} should succeed: {:?}", result.result); + assert_single_start_end_pair(&events); + assert_call_create_balanced(&events); + assert!( + events.iter().any(|e| matches!(e, ObservedEvent::Create)), + "{spec:?}: pre-REX4 nonempty parent env must emit CREATE: {events:?}" + ); + assert!( + events.iter().any(|e| matches!(e, ObservedEvent::Step(_))), + "{spec:?}: pre-REX4 nonempty parent env must emit opcode steps: {events:?}" + ); + } +} + +#[test] +fn test_parent_env_only_observer_skips_pre_rex4_opcode_hooks() { + let spec = MegaSpecId::REX2; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let observer: Rc>> = recorder.clone(); + + let mut context = + MegaContext::new(&mut db, spec).with_external_envs(crowded_parent_env().into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer_for_parent_env(Some(observer)); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + let events = recorder.borrow().events.clone(); + + assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); + assert_single_start_end_pair(&events); + assert!( + !events.iter().any(|e| matches!(e, ObservedEvent::Step(_) | ObservedEvent::Create)), + "parent-env-only observer must not emit opcode hooks on pre-REX4: {events:?}" + ); +} + #[test] fn test_observer_parity_success_across_specs() { for spec in SPECS { @@ -665,9 +788,7 @@ fn test_event_order_create_wraps_steps() { fn test_inspector_adapter_records_sandbox_create_for_generic_inspector() { let tracer = GenericCreateCounter::default(); let creates = Rc::clone(&tracer.creates); - let adapter = InspectorSandboxObserver(tracer); - let observer: Rc>> = - Rc::new(RefCell::new(adapter)); + let observer = Rc::new(RefCell::new(InspectorSandboxObserver(tracer))); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); @@ -696,7 +817,7 @@ fn test_no_observer_skips_lifecycle_hooks() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None, + observer: None::>>, tx_limits: None, }); assert!(result.result.is_success(), "default path must still succeed"); From c80372a7a94321b3f5dbd5c703fe9fc18ee7e61c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 02:31:03 +0800 Subject: [PATCH 04/28] fix: close sandbox observer review findings Narrow observer call/create to shared inputs, split start-info gas fields, add a non-generic clear API, and cover revert/split-CREATE/ApplyFailed paths. --- crates/mega-evm/src/block/executor.rs | 12 +- crates/mega-evm/src/evm/context.rs | 72 ++++- crates/mega-evm/src/evm/mod.rs | 11 +- crates/mega-evm/src/sandbox/execution.rs | 120 ++++++--- crates/mega-evm/src/sandbox/mod.rs | 10 +- crates/mega-evm/src/sandbox/observer.rs | 69 +++-- crates/mega-evm/src/test_utils/database.rs | 21 +- .../tests/rex2/keyless_sandbox_observer.rs | 253 +++++++++++++++++- 8 files changed, 471 insertions(+), 97 deletions(-) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 6599d0b6..97a02a3e 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -156,8 +156,8 @@ where /// /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. The observer /// must implement [`crate::sandbox::SandboxObserver`] for both this executor's - /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. `None` restores the no-observer - /// sandbox path. + /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use + /// [`Self::clear_keyless_sandbox_observer`] to detach. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: crate::sandbox::SandboxObserver @@ -172,12 +172,20 @@ where /// /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + /// Use [`Self::clear_keyless_sandbox_observer`] to detach. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, ) { self.evm.set_keyless_sandbox_observer_for_parent_env(observer); } + + /// Detaches any sandbox observer from both env-type slots. + /// + /// Forwards to [`crate::MegaEvm::clear_keyless_sandbox_observer`]. + pub fn clear_keyless_sandbox_observer(&mut self) { + self.evm.clear_keyless_sandbox_observer(); + } } impl<'db, DB, C, R, INSP, ExtEnvs> diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 62fb0141..6bdadff4 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -83,9 +83,9 @@ pub struct MegaContext { /// Changing [`ExtEnvs`] via [`Self::with_external_envs`] resets this field /// and the [`EmptyExternalEnv`] observer slot: the observer cannot be /// carried across an env-type change and must be attached after external - /// environments are assembled. The observer is read-only: mutating - /// interpreter or context state through its hooks is undefined and may - /// diverge consensus. + /// environments are assembled. A compliant (read-only) observer does not + /// change execution results; mutating interpreter or context state through + /// its hooks is undefined and may diverge consensus. #[debug(ignore)] pub(crate) keyless_sandbox_observer: Option>>>, @@ -374,7 +374,8 @@ impl MegaContext { /// Changing `ExtEnvs` resets the sandbox observer (both the parent-env slot /// and the [`EmptyExternalEnv`] slot): the observer is parameterized by env /// type and cannot be carried across this conversion. Attach the observer - /// after external environments are assembled. + /// after external environments are assembled. Debug builds assert if an + /// observer is already attached (`set observer after external envs are wired`). /// /// # Arguments /// @@ -387,6 +388,11 @@ impl MegaContext { self, external_envs: ExternalEnvs, ) -> MegaContext { + debug_assert!( + self.keyless_sandbox_observer.is_none() && + self.keyless_sandbox_observer_empty.is_none(), + "set observer after external envs are wired", + ); let parent_block_number = self.inner.block.number.to::().saturating_sub(1); let spec = self.spec; let salt_env = Rc::new(external_envs.salt_env); @@ -502,10 +508,11 @@ impl MegaContext { /// Attaching an observer does not change sandbox external-env semantics at /// any spec. /// - /// `None` restores the no-observer sandbox path and clears both slots. - /// Observation is read-only: mutating interpreter or context state through - /// the hooks is undefined and may diverge consensus. There is no take/drain - /// API; recorded data stays in the caller's observer. + /// Use [`Self::clear_keyless_sandbox_observer`] to detach. Passing `None` + /// also clears both slots, but type inference for `O` often fails on that + /// path. Observation is read-only: mutating interpreter or context state + /// through the hooks is undefined and may diverge consensus. There is no + /// take/drain API; recorded data stays in the caller's observer. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: SandboxObserver + SandboxObserver + 'static, @@ -518,10 +525,7 @@ impl MegaContext { self.keyless_sandbox_observer = Some(parent); self.keyless_sandbox_observer_empty = Some(empty); } - None => { - self.keyless_sandbox_observer = None; - self.keyless_sandbox_observer_empty = None; - } + None => self.clear_keyless_sandbox_observer(), } } @@ -533,7 +537,8 @@ impl MegaContext { /// only `sandbox_start` / `sandbox_end` through this handle. /// /// Prefer [`Self::set_keyless_sandbox_observer`] when the observer - /// implements both env types. `None` clears both slots. + /// implements both env types. Use [`Self::clear_keyless_sandbox_observer`] + /// to detach. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, @@ -542,6 +547,15 @@ impl MegaContext { self.keyless_sandbox_observer_empty = None; } + /// Detaches any sandbox observer from both env-type slots. + /// + /// Restores the no-observer sandbox path. Prefer this over passing `None` to + /// [`Self::set_keyless_sandbox_observer`] when `O` cannot be inferred. + pub fn clear_keyless_sandbox_observer(&mut self) { + self.keyless_sandbox_observer = None; + self.keyless_sandbox_observer_empty = None; + } + /// Gets the current total data size generated from transaction execution. /// /// # Returns @@ -975,15 +989,45 @@ mod tests { } #[test] - fn test_with_external_envs_resets_sandbox_observer() { + fn test_clear_keyless_sandbox_observer_clears_both_slots() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); assert!(context.keyless_sandbox_observer.is_some()); assert!(context.keyless_sandbox_observer_empty.is_some()); + context.clear_keyless_sandbox_observer(); + assert!(context.keyless_sandbox_observer.is_none()); + assert!(context.keyless_sandbox_observer_empty.is_none()); + } + + #[test] + fn test_mega_evm_clear_keyless_sandbox_observer() { + use revm::handler::EvmTr; + + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + let mut evm = crate::MegaEvm::new(context); + evm.clear_keyless_sandbox_observer(); + assert!(evm.ctx_ref().keyless_sandbox_observer.is_none()); + assert!(evm.ctx_ref().keyless_sandbox_observer_empty.is_none()); + } + + #[test] + fn test_with_external_envs_without_observer_leaves_slots_empty() { + let context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); let context = context.with_external_envs(TestExternalEnvs::::new().into()); assert!(context.keyless_sandbox_observer.is_none()); assert!(context.keyless_sandbox_observer_empty.is_none()); } + + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "set observer after external envs are wired")] + fn test_with_external_envs_panics_in_debug_when_observer_is_attached() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + let _ = + context.with_external_envs(TestExternalEnvs::::new().into()); + } } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index e54d45df..f68643a6 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -241,7 +241,8 @@ impl MegaEvm { /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. The observer /// must implement [`SandboxObserver`] for both this EVM's [`ExtEnvs`] and - /// [`EmptyExternalEnv`]. `None` restores the no-observer sandbox path. + /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_observer`] to + /// detach. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: SandboxObserver + SandboxObserver + 'static, @@ -254,6 +255,7 @@ impl MegaEvm { /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + /// Use [`Self::clear_keyless_sandbox_observer`] to detach. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, @@ -261,6 +263,13 @@ impl MegaEvm { self.inner.ctx.set_keyless_sandbox_observer_for_parent_env(observer); } + /// Detaches any sandbox observer from both env-type slots. + /// + /// Forwards to [`MegaContext::clear_keyless_sandbox_observer`]. + pub fn clear_keyless_sandbox_observer(&mut self) { + self.inner.ctx.clear_keyless_sandbox_observer(); + } + /// Provides a reference to the block environment. /// /// The block environment contains information about the current block being processed, diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 24be4433..cac32902 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -246,7 +246,7 @@ pub fn execute_keyless_deploy_call // Step 4: validate `gasLimitOverride` covers the keyless tx's own gas limit. The // Rex5+ cap is deferred to step 4b below so it observes the materialization charge. let tx_gas_limit = keyless_tx.gas_limit(); - let mut gas_limit_override_u64: u64 = gas_limit_override.try_into().unwrap_or(u64::MAX); + let gas_limit_override_u64: u64 = gas_limit_override.try_into().unwrap_or(u64::MAX); if gas_limit_override_u64 < tx_gas_limit { return make_error!(KeylessDeployError::GasLimitTooLow { tx_gas_limit, @@ -317,24 +317,26 @@ pub fn execute_keyless_deploy_call } } - // Step 4b: Rex5+ cap of `gas_limit_override` to the outer's remaining gas. Runs after - // materialization has been debited so the reservation in step 8b only covers the - // sandbox envelope itself. + // Step 4b: Rex5+ cap of the granted sandbox gas to the outer's remaining gas. + // Runs after materialization has been debited so the reservation in step 8b + // only covers the sandbox envelope itself. Pre-REX5 grants the decoded override + // unchanged. // // After the cap, re-enforce the signer's "must execute with at least `tx_gas_limit`" // guarantee. The relayer can shrink the outer envelope so that `gas.remaining()` - // drops below the keyless `tx_gas_limit`, in which case the cap brings the override - // below the signed minimum. Rejecting here with the same `GasLimitTooLow` shape + // drops below the keyless `tx_gas_limit`, in which case the cap brings the granted + // gas below the signed minimum. Rejecting here with the same `GasLimitTooLow` shape // surfaces the failure cleanly instead of letting the sandbox OOG silently. The // pre-cap check above (Step 4) is retained so a relayer passing // `override < tx_gas_limit` outright still fails before the signer-recovery / // materialization work. + let mut effective_gas_limit = gas_limit_override_u64; if ctx.spec.is_enabled(MegaSpecId::REX5) { - gas_limit_override_u64 = gas_limit_override_u64.min(gas.remaining()); - if gas_limit_override_u64 < tx_gas_limit { + effective_gas_limit = effective_gas_limit.min(gas.remaining()); + if effective_gas_limit < tx_gas_limit { return make_error!(KeylessDeployError::GasLimitTooLow { tx_gas_limit, - provided_gas_limit: gas_limit_override_u64, + provided_gas_limit: effective_gas_limit, }); } } @@ -344,19 +346,14 @@ pub fn execute_keyless_deploy_call // (gas_price=0, source_hash set) so caller balance is never debited for gas; pre-Rex5 // keeps the original signed gas price. let sandbox_tx = if ctx.spec.is_enabled(MegaSpecId::REX5) { - build_fee_free_sandbox_deposit_tx( - deploy_signer, - &keyless_tx, - tx_bytes, - gas_limit_override_u64, - ) + build_fee_free_sandbox_deposit_tx(deploy_signer, &keyless_tx, tx_bytes, effective_gas_limit) } else { let tx = TxEnv { caller: deploy_signer, kind: TxKind::Create, data: keyless_tx.input().clone(), value: keyless_tx.value(), - gas_limit: gas_limit_override_u64, + gas_limit: effective_gas_limit, gas_price: keyless_tx.effective_gas_price(None), nonce: 0, ..Default::default() @@ -438,10 +435,10 @@ pub fn execute_keyless_deploy_call // `gas_limit_override.min(gas.remaining())` cap — there is no intervening gas // movement between the cap and this debit. if ctx.spec.is_enabled(MegaSpecId::REX5) { - let ok = gas.record_cost(gas_limit_override_u64); + let ok = gas.record_cost(effective_gas_limit); debug_assert!( ok, - "Rex5+ sandbox pre-debit must succeed: gas_limit_override is capped to gas.remaining()", + "Rex5+ sandbox pre-debit must succeed: effective_gas_limit is capped to gas.remaining()", ); } @@ -456,6 +453,7 @@ pub fn execute_keyless_deploy_call signer: deploy_signer, deploy_address, gas_limit_override: gas_limit_override_u64, + effective_gas_limit, tx_gas_limit, }); } @@ -470,7 +468,7 @@ pub fn execute_keyless_deploy_call &mut gas, limit_usage, volatile_accesses, - gas_limit_override_u64, + effective_gas_limit, gas_used, &return_memory_offset, ) { @@ -558,7 +556,7 @@ pub fn execute_keyless_deploy_call // upfront is intentionally retained, mirroring the upfront // `KEYLESS_DEPLOY_OVERHEAD_GAS` charge. if ctx.spec.is_enabled(MegaSpecId::REX5) { - gas.erase_cost(gas_limit_override_u64); + gas.erase_cost(effective_gas_limit); } notify_sandbox_end( &observer, @@ -1742,6 +1740,44 @@ mod tests { impl SandboxObserver for SplitNopObserver {} + fn assert_sandbox_outcomes_eq(left: &SandboxOutcome, right: &SandboxOutcome, case: &str) { + match (left, right) { + ( + SandboxOutcome::Completed { + state: left_state, + completion: left_completion, + volatile_accesses: left_accesses, + .. + }, + SandboxOutcome::Completed { + state: right_state, + completion: right_completion, + volatile_accesses: right_accesses, + .. + }, + ) => { + assert_eq!( + core::mem::discriminant(left_completion), + core::mem::discriminant(right_completion), + "{case}: completion kind" + ); + assert_eq!(left_completion.gas_used(), right_completion.gas_used(), "{case}: gas"); + assert_eq!(left_accesses, right_accesses, "{case}: volatile accesses"); + assert_eq!(left_state.len(), right_state.len(), "{case}: account count"); + for (addr, account) in left_state { + let other = right_state.get(addr).unwrap_or_else(|| { + panic!("{case}: missing account {addr:?} in observer state") + }); + assert_eq!(account, other, "{case}: account {addr:?}"); + } + } + (SandboxOutcome::Rejected(left_err), SandboxOutcome::Rejected(right_err)) => { + assert_eq!(left_err, right_err, "{case}: rejected error"); + } + _ => panic!("{case}: outcome variants differ: {left:?} vs {right:?}"), + } + } + fn run_split_create_fixture( spec: MegaSpecId, observer: Option>>>, @@ -1780,35 +1816,37 @@ mod tests { run_sandbox_ctx(context, tx, Some(limits), block, chain, observer) } - fn split_outcome_fingerprint(outcome: &SandboxOutcome) -> (bool, Option, u64) { + fn split_create_slot(outcome: &SandboxOutcome) -> (bool, Option) { match outcome { - SandboxOutcome::Completed { state, completion, .. } => { + SandboxOutcome::Completed { state, .. } => { let deployed = SPLIT_CREATE_CALLER.create(0); let created = state.get(&deployed).is_some_and(|account| account.is_created()); let slot = state.get(&deployed).and_then(|account| { account.storage.get(&SPLIT_CREATE_SLOT).map(|slot| slot.present_value()) }); - (created, slot, completion.gas_used()) + (created, slot) } - SandboxOutcome::Rejected(_) => (false, None, 0), + SandboxOutcome::Rejected(_) => (false, None), } } #[test] fn test_observer_parity_pre_rex5_split_create_commits_sstore() { - let baseline = run_split_create_fixture(MegaSpecId::REX4, None); - let observer: Rc>> = - Rc::new(RefCell::new(SplitNopObserver)); - let observed = run_split_create_fixture(MegaSpecId::REX4, Some(observer)); - - let (created, slot, gas) = split_outcome_fingerprint(&baseline); - assert!(created, "pre-REX5 split CREATE must leave the account created"); - assert_eq!(slot, Some(U256::from(0x2a)), "SSTORE must survive the failed deploy"); - assert_eq!( - split_outcome_fingerprint(&observed), - (created, slot, gas), - "observer must not change split-CREATE sandbox outcome" - ); + for spec in [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4] { + let baseline = run_split_create_fixture(spec, None); + let observer: Rc>> = + Rc::new(RefCell::new(SplitNopObserver)); + let observed = run_split_create_fixture(spec, Some(observer)); + + let (created, slot) = split_create_slot(&baseline); + assert!(created, "{spec:?}: pre-REX5 split CREATE must leave the account created"); + assert_eq!( + slot, + Some(U256::from(0x2a)), + "{spec:?}: SSTORE must survive the failed deploy" + ); + assert_sandbox_outcomes_eq(&observed, &baseline, &format!("split-CREATE {spec:?}")); + } } #[test] @@ -1818,13 +1856,9 @@ mod tests { Rc::new(RefCell::new(SplitNopObserver)); let observed = run_split_create_fixture(MegaSpecId::REX5, Some(observer)); - let (created, slot, gas) = split_outcome_fingerprint(&baseline); + let (created, slot) = split_create_slot(&baseline); assert!(!created, "REX5 atomic CREATE failure must not leave a created account"); assert!(slot.is_none() || slot == Some(U256::ZERO)); - assert_eq!( - split_outcome_fingerprint(&observed), - (created, slot, gas), - "observer must not change atomic CREATE-failure outcome" - ); + assert_sandbox_outcomes_eq(&observed, &baseline, "atomic CREATE-failure REX5"); } } diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 58615ebd..bec595e8 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -71,10 +71,12 @@ //! attach a [`SandboxObserver`] via [`crate::MegaContext::set_keyless_sandbox_observer`] //! (also forwarded from [`crate::MegaEvm`] and [`crate::MegaBlockExecutor`]). The observer //! sees interpreter hooks inside the sandbox plus a paired `sandbox_start` / `sandbox_end` -//! lifecycle. Observation is read-only and cannot short-circuit `CALL`/`CREATE`. -//! Reverted inner frames still emit their events; whether sandbox state was applied to -//! the parent is reported by [`SandboxEndOutcome::state_applied`]. With no observer -//! attached the sandbox path is unchanged. +//! lifecycle. Observation cannot short-circuit `CALL`/`CREATE`. A compliant (read-only) +//! observer does not change execution results; no such guarantee is made for an observer +//! that mutates interpreter or context state. Reverted inner frames still emit their +//! events; whether sandbox state was applied to the parent is reported by +//! [`SandboxEndOutcome::state_applied`]. With no observer attached the sandbox path is +//! unchanged. //! //! Observation does not change sandbox external-env semantics: pre-REX4 sandboxes //! always run with [`crate::EmptyExternalEnv`] (minimum bucket capacity, no oracle diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 42e51dfc..2000f26c 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -8,19 +8,30 @@ //! //! # Read-only contract //! -//! Implementations must not mutate interpreter or context state through the -//! `&mut` hook arguments. Doing so is not structurally prevented for every -//! argument (the signatures match revm so generic inspectors can adapt) and -//! may cause consensus divergence; debug builds already trip conservation -//! asserts on many such mutations. +//! Two layers close intervention: +//! +//! - **Structural.** [`SandboxObserver::call`] / [`SandboxObserver::create`] take shared references +//! to [`CallInputs`] / [`CreateInputs`]. [`InspectorSandboxObserver`] clones a temporary copy +//! when forwarding to an inner [`Inspector`]; mutations of that copy are discarded, as are `Some` +//! override outcomes. Combined with [`ObserverBridge`] always returning `None`, rewriting inputs +//! and short-circuiting the frame are both closed. +//! - **Contractual.** [`SandboxObserver::step`] / [`SandboxObserver::step_end`] take `&mut +//! Interpreter` and hooks take `&mut MegaContext` because those types are not cheaply cloneable. +//! Implementations must not mutate them. Debug builds already trip conservation asserts on many +//! such mutations. +//! +//! Attaching a compliant (read-only) observer does not change sandbox execution +//! results. No such guarantee is made for an observer that mutates interpreter +//! or context state. //! //! # Lifecycle //! //! [`sandbox_start`](SandboxObserver::sandbox_start) and //! [`sandbox_end`](SandboxObserver::sandbox_end) fire exactly once per sandbox -//! that begins execution, and only when an observer is attached. Reverted -//! inner frames still emit their events; whether sandbox state was applied to -//! the parent is reported by [`SandboxEndOutcome::state_applied`]. +//! attempt, and only when an observer is attached. A validate-reject path that +//! never constructs a sandbox EVM still delivers the pair. Reverted inner +//! frames still emit their events; whether sandbox state was applied to the +//! parent is reported by [`SandboxEndOutcome::state_applied`]. //! //! # External-environment invariance //! @@ -64,8 +75,12 @@ pub struct SandboxStartInfo { pub signer: Address, /// Deterministic deploy address derived from the signer. pub deploy_address: Address, - /// Gas limit override actually supplied to the sandbox (after any cap). + /// Caller-supplied gas limit override exactly as decoded from the payload. pub gas_limit_override: u64, + /// Gas limit actually granted to the sandbox after outer-gas capping + /// (REX5+ caps to the outer frame's remaining gas; pre-REX5 equals + /// [`Self::gas_limit_override`]). + pub effective_gas_limit: u64, /// Gas limit carried by the signed keyless transaction itself. pub tx_gas_limit: u64, } @@ -127,8 +142,11 @@ impl SandboxEndOutcome { /// breaking change. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` /// keep the trait object-safe. /// -/// INVARIANT: hooks observe; they must not mutate execution state. [`ObserverBridge`] -/// structurally drops `call`/`create` override outcomes. +/// A compliant (read-only) observer does not change sandbox execution results. +/// [`call`](Self::call) / [`create`](Self::create) cannot rewrite inputs or +/// override the frame; [`step`](Self::step) and context arguments remain +/// contractually read-only. [`ObserverBridge`] always drops `call`/`create` +/// override outcomes. pub trait SandboxObserver { /// Called once after the sandbox interpreter is created, before the first opcode. #[inline] @@ -178,7 +196,7 @@ pub trait SandboxObserver { /// Called when a call frame is about to start. Cannot override the call. #[inline] - fn call(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &mut CallInputs) { + fn call(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &CallInputs) { let _ = context; let _ = inputs; } @@ -198,11 +216,7 @@ pub trait SandboxObserver { /// Called when a create frame is about to start. Cannot override the create. #[inline] - fn create( - &mut self, - context: &mut MegaContext, ExtEnvs>, - inputs: &mut CreateInputs, - ) { + fn create(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &CreateInputs) { let _ = context; let _ = inputs; } @@ -235,6 +249,11 @@ pub trait SandboxObserver { } /// Called once with the terminal sandbox outcome. Paired with [`Self::sandbox_start`]. + /// + /// When an internal database error aborts sandbox execution, the opcode-level + /// event stream may truncate on an unclosed frame (the upstream inspect path + /// propagates the error with `?`). [`sandbox_end`](Self::sandbox_end) is still + /// delivered and is the terminus of the event stream. #[inline] fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { let _ = outcome; @@ -243,9 +262,9 @@ pub trait SandboxObserver { /// Adapts any HRTB-compatible revm [`Inspector`] into a [`SandboxObserver`]. /// -/// `call`/`create` override outcomes from the inner inspector are discarded. -/// [`SandboxObserver::sandbox_start`] and [`SandboxObserver::sandbox_end`] keep -/// their default empty implementations. +/// `call`/`create` clone a temporary input copy for the inner inspector; that +/// copy and any override outcome are discarded. [`SandboxObserver::sandbox_start`] +/// and [`SandboxObserver::sandbox_end`] keep their default empty implementations. pub struct InspectorSandboxObserver(pub I); impl core::fmt::Debug for InspectorSandboxObserver { @@ -297,8 +316,9 @@ where } #[inline] - fn call(&mut self, context: &mut MegaContext, E>, inputs: &mut CallInputs) { - let _ = self.0.call(context, inputs); + fn call(&mut self, context: &mut MegaContext, E>, inputs: &CallInputs) { + let mut inputs = inputs.clone(); + let _ = self.0.call(context, &mut inputs); } #[inline] @@ -314,8 +334,9 @@ where } #[inline] - fn create(&mut self, context: &mut MegaContext, E>, inputs: &mut CreateInputs) { - let _ = self.0.create(context, inputs); + fn create(&mut self, context: &mut MegaContext, E>, inputs: &CreateInputs) { + let mut inputs = inputs.clone(); + let _ = self.0.create(context, &mut inputs); } #[inline] diff --git a/crates/mega-evm/src/test_utils/database.rs b/crates/mega-evm/src/test_utils/database.rs index 4bfb1817..7c521b18 100644 --- a/crates/mega-evm/src/test_utils/database.rs +++ b/crates/mega-evm/src/test_utils/database.rs @@ -138,8 +138,13 @@ pub struct ErrorInjectingDatabase { #[deref] #[deref_mut] inner: MemoryDatabase, - /// When set, `basic()` calls for this address return an error. + /// When set, `basic()` calls for this address return an error after + /// [`Self::fail_on_account_skip`] successful matching lookups. pub fail_on_account: Option
, + /// Matching `basic()` calls to let succeed before injecting the error. + /// `0` fails on the first matching call. + pub fail_on_account_skip: usize, + fail_on_account_hits: usize, /// When set, `storage()` calls for this (address, key) return an error. pub fail_on_storage: Option<(Address, StorageKey)>, /// When set, `code_by_hash()` calls for this hash return an error. Lets a code load fail @@ -151,7 +156,14 @@ pub struct ErrorInjectingDatabase { impl ErrorInjectingDatabase { /// Creates a new `ErrorInjectingDatabase` wrapping the given [`MemoryDatabase`]. pub fn new(inner: MemoryDatabase) -> Self { - Self { inner, fail_on_account: None, fail_on_storage: None, fail_on_code_by_hash: None } + Self { + inner, + fail_on_account: None, + fail_on_account_skip: 0, + fail_on_account_hits: 0, + fail_on_storage: None, + fail_on_code_by_hash: None, + } } } @@ -160,7 +172,10 @@ impl revm::Database for ErrorInjectingDatabase { fn basic(&mut self, address: Address) -> Result, Self::Error> { if self.fail_on_account == Some(address) { - return Err(InjectedDbError(format!("injected basic() error for {address}"))); + self.fail_on_account_hits = self.fail_on_account_hits.saturating_add(1); + if self.fail_on_account_hits > self.fail_on_account_skip { + return Err(InjectedDbError(format!("injected basic() error for {address}"))); + } } self.inner.basic(address).map_err(|e| match e {}) } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 033ea11c..8924b9cd 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -7,17 +7,18 @@ use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U2 use alloy_sol_types::SolCall; use mega_evm::{ alloy_consensus::{Signed, TxLegacy}, + constants, revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ decode_error_result, InspectorSandboxObserver, SandboxCompletionKind, SandboxEndOutcome, SandboxObserver, SandboxRejectKind, SandboxStartInfo, }, - test_utils::{BytecodeBuilder, MemoryDatabase}, + test_utils::{BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, MIN_BUCKET_SIZE, }; use revm::{ - bytecode::opcode::{CALL, POP, PUSH0, RETURN, STATICCALL}, + bytecode::opcode::{BALANCE, CALL, POP, PUSH0, RETURN, STATICCALL}, context::TxEnv, handler::EvmTr, inspector::NoOpInspector, @@ -34,6 +35,8 @@ const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; const SIGNED_TX_GAS_LIMIT: u64 = 1_000_000; const REVERTER: Address = address!("0000000000000000000000000000000000aaaaaa"); const IDENTITY_PRECOMPILE: Address = address!("0000000000000000000000000000000000000004"); +const MERGE_FAIL_SENTINEL: Address = address!("0000000000000000000000000000000000bbbbbb"); +const DEFAULT_OUTER_GAS_LIMIT: u64 = 1_000_000_000_000; const SPECS: [MegaSpecId; 5] = [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4, MegaSpecId::REX5, MegaSpecId::REX6]; @@ -45,6 +48,7 @@ enum ObservedEvent { signer: Address, deploy_address: Address, gas_limit_override: u64, + effective_gas_limit: u64, tx_gas_limit: u64, }, End(SandboxEndOutcome), @@ -96,7 +100,7 @@ impl SandboxObserver for RecordingObserver { fn call( &mut self, _context: &mut mega_evm::MegaContext, E>, - inputs: &mut CallInputs, + inputs: &CallInputs, ) { self.events.push(ObservedEvent::Call { target: inputs.target_address }); } @@ -113,7 +117,7 @@ impl SandboxObserver for RecordingObserver { fn create( &mut self, _context: &mut mega_evm::MegaContext, E>, - _inputs: &mut CreateInputs, + _inputs: &CreateInputs, ) { self.events.push(ObservedEvent::Create); } @@ -146,6 +150,7 @@ impl SandboxObserver for RecordingObserver { signer: info.signer, deploy_address: info.deploy_address, gas_limit_override: info.gas_limit_override, + effective_gas_limit: info.effective_gas_limit, tx_gas_limit: info.tx_gas_limit, }); } @@ -209,6 +214,46 @@ fn constructor_calls_reverter() -> Bytes { .build() } +fn constructor_touches_sentinel() -> Bytes { + BytecodeBuilder::default() + .push_address(MERGE_FAIL_SENTINEL) + .append(BALANCE) + .append(POP) + .sstore(U256::from(0), U256::from(1)) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +fn split_create_initcode() -> Bytes { + let code_len = 8_000u32.to_be_bytes(); + Bytes::from(vec![ + revm::bytecode::opcode::PUSH1, + 0x2a, + revm::bytecode::opcode::PUSH1, + 0x02, + revm::bytecode::opcode::SSTORE, + revm::bytecode::opcode::PUSH1, + 0x16, + revm::bytecode::opcode::PUSH1, + 0x00, + revm::bytecode::opcode::KECCAK256, + revm::bytecode::opcode::POP, + revm::bytecode::opcode::PUSH3, + code_len[1], + code_len[2], + code_len[3], + revm::bytecode::opcode::PUSH1, + 0x00, + revm::bytecode::opcode::RETURN, + ]) +} + fn constructor_calls_identity_precompile() -> Bytes { BytecodeBuilder::default() .push_number(0_u8) @@ -261,6 +306,18 @@ fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { fn keyless_deploy_call_tx( keyless_deployment_tx: Bytes, gas_limit_override: u64, +) -> MegaTransaction { + keyless_deploy_call_tx_with_outer_gas( + keyless_deployment_tx, + gas_limit_override, + DEFAULT_OUTER_GAS_LIMIT, + ) +} + +fn keyless_deploy_call_tx_with_outer_gas( + keyless_deployment_tx: Bytes, + gas_limit_override: u64, + outer_gas_limit: u64, ) -> MegaTransaction { let call_data = IKeylessDeploy::keylessDeployCall { keylessDeploymentTransaction: keyless_deployment_tx, @@ -272,7 +329,7 @@ fn keyless_deploy_call_tx( kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), data: call_data.into(), value: U256::ZERO, - gas_limit: 1_000_000_000_000, + gas_limit: outer_gas_limit, gas_price: 0, ..Default::default() }; @@ -294,6 +351,7 @@ struct RunConfig<'a, O> { gas_limit_override: u64, observer: Option>>, tx_limits: Option, + outer_gas_limit: u64, } fn run_keyless(config: RunConfig<'_, O>) -> ResultAndState @@ -310,7 +368,11 @@ where } context.set_keyless_sandbox_observer(config.observer); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(config.tx_bytes, config.gas_limit_override); + let tx = keyless_deploy_call_tx_with_outer_gas( + config.tx_bytes, + config.gas_limit_override, + config.outer_gas_limit, + ); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") } @@ -361,6 +423,7 @@ fn parity_pair( gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: Some(observer), tx_limits, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); let baseline = run_keyless(RunConfig { spec, @@ -369,6 +432,7 @@ fn parity_pair( gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: None::>>, tx_limits, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); assert_result_and_state_eq(&observed, &baseline, case); } @@ -388,6 +452,7 @@ fn run_with_recorder( gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: Some(observer), tx_limits, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); let events = recorder.borrow().events.clone(); (result, events) @@ -493,6 +558,43 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { } } +#[test] +fn test_observer_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert() { + for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_constructor()); + let mut db_obs = funded_db(signer); + let mut db_base = db_obs.clone(); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let env = crowded_parent_env(); + + let observed = run_keyless_with_parent_env( + spec, + &mut db_obs, + tx_bytes.clone(), + env.clone(), + Some(observer), + ); + let baseline = run_keyless_with_parent_env( + spec, + &mut db_base, + tx_bytes, + env, + None::>>, + ); + + assert!( + baseline.result.is_success(), + "{spec:?} constructor revert is a success-style outer return: {:?}", + baseline.result + ); + assert_result_and_state_eq( + &observed, + &baseline, + &format!("pre-REX4 nonempty parent env revert {spec:?}"), + ); + } +} + #[test] fn test_opcode_events_flow_on_pre_rex4_with_nonempty_parent_env() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { @@ -615,12 +717,14 @@ fn test_sandbox_start_end_pair_on_successful_deploy() { signer: start_signer, deploy_address: start_deploy, gas_limit_override, + effective_gas_limit, tx_gas_limit, } => { assert_eq!(*start_spec, spec); assert_eq!(*start_signer, signer); assert_eq!(*start_deploy, deploy_address); assert_eq!(*gas_limit_override, LARGE_GAS_LIMIT_OVERRIDE); + assert_eq!(*effective_gas_limit, LARGE_GAS_LIMIT_OVERRIDE); assert_eq!(*tx_gas_limit, SIGNED_TX_GAS_LIMIT); } other => panic!("expected Start, got {other:?}"), @@ -799,6 +903,7 @@ fn test_inspector_adapter_records_sandbox_create_for_generic_inspector() { gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: Some(observer), tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); assert!( @@ -819,6 +924,142 @@ fn test_no_observer_skips_lifecycle_hooks() { gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: None::>>, tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); assert!(result.result.is_success(), "default path must still succeed"); } + +#[test] +fn test_sandbox_start_info_rex5_caps_effective_gas_limit_below_override() { + const OVERRIDE: u64 = LARGE_GAS_LIMIT_OVERRIDE; + const OUTER_GAS: u64 = 5_000_000; + + for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let result = run_keyless(RunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: OVERRIDE, + observer: Some(Rc::clone(&recorder)), + tx_limits: None, + outer_gas_limit: OUTER_GAS, + }); + assert!( + result.result.is_success(), + "{spec:?} capped deploy should succeed: {:?}", + result.result + ); + + let events = recorder.borrow().events.clone(); + assert_single_start_end_pair(&events); + match start_and_end(&events).0 { + ObservedEvent::Start { + gas_limit_override, effective_gas_limit, tx_gas_limit, .. + } => { + assert_eq!(*gas_limit_override, OVERRIDE, "{spec:?}: decoded override"); + assert_eq!(*tx_gas_limit, SIGNED_TX_GAS_LIMIT); + assert!( + *effective_gas_limit < OVERRIDE, + "{spec:?}: effective_gas_limit ({effective_gas_limit}) must be capped below override ({OVERRIDE})" + ); + assert!( + *effective_gas_limit >= SIGNED_TX_GAS_LIMIT, + "{spec:?}: effective_gas_limit ({effective_gas_limit}) must still cover the signed tx gas limit" + ); + assert!( + *effective_gas_limit + constants::rex2::KEYLESS_DEPLOY_OVERHEAD_GAS + <= OUTER_GAS, + "{spec:?}: effective_gas_limit plus dispatch overhead must fit in the outer envelope" + ); + } + other => panic!("{spec:?}: expected Start, got {other:?}"), + } + } +} + +#[test] +fn test_observer_parity_split_create_through_interceptor() { + const COMPUTE_BUDGET: u64 = 1_000_000; + let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(COMPUTE_BUDGET); + + for spec in [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(split_create_initcode()); + let mut db_obs = funded_db(signer); + let mut db_base = db_obs.clone(); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); + + let observed = run_keyless(RunConfig { + spec, + db: &mut db_obs, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(observer), + tx_limits: Some(limits), + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let baseline = run_keyless(RunConfig { + spec, + db: &mut db_base, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: Some(limits), + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + assert_result_and_state_eq( + &observed, + &baseline, + &format!("split-CREATE interceptor {spec:?}"), + ); + } +} + +#[test] +fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { + let spec = MegaSpecId::REX5; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_touches_sentinel()); + let mut inner = funded_db(signer); + inner.set_account_balance(MERGE_FAIL_SENTINEL, U256::from(1)); + + let mut db = ErrorInjectingDatabase::new(inner); + db.fail_on_account = Some(MERGE_FAIL_SENTINEL); + // Occupancy / sandbox execution load the sentinel once; merge inspects it again. + db.fail_on_account_skip = 1; + + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let mut context = MegaContext::new(&mut db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer(Some(Rc::clone(&recorder))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); + let events = recorder.borrow().events.clone(); + + assert!( + matches!(result.result, ExecutionResult::Revert { .. }), + "merge DB error must revert the outer call: {:?}", + result.result + ); + let error = match &result.result { + ExecutionResult::Revert { output, .. } => decode_error_result(output), + other => panic!("expected revert, got {other:?}"), + }; + assert!( + matches!(error, Some(mega_evm::sandbox::KeylessDeployError::InternalError)), + "apply failure surfaces as InternalError, got {error:?}" + ); + assert_single_start_end_pair(&events); + match start_and_end(&events).1 { + ObservedEvent::End(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::ApplyFailed, + }) => {} + other => panic!("expected NotApplied(ApplyFailed), got {other:?}"), + } +} From 7e8582a350ef0fa0fe935193b854f86350295b58 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 10:22:30 +0800 Subject: [PATCH 05/28] test: pin split-CREATE interceptor overflow and override saturation Rebuild the keyless interceptor split-CREATE fixture so sandbox compute crosses the pre-REX5 200M default, assert the split shape before parity, and document u64 saturation of gas_limit_override. --- crates/mega-evm/src/sandbox/observer.rs | 5 +- .../tests/rex2/keyless_sandbox_observer.rs | 193 +++++++++++++++--- 2 files changed, 165 insertions(+), 33 deletions(-) diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 2000f26c..2bd51a31 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -75,7 +75,10 @@ pub struct SandboxStartInfo { pub signer: Address, /// Deterministic deploy address derived from the signer. pub deploy_address: Address, - /// Caller-supplied gas limit override exactly as decoded from the payload. + /// Caller-supplied gas limit override decoded from the payload. + /// + /// The ABI value is a `U256` and is saturating-converted to `u64`, so a + /// payload larger than [`u64::MAX`] is reported as [`u64::MAX`]. pub gas_limit_override: u64, /// Gas limit actually granted to the sandbox after outer-gas capping /// (REX5+ caps to the outer frame's remaining gas; pre-REX5 equals diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 8924b9cd..80c8a60b 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -10,15 +10,15 @@ use mega_evm::{ constants, revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ - decode_error_result, InspectorSandboxObserver, SandboxCompletionKind, SandboxEndOutcome, - SandboxObserver, SandboxRejectKind, SandboxStartInfo, + decode_error_result, InspectorSandboxObserver, KeylessDeployError, SandboxCompletionKind, + SandboxEndOutcome, SandboxObserver, SandboxRejectKind, SandboxStartInfo, }, test_utils::{BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, MIN_BUCKET_SIZE, }; use revm::{ - bytecode::opcode::{BALANCE, CALL, POP, PUSH0, RETURN, STATICCALL}, + bytecode::opcode::{BALANCE, CALL, MSTORE, POP, PUSH0, RETURN, STATICCALL}, context::TxEnv, handler::EvmTr, inspector::NoOpInspector, @@ -230,28 +230,31 @@ fn constructor_touches_sentinel() -> Bytes { .build() } +/// Words of memory expanded by the constructor `MSTORE`. +/// +/// Pre-REX5 sandboxes ignore the parent's compute limit and run at the spec +/// default of 200M (`rex::TX_COMPUTE_GAS_LIMIT`). An 8_000-byte code deposit +/// is only 1.6M compute, so the constructor has to land in the window +/// `(200M - 1.6M, 200M)` and then `RETURN` `8_000` bytes so the deposit charge +/// splits the CREATE. Memory-expansion gas is `3*words + words²/512`; `318_191` +/// words (~10.18 MiB) costs `198_699_714`, which sits in that window without a +/// multi-million-iteration loop. +const SPLIT_CREATE_MEM_WORDS: u64 = 318_191; +const SPLIT_CREATE_MEM_OFFSET: u32 = (SPLIT_CREATE_MEM_WORDS * 32 - 32) as u32; +const SPLIT_CREATE_CODE_LEN: u32 = 8_000; +const SPLIT_CREATE_SLOT: u64 = 2; +const SPLIT_CREATE_SLOT_VALUE: u64 = 0x2a; + fn split_create_initcode() -> Bytes { - let code_len = 8_000u32.to_be_bytes(); - Bytes::from(vec![ - revm::bytecode::opcode::PUSH1, - 0x2a, - revm::bytecode::opcode::PUSH1, - 0x02, - revm::bytecode::opcode::SSTORE, - revm::bytecode::opcode::PUSH1, - 0x16, - revm::bytecode::opcode::PUSH1, - 0x00, - revm::bytecode::opcode::KECCAK256, - revm::bytecode::opcode::POP, - revm::bytecode::opcode::PUSH3, - code_len[1], - code_len[2], - code_len[3], - revm::bytecode::opcode::PUSH1, - 0x00, - revm::bytecode::opcode::RETURN, - ]) + BytecodeBuilder::default() + .sstore(U256::from(SPLIT_CREATE_SLOT), U256::from(SPLIT_CREATE_SLOT_VALUE)) + .push_number(0_u8) + .push_number(SPLIT_CREATE_MEM_OFFSET) + .append(MSTORE) + .push_number(SPLIT_CREATE_CODE_LEN) + .push_number(0_u8) + .append(RETURN) + .build() } fn constructor_calls_identity_precompile() -> Bytes { @@ -318,10 +321,22 @@ fn keyless_deploy_call_tx_with_outer_gas( keyless_deployment_tx: Bytes, gas_limit_override: u64, outer_gas_limit: u64, +) -> MegaTransaction { + keyless_deploy_call_tx_with_override_u256( + keyless_deployment_tx, + U256::from(gas_limit_override), + outer_gas_limit, + ) +} + +fn keyless_deploy_call_tx_with_override_u256( + keyless_deployment_tx: Bytes, + gas_limit_override: U256, + outer_gas_limit: u64, ) -> MegaTransaction { let call_data = IKeylessDeploy::keylessDeployCall { keylessDeploymentTransaction: keyless_deployment_tx, - gasLimitOverride: U256::from(gas_limit_override), + gasLimitOverride: gas_limit_override, } .abi_encode(); let tx = TxEnv { @@ -404,6 +419,83 @@ fn assert_result_and_state_eq( } } +/// Loud split-CREATE shape: constructor finished (SSTORE stuck, account created) but the +/// sandbox reported `ExecutionFailed` and the ABI does not claim a deploy. An empty spin +/// that actually deployed would fail here as `Applied(Deployed)` / empty `errorData`. +fn assert_split_create_shape( + spec: MegaSpecId, + result: &ResultAndState, + signer: Address, + events: Option<&[ObservedEvent]>, +) { + if let Some(events) = events { + assert_single_start_end_pair(events); + match start_and_end(events).1 { + ObservedEvent::End(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, + .. + }) => {} + other => panic!("{spec:?}: expected Applied(ExecutionFailed), got {other:?}"), + } + } + + assert!( + result.result.is_success(), + "{spec:?}: split CREATE is a success-style outer return: {:?}", + result.result + ); + let output = result.result.output().expect("split CREATE outer output"); + let ret = IKeylessDeploy::keylessDeployCall::abi_decode_returns(output) + .expect("split CREATE ABI return"); + assert_eq!( + ret.deployedAddress, + Address::ZERO, + "{spec:?}: split CREATE must not report a deployed address" + ); + assert!( + !ret.errorData.is_empty(), + "{spec:?}: split CREATE must carry errorData (empty spin succeeds with empty errorData)" + ); + let error = decode_error_result(&ret.errorData); + assert!( + matches!( + error, + Some( + KeylessDeployError::ExecutionHalted { .. } | + KeylessDeployError::ExecutionReverted { .. } + ) + ), + "{spec:?}: split CREATE errorData must be halt/revert, got {error:?}" + ); + + let deploy_address = signer.create(0); + let account = result.state.get(&deploy_address).unwrap_or_else(|| { + panic!("{spec:?}: split CREATE must leave a state entry at {deploy_address}") + }); + assert!( + account.is_created(), + "{spec:?}: deploy address must be is_created(); status={:?}", + account.status + ); + let slot = account.storage.get(&U256::from(SPLIT_CREATE_SLOT)).map(|slot| slot.present_value()); + assert_eq!( + slot, + Some(U256::from(SPLIT_CREATE_SLOT_VALUE)), + "{spec:?}: constructor SSTORE must survive the failed code deposit" + ); + + // Pre-REX5 `return_create` commits the runtime blob before the code-deposit compute + // charge marks the frame as exceeding, so the account still carries the 8_000-byte + // code. The ABI `deployedAddress == 0` above is the "not deployed" signal. + let code_len = account.info.code.as_ref().map(|c| c.len()).unwrap_or(0); + assert!( + code_len == SPLIT_CREATE_CODE_LEN as usize || + account.info.code_hash != revm::primitives::KECCAK_EMPTY, + "{spec:?}: pre-REX5 split commits returned bytecode; code_len={code_len} hash={:?}", + account.info.code_hash + ); +} + fn parity_pair( spec: MegaSpecId, init_code: Bytes, @@ -982,22 +1074,24 @@ fn test_sandbox_start_info_rex5_caps_effective_gas_limit_below_override() { #[test] fn test_observer_parity_split_create_through_interceptor() { - const COMPUTE_BUDGET: u64 = 1_000_000; - let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(COMPUTE_BUDGET); - + // Parent 1M compute is not forwarded into pre-REX5 sandboxes; the sandbox + // runs at the 200M spec default. Do not set a parent limit — the initcode + // itself must overflow that 200M default. EVM gas and compute gas are + // independent: the 10B override / 1T outer envelope cover ~199M memory + // expansion plus 80M code-deposit storage gas. for spec in [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(split_create_initcode()); let mut db_obs = funded_db(signer); let mut db_base = db_obs.clone(); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); let observed = run_keyless(RunConfig { spec, db: &mut db_obs, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(observer), - tx_limits: Some(limits), + observer: Some(Rc::clone(&recorder)), + tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); let baseline = run_keyless(RunConfig { @@ -1006,10 +1100,13 @@ fn test_observer_parity_split_create_through_interceptor() { tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, observer: None::>>, - tx_limits: Some(limits), + tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); + let events = recorder.borrow().events.clone(); + assert_split_create_shape(spec, &observed, signer, Some(&events)); + assert_split_create_shape(spec, &baseline, signer, None); assert_result_and_state_eq( &observed, &baseline, @@ -1018,6 +1115,38 @@ fn test_observer_parity_split_create_through_interceptor() { } } +#[test] +fn test_sandbox_start_info_saturates_gas_limit_override_above_u64_max() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + + let mut context = MegaContext::new(&mut db, MegaSpecId::REX5); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer(Some(Rc::clone(&recorder))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = + keyless_deploy_call_tx_with_override_u256(tx_bytes, U256::MAX, DEFAULT_OUTER_GAS_LIMIT); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); + + let events = recorder.borrow().events.clone(); + assert_single_start_end_pair(&events); + match start_and_end(&events).0 { + ObservedEvent::Start { gas_limit_override, .. } => { + assert_eq!( + *gas_limit_override, + u64::MAX, + "payload > u64::MAX must saturate gas_limit_override to u64::MAX" + ); + } + other => panic!("expected Start, got {other:?}"), + } +} + #[test] fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { let spec = MegaSpecId::REX5; From a96e795bda59cd5280f58f7a3a2768a727454fba Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 10:37:28 +0800 Subject: [PATCH 06/28] test: assert split CREATE committed code length and hash independently --- .../tests/rex2/keyless_sandbox_observer.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 80c8a60b..3b549adf 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -487,11 +487,17 @@ fn assert_split_create_shape( // Pre-REX5 `return_create` commits the runtime blob before the code-deposit compute // charge marks the frame as exceeding, so the account still carries the 8_000-byte // code. The ABI `deployedAddress == 0` above is the "not deployed" signal. + // Length and hash are checked independently: a non-empty deposit of the wrong size + // (e.g. 7999) must fail rather than pass via the hash arm of a disjunction. let code_len = account.info.code.as_ref().map(|c| c.len()).unwrap_or(0); - assert!( - code_len == SPLIT_CREATE_CODE_LEN as usize || - account.info.code_hash != revm::primitives::KECCAK_EMPTY, - "{spec:?}: pre-REX5 split commits returned bytecode; code_len={code_len} hash={:?}", + assert_eq!( + code_len, SPLIT_CREATE_CODE_LEN as usize, + "{spec:?}: pre-REX5 split commits 8000-byte returned bytecode; code_len={code_len}" + ); + assert_ne!( + account.info.code_hash, + revm::primitives::KECCAK_EMPTY, + "{spec:?}: pre-REX5 split commits non-empty code_hash; hash={:?}", account.info.code_hash ); } From 38fe24c53c92478d0b1b460f02f72b8627eee86d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 11:04:58 +0800 Subject: [PATCH 07/28] feat(mega-evme): include keyless sandbox frames in traces Attach InspectorSandboxObserver when --trace is on. Sandbox CREATE is depth 0 in its own journal, so it is recorded separately and spliced under the outer KeylessDeploy CALL. --- bin/mega-evme/src/common/trace.rs | 398 +++++++++++++++++++++++++- bin/mega-evme/src/replay/cmd.rs | 21 +- docs/mega-evme/tracing/call-tracer.md | 1 + docs/mega-evme/tracing/overview.md | 3 + 4 files changed, 407 insertions(+), 16 deletions(-) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 0086f07e..c771da93 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -1,8 +1,12 @@ //! Trace configuration for mega-evme -use std::path::PathBuf; +use std::{ + cell::{Ref, RefCell}, + path::PathBuf, + rc::Rc, +}; -use alloy_primitives::Bytes; +use alloy_primitives::{Address, Bytes, Log, U256}; use alloy_rpc_types_trace::geth::{ CallConfig, CallFrame, GethDefaultTracingOptions, PreStateConfig, }; @@ -14,16 +18,164 @@ use mega_evm::{ ContextTr, }, database::DatabaseRef, + inspector::JournalExt, + interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}, state::EvmState, - ExecuteEvm, InspectEvm, + ExecuteEvm, InspectEvm, Inspector, }, - MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, + sandbox::InspectorSandboxObserver, + MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, }; -use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; +use revm_inspectors::tracing::{types::TraceMemberOrder, TracingInspector, TracingInspectorConfig}; use tracing::{debug, info, trace}; use super::{EvmeError, EvmeExternalEnvs, EvmeState}; +/// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. +/// +/// Nested keyless-deploy sandbox execution is a separate EVM, so a parent inspector +/// never sees those frames. This wrapper is the outer inspector and, cloned, the +/// inner inspector of [`InspectorSandboxObserver`]. +/// +/// Outer and sandbox use *two* of these handles. The sandbox journal reports depth 0 +/// for its top-level CREATE; recording that into the outer inspector would overwrite +/// the CALL root (`CallTraceArena` treats depth 0 as the entry). After the transaction +/// returns, [`splice_sandbox_traces`] grafts the sandbox arena under the `KeylessDeploy` +/// CALL. +#[derive(Clone, Debug)] +pub(crate) struct RcTracingInspector(Rc>); + +impl RcTracingInspector { + /// Wraps an existing [`TracingInspector`]. + pub(crate) fn new(inspector: TracingInspector) -> Self { + Self(Rc::new(RefCell::new(inspector))) + } + + /// Resets recorded traces so a subsequent transaction starts from a clean arena. + pub(crate) fn fuse(&self) { + self.0.borrow_mut().fuse(); + } + + /// Borrows the inner inspector. + pub(crate) fn borrow(&self) -> Ref<'_, TracingInspector> { + self.0.borrow() + } + + /// Observer handle for [`MegaEvm::set_keyless_sandbox_observer`]. + pub(crate) fn as_sandbox_observer(&self) -> Rc>> { + Rc::new(RefCell::new(InspectorSandboxObserver(self.clone()))) + } +} + +impl Inspector for RcTracingInspector +where + CTX: ContextTr, +{ + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.borrow_mut().step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.borrow_mut().step_end(interp, context); + } + + fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.borrow_mut().log(interp, context, log); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.borrow_mut().call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.borrow_mut().call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.borrow_mut().create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.borrow_mut().create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + >::selfdestruct( + &mut self.0.borrow_mut(), + contract, + target, + value, + ); + } +} + +/// Returns true when `sandbox` recorded at least one completed frame. +fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { + sandbox.traces().nodes().iter().any(|node| { + node.trace.status.is_some() || !node.trace.steps.is_empty() || !node.children.is_empty() + }) +} + +/// Attaches `sandbox` traces as children of the outer [`KEYLESS_DEPLOY_ADDRESS`] CALL. +/// +/// Sandbox execution uses a fresh journal whose top-level CREATE is depth 0. Recording +/// that into the outer inspector would replace the CALL root, so the sandbox is traced +/// on a sibling inspector and grafted here. Depths and arena indices are rewritten so +/// the Geth call tree hangs the CREATE under the CALL. +fn splice_sandbox_traces(outer: &RcTracingInspector, sandbox: &RcTracingInspector) { + let sandbox_inspector = sandbox.borrow(); + if !sandbox_has_frames(&sandbox_inspector) { + return; + } + + let mut outer_inspector = outer.0.borrow_mut(); + let parent_idx = outer_inspector + .traces() + .nodes() + .iter() + .rposition(|node| { + node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() + }) + .unwrap_or(0); + let depth_offset = outer_inspector.traces().nodes()[parent_idx].trace.depth + 1; + let base = outer_inspector.traces().nodes().len(); + + let remapped: Vec<_> = sandbox_inspector + .traces() + .nodes() + .iter() + .cloned() + .enumerate() + .map(|(old_idx, mut node)| { + node.idx = base + old_idx; + node.trace.depth += depth_offset; + for step in &mut node.trace.steps { + step.depth += depth_offset as u64; + } + node.parent = Some(match node.parent { + None => parent_idx, + Some(parent) => base + parent, + }); + for child in &mut node.children { + *child += base; + } + node + }) + .collect(); + drop(sandbox_inspector); + + outer_inspector.traces_mut().nodes_mut().extend(remapped); + let parent = &mut outer_inspector.traces_mut().nodes_mut()[parent_idx]; + let child_slot = parent.children.len(); + parent.children.push(base); + parent.ordering.push(TraceMemberOrder::Call(child_slot)); +} + /// Tracer type for execution analysis #[derive(Debug, Clone, Copy, ValueEnum, Default)] #[non_exhaustive] @@ -212,6 +364,18 @@ impl TraceArgs { } } + /// Splices sandbox frames under the outer `KeylessDeploy` CALL and renders the trace. + pub(crate) fn generate_trace_with_sandbox( + &self, + outer: &RcTracingInspector, + sandbox: &RcTracingInspector, + result_and_state: &ResultAndState, + prestate: impl DatabaseRef, + ) -> String { + splice_sandbox_traces(outer, sandbox); + self.generate_trace(&outer.borrow(), result_and_state, prestate) + } + /// Execute transaction with optional tracing pub fn execute_transaction( &self, @@ -224,17 +388,18 @@ impl TraceArgs { { if self.is_tracing_enabled() { info!(tracer = ?self.tracer, "Evm executing with tracing"); - // Execute with tracing inspector - let mut inspector = self.create_inspector(); - let mut evm = MegaEvm::new(evm_context).with_inspector(&mut inspector); + let outer = RcTracingInspector::new(self.create_inspector()); + let sandbox = RcTracingInspector::new(self.create_inspector()); + let mut evm = MegaEvm::new(evm_context).with_inspector(outer.clone()); + evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); let result_and_state = evm .inspect_tx(tx) .map_err(|e| EvmeError::ExecutionError(format!("EVM execution failed: {:?}", e)))?; trace!(result_and_state = ?result_and_state, "Evm execution result and state"); - // Generate trace string based on tracer type - let trace_str = self.generate_trace(evm.inspector, &result_and_state, evm.db_ref()); + let trace_str = + self.generate_trace_with_sandbox(&outer, &sandbox, &result_and_state, evm.db_ref()); trace!(trace_str = ?trace_str, "Generated trace"); Ok((result_and_state.result, result_and_state.state, Some(trace_str))) @@ -251,3 +416,216 @@ impl TraceArgs { } } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::{transaction::Recovered, Signed, TxLegacy}; + use alloy_primitives::{address, hex, Signature, TxKind, B256}; + use alloy_sol_types::SolCall; + use mega_evm::{ + alloy_evm::EvmEnv, + alloy_op_evm::block::OpAlloyReceiptBuilder, + revm::{ + bytecode::opcode::{CODECOPY, RETURN, SSTORE}, + context::{BlockEnv, CfgEnv, TxEnv}, + database::StateBuilder, + }, + test_utils::{BytecodeBuilder, MemoryDatabase}, + BlockLimits, EmptyExternalEnv, IKeylessDeploy, MegaBlockExecutionCtx, + MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, + }; + + const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); + const LARGE_GAS_LIMIT_OVERRIDE: u64 = 10_000_000_000; + const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; + const SIGNED_TX_GAS_LIMIT: u64 = 1_000_000; + const OUTER_GAS_LIMIT: u64 = 1_000_000_000_000; + + fn success_constructor() -> Bytes { + BytecodeBuilder::default() + .sstore(U256::from(0), U256::from(1)) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() + } + + fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: SIGNED_TX_GAS_LIMIT, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let r = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let s = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let sig = Signature::new(r, s, false); + let signed = Signed::new_unchecked(tx, sig, B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + let tx_bytes = Bytes::from(buf); + let signer = signed.recover_signer().expect("should recover signer"); + (tx_bytes, signer) + } + + fn keyless_deploy_tx(keyless_deployment_tx: Bytes) -> MegaTransaction { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_deployment_tx, + gasLimitOverride: U256::from(LARGE_GAS_LIMIT_OVERRIDE), + } + .abi_encode(); + let tx = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + data: call_data.into(), + value: U256::ZERO, + gas_limit: OUTER_GAS_LIMIT, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + tx + } + + fn funded_db(signer: Address) -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_balance(signer, U256::from(LARGE_SIGNER_BALANCE)); + db.set_account_balance(TEST_CALLER, U256::from(LARGE_SIGNER_BALANCE)); + db + } + + fn keyless_context( + db: &mut MemoryDatabase, + ) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX5); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context + } + + fn run_traced_keyless( + outer: RcTracingInspector, + sandbox: RcTracingInspector, + ) -> (mega_evm::revm::context::result::ExecutionResult, Address) { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let context = keyless_context(&mut db); + let mut evm = MegaEvm::new(context).with_inspector(outer); + evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + let result = evm.inspect_tx(keyless_deploy_tx(tx_bytes)).expect("keyless deploy"); + assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); + (result.result, signer) + } + + #[test] + fn test_keyless_call_trace_nests_sandbox_create() { + let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (exec_result, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); + splice_sandbox_traces(&outer, &sandbox); + + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS, "root is KeylessDeploy CALL"); + assert!(!nodes[0].trace.kind.is_any_create(), "root stays a CALL, not overwritten CREATE"); + assert!(!nodes[0].children.is_empty(), "sandbox CREATE is a child of the CALL"); + + let create = &nodes[nodes[0].children[0]]; + assert!(create.trace.kind.is_any_create(), "child is the sandbox CREATE"); + assert_eq!(create.trace.depth, nodes[0].trace.depth + 1); + assert!( + create.trace.steps.iter().any(|s| s.op.get() == SSTORE), + "sandbox constructor steps are on the CREATE node" + ); + + let call_frame: CallFrame = outer_ref.geth_builder().geth_call_traces( + CallConfig { only_top_call: Some(false), with_log: Some(false) }, + exec_result.gas_used(), + ); + assert_eq!(call_frame.typ, "CALL"); + assert_eq!(call_frame.to, Some(KEYLESS_DEPLOY_ADDRESS)); + assert_eq!(call_frame.calls.len(), 1, "CREATE nested under KeylessDeploy CALL"); + assert_eq!(call_frame.calls[0].typ, "CREATE"); + } + + #[test] + fn test_replay_executor_nests_sandbox_create() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let mut state = StateBuilder::new().with_database(&mut db).with_bundle_update().build(); + + let evm_factory = MegaEvmFactory::new().with_external_env_factory(EvmeExternalEnvs::new()); + let hardforks = MegaHardforkConfig::default().with_all_activated(); + let factory = MegaBlockExecutorFactory::new( + &hardforks, + evm_factory, + OpAlloyReceiptBuilder::default(), + ); + + let mut cfg_env = CfgEnv::default(); + cfg_env.spec = MegaSpecId::REX5; + let evm_env = + EvmEnv::new(cfg_env, BlockEnv { gas_limit: OUTER_GAS_LIMIT, ..Default::default() }); + let block_ctx = MegaBlockExecutionCtx::new( + B256::ZERO, + Some(B256::ZERO), + Bytes::new(), + BlockLimits::no_limits(), + ); + + let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let mut executor = + factory.create_executor_with_inspector(&mut state, block_ctx, evm_env, outer.clone()); + // Skip `apply_pre_execution_changes`: it needs a configured SequencerRegistry on + // REX5+, and KeylessDeploy interception does not require the predeploy bytecode. + executor.inspector_mut().fuse(); + executor.evm.ctx.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + executor.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: tx_bytes, + gasLimitOverride: U256::from(LARGE_GAS_LIMIT_OVERRIDE), + } + .abi_encode(); + let tx_legacy = TxLegacy { + chain_id: Some(1), + nonce: 0, + gas_price: 0, + gas_limit: OUTER_GAS_LIMIT, + to: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + value: U256::ZERO, + input: call_data.into(), + }; + let signed = Signed::new_unchecked(tx_legacy, Signature::test_signature(), B256::ZERO); + let recovered = Recovered::new_unchecked(MegaTxEnvelope::Legacy(signed), TEST_CALLER); + + let outcome = executor.run_transaction(&recovered).expect("run keyless deploy"); + assert!(outcome.inner.result.is_success(), "{:?}", outcome.inner.result); + + splice_sandbox_traces(&outer, &sandbox); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS); + assert!(!nodes[0].children.is_empty()); + assert!(nodes[nodes[0].children[0]].trace.kind.is_any_create()); + } +} diff --git a/bin/mega-evme/src/replay/cmd.rs b/bin/mega-evme/src/replay/cmd.rs index 572bb722..f481a009 100644 --- a/bin/mega-evme/src/replay/cmd.rs +++ b/bin/mega-evme/src/replay/cmd.rs @@ -26,7 +26,8 @@ use crate::{ common::{ op_receipt_to_tx_receipt, parse_bucket_capacity, print_execution_summary, print_execution_trace, print_receipt, BuildProviderOutput, EvmeExternalEnvs, EvmeOutcome, - ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RpcCacheStore, TxOverrideArgs, + ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RcTracingInspector, RpcCacheStore, + TxOverrideArgs, }, replay::get_hardfork_config, run, ChainArgs, EvmeState, @@ -470,14 +471,18 @@ impl Cmd { ); let start = Instant::now(); - let mut inspector = self.trace_args.create_inspector(); + let outer = RcTracingInspector::new(self.trace_args.create_inspector()); + let sandbox = self + .trace_args + .is_tracing_enabled() + .then(|| RcTracingInspector::new(self.trace_args.create_inspector())); let mut state = StateBuilder::new().with_database(&mut database).with_bundle_update().build(); let mut block_executor = block_executor_factory.create_executor_with_inspector( &mut state, block_ctx, evm_env, - &mut inspector, + outer.clone(), ); block_executor @@ -521,6 +526,9 @@ impl Cmd { .unwrap_or(0); block_executor.inspector_mut().fuse(); + if let Some(sandbox) = &sandbox { + block_executor.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + } let outcome = block_executor .run_transaction(wrapped_tx) .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?; @@ -541,9 +549,10 @@ impl Cmd { state: evm_state.clone(), }; - let trace_data = self.trace_args.is_tracing_enabled().then(|| { - self.trace_args.generate_trace( - block_executor.inspector(), + let trace_data = sandbox.as_ref().map(|sandbox| { + self.trace_args.generate_trace_with_sandbox( + &outer, + sandbox, &result_and_state, block_executor.evm().db_ref(), ) diff --git a/docs/mega-evme/tracing/call-tracer.md b/docs/mega-evme/tracing/call-tracer.md index ee089a6a..5436db85 100644 --- a/docs/mega-evme/tracing/call-tracer.md +++ b/docs/mega-evme/tracing/call-tracer.md @@ -6,6 +6,7 @@ description: Capture the nested CALL/CREATE tree with gas usage, return data, an The call tracer records the nested tree of CALL, STATICCALL, DELEGATECALL, CALLCODE, and CREATE operations. It shows which contracts called which, with gas usage and return data at each level. +A KeylessDeploy call includes the sandbox CREATE (and any calls it makes) as nested children of that CALL. ## Usage diff --git a/docs/mega-evme/tracing/overview.md b/docs/mega-evme/tracing/overview.md index e5a3a37f..f2e46f43 100644 --- a/docs/mega-evme/tracing/overview.md +++ b/docs/mega-evme/tracing/overview.md @@ -18,6 +18,9 @@ mega-evme run 0x60016000526001601ff3 --trace By default, this uses the [opcode tracer](opcode-tracer.md). Use `--tracer` to pick a different tracer. +When tracing is enabled, nested KeylessDeploy sandbox execution is included automatically: the inner CREATE and its child frames appear under the outer call to the KeylessDeploy system contract. +No extra flag is required. + ## Tracers | Tracer | `--tracer` value | Description | From 6311c67605d13e5858aa8d821520f8d271a8f381 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 26 Aug 2026 11:37:07 +0800 Subject: [PATCH 08/28] fix(mega-evme): include sandbox steps in default opcode traces Intercepted KeylessDeploy records no bytecode, so geth_traces skipped the spliced CREATE. Graft a CREATE step so struct-log nests constructor ops in execution order. --- bin/mega-evme/src/common/trace.rs | 131 ++++++++++++++++++++++-- docs/mega-evme/tracing/opcode-tracer.md | 1 + 2 files changed, 122 insertions(+), 10 deletions(-) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index c771da93..0e0bf211 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -13,6 +13,7 @@ use alloy_rpc_types_trace::geth::{ use clap::{Parser, ValueEnum}; use mega_evm::{ revm::{ + bytecode::opcode::{self, OpCode}, context::{ result::{ExecutionResult, ResultAndState}, ContextTr, @@ -26,7 +27,10 @@ use mega_evm::{ sandbox::InspectorSandboxObserver, MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, }; -use revm_inspectors::tracing::{types::TraceMemberOrder, TracingInspector, TracingInspectorConfig}; +use revm_inspectors::tracing::{ + types::{CallTraceStep, TraceMemberOrder}, + TracingInspector, TracingInspectorConfig, +}; use tracing::{debug, info, trace}; use super::{EvmeError, EvmeExternalEnvs, EvmeState}; @@ -127,6 +131,11 @@ fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { /// that into the outer inspector would replace the CALL root, so the sandbox is traced /// on a sibling inspector and grafted here. Depths and arena indices are rewritten so /// the Geth call tree hangs the CREATE under the CALL. +/// +/// `geth_traces` only descends into a child when the parent frame has a matching +/// call-like opcode in `steps`. Intercepted `KeylessDeploy` records no bytecode, so +/// a CREATE step is grafted onto the parent; without it, sandbox constructor +/// steps are omitted from struct-log output rather than nested in execution order. fn splice_sandbox_traces(outer: &RcTracingInspector, sandbox: &RcTracingInspector) { let sandbox_inspector = sandbox.borrow(); if !sandbox_has_frames(&sandbox_inspector) { @@ -173,9 +182,57 @@ fn splice_sandbox_traces(outer: &RcTracingInspector, sandbox: &RcTracingInspecto let parent = &mut outer_inspector.traces_mut().nodes_mut()[parent_idx]; let child_slot = parent.children.len(); parent.children.push(base); + + // geth_traces pairs call-like parent steps with `children` in order. A + // missing CREATE step here would leave the grafted child unreachable. + let calllike = parent.trace.steps.iter().filter(|s| is_calllike_opcode(s.op.get())).count(); + if calllike < parent.children.len() { + let step_idx = parent.trace.steps.len(); + let create_depth = parent.trace.depth as u64 + 1; + let contract = parent.trace.address; + let gas_remaining = parent.trace.gas_limit; + parent.trace.steps.push(intercepted_create_step(create_depth, contract, gas_remaining)); + parent.ordering.push(TraceMemberOrder::Step(step_idx)); + } parent.ordering.push(TraceMemberOrder::Call(child_slot)); } +/// CREATE/CALL-family opcodes that `geth_traces` uses to walk into children. +fn is_calllike_opcode(op: u8) -> bool { + matches!( + op, + opcode::CALL | + opcode::DELEGATECALL | + opcode::STATICCALL | + opcode::CREATE | + opcode::CALLCODE | + opcode::CREATE2 + ) +} + +/// Placeholder CREATE recorded on an intercepted `KeylessDeploy` CALL so struct-log +/// output can descend into the spliced sandbox frame. +fn intercepted_create_step(depth: u64, contract: Address, gas_remaining: u64) -> CallTraceStep { + CallTraceStep { + depth, + pc: 0, + op: OpCode::new(opcode::CREATE).expect("CREATE is a defined opcode"), + contract, + stack: None, + push_stack: None, + memory: None, + returndata: Bytes::new(), + gas_remaining, + gas_refund_counter: 0, + gas_used: 0, + gas_cost: 0, + storage_change: None, + status: None, + immediate_bytes: None, + decoded: None, + } +} + /// Tracer type for execution analysis #[derive(Debug, Clone, Copy, ValueEnum, Default)] #[non_exhaustive] @@ -521,22 +578,25 @@ mod tests { fn run_traced_keyless( outer: RcTracingInspector, sandbox: RcTracingInspector, - ) -> (mega_evm::revm::context::result::ExecutionResult, Address) { + ) -> (ResultAndState, MemoryDatabase, Address) { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let context = keyless_context(&mut db); - let mut evm = MegaEvm::new(context).with_inspector(outer); - evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); - let result = evm.inspect_tx(keyless_deploy_tx(tx_bytes)).expect("keyless deploy"); - assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); - (result.result, signer) + let result = { + let context = keyless_context(&mut db); + let mut evm = MegaEvm::new(context).with_inspector(outer); + evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + let result = evm.inspect_tx(keyless_deploy_tx(tx_bytes)).expect("keyless deploy"); + assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); + result + }; + (result, db, signer) } #[test] fn test_keyless_call_trace_nests_sandbox_create() { let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let (exec_result, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); + let (result_and_state, _db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); splice_sandbox_traces(&outer, &sandbox); let outer_ref = outer.borrow(); @@ -555,7 +615,7 @@ mod tests { let call_frame: CallFrame = outer_ref.geth_builder().geth_call_traces( CallConfig { only_top_call: Some(false), with_log: Some(false) }, - exec_result.gas_used(), + result_and_state.result.gas_used(), ); assert_eq!(call_frame.typ, "CALL"); assert_eq!(call_frame.to, Some(KEYLESS_DEPLOY_ADDRESS)); @@ -563,6 +623,57 @@ mod tests { assert_eq!(call_frame.calls[0].typ, "CREATE"); } + #[test] + fn test_keyless_opcode_trace_includes_sandbox_steps_in_execution_order() { + let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (result_and_state, db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); + splice_sandbox_traces(&outer, &sandbox); + + let outer_ref = outer.borrow(); + let geth_trace = outer_ref.geth_builder().geth_traces( + result_and_state.result.gas_used(), + Bytes::new(), + GethDefaultTracingOptions::default(), + ); + let logs = &geth_trace.struct_logs; + let ops: Vec<_> = logs.iter().map(|s| format!("d{}:{}", s.depth, s.op)).collect(); + + let create = logs + .iter() + .position(|s| s.op == "CREATE") + .unwrap_or_else(|| panic!("CREATE missing from structLogs: {ops:?}")); + let sstore = logs.iter().position(|s| s.op == "SSTORE").unwrap_or_else(|| { + panic!("sandbox constructor SSTORE missing from structLogs: {ops:?}") + }); + let first_outer_after_create = logs + .iter() + .skip(create + 1) + .position(|s| s.depth <= logs[create].depth) + .map(|rel| create + 1 + rel) + .unwrap_or(logs.len()); + + assert!(create < sstore, "sandbox SSTORE should follow the outer CREATE: {ops:?}"); + assert_eq!( + sstore, + create + 3, + "SSTORE should be the constructor's third opcode after CREATE, not delayed: {ops:?}" + ); + assert!( + logs[sstore].depth > logs[create].depth, + "sandbox SSTORE should nest deeper than CREATE: {ops:?}" + ); + assert!( + sstore < first_outer_after_create, + "sandbox SSTORE should sit inside the CREATE nest, not after later parent steps: {ops:?}" + ); + + outer_ref + .geth_builder() + .geth_prestate_traces(&result_and_state, &PreStateConfig::default(), &*db) + .expect("prestate tracer after sandbox splice"); + } + #[test] fn test_replay_executor_nests_sandbox_create() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); diff --git a/docs/mega-evme/tracing/opcode-tracer.md b/docs/mega-evme/tracing/opcode-tracer.md index 62f3f1c7..966a4d41 100644 --- a/docs/mega-evme/tracing/opcode-tracer.md +++ b/docs/mega-evme/tracing/opcode-tracer.md @@ -6,6 +6,7 @@ description: Log every instruction with gas, stack, memory, and storage at each The opcode tracer records every EVM instruction executed, along with gas costs, stack state, memory, and storage changes. This is the default tracer when you pass `--trace` without specifying `--tracer`. +A KeylessDeploy call includes the sandbox constructor's opcodes in `structLogs`, nested under that call in execution order. ## Usage From f1dd0172208597953ed2f13fba8f7475612c645e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:00:06 +0800 Subject: [PATCH 09/28] test: compare keyless sandbox observer usage field-by-field --- crates/mega-evm/src/limit/limit.rs | 2 +- .../tests/rex2/keyless_sandbox_observer.rs | 363 ++---------------- .../tests/rex2/keyless_sandbox_support.rs | 356 +++++++++++++++++ crates/mega-evm/tests/rex2/main.rs | 1 + 4 files changed, 391 insertions(+), 331 deletions(-) create mode 100644 crates/mega-evm/tests/rex2/keyless_sandbox_support.rs diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 2bcec8b8..3fa5bd8c 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -110,7 +110,7 @@ pub struct AdditionalLimit { } /// The usage of the additional limits. -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct LimitUsage { /// The data size usage in bytes. pub data_size: u64, diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 3b549adf..06ec19e1 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -3,24 +3,20 @@ use std::{cell::RefCell, rc::Rc}; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_primitives::{hex, Address, Bytes, U256}; use alloy_sol_types::SolCall; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, constants, revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ decode_error_result, InspectorSandboxObserver, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, SandboxObserver, SandboxRejectKind, SandboxStartInfo, }, - test_utils::{BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase}, - EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, - MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, MIN_BUCKET_SIZE, + test_utils::{ErrorInjectingDatabase, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, + TestExternalEnvs, }; use revm::{ - bytecode::opcode::{BALANCE, CALL, MSTORE, POP, PUSH0, RETURN, STATICCALL}, - context::TxEnv, - handler::EvmTr, inspector::NoOpInspector, interpreter::{ interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, @@ -29,17 +25,17 @@ use revm::{ Inspector, }; -const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); -const LARGE_GAS_LIMIT_OVERRIDE: u64 = 10_000_000_000; -const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; -const SIGNED_TX_GAS_LIMIT: u64 = 1_000_000; -const REVERTER: Address = address!("0000000000000000000000000000000000aaaaaa"); -const IDENTITY_PRECOMPILE: Address = address!("0000000000000000000000000000000000000004"); -const MERGE_FAIL_SENTINEL: Address = address!("0000000000000000000000000000000000bbbbbb"); -const DEFAULT_OUTER_GAS_LIMIT: u64 = 1_000_000_000_000; - -const SPECS: [MegaSpecId; 5] = - [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4, MegaSpecId::REX5, MegaSpecId::REX6]; +use super::keyless_sandbox_support::{ + assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_precompile, + constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, + create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, + keyless_deploy_call_tx, keyless_deploy_call_tx_with_override_u256, parent_compute_gas_used, + revert_constructor, run_keyless, run_keyless_with_parent_env, + run_keyless_with_parent_env_usage, run_keyless_with_usage, split_create_initcode, + success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_PRECOMPILE, + LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, SIGNED_TX_GAS_LIMIT, SPECS, + SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, +}; #[derive(Clone, Debug, PartialEq, Eq)] enum ObservedEvent { @@ -172,253 +168,6 @@ impl Inspector for GenericCreateCounter { } } -fn success_constructor() -> Bytes { - BytecodeBuilder::default() - .sstore(U256::from(0), U256::from(1)) - .push_number(1_u8) - .push_number(0_u8) - .push_number(0_u8) - .append(revm::bytecode::opcode::CODECOPY) - .push_number(1_u8) - .push_number(0_u8) - .append(RETURN) - .build() -} - -fn revert_constructor() -> Bytes { - Bytes::from_static(&hex!("60006000fd")) -} - -fn empty_code_constructor() -> Bytes { - BytecodeBuilder::default().append_many([PUSH0, PUSH0, RETURN]).build() -} - -fn constructor_calls_reverter() -> Bytes { - BytecodeBuilder::default() - .push_number(0_u8) - .push_number(0_u8) - .push_number(0_u8) - .push_number(0_u8) - .push_number(0_u8) - .push_address(REVERTER) - .push_number(50_000_u32) - .append(CALL) - .append(POP) - .push_number(1_u8) - .push_number(0_u8) - .push_number(0_u8) - .append(revm::bytecode::opcode::CODECOPY) - .push_number(1_u8) - .push_number(0_u8) - .append(RETURN) - .build() -} - -fn constructor_touches_sentinel() -> Bytes { - BytecodeBuilder::default() - .push_address(MERGE_FAIL_SENTINEL) - .append(BALANCE) - .append(POP) - .sstore(U256::from(0), U256::from(1)) - .push_number(1_u8) - .push_number(0_u8) - .push_number(0_u8) - .append(revm::bytecode::opcode::CODECOPY) - .push_number(1_u8) - .push_number(0_u8) - .append(RETURN) - .build() -} - -/// Words of memory expanded by the constructor `MSTORE`. -/// -/// Pre-REX5 sandboxes ignore the parent's compute limit and run at the spec -/// default of 200M (`rex::TX_COMPUTE_GAS_LIMIT`). An 8_000-byte code deposit -/// is only 1.6M compute, so the constructor has to land in the window -/// `(200M - 1.6M, 200M)` and then `RETURN` `8_000` bytes so the deposit charge -/// splits the CREATE. Memory-expansion gas is `3*words + words²/512`; `318_191` -/// words (~10.18 MiB) costs `198_699_714`, which sits in that window without a -/// multi-million-iteration loop. -const SPLIT_CREATE_MEM_WORDS: u64 = 318_191; -const SPLIT_CREATE_MEM_OFFSET: u32 = (SPLIT_CREATE_MEM_WORDS * 32 - 32) as u32; -const SPLIT_CREATE_CODE_LEN: u32 = 8_000; -const SPLIT_CREATE_SLOT: u64 = 2; -const SPLIT_CREATE_SLOT_VALUE: u64 = 0x2a; - -fn split_create_initcode() -> Bytes { - BytecodeBuilder::default() - .sstore(U256::from(SPLIT_CREATE_SLOT), U256::from(SPLIT_CREATE_SLOT_VALUE)) - .push_number(0_u8) - .push_number(SPLIT_CREATE_MEM_OFFSET) - .append(MSTORE) - .push_number(SPLIT_CREATE_CODE_LEN) - .push_number(0_u8) - .append(RETURN) - .build() -} - -fn constructor_calls_identity_precompile() -> Bytes { - BytecodeBuilder::default() - .push_number(0_u8) - .push_number(0_u8) - .push_number(0_u8) - .push_number(0_u8) - .push_address(IDENTITY_PRECOMPILE) - .push_number(50_000_u32) - .append(STATICCALL) - .append(POP) - .push_number(1_u8) - .push_number(0_u8) - .push_number(0_u8) - .append(revm::bytecode::opcode::CODECOPY) - .push_number(1_u8) - .push_number(0_u8) - .append(RETURN) - .build() -} - -fn create_pre_eip155_deploy_tx_with_value(init_code: Bytes, value: U256) -> (Bytes, Address) { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: SIGNED_TX_GAS_LIMIT, - to: TxKind::Create, - value, - input: init_code, - chain_id: None, - }; - let r = U256::from_be_bytes(hex!( - "2222222222222222222222222222222222222222222222222222222222222222" - )); - let s = U256::from_be_bytes(hex!( - "2222222222222222222222222222222222222222222222222222222222222222" - )); - let sig = Signature::new(r, s, false); - let signed = Signed::new_unchecked(tx, sig, B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - let tx_bytes = Bytes::from(buf); - let signer = signed.recover_signer().expect("should recover signer"); - (tx_bytes, signer) -} - -fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { - create_pre_eip155_deploy_tx_with_value(init_code, U256::ZERO) -} - -fn keyless_deploy_call_tx( - keyless_deployment_tx: Bytes, - gas_limit_override: u64, -) -> MegaTransaction { - keyless_deploy_call_tx_with_outer_gas( - keyless_deployment_tx, - gas_limit_override, - DEFAULT_OUTER_GAS_LIMIT, - ) -} - -fn keyless_deploy_call_tx_with_outer_gas( - keyless_deployment_tx: Bytes, - gas_limit_override: u64, - outer_gas_limit: u64, -) -> MegaTransaction { - keyless_deploy_call_tx_with_override_u256( - keyless_deployment_tx, - U256::from(gas_limit_override), - outer_gas_limit, - ) -} - -fn keyless_deploy_call_tx_with_override_u256( - keyless_deployment_tx: Bytes, - gas_limit_override: U256, - outer_gas_limit: u64, -) -> MegaTransaction { - let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_deployment_tx, - gasLimitOverride: gas_limit_override, - } - .abi_encode(); - let tx = TxEnv { - caller: TEST_CALLER, - kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), - data: call_data.into(), - value: U256::ZERO, - gas_limit: outer_gas_limit, - gas_price: 0, - ..Default::default() - }; - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); - tx -} - -fn funded_db(signer: Address) -> MemoryDatabase { - let mut db = MemoryDatabase::default(); - db.set_account_balance(signer, U256::from(LARGE_SIGNER_BALANCE)); - db -} - -struct RunConfig<'a, O> { - spec: MegaSpecId, - db: &'a mut MemoryDatabase, - tx_bytes: Bytes, - gas_limit_override: u64, - observer: Option>>, - tx_limits: Option, - outer_gas_limit: u64, -} - -fn run_keyless(config: RunConfig<'_, O>) -> ResultAndState -where - O: SandboxObserver + 'static, -{ - let mut context = MegaContext::new(config.db, config.spec); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - if let Some(limits) = config.tx_limits { - context = context.with_tx_runtime_limits(limits); - } - context.set_keyless_sandbox_observer(config.observer); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx_with_outer_gas( - config.tx_bytes, - config.gas_limit_override, - config.outer_gas_limit, - ); - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") -} - -fn assert_result_and_state_eq( - with_observer: &ResultAndState, - without_observer: &ResultAndState, - case: &str, -) { - assert_eq!( - with_observer.result, without_observer.result, - "{case}: execution result must match" - ); - assert_eq!( - with_observer.result.gas_used(), - without_observer.result.gas_used(), - "{case}: gas_used must match" - ); - assert_eq!( - with_observer.state.len(), - without_observer.state.len(), - "{case}: state account count must match" - ); - for (addr, account) in &with_observer.state { - let other = without_observer - .state - .get(addr) - .unwrap_or_else(|| panic!("{case}: missing account {addr:?} in no-observer state")); - assert_eq!(account, other, "{case}: account {addr:?}"); - } -} - /// Loud split-CREATE shape: constructor finished (SSTORE stuck, account created) but the /// sandbox reported `ExecutionFailed` and the ABI does not claim a deploy. An empty spin /// that actually deployed would fail here as `Applied(Deployed)` / empty `errorData`. @@ -514,7 +263,7 @@ fn parity_pair( let mut db_base = db_obs.clone(); let observer = Rc::new(RefCell::new(RecordingObserver::default())); - let observed = run_keyless(RunConfig { + let (observed, observed_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_obs, tx_bytes: tx_bytes.clone(), @@ -523,7 +272,7 @@ fn parity_pair( tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); - let baseline = run_keyless(RunConfig { + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_base, tx_bytes, @@ -533,6 +282,7 @@ fn parity_pair( outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); assert_result_and_state_eq(&observed, &baseline, case); + assert_usage_eq(observed_usage, baseline_usage, case); } fn run_with_recorder( @@ -592,33 +342,6 @@ fn assert_call_create_balanced(events: &[ObservedEvent]) { assert_eq!(create_depth, 0, "unbalanced create frames: {events:?}"); } -fn crowded_parent_env() -> TestExternalEnvs { - TestExternalEnvs::new() - .with_default_bucket_capacity((MIN_BUCKET_SIZE as u64) * 8) - .with_oracle_storage(U256::ZERO, U256::from(0x42)) -} - -fn run_keyless_with_parent_env( - spec: MegaSpecId, - db: &mut MemoryDatabase, - tx_bytes: Bytes, - env: TestExternalEnvs, - observer: Option>>, -) -> ResultAndState -where - O: SandboxObserver + SandboxObserver + 'static, -{ - let mut context = MegaContext::new(db, spec).with_external_envs(env.into()); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context.set_keyless_sandbox_observer(observer); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") -} - #[test] fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { @@ -628,14 +351,14 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { let observer = Rc::new(RefCell::new(RecordingObserver::default())); let env = crowded_parent_env(); - let observed = run_keyless_with_parent_env( + let (observed, observed_usage) = run_keyless_with_parent_env_usage( spec, &mut db_obs, tx_bytes.clone(), env.clone(), Some(observer), ); - let baseline = run_keyless_with_parent_env( + let (baseline, baseline_usage) = run_keyless_with_parent_env_usage( spec, &mut db_base, tx_bytes, @@ -648,11 +371,9 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { "{spec:?} baseline must succeed: {:?}", baseline.result ); - assert_result_and_state_eq( - &observed, - &baseline, - &format!("pre-REX4 nonempty parent env {spec:?}"), - ); + let case = format!("pre-REX4 nonempty parent env {spec:?}"); + assert_result_and_state_eq(&observed, &baseline, &case); + assert_usage_eq(observed_usage, baseline_usage, &case); } } @@ -665,14 +386,14 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert( let observer = Rc::new(RefCell::new(RecordingObserver::default())); let env = crowded_parent_env(); - let observed = run_keyless_with_parent_env( + let (observed, observed_usage) = run_keyless_with_parent_env_usage( spec, &mut db_obs, tx_bytes.clone(), env.clone(), Some(observer), ); - let baseline = run_keyless_with_parent_env( + let (baseline, baseline_usage) = run_keyless_with_parent_env_usage( spec, &mut db_base, tx_bytes, @@ -685,11 +406,9 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert( "{spec:?} constructor revert is a success-style outer return: {:?}", baseline.result ); - assert_result_and_state_eq( - &observed, - &baseline, - &format!("pre-REX4 nonempty parent env revert {spec:?}"), - ); + let case = format!("pre-REX4 nonempty parent env revert {spec:?}"); + assert_result_and_state_eq(&observed, &baseline, &case); + assert_usage_eq(observed_usage, baseline_usage, &case); } } @@ -764,20 +483,6 @@ fn test_observer_parity_constructor_revert_across_specs() { } } -fn parent_compute_gas_used(spec: MegaSpecId, signer: Address, tx_bytes: Bytes) -> u64 { - let mut usage_db = funded_db(signer); - let mut context = MegaContext::new(&mut usage_db, spec); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); - alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap(); - let used = evm.ctx_ref().additional_limit.borrow().get_usage().compute_gas; - used -} - #[test] fn test_observer_parity_rex5_resource_limit_halt() { for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { @@ -1091,7 +796,7 @@ fn test_observer_parity_split_create_through_interceptor() { let mut db_base = db_obs.clone(); let recorder = Rc::new(RefCell::new(RecordingObserver::default())); - let observed = run_keyless(RunConfig { + let (observed, observed_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_obs, tx_bytes: tx_bytes.clone(), @@ -1100,7 +805,7 @@ fn test_observer_parity_split_create_through_interceptor() { tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); - let baseline = run_keyless(RunConfig { + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_base, tx_bytes, @@ -1113,11 +818,9 @@ fn test_observer_parity_split_create_through_interceptor() { let events = recorder.borrow().events.clone(); assert_split_create_shape(spec, &observed, signer, Some(&events)); assert_split_create_shape(spec, &baseline, signer, None); - assert_result_and_state_eq( - &observed, - &baseline, - &format!("split-CREATE interceptor {spec:?}"), - ); + let case = format!("split-CREATE interceptor {spec:?}"); + assert_result_and_state_eq(&observed, &baseline, &case); + assert_usage_eq(observed_usage, baseline_usage, &case); } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs new file mode 100644 index 00000000..25a65d51 --- /dev/null +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -0,0 +1,356 @@ +//! Shared fixtures for keyless-deploy sandbox observer and inspector tests. + +use std::{cell::RefCell, rc::Rc}; + +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + revm::context::result::ResultAndState, + sandbox::SandboxObserver, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, LimitUsage, MegaContext, MegaEvm, + MegaHaltReason, MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, + MIN_BUCKET_SIZE, +}; +use revm::{ + bytecode::opcode::{BALANCE, CALL, MSTORE, POP, PUSH0, RETURN, STATICCALL}, + context::TxEnv, + handler::EvmTr, + inspector::NoOpInspector, +}; + +pub(crate) const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); +pub(crate) const LARGE_GAS_LIMIT_OVERRIDE: u64 = 10_000_000_000; +pub(crate) const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; +pub(crate) const SIGNED_TX_GAS_LIMIT: u64 = 1_000_000; +pub(crate) const REVERTER: Address = address!("0000000000000000000000000000000000aaaaaa"); +pub(crate) const IDENTITY_PRECOMPILE: Address = + address!("0000000000000000000000000000000000000004"); +pub(crate) const MERGE_FAIL_SENTINEL: Address = + address!("0000000000000000000000000000000000bbbbbb"); +pub(crate) const DEFAULT_OUTER_GAS_LIMIT: u64 = 1_000_000_000_000; + +pub(crate) const SPECS: [MegaSpecId; 5] = + [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4, MegaSpecId::REX5, MegaSpecId::REX6]; + +/// Words of memory expanded by the constructor `MSTORE`. +/// +/// Pre-REX5 sandboxes ignore the parent's compute limit and run at the spec +/// default of 200M (`rex::TX_COMPUTE_GAS_LIMIT`). An 8_000-byte code deposit +/// is only 1.6M compute, so the constructor has to land in the window +/// `(200M - 1.6M, 200M)` and then `RETURN` `8_000` bytes so the deposit charge +/// splits the CREATE. Memory-expansion gas is `3*words + words²/512`; `318_191` +/// words (~10.18 MiB) costs `198_699_714`, which sits in that window without a +/// multi-million-iteration loop. +pub(crate) const SPLIT_CREATE_MEM_WORDS: u64 = 318_191; +pub(crate) const SPLIT_CREATE_MEM_OFFSET: u32 = (SPLIT_CREATE_MEM_WORDS * 32 - 32) as u32; +pub(crate) const SPLIT_CREATE_CODE_LEN: u32 = 8_000; +pub(crate) const SPLIT_CREATE_SLOT: u64 = 2; +pub(crate) const SPLIT_CREATE_SLOT_VALUE: u64 = 0x2a; + +pub(crate) fn success_constructor() -> Bytes { + BytecodeBuilder::default() + .sstore(U256::from(0), U256::from(1)) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +pub(crate) fn revert_constructor() -> Bytes { + Bytes::from_static(&hex!("60006000fd")) +} + +pub(crate) fn empty_code_constructor() -> Bytes { + BytecodeBuilder::default().append_many([PUSH0, PUSH0, RETURN]).build() +} + +pub(crate) fn constructor_calls_reverter() -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(REVERTER) + .push_number(50_000_u32) + .append(CALL) + .append(POP) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +pub(crate) fn constructor_touches_sentinel() -> Bytes { + BytecodeBuilder::default() + .push_address(MERGE_FAIL_SENTINEL) + .append(BALANCE) + .append(POP) + .sstore(U256::from(0), U256::from(1)) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +pub(crate) fn split_create_initcode() -> Bytes { + BytecodeBuilder::default() + .sstore(U256::from(SPLIT_CREATE_SLOT), U256::from(SPLIT_CREATE_SLOT_VALUE)) + .push_number(0_u8) + .push_number(SPLIT_CREATE_MEM_OFFSET) + .append(MSTORE) + .push_number(SPLIT_CREATE_CODE_LEN) + .push_number(0_u8) + .append(RETURN) + .build() +} + +pub(crate) fn constructor_calls_identity_precompile() -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(IDENTITY_PRECOMPILE) + .push_number(50_000_u32) + .append(STATICCALL) + .append(POP) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +pub(crate) fn create_pre_eip155_deploy_tx_with_value( + init_code: Bytes, + value: U256, +) -> (Bytes, Address) { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: SIGNED_TX_GAS_LIMIT, + to: TxKind::Create, + value, + input: init_code, + chain_id: None, + }; + let r = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let s = U256::from_be_bytes(hex!( + "2222222222222222222222222222222222222222222222222222222222222222" + )); + let sig = Signature::new(r, s, false); + let signed = Signed::new_unchecked(tx, sig, B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + let tx_bytes = Bytes::from(buf); + let signer = signed.recover_signer().expect("should recover signer"); + (tx_bytes, signer) +} + +pub(crate) fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { + create_pre_eip155_deploy_tx_with_value(init_code, U256::ZERO) +} + +pub(crate) fn keyless_deploy_call_tx( + keyless_deployment_tx: Bytes, + gas_limit_override: u64, +) -> MegaTransaction { + keyless_deploy_call_tx_with_outer_gas( + keyless_deployment_tx, + gas_limit_override, + DEFAULT_OUTER_GAS_LIMIT, + ) +} + +pub(crate) fn keyless_deploy_call_tx_with_outer_gas( + keyless_deployment_tx: Bytes, + gas_limit_override: u64, + outer_gas_limit: u64, +) -> MegaTransaction { + keyless_deploy_call_tx_with_override_u256( + keyless_deployment_tx, + U256::from(gas_limit_override), + outer_gas_limit, + ) +} + +pub(crate) fn keyless_deploy_call_tx_with_override_u256( + keyless_deployment_tx: Bytes, + gas_limit_override: U256, + outer_gas_limit: u64, +) -> MegaTransaction { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_deployment_tx, + gasLimitOverride: gas_limit_override, + } + .abi_encode(); + let tx = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + data: call_data.into(), + value: U256::ZERO, + gas_limit: outer_gas_limit, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +pub(crate) fn funded_db(signer: Address) -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_balance(signer, U256::from(LARGE_SIGNER_BALANCE)); + db +} + +pub(crate) struct RunConfig<'a, O> { + pub spec: MegaSpecId, + pub db: &'a mut MemoryDatabase, + pub tx_bytes: Bytes, + pub gas_limit_override: u64, + pub observer: Option>>, + pub tx_limits: Option, + pub outer_gas_limit: u64, +} + +pub(crate) fn run_keyless(config: RunConfig<'_, O>) -> ResultAndState +where + O: SandboxObserver + 'static, +{ + run_keyless_with_usage(config).0 +} + +pub(crate) fn run_keyless_with_usage( + config: RunConfig<'_, O>, +) -> (ResultAndState, LimitUsage) +where + O: SandboxObserver + 'static, +{ + let mut context = MegaContext::new(config.db, config.spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + if let Some(limits) = config.tx_limits { + context = context.with_tx_runtime_limits(limits); + } + context.set_keyless_sandbox_observer(config.observer); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx_with_outer_gas( + config.tx_bytes, + config.gas_limit_override, + config.outer_gas_limit, + ); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + (result, usage) +} + +pub(crate) fn assert_result_and_state_eq( + with_hook: &ResultAndState, + without_hook: &ResultAndState, + case: &str, +) { + assert_eq!(with_hook.result, without_hook.result, "{case}: execution result must match"); + assert_eq!( + with_hook.result.gas_used(), + without_hook.result.gas_used(), + "{case}: gas_used must match" + ); + assert_eq!( + with_hook.state.len(), + without_hook.state.len(), + "{case}: state account count must match" + ); + for (addr, account) in &with_hook.state { + let other = without_hook + .state + .get(addr) + .unwrap_or_else(|| panic!("{case}: missing account {addr:?} in no-hook state")); + assert_eq!(account, other, "{case}: account {addr:?}"); + } +} + +pub(crate) fn assert_usage_eq(got: LimitUsage, expected: LimitUsage, case: &str) { + assert_eq!(got.data_size, expected.data_size, "{case}: data_size"); + assert_eq!(got.kv_updates, expected.kv_updates, "{case}: kv_updates"); + assert_eq!(got.compute_gas, expected.compute_gas, "{case}: compute_gas"); + assert_eq!(got.state_growth, expected.state_growth, "{case}: state_growth"); +} + +pub(crate) fn crowded_parent_env() -> TestExternalEnvs { + TestExternalEnvs::new() + .with_default_bucket_capacity((MIN_BUCKET_SIZE as u64) * 8) + .with_oracle_storage(U256::ZERO, U256::from(0x42)) +} + +pub(crate) fn run_keyless_with_parent_env( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + env: TestExternalEnvs, + observer: Option>>, +) -> ResultAndState +where + O: SandboxObserver + SandboxObserver + 'static, +{ + run_keyless_with_parent_env_usage(spec, db, tx_bytes, env, observer).0 +} + +pub(crate) fn run_keyless_with_parent_env_usage( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + env: TestExternalEnvs, + observer: Option>>, +) -> (ResultAndState, LimitUsage) +where + O: SandboxObserver + SandboxObserver + 'static, +{ + let mut context = MegaContext::new(db, spec).with_external_envs(env.into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer(observer); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + (result, usage) +} + +pub(crate) fn parent_compute_gas_used(spec: MegaSpecId, signer: Address, tx_bytes: Bytes) -> u64 { + let mut usage_db = funded_db(signer); + let mut context = MegaContext::new(&mut usage_db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap(); + let used = evm.ctx_ref().additional_limit.borrow().get_usage().compute_gas; + used +} diff --git a/crates/mega-evm/tests/rex2/main.rs b/crates/mega-evm/tests/rex2/main.rs index c734edfe..1e8d0252 100644 --- a/crates/mega-evm/tests/rex2/main.rs +++ b/crates/mega-evm/tests/rex2/main.rs @@ -2,5 +2,6 @@ mod keyless_deploy; mod keyless_sandbox_observer; +mod keyless_sandbox_support; mod oracle_hint; mod selfdestruct; From f3be3a1ed2d8142774ffb00b92804c293ff134a5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:00:20 +0800 Subject: [PATCH 10/28] feat: add a rewriting inspector channel for keyless sandbox execution --- crates/mega-evm/src/block/executor.rs | 40 +- crates/mega-evm/src/evm/context.rs | 175 +++++---- crates/mega-evm/src/evm/mod.rs | 41 +- crates/mega-evm/src/sandbox/execution.rs | 168 +++++---- crates/mega-evm/src/sandbox/inspector.rs | 454 +++++++++++++++++++++++ crates/mega-evm/src/sandbox/mod.rs | 44 ++- 6 files changed, 762 insertions(+), 160 deletions(-) create mode 100644 crates/mega-evm/src/sandbox/inspector.rs diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 97a02a3e..ff269c5e 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -157,7 +157,7 @@ where /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. The observer /// must implement [`crate::sandbox::SandboxObserver`] for both this executor's /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use - /// [`Self::clear_keyless_sandbox_observer`] to detach. + /// [`Self::clear_keyless_sandbox_hook`] to detach. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: crate::sandbox::SandboxObserver @@ -172,7 +172,7 @@ where /// /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_observer`] to detach. + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, @@ -180,11 +180,39 @@ where self.evm.set_keyless_sandbox_observer_for_parent_env(observer); } - /// Detaches any sandbox observer from both env-type slots. + /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// - /// Forwards to [`crate::MegaEvm::clear_keyless_sandbox_observer`]. - pub fn clear_keyless_sandbox_observer(&mut self) { - self.evm.clear_keyless_sandbox_observer(); + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_inspector`]. The inspector + /// must implement [`crate::sandbox::SandboxInspector`] for both this executor's + /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use + /// [`Self::clear_keyless_sandbox_hook`] to detach. + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: crate::sandbox::SandboxInspector + + crate::sandbox::SandboxInspector + + 'static, + { + self.evm.set_keyless_sandbox_inspector(inspector); + } + + /// Attaches an inspector that implements [`crate::sandbox::SandboxInspector`] + /// only for this executor's `ExtEnvs`. + /// + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_inspector_for_parent_env`]. + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. + pub fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + self.evm.set_keyless_sandbox_inspector_for_parent_env(inspector); + } + + /// Detaches any sandbox hook from both env-type slots. + /// + /// Forwards to [`crate::MegaEvm::clear_keyless_sandbox_hook`]. + pub fn clear_keyless_sandbox_hook(&mut self) { + self.evm.clear_keyless_sandbox_hook(); } } diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 6bdadff4..c279365c 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -29,10 +29,11 @@ use revm::{ }; use crate::{ - constants, is_system_originated, sandbox::SandboxObserver, AdditionalLimit, BucketId, - DynamicGasCost, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvTypes, ExternalEnvs, - MegaSpecId, TxRuntimeLimit, VolatileDataAccess, VolatileDataAccessTracker, - VolatileDataAccessType, + constants, is_system_originated, + sandbox::{SandboxHook, SandboxInspector, SandboxObserver}, + AdditionalLimit, BucketId, DynamicGasCost, EmptyExternalEnv, EvmTxRuntimeLimits, + ExternalEnvTypes, ExternalEnvs, MegaSpecId, TxRuntimeLimit, VolatileDataAccess, + VolatileDataAccessTracker, VolatileDataAccessType, }; /// `MegaETH` EVM context type. This struct wraps [`OpContext`] and implements the [`ContextTr`] @@ -76,28 +77,25 @@ pub struct MegaContext { /// before the sandbox runs). pub(crate) inside_sandbox: Rc>, - /// Observer for nested sandbox execution, typed against this context's [`ExtEnvs`]. + /// Hook for nested sandbox execution, typed against this context's [`ExtEnvs`]. /// /// `None` keeps the historical no-inspector sandbox path. REX4+ sandboxes /// share the parent env and deliver opcode-level hooks through this slot. /// Changing [`ExtEnvs`] via [`Self::with_external_envs`] resets this field - /// and the [`EmptyExternalEnv`] observer slot: the observer cannot be - /// carried across an env-type change and must be attached after external - /// environments are assembled. A compliant (read-only) observer does not - /// change execution results; mutating interpreter or context state through - /// its hooks is undefined and may diverge consensus. + /// and the [`EmptyExternalEnv`] hook slot: the hook cannot be carried across + /// an env-type change and must be attached after external environments are + /// assembled. Observer and inspector occupy the same slot exclusively. #[debug(ignore)] - pub(crate) keyless_sandbox_observer: Option>>>, + pub(crate) keyless_sandbox_hook: Option>, - /// Observer handle typed against [`EmptyExternalEnv`]. + /// Hook handle typed against [`EmptyExternalEnv`]. /// /// Pre-REX4 sandboxes always execute with [`EmptyExternalEnv`]. Opcode-level - /// hooks on that path use this slot so attaching an observer cannot change - /// sandbox env semantics. `None` when no observer is attached, or when the - /// caller used [`Self::set_keyless_sandbox_observer_for_parent_env`]. + /// hooks on that path use this slot so attaching a hook cannot change + /// sandbox env semantics. `None` when no hook is attached, or when the + /// caller used a `_for_parent_env` setter. #[debug(ignore)] - pub(crate) keyless_sandbox_observer_empty: - Option>>>, + pub(crate) keyless_sandbox_hook_empty: Option>, /// The system address for the current block. /// Pre-REX5: always `MEGA_SYSTEM_ADDRESS` (the legacy hardcoded constant). @@ -198,8 +196,8 @@ impl MegaContext { tx_limits.oracle_access_compute_gas_limit, ))), inside_sandbox: Rc::new(RefCell::new(false)), - keyless_sandbox_observer: None, - keyless_sandbox_observer_empty: None, + keyless_sandbox_hook: None, + keyless_sandbox_hook_empty: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -263,8 +261,8 @@ impl MegaContext { tx_limits.oracle_access_compute_gas_limit, ))), inside_sandbox: Rc::new(RefCell::new(false)), - keyless_sandbox_observer: None, - keyless_sandbox_observer_empty: None, + keyless_sandbox_hook: None, + keyless_sandbox_hook_empty: None, system_address: crate::MEGA_SYSTEM_ADDRESS, inner, } @@ -293,8 +291,8 @@ impl MegaContext { oracle_env: self.oracle_env, volatile_data_tracker: self.volatile_data_tracker, inside_sandbox: self.inside_sandbox, - keyless_sandbox_observer: self.keyless_sandbox_observer, - keyless_sandbox_observer_empty: self.keyless_sandbox_observer_empty, + keyless_sandbox_hook: self.keyless_sandbox_hook, + keyless_sandbox_hook_empty: self.keyless_sandbox_hook_empty, system_address: self.system_address, } } @@ -371,11 +369,11 @@ impl MegaContext { /// external environments, the dynamic gas cost calculator and oracle environment /// are reinitialized with the new configurations. /// - /// Changing `ExtEnvs` resets the sandbox observer (both the parent-env slot - /// and the [`EmptyExternalEnv`] slot): the observer is parameterized by env - /// type and cannot be carried across this conversion. Attach the observer - /// after external environments are assembled. Debug builds assert if an - /// observer is already attached (`set observer after external envs are wired`). + /// Changing `ExtEnvs` resets the sandbox hook (both the parent-env slot + /// and the [`EmptyExternalEnv`] slot): the hook is parameterized by env + /// type and cannot be carried across this conversion. Attach the hook + /// after external environments are assembled. Debug builds assert if a + /// hook is already attached (`set sandbox hook after external envs are wired`). /// /// # Arguments /// @@ -389,9 +387,8 @@ impl MegaContext { external_envs: ExternalEnvs, ) -> MegaContext { debug_assert!( - self.keyless_sandbox_observer.is_none() && - self.keyless_sandbox_observer_empty.is_none(), - "set observer after external envs are wired", + self.keyless_sandbox_hook.is_none() && self.keyless_sandbox_hook_empty.is_none(), + "set sandbox hook after external envs are wired", ); let parent_block_number = self.inner.block.number.to::().saturating_sub(1); let spec = self.spec; @@ -410,9 +407,9 @@ impl MegaContext { oracle_env: Rc::new(RefCell::new(external_envs.oracle_env)), volatile_data_tracker: self.volatile_data_tracker, inside_sandbox: self.inside_sandbox, - // Observer is parameterized by `ExtEnvs`; a type change cannot keep it. - keyless_sandbox_observer: None, - keyless_sandbox_observer_empty: None, + // Hook is parameterized by `ExtEnvs`; a type change cannot keep it. + keyless_sandbox_hook: None, + keyless_sandbox_hook_empty: None, system_address: self.system_address, } } @@ -506,9 +503,9 @@ impl MegaContext { /// inspector satisfies both bounds. /// /// Attaching an observer does not change sandbox external-env semantics at - /// any spec. + /// any spec. Replaces any attached inspector. /// - /// Use [`Self::clear_keyless_sandbox_observer`] to detach. Passing `None` + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. Passing `None` /// also clears both slots, but type inference for `O` often fails on that /// path. Observation is read-only: mutating interpreter or context state /// through the hooks is undefined and may diverge consensus. There is no @@ -522,10 +519,10 @@ impl MegaContext { let cloned = Rc::clone(&obs); let parent: Rc>> = cloned; let empty: Rc>> = obs; - self.keyless_sandbox_observer = Some(parent); - self.keyless_sandbox_observer_empty = Some(empty); + self.keyless_sandbox_hook = Some(SandboxHook::Observer(parent)); + self.keyless_sandbox_hook_empty = Some(SandboxHook::Observer(empty)); } - None => self.clear_keyless_sandbox_observer(), + None => self.clear_keyless_sandbox_hook(), } } @@ -537,23 +534,75 @@ impl MegaContext { /// only `sandbox_start` / `sandbox_end` through this handle. /// /// Prefer [`Self::set_keyless_sandbox_observer`] when the observer - /// implements both env types. Use [`Self::clear_keyless_sandbox_observer`] - /// to detach. + /// implements both env types. Use [`Self::clear_keyless_sandbox_hook`] + /// to detach. Replaces any attached inspector. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, ) { - self.keyless_sandbox_observer = observer; - self.keyless_sandbox_observer_empty = None; + self.keyless_sandbox_hook = observer.map(SandboxHook::Observer); + self.keyless_sandbox_hook_empty = None; } - /// Detaches any sandbox observer from both env-type slots. + /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// - /// Restores the no-observer sandbox path. Prefer this over passing `None` to - /// [`Self::set_keyless_sandbox_observer`] when `O` cannot be inferred. - pub fn clear_keyless_sandbox_observer(&mut self) { - self.keyless_sandbox_observer = None; - self.keyless_sandbox_observer_empty = None; + /// The inspector must implement [`SandboxInspector`] for both this context's + /// [`ExtEnvs`] and [`EmptyExternalEnv`]. The same handle is stored as two + /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes + /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent + /// env). A type that implements [`revm::Inspector`] for every sandbox + /// context lifetime satisfies both bounds via the blanket impl. + /// + /// Attaching an inspector does not change sandbox external-env semantics at + /// any spec. Replaces any attached observer. Interventions take effect + /// inside the sandbox as they would on a top-level EVM; reported gas and + /// usage are the post-intervention values. The channel is node-local and + /// non-consensus. + /// + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. Passing `None` also + /// clears both slots, but type inference for `I` often fails on that path. + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static, + { + match inspector { + Some(insp) => { + let cloned = Rc::clone(&insp); + let parent: Rc>> = cloned; + let empty: Rc>> = insp; + self.keyless_sandbox_hook = Some(SandboxHook::Inspector(parent)); + self.keyless_sandbox_hook_empty = Some(SandboxHook::Inspector(empty)); + } + None => self.clear_keyless_sandbox_hook(), + } + } + + /// Attaches an inspector that implements [`SandboxInspector`] only for this + /// context's [`ExtEnvs`]. + /// + /// Opcode-level inspection is available on REX4+ sandboxes, which share + /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit + /// only `sandbox_start` / `sandbox_end` through this handle. + /// + /// Prefer [`Self::set_keyless_sandbox_inspector`] when the inspector + /// implements both env types. Use [`Self::clear_keyless_sandbox_hook`] + /// to detach. Replaces any attached observer. + pub fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + self.keyless_sandbox_hook = inspector.map(SandboxHook::Inspector); + self.keyless_sandbox_hook_empty = None; + } + + /// Detaches any sandbox hook from both env-type slots. + /// + /// Restores the no-hook sandbox path. Prefer this over passing `None` to + /// [`Self::set_keyless_sandbox_observer`] or + /// [`Self::set_keyless_sandbox_inspector`] when the generic cannot be inferred. + pub fn clear_keyless_sandbox_hook(&mut self) { + self.keyless_sandbox_hook = None; + self.keyless_sandbox_hook_empty = None; } /// Gets the current total data size generated from transaction execution. @@ -989,41 +1038,41 @@ mod tests { } #[test] - fn test_clear_keyless_sandbox_observer_clears_both_slots() { + fn test_clear_keyless_sandbox_hook_clears_both_slots() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); - assert!(context.keyless_sandbox_observer.is_some()); - assert!(context.keyless_sandbox_observer_empty.is_some()); + assert!(context.keyless_sandbox_hook.is_some()); + assert!(context.keyless_sandbox_hook_empty.is_some()); - context.clear_keyless_sandbox_observer(); - assert!(context.keyless_sandbox_observer.is_none()); - assert!(context.keyless_sandbox_observer_empty.is_none()); + context.clear_keyless_sandbox_hook(); + assert!(context.keyless_sandbox_hook.is_none()); + assert!(context.keyless_sandbox_hook_empty.is_none()); } #[test] - fn test_mega_evm_clear_keyless_sandbox_observer() { + fn test_mega_evm_clear_keyless_sandbox_hook() { use revm::handler::EvmTr; let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); let mut evm = crate::MegaEvm::new(context); - evm.clear_keyless_sandbox_observer(); - assert!(evm.ctx_ref().keyless_sandbox_observer.is_none()); - assert!(evm.ctx_ref().keyless_sandbox_observer_empty.is_none()); + evm.clear_keyless_sandbox_hook(); + assert!(evm.ctx_ref().keyless_sandbox_hook.is_none()); + assert!(evm.ctx_ref().keyless_sandbox_hook_empty.is_none()); } #[test] - fn test_with_external_envs_without_observer_leaves_slots_empty() { + fn test_with_external_envs_without_hook_leaves_slots_empty() { let context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); let context = context.with_external_envs(TestExternalEnvs::::new().into()); - assert!(context.keyless_sandbox_observer.is_none()); - assert!(context.keyless_sandbox_observer_empty.is_none()); + assert!(context.keyless_sandbox_hook.is_none()); + assert!(context.keyless_sandbox_hook_empty.is_none()); } #[test] #[cfg(debug_assertions)] - #[should_panic(expected = "set observer after external envs are wired")] + #[should_panic(expected = "set sandbox hook after external envs are wired")] fn test_with_external_envs_panics_in_debug_when_observer_is_attached() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index f68643a6..2071ae89 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -70,8 +70,8 @@ use revm::{ }; use crate::{ - sandbox::SandboxObserver, BucketId, EmptyExternalEnv, ExternalEnvTypes, LimitUsage, - MegaTransaction, + sandbox::{SandboxInspector, SandboxObserver}, + BucketId, EmptyExternalEnv, ExternalEnvTypes, LimitUsage, MegaTransaction, }; /// The main EVM implementation for the `MegaETH` chain. @@ -241,7 +241,7 @@ impl MegaEvm { /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. The observer /// must implement [`SandboxObserver`] for both this EVM's [`ExtEnvs`] and - /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_observer`] to + /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to /// detach. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where @@ -255,7 +255,7 @@ impl MegaEvm { /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_observer`] to detach. + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. pub fn set_keyless_sandbox_observer_for_parent_env( &mut self, observer: Option>>>, @@ -263,11 +263,36 @@ impl MegaEvm { self.inner.ctx.set_keyless_sandbox_observer_for_parent_env(observer); } - /// Detaches any sandbox observer from both env-type slots. + /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// - /// Forwards to [`MegaContext::clear_keyless_sandbox_observer`]. - pub fn clear_keyless_sandbox_observer(&mut self) { - self.inner.ctx.clear_keyless_sandbox_observer(); + /// Forwards to [`MegaContext::set_keyless_sandbox_inspector`]. The inspector + /// must implement [`SandboxInspector`] for both this EVM's [`ExtEnvs`] and + /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to detach. + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static, + { + self.inner.ctx.set_keyless_sandbox_inspector(inspector); + } + + /// Attaches an inspector that implements [`SandboxInspector`] only for this + /// EVM's [`ExtEnvs`]. + /// + /// Forwards to [`MegaContext::set_keyless_sandbox_inspector_for_parent_env`]. + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. + /// Use [`Self::clear_keyless_sandbox_hook`] to detach. + pub fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + self.inner.ctx.set_keyless_sandbox_inspector_for_parent_env(inspector); + } + + /// Detaches any sandbox hook from both env-type slots. + /// + /// Forwards to [`MegaContext::clear_keyless_sandbox_hook`]. + pub fn clear_keyless_sandbox_hook(&mut self) { + self.inner.ctx.clear_keyless_sandbox_hook(); } /// Provides a reference to the block environment. diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index cac32902..dc1bcf2c 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -32,7 +32,6 @@ #[cfg(not(feature = "std"))] use alloc as std; -use core::cell::RefCell; use std::{rc::Rc, vec::Vec}; use alloy_consensus::{Signed, Transaction as AlloyTransaction, TxLegacy}; @@ -69,9 +68,10 @@ use super::{ use super::{ error::{encode_error_result, KeylessDeployError}, + inspector::{InspectorBridge, SandboxHook}, observer::{ - ObserverBridge, SandboxCompletionKind, SandboxEndOutcome, SandboxObserver, - SandboxRejectKind, SandboxStartInfo, + ObserverBridge, SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, + SandboxStartInfo, }, state::SandboxDb, }; @@ -444,11 +444,11 @@ pub fn execute_keyless_deploy_call // Step 9: Execute sandbox and apply state changes. // - // INVARIANT: when an observer is attached, `sandbox_start` and `sandbox_end` + // INVARIANT: when a hook is attached, `sandbox_start` and `sandbox_end` // fire exactly once on this path, covering every terminal arm below. - let observer = ctx.keyless_sandbox_observer.clone(); - if let Some(obs) = &observer { - obs.borrow_mut().sandbox_start(&SandboxStartInfo { + let hook = ctx.keyless_sandbox_hook.clone(); + if let Some(h) = &hook { + h.notify_start(&SandboxStartInfo { spec: ctx.spec, signer: deploy_signer, deploy_address, @@ -473,7 +473,7 @@ pub fn execute_keyless_deploy_call &return_memory_offset, ) { notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::PostAccountingHalt, }, @@ -482,9 +482,25 @@ pub fn execute_keyless_deploy_call } } + // Address is known on `Deployed` before apply, and apply does not change + // either side of the comparison. Checking first keeps `NotApplied { + // AddressMismatch }` honest: the parent journal never receives sandbox + // state on this path. + if let SandboxCompletion::Deployed { deploy_address: deployed, .. } = &completion { + if *deployed != deploy_address { + notify_sandbox_end( + &hook, + SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::AddressMismatch, + }, + ); + return make_error!(KeylessDeployError::AddressMismatch); + } + } + if let Err(e) = apply_sandbox_state(ctx, state, deploy_signer) { notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::ApplyFailed }, ); return make_error!(e); @@ -496,20 +512,11 @@ pub fn execute_keyless_deploy_call // own frame accounting already rolled the failed frame's logs back. match completion { SandboxCompletion::Deployed { gas_used, deploy_address: deployed, logs } => { - if deployed != deploy_address { - notify_sandbox_end( - &observer, - SandboxEndOutcome::NotApplied { - reason: SandboxRejectKind::AddressMismatch, - }, - ); - return make_error!(KeylessDeployError::AddressMismatch); - } for log in logs { ctx.log(log); } notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, gas_used, @@ -522,7 +529,7 @@ pub fn execute_keyless_deploy_call ctx.log(log); } notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::Applied { completion: SandboxCompletionKind::EmptyCode, gas_used, @@ -539,7 +546,7 @@ pub fn execute_keyless_deploy_call // writes) persists; outer caller sees the failure reason in // `errorData`. notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::Applied { completion: SandboxCompletionKind::ExecutionFailed, gas_used, @@ -559,7 +566,7 @@ pub fn execute_keyless_deploy_call gas.erase_cost(effective_gas_limit); } notify_sandbox_end( - &observer, + &hook, SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }, ); make_error!(e) @@ -620,12 +627,12 @@ pub(crate) fn execute_keyless_deploy_sandbox::new_with_shared_ext_envs( sandbox_db, mega_spec, salt_env, oracle_env, ); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, parent_observer) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, parent_hook) } else { let sandbox_ctx = MegaContext::new(sandbox_db, mega_spec); - run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, empty_observer) + run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain, empty_hook) } } /// Applies the shared sandbox-context configuration, runs the sandbox tx, and returns the /// processed outcome. Factored out of `execute_keyless_deploy_sandbox` so the REX4+ and -/// pre-REX4 branches only differ in the `MegaContext` constructor and observer slot. +/// pre-REX4 branches only differ in the `MegaContext` constructor and hook slot. fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( sandbox_ctx: MegaContext, ExtEnvs>, sandbox_tx: MegaTransaction, sandbox_tx_limits: Option, block: BlockEnv, chain: L1BlockInfo, - observer: Option>>>, + hook: Option>, ) -> SandboxOutcome { let sandbox_ctx = match sandbox_tx_limits { Some(limits) => sandbox_ctx.with_tx_runtime_limits(limits), @@ -711,45 +718,63 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( let sandbox_ctx = sandbox_ctx.with_block(block).with_chain(chain).with_inside_sandbox(true); let is_rex5_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX5); let is_rex6_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX6); - if let Some(observer) = observer { - let mut sandbox_evm = - MegaEvm::new(sandbox_ctx).with_inspector(ObserverBridge::new(observer)); - let result = sandbox_evm.transact_raw(sandbox_tx); - let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); - let volatile_accesses = - sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); - process_sandbox_transact_result( - result, - limit_usage, - volatile_accesses, - is_rex5_enabled, - is_rex6_enabled, - ) - } else { - // Preserve the existing zero-observation path for every caller which - // has not attached a sandbox observer. - let mut sandbox_evm = MegaEvm::new(sandbox_ctx); - let result = sandbox_evm.transact_raw(sandbox_tx); - let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); - let volatile_accesses = - sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); - process_sandbox_transact_result( - result, - limit_usage, - volatile_accesses, - is_rex5_enabled, - is_rex6_enabled, - ) + match hook { + Some(SandboxHook::Observer(observer)) => { + let mut sandbox_evm = + MegaEvm::new(sandbox_ctx).with_inspector(ObserverBridge::new(observer)); + let result = sandbox_evm.transact_raw(sandbox_tx); + let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); + let volatile_accesses = + sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); + process_sandbox_transact_result( + result, + limit_usage, + volatile_accesses, + is_rex5_enabled, + is_rex6_enabled, + ) + } + Some(SandboxHook::Inspector(inspector)) => { + let mut sandbox_evm = + MegaEvm::new(sandbox_ctx).with_inspector(InspectorBridge::new(inspector)); + let result = sandbox_evm.transact_raw(sandbox_tx); + let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); + let volatile_accesses = + sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); + process_sandbox_transact_result( + result, + limit_usage, + volatile_accesses, + is_rex5_enabled, + is_rex6_enabled, + ) + } + None => { + // Preserve the existing zero-observation path for every caller which + // has not attached a sandbox observer. + let mut sandbox_evm = MegaEvm::new(sandbox_ctx); + let result = sandbox_evm.transact_raw(sandbox_tx); + let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); + let volatile_accesses = + sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); + process_sandbox_transact_result( + result, + limit_usage, + volatile_accesses, + is_rex5_enabled, + is_rex6_enabled, + ) + } } } -/// Delivers [`SandboxObserver::sandbox_end`] when an observer is attached. +/// Delivers `sandbox_end` when a hook is attached. fn notify_sandbox_end( - observer: &Option>>>, + hook: &Option>, outcome: SandboxEndOutcome, ) { - if let Some(obs) = observer { - obs.borrow_mut().sandbox_end(&outcome); + if let Some(h) = hook { + h.notify_end(&outcome); } } @@ -1301,7 +1326,11 @@ fn validate_signer_code( #[cfg(test)] mod tests { use super::*; + use core::cell::RefCell; use revm::context::result::Output; + use std::rc::Rc; + + use super::super::observer::SandboxObserver; /// Test error type that lets us drive `process_sandbox_transact_result`'s /// `Err` arms (which map to `SandboxOutcome::Rejected`) directly without @@ -1813,7 +1842,14 @@ mod tests { let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(SPLIT_CREATE_COMPUTE_BUDGET); - run_sandbox_ctx(context, tx, Some(limits), block, chain, observer) + run_sandbox_ctx( + context, + tx, + Some(limits), + block, + chain, + observer.map(SandboxHook::Observer), + ) } fn split_create_slot(outcome: &SandboxOutcome) -> (bool, Option) { diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs new file mode 100644 index 00000000..4dc06107 --- /dev/null +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -0,0 +1,454 @@ +//! Rewriting inspector channel for nested sandbox execution. +//! +//! A [`SandboxInspector`] is the intervention counterpart of [`super::SandboxObserver`]. +//! Hook signatures match revm's [`Inspector`] on the sandbox EVM: `&mut` inputs and +//! override return values are forwarded, so `CALL`/`CREATE` can be short-circuited and +//! outcomes rewritten. [`InspectorBridge`] installs the handle as the sandbox EVM's +//! inspector. Attaching an inspector is exclusive with attaching an observer. +//! +//! # Contract +//! +//! 1. With no hook attached, the sandbox path is unchanged. +//! 2. Attaching this channel without intervening leaves result, state, gas, and usage identical to +//! the unattached path. +//! 3. Interventions take effect inside the sandbox as they would on a top-level EVM. Reported +//! `gas_used` and usage are the post-intervention values; the parent frame records them as-is +//! and does not check conservation. +//! 4. The channel is node-local and non-consensus. An intervening node may diverge from the +//! network; the caller accepts that risk. +//! 5. Later specs measure interventions and refuse some shapes. Integrators must not depend on this +//! base being permissive. +//! +//! `sandbox_start` / `sandbox_end` are informational and cannot veto execution. + +#[cfg(not(feature = "std"))] +use alloc as std; +use core::cell::RefCell; +use std::rc::Rc; + +use alloy_primitives::{Address, Log, U256}; +use revm::{ + interpreter::{ + interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + Interpreter, + }, + Inspector, +}; + +use crate::{ExternalEnvTypes, MegaContext}; + +use super::{ + observer::{SandboxEndOutcome, SandboxObserver, SandboxStartInfo}, + state::SandboxDb, +}; + +/// Object-safe rewriting inspector for nested sandbox execution. +/// +/// Signatures match [`Inspector`]`<`[`MegaContext`]`<`[`SandboxDb`]`<'_>, ExtEnvs>, +/// `[`EthInterpreter`]`>`. All hooks have empty / `None` defaults so adding a hook is not a +/// breaking change. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` keep the +/// trait object-safe. +/// +/// Types that already implement [`Inspector`] for every sandbox context lifetime +/// receive a blanket [`SandboxInspector`] impl. Local types that are not inspectors +/// may still implement this trait by hand. +pub trait SandboxInspector { + /// Called before the sandbox interpreter is initialized. + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called before each opcode executes. + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called after each opcode executes. + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + ) { + let _ = interp; + let _ = context; + } + + /// Called when a log is emitted inside the sandbox. + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, ExtEnvs>, + log: Log, + ) { + let _ = interp; + let _ = context; + let _ = log; + } + + /// Called when a call frame is about to start. + /// + /// Returning `Some` overrides the call, matching [`Inspector::call`]. + #[inline] + fn call( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &mut CallInputs, + ) -> Option { + let _ = context; + let _ = inputs; + None + } + + /// Called when a call frame has concluded. Mutations of `outcome` are kept. + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + let _ = context; + let _ = inputs; + let _ = outcome; + } + + /// Called when a create frame is about to start. + /// + /// Returning `Some` overrides the create, matching [`Inspector::create`]. + #[inline] + fn create( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &mut CreateInputs, + ) -> Option { + let _ = context; + let _ = inputs; + None + } + + /// Called when a create frame has concluded. Mutations of `outcome` are kept. + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, ExtEnvs>, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + let _ = context; + let _ = inputs; + let _ = outcome; + } + + /// Called when a contract self-destructs inside the sandbox. + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + let _ = contract; + let _ = target; + let _ = value; + } + + /// Called once immediately before sandbox execution starts. Cannot veto. + #[inline] + fn sandbox_start(&mut self, info: &SandboxStartInfo) { + let _ = info; + } + + /// Called once with the terminal sandbox outcome. Paired with [`Self::sandbox_start`]. + /// Cannot veto. + #[inline] + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + let _ = outcome; + } +} + +impl SandboxInspector for I +where + E: ExternalEnvTypes, + I: for<'a> Inspector, E>, EthInterpreter>, +{ + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + Inspector::initialize_interp(self, interp, context); + } + + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + Inspector::step(self, interp, context); + } + + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + Inspector::step_end(self, interp, context); + } + + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + log: Log, + ) { + Inspector::log(self, interp, context, log); + } + + #[inline] + fn call( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + Inspector::call(self, context, inputs) + } + + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + Inspector::call_end(self, context, inputs, outcome); + } + + #[inline] + fn create( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { + Inspector::create(self, context, inputs) + } + + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + Inspector::create_end(self, context, inputs, outcome); + } + + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + Inspector::selfdestruct(self, contract, target, value); + } +} + +/// Inspector that forwards sandbox frames to a [`SandboxInspector`]. +/// +/// `call` / `create` return the inner override; `call_end` / `create_end` pass +/// `&mut outcome` through so mutations are visible to the EVM. +pub(crate) struct InspectorBridge { + inspector: Rc>>, +} + +impl core::fmt::Debug for InspectorBridge { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("InspectorBridge").finish_non_exhaustive() + } +} + +impl InspectorBridge { + /// Wraps a shared inspector handle as a revm inspector. + pub(crate) fn new(inspector: Rc>>) -> Self { + Self { inspector } + } +} + +impl Inspector, E>, EthInterpreter> + for InspectorBridge +{ + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.inspector.borrow_mut().initialize_interp(interp, context); + } + + #[inline] + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.inspector.borrow_mut().step(interp, context); + } + + #[inline] + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.inspector.borrow_mut().step_end(interp, context); + } + + #[inline] + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + log: Log, + ) { + self.inspector.borrow_mut().log(interp, context, log); + } + + #[inline] + fn call( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + self.inspector.borrow_mut().call(context, inputs) + } + + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + self.inspector.borrow_mut().call_end(context, inputs, outcome); + } + + #[inline] + fn create( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { + self.inspector.borrow_mut().create(context, inputs) + } + + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.inspector.borrow_mut().create_end(context, inputs, outcome); + } + + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.inspector.borrow_mut().selfdestruct(contract, target, value); + } +} + +/// Exclusive hook slot for nested sandbox execution. +/// +/// Either channel may occupy a slot; attaching one replaces the other. +pub(crate) enum SandboxHook { + /// Read-only observer channel. + Observer(Rc>>), + /// Rewriting inspector channel. + Inspector(Rc>>), +} + +impl Clone for SandboxHook { + fn clone(&self) -> Self { + match self { + Self::Observer(observer) => Self::Observer(Rc::clone(observer)), + Self::Inspector(inspector) => Self::Inspector(Rc::clone(inspector)), + } + } +} + +impl SandboxHook { + /// Delivers `sandbox_start` to the attached channel. + pub(crate) fn notify_start(&self, info: &SandboxStartInfo) { + match self { + Self::Observer(observer) => observer.borrow_mut().sandbox_start(info), + Self::Inspector(inspector) => inspector.borrow_mut().sandbox_start(info), + } + } + + /// Delivers `sandbox_end` to the attached channel. + pub(crate) fn notify_end(&self, outcome: &SandboxEndOutcome) { + match self { + Self::Observer(observer) => observer.borrow_mut().sandbox_end(outcome), + Self::Inspector(inspector) => inspector.borrow_mut().sandbox_end(outcome), + } + } +} + +impl core::fmt::Debug for SandboxHook { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Observer(_) => f.debug_tuple("Observer").finish_non_exhaustive(), + Self::Inspector(_) => f.debug_tuple("Inspector").finish_non_exhaustive(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{EmptyExternalEnv, TestExternalEnvs}; + use std::boxed::Box; + + struct LocalInspector; + + impl SandboxInspector for LocalInspector {} + + struct GenericInspector; + + impl Inspector for GenericInspector {} + + #[test] + fn test_sandbox_inspector_is_object_safe() { + let _boxed: Box> = Box::new(LocalInspector); + let _rc: Rc>> = + Rc::new(RefCell::new(LocalInspector)); + } + + #[test] + fn test_local_sandbox_inspector_coexists_with_blanket() { + fn assert_inspector>(_: T) {} + assert_inspector::(LocalInspector); + assert_inspector::(GenericInspector); + } + + #[test] + fn test_blanket_inspector_satisfies_empty_and_parent_env_bounds() { + fn assert_dual< + T: SandboxInspector + SandboxInspector, + >( + _: T, + ) { + } + assert_dual(GenericInspector); + assert_dual(LocalInspector); + } +} diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index bec595e8..9806897b 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -60,34 +60,42 @@ //! - `execution` - Core sandbox execution logic and the main entry point //! [`execute_keyless_deploy_call`] //! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution +//! - `inspector` - Rewriting [`SandboxInspector`] channel into nested sandbox execution //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal //! - `tx` - Transaction decoding and validation for pre-EIP-155 transactions //! - `error` - Error types ([`KeylessDeployError`]) that map to Solidity errors in `IKeylessDeploy` //! -//! # Sandbox observation +//! # Sandbox hooks //! -//! Nested sandbox execution is otherwise invisible to a parent inspector. Callers may -//! attach a [`SandboxObserver`] via [`crate::MegaContext::set_keyless_sandbox_observer`] -//! (also forwarded from [`crate::MegaEvm`] and [`crate::MegaBlockExecutor`]). The observer -//! sees interpreter hooks inside the sandbox plus a paired `sandbox_start` / `sandbox_end` -//! lifecycle. Observation cannot short-circuit `CALL`/`CREATE`. A compliant (read-only) -//! observer does not change execution results; no such guarantee is made for an observer -//! that mutates interpreter or context state. Reverted inner frames still emit their -//! events; whether sandbox state was applied to the parent is reported by -//! [`SandboxEndOutcome::state_applied`]. With no observer attached the sandbox path is -//! unchanged. +//! Nested sandbox execution is otherwise invisible to a parent inspector. Two exclusive +//! channels can attach through [`crate::MegaContext`] (also forwarded from [`crate::MegaEvm`] +//! and [`crate::MegaBlockExecutor`]): //! -//! Observation does not change sandbox external-env semantics: pre-REX4 sandboxes +//! - **Read-only (default).** [`SandboxObserver`] via +//! [`crate::MegaContext::set_keyless_sandbox_observer`]. Interpreter hooks plus a paired +//! `sandbox_start` / `sandbox_end` lifecycle. Cannot short-circuit `CALL`/`CREATE`. +//! - **Rewriting (explicit).** [`SandboxInspector`] via +//! [`crate::MegaContext::set_keyless_sandbox_inspector`]. Same lifecycle, but `&mut` inputs and +//! override return values are forwarded so interventions take effect inside the sandbox as they +//! would on a top-level EVM. +//! +//! A compliant (read-only) observer does not change execution results; no such +//! guarantee is made for an observer that mutates interpreter or context state. +//! Reverted inner frames still emit their events; whether sandbox state was applied +//! to the parent is reported by [`SandboxEndOutcome::state_applied`]. With no hook +//! attached the sandbox path is unchanged. +//! +//! Attaching a hook does not change sandbox external-env semantics: pre-REX4 sandboxes //! always run with [`crate::EmptyExternalEnv`] (minimum bucket capacity, no oracle //! data), and REX4+ sandboxes always share the parent env. Opcode-level hooks on -//! pre-REX4 are delivered through a second observer slot typed against -//! [`crate::EmptyExternalEnv`]. Observers that only implement [`SandboxObserver`] -//! for the parent env type can use -//! [`crate::MegaContext::set_keyless_sandbox_observer_for_parent_env`]; pre-REX4 -//! then emits only `sandbox_start` / `sandbox_end`. +//! pre-REX4 are delivered through a second slot typed against +//! [`crate::EmptyExternalEnv`]. Handles that only implement the parent env type can use +//! the `_for_parent_env` setters; pre-REX4 then emits only `sandbox_start` / +//! `sandbox_end`. //! //! [`SandboxObserver`]: observer::SandboxObserver +//! [`SandboxInspector`]: inspector::SandboxInspector //! [`SandboxEndOutcome::state_applied`]: observer::SandboxEndOutcome::state_applied //! //! # Type Erasure Strategy @@ -121,6 +129,7 @@ mod error; mod execution; +mod inspector; mod observer; mod state; mod state_merge; @@ -128,6 +137,7 @@ mod tx; pub use error::*; pub use execution::*; +pub use inspector::*; pub use observer::*; pub use state::*; pub use tx::*; From 62b2b615295ad2b6609b63895d154aeffb0a6fbb Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:00:41 +0800 Subject: [PATCH 11/28] test: cover keyless sandbox inspector interventions --- .../tests/rex2/keyless_sandbox_inspector.rs | 1183 +++++++++++++++++ crates/mega-evm/tests/rex2/main.rs | 1 + 2 files changed, 1184 insertions(+) create mode 100644 crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs new file mode 100644 index 00000000..d14a2cc6 --- /dev/null +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -0,0 +1,1183 @@ +//! Rewriting sandbox inspector channel: intervention cases with a no-intervention control arm. + +use std::{cell::RefCell, rc::Rc}; + +use alloy_primitives::{address, hex, Address, Bytes, U256}; +use mega_evm::{ + revm::context::result::{ExecutionResult, ResultAndState}, + sandbox::{ + decode_error_result, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, + SandboxInspector, SandboxObserver, SandboxRejectKind, SandboxStartInfo, + }, + test_utils::{BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, LimitUsage, MegaContext, MegaEvm, MegaHaltReason, + MegaSpecId, TestExternalEnvs, +}; +use revm::{ + bytecode::opcode::{CODECOPY, ISZERO, JUMPDEST, JUMPI, MLOAD, RETURN, SSTORE, STATICCALL}, + context::{ContextTr, JournalTr}, + handler::EvmTr, + inspector::NoOpInspector, + interpreter::{ + interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, + CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, InterpreterResult, + }, +}; +use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; + +use super::keyless_sandbox_support::{ + assert_result_and_state_eq, assert_usage_eq, constructor_calls_reverter, + constructor_touches_sentinel, create_pre_eip155_deploy_tx, + create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, + keyless_deploy_call_tx, keyless_deploy_call_tx_with_outer_gas, parent_compute_gas_used, + revert_constructor, run_keyless, run_keyless_with_usage, split_create_initcode, + success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_PRECOMPILE, + LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, SPECS, +}; + +const SUCCESS_TARGET: Address = address!("0000000000000000000000000000000000cccccc"); +const OTHER_DEPLOY_ADDRESS: Address = address!("0000000000000000000000000000000000dddddd"); +const IDENTITY_INPUT: U256 = U256::from_limbs([0x11, 0, 0, 0]); +const IDENTITY_OVERRIDE: U256 = U256::from_limbs([0x42, 0, 0, 0]); +const JOURNAL_SLOT: U256 = U256::from_limbs([0x53, 0, 0, 0]); +const JOURNAL_VALUE: U256 = U256::from_limbs([0x54, 0, 0, 0]); +const STEP_GAS_SURCHARGE: u64 = 1_000; + +fn constructor_calls_identity_and_stores_return() -> Bytes { + BytecodeBuilder::default() + .push_u256(IDENTITY_INPUT) + .push_number(0_u8) + .append(revm::bytecode::opcode::MSTORE) + .push_number(32_u8) + .push_number(0_u8) + .push_number(32_u8) + .push_number(0_u8) + .push_address(IDENTITY_PRECOMPILE) + .push_number(50_000_u32) + .append(STATICCALL) + .append(revm::bytecode::opcode::POP) + .push_number(0_u8) + .append(MLOAD) + .push_number(0_u8) + .append(SSTORE) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + +fn constructor_calls_success_target() -> Bytes { + let prefix = BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(SUCCESS_TARGET) + .push_number(50_000_u32) + .append(revm::bytecode::opcode::CALL) + .append(ISZERO) + .build_vec(); + let success_body = BytecodeBuilder::default() + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build_vec(); + let revert_pc = prefix.len() + 3 + success_body.len(); + assert!(revert_pc <= 255, "revert dest must fit in PUSH1"); + let mut code = prefix; + code.push(0x60); + code.push(revert_pc as u8); + code.push(JUMPI); + code.extend(success_body); + code.push(JUMPDEST); + code.extend(BytecodeBuilder::default().revert().build_vec()); + Bytes::from(code) +} + +struct InspectorRunConfig<'a, I> { + spec: MegaSpecId, + db: &'a mut MemoryDatabase, + tx_bytes: Bytes, + gas_limit_override: u64, + inspector: Option>>, + tx_limits: Option, + outer_gas_limit: u64, +} + +fn run_keyless_inspector(config: InspectorRunConfig<'_, I>) -> ResultAndState +where + I: SandboxInspector + 'static, +{ + run_keyless_inspector_with_usage(config).0 +} + +fn run_keyless_inspector_with_usage( + config: InspectorRunConfig<'_, I>, +) -> (ResultAndState, LimitUsage) +where + I: SandboxInspector + 'static, +{ + let mut context = MegaContext::new(config.db, config.spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + if let Some(limits) = config.tx_limits { + context = context.with_tx_runtime_limits(limits); + } + context.set_keyless_sandbox_inspector(config.inspector); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx_with_outer_gas( + config.tx_bytes, + config.gas_limit_override, + config.outer_gas_limit, + ); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + (result, usage) +} + +fn run_keyless_inspector_with_parent_env( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + env: TestExternalEnvs, + inspector: Option>>, +) -> ResultAndState +where + I: SandboxInspector + SandboxInspector + 'static, +{ + let mut context = MegaContext::new(db, spec).with_external_envs(env.into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(inspector); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact") +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum InspectedEvent { + Start, + End(SandboxEndOutcome), + Step(u8), + Call { target: Address }, + CallEnd { target: Address }, + Create, + CreateEnd, +} + +#[derive(Default)] +struct NopSandboxInspector; + +impl SandboxInspector for NopSandboxInspector {} + +#[derive(Default)] +struct RecordingInspector { + events: Vec, +} + +impl SandboxInspector for RecordingInspector { + fn step( + &mut self, + interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + self.events.push(InspectedEvent::Step(interp.bytecode.opcode())); + } + + fn call( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + self.events.push(InspectedEvent::Call { target: inputs.target_address }); + None + } + + fn call_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &CallInputs, + _outcome: &mut CallOutcome, + ) { + self.events.push(InspectedEvent::CallEnd { target: inputs.target_address }); + } + + fn create( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + _inputs: &mut CreateInputs, + ) -> Option { + self.events.push(InspectedEvent::Create); + None + } + + fn create_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.events.push(InspectedEvent::CreateEnd); + } + + fn sandbox_start(&mut self, _info: &SandboxStartInfo) { + self.events.push(InspectedEvent::Start); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(InspectedEvent::End(outcome.clone())); + } +} + +#[derive(Default)] +struct RecordingObserver { + events: Vec, +} + +impl SandboxObserver for RecordingObserver { + fn step( + &mut self, + interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + self.events.push(InspectedEvent::Step(interp.bytecode.opcode())); + } + + fn create( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + _inputs: &revm::interpreter::CreateInputs, + ) { + self.events.push(InspectedEvent::Create); + } + + fn sandbox_start(&mut self, _info: &SandboxStartInfo) { + self.events.push(InspectedEvent::Start); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(InspectedEvent::End(outcome.clone())); + } +} + +#[derive(Default)] +struct IdentityShortCircuit { + events: Vec, +} + +impl SandboxInspector for IdentityShortCircuit { + fn call( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + self.events.push(InspectedEvent::Call { target: inputs.target_address }); + if inputs.target_address == IDENTITY_PRECOMPILE { + return Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Return, + Bytes::from(IDENTITY_OVERRIDE.to_be_bytes::<32>()), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )); + } + None + } + + fn call_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &CallInputs, + _outcome: &mut CallOutcome, + ) { + self.events.push(InspectedEvent::CallEnd { target: inputs.target_address }); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(InspectedEvent::End(outcome.clone())); + } +} + +struct CreateShortCircuit { + address: Option
, + intercepted: bool, + end: Option, +} + +impl CreateShortCircuit { + fn new(address: Option
) -> Self { + Self { address, intercepted: false, end: None } + } +} + +impl SandboxInspector for CreateShortCircuit { + fn create( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { + if self.intercepted { + return None; + } + self.intercepted = true; + Some(CreateOutcome::new( + InterpreterResult::new( + InstructionResult::Return, + Bytes::from_static(&[0x00]), + Gas::new(inputs.gas_limit), + ), + self.address, + )) + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.end = Some(outcome.clone()); + } +} + +#[derive(Default)] +struct CallEndReverterRecorder { + end: Option, +} + +impl SandboxInspector for CallEndReverterRecorder { + fn call_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + if inputs.target_address == SUCCESS_TARGET { + outcome.result.result = InstructionResult::Revert; + outcome.result.output = Bytes::new(); + } + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.end = Some(outcome.clone()); + } +} + +#[derive(Default)] +struct StepGasSurcharge { + charged: bool, + end: Option, +} + +impl SandboxInspector for StepGasSurcharge { + fn step( + &mut self, + interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext, E>, + ) { + if !self.charged && interp.bytecode.opcode() == SSTORE { + self.charged = true; + let ok = interp.gas.record_cost(STEP_GAS_SURCHARGE); + assert!(ok, "surcharge must fit in remaining gas"); + } + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.end = Some(outcome.clone()); + } +} + +struct JournalWriter { + deploy_address: Address, + written: bool, +} + +impl JournalWriter { + fn new(deploy_address: Address) -> Self { + Self { deploy_address, written: false } + } +} + +impl SandboxInspector for JournalWriter { + fn step( + &mut self, + _interp: &mut Interpreter, + context: &mut mega_evm::MegaContext, E>, + ) { + if self.written { + return; + } + self.written = true; + context.journal_mut().load_account(self.deploy_address).expect("load deploy account"); + context + .journal_mut() + .sstore(self.deploy_address, JOURNAL_SLOT, JOURNAL_VALUE) + .expect("journal sstore"); + } +} + +fn slot_value(result: &ResultAndState, addr: Address, slot: U256) -> Option { + result + .state + .get(&addr) + .and_then(|account| account.storage.get(&slot).map(|storage| storage.present_value())) +} + +fn account_has_code(result: &ResultAndState, addr: Address) -> bool { + result.state.get(&addr).is_some_and(|account| { + account.info.code.as_ref().is_some_and(|code| !code.is_empty()) || + account.info.code_hash != revm::primitives::KECCAK_EMPTY + }) +} + +fn last_end(events: &[InspectedEvent]) -> SandboxEndOutcome { + events + .iter() + .rev() + .find_map(|event| match event { + InspectedEvent::End(outcome) => Some(outcome.clone()), + _ => None, + }) + .expect("sandbox_end") +} + +#[test] +fn test_inspector_no_intervention_parity_across_specs_and_constructors() { + let cases: [(&str, Bytes, bool, bool); 6] = [ + ("success", success_constructor(), false, false), + ("revert", revert_constructor(), false, false), + ("empty", empty_code_constructor(), false, false), + ("calls_reverter", constructor_calls_reverter(), true, false), + ("touches_sentinel", constructor_touches_sentinel(), false, true), + ("split", split_create_initcode(), false, false), + ]; + + for spec in SPECS { + for (name, init_code, with_reverter, with_sentinel) in &cases { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code.clone()); + let mut db_base = funded_db(signer); + if *with_reverter { + db_base.set_account_code(REVERTER, Bytes::from_static(&hex!("60006000fd"))); + } + if *with_sentinel { + db_base.set_account_balance(MERGE_FAIL_SENTINEL, U256::from(1)); + } + let mut db_nop = db_base.clone(); + let mut db_tracer = db_base.clone(); + + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + let nop = Rc::new(RefCell::new(NopSandboxInspector)); + let (nop_result, nop_usage) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_nop, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(nop), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + let case = format!("nop {name} {spec:?}"); + assert_result_and_state_eq(&nop_result, &baseline, &case); + assert_usage_eq(nop_usage, baseline_usage, &case); + + let tracer = Rc::new(RefCell::new(TracingInspector::new( + TracingInspectorConfig::default_parity(), + ))); + let (traced, traced_usage) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_tracer, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&tracer)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let case = format!("tracing {name} {spec:?}"); + assert_result_and_state_eq(&traced, &baseline, &case); + assert_usage_eq(traced_usage, baseline_usage, &case); + assert!( + tracer.borrow().traces().nodes().iter().any(|node| node.trace.kind.is_any_create()), + "{case}: TracingInspector must record a sandbox CREATE frame" + ); + } + } +} + +#[test] +fn test_inspector_call_short_circuit_writes_override_and_still_emits_call_end() { + for spec in SPECS { + let (tx_bytes, signer) = + create_pre_eip155_deploy_tx(constructor_calls_identity_and_stores_return()); + let deploy_address = signer.create(0); + let mut db_ctrl = funded_db(signer); + let mut db_int = db_ctrl.clone(); + + let control = run_keyless(RunConfig { + spec, + db: &mut db_ctrl, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_eq!( + slot_value(&control, deploy_address, U256::ZERO), + Some(IDENTITY_INPUT), + "{spec:?}: control arm stores identity echo" + ); + + let inspector = Rc::new(RefCell::new(IdentityShortCircuit::default())); + let result = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_eq!( + slot_value(&result, deploy_address, U256::ZERO), + Some(IDENTITY_OVERRIDE), + "{spec:?}: short-circuit arm stores override" + ); + + let events = inspector.borrow().events.clone(); + let call_ends = events + .iter() + .filter(|e| matches!(e, InspectedEvent::CallEnd { target } if *target == IDENTITY_PRECOMPILE)) + .count(); + assert_eq!(call_ends, 1, "{spec:?}: call_end on short-circuit frame once, got {events:?}"); + match last_end(&events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} + other => panic!("{spec:?}: expected Applied(Deployed), got {other:?}"), + } + } +} + +#[test] +fn test_inspector_create_short_circuit_address_mismatch_skips_apply() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let deploy_address = signer.create(0); + let mut db_ctrl = funded_db(signer); + let mut db_int = db_ctrl.clone(); + + let control = run_keyless(RunConfig { + spec, + db: &mut db_ctrl, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let control_inspector = Rc::new(RefCell::new(RecordingInspector::default())); + let mut db_ctrl_rec = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_ctrl_rec, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&control_inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&control_inspector.borrow().events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} + other => panic!("{spec:?}: control arm expected Applied(Deployed), got {other:?}"), + } + assert!(account_has_code(&control, deploy_address), "{spec:?}: control arm deploys code"); + + let inspector = Rc::new(RefCell::new(CreateShortCircuit::new(Some(OTHER_DEPLOY_ADDRESS)))); + let result = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert!( + matches!(result.result, ExecutionResult::Revert { .. }), + "{spec:?}: AddressMismatch is a reverting outer CALL: {:?}", + result.result + ); + let error = match &result.result { + ExecutionResult::Revert { output, .. } => decode_error_result(output), + other => panic!("{spec:?}: expected revert, got {other:?}"), + }; + assert!( + matches!(error, Some(KeylessDeployError::AddressMismatch)), + "{spec:?}: expected AddressMismatch, got {error:?}" + ); + match inspector.borrow().end.clone() { + Some(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::AddressMismatch }) => {} + other => panic!("{spec:?}: expected NotApplied(AddressMismatch), got {other:?}"), + } + assert!( + !account_has_code(&result, deploy_address), + "{spec:?}: parent must not hold code at deploy_address" + ); + assert!( + !account_has_code(&result, OTHER_DEPLOY_ADDRESS), + "{spec:?}: parent must not hold code at OTHER" + ); + assert!( + result.state.get(&OTHER_DEPLOY_ADDRESS).is_none_or(|account| { + account.storage.is_empty() && + account.info.code.as_ref().is_none_or(|c| c.is_empty()) + }), + "{spec:?}: parent must not hold sandbox state at OTHER" + ); + assert!( + result.state.get(&deploy_address).is_none_or(|account| { + account.storage.is_empty() && + account.info.code.as_ref().is_none_or(|c| c.is_empty()) + }), + "{spec:?}: parent must not hold sandbox state at deploy_address" + ); + } +} + +#[test] +fn test_inspector_create_short_circuit_without_address_is_rejected() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let deploy_address = signer.create(0); + let mut db = funded_db(signer); + let inspector = Rc::new(RefCell::new(CreateShortCircuit::new(None))); + let result = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert!( + matches!(result.result, ExecutionResult::Revert { .. }), + "{spec:?}: NoContractCreated reverts the outer CALL: {:?}", + result.result + ); + let error = match &result.result { + ExecutionResult::Revert { output, .. } => decode_error_result(output), + other => panic!("{spec:?}: expected revert, got {other:?}"), + }; + assert!( + matches!(error, Some(KeylessDeployError::NoContractCreated)), + "{spec:?}: expected NoContractCreated, got {error:?}" + ); + match inspector.borrow().end.clone() { + Some(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }) => {} + other => panic!("{spec:?}: expected NotApplied(Rejected), got {other:?}"), + } + assert!( + result.state.get(&deploy_address).is_none_or(|account| { + account.storage.is_empty() && + account.info.code.as_ref().is_none_or(|c| c.is_empty()) + }), + "{spec:?}: parent must not hold sandbox state" + ); + } +} + +#[test] +fn test_inspector_call_end_rewrite_reverts_constructor() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_calls_success_target()); + let mut db_ctrl = funded_db(signer); + db_ctrl.set_account_code(SUCCESS_TARGET, Bytes::from_static(&[0x00])); + let mut db_int = db_ctrl.clone(); + + let control_rec = Rc::new(RefCell::new(RecordingInspector::default())); + let control = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_ctrl, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&control_rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&control_rec.borrow().events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} + other => panic!("{spec:?}: control expected Applied(Deployed), got {other:?}"), + } + assert!(control.result.is_success(), "{spec:?}: control deploys"); + + let inspector = Rc::new(RefCell::new(CallEndReverterRecorder::default())); + let result = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match inspector.borrow().end.clone() { + Some(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, + .. + }) => {} + other => panic!("{spec:?}: expected Applied(ExecutionFailed), got {other:?}"), + } + assert!(result.result.is_success(), "{spec:?}: execution-failed is success-style outer"); + } +} + +#[test] +fn test_inspector_step_records_exact_sandbox_gas_surcharge() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db_base = funded_db(signer); + let mut db_int = db_base.clone(); + + let control_rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (baseline, _) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&control_rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let baseline_sandbox = match last_end(&control_rec.borrow().events) { + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::Deployed, + gas_used, + } => gas_used, + other => panic!("{spec:?}: baseline expected Applied(Deployed), got {other:?}"), + }; + + let inspector = Rc::new(RefCell::new(StepGasSurcharge::default())); + let (result, _) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&inspector)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match inspector.borrow().end.clone() { + Some(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::Deployed, + gas_used, + }) => { + assert_eq!( + gas_used, + baseline_sandbox + STEP_GAS_SURCHARGE, + "{spec:?}: sandbox_end.gas_used" + ); + } + other => panic!("{spec:?}: expected Applied(Deployed), got {other:?}"), + } + + // Outer CALL gas: pre-REX5 the signer pays sandbox gas, so the surcharge stays + // inside the sandbox. REX5+ pre-debits the reservation on the outer frame and + // refunds unused, so the extra 1_000 is billed to the outer tx. + let expected_outer_delta = match spec { + MegaSpecId::REX2 | MegaSpecId::REX3 | MegaSpecId::REX4 => 0, + MegaSpecId::REX5 | MegaSpecId::REX6 => STEP_GAS_SURCHARGE, + other => panic!("unexpected spec {other:?}"), + }; + assert_eq!( + result.result.gas_used().saturating_sub(baseline.result.gas_used()), + expected_outer_delta, + "{spec:?}: outer gas_used delta (got {}, baseline {})", + result.result.gas_used(), + baseline.result.gas_used() + ); + } +} + +#[test] +fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let deploy_address = signer.create(0); + let mut db_base = funded_db(signer); + let mut db_int = db_base.clone(); + + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let inspector = Rc::new(RefCell::new(JournalWriter::new(deploy_address))); + let (result, usage) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(inspector), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_eq!( + slot_value(&result, deploy_address, JOURNAL_SLOT), + Some(JOURNAL_VALUE), + "{spec:?}: successful deploy keeps journal write" + ); + assert_usage_eq(usage, baseline_usage, &format!("journal success {spec:?}")); + let _ = baseline; + + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_constructor()); + let deploy_address = signer.create(0); + let mut db_base = funded_db(signer); + let mut db_int = db_base.clone(); + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let inspector = Rc::new(RefCell::new(JournalWriter::new(deploy_address))); + let (result, usage) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_int, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(inspector), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_ne!( + slot_value(&result, deploy_address, JOURNAL_SLOT), + Some(JOURNAL_VALUE), + "{spec:?}: reverted constructor must not keep journal write" + ); + assert_usage_eq(usage, baseline_usage, &format!("journal revert {spec:?}")); + let _ = baseline; + } +} + +#[test] +fn test_inspector_pre_rex4_crowded_parent_env_keeps_empty_env_gas() { + for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db_empty = funded_db(signer); + let mut db_crowded = db_empty.clone(); + + let (empty_baseline, _) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_empty, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + let recorder = Rc::new(RefCell::new(RecordingInspector::default())); + let crowded = run_keyless_inspector_with_parent_env( + spec, + &mut db_crowded, + tx_bytes, + crowded_parent_env(), + Some(Rc::clone(&recorder)), + ); + assert_eq!( + crowded.result.gas_used(), + empty_baseline.result.gas_used(), + "{spec:?}: crowded parent env must not change pre-REX4 sandbox gas" + ); + let events = recorder.borrow().events.clone(); + assert!( + events.iter().any(|e| matches!(e, InspectedEvent::Step(_))), + "{spec:?}: opcode event stream must be non-empty: {events:?}" + ); + } +} + +#[test] +fn test_inspector_for_parent_env_skips_pre_rex4_opcode_hooks() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let recorder = Rc::new(RefCell::new(RecordingInspector::default())); + let inspector: Rc>> = recorder.clone(); + + let mut context = + MegaContext::new(&mut db, spec).with_external_envs(crowded_parent_env().into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector_for_parent_env(Some(inspector)); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); + let events = recorder.borrow().events.clone(); + + assert!(result.result.is_success(), "{spec:?} deploy should succeed: {:?}", result.result); + let starts = events.iter().filter(|e| matches!(e, InspectedEvent::Start)).count(); + let ends = events.iter().filter(|e| matches!(e, InspectedEvent::End(_))).count(); + assert_eq!(starts, 1, "{spec:?}: sandbox_start once"); + assert_eq!(ends, 1, "{spec:?}: sandbox_end once"); + let has_opcode = + events.iter().any(|e| matches!(e, InspectedEvent::Step(_) | InspectedEvent::Create)); + if spec.is_enabled(MegaSpecId::REX4) { + assert!( + has_opcode, + "{spec:?}: REX4+ parent-env inspector sees opcode hooks: {events:?}" + ); + } else { + assert!( + !has_opcode, + "{spec:?}: pre-REX4 parent-env inspector must not emit opcode hooks: {events:?}" + ); + } + } +} + +#[test] +fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { + let spec = MegaSpecId::REX5; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + + let mut db_obs_then_insp = funded_db(signer); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let inspector = Rc::new(RefCell::new(RecordingInspector::default())); + let mut context = MegaContext::new(&mut db_obs_then_insp, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_observer(Some(Rc::clone(&observer))); + context.set_keyless_sandbox_inspector(Some(Rc::clone(&inspector))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); + assert!( + observer.borrow().events.is_empty(), + "observer must be displaced by inspector: {:?}", + observer.borrow().events + ); + assert!( + inspector.borrow().events.iter().any(|e| matches!(e, InspectedEvent::Start)), + "inspector that replaced observer must receive events" + ); + + let mut db_insp_then_obs = funded_db(signer); + let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let inspector = Rc::new(RefCell::new(RecordingInspector::default())); + let mut context = MegaContext::new(&mut db_insp_then_obs, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(Some(Rc::clone(&inspector))); + context.set_keyless_sandbox_observer(Some(Rc::clone(&observer))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); + assert!( + inspector.borrow().events.is_empty(), + "inspector must be displaced by observer: {:?}", + inspector.borrow().events + ); + assert!( + observer.borrow().events.iter().any(|e| matches!(e, InspectedEvent::Start)), + "observer that replaced inspector must receive events" + ); + + let mut db_cleared = funded_db(signer); + let mut db_base = db_cleared.clone(); + let mut context = MegaContext::new(&mut db_cleared, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopSandboxInspector)))); + context.clear_keyless_sandbox_hook(); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); + let cleared = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_result_and_state_eq(&cleared, &baseline, "cleared hook"); + let _ = baseline_usage; +} + +#[test] +#[cfg(debug_assertions)] +#[should_panic(expected = "set sandbox hook after external envs are wired")] +fn test_with_external_envs_panics_in_debug_when_inspector_is_attached() { + let mut context = MegaContext::new(revm::database::EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopSandboxInspector)))); + let _ = context.with_external_envs(TestExternalEnvs::::new().into()); +} + +#[test] +fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { + let spec = MegaSpecId::REX5; + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} + other => panic!("Deployed: {other:?}"), + } + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(empty_code_constructor()); + let mut db = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::EmptyCode, .. } => {} + other => panic!("EmptyCode: {other:?}"), + } + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_constructor()); + let mut db = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, .. + } => {} + other => panic!("ExecutionFailed: {other:?}"), + } + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, _signer) = + create_pre_eip155_deploy_tx_with_value(success_constructor(), U256::from(1)); + let mut db = MemoryDatabase::default(); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected } => {} + other => panic!("Rejected: {other:?}"), + } + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let used = parent_compute_gas_used(spec, signer, tx_bytes.clone()); + let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(used.saturating_sub(1)); + let mut db = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: Some(limits), + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::PostAccountingHalt } => {} + other => panic!("PostAccountingHalt: {other:?}"), + } + + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_touches_sentinel()); + let mut inner = funded_db(signer); + inner.set_account_balance(MERGE_FAIL_SENTINEL, U256::from(1)); + let mut db = ErrorInjectingDatabase::new(inner); + db.fail_on_account = Some(MERGE_FAIL_SENTINEL); + db.fail_on_account_skip = 1; + let mut context = MegaContext::new(&mut db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(Some(Rc::clone(&rec))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::ApplyFailed } => {} + other => panic!("ApplyFailed: {other:?}"), + } + + let rec = Rc::new(RefCell::new(CreateShortCircuit::new(Some(OTHER_DEPLOY_ADDRESS)))); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let _ = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let end = rec.borrow().end.clone(); + match end { + Some(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::AddressMismatch }) => {} + other => panic!("AddressMismatch: {other:?}"), + } +} diff --git a/crates/mega-evm/tests/rex2/main.rs b/crates/mega-evm/tests/rex2/main.rs index 1e8d0252..f0272ed0 100644 --- a/crates/mega-evm/tests/rex2/main.rs +++ b/crates/mega-evm/tests/rex2/main.rs @@ -1,6 +1,7 @@ //! Tests for Rex2 hardfork features. mod keyless_deploy; +mod keyless_sandbox_inspector; mod keyless_sandbox_observer; mod keyless_sandbox_support; mod oracle_hint; From 60a14c4b45d11fa4680e5398d9c5d9fdf0718f5c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:00:54 +0800 Subject: [PATCH 12/28] docs: document the keyless sandbox inspector channel --- crates/mega-evm/src/evm/AGENTS.md | 12 ++++++++++++ crates/mega-evm/src/sandbox/observer.rs | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 0245c104..4988f914 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -22,6 +22,18 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - `MegaEvm` methods read aggregate resource usage from `additional_limit` after execution. - Keep inspector and non-inspector paths behaviorally aligned. +## Keyless sandbox hooks +Two exclusive channels into nested keyless-deploy sandbox execution, attached on `MegaContext` (and forwarded from `MegaEvm` / `MegaBlockExecutor`). +Read-only default: `SandboxObserver` cannot short-circuit `CALL`/`CREATE` and must not mutate interpreter or context state. +Rewriting explicit: `SandboxInspector` forwards `&mut` inputs and override return values so interventions take effect inside the sandbox as they would on a top-level EVM. + +Contract: +1. With no hook attached, the sandbox path is unchanged. +2. Attaching the rewriting channel without intervening leaves result, state, gas, and usage identical to the unattached path. +3. Interventions take effect inside the sandbox as they would on a top-level EVM; reported `gas_used` and usage are the post-intervention values, and the parent frame records them as-is without a conservation check. +4. The channel is node-local and non-consensus; an intervening node may diverge from the network, and the caller accepts that risk. +5. Later specs measure interventions and refuse some shapes; integrators must not depend on this base being permissive. + ## WHERE TO LOOK - New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` and `rex7` currently alias their predecessor, expressing their deltas as `is_enabled` dispatch inside the shared handlers). - Volatile access detention trigger changes: `host.rs` and volatile wrappers in `instructions.rs`. diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 2bd51a31..5d502fb5 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -1,5 +1,11 @@ //! Read-only observation channel for nested sandbox execution. //! +//! Keyless sandbox execution has two hook channels. This module is the +//! **read-only default**: attach via [`crate::MegaContext::set_keyless_sandbox_observer`]. +//! The rewriting channel is [`crate::sandbox::SandboxInspector`], attached explicitly +//! via [`crate::MegaContext::set_keyless_sandbox_inspector`]. The two occupy the same +//! slot; attaching one replaces the other. +//! //! A [`SandboxObserver`] sees every interpreter hook that revm's [`Inspector`] //! would see inside a sandbox, plus a paired [`SandboxObserver::sandbox_start`] / //! [`SandboxObserver::sandbox_end`] lifecycle. Observation cannot short-circuit From e7374299130975eec01dbc87555ca9df98fb8a82 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:59:17 +0800 Subject: [PATCH 13/28] test: tighten keyless sandbox inspector assertions --- .../tests/rex2/keyless_sandbox_inspector.rs | 297 +++++++++++++----- 1 file changed, 217 insertions(+), 80 deletions(-) diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index d14a2cc6..1ef9f8e2 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -32,7 +32,7 @@ use super::keyless_sandbox_support::{ keyless_deploy_call_tx, keyless_deploy_call_tx_with_outer_gas, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_usage, split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_PRECOMPILE, - LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, SPECS, + LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, MERGE_FAIL_SENTINEL, REVERTER, SPECS, }; const SUCCESS_TARGET: Address = address!("0000000000000000000000000000000000cccccc"); @@ -450,6 +450,140 @@ fn last_end(events: &[InspectedEvent]) -> SandboxEndOutcome { .expect("sandbox_end") } +fn sandbox_applied_deployed_gas(events: &[InspectedEvent], case: &str) -> u64 { + match last_end(events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, gas_used } => { + gas_used + } + other => panic!("{case}: expected Applied(Deployed), got {other:?}"), + } +} + +/// No-intervention success deploy used as the control arm for CREATE short-circuit cases. +fn run_unintervened_success_deploy( + spec: MegaSpecId, + tx_bytes: Bytes, + signer: Address, +) -> ResultAndState { + let rec = Rc::new(RefCell::new(RecordingInspector::default())); + let mut db = funded_db(signer); + let result = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + match last_end(&rec.borrow().events) { + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} + other => panic!("{spec:?}: control arm expected Applied(Deployed), got {other:?}"), + } + assert!(account_has_code(&result, signer.create(0)), "{spec:?}: control arm deploys code"); + assert_control_signer_applied(&result, signer, spec); + result +} + +fn assert_control_signer_applied( + result: &ResultAndState, + signer: Address, + spec: MegaSpecId, +) { + let account = result.state.get(&signer).unwrap_or_else(|| { + panic!("{spec:?}: control arm parent state must contain signer after apply") + }); + assert!(account.is_touched(), "{spec:?}: control arm signer must be touched after apply"); + assert_eq!(account.info.nonce, 1, "{spec:?}: control arm signer nonce after apply"); + if spec.is_enabled(MegaSpecId::REX5) { + assert_eq!( + account.info.balance, + U256::from(LARGE_SIGNER_BALANCE), + "{spec:?}: REX5+ control arm signer balance stays at LARGE_SIGNER_BALANCE" + ); + } else { + assert!( + account.info.balance < U256::from(LARGE_SIGNER_BALANCE), + "{spec:?}: pre-REX5 control arm signer balance must drop, got {}", + account.info.balance + ); + let beneficiary = result.state.get(&Address::ZERO).unwrap_or_else(|| { + panic!( + "{spec:?}: pre-REX5 control arm parent state must contain beneficiary after apply" + ) + }); + assert!( + beneficiary.is_touched(), + "{spec:?}: pre-REX5 control arm beneficiary must be touched after apply" + ); + assert!( + beneficiary.info.balance > U256::ZERO, + "{spec:?}: pre-REX5 control arm beneficiary balance must increase after apply, got {}", + beneficiary.info.balance + ); + } +} + +fn assert_parent_did_not_receive_sandbox_apply( + result: &ResultAndState, + signer: Address, + deploy_address: Address, + spec: MegaSpecId, +) { + match result.state.get(&signer) { + None => {} + Some(account) => { + assert!( + !account.is_touched(), + "{spec:?}: signer must not be touched when apply is skipped" + ); + assert_eq!( + account.info.balance, + U256::from(LARGE_SIGNER_BALANCE), + "{spec:?}: signer balance must stay at LARGE_SIGNER_BALANCE when apply is skipped" + ); + assert_eq!( + account.info.nonce, 0, + "{spec:?}: signer nonce must stay 0 when apply is skipped" + ); + } + } + + // Tests use `MegaContext::new`, whose `BlockEnv` beneficiary defaults to `Address::ZERO`. + // The outer tx still marks the coinbase touched; apply is visible here as a balance change. + match result.state.get(&Address::ZERO) { + None => {} + Some(account) => { + assert_eq!( + account.info.balance, + U256::ZERO, + "{spec:?}: beneficiary balance must be unchanged when apply is skipped" + ); + } + } + + assert!( + !account_has_code(result, deploy_address), + "{spec:?}: parent must not hold code at deploy_address" + ); + assert!( + !account_has_code(result, OTHER_DEPLOY_ADDRESS), + "{spec:?}: parent must not hold code at OTHER" + ); + assert!( + result.state.get(&OTHER_DEPLOY_ADDRESS).is_none_or(|account| { + account.storage.is_empty() && account.info.code.as_ref().is_none_or(|c| c.is_empty()) + }), + "{spec:?}: parent must not hold sandbox state at OTHER" + ); + assert!( + result.state.get(&deploy_address).is_none_or(|account| { + account.storage.is_empty() && account.info.code.as_ref().is_none_or(|c| c.is_empty()) + }), + "{spec:?}: parent must not hold sandbox state at deploy_address" + ); +} + #[test] fn test_inspector_no_intervention_parity_across_specs_and_constructors() { let cases: [(&str, Bytes, bool, bool); 6] = [ @@ -580,36 +714,11 @@ fn test_inspector_create_short_circuit_address_mismatch_skips_apply() { for spec in SPECS { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let deploy_address = signer.create(0); - let mut db_ctrl = funded_db(signer); - let mut db_int = db_ctrl.clone(); - let control = run_keyless(RunConfig { - spec, - db: &mut db_ctrl, - tx_bytes: tx_bytes.clone(), - gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, - tx_limits: None, - outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, - }); - let control_inspector = Rc::new(RefCell::new(RecordingInspector::default())); - let mut db_ctrl_rec = funded_db(signer); - let _ = run_keyless_inspector(InspectorRunConfig { - spec, - db: &mut db_ctrl_rec, - tx_bytes: tx_bytes.clone(), - gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - inspector: Some(Rc::clone(&control_inspector)), - tx_limits: None, - outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, - }); - match last_end(&control_inspector.borrow().events) { - SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} - other => panic!("{spec:?}: control arm expected Applied(Deployed), got {other:?}"), - } - assert!(account_has_code(&control, deploy_address), "{spec:?}: control arm deploys code"); + let _control = run_unintervened_success_deploy(spec, tx_bytes.clone(), signer); let inspector = Rc::new(RefCell::new(CreateShortCircuit::new(Some(OTHER_DEPLOY_ADDRESS)))); + let mut db_int = funded_db(signer); let result = run_keyless_inspector(InspectorRunConfig { spec, db: &mut db_int, @@ -636,28 +745,7 @@ fn test_inspector_create_short_circuit_address_mismatch_skips_apply() { Some(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::AddressMismatch }) => {} other => panic!("{spec:?}: expected NotApplied(AddressMismatch), got {other:?}"), } - assert!( - !account_has_code(&result, deploy_address), - "{spec:?}: parent must not hold code at deploy_address" - ); - assert!( - !account_has_code(&result, OTHER_DEPLOY_ADDRESS), - "{spec:?}: parent must not hold code at OTHER" - ); - assert!( - result.state.get(&OTHER_DEPLOY_ADDRESS).is_none_or(|account| { - account.storage.is_empty() && - account.info.code.as_ref().is_none_or(|c| c.is_empty()) - }), - "{spec:?}: parent must not hold sandbox state at OTHER" - ); - assert!( - result.state.get(&deploy_address).is_none_or(|account| { - account.storage.is_empty() && - account.info.code.as_ref().is_none_or(|c| c.is_empty()) - }), - "{spec:?}: parent must not hold sandbox state at deploy_address" - ); + assert_parent_did_not_receive_sandbox_apply(&result, signer, deploy_address, spec); } } @@ -666,6 +754,8 @@ fn test_inspector_create_short_circuit_without_address_is_rejected() { for spec in SPECS { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let deploy_address = signer.create(0); + let _control = run_unintervened_success_deploy(spec, tx_bytes.clone(), signer); + let mut db = funded_db(signer); let inspector = Rc::new(RefCell::new(CreateShortCircuit::new(None))); let result = run_keyless_inspector(InspectorRunConfig { @@ -807,9 +897,9 @@ fn test_inspector_step_records_exact_sandbox_gas_surcharge() { other => panic!("unexpected spec {other:?}"), }; assert_eq!( - result.result.gas_used().saturating_sub(baseline.result.gas_used()), - expected_outer_delta, - "{spec:?}: outer gas_used delta (got {}, baseline {})", + result.result.gas_used(), + baseline.result.gas_used() + expected_outer_delta, + "{spec:?}: outer gas_used (got {}, baseline {})", result.result.gas_used(), baseline.result.gas_used() ); @@ -884,42 +974,85 @@ fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { } } +fn run_inspector_empty_and_crowded_parent_env( + spec: MegaSpecId, + tx_bytes: Bytes, + signer: Address, +) -> (ResultAndState, u64, ResultAndState, u64, Vec) +{ + let mut db_empty = funded_db(signer); + let mut db_crowded = db_empty.clone(); + + let empty_rec = Rc::new(RefCell::new(RecordingInspector::default())); + let empty = run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db_empty, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::clone(&empty_rec)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let empty_sandbox_gas = + sandbox_applied_deployed_gas(&empty_rec.borrow().events, &format!("empty {spec:?}")); + + let crowded_rec = Rc::new(RefCell::new(RecordingInspector::default())); + let crowded = run_keyless_inspector_with_parent_env( + spec, + &mut db_crowded, + tx_bytes, + crowded_parent_env(), + Some(Rc::clone(&crowded_rec)), + ); + let crowded_events = crowded_rec.borrow().events.clone(); + let crowded_sandbox_gas = + sandbox_applied_deployed_gas(&crowded_events, &format!("crowded {spec:?}")); + + (empty, empty_sandbox_gas, crowded, crowded_sandbox_gas, crowded_events) +} + #[test] fn test_inspector_pre_rex4_crowded_parent_env_keeps_empty_env_gas() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); - let mut db_empty = funded_db(signer); - let mut db_crowded = db_empty.clone(); + let (empty, empty_sandbox_gas, crowded, crowded_sandbox_gas, crowded_events) = + run_inspector_empty_and_crowded_parent_env(spec, tx_bytes, signer); - let (empty_baseline, _) = run_keyless_with_usage(RunConfig { - spec, - db: &mut db_empty, - tx_bytes: tx_bytes.clone(), - gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, - tx_limits: None, - outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, - }); - - let recorder = Rc::new(RefCell::new(RecordingInspector::default())); - let crowded = run_keyless_inspector_with_parent_env( - spec, - &mut db_crowded, - tx_bytes, - crowded_parent_env(), - Some(Rc::clone(&recorder)), + assert_eq!( + crowded_sandbox_gas, empty_sandbox_gas, + "{spec:?}: crowded parent env must not change pre-REX4 sandbox gas" ); assert_eq!( crowded.result.gas_used(), - empty_baseline.result.gas_used(), - "{spec:?}: crowded parent env must not change pre-REX4 sandbox gas" + empty.result.gas_used(), + "{spec:?}: crowded parent env must not change pre-REX4 outer gas" + ); + assert_result_and_state_eq( + &crowded, + &empty, + &format!("pre-REX4 crowded vs empty parent env {spec:?}"), ); - let events = recorder.borrow().events.clone(); assert!( - events.iter().any(|e| matches!(e, InspectedEvent::Step(_))), - "{spec:?}: opcode event stream must be non-empty: {events:?}" + crowded_events.iter().any(|e| matches!(e, InspectedEvent::Step(_))), + "{spec:?}: opcode event stream must be non-empty: {crowded_events:?}" ); } + + // REX4 shares the parent env, so crowded buckets must change sandbox gas. + // This arm proves the equality above can fail when env freezing is absent. + let spec = MegaSpecId::REX4; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let (_empty, empty_sandbox_gas, _crowded, crowded_sandbox_gas, _) = + run_inspector_empty_and_crowded_parent_env(spec, tx_bytes, signer); + assert_ne!( + crowded_sandbox_gas, empty_sandbox_gas, + "{spec:?}: crowded parent env must change sandbox gas once env sharing is on" + ); + assert_eq!(empty_sandbox_gas, 129_528, "{spec:?}: empty-env sandbox gas (env-freeze baseline)"); + assert_eq!( + crowded_sandbox_gas, 493_528, + "{spec:?}: crowded-env sandbox gas (env-sharing witness)" + ); } #[test] @@ -1026,6 +1159,7 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); let cleared = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); + let cleared_usage = evm.ctx_ref().additional_limit.borrow().get_usage(); let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_base, @@ -1036,7 +1170,7 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); assert_result_and_state_eq(&cleared, &baseline, "cleared hook"); - let _ = baseline_usage; + assert_usage_eq(cleared_usage, baseline_usage, "cleared hook"); } #[test] @@ -1055,7 +1189,7 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { let rec = Rc::new(RefCell::new(RecordingInspector::default())); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let _ = run_keyless_inspector(InspectorRunConfig { + let deployed = run_keyless_inspector(InspectorRunConfig { spec, db: &mut db, tx_bytes, @@ -1068,6 +1202,7 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } => {} other => panic!("Deployed: {other:?}"), } + assert_control_signer_applied(&deployed, signer, spec); let rec = Rc::new(RefCell::new(RecordingInspector::default())); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(empty_code_constructor()); @@ -1165,8 +1300,9 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { let rec = Rc::new(RefCell::new(CreateShortCircuit::new(Some(OTHER_DEPLOY_ADDRESS)))); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let deploy_address = signer.create(0); let mut db = funded_db(signer); - let _ = run_keyless_inspector(InspectorRunConfig { + let mismatch = run_keyless_inspector(InspectorRunConfig { spec, db: &mut db, tx_bytes, @@ -1180,4 +1316,5 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { Some(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::AddressMismatch }) => {} other => panic!("AddressMismatch: {other:?}"), } + assert_parent_did_not_receive_sandbox_apply(&mismatch, signer, deploy_address, spec); } From c67826b73a89aa3459970a9be5b32dd4958fd35f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:59:17 +0800 Subject: [PATCH 14/28] docs: unlink ExtEnvs rustdoc and note malformed synthetic panics --- crates/mega-evm/src/evm/AGENTS.md | 1 + crates/mega-evm/src/evm/context.rs | 12 ++++++------ crates/mega-evm/src/evm/mod.rs | 8 ++++---- crates/mega-evm/src/sandbox/inspector.rs | 4 +++- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 4988f914..2bcb3f23 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -31,6 +31,7 @@ Contract: 1. With no hook attached, the sandbox path is unchanged. 2. Attaching the rewriting channel without intervening leaves result, state, gas, and usage identical to the unattached path. 3. Interventions take effect inside the sandbox as they would on a top-level EVM; reported `gas_used` and usage are the post-intervention values, and the parent frame records them as-is without a conservation check. + Malformed synthetic outcomes, such as a `memory_offset` outside the frame's memory, panic exactly as they would on a top-level EVM; the sandbox neither isolates nor amplifies that. 4. The channel is node-local and non-consensus; an intervening node may diverge from the network, and the caller accepts that risk. 5. Later specs measure interventions and refuse some shapes; integrators must not depend on this base being permissive. diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index c279365c..d2cfc1d6 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -77,11 +77,11 @@ pub struct MegaContext { /// before the sandbox runs). pub(crate) inside_sandbox: Rc>, - /// Hook for nested sandbox execution, typed against this context's [`ExtEnvs`]. + /// Hook for nested sandbox execution, typed against this context's `ExtEnvs`. /// /// `None` keeps the historical no-inspector sandbox path. REX4+ sandboxes /// share the parent env and deliver opcode-level hooks through this slot. - /// Changing [`ExtEnvs`] via [`Self::with_external_envs`] resets this field + /// Changing `ExtEnvs` via [`Self::with_external_envs`] resets this field /// and the [`EmptyExternalEnv`] hook slot: the hook cannot be carried across /// an env-type change and must be attached after external environments are /// assembled. Observer and inspector occupy the same slot exclusively. @@ -496,7 +496,7 @@ impl MegaContext { /// Attaches an observer for nested sandbox execution on every spec. /// /// The observer must implement [`SandboxObserver`] for both this context's - /// [`ExtEnvs`] and [`EmptyExternalEnv`]. The same handle is stored as two + /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored as two /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent /// env). [`crate::sandbox::InspectorSandboxObserver`] with a fully generic @@ -527,7 +527,7 @@ impl MegaContext { } /// Attaches an observer that implements [`SandboxObserver`] only for this - /// context's [`ExtEnvs`]. + /// context's `ExtEnvs`. /// /// Opcode-level observation is available on REX4+ sandboxes, which share /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit @@ -547,7 +547,7 @@ impl MegaContext { /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// /// The inspector must implement [`SandboxInspector`] for both this context's - /// [`ExtEnvs`] and [`EmptyExternalEnv`]. The same handle is stored as two + /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored as two /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent /// env). A type that implements [`revm::Inspector`] for every sandbox @@ -578,7 +578,7 @@ impl MegaContext { } /// Attaches an inspector that implements [`SandboxInspector`] only for this - /// context's [`ExtEnvs`]. + /// context's `ExtEnvs`. /// /// Opcode-level inspection is available on REX4+ sandboxes, which share /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 2071ae89..56380679 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -240,7 +240,7 @@ impl MegaEvm { /// Attaches an observer for nested sandbox execution on every spec. /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. The observer - /// must implement [`SandboxObserver`] for both this EVM's [`ExtEnvs`] and + /// must implement [`SandboxObserver`] for both this EVM's `ExtEnvs` and /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to /// detach. pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) @@ -251,7 +251,7 @@ impl MegaEvm { } /// Attaches an observer that implements [`SandboxObserver`] only for this - /// EVM's [`ExtEnvs`]. + /// EVM's `ExtEnvs`. /// /// Forwards to [`MegaContext::set_keyless_sandbox_observer_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. @@ -266,7 +266,7 @@ impl MegaEvm { /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// /// Forwards to [`MegaContext::set_keyless_sandbox_inspector`]. The inspector - /// must implement [`SandboxInspector`] for both this EVM's [`ExtEnvs`] and + /// must implement [`SandboxInspector`] for both this EVM's `ExtEnvs` and /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to detach. pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) where @@ -276,7 +276,7 @@ impl MegaEvm { } /// Attaches an inspector that implements [`SandboxInspector`] only for this - /// EVM's [`ExtEnvs`]. + /// EVM's `ExtEnvs`. /// /// Forwards to [`MegaContext::set_keyless_sandbox_inspector_for_parent_env`]. /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs index 4dc06107..1103f5ec 100644 --- a/crates/mega-evm/src/sandbox/inspector.rs +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -13,7 +13,9 @@ //! the unattached path. //! 3. Interventions take effect inside the sandbox as they would on a top-level EVM. Reported //! `gas_used` and usage are the post-intervention values; the parent frame records them as-is -//! and does not check conservation. +//! and does not check conservation. Malformed synthetic outcomes, such as a `memory_offset` +//! outside the frame's memory, panic exactly as they would on a top-level EVM; the sandbox +//! neither isolates nor amplifies that. //! 4. The channel is node-local and non-consensus. An intervening node may diverge from the //! network; the caller accepts that risk. //! 5. Later specs measure interventions and refuse some shapes. Integrators must not depend on this From d48dce31debeb99a6027c890a956f4ec1d658c3c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:01:24 +0800 Subject: [PATCH 15/28] test: add mainnet keyless deploy replay fixtures --- .../fixtures/keyless_deploy_rex2_large.json | 106 ++++++++ .../fixtures/keyless_deploy_rex2_medium.json | 106 ++++++++ .../fixtures/keyless_deploy_rex2_small.json | 106 ++++++++ bench/replay/manifest.json | 8 + .../replay_offline_keyless.cache.json | 257 ++++++++++++++++++ bin/mega-evme/tests/replay_keyless.rs | 125 +++++++++ 6 files changed, 708 insertions(+) create mode 100644 bench/replay/fixtures/keyless_deploy_rex2_large.json create mode 100644 bench/replay/fixtures/keyless_deploy_rex2_medium.json create mode 100644 bench/replay/fixtures/keyless_deploy_rex2_small.json create mode 100644 bin/mega-evme/tests/fixtures/replay_offline_keyless.cache.json create mode 100644 bin/mega-evme/tests/replay_keyless.rs diff --git a/bench/replay/fixtures/keyless_deploy_rex2_large.json b/bench/replay/fixtures/keyless_deploy_rex2_large.json new file mode 100644 index 00000000..c6627c59 --- /dev/null +++ b/bench/replay/fixtures/keyless_deploy_rex2_large.json @@ -0,0 +1,106 @@ +{ + "replay_0xe2e63e7b5fe6d247737ec0ec32b90bfdc565754144cb065a013829d3d81b0aac": { + "_info": null, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x1820a4b7618bde71dce8cdc73aab6c95905fad24": { + "balance": "0x0", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0x4200000000000000000000000000000000000011": { + "balance": "0x3bd8dbefab882752", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x4200000000000000000000000000000000000019": { + "balance": "0x378742917f0992c0", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001a": { + "balance": "0x34c5c27a29ff462d", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001b": { + "balance": "0x0", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x551c615f7a63ca915857048133f827147f52f25d": { + "balance": "0x235d36f9970f", + "code": "0x", + "nonce": "0xa", + "storage": {} + }, + "0xa990077c3205cbdf861e17fa532eeb069ce9ff96": { + "balance": "0x241bb6d01211c38e", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex2": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x159aa597ae030d4584d69c875fdd9d2798bd41382b2a80a5b0407a7e51f94179", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 1112970, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000018d03b50000000000000000000000000000000000000000000000000000000000000a3bf90a388085174876e800830c35008080b909e5608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c00291ba01820182018201820182018201820182018201820182018201820182018201820a018201820182018201820182018201820182018201820182018201820182018200000000000" + ], + "gasLimit": [ + "0x1149ac" + ], + "gasPrice": null, + "nonce": "0xa", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sender": "0x551c615f7a63ca915857048133f827147f52f25d", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x00000000000000000000000000000000000000000000000000000000018d03b50000000000000000000000001820a4b7618bde71dce8cdc73aab6c95905fad2400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_deploy_rex2_medium.json b/bench/replay/fixtures/keyless_deploy_rex2_medium.json new file mode 100644 index 00000000..80ed92a2 --- /dev/null +++ b/bench/replay/fixtures/keyless_deploy_rex2_medium.json @@ -0,0 +1,106 @@ +{ + "replay_0xdacfcad9be0b4867d9a2e676706a7b6a17c3096e3e148f1aa170eed6d1d84296": { + "_info": null, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x7b0c95", + "currentTimestamp": "0x698d3268", + "currentBaseFee": "0xf4240", + "currentRandom": "0x6394f13742b0fca9ae6084a56bbc4a5d724e383944dbbc8e926cb70b0e86e40", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x4200000000000000000000000000000000000011": { + "balance": "0x995494d86aee18dc", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x4200000000000000000000000000000000000019": { + "balance": "0x7020024a8de37780", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001a": { + "balance": "0x34d6bb89838b60d9", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001b": { + "balance": "0x0", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x551c615f7a63ca915857048133f827147f52f25d": { + "balance": "0x21d10290c520", + "code": "0x", + "nonce": "0xf", + "storage": {} + }, + "0xbb6e024b9cffacb947a71991e386681b1cd1477d": { + "balance": "0x490f0961b56fbd0", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0xce0042b868300000d44a59004da54a005ffdcf9f": { + "balance": "0x0", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex2": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x9346332ef1f737a4360f242665465dccc3d2df22d8e5ee7b5637224ea3735651", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 227496, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000003234a1000000000000000000000000000000000000000000000000000000000000016ff9016c8085174876e8008303c4d88080b90154608060405234801561001057600080fd5b50610134806100206000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634af63f0214602d575b600080fd5b60cf60048036036040811015604157600080fd5b810190602081018135640100000000811115605b57600080fd5b820183602082011115606c57600080fd5b80359060200191846001830284011164010000000083111715608d57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550509135925060eb915050565b604080516001600160a01b039092168252519081900360200190f35b6000818351602085016000f5939250505056fea26469706673582212206b44f8a82cb6b156bfcc3dc6aadd6df4eefd204bc928a4397fd15dacf6d5320564736f6c634300060200331b832470008224700000000000000000000000000000000000" + ], + "gasLimit": [ + "0x38fe3" + ], + "gasPrice": null, + "nonce": "0xf", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sender": "0x551c615f7a63ca915857048133f827147f52f25d", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x00000000000000000000000000000000000000000000000000000000003234a1000000000000000000000000ce0042b868300000d44a59004da54a005ffdcf9f00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_deploy_rex2_small.json b/bench/replay/fixtures/keyless_deploy_rex2_small.json new file mode 100644 index 00000000..10949604 --- /dev/null +++ b/bench/replay/fixtures/keyless_deploy_rex2_small.json @@ -0,0 +1,106 @@ +{ + "replay_0x6e5db13e705447ed3c5d8bb8a875a3429a532e583abbfb1390b0ece26d17081e": { + "_info": null, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x723fe1", + "currentTimestamp": "0x698465b4", + "currentBaseFee": "0xf4240", + "currentRandom": "0x9b36e3e657b982884056edf8f9986ecffc817c82c968b2e9f24af5b2ed4b2053", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x4200000000000000000000000000000000000011": { + "balance": "0x2133e6bc56c4814b", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x4200000000000000000000000000000000000019": { + "balance": "0x309359fe857af340", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001a": { + "balance": "0x3488c5c02b29c53a", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x420000000000000000000000000000000000001b": { + "balance": "0x0", + "code": "0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a", + "nonce": "0x0", + "storage": {} + }, + "0x4c8d290a1b368ac4728d83a9e8321fc3af2b39b1": { + "balance": "0x93b4b29cd75783", + "code": "0x", + "nonce": "0x1", + "storage": {} + }, + "0x551c615f7a63ca915857048133f827147f52f25d": { + "balance": "0x238902b297e7", + "code": "0x", + "nonce": "0x9", + "storage": {} + }, + "0x7a0d94f55792c434d74a40883c6ed8545e406d12": { + "balance": "0x0", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex2": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xae5c7c6848a1150f50a70359ee8f37c7fab68fdb777e54ef49831be20ad72049", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 187588, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 0, + "data": [ + "0x846365d50000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000006580b0000000000000000000000000000000000000000000000000000000000000080f87e8085174876e800830186a08080ad601f80600e600039806000f350fe60003681823780368234f58015156014578182fd5b80825250506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222" + ], + "gasLimit": [ + "0x493e0" + ], + "gasPrice": "0xf4240", + "nonce": "0x9", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sender": "0x551c615f7a63ca915857048133f827147f52f25d", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": null, + "maxPriorityFeePerGas": null, + "initcodes": null, + "accessLists": [ + null + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000006580b0000000000000000000000007a0d94f55792c434d74a40883c6ed8545e406d1200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/manifest.json b/bench/replay/manifest.json index 68699eaa..3f22256f 100644 --- a/bench/replay/manifest.json +++ b/bench/replay/manifest.json @@ -42,6 +42,14 @@ "fixture": "fixtures/attack_deploy.json", "expected_gas": 141927106, "note": "Mainnet attack contract deployment captured as a self-contained prestate fixture (not a mined tx; converted from crates/mega-evm/benches/fixtures/known_attack_deploy.json, #299). 142M gas of CREATE — the heaviest stress on the multi-dimensional AdditionalLimit accounting. Its post was filled offline (`state-test --fill --bench-spec Rex5`), so it is validated and benched like the others." + }, + { + "name": "keyless_deploy", + "category": "system-contract/sandbox", + "type": "fixture", + "fixture": "fixtures/keyless_deploy_rex2_large.json", + "expected_gas": 1112970, + "note": "Real mainnet KeylessDeploy interception with a nested sandbox EVM (tx 0xe2e63e7b…, block 7560053, REX2). The sandbox CREATE burns 25.5M gas; the outer transaction records only the 100K interceptor overhead plus calldata (pre-REX5 accounting)." } ] } diff --git a/bin/mega-evme/tests/fixtures/replay_offline_keyless.cache.json b/bin/mega-evme/tests/fixtures/replay_offline_keyless.cache.json new file mode 100644 index 00000000..d02ffde3 --- /dev/null +++ b/bin/mega-evme/tests/fixtures/replay_offline_keyless.cache.json @@ -0,0 +1,257 @@ +{ + "version": 1, + "chain_id": 4326, + "cache": [ + { + "key": "0x03edd72433ee16462bdd729f53b31d06ddeb716d5c3d7b79144feef039ec4657", + "value": "{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"hash\":\"0xbdea7125ed72d9479f30bd31f485e0b7d1773d5f38f9954ddc9c6b3ab66477a2\",\"parentHash\":\"0x238ecc53f5015f3dc15b5c41020ad4db49524cad04ece5de4f9029aadfa80c2e\",\"sha3Uncles\":\"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\"miner\":\"0x4200000000000000000000000000000000000011\",\"stateRoot\":\"0xb50dddb0296982f80cdfd135363a9561d5ffd2724a0cd9203ae9c55c9b20f011\",\"transactionsRoot\":\"0x84cb84c697e95da5747f006a09af73b5aa97395dbc76be5435a8426e9cc5c160\",\"receiptsRoot\":\"0xb974333412d8bea46f8ae6ecffa18c87b063a5e0f12db698b24cd6a68f587bd9\",\"logsBloom\":\"0x00000400000000000000000000000000000002000001000000200000000000000400020002000280000000001000100000000000000000000000000000400080000000000000000000000000000000000000000000000000000000000003000000000000000800000000000000000000000000000000000000000000004000200000000000000800000000000400000020000000000008000000800000400000000000000000000000010000000000000000000024000200000000020000010000000000000000000000000000200000000000800000000000000000000000200400000200000000000100000000000000000000000440800000000000000000\",\"difficulty\":\"0x0\",\"number\":\"0x735b74\",\"gasLimit\":\"0x2540be400\",\"gasUsed\":\"0x6a701d\",\"timestamp\":\"0x69858147\",\"extraData\":\"0x00000000fa00000001\",\"mixHash\":\"0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6\",\"nonce\":\"0x0000000000000000\",\"baseFeePerGas\":\"0xf4240\",\"withdrawalsRoot\":\"0xf8e41faadb529f9293bc1d0ded07fbb1fd5a9e2f5195e377b56e16b0da77b6b1\",\"blobGasUsed\":\"0x0\",\"excessBlobGas\":\"0x0\",\"parentBeaconBlockRoot\":\"0x1a8cc19fc08e396b212b0ec1427bf20618631781e6890c95a7b405c441bd9524\",\"requestsHash\":\"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"size\":\"0x3509\",\"uncles\":[],\"transactions\":[\"0x2cf5219643c848e76756050aec51d3d1b82832cf88650cd74c857e224c43b491\",\"0xa4a86a835be234ff47bf6df6c68609d3ad3ad77b5d34c5ecca385f37b74f9441\",\"0x699afe82cf8fbedf5f7b0936697ded6bd95398b1d3e1cb01a06ebd0214e5efdd\",\"0xadeb4b05d0fbbaad492f7d33bee57029ad90ba21346f6fb37250666853bad770\",\"0xe52dbc658d50ea36d17350b0e2df6b7e4ca6b06bbabc68f71e3dd3eb8e00fa89\",\"0xbfec15a04fb77a0f4389b627067bc7b24fd84c6f1309606fc719eb6ad6093e59\",\"0x8f023f4dcbac86b9e5c398a23abafac5b93300a8bed73f956ae53aaaf6228956\",\"0x0eb119cfecdfe0ca1774850a773c3947c4def2450b9351498aae7d4020df28a7\",\"0x07e5d896769964b79feb37f78e947c090d10cc9a43a1a00f3c4a5420660ddab7\",\"0x6f990890cf47a033f5261afcbe928be3991c209484c7c2b6df90bfa0e1b3b381\",\"0xf20da957f7d9d2f65998711162f419da58e4c3ea85f2be6dfbdea1b75d24d9ef\",\"0xacb79193de7c661c894b96d4ee7436ce2406437ad34a72da84b06e26213838d4\",\"0xf0b89d55e64df8e9bd8ac990e94da767e2f35e02fce67726eb10cd293bc5226f\",\"0x3211b8dbdd7e6b6d100e121240c6bace768b13c886de7850d67da4f36ecc3bc1\",\"0xa93741f0f3eb7a80f2e69d5973c2eb0cb0266071fd7e86ca703a975edd9a9559\",\"0xec1842381380a6764061ad4ba8d675bf580624fbdaf4bf3128d4c4680201a28f\",\"0xf65ed03d1663fc5554397d7a9fdcbbf307b5ac43909c80fe8cd424229360ebf0\",\"0x1e97995b1f0370fe552432498f341878b23bd3c51f544cfabc53cff13a0f9085\",\"0x2d46571bdc7d9dbc035ae7446b0b86437f7ace78b6d9e4287e6d968f6c0cac14\",\"0x11e23d88905e4f6955f823e1b99f64d6b7d00bda2ebf0b44c85ebc12d889a47a\",\"0x3a32a1d36a659cffdde784e5cb02a6e69a9385fe8ae9306b0763a7cdd7b24663\",\"0xc0cace9b221f7934d2234fbf9328a9c61c3a0074e85628919992d029a05841c7\",\"0x76ce1c4dc0d99a951998ed3552b8ad4d01502ad2a05a32a4f46b81523f9bbb87\",\"0x7aaaa50f9b8920e4991c5f39162de12e52d4ab553c1b24aa73a9abb9265ec610\",\"0x594c3c7345ec05e07e21eb8a27e35a889efc651565e0c4accefa5e92c20d59fd\",\"0x2856b18de6e0c4be768a87923e3b8563bb05441f686adae575205e09a7a10acf\"],\"withdrawals\":[],\"miniBlockCount\":\"0x63\",\"miniBlockOffset\":\"0x2ad677f4\",\"signature\":\"0xba676e33cab9784f888a0bb9247971c71974165923e51288a01978c3b7c0b8b87f8eab188f4d64e66ebf29687f0b6e3b315568d54a988bfa7cc650973de801fe1c\",\"txOffset\":\"0x2a78c533b\"}}" + }, + { + "key": "0x0b202e6551ed40e12acf9353c819881a5527d8fc4893401449e3cb01346c4caf", + "value": "{\"jsonrpc\":\"2.0\",\"id\":29,\"result\":\"0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a\"}" + }, + { + "key": "0x0bacee09a1e746610cd887da36a4e1407cda14e8e97036dfb241074735451a6b", + "value": "{\"jsonrpc\":\"2.0\",\"id\":36,\"result\":\"0x00000000000000000000000000000000000000000000000000000000187d7e79\"}" + }, + { + "key": "0x112b1e940deb330b4baa6fa9dcf55e2c4955bf07f0e981c992e7dae7dcd46eac", + "value": "{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":\"0x10e6\"}" + }, + { + "key": "0x13384db5cf92b973a1e63c31ad717e73e8ef94f9241df4bf0b4649f44007381e", + "value": "{\"jsonrpc\":\"2.0\",\"id\":43,\"result\":\"0x\"}" + }, + { + "key": "0x177b252aabb1f94002153d62e1399176591a808fda8290cfc31068a628e7eea4", + "value": "{\"jsonrpc\":\"2.0\",\"id\":58,\"result\":\"0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a\"}" + }, + { + "key": "0x196820ddd1b4289304c1f2b1c27d44b6af3c3bf614059ffa4558ec1a25ce8e67", + "value": "{\"jsonrpc\":\"2.0\",\"id\":28,\"result\":\"0x0\"}" + }, + { + "key": "0x1d2e3922a1575b1555fde67e9d2d14da0c9e212b1a43c5b53315a224da59f77a", + "value": "{\"jsonrpc\":\"2.0\",\"id\":60,\"result\":\"0x378742917f0992c0\"}" + }, + { + "key": "0x1e86109133169d7d034ba65a5620e3214872e6c5344f455b79c4e81e84c12bef", + "value": "{\"jsonrpc\":\"2.0\",\"id\":55,\"result\":\"0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a\"}" + }, + { + "key": "0x20fcb82a34266793b6c56b59f90475b4788005f6864d353c097797f28aebe1ab", + "value": "{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":\"0x235d36f9970f\"}" + }, + { + "key": "0x24c9853df71c1c958f29bcad1217a546f32b8878057adbccbc4f16b6ab04fcab", + "value": "{\"jsonrpc\":\"2.0\",\"id\":20,\"result\":\"0x0\"}" + }, + { + "key": "0x2e07dc8e1e3617f491fe552eddbc609d71183f79f12faa33f88913e59ba11bbc", + "value": "{\"jsonrpc\":\"2.0\",\"id\":59,\"result\":\"0x0\"}" + }, + { + "key": "0x2eab71a935f7b7c8e0b28df4ec1c416bc86fec30d20340bbe78727caccb4fd3d", + "value": "{\"jsonrpc\":\"2.0\",\"id\":35,\"result\":\"0x000000000000000000000000000000000000000069857f570000000001743ff6\"}" + }, + { + "key": "0x2ed6d224789096136fc1871cf079101db80ab2939c5497a33e1398a6f26da707", + "value": "{\"jsonrpc\":\"2.0\",\"id\":39,\"result\":\"0x000000000000000000000000b98c6b1a805b96707a43e1f1acfa163b68098fa6\"}" + }, + { + "key": "0x2ee8c47dec28461e490dafceca28bb76bc40764d8ec1e2ceb5ac86922f6edb88", + "value": "{\"jsonrpc\":\"2.0\",\"id\":10,\"result\":\"0x0\"}" + }, + { + "key": "0x318a626635155db06f07ea7ee8ab97229f6432bd12e92590d84d1335c70a3194", + "value": "{\"jsonrpc\":\"2.0\",\"id\":40,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}" + }, + { + "key": "0x325efb45ec5dfa3b1c48abf2fc2b02314586253f8003aeb9f15a32247b6339ac", + "value": "{\"jsonrpc\":\"2.0\",\"id\":6,\"result\":\"0x0\"}" + }, + { + "key": "0x33a2c9fa7eba2f3dd74089ce13097f3d78404a9af4d7b8043b749c33d4dd586d", + "value": "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"type\":\"0x2\",\"chainId\":\"0x10e6\",\"nonce\":\"0xa\",\"gas\":\"0x1149ac\",\"maxFeePerGas\":\"0x1e8481\",\"maxPriorityFeePerGas\":\"0x1\",\"to\":\"0x6342000000000000000000000000000000000003\",\"value\":\"0x0\",\"accessList\":[],\"input\":\"0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000018d03b50000000000000000000000000000000000000000000000000000000000000a3bf90a388085174876e800830c35008080b909e5608060405234801561001057600080fd5b506109c5806100206000396000f3fe608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c00291ba01820182018201820182018201820182018201820182018201820182018201820a018201820182018201820182018201820182018201820182018201820182018200000000000\",\"r\":\"0x7583efd03ad52d254eb2ed84db71b2e0b35ef75ccfa54eac7b2a32dc9d7d0f1e\",\"s\":\"0x5c68848f5a0832392cb6df000ae22570a1e9081293203d4d8697d7a28afa7d38\",\"yParity\":\"0x1\",\"v\":\"0x1\",\"hash\":\"0xe2e63e7b5fe6d247737ec0ec32b90bfdc565754144cb065a013829d3d81b0aac\",\"blockHash\":\"0xc30eaf571bd4f27c4123e267b60a0783e37139c0589f125532151964b23b2dcf\",\"blockNumber\":\"0x735b75\",\"transactionIndex\":\"0x1\",\"from\":\"0x551c615f7a63ca915857048133f827147f52f25d\",\"gasPrice\":\"0xf4241\"}}" + }, + { + "key": "0x3b44f9b020437c10c36d3a0a9d811fb5024c4d3b4c85f936be5a6f2824551c96", + "value": "{\"jsonrpc\":\"2.0\",\"id\":61,\"result\":\"0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a\"}" + }, + { + "key": "0x3f1535c55e3e7f271c4ff6b6c5445ff30a74bacd5dedd1972d4ff1cfba36148b", + "value": "{\"jsonrpc\":\"2.0\",\"id\":52,\"result\":\"0x\"}" + }, + { + "key": "0x473b69d4f8c308695686cf05eba828e64927de43ae64fd45b9720fec7d32bb34", + "value": "{\"jsonrpc\":\"2.0\",\"id\":62,\"result\":\"0x0\"}" + }, + { + "key": "0x48080fe168e5785946c22c829d3ff9aa144b8de4c46a5eb9cd2f4c038fec84dc", + "value": "{\"jsonrpc\":\"2.0\",\"id\":54,\"result\":\"0x3bd8dbefab882752\"}" + }, + { + "key": "0x496087bd5011415ae73a6f68f5d24b69c166606c951a50013016eb5870b95f18", + "value": "{\"jsonrpc\":\"2.0\",\"id\":23,\"result\":{\"type\":\"0x7e\",\"sourceHash\":\"0x88a693d1eb18905d59e6e25864a6ef50741a49bae398b7842680cbf2c9cb7c0d\",\"from\":\"0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001\",\"to\":\"0x4200000000000000000000000000000000000015\",\"mint\":\"0x0\",\"value\":\"0x0\",\"gas\":\"0x5f5e100\",\"input\":\"0x098999be000005580000000000000000000000500000000069857f570000000001743ff600000000000000000000000000000000000000000000000000000000187d7e7900000000000000000000000000000000000000000000000002355555480549296e9932d7d79bb4714ac316ae93c8be2b8112d087f1d313c2c459e9650cf0e85c000000000000000000000000b98c6b1a805b96707a43e1f1acfa163b68098fa6000000000000000000000000\",\"hash\":\"0x8eff53269421efd1e90d539fcf1f7b01b0aa0b4a3500ff88e63f17ebddf8b4ae\",\"r\":\"0x0\",\"s\":\"0x0\",\"yParity\":\"0x0\",\"v\":\"0x0\",\"blockHash\":\"0xc30eaf571bd4f27c4123e267b60a0783e37139c0589f125532151964b23b2dcf\",\"blockNumber\":\"0x735b75\",\"transactionIndex\":\"0x0\",\"depositReceiptVersion\":\"0x1\",\"gasPrice\":\"0x0\",\"nonce\":\"0x735b74\"}}" + }, + { + "key": "0x4a9f6696fc7e215f4618c2f4e77f99e697ed2415845b1af50d61e9bd2728a24a", + "value": "{\"jsonrpc\":\"2.0\",\"id\":34,\"result\":\"0x000000000000000000000000000000000000055800000000000000000000004f\"}" + }, + { + "key": "0x4fd414fda3cf66952f40de5c5fd4042a1ec202a0ecceaeec366d58d105480533", + "value": "{\"jsonrpc\":\"2.0\",\"id\":37,\"result\":\"0x0000000000000000000000000000000000000000000000000235555548054929\"}" + }, + { + "key": "0x5abf45c60cc4b3b9e765c887710617c31f61c6b845afe178cc4550789162b65a", + "value": "{\"jsonrpc\":\"2.0\",\"id\":22,\"result\":\"0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c806354fd4d5014610038578063846365d514610080575b5f5ffd5b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516100779190610124565b60405180910390f35b61009361008e36600461013d565b6100a2565b604051610077939291906101af565b5f5f60606040517f1894f07600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f61013660208301846100d8565b9392505050565b5f5f5f6040848603121561014f575f5ffd5b833567ffffffffffffffff811115610165575f5ffd5b8401601f81018613610175575f5ffd5b803567ffffffffffffffff81111561018b575f5ffd5b86602082840101111561019c575f5ffd5b6020918201979096509401359392505050565b67ffffffffffffffff8416815273ffffffffffffffffffffffffffffffffffffffff83166020820152606060408201525f6101ed60608301846100d8565b9594505050505056fea164736f6c634300081e000a\"}" + }, + { + "key": "0x5bd1bf281809317abb0a7f881a283049c1dd53e5b04ba4acfae8a6583ed45f6e", + "value": "{\"jsonrpc\":\"2.0\",\"id\":51,\"result\":\"0x0\"}" + }, + { + "key": "0x6e8237723475e1cca3407eb5336f23abcb1d2eefa39942c3f839447d9c897d1c", + "value": "{\"jsonrpc\":\"2.0\",\"id\":56,\"result\":\"0x0\"}" + }, + { + "key": "0x6eab95efca8f86cafed2287aa318e42bbf592cbf242a2ad386c09e67ac7cc3c5", + "value": "{\"jsonrpc\":\"2.0\",\"id\":4,\"result\":{\"type\":\"0x2\",\"status\":\"0x1\",\"cumulativeGasUsed\":\"0x1288e6\",\"logs\":[],\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"transactionHash\":\"0xe2e63e7b5fe6d247737ec0ec32b90bfdc565754144cb065a013829d3d81b0aac\",\"transactionIndex\":\"0x1\",\"blockHash\":\"0xc30eaf571bd4f27c4123e267b60a0783e37139c0589f125532151964b23b2dcf\",\"blockNumber\":\"0x735b75\",\"gasUsed\":\"0x10fb8a\",\"effectiveGasPrice\":\"0xf4241\",\"from\":\"0x551c615f7a63ca915857048133f827147f52f25d\",\"to\":\"0x6342000000000000000000000000000000000003\",\"contractAddress\":null,\"l1GasPrice\":\"0x187d7e79\",\"l1GasUsed\":\"0x5a15\",\"l1Fee\":\"0x304a1cbda\",\"l1BaseFeeScalar\":\"0x558\",\"l1BlobBaseFee\":\"0x235555548054929\",\"l1BlobBaseFeeScalar\":\"0x0\"}}" + }, + { + "key": "0x72ea21feb21ab3d075a4541ac3248956384dc4b4487987b2fcd8c53aeff72ecf", + "value": "{\"jsonrpc\":\"2.0\",\"id\":64,\"result\":\"0x60806040526004361061005e5760003560e01c80635c60da1b116100435780635c60da1b146100be5780638f283970146100f8578063f851a440146101185761006d565b80633659cfe6146100755780634f1ef286146100955761006d565b3661006d5761006b61012d565b005b61006b61012d565b34801561008157600080fd5b5061006b6100903660046106dd565b610224565b6100a86100a33660046106f8565b610296565b6040516100b5919061077b565b60405180910390f35b3480156100ca57600080fd5b506100d3610419565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b5565b34801561010457600080fd5b5061006b6101133660046106dd565b6104b0565b34801561012457600080fd5b506100d3610517565b60006101577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b905073ffffffffffffffffffffffffffffffffffffffff8116610201576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f50726f78793a20696d706c656d656e746174696f6e206e6f7420696e6974696160448201527f6c697a656400000000000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b3660008037600080366000845af43d6000803e8061021e573d6000fd5b503d6000f35b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061027d575033155b1561028e5761028b816105a3565b50565b61028b61012d565b60606102c07fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614806102f7575033155b1561040a57610305846105a3565b6000808573ffffffffffffffffffffffffffffffffffffffff16858560405161032f9291906107ee565b600060405180830381855af49150503d806000811461036a576040519150601f19603f3d011682016040523d82523d6000602084013e61036f565b606091505b509150915081610401576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527f50726f78793a2064656c656761746563616c6c20746f206e657720696d706c6560448201527f6d656e746174696f6e20636f6e7472616374206661696c65640000000000000060648201526084016101f8565b91506104129050565b61041261012d565b9392505050565b60006104437fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148061047a575033155b156104a557507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5490565b6104ad61012d565b90565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610509575033155b1561028e5761028b8161060c565b60006105417fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480610578575033155b156104a557507fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81815560405173ffffffffffffffffffffffffffffffffffffffff8316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a25050565b60006106367fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61035490565b7fb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d61038381556040805173ffffffffffffffffffffffffffffffffffffffff80851682528616602082015292935090917f7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f910160405180910390a1505050565b803573ffffffffffffffffffffffffffffffffffffffff811681146106d857600080fd5b919050565b6000602082840312156106ef57600080fd5b610412826106b4565b60008060006040848603121561070d57600080fd5b610716846106b4565b9250602084013567ffffffffffffffff8082111561073357600080fd5b818601915086601f83011261074757600080fd5b81358181111561075657600080fd5b87602082850101111561076857600080fd5b6020830194508093505050509250925092565b600060208083528351808285015260005b818110156107a85785810183015185820160400152820161078c565b818111156107ba576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016929092016040019392505050565b818382376000910190815291905056fea164736f6c634300080f000a\"}" + }, + { + "key": "0x74d043a94ea55a2a498b0831cd26543e77025ca26276e42d1412726bac94f928", + "value": "{\"jsonrpc\":\"2.0\",\"id\":47,\"result\":\"0x0\"}" + }, + { + "key": "0x754c66356c503c523ce0d45bc79b34f618c32bcc0a47ac83b3a484975df4cac1", + "value": "{\"jsonrpc\":\"2.0\",\"id\":14,\"result\":\"0x0\"}" + }, + { + "key": "0x7e345d9a1199c8bfd8847c9724189611a2efe48b45715684625f0186172e7063", + "value": "{\"jsonrpc\":\"2.0\",\"id\":11,\"result\":\"0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500\"}" + }, + { + "key": "0x829c12fb4c2cbf6d33061c959c16dc090e875c9d7b598bd59011db1274d5a6fd", + "value": "{\"jsonrpc\":\"2.0\",\"id\":53,\"result\":\"0x0\"}" + }, + { + "key": "0x88156fe42eceb88352beb47014da4c5d4d5796b4358bd36c7d777e8a0cee6054", + "value": "{\"jsonrpc\":\"2.0\",\"id\":12,\"result\":\"0x0000000000000000000000000000000000000000000000000000000069856149\"}" + }, + { + "key": "0x8a32cb46ece3a3d0a47443429b04145fd4db9da42727c795a446ae9a2f2360dc", + "value": "{\"jsonrpc\":\"2.0\",\"id\":31,\"result\":\"0x0\"}" + }, + { + "key": "0x8fd1871d04fb24634c99b4cd7c722f3ca72d64add862517924b7f1d49f2eda95", + "value": "{\"jsonrpc\":\"2.0\",\"id\":27,\"result\":\"0x0\"}" + }, + { + "key": "0x92ccee40c05101d86ee08b23be9ec5061cb16faf7dc9854be92cb23e08e423cd", + "value": "{\"jsonrpc\":\"2.0\",\"id\":26,\"result\":\"0x\"}" + }, + { + "key": "0x9458660e766c4e080b6e1b810d7e426008dc32d8e830626637420748f1e5e2a9", + "value": "{\"jsonrpc\":\"2.0\",\"id\":5,\"result\":\"0x1\"}" + }, + { + "key": "0xa0df29f8e1aa86c88459792f2678bdf9a2ede9d76cf52279df7ac4872c7eba11", + "value": "{\"jsonrpc\":\"2.0\",\"id\":49,\"result\":\"0x\"}" + }, + { + "key": "0xa39074909e3954acf1746e5eeed4c83a0494388d40f7913551184c416a5be494", + "value": "{\"jsonrpc\":\"2.0\",\"id\":48,\"result\":\"0x241bb6d01211c38e\"}" + }, + { + "key": "0xa83dd07240aeaaa247578251b6eb15dff1f0b4aef27e3b69649e0b6e2b116cf1", + "value": "{\"jsonrpc\":\"2.0\",\"id\":15,\"result\":\"0x0\"}" + }, + { + "key": "0xab003cbb638981561685da2aecbbf8bda57c66ffb01bf3e49e5901dc54dd47b0", + "value": "{\"jsonrpc\":\"2.0\",\"id\":32,\"result\":\"0x0\"}" + }, + { + "key": "0xacfc7bbe1693db87508c6739ebb2ed175627b58d44f9d25fd9e5e596f5431c15", + "value": "{\"jsonrpc\":\"2.0\",\"id\":8,\"result\":\"0x1b74bd6c6c2d004072fb2e38df887043beaf2c16b1e8df711685280da191b1ad\"}" + }, + { + "key": "0xb1a9435b555f30d0003e2a71120fc3e97a20f34bea5aa11180de4094612f48d2", + "value": "{\"jsonrpc\":\"2.0\",\"id\":30,\"result\":\"0x000000000000000000000000c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d3c0d30015\"}" + }, + { + "key": "0xb351c8628298a69e99b528ff4bfbafd49a58104f9e498cff16e9276cbe796c23", + "value": "{\"jsonrpc\":\"2.0\",\"id\":50,\"result\":\"0x0\"}" + }, + { + "key": "0xbc039e0a8d3f18a92d46fb9b2c56d829fe02a35de14e731b1457dd780a611b11", + "value": "{\"jsonrpc\":\"2.0\",\"id\":57,\"result\":\"0x34c5c27a29ff462d\"}" + }, + { + "key": "0xc980c9990e61612a39cea3eb5c165919679dc10569f02b2f2ac8a4f1304248cc", + "value": "{\"jsonrpc\":\"2.0\",\"id\":63,\"result\":\"0x0\"}" + }, + { + "key": "0xd037fb3de3cf316e2f8b5247c781c5a25993276ea61dde53ec54720e42ee6231", + "value": "{\"jsonrpc\":\"2.0\",\"id\":24,\"result\":\"0x735b74\"}" + }, + { + "key": "0xd6bdfb559c8d61f32989e566ee7bad735ea0e5b6a554fd1f8208458a132c653d", + "value": "{\"jsonrpc\":\"2.0\",\"id\":38,\"result\":\"0x6e9932d7d79bb4714ac316ae93c8be2b8112d087f1d313c2c459e9650cf0e85c\"}" + }, + { + "key": "0xd99c99ff611fdcfb9ba5b2307ec9aca22006388a4e48e7f22d7e769361ee8a8b", + "value": "{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"hash\":\"0xc30eaf571bd4f27c4123e267b60a0783e37139c0589f125532151964b23b2dcf\",\"parentHash\":\"0xbdea7125ed72d9479f30bd31f485e0b7d1773d5f38f9954ddc9c6b3ab66477a2\",\"sha3Uncles\":\"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\"miner\":\"0x4200000000000000000000000000000000000011\",\"stateRoot\":\"0x6128e06c68480114336ab1ef5fca71675403f0753d0cbc0d811af3c5640f8110\",\"transactionsRoot\":\"0x8add85d7d4332410a3bc434719931438059d8c78ea67fb57118348b2323f27b4\",\"receiptsRoot\":\"0x16723b402a78e59af26e84bd5b3e5cc25dfdb9d6ebe4958c14a88b412314a96c\",\"logsBloom\":\"0x00000000002000000000000004000000000000000000000000000000000000000000020002000080000000000000000000000000000200000000000000400080000000000000000000000000002000000002002001000000000000000000000000000000020000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000481000000000000000000400000010000801000000000000000400000000000000020000000000000020000010100000000000000000000000080000000000000000000000000000000000020040400000000000000000000000000000000000000000440000000000000000000\",\"difficulty\":\"0x0\",\"number\":\"0x735b75\",\"gasLimit\":\"0x2540be400\",\"gasUsed\":\"0x789b07\",\"timestamp\":\"0x69858148\",\"extraData\":\"0x00000000fa00000001\",\"mixHash\":\"0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6\",\"nonce\":\"0x0000000000000000\",\"baseFeePerGas\":\"0xf4240\",\"withdrawalsRoot\":\"0xf8e41faadb529f9293bc1d0ded07fbb1fd5a9e2f5195e377b56e16b0da77b6b1\",\"blobGasUsed\":\"0x0\",\"excessBlobGas\":\"0x0\",\"parentBeaconBlockRoot\":\"0x1a8cc19fc08e396b212b0ec1427bf20618631781e6890c95a7b405c441bd9524\",\"requestsHash\":\"0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"size\":\"0x47ed\",\"uncles\":[],\"transactions\":[\"0x8eff53269421efd1e90d539fcf1f7b01b0aa0b4a3500ff88e63f17ebddf8b4ae\",\"0xe2e63e7b5fe6d247737ec0ec32b90bfdc565754144cb065a013829d3d81b0aac\",\"0xcdae515f7b8af4ee0b2e04400cf74a4336e4fbc5b2719e158370c2415774f575\",\"0xf286cb80cbbe2831962f22da29761c92f75c270a4278f558627ddc41f1b6bdba\",\"0xb4ccae54676c757708cb73c738b2e93baa24c05408502b24e466d07b27559b76\",\"0x1521f2effb2d8522174c560971c78426291f64ec1af46768a0797120337f3ed0\",\"0xf8bba94990cb317bccaab4381aded2311f39828fbe65d7d9491aca85995bb7b9\",\"0xca61c97b19208f32fb4c80be62e2a5e88c98a119ed6a2e527c6d566ef55b8ced\",\"0x29ea87be4bee2f0aa0f670e8aef69cf95a387e2898e29d62b924a4dc81d2cf1a\",\"0xba8d0abf4dd924d097a14960045f968f508172916744fb5448667e7ae8434472\",\"0x6fe4d8840fd59daa60fdc5562b96082c34479ee7bd01b14eee9e546f00bf16e4\",\"0x73c8f9e2c870725b9446539ad27ea51d30d5b1d3e29f03cbed751bcf7d1278b0\",\"0xeb4231fcb092dc95593394276b67f86c21d17ab6d07f0afc6fca20f7ab3e58dc\",\"0xe30bf059748f59bda444ad478b612219f6ffecca27221eb0ffa9da2a013b2d9a\",\"0xa8d70a1e8ac4849e078f6f0812acdedd1fee5a4a0eb96188788b92d707672412\",\"0x05245bd01b0b7cdab981c2fdc32e9112247f1621adf0d0ecc14e66676f1dce5e\",\"0xb8544b34a1cc846b7d7591b2c5ea7c470350e648864f7f4e95706fb52e999bcc\",\"0x45e32342a2066ce090aefbf3231793ce0d01b33efb75d3b55486f62e80ee2164\",\"0x73c7038c6d2441abf91bd0806360d7fa2749a8acc2ff96c3fad090c828c5ef27\",\"0x4d70f30c78481985830eddd8ac59ad7b4dda704c6db9834bb37ba988610808a2\",\"0xbe1f45d0b2dcbfc49a8c03884b4b871057414b03dd8649f7474b455eb48c6f04\",\"0x121409aa8eacd0aefb14b79fe07d852678d5432da52feca3e2b4be87b0b0997b\",\"0xb7945f10742f5b552f849891f945fd8ff2f15fa46fbe74b03da962097872937c\",\"0xefe624c2411142b08c947f3e6f3aac5371b23d1b357c98c05907b41643a6fc7f\",\"0xd24393b793eb6dd5798ae0541f58ba60f7176191b33fca157144f2a690782325\"],\"withdrawals\":[],\"miniBlockCount\":\"0x63\",\"miniBlockOffset\":\"0x2ad67857\",\"signature\":\"0x26c4852812acfb7889a18fe8cf5fb075ce0062ca69cc2cc4f5d3f673601b611644460a84a1952b936ea42234d37cdbba7c96b0b6f99b6a7552172c199274b7711b\",\"txOffset\":\"0x2a78c5355\"}}" + }, + { + "key": "0xe67d16fb31341697fc3853c9a345838581a1521b9d24ca7072cab3310a48ad42", + "value": "{\"jsonrpc\":\"2.0\",\"id\":19,\"result\":\"0x608060405234801561000f575f5ffd5b5060043610610064575f3560e01c806382ab890a1161004d57806382ab890a146100f55780638582b7bc1461010a578063b80777ea1461013f575f5ffd5b806328af7f3c146100685780633e6890fb146100a9575b5f5ffd5b61008f7f000000000000000000000000000000000000000000000000000000000000000881565b60405163ffffffff90911681526020015b60405180910390f35b6100d07f000000000000000000000000634200000000000000000000000000000000000181565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100a0565b6101086101033660046103b6565b610147565b005b6101317f000000000000000000000000000000000000000000000000000000000000000081565b6040519081526020016100a0565b610131610154565b6101515f82610165565b50565b5f5f61015f5f610245565b92915050565b8161016f8161031a565b73ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000063420000000000000000000000000000000000011663fbc0d0356101d5857f00000000000000000000000000000000000000000000000000000000000000006103cd565b60405160e083901b7fffffffff000000000000000000000000000000000000000000000000000000001681526004810191909152602481018590526044015f604051808303815f87803b15801561022a575f5ffd5b505af115801561023c573d5f5f3e3d5ffd5b50505050505050565b5f816102508161031a565b73ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000634200000000000000000000000000000000000116637eba7ba66102b6857f00000000000000000000000000000000000000000000000000000000000000006103cd565b6040518263ffffffff1660e01b81526004016102d491815260200190565b602060405180830381865afa1580156102ef573d5f5f3e3d5ffd5b505050506040513d601f19601f820116820180604052508101906103139190610405565b9392505050565b63ffffffff7f00000000000000000000000000000000000000000000000000000000000000081661034c8260016103cd565b10610151576040517fef94d8420000000000000000000000000000000000000000000000000000000081526004810182905263ffffffff7f000000000000000000000000000000000000000000000000000000000000000816602482015260440160405180910390fd5b5f602082840312156103c6575f5ffd5b5035919050565b8082018082111561015f577f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f60208284031215610415575f5ffd5b505191905056fea2646970667358221220290614c973d24b9016b73a6b3fa98b2485a5c324199ceeb767818495dbe785e864736f6c634300081e0033\"}" + }, + { + "key": "0xeb09511605b905a04932e81f431a8380c5a4b3835bd3417767b9de8a83b494fa", + "value": "{\"jsonrpc\":\"2.0\",\"id\":17,\"result\":\"0x0\"}" + }, + { + "key": "0xefaa64c6fe241063d420cfadfbaf3b9a9ca595281c63751e1c273e8a9fc7464a", + "value": "{\"jsonrpc\":\"2.0\",\"id\":7,\"result\":\"0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500\"}" + }, + { + "key": "0xf2e20cec97c17bdca2a68044d037c3c18153183c7f4bed43805be38ff13d2f46", + "value": "{\"jsonrpc\":\"2.0\",\"id\":16,\"result\":\"0x608060405234801561000f575f5ffd5b50600436106100b9575f3560e01c806366cdf82f116100725780638d4909dc116100585780638d4909dc146101c8578063a21e2d69146101db578063fbc0d035146101fb575f5ffd5b806366cdf82f146101955780637eba7ba6146101a8575f5ffd5b8063138f5ec5116100a2578063138f5ec514610123578063348a0cdc1461013657806354fd4d5014610156575f5ffd5b806301caec13146100bd5780630dc9b5da146100d2575b5f5ffd5b6100d06100cb3660046105db565b61020e565b005b6100f97f000000000000000000000000a887dcb9d5f39ef79272801d05abdf707cfbbd1d81565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020015b60405180910390f35b6100d0610131366004610647565b61028a565b61014961014436600461068f565b6102d4565b60405161011a919061071a565b604080518082018252600581527f312e312e300000000000000000000000000000000000000000000000000000006020820152905161011a919061079b565b6100d06101a33660046107b4565b505050565b6101ba6101b636600461082b565b5490565b60405190815260200161011a565b6100d06101d63660046107b4565b61044b565b6101ee6101e936600461068f565b61045e565b60405161011a9190610842565b6100d0610209366004610884565b6104d4565b828114610256576040517f5b7232fa00000000000000000000000000000000000000000000000000000000815260048101849052602481018290526044015b60405180910390fd5b8382845f5b81811015610278576020810283810135908501355560010161025b565b505050506102846104e3565b50505050565b805f5b818110156102ca576102c2858585848181106102ab576102ab6108a4565b90506020028101906102bd91906108d1565b610554565b60010161028d565b50506101a36104e3565b60608167ffffffffffffffff8111156102ef576102ef610932565b60405190808252806020026020018201604052801561032257816020015b606081526020019060019003908161030d5790505b5090505f5b82811015610444575f8030868685818110610344576103446108a4565b905060200281019061035691906108d1565b60405161036492919061095f565b5f60405180830381855af49150503d805f811461039c576040519150601f19603f3d011682016040523d82523d5f602084013e6103a1565b606091505b50915091508161041c578051156103ba57805181602001fd5b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d756c746963616c6c3a2063616c6c206661696c656400000000000000000000604482015260640161024d565b8084848151811061042f5761042f6108a4565b60209081029190910101525050600101610327565b5092915050565b610456838383610554565b6101a36104e3565b60608167ffffffffffffffff81111561047957610479610932565b6040519080825280602002602001820160405280156104a2578160200160208202803683370190505b5090506020810183835f5b818110156104ca57602081028381013554908501526001016104ad565b5050505092915050565b8082556104df6104e3565b5050565b3373ffffffffffffffffffffffffffffffffffffffff7f000000000000000000000000a887dcb9d5f39ef79272801d05abdf707cfbbd1d1614610552576040517f5e742c5a00000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b565b827fda678492695e6a825d786c2375f7fdf3c1dc012451c61c1804227f499b0fc53e838360405161058692919061096e565b60405180910390a2505050565b5f5f83601f8401126105a3575f5ffd5b50813567ffffffffffffffff8111156105ba575f5ffd5b6020830191508360208260051b85010111156105d4575f5ffd5b9250929050565b5f5f5f5f604085870312156105ee575f5ffd5b843567ffffffffffffffff811115610604575f5ffd5b61061087828801610593565b909550935050602085013567ffffffffffffffff81111561062f575f5ffd5b61063b87828801610593565b95989497509550505050565b5f5f5f60408486031215610659575f5ffd5b83359250602084013567ffffffffffffffff811115610676575f5ffd5b61068286828701610593565b9497909650939450505050565b5f5f602083850312156106a0575f5ffd5b823567ffffffffffffffff8111156106b6575f5ffd5b6106c285828601610593565b90969095509350505050565b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b5f602082016020835280845180835260408501915060408160051b8601019250602086015f5b8281101561078f577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc087860301845261077a8583516106ce565b94506020938401939190910190600101610740565b50929695505050505050565b602081525f6107ad60208301846106ce565b9392505050565b5f5f5f604084860312156107c6575f5ffd5b83359250602084013567ffffffffffffffff8111156107e3575f5ffd5b8401601f810186136107f3575f5ffd5b803567ffffffffffffffff811115610809575f5ffd5b86602082840101111561081a575f5ffd5b939660209190910195509293505050565b5f6020828403121561083b575f5ffd5b5035919050565b602080825282518282018190525f918401906040840190835b8181101561087957835183526020938401939092019160010161085b565b509095945050505050565b5f5f60408385031215610895575f5ffd5b50508035926020909101359150565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b5f5f83357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610904575f5ffd5b83018035915067ffffffffffffffff82111561091e575f5ffd5b6020019150368190038213156105d4575f5ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b818382375f9101908152919050565b60208152816020820152818360408301375f818301604090810191909152601f9092017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe016010191905056fea164736f6c634300081e000a\"}" + }, + { + "key": "0xf330748134c34b0820d9815b5fae45acc26f16028cf53de64a9bf06cda1b2c5b", + "value": "{\"jsonrpc\":\"2.0\",\"id\":33,\"result\":\"0x608060405234801561001057600080fd5b50600436106101825760003560e01c806364ca23ef116100d8578063b80777ea1161008c578063e591b28211610066578063e591b282146103b0578063e81b2c6d146103d2578063f8206140146103db57600080fd5b8063b80777ea14610337578063c598591814610357578063d84447151461037757600080fd5b80638381f58a116100bd5780638381f58a146103115780638b239f73146103255780639e8c49661461032e57600080fd5b806364ca23ef146102e157806368d5dca6146102f557600080fd5b80634397dfef1161013a57806354fd4d501161011457806354fd4d501461025d578063550fcdc91461029f5780635cf24969146102d857600080fd5b80634397dfef146101fc578063440a5e20146102245780634d5d9a2a1461022c57600080fd5b806309bd5a601161016b57806309bd5a60146101a457806316d3bc7f146101c057806321326849146101ed57600080fd5b8063015d8eb914610187578063098999be1461019c575b600080fd5b61019a6101953660046105bc565b6103e4565b005b61019a610523565b6101ad60025481565b6040519081526020015b60405180910390f35b6008546101d49067ffffffffffffffff1681565b60405167ffffffffffffffff90911681526020016101b7565b604051600081526020016101b7565b6040805173eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee815260126020820152016101b7565b61019a61052d565b6008546102489068010000000000000000900463ffffffff1681565b60405163ffffffff90911681526020016101b7565b60408051808201909152600581527f312e362e3000000000000000000000000000000000000000000000000000000060208201525b6040516101b7919061062e565b60408051808201909152600381527f45544800000000000000000000000000000000000000000000000000000000006020820152610292565b6101ad60015481565b6003546101d49067ffffffffffffffff1681565b6003546102489068010000000000000000900463ffffffff1681565b6000546101d49067ffffffffffffffff1681565b6101ad60055481565b6101ad60065481565b6000546101d49068010000000000000000900467ffffffffffffffff1681565b600354610248906c01000000000000000000000000900463ffffffff1681565b60408051808201909152600581527f45746865720000000000000000000000000000000000000000000000000000006020820152610292565b60405173deaddeaddeaddeaddeaddeaddeaddeaddead000181526020016101b7565b6101ad60045481565b6101ad60075481565b3373deaddeaddeaddeaddeaddeaddeaddeaddead00011461048b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603b60248201527f4c31426c6f636b3a206f6e6c7920746865206465706f7369746f72206163636f60448201527f756e742063616e20736574204c3120626c6f636b2076616c7565730000000000606482015260840160405180910390fd5b6000805467ffffffffffffffff98891668010000000000000000027fffffffffffffffffffffffffffffffff00000000000000000000000000000000909116998916999099179890981790975560019490945560029290925560038054919094167fffffffffffffffffffffffffffffffffffffffffffffffff00000000000000009190911617909255600491909155600555600655565b61052b610535565b565b61052b610548565b61053d610548565b60a43560a01c600855565b73deaddeaddeaddeaddeaddeaddeaddeaddead000133811461057257633cc50b456000526004601cfd5b60043560801c60035560143560801c60005560243560015560443560075560643560025560843560045550565b803567ffffffffffffffff811681146105b757600080fd5b919050565b600080600080600080600080610100898b0312156105d957600080fd5b6105e28961059f565b97506105f060208a0161059f565b9650604089013595506060890135945061060c60808a0161059f565b979a969950949793969560a0850135955060c08501359460e001359350915050565b600060208083528351808285015260005b8181101561065b5785810183015185820160400152820161063f565b8181111561066d576000604083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe01692909201604001939250505056fea164736f6c634300080f000a\"}" + }, + { + "key": "0xf4a42ceb4c9a70f548864dfefce97e974c3dc9b43942d57a4ef5b035a1ca930b", + "value": "{\"jsonrpc\":\"2.0\",\"id\":41,\"result\":\"0xa\"}" + }, + { + "key": "0xf6e348fb3153840c101f6a639e2f3d2fe99a44e0120cf9693c1bc3d362582001", + "value": "{\"jsonrpc\":\"2.0\",\"id\":18,\"result\":\"0x0\"}" + }, + { + "key": "0xf77fcd6f968ffd20720cdb40f95b56979c8ac424b89c7df74c7baaaace9c50fb", + "value": "{\"jsonrpc\":\"2.0\",\"id\":21,\"result\":\"0x0\"}" + }, + { + "key": "0xf867ecd7d874e759a000ef24a27ce76bcba352c6e5cf4bd9172dbf1ead99c1c4", + "value": "{\"jsonrpc\":\"2.0\",\"id\":25,\"result\":\"0x0\"}" + }, + { + "key": "0xfadd88528a76dcf8e4add50b2d0a6d307e16ef83e30dc67458ab9ea6dface7fe", + "value": "{\"jsonrpc\":\"2.0\",\"id\":13,\"result\":\"0x7ce2679ec640f4d87f52dc353e5391233353db7c5454faa26d4dbd4ec6d2a5c6\"}" + }, + { + "key": "0xfe22e3eeb9c718186d46d906e1c1cf7dfad8a92572f72f2456226430a966b44a", + "value": "{\"jsonrpc\":\"2.0\",\"id\":9,\"result\":\"0x1\"}" + } + ], + "external_env": { + "bucket_capacities": [] + } +} \ No newline at end of file diff --git a/bin/mega-evme/tests/replay_keyless.rs b/bin/mega-evme/tests/replay_keyless.rs new file mode 100644 index 00000000..49037c5e --- /dev/null +++ b/bin/mega-evme/tests/replay_keyless.rs @@ -0,0 +1,125 @@ +//! Offline replay of a real mainnet `KeylessDeploy` transaction. +//! +//! Uses a committed RPC capture (`fixtures/replay_offline_keyless.cache.json`) +//! so the tests are deterministic: the call tracer nests the sandbox CREATE, +//! the opcode tracer includes the synthetic CREATE plus constructor steps, and +//! `--dump-fixture` matches the committed replay-corpus fixture byte-for-byte. + +use std::process::Command; + +/// Offline RPC capture of mainnet tx `0xe2e63e7b…` (`KeylessDeploy`, REX2). +const CACHE: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/replay_offline_keyless.cache.json"); + +/// The captured transaction. +const TX: &str = "0xe2e63e7b5fe6d247737ec0ec32b90bfdc565754144cb065a013829d3d81b0aac"; + +/// Committed corpus fixture dumped from the same capture. +const CORPUS_LARGE: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../bench/replay/fixtures/keyless_deploy_rex2_large.json" +); + +const KEYLESS_DEPLOY: &str = "0x6342000000000000000000000000000000000003"; +const DEPLOYED: &str = "0x1820a4b7618bde71dce8cdc73aab6c95905fad24"; + +fn mega_evme() -> Command { + Command::new(env!("CARGO_BIN_EXE_mega-evme")) +} + +fn replay_offline(args: &[&str]) -> std::process::Output { + let output = mega_evme() + .args(["replay", "--rpc.replay-file", CACHE]) + .args(args) + .arg(TX) + .output() + .expect("failed to run mega-evme"); + assert!( + output.status.success(), + "replay failed.\nstderr: {}\nstdout: {}", + String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stdout) + ); + output +} + +fn replay_json(args: &[&str]) -> serde_json::Value { + let output = replay_offline(args); + serde_json::from_slice(&output.stdout).unwrap_or_else(|e| { + panic!( + "failed to parse replay JSON: {e}\nstdout: {}", + String::from_utf8_lossy(&output.stdout) + ) + }) +} + +/// Call tracer must hang the sandbox CREATE under the intercepted `KeylessDeploy` CALL. +#[test] +fn test_replay_keyless_call_tracer_nests_sandbox_create() { + let summary = replay_json(&["--trace", "--tracer", "call", "--json"]); + + assert_eq!(summary["success"].as_bool(), Some(true)); + assert_eq!(summary["gas_used"].as_u64(), Some(1_112_970)); + + let root = &summary["trace"]; + assert_eq!(root["type"].as_str(), Some("CALL")); + assert_eq!(root["to"].as_str(), Some(KEYLESS_DEPLOY)); + assert_eq!(root["gasUsed"].as_str(), Some("0x10fb8a")); + + let create = &root["calls"][0]; + assert_eq!(create["type"].as_str(), Some("CREATE")); + assert_eq!(create["to"].as_str(), Some(DEPLOYED)); + assert_eq!(create["gasUsed"].as_str(), Some("0x1854351")); + assert!(create.get("error").is_none_or(serde_json::Value::is_null)); +} + +/// Opcode tracer must emit the synthetic CREATE, then sandbox constructor steps +/// at intercept-frame depth + 1, including the constructor RETURN. +#[test] +fn test_replay_keyless_opcode_tracer_includes_sandbox_steps() { + let summary = replay_json(&[ + "--trace", + "--tracer", + "opcode", + "--trace.opcode.disable-memory", + "--trace.opcode.disable-stack", + "--trace.opcode.disable-storage", + "--json", + ]); + + let logs = summary["trace"]["structLogs"].as_array().expect("structLogs array"); + assert_eq!(logs.len(), 18, "intercepted CREATE plus 17 constructor opcodes"); + + // Synthetic CREATE grafted onto the intercepted CALL (depth 1, geth convention). + assert_eq!(logs[0]["op"].as_str(), Some("CREATE")); + assert_eq!(logs[0]["depth"].as_u64(), Some(1)); + assert_eq!(logs[0]["gas"].as_u64(), Some(636_184)); + assert_eq!(logs[0]["gasCost"].as_u64(), Some(0)); + + // Sandbox constructor starts on the next step at intercept-frame depth + 1. + assert_eq!(logs[1]["op"].as_str(), Some("PUSH1")); + assert_eq!(logs[1]["depth"].as_u64(), Some(2)); + assert_eq!(logs[1]["gas"].as_u64(), Some(25_510_737)); + + // Constructor RETURN at the same sandbox depth. + assert_eq!(logs[17]["op"].as_str(), Some("RETURN")); + assert_eq!(logs[17]["depth"].as_u64(), Some(2)); +} + +/// Offline `--dump-fixture` of this capture must match the committed corpus fixture. +#[test] +fn test_replay_keyless_dump_matches_committed_corpus_fixture() { + let out = + std::env::temp_dir().join(format!("mega_evme_keyless_dump_{}.json", std::process::id())); + let _ = std::fs::remove_file(&out); + + replay_offline(&["--dump-fixture", out.to_str().expect("utf-8 temp path")]); + + let dumped = std::fs::read(&out).expect("read dumped fixture"); + let _ = std::fs::remove_file(&out); + let committed = std::fs::read(CORPUS_LARGE).expect("read committed corpus fixture"); + assert_eq!( + dumped, committed, + "offline dump must match the committed corpus fixture byte-for-byte" + ); +} From a788b95b4f40a2cc9636b42127f95aa063eff304 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:17:46 +0800 Subject: [PATCH 16/28] feat: share sandbox trace splicing and expose KeylessSandboxHooks Move the two-inspector splice pattern out of mega-evme into mega_evm::sandbox::trace behind the new inspectors feature so node tracing RPCs can reuse it, and add the KeylessSandboxHooks trait so hosts held behind a generic EVM projection can attach a sandbox hook through a bound. --- bin/mega-evme/Cargo.toml | 2 +- bin/mega-evme/src/common/trace.rs | 261 +++------------------------ bin/mega-evme/src/replay/cmd.rs | 8 +- crates/mega-evm/Cargo.toml | 3 + crates/mega-evm/src/evm/AGENTS.md | 2 + crates/mega-evm/src/sandbox/hooks.rs | 197 ++++++++++++++++++++ crates/mega-evm/src/sandbox/mod.rs | 6 + crates/mega-evm/src/sandbox/trace.rs | 230 +++++++++++++++++++++++ 8 files changed, 473 insertions(+), 236 deletions(-) create mode 100644 crates/mega-evm/src/sandbox/hooks.rs create mode 100644 crates/mega-evm/src/sandbox/trace.rs diff --git a/bin/mega-evme/Cargo.toml b/bin/mega-evme/Cargo.toml index ea3a8a71..62b420d4 100644 --- a/bin/mega-evme/Cargo.toml +++ b/bin/mega-evme/Cargo.toml @@ -19,7 +19,7 @@ path = "src/main.rs" [dependencies] # megaeth -mega-evm = { workspace = true, features = ["default", "test-utils"] } +mega-evm = { workspace = true, features = ["default", "inspectors", "test-utils"] } mega-state-test.workspace = true # revm diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 0e0bf211..18de3db6 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -1,238 +1,30 @@ //! Trace configuration for mega-evme -use std::{ - cell::{Ref, RefCell}, - path::PathBuf, - rc::Rc, -}; +use std::path::PathBuf; -use alloy_primitives::{Address, Bytes, Log, U256}; +use alloy_primitives::Bytes; use alloy_rpc_types_trace::geth::{ CallConfig, CallFrame, GethDefaultTracingOptions, PreStateConfig, }; use clap::{Parser, ValueEnum}; use mega_evm::{ revm::{ - bytecode::opcode::{self, OpCode}, context::{ result::{ExecutionResult, ResultAndState}, ContextTr, }, database::DatabaseRef, - inspector::JournalExt, - interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}, state::EvmState, - ExecuteEvm, InspectEvm, Inspector, + ExecuteEvm, InspectEvm, }, - sandbox::InspectorSandboxObserver, - MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, -}; -use revm_inspectors::tracing::{ - types::{CallTraceStep, TraceMemberOrder}, - TracingInspector, TracingInspectorConfig, + sandbox::trace::{splice_sandbox_traces, SharedTracingInspector}, + MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, }; +use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; use tracing::{debug, info, trace}; use super::{EvmeError, EvmeExternalEnvs, EvmeState}; -/// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. -/// -/// Nested keyless-deploy sandbox execution is a separate EVM, so a parent inspector -/// never sees those frames. This wrapper is the outer inspector and, cloned, the -/// inner inspector of [`InspectorSandboxObserver`]. -/// -/// Outer and sandbox use *two* of these handles. The sandbox journal reports depth 0 -/// for its top-level CREATE; recording that into the outer inspector would overwrite -/// the CALL root (`CallTraceArena` treats depth 0 as the entry). After the transaction -/// returns, [`splice_sandbox_traces`] grafts the sandbox arena under the `KeylessDeploy` -/// CALL. -#[derive(Clone, Debug)] -pub(crate) struct RcTracingInspector(Rc>); - -impl RcTracingInspector { - /// Wraps an existing [`TracingInspector`]. - pub(crate) fn new(inspector: TracingInspector) -> Self { - Self(Rc::new(RefCell::new(inspector))) - } - - /// Resets recorded traces so a subsequent transaction starts from a clean arena. - pub(crate) fn fuse(&self) { - self.0.borrow_mut().fuse(); - } - - /// Borrows the inner inspector. - pub(crate) fn borrow(&self) -> Ref<'_, TracingInspector> { - self.0.borrow() - } - - /// Observer handle for [`MegaEvm::set_keyless_sandbox_observer`]. - pub(crate) fn as_sandbox_observer(&self) -> Rc>> { - Rc::new(RefCell::new(InspectorSandboxObserver(self.clone()))) - } -} - -impl Inspector for RcTracingInspector -where - CTX: ContextTr, -{ - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.borrow_mut().step(interp, context); - } - - fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.borrow_mut().step_end(interp, context); - } - - fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { - self.0.borrow_mut().log(interp, context, log); - } - - fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { - self.0.borrow_mut().call(context, inputs) - } - - fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.0.borrow_mut().call_end(context, inputs, outcome); - } - - fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { - self.0.borrow_mut().create(context, inputs) - } - - fn create_end( - &mut self, - context: &mut CTX, - inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - self.0.borrow_mut().create_end(context, inputs, outcome); - } - - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - >::selfdestruct( - &mut self.0.borrow_mut(), - contract, - target, - value, - ); - } -} - -/// Returns true when `sandbox` recorded at least one completed frame. -fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { - sandbox.traces().nodes().iter().any(|node| { - node.trace.status.is_some() || !node.trace.steps.is_empty() || !node.children.is_empty() - }) -} - -/// Attaches `sandbox` traces as children of the outer [`KEYLESS_DEPLOY_ADDRESS`] CALL. -/// -/// Sandbox execution uses a fresh journal whose top-level CREATE is depth 0. Recording -/// that into the outer inspector would replace the CALL root, so the sandbox is traced -/// on a sibling inspector and grafted here. Depths and arena indices are rewritten so -/// the Geth call tree hangs the CREATE under the CALL. -/// -/// `geth_traces` only descends into a child when the parent frame has a matching -/// call-like opcode in `steps`. Intercepted `KeylessDeploy` records no bytecode, so -/// a CREATE step is grafted onto the parent; without it, sandbox constructor -/// steps are omitted from struct-log output rather than nested in execution order. -fn splice_sandbox_traces(outer: &RcTracingInspector, sandbox: &RcTracingInspector) { - let sandbox_inspector = sandbox.borrow(); - if !sandbox_has_frames(&sandbox_inspector) { - return; - } - - let mut outer_inspector = outer.0.borrow_mut(); - let parent_idx = outer_inspector - .traces() - .nodes() - .iter() - .rposition(|node| { - node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() - }) - .unwrap_or(0); - let depth_offset = outer_inspector.traces().nodes()[parent_idx].trace.depth + 1; - let base = outer_inspector.traces().nodes().len(); - - let remapped: Vec<_> = sandbox_inspector - .traces() - .nodes() - .iter() - .cloned() - .enumerate() - .map(|(old_idx, mut node)| { - node.idx = base + old_idx; - node.trace.depth += depth_offset; - for step in &mut node.trace.steps { - step.depth += depth_offset as u64; - } - node.parent = Some(match node.parent { - None => parent_idx, - Some(parent) => base + parent, - }); - for child in &mut node.children { - *child += base; - } - node - }) - .collect(); - drop(sandbox_inspector); - - outer_inspector.traces_mut().nodes_mut().extend(remapped); - let parent = &mut outer_inspector.traces_mut().nodes_mut()[parent_idx]; - let child_slot = parent.children.len(); - parent.children.push(base); - - // geth_traces pairs call-like parent steps with `children` in order. A - // missing CREATE step here would leave the grafted child unreachable. - let calllike = parent.trace.steps.iter().filter(|s| is_calllike_opcode(s.op.get())).count(); - if calllike < parent.children.len() { - let step_idx = parent.trace.steps.len(); - let create_depth = parent.trace.depth as u64 + 1; - let contract = parent.trace.address; - let gas_remaining = parent.trace.gas_limit; - parent.trace.steps.push(intercepted_create_step(create_depth, contract, gas_remaining)); - parent.ordering.push(TraceMemberOrder::Step(step_idx)); - } - parent.ordering.push(TraceMemberOrder::Call(child_slot)); -} - -/// CREATE/CALL-family opcodes that `geth_traces` uses to walk into children. -fn is_calllike_opcode(op: u8) -> bool { - matches!( - op, - opcode::CALL | - opcode::DELEGATECALL | - opcode::STATICCALL | - opcode::CREATE | - opcode::CALLCODE | - opcode::CREATE2 - ) -} - -/// Placeholder CREATE recorded on an intercepted `KeylessDeploy` CALL so struct-log -/// output can descend into the spliced sandbox frame. -fn intercepted_create_step(depth: u64, contract: Address, gas_remaining: u64) -> CallTraceStep { - CallTraceStep { - depth, - pc: 0, - op: OpCode::new(opcode::CREATE).expect("CREATE is a defined opcode"), - contract, - stack: None, - push_stack: None, - memory: None, - returndata: Bytes::new(), - gas_remaining, - gas_refund_counter: 0, - gas_used: 0, - gas_cost: 0, - storage_change: None, - status: None, - immediate_bytes: None, - decoded: None, - } -} - /// Tracer type for execution analysis #[derive(Debug, Clone, Copy, ValueEnum, Default)] #[non_exhaustive] @@ -424,12 +216,12 @@ impl TraceArgs { /// Splices sandbox frames under the outer `KeylessDeploy` CALL and renders the trace. pub(crate) fn generate_trace_with_sandbox( &self, - outer: &RcTracingInspector, - sandbox: &RcTracingInspector, + outer: &SharedTracingInspector, + sandbox: &SharedTracingInspector, result_and_state: &ResultAndState, prestate: impl DatabaseRef, ) -> String { - splice_sandbox_traces(outer, sandbox); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); self.generate_trace(&outer.borrow(), result_and_state, prestate) } @@ -445,8 +237,8 @@ impl TraceArgs { { if self.is_tracing_enabled() { info!(tracer = ?self.tracer, "Evm executing with tracing"); - let outer = RcTracingInspector::new(self.create_inspector()); - let sandbox = RcTracingInspector::new(self.create_inspector()); + let outer = SharedTracingInspector::new(self.create_inspector()); + let sandbox = SharedTracingInspector::new(self.create_inspector()); let mut evm = MegaEvm::new(evm_context).with_inspector(outer.clone()); evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); @@ -478,7 +270,7 @@ impl TraceArgs { mod tests { use super::*; use alloy_consensus::{transaction::Recovered, Signed, TxLegacy}; - use alloy_primitives::{address, hex, Signature, TxKind, B256}; + use alloy_primitives::{address, hex, Address, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall; use mega_evm::{ alloy_evm::EvmEnv, @@ -491,6 +283,7 @@ mod tests { test_utils::{BytecodeBuilder, MemoryDatabase}, BlockLimits, EmptyExternalEnv, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, + KEYLESS_DEPLOY_ADDRESS, }; const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); @@ -576,8 +369,8 @@ mod tests { } fn run_traced_keyless( - outer: RcTracingInspector, - sandbox: RcTracingInspector, + outer: SharedTracingInspector, + sandbox: SharedTracingInspector, ) -> (ResultAndState, MemoryDatabase, Address) { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); @@ -594,10 +387,12 @@ mod tests { #[test] fn test_keyless_call_trace_nests_sandbox_create() { - let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let outer = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); let (result_and_state, _db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); - splice_sandbox_traces(&outer, &sandbox); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); @@ -625,10 +420,12 @@ mod tests { #[test] fn test_keyless_opcode_trace_includes_sandbox_steps_in_execution_order() { - let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let outer = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); let (result_and_state, db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); - splice_sandbox_traces(&outer, &sandbox); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); let outer_ref = outer.borrow(); let geth_trace = outer_ref.geth_builder().geth_traces( @@ -699,8 +496,10 @@ mod tests { BlockLimits::no_limits(), ); - let outer = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = RcTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let outer = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); let mut executor = factory.create_executor_with_inspector(&mut state, block_ctx, evm_env, outer.clone()); // Skip `apply_pre_execution_changes`: it needs a configured SequencerRegistry on @@ -732,7 +531,7 @@ mod tests { let outcome = executor.run_transaction(&recovered).expect("run keyless deploy"); assert!(outcome.inner.result.is_success(), "{:?}", outcome.inner.result); - splice_sandbox_traces(&outer, &sandbox); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS); diff --git a/bin/mega-evme/src/replay/cmd.rs b/bin/mega-evme/src/replay/cmd.rs index f481a009..ab27f7f4 100644 --- a/bin/mega-evme/src/replay/cmd.rs +++ b/bin/mega-evme/src/replay/cmd.rs @@ -14,6 +14,7 @@ use mega_evm::{ primitives::eip4844, DatabaseRef, }, + sandbox::trace::SharedTracingInspector, BlockLimits, EvmTxRuntimeLimits, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforks, MegaSpecId, }; @@ -26,8 +27,7 @@ use crate::{ common::{ op_receipt_to_tx_receipt, parse_bucket_capacity, print_execution_summary, print_execution_trace, print_receipt, BuildProviderOutput, EvmeExternalEnvs, EvmeOutcome, - ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RcTracingInspector, RpcCacheStore, - TxOverrideArgs, + ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RpcCacheStore, TxOverrideArgs, }, replay::get_hardfork_config, run, ChainArgs, EvmeState, @@ -471,11 +471,11 @@ impl Cmd { ); let start = Instant::now(); - let outer = RcTracingInspector::new(self.trace_args.create_inspector()); + let outer = SharedTracingInspector::new(self.trace_args.create_inspector()); let sandbox = self .trace_args .is_tracing_enabled() - .then(|| RcTracingInspector::new(self.trace_args.create_inspector())); + .then(|| SharedTracingInspector::new(self.trace_args.create_inspector())); let mut state = StateBuilder::new().with_database(&mut database).with_bundle_update().build(); let mut block_executor = block_executor_factory.create_executor_with_inspector( diff --git a/crates/mega-evm/Cargo.toml b/crates/mega-evm/Cargo.toml index dccf6eaf..720badf2 100644 --- a/crates/mega-evm/Cargo.toml +++ b/crates/mega-evm/Cargo.toml @@ -28,6 +28,7 @@ op-revm = { workspace = true, features = ["dev", "serde", "kzg-rs"] } # revm revm = { workspace = true, features = ["dev", "serde", "kzg-rs"] } +revm-inspectors = { workspace = true, features = ["std"], optional = true } # megaeth mega-system-contracts = { workspace = true, default-features = false } @@ -61,6 +62,8 @@ sha2.workspace = true [features] default = ["std", "revm/default", "op-revm/default"] std = ["revm/std", "op-revm/std", "revm/alloydb", "mega-system-contracts/std"] +# Shared `revm-inspectors` helpers for tracing nested sandbox execution (`sandbox::trace`). +inspectors = ["std", "dep:revm-inspectors"] test-utils = [] [[bench]] diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 2bcb3f23..228f1b84 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -26,6 +26,8 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, Two exclusive channels into nested keyless-deploy sandbox execution, attached on `MegaContext` (and forwarded from `MegaEvm` / `MegaBlockExecutor`). Read-only default: `SandboxObserver` cannot short-circuit `CALL`/`CREATE` and must not mutate interpreter or context state. Rewriting explicit: `SandboxInspector` forwards `&mut` inputs and override return values so interventions take effect inside the sandbox as they would on a top-level EVM. +`sandbox::KeylessSandboxHooks` restates the setters as a trait so a host held behind a generic EVM projection (a node's `ConfigureEvm::Evm`) can attach a hook through a bound. +With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` handle for the outer EVM plus a second one on the observer channel, and `splice_sandbox_traces` to graft the sandbox call tree under the intercepted `KeylessDeploy` CALL after execution; `mega-evme` and node tracing RPCs use the same implementation. Contract: 1. With no hook attached, the sandbox path is unchanged. diff --git a/crates/mega-evm/src/sandbox/hooks.rs b/crates/mega-evm/src/sandbox/hooks.rs new file mode 100644 index 00000000..97a8a127 --- /dev/null +++ b/crates/mega-evm/src/sandbox/hooks.rs @@ -0,0 +1,197 @@ +//! Trait view of the keyless sandbox hook setters. +//! +//! [`crate::MegaContext`], [`crate::MegaEvm`], and [`crate::MegaBlockExecutor`] each expose the +//! same setters as inherent methods. Callers that only hold the EVM behind a generic (for +//! example a node's `ConfigureEvm::Evm` projection) cannot name those inherent methods, so +//! [`KeylessSandboxHooks`] restates them as a trait bound. + +#[cfg(not(feature = "std"))] +use alloc as std; +use core::cell::RefCell; +use std::rc::Rc; + +use alloy_evm::Database; +use alloy_op_evm::block::receipt_builder::OpReceiptBuilder; + +use super::{SandboxInspector, SandboxObserver}; +use crate::{EmptyExternalEnv, ExternalEnvTypes, MegaBlockExecutor, MegaContext, MegaEvm}; + +/// Attaches or detaches the keyless sandbox hook on a host that runs sandboxes. +/// +/// Both channels share one slot: attaching either replaces the other. See +/// [`SandboxObserver`] for the read-only channel and [`SandboxInspector`] for the rewriting +/// channel. +pub trait KeylessSandboxHooks { + /// External environment types the host's parent context is typed against. + type ExtEnvs: ExternalEnvTypes; + + /// Attaches a read-only observer on every spec. + fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static; + + /// Attaches a read-only observer that only implements the parent env type. + /// + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end` through this handle. + fn set_keyless_sandbox_observer_for_parent_env( + &mut self, + observer: Option>>>, + ); + + /// Attaches a rewriting inspector on every spec. + fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static; + + /// Attaches a rewriting inspector that only implements the parent env type. + /// + /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end` through this handle. + fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ); + + /// Detaches any sandbox hook from both env-type slots. + fn clear_keyless_sandbox_hook(&mut self); +} + +impl KeylessSandboxHooks for MegaContext { + type ExtEnvs = ExtEnvs; + + fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static, + { + MegaContext::set_keyless_sandbox_observer(self, observer); + } + + fn set_keyless_sandbox_observer_for_parent_env( + &mut self, + observer: Option>>>, + ) { + MegaContext::set_keyless_sandbox_observer_for_parent_env(self, observer); + } + + fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static, + { + MegaContext::set_keyless_sandbox_inspector(self, inspector); + } + + fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + MegaContext::set_keyless_sandbox_inspector_for_parent_env(self, inspector); + } + + fn clear_keyless_sandbox_hook(&mut self) { + MegaContext::clear_keyless_sandbox_hook(self); + } +} + +impl KeylessSandboxHooks + for MegaEvm +{ + type ExtEnvs = ExtEnvs; + + fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static, + { + MegaEvm::set_keyless_sandbox_observer(self, observer); + } + + fn set_keyless_sandbox_observer_for_parent_env( + &mut self, + observer: Option>>>, + ) { + MegaEvm::set_keyless_sandbox_observer_for_parent_env(self, observer); + } + + fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static, + { + MegaEvm::set_keyless_sandbox_inspector(self, inspector); + } + + fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + MegaEvm::set_keyless_sandbox_inspector_for_parent_env(self, inspector); + } + + fn clear_keyless_sandbox_hook(&mut self) { + MegaEvm::clear_keyless_sandbox_hook(self); + } +} + +impl KeylessSandboxHooks + for MegaBlockExecutor +{ + type ExtEnvs = E::ExtEnvs; + + fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + where + O: SandboxObserver + SandboxObserver + 'static, + { + self.evm.set_keyless_sandbox_observer(observer); + } + + fn set_keyless_sandbox_observer_for_parent_env( + &mut self, + observer: Option>>>, + ) { + self.evm.set_keyless_sandbox_observer_for_parent_env(observer); + } + + fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + where + I: SandboxInspector + SandboxInspector + 'static, + { + self.evm.set_keyless_sandbox_inspector(inspector); + } + + fn set_keyless_sandbox_inspector_for_parent_env( + &mut self, + inspector: Option>>>, + ) { + self.evm.set_keyless_sandbox_inspector_for_parent_env(inspector); + } + + fn clear_keyless_sandbox_hook(&mut self) { + self.evm.clear_keyless_sandbox_hook(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::MemoryDatabase, MegaSpecId}; + + struct NopObserver; + + impl SandboxObserver for NopObserver {} + + fn attach_through_trait(host: &mut T) { + host.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + host.clear_keyless_sandbox_hook(); + } + + #[test] + fn test_hooks_trait_reaches_context_and_evm() { + let mut db = MemoryDatabase::default(); + let mut ctx = MegaContext::new(&mut db, MegaSpecId::REX6); + attach_through_trait(&mut ctx); + + let mut evm = MegaEvm::new(ctx); + attach_through_trait(&mut evm); + evm.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + assert!(evm.ctx.keyless_sandbox_hook.is_some()); + KeylessSandboxHooks::clear_keyless_sandbox_hook(&mut evm); + assert!(evm.ctx.keyless_sandbox_hook.is_none()); + } +} diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 9806897b..1798d31d 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -61,6 +61,8 @@ //! [`execute_keyless_deploy_call`] //! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution //! - `inspector` - Rewriting [`SandboxInspector`] channel into nested sandbox execution +//! - `hooks` - [`KeylessSandboxHooks`], the trait view of the hook setters for generic hosts +//! - `trace` - Shared `revm-inspectors` splicing helpers (`inspectors` feature) //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal //! - `tx` - Transaction decoding and validation for pre-EIP-155 transactions @@ -129,14 +131,18 @@ mod error; mod execution; +mod hooks; mod inspector; mod observer; mod state; mod state_merge; +#[cfg(feature = "inspectors")] +pub mod trace; mod tx; pub use error::*; pub use execution::*; +pub use hooks::*; pub use inspector::*; pub use observer::*; pub use state::*; diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs new file mode 100644 index 00000000..937ef19d --- /dev/null +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -0,0 +1,230 @@ +//! Shared helpers for tracing nested sandbox execution with `revm-inspectors`. +//! +//! A [`TracingInspector`] attached to the outer EVM never sees sandbox frames: the sandbox +//! is a separate EVM whose journal reports its top-level CREATE at depth 0, and recording that +//! into the outer arena would overwrite the CALL root. The pattern here is two inspectors — +//! the outer one installed on the EVM and a second one attached through the keyless sandbox +//! observer channel — followed by [`splice_sandbox_traces`], which grafts the sandbox arena +//! under the intercepted `KeylessDeploy` CALL after the transaction returns. +//! +//! Requires the `inspectors` feature. + +#[cfg(not(feature = "std"))] +use alloc as std; +use core::cell::{Ref, RefCell, RefMut}; +use std::{rc::Rc, vec::Vec}; + +use alloy_primitives::{Address, Bytes, Log, U256}; +use revm::{ + bytecode::opcode::{self, OpCode}, + context::ContextTr, + inspector::JournalExt, + interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}, + Inspector, +}; +use revm_inspectors::tracing::{ + types::{CallTraceStep, TraceMemberOrder}, + TracingInspector, +}; + +use super::InspectorSandboxObserver; +use crate::KEYLESS_DEPLOY_ADDRESS; + +/// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. +/// +/// One handle serves as the outer inspector; a second, independent handle is attached to the +/// sandbox through [`Self::as_sandbox_observer`]. Both are `Clone`, so a caller can keep a +/// handle to read the recorded traces after execution. +#[derive(Clone, Debug)] +pub struct SharedTracingInspector(Rc>); + +impl SharedTracingInspector { + /// Wraps an existing [`TracingInspector`]. + pub fn new(inspector: TracingInspector) -> Self { + Self(Rc::new(RefCell::new(inspector))) + } + + /// Resets recorded traces so a subsequent transaction starts from a clean arena. + pub fn fuse(&self) { + self.0.borrow_mut().fuse(); + } + + /// Borrows the inner inspector. + pub fn borrow(&self) -> Ref<'_, TracingInspector> { + self.0.borrow() + } + + /// Mutably borrows the inner inspector. + pub fn borrow_mut(&self) -> RefMut<'_, TracingInspector> { + self.0.borrow_mut() + } + + /// Observer handle for the read-only keyless sandbox channel. + pub fn as_sandbox_observer(&self) -> Rc>> { + Rc::new(RefCell::new(InspectorSandboxObserver(self.clone()))) + } +} + +impl Inspector for SharedTracingInspector +where + CTX: ContextTr, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.borrow_mut().initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.borrow_mut().step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.borrow_mut().step_end(interp, context); + } + + fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.borrow_mut().log(interp, context, log); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.borrow_mut().call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.borrow_mut().call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.borrow_mut().create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.borrow_mut().create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + >::selfdestruct( + &mut self.0.borrow_mut(), + contract, + target, + value, + ); + } +} + +/// Returns true when `sandbox` recorded at least one completed frame. +pub fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { + sandbox.traces().nodes().iter().any(|node| { + node.trace.status.is_some() || !node.trace.steps.is_empty() || !node.children.is_empty() + }) +} + +/// Attaches `sandbox` traces as children of the outer [`KEYLESS_DEPLOY_ADDRESS`] CALL. +/// +/// Sandbox execution uses a fresh journal whose top-level CREATE is depth 0. Recording that +/// into the outer inspector would replace the CALL root, so the sandbox is traced on a +/// sibling inspector and grafted here. Depths and arena indices are rewritten so the Geth +/// call tree hangs the CREATE under the CALL. When the sandbox recorded nothing, `outer` is +/// left untouched. +/// +/// Geth-style struct logs only descend into a child when the parent frame has a matching +/// call-like opcode in its steps. An intercepted `KeylessDeploy` CALL records no bytecode, so +/// a CREATE step is grafted onto the parent; without it the sandbox constructor steps would +/// be omitted from struct-log output instead of nested in execution order. +pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &TracingInspector) { + if !sandbox_has_frames(sandbox) { + return; + } + + let parent_idx = outer + .traces() + .nodes() + .iter() + .rposition(|node| { + node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() + }) + .unwrap_or(0); + let depth_offset = outer.traces().nodes()[parent_idx].trace.depth + 1; + let base = outer.traces().nodes().len(); + + let remapped: Vec<_> = sandbox + .traces() + .nodes() + .iter() + .cloned() + .enumerate() + .map(|(old_idx, mut node)| { + node.idx = base + old_idx; + node.trace.depth += depth_offset; + for step in &mut node.trace.steps { + step.depth += depth_offset as u64; + } + node.parent = Some(match node.parent { + None => parent_idx, + Some(parent) => base + parent, + }); + for child in &mut node.children { + *child += base; + } + node + }) + .collect(); + + outer.traces_mut().nodes_mut().extend(remapped); + let parent = &mut outer.traces_mut().nodes_mut()[parent_idx]; + let child_slot = parent.children.len(); + parent.children.push(base); + + // Struct-log rendering pairs call-like parent steps with `children` in order. A missing + // CREATE step here would leave the grafted child unreachable. + let calllike = parent.trace.steps.iter().filter(|s| is_calllike_opcode(s.op.get())).count(); + if calllike < parent.children.len() { + let step_idx = parent.trace.steps.len(); + let create_depth = parent.trace.depth as u64 + 1; + let contract = parent.trace.address; + let gas_remaining = parent.trace.gas_limit; + parent.trace.steps.push(intercepted_create_step(create_depth, contract, gas_remaining)); + parent.ordering.push(TraceMemberOrder::Step(step_idx)); + } + parent.ordering.push(TraceMemberOrder::Call(child_slot)); +} + +/// CREATE/CALL-family opcodes that struct-log rendering uses to walk into children. +fn is_calllike_opcode(op: u8) -> bool { + matches!( + op, + opcode::CALL | + opcode::DELEGATECALL | + opcode::STATICCALL | + opcode::CREATE | + opcode::CALLCODE | + opcode::CREATE2 + ) +} + +/// Placeholder CREATE recorded on an intercepted `KeylessDeploy` CALL so struct-log output can +/// descend into the spliced sandbox frame. +fn intercepted_create_step(depth: u64, contract: Address, gas_remaining: u64) -> CallTraceStep { + CallTraceStep { + depth, + pc: 0, + op: OpCode::new(opcode::CREATE).expect("CREATE is a defined opcode"), + contract, + stack: None, + push_stack: None, + memory: None, + returndata: Bytes::new(), + gas_remaining, + gas_refund_counter: 0, + gas_used: 0, + gas_cost: 0, + storage_change: None, + status: None, + immediate_bytes: None, + decoded: None, + } +} From ed906dac7819701856537b9eda323fdb8c90a4a5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:46:15 +0800 Subject: [PATCH 17/28] test: pin keyless sandbox corner cases on both channels and the tracer Add shared init codes (test_utils::keyless) for four sandbox shapes: a constructor that CREATEs then REVERTs, three nesting levels mixed with reverting calls and a log, an out-of-gas constructor, and a call back into KeylessDeploy from inside the sandbox. Each is pinned through the read-only observer, the rewriting inspector (same event stream, same result), the Geth call/opcode/prestate renderers in mega-evme, and as offline-filled state-test fixtures under Rex5 and Rex6. --- .../keyless_sandbox_deep_mixed_rex5.json | 87 ++++ .../keyless_sandbox_deep_mixed_rex6.json | 87 ++++ ...less_sandbox_nested_keyless_call_rex5.json | 87 ++++ ...less_sandbox_nested_keyless_call_rex6.json | 87 ++++ ...less_sandbox_revert_after_create_rex5.json | 81 ++++ ...less_sandbox_revert_after_create_rex6.json | 81 ++++ .../keyless_sandbox_sandbox_oog_rex5.json | 81 ++++ .../keyless_sandbox_sandbox_oog_rex6.json | 81 ++++ bin/mega-evme/src/common/trace.rs | 207 ++++++++- crates/mega-evm/src/test_utils/keyless.rs | 169 +++++++ crates/mega-evm/src/test_utils/mod.rs | 2 + .../tests/rex2/keyless_sandbox_extreme.rs | 429 ++++++++++++++++++ .../tests/rex2/keyless_sandbox_support.rs | 12 +- crates/mega-evm/tests/rex2/main.rs | 1 + 14 files changed, 1485 insertions(+), 7 deletions(-) create mode 100644 bench/replay/fixtures/keyless_sandbox_deep_mixed_rex5.json create mode 100644 bench/replay/fixtures/keyless_sandbox_deep_mixed_rex6.json create mode 100644 bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex5.json create mode 100644 bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex6.json create mode 100644 bench/replay/fixtures/keyless_sandbox_revert_after_create_rex5.json create mode 100644 bench/replay/fixtures/keyless_sandbox_revert_after_create_rex6.json create mode 100644 bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex5.json create mode 100644 bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex6.json create mode 100644 crates/mega-evm/src/test_utils/keyless.rs create mode 100644 crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs diff --git a/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex5.json b/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex5.json new file mode 100644 index 00000000..ab1b749f --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex5.json @@ -0,0 +1,87 @@ +{ + "keyless_sandbox_deep_mixed_rex5": { + "_info": { + "comment": "keyless sandbox corner case (deep_mixed): three CREATE levels, two reverting CALLs into 0x..aaaaaa, a LOG1, and a storage write inside one sandbox. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex5`.", + "init_code": "0x604461006e600039604460006000f060005560006000600060006000730000000000000000000000000000000000aaaaaa61c350f15063646565706020527fdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd6004603ca1600060605360016060f369600060005360016000f3600052600a60166000f05060006000600060006000730000000000000000000000000000000000aaaaaa61c350f150600060405360016040f3", + "inner_signer": "0x7ce5403d2297e4ca6b648a723130e546d3741b17", + "deploy_address": "0x9dacb7da59642293dcf9f6a037ad6dd8e45ad876" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x0000000000000000000000000000000000aaaaaa": { + "balance": "0x0", + "code": "0x60006000fd", + "nonce": "0x1", + "storage": {} + }, + "0x7ce5403d2297e4ca6b648a723130e546d3741b17": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex5": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x51f08758161a6ef32d454fa79717733e944b17533075fb6aca256ec30860986c", + "logs": "0x8eafa7e60b3e427ae50fb227787e035402075a089427ec0f14041ace04baa5d5", + "megaGasUsed": 446122, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000000107f901048085174876e800830f42408080b8b2604461006e600039604460006000f060005560006000600060006000730000000000000000000000000000000000aaaaaa61c350f15063646565706020527fdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd6004603ca1600060605360016060f369600060005360016000f3600052600a60166000f05060006000600060006000730000000000000000000000000000000000aaaaaa61c350f150600060405360016040f31ba02222222222222222222222222222222222222222222222222222222222222222a0222222222222222222222222222222222222222222222222222222222222222200000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000003ab160000000000000000000000009dacb7da59642293dcf9f6a037ad6dd8e45ad87600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex6.json b/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex6.json new file mode 100644 index 00000000..9f89ca98 --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_deep_mixed_rex6.json @@ -0,0 +1,87 @@ +{ + "keyless_sandbox_deep_mixed_rex6": { + "_info": { + "comment": "keyless sandbox corner case (deep_mixed): three CREATE levels, two reverting CALLs into 0x..aaaaaa, a LOG1, and a storage write inside one sandbox. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex6`.", + "init_code": "0x604461006e600039604460006000f060005560006000600060006000730000000000000000000000000000000000aaaaaa61c350f15063646565706020527fdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd6004603ca1600060605360016060f369600060005360016000f3600052600a60166000f05060006000600060006000730000000000000000000000000000000000aaaaaa61c350f150600060405360016040f3", + "inner_signer": "0x7ce5403d2297e4ca6b648a723130e546d3741b17", + "deploy_address": "0x9dacb7da59642293dcf9f6a037ad6dd8e45ad876" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x0000000000000000000000000000000000aaaaaa": { + "balance": "0x0", + "code": "0x60006000fd", + "nonce": "0x1", + "storage": {} + }, + "0x7ce5403d2297e4ca6b648a723130e546d3741b17": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex6": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x51f08758161a6ef32d454fa79717733e944b17533075fb6aca256ec30860986c", + "logs": "0x8eafa7e60b3e427ae50fb227787e035402075a089427ec0f14041ace04baa5d5", + "megaGasUsed": 446122, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000000107f901048085174876e800830f42408080b8b2604461006e600039604460006000f060005560006000600060006000730000000000000000000000000000000000aaaaaa61c350f15063646565706020527fdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd6004603ca1600060605360016060f369600060005360016000f3600052600a60166000f05060006000600060006000730000000000000000000000000000000000aaaaaa61c350f150600060405360016040f31ba02222222222222222222222222222222222222222222222222222222222222222a0222222222222222222222222222222222222222222222222222222222222222200000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000003ab160000000000000000000000009dacb7da59642293dcf9f6a037ad6dd8e45ad87600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex5.json b/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex5.json new file mode 100644 index 00000000..086c830c --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex5.json @@ -0,0 +1,87 @@ +{ + "keyless_sandbox_nested_keyless_call_rex5": { + "_info": { + "comment": "keyless sandbox corner case (nested_keyless_call): constructor CALLs KeylessDeploy from inside the sandbox; not intercepted at depth > 0, the call reverts. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex5`.", + "init_code": "0x6000600060006000600073634200000000000000000000000000000000000361c350f150600060005360016000f3", + "inner_signer": "0xdc7e3a94ba7db4c1de83a77223a5fced6d7273e8", + "deploy_address": "0x0fc32ba0751255e3057226a92cd310e022097093" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x6342000000000000000000000000000000000003": { + "balance": "0x0", + "code": "0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c806354fd4d5014610038578063846365d514610080575b5f5ffd5b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516100779190610124565b60405180910390f35b61009361008e36600461013d565b6100a2565b604051610077939291906101af565b5f5f60606040517f1894f07600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f61013660208301846100d8565b9392505050565b5f5f5f6040848603121561014f575f5ffd5b833567ffffffffffffffff811115610165575f5ffd5b8401601f81018613610175575f5ffd5b803567ffffffffffffffff81111561018b575f5ffd5b86602082840101111561019c575f5ffd5b6020918201979096509401359392505050565b67ffffffffffffffff8416815273ffffffffffffffffffffffffffffffffffffffff83166020820152606060408201525f6101ed60608301846100d8565b9594505050505056fea164736f6c634300081e000a", + "nonce": "0x1", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0xdc7e3a94ba7db4c1de83a77223a5fced6d7273e8": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex5": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x807d79c716cee6d2b0201f188cb3ad70276c40da705fd37bafd7708a6cddad16", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 296065, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000000081f87f8085174876e800830f42408080ae6000600060006000600073634200000000000000000000000000000000000361c350f150600060005360016000f31ba02222222222222222222222222222222222222222222222222222222222222222a0222222222222222222222222222222222222222222222222222222222222222200000000000000000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000001ac8d0000000000000000000000000fc32ba0751255e3057226a92cd310e02209709300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex6.json b/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex6.json new file mode 100644 index 00000000..b8d51a2c --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_nested_keyless_call_rex6.json @@ -0,0 +1,87 @@ +{ + "keyless_sandbox_nested_keyless_call_rex6": { + "_info": { + "comment": "keyless sandbox corner case (nested_keyless_call): constructor CALLs KeylessDeploy from inside the sandbox; not intercepted at depth > 0, the call reverts. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex6`.", + "init_code": "0x6000600060006000600073634200000000000000000000000000000000000361c350f150600060005360016000f3", + "inner_signer": "0xdc7e3a94ba7db4c1de83a77223a5fced6d7273e8", + "deploy_address": "0x0fc32ba0751255e3057226a92cd310e022097093" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x6342000000000000000000000000000000000003": { + "balance": "0x0", + "code": "0x608060405234801561000f575f5ffd5b5060043610610034575f3560e01c806354fd4d5014610038578063846365d514610080575b5f5ffd5b604080518082018252600581527f312e302e30000000000000000000000000000000000000000000000000000000602082015290516100779190610124565b60405180910390f35b61009361008e36600461013d565b6100a2565b604051610077939291906101af565b5f5f60606040517f1894f07600000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b5f81518084528060208401602086015e5f6020828601015260207fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f83011685010191505092915050565b602081525f61013660208301846100d8565b9392505050565b5f5f5f6040848603121561014f575f5ffd5b833567ffffffffffffffff811115610165575f5ffd5b8401601f81018613610175575f5ffd5b803567ffffffffffffffff81111561018b575f5ffd5b86602082840101111561019c575f5ffd5b6020918201979096509401359392505050565b67ffffffffffffffff8416815273ffffffffffffffffffffffffffffffffffffffff83166020820152606060408201525f6101ed60608301846100d8565b9594505050505056fea164736f6c634300081e000a", + "nonce": "0x1", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0xdc7e3a94ba7db4c1de83a77223a5fced6d7273e8": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex6": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x807d79c716cee6d2b0201f188cb3ad70276c40da705fd37bafd7708a6cddad16", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 296065, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000000000000000000081f87f8085174876e800830f42408080ae6000600060006000600073634200000000000000000000000000000000000361c350f150600060005360016000f31ba02222222222222222222222222222222222222222222222222222222222222222a0222222222222222222222222222222222222222222222222222222222222222200000000000000000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000001ac8d0000000000000000000000000fc32ba0751255e3057226a92cd310e02209709300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex5.json b/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex5.json new file mode 100644 index 00000000..59b9e934 --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex5.json @@ -0,0 +1,81 @@ +{ + "keyless_sandbox_revert_after_create_rex5": { + "_info": { + "comment": "keyless sandbox corner case (revert_after_create): constructor CREATEs a child then REVERTs: sandbox ExecutionFailed, both contracts rolled back. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex5`.", + "init_code": "0x69600060005360016000f3600052600a60166000f05060006000fd", + "inner_signer": "0xef24829e39a1e6805be20d548c9eb1b7adaa1c07", + "deploy_address": "0x4a85c373cd63de73356667c6f3b21e4076092a50" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0xef24829e39a1e6805be20d548c9eb1b7adaa1c07": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex5": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x67b30e3269922fe570a904131b440770a17fa9fc6adbf8dec73d4d0052dea42d", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 322895, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be400000000000000000000000000000000000000000000000000000000000000006ef86c8085174876e800830f424080809b69600060005360016000f3600052600a60166000f05060006000fd1ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x0000000000000000000000000000000000000000000000000000000000021b5f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064ad2bd1d90000000000000000000000000000000000000000000000000000000000021b5f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex6.json b/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex6.json new file mode 100644 index 00000000..819b24e1 --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_revert_after_create_rex6.json @@ -0,0 +1,81 @@ +{ + "keyless_sandbox_revert_after_create_rex6": { + "_info": { + "comment": "keyless sandbox corner case (revert_after_create): constructor CREATEs a child then REVERTs: sandbox ExecutionFailed, both contracts rolled back. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex6`.", + "init_code": "0x69600060005360016000f3600052600a60166000f05060006000fd", + "inner_signer": "0xef24829e39a1e6805be20d548c9eb1b7adaa1c07", + "deploy_address": "0x4a85c373cd63de73356667c6f3b21e4076092a50" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0xef24829e39a1e6805be20d548c9eb1b7adaa1c07": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex6": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0x67b30e3269922fe570a904131b440770a17fa9fc6adbf8dec73d4d0052dea42d", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 322895, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d5000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000002540be400000000000000000000000000000000000000000000000000000000000000006ef86c8085174876e800830f424080809b69600060005360016000f3600052600a60166000f05060006000fd1ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x0000000000000000000000000000000000000000000000000000000000021b5f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064ad2bd1d90000000000000000000000000000000000000000000000000000000000021b5f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex5.json b/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex5.json new file mode 100644 index 00000000..8a12afed --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex5.json @@ -0,0 +1,81 @@ +{ + "keyless_sandbox_sandbox_oog_rex5": { + "_info": { + "comment": "keyless sandbox corner case (sandbox_oog): inner gas limit 110000 clears the intrinsic cost but not the constructor: sandbox halts out of gas. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex5`.", + "init_code": "0x7f00000000000000000000000000000000000000000000000000000000000000017f0000000000000000000000000000000000000000000000000000000000000000556001600060003960016000f3", + "inner_signer": "0x2bef1220c12c50f65b3169fe7b01e480738599af", + "deploy_address": "0xb07c71c732b5b216e6597892e846d7eb3b3f8887" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x2bef1220c12c50f65b3169fe7b01e480738599af": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex5": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xfc73cfc650c0dba8251d962020325a6edb656fa57250d699fc3b37aeec5f346b", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 296708, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d50000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000001adb000000000000000000000000000000000000000000000000000000000000000a3f8a18085174876e8008301adb08080b84f7f00000000000000000000000000000000000000000000000000000000000000017f0000000000000000000000000000000000000000000000000000000000000000556001600060003960016000f31ba02222222222222222222222222222222222222222222222222222222222222222a022222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000001adb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024de972b6e000000000000000000000000000000000000000000000000000000000001adb000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex6.json b/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex6.json new file mode 100644 index 00000000..e9c2ed86 --- /dev/null +++ b/bench/replay/fixtures/keyless_sandbox_sandbox_oog_rex6.json @@ -0,0 +1,81 @@ +{ + "keyless_sandbox_sandbox_oog_rex6": { + "_info": { + "comment": "keyless sandbox corner case (sandbox_oog): inner gas limit 110000 clears the intrinsic cost but not the constructor: sandbox halts out of gas. Hand-built from mega_evm::test_utils::keyless, post filled offline with `state-test --fill --bench-spec Rex6`.", + "init_code": "0x7f00000000000000000000000000000000000000000000000000000000000000017f0000000000000000000000000000000000000000000000000000000000000000556001600060003960016000f3", + "inner_signer": "0x2bef1220c12c50f65b3169fe7b01e480738599af", + "deploy_address": "0xb07c71c732b5b216e6597892e846d7eb3b3f8887" + }, + "env": { + "currentChainID": "0x10e6", + "currentCoinbase": "0x4200000000000000000000000000000000000011", + "currentDifficulty": "0x0", + "currentGasLimit": "0x2540be400", + "currentNumber": "0x735b75", + "currentTimestamp": "0x69858148", + "currentBaseFee": "0xf4240", + "currentRandom": "0x5ed22b2a6937705d6c9677f844729acf17d94ad1381c5bd88f3aa7ea2f178af6", + "currentExcessBlobGas": "0x0" + }, + "pre": { + "0x2bef1220c12c50f65b3169fe7b01e480738599af": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + }, + "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf": { + "balance": "0x3635c9adc5dea00000", + "code": "0x", + "nonce": "0x0", + "storage": {} + } + }, + "post": { + "Rex6": [ + { + "indexes": { + "data": 0, + "gas": 0, + "value": 0 + }, + "hash": "0xfc73cfc650c0dba8251d962020325a6edb656fa57250d699fc3b37aeec5f346b", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "megaGasUsed": 296708, + "megaStatus": "success" + } + ] + }, + "transaction": { + "type": 2, + "data": [ + "0x846365d50000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000001adb000000000000000000000000000000000000000000000000000000000000000a3f8a18085174876e8008301adb08080b84f7f00000000000000000000000000000000000000000000000000000000000000017f0000000000000000000000000000000000000000000000000000000000000000556001600060003960016000f31ba02222222222222222222222222222222222222222222222222222222222222222a022222222222222222222222222222222222222222222222222222222222222220000000000000000000000000000000000000000000000000000000000" + ], + "gasLimit": [ + "0x1e8480" + ], + "gasPrice": null, + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000001", + "sender": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf", + "to": "0x6342000000000000000000000000000000000003", + "value": [ + "0x0" + ], + "maxFeePerGas": "0x1e8481", + "maxPriorityFeePerGas": "0x1", + "initcodes": null, + "accessLists": [ + [] + ], + "authorizationList": null, + "blobVersionedHashes": [], + "maxFeePerBlobGas": null + }, + "out": "0x000000000000000000000000000000000000000000000000000000000001adb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000024de972b6e000000000000000000000000000000000000000000000000000000000001adb000000000000000000000000000000000000000000000000000000000", + "megaEnv": { + "bucketCapacities": [], + "oracleStorage": [] + } + } +} \ No newline at end of file diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 18de3db6..254b116e 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -280,12 +280,17 @@ mod tests { context::{BlockEnv, CfgEnv, TxEnv}, database::StateBuilder, }, - test_utils::{BytecodeBuilder, MemoryDatabase}, + test_utils::{ + deep_mixed_init, nested_keyless_call_init, revert_after_create_init, BytecodeBuilder, + MemoryDatabase, DEEP_MIXED_LOG_TOPIC, REVERTING_RUNTIME, + }, BlockLimits, EmptyExternalEnv, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, - KEYLESS_DEPLOY_ADDRESS, + KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE, }; + const REVERTER: Address = address!("0000000000000000000000000000000000aaaaaa"); + const TEST_CALLER: Address = address!("0000000000000000000000000000000000100000"); const LARGE_GAS_LIMIT_OVERRIDE: u64 = 10_000_000_000; const LARGE_SIGNER_BALANCE: u128 = 1_000_000_000_000_000_000_000; @@ -306,10 +311,17 @@ mod tests { } fn create_pre_eip155_deploy_tx(init_code: Bytes) -> (Bytes, Address) { + create_pre_eip155_deploy_tx_with_gas_limit(init_code, SIGNED_TX_GAS_LIMIT) + } + + fn create_pre_eip155_deploy_tx_with_gas_limit( + init_code: Bytes, + gas_limit: u64, + ) -> (Bytes, Address) { let tx = TxLegacy { nonce: 0, gas_price: 100_000_000_000, - gas_limit: SIGNED_TX_GAS_LIMIT, + gas_limit, to: TxKind::Create, value: U256::ZERO, input: init_code, @@ -330,10 +342,13 @@ mod tests { (tx_bytes, signer) } - fn keyless_deploy_tx(keyless_deployment_tx: Bytes) -> MegaTransaction { + fn keyless_deploy_tx_with_override( + keyless_deployment_tx: Bytes, + gas_limit_override: u64, + ) -> MegaTransaction { let call_data = IKeylessDeploy::keylessDeployCall { keylessDeploymentTransaction: keyless_deployment_tx, - gasLimitOverride: U256::from(LARGE_GAS_LIMIT_OVERRIDE), + gasLimitOverride: U256::from(gas_limit_override), } .abi_encode(); let tx = TxEnv { @@ -373,18 +388,61 @@ mod tests { sandbox: SharedTracingInspector, ) -> (ResultAndState, MemoryDatabase, Address) { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + run_traced_keyless_bytes(outer, sandbox, tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |_| {}) + } + + /// Runs one traced keyless deploy of an already-signed `tx_bytes`, with `setup` applied + /// to the funded database first (extra contracts the constructor calls into). + fn run_traced_keyless_bytes( + outer: SharedTracingInspector, + sandbox: SharedTracingInspector, + tx_bytes: Bytes, + signer: Address, + gas_limit_override: u64, + setup: impl Fn(&mut MemoryDatabase), + ) -> (ResultAndState, MemoryDatabase, Address) { let mut db = funded_db(signer); + setup(&mut db); let result = { let context = keyless_context(&mut db); let mut evm = MegaEvm::new(context).with_inspector(outer); evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); - let result = evm.inspect_tx(keyless_deploy_tx(tx_bytes)).expect("keyless deploy"); + let result = evm + .inspect_tx(keyless_deploy_tx_with_override(tx_bytes, gas_limit_override)) + .expect("keyless deploy"); assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); result }; (result, db, signer) } + /// Traces one keyless deploy and renders the Geth call tree with logs. + fn traced_call_frame( + tx_bytes: Bytes, + signer: Address, + gas_limit_override: u64, + setup: impl Fn(&mut MemoryDatabase), + ) -> (CallFrame, SharedTracingInspector, ResultAndState, MemoryDatabase) { + let outer = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (result_and_state, db, _signer) = run_traced_keyless_bytes( + outer.clone(), + sandbox.clone(), + tx_bytes, + signer, + gas_limit_override, + setup, + ); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + let frame = outer.borrow().geth_builder().geth_call_traces( + CallConfig { only_top_call: Some(false), with_log: Some(true) }, + result_and_state.result.gas_used(), + ); + (frame, outer, result_and_state, db) + } + #[test] fn test_keyless_call_trace_nests_sandbox_create() { let outer = @@ -538,4 +596,141 @@ mod tests { assert!(!nodes[0].children.is_empty()); assert!(nodes[nodes[0].children[0]].trace.kind.is_any_create()); } + + /// A constructor that CREATEs a child and then reverts: the sandbox CREATE frame carries + /// the revert, the child CREATE stays visible beneath it (events stream, they are not + /// retroactively truncated), and the prestate diff shows neither contract landing. + #[test] + fn test_keyless_trace_revert_after_create_keeps_child_frame() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_after_create_init()); + let deploy_address = signer.create(0); + let child = deploy_address.create(1); + let (frame, outer, result_and_state, db) = + traced_call_frame(tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |_| {}); + + assert_eq!(frame.typ, "CALL"); + assert!(frame.error.is_none(), "outer call succeeds with errorData"); + let create = &frame.calls[0]; + assert_eq!(create.typ, "CREATE"); + assert!(create.error.is_some(), "sandbox CREATE reverted: {:?}", create.error); + assert_eq!(create.calls.len(), 1, "the child CREATE is still nested under it"); + assert_eq!(create.calls[0].typ, "CREATE"); + assert_eq!(create.calls[0].to, Some(child)); + assert!(create.calls[0].error.is_none(), "the child itself succeeded"); + + let prestate = outer + .borrow() + .geth_builder() + .geth_prestate_traces( + &result_and_state, + &PreStateConfig { diff_mode: Some(true), ..Default::default() }, + &*db, + ) + .expect("prestate diff"); + let post = prestate.as_diff().expect("diff mode").post.clone(); + assert!(!post.contains_key(&deploy_address), "parent rolled back"); + assert!(!post.contains_key(&child), "child rolled back"); + } + + /// Three CREATE levels, two reverting CALLs, and a log inside one sandbox: the call tree + /// nests the whole shape under the `KeylessDeploy` CALL and the struct logs descend one + /// depth level per frame. + #[test] + fn test_keyless_trace_deep_mixed_nests_three_levels() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let parent = signer.create(0); + let child = parent.create(1); + let grandchild = child.create(1); + let (frame, outer, result_and_state, _db) = + traced_call_frame(tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |db| { + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + }); + + let sandbox = &frame.calls[0]; + assert_eq!((sandbox.typ.as_str(), sandbox.to), ("CREATE", Some(parent))); + assert!(sandbox.error.is_none()); + let kinds: Vec<(String, Option
, bool)> = + sandbox.calls.iter().map(|c| (c.typ.clone(), c.to, c.error.is_some())).collect(); + assert_eq!( + kinds, + vec![ + ("CREATE".to_owned(), Some(child), false), + ("CALL".to_owned(), Some(REVERTER), true) + ], + "parent frame: child CREATE then reverting CALL" + ); + let child_frame = &sandbox.calls[0]; + let child_kinds: Vec<(String, Option
, bool)> = + child_frame.calls.iter().map(|c| (c.typ.clone(), c.to, c.error.is_some())).collect(); + assert_eq!( + child_kinds, + vec![ + ("CREATE".to_owned(), Some(grandchild), false), + ("CALL".to_owned(), Some(REVERTER), true) + ], + "child frame: grandchild CREATE then reverting CALL" + ); + assert_eq!(sandbox.logs.len(), 1, "the parent constructor's LOG1"); + assert_eq!( + sandbox.logs[0].topics.as_deref().map(|t| t[0]), + Some(B256::from(DEEP_MIXED_LOG_TOPIC)) + ); + + let geth_trace = outer.borrow().geth_builder().geth_traces( + result_and_state.result.gas_used(), + Bytes::new(), + GethDefaultTracingOptions::default(), + ); + let max_depth = geth_trace.struct_logs.iter().map(|s| s.depth).max().unwrap_or(0); + assert_eq!(max_depth, 4, "synthetic CREATE at 1, parent 2, child 3, grandchild 4"); + } + + /// An inner gas budget above the intrinsic cost but below the constructor's needs: the + /// sandbox CREATE frame reports out of gas while the outer call still succeeds. + #[test] + fn test_keyless_trace_sandbox_out_of_gas_marks_create_frame() { + const TIGHT_GAS: u64 = 110_000; + let (tx_bytes, signer) = + create_pre_eip155_deploy_tx_with_gas_limit(success_constructor(), TIGHT_GAS); + let (frame, _outer, _result, _db) = traced_call_frame(tx_bytes, signer, TIGHT_GAS, |_| {}); + + assert!(frame.error.is_none(), "outer call succeeds with errorData"); + let create = &frame.calls[0]; + assert_eq!(create.typ, "CREATE"); + let error = create.error.as_deref().unwrap_or_default().to_ascii_lowercase(); + assert!(error.contains("out of gas"), "sandbox CREATE halted out of gas: {error:?}"); + } + + /// A constructor that CALLs `KeylessDeploy` from inside the sandbox: interception is + /// depth-0 only, so the trace shows an ordinary reverted CALL to the system contract + /// nested under the sandbox CREATE. + #[test] + fn test_keyless_trace_nested_keyless_call_is_a_reverted_frame() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(nested_keyless_call_init()); + let (frame, outer, result_and_state, _db) = + traced_call_frame(tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |db| { + db.set_account_code(KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE); + }); + + let sandbox = &frame.calls[0]; + assert_eq!(sandbox.typ, "CREATE"); + assert!(sandbox.error.is_none(), "the constructor ignores the failed call"); + assert_eq!(sandbox.calls.len(), 1); + let nested = &sandbox.calls[0]; + assert_eq!((nested.typ.as_str(), nested.to), ("CALL", Some(KEYLESS_DEPLOY_ADDRESS))); + assert!(nested.error.is_some(), "not intercepted at depth > 0, so it reverted"); + + // The contract's dispatcher runs at depth 3 before it reverts, so the struct logs + // reach one level below the sandbox constructor. + let geth_trace = outer.borrow().geth_builder().geth_traces( + result_and_state.result.gas_used(), + Bytes::new(), + GethDefaultTracingOptions::default(), + ); + let max_depth = geth_trace.struct_logs.iter().map(|s| s.depth).max().unwrap_or(0); + assert_eq!( + max_depth, 3, + "synthetic CREATE at 1, constructor at 2, KeylessDeploy code at 3" + ); + } } diff --git a/crates/mega-evm/src/test_utils/keyless.rs b/crates/mega-evm/src/test_utils/keyless.rs new file mode 100644 index 00000000..03038273 --- /dev/null +++ b/crates/mega-evm/src/test_utils/keyless.rs @@ -0,0 +1,169 @@ +//! Init codes that exercise the corners of nested keyless-deploy sandbox execution. +//! +//! Each builder returns raw init code for a keyless deployment whose constructor does +//! something an observer or tracer has to see through: a CREATE that is later rolled back, +//! three levels of nested frames mixed with reverting calls and logs, or a call back into +//! the `KeylessDeploy` system contract from inside the sandbox. The same bytes are used by +//! the in-process tests, the offline state-test corpus, and the end-to-end suite. + +#[cfg(not(feature = "std"))] +use alloc as std; +use std::vec::Vec; + +use alloy_primitives::{Address, Bytes}; +use revm::bytecode::opcode::{ + CALL, CODECOPY, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, REVERT, SSTORE, +}; + +use super::BytecodeBuilder; +use crate::KEYLESS_DEPLOY_ADDRESS; + +/// Init code for the smallest deployable contract: returns a one-byte `STOP` runtime. +/// +/// `PUSH1 0, PUSH1 0, MSTORE8, PUSH1 1, PUSH1 0, RETURN`. +pub const STOP_RUNTIME_INIT: [u8; 10] = + [0x60, 0x00, 0x60, 0x00, 0x53, 0x60, 0x01, 0x60, 0x00, 0xf3]; + +/// Runtime that always reverts with empty data: `PUSH1 0, PUSH1 0, REVERT`. +pub const REVERTING_RUNTIME: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; + +/// The topic the deep-mixed constructor logs under. +pub const DEEP_MIXED_LOG_TOPIC: [u8; 32] = [0xdd; 32]; + +/// Gas forwarded on every internal CALL these constructors make. +const INTERNAL_CALL_GAS: u16 = 50_000; + +/// Emits `CREATE(value = 0, offset = 22, size = 10)` of [`STOP_RUNTIME_INIT`] staged in word 0. +fn create_stop_runtime_child(b: BytecodeBuilder) -> BytecodeBuilder { + b.push_bytes(STOP_RUNTIME_INIT) + .push_number(0_u8) + .append(MSTORE) + .push_number(STOP_RUNTIME_INIT.len() as u8) + .push_number(22_u8) + .push_number(0_u8) + .append(CREATE) +} + +/// Emits `CALL(gas, target, 0, 0, 0, 0, 0)` followed by `POP`. +fn call_and_pop(b: BytecodeBuilder, target: Address) -> BytecodeBuilder { + b.push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(target) + .push_number(INTERNAL_CALL_GAS) + .append(CALL) + .append(POP) +} + +/// Emits `MSTORE8(offset, 0); RETURN(offset, 1)`: a one-byte `STOP` runtime. +fn return_stop_runtime(b: BytecodeBuilder, offset: u8) -> BytecodeBuilder { + b.push_number(0_u8) + .push_number(offset) + .append(MSTORE8) + .push_number(1_u8) + .push_number(offset) + .append(RETURN) +} + +/// Constructor that CREATEs a child and then REVERTs. +/// +/// The child creation succeeds inside the sandbox and is rolled back with the constructor, +/// so the sandbox ends as `ExecutionFailed` while its event stream still carried a nested +/// CREATE. +pub fn revert_after_create_init() -> Bytes { + let b = create_stop_runtime_child(BytecodeBuilder::default()).append(POP); + b.push_number(0_u8).push_number(0_u8).append(REVERT).build() +} + +/// Constructor with three nesting levels, reverting calls, a log, and a storage write. +/// +/// The parent CREATEs a child (stored in slot 0) whose own constructor CREATEs a grandchild +/// and CALLs `reverter`; the parent then CALLs `reverter` itself, emits a `LOG1` under +/// [`DEEP_MIXED_LOG_TOPIC`] with data `deep`, and returns a one-byte `STOP` runtime. The +/// child init code is appended after the parent logic and copied with `CODECOPY`. +pub fn deep_mixed_init(reverter: Address) -> Bytes { + let child = + call_and_pop(create_stop_runtime_child(BytecodeBuilder::default()).append(POP), reverter); + let child = return_stop_runtime(child, 0x40).build_vec(); + + let parent_logic = |child_offset: u16| -> Vec { + let b = BytecodeBuilder::default() + // CODECOPY(dest = 0, code_offset = child_offset, size = child.len()) + .push_number(child.len() as u8) + .push_number(child_offset) + .push_number(0_u8) + .append(CODECOPY) + // CREATE(value = 0, offset = 0, size = child.len()) ; SSTORE(0, child) + .push_number(child.len() as u8) + .push_number(0_u8) + .push_number(0_u8) + .append(CREATE) + .push_number(0_u8) + .append(SSTORE); + let b = call_and_pop(b, reverter); + // LOG1(offset = 0x3c, size = 4, topic) with "deep" staged in word 0x20. + let b = b + .push_bytes(*b"deep") + .push_number(0x20_u8) + .append(MSTORE) + .push_bytes(DEEP_MIXED_LOG_TOPIC) + .push_number(4_u8) + .push_number(0x3c_u8) + .append(LOG1); + return_stop_runtime(b, 0x60).build_vec() + }; + + // The child code starts right after the parent logic; the PUSH2 keeps the logic length + // independent of the offset value. + let logic_len = parent_logic(0).len(); + let mut code = parent_logic(logic_len as u16); + debug_assert_eq!(code.len(), logic_len); + code.extend_from_slice(&child); + Bytes::from(code) +} + +/// Constructor that CALLs the `KeylessDeploy` system contract from inside the sandbox. +/// +/// Interception only applies at depth 0, so the call reaches the contract's bytecode and +/// reverts; the constructor ignores the result and deploys a one-byte `STOP` runtime. +pub fn nested_keyless_call_init() -> Bytes { + let b = call_and_pop(BytecodeBuilder::default(), KEYLESS_DEPLOY_ADDRESS); + return_stop_runtime(b, 0).build() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_revert_after_create_init_bytes() { + let code = revert_after_create_init(); + assert_eq!(&code[..1], &[0x69], "PUSH10 of the child init"); + assert_eq!(&code[1..11], &STOP_RUNTIME_INIT); + assert_eq!(code.last(), Some(&REVERT)); + } + + #[test] + fn test_deep_mixed_init_embeds_child_after_logic() { + let reverter = Address::repeat_byte(0xaa); + let code = deep_mixed_init(reverter); + // The child init ends with RETURN and starts with the PUSH10 of the grandchild init. + assert_eq!(code.last(), Some(&RETURN)); + let child_start = code.len() - code.iter().rev().position(|&b| b == 0x69).unwrap() - 1; + assert_eq!(&code[child_start + 1..child_start + 11], &STOP_RUNTIME_INIT); + // CODECOPY reads the child from exactly that offset. + let offset = u16::from_be_bytes([code[3], code[4]]) as usize; + assert_eq!(offset, child_start); + assert_eq!(code[1] as usize, code.len() - child_start, "CODECOPY size is the child length"); + } + + #[test] + fn test_nested_keyless_call_init_targets_system_contract() { + let code = nested_keyless_call_init(); + let at = code.windows(20).position(|w| w == KEYLESS_DEPLOY_ADDRESS.as_slice()).unwrap(); + assert_eq!(code[at - 1], 0x73, "PUSH20 of the KeylessDeploy address"); + assert_eq!(code.last(), Some(&RETURN)); + } +} diff --git a/crates/mega-evm/src/test_utils/mod.rs b/crates/mega-evm/src/test_utils/mod.rs index 3e6f7c54..a42d66b6 100644 --- a/crates/mega-evm/src/test_utils/mod.rs +++ b/crates/mega-evm/src/test_utils/mod.rs @@ -4,10 +4,12 @@ mod bytes; mod database; mod evm; mod inspectors; +mod keyless; mod opcode_gen; pub use bytes::*; pub use database::*; pub use evm::*; pub use inspectors::*; +pub use keyless::*; pub use opcode_gen::*; diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs new file mode 100644 index 00000000..0116052f --- /dev/null +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs @@ -0,0 +1,429 @@ +//! Corner-case sandbox shapes seen through the read-only observer channel. +//! +//! Each case uses one of the init codes in `mega_evm::test_utils::keyless` — the same bytes +//! the offline state-test corpus and the end-to-end suite run — and pins two things on every +//! spec: the exact event stream the observer receives, and that attaching the observer does +//! not change the result, state, or usage of the transaction. + +use std::{cell::RefCell, rc::Rc}; + +use alloy_primitives::{Address, Bytes, U256}; +use mega_evm::{ + revm::interpreter::{ + interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + Interpreter, + }, + sandbox::{SandboxCompletionKind, SandboxEndOutcome, SandboxInspector, SandboxObserver}, + test_utils::{ + deep_mixed_init, nested_keyless_call_init, revert_after_create_init, MemoryDatabase, + REVERTING_RUNTIME, + }, + ExternalEnvTypes, MegaContext, MegaSpecId, KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE, +}; + +use crate::keyless_sandbox_support::{ + assert_result_and_state_eq, assert_usage_eq, create_pre_eip155_deploy_tx, + create_pre_eip155_deploy_tx_with_value_and_gas_limit, funded_db, + keyless_deploy_call_tx_with_outer_gas, run_keyless_with_usage, success_constructor, RunConfig, + DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, REVERTER, SPECS, +}; + +/// The frame-level events these cases assert on. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Ev { + Create, + CreateEnd { ok: bool, address: Option
}, + Call { target: Address }, + CallEnd { target: Address, reverted: bool }, + Log, + End(SandboxEndOutcome), +} + +#[derive(Default)] +struct Recorder { + events: Vec, +} + +impl SandboxObserver for Recorder { + fn call( + &mut self, + _context: &mut MegaContext, E>, + inputs: &CallInputs, + ) { + self.events.push(Ev::Call { target: inputs.target_address }); + } + + fn call_end( + &mut self, + _context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &CallOutcome, + ) { + self.events.push(Ev::CallEnd { + target: inputs.target_address, + reverted: outcome.result.is_revert(), + }); + } + + fn create( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &CreateInputs, + ) { + self.events.push(Ev::Create); + } + + fn create_end( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &CreateInputs, + outcome: &CreateOutcome, + ) { + self.events.push(Ev::CreateEnd { ok: outcome.result.is_ok(), address: outcome.address }); + } + + fn log( + &mut self, + _interp: &mut Interpreter, + _context: &mut MegaContext, E>, + _log: alloy_primitives::Log, + ) { + self.events.push(Ev::Log); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(Ev::End(outcome.clone())); + } +} + +/// The same recorder on the rewriting channel: records identically and never intervenes, so +/// both channels must produce the same stream and the same execution. +impl SandboxInspector for Recorder { + fn call( + &mut self, + _context: &mut MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + self.events.push(Ev::Call { target: inputs.target_address }); + None + } + + fn call_end( + &mut self, + _context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + self.events.push(Ev::CallEnd { + target: inputs.target_address, + reverted: outcome.result.is_revert(), + }); + } + + fn create( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &mut CreateInputs, + ) -> Option { + self.events.push(Ev::Create); + None + } + + fn create_end( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.events.push(Ev::CreateEnd { ok: outcome.result.is_ok(), address: outcome.address }); + } + + fn log( + &mut self, + _interp: &mut Interpreter, + _context: &mut MegaContext, E>, + _log: alloy_primitives::Log, + ) { + self.events.push(Ev::Log); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.events.push(Ev::End(outcome.clone())); + } +} + +/// Runs `tx_bytes` with the recorder attached through the rewriting channel. +fn run_through_inspector_channel( + spec: MegaSpecId, + tx_bytes: &Bytes, + signer: Address, + gas_limit_override: u64, + setup: &impl Fn(&mut MemoryDatabase), +) -> ( + mega_evm::revm::context::result::ResultAndState, + mega_evm::LimitUsage, + Vec, +) { + use mega_evm::{ + revm::{inspector::NoOpInspector, ExecuteEvm}, + MegaEvm, + }; + let recorder = Rc::new(RefCell::new(Recorder::default())); + let mut db = funded_db(signer); + setup(&mut db); + let mut context = MegaContext::new(&mut db, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(Some(Rc::clone(&recorder))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx_with_outer_gas( + tx_bytes.clone(), + gas_limit_override, + DEFAULT_OUTER_GAS_LIMIT, + ); + let result = evm.transact(tx).expect("keyless deploy through the inspector channel"); + let usage = evm.ctx.additional_limit.borrow().get_usage(); + let events = recorder.borrow().events.clone(); + (result, usage, events) +} + +/// One keyless deploy of `tx_bytes` under `spec`: once with the recorder on the read-only +/// channel, once with no hook, and once with the recorder on the rewriting channel. The three +/// runs must agree on result, state, and usage, and both channels must stream the same +/// events. Returns the observed run and the event stream. +fn run_case( + spec: MegaSpecId, + tx_bytes: &Bytes, + signer: Address, + gas_limit_override: u64, + setup: impl Fn(&mut MemoryDatabase), +) -> (mega_evm::revm::context::result::ResultAndState, Vec) { + let case = format!("{spec:?}"); + let recorder = Rc::new(RefCell::new(Recorder::default())); + + let mut db = funded_db(signer); + setup(&mut db); + let (observed, observed_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db, + tx_bytes: tx_bytes.clone(), + gas_limit_override, + observer: Some(Rc::clone(&recorder)), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + let mut db = funded_db(signer); + setup(&mut db); + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db, + tx_bytes: tx_bytes.clone(), + gas_limit_override, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + + assert_result_and_state_eq(&observed, &baseline, &case); + assert_usage_eq(observed_usage, baseline_usage, &case); + let events = recorder.borrow().events.clone(); + + let (rewritten, rewritten_usage, rewritten_events) = + run_through_inspector_channel(spec, tx_bytes, signer, gas_limit_override, &setup); + assert_result_and_state_eq(&rewritten, &baseline, &format!("{case} (inspector channel)")); + assert_usage_eq(rewritten_usage, baseline_usage, &format!("{case} (inspector channel)")); + assert_eq!(rewritten_events, events, "{case}: both channels must stream the same frame events"); + + (observed, events) +} + +fn has_code( + result: &mega_evm::revm::context::result::ResultAndState, + addr: Address, +) -> bool { + result + .state + .get(&addr) + .and_then(|account| account.info.code.as_ref()) + .is_some_and(|code| !code.is_empty()) +} + +fn last_end(events: &[Ev]) -> &SandboxEndOutcome { + match events.last() { + Some(Ev::End(outcome)) => outcome, + other => panic!("stream must end with sandbox_end, got {other:?}"), + } +} + +fn count(events: &[Ev], pred: impl Fn(&Ev) -> bool) -> usize { + events.iter().filter(|e| pred(e)).count() +} + +/// The constructor CREATEs a child and then REVERTs: the nested CREATE streams through the +/// observer, the sandbox ends as `ExecutionFailed`, and the parent journal receives neither +/// contract. +#[test] +fn test_extreme_revert_after_create_streams_child_then_rolls_back() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_after_create_init()); + let deploy_address = signer.create(0); + let child = deploy_address.create(1); + + for spec in SPECS { + let (result, events) = run_case(spec, &tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |_| {}); + + assert!(result.result.is_success(), "{spec:?}: outer call returns success with errorData"); + assert_eq!( + count(&events, |e| matches!(e, Ev::Create)), + 2, + "{spec:?}: parent + child CREATE" + ); + let ends: Vec<_> = events.iter().filter(|e| matches!(e, Ev::CreateEnd { .. })).collect(); + assert_eq!( + ends[0], + &Ev::CreateEnd { ok: true, address: Some(child) }, + "{spec:?}: the child CREATE completed first and succeeded" + ); + assert!( + matches!(ends[1], Ev::CreateEnd { ok: false, .. }), + "{spec:?}: the parent CREATE ended in revert: {:?}", + ends[1] + ); + assert!( + matches!( + last_end(&events), + SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::ExecutionFailed, + .. + } + ), + "{spec:?}: {:?}", + last_end(&events) + ); + assert!(!has_code(&result, deploy_address), "{spec:?}: parent rolled back"); + assert!(!has_code(&result, child), "{spec:?}: child rolled back with the parent"); + } +} + +/// Three nesting levels, two reverting calls, a log, and a storage write, all inside one +/// sandbox: every frame boundary streams, and all three contracts land in the parent. +#[test] +fn test_extreme_deep_mixed_streams_every_nested_frame() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let parent = signer.create(0); + let child = parent.create(1); + let grandchild = child.create(1); + let setup = |db: &mut MemoryDatabase| { + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + }; + + for spec in SPECS { + let (result, events) = run_case(spec, &tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, setup); + + assert!(result.result.is_success(), "{spec:?}"); + assert_eq!(count(&events, |e| matches!(e, Ev::Create)), 3, "{spec:?}: three CREATE levels"); + assert_eq!( + count(&events, |e| matches!(e, Ev::CreateEnd { ok: true, .. })), + 3, + "{spec:?}: all three CREATEs succeed" + ); + assert_eq!( + count(&events, |e| *e == Ev::Call { target: REVERTER }), + 2, + "{spec:?}: child and parent each CALL the reverter" + ); + assert_eq!( + count(&events, |e| *e == Ev::CallEnd { target: REVERTER, reverted: true }), + 2, + "{spec:?}: both reverter calls report revert through call_end" + ); + assert_eq!(count(&events, |e| matches!(e, Ev::Log)), 1, "{spec:?}: the parent's LOG1"); + assert!(matches!( + last_end(&events), + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } + )); + assert!(has_code(&result, parent), "{spec:?}: parent deployed"); + assert!(has_code(&result, child), "{spec:?}: child deployed"); + assert!(has_code(&result, grandchild), "{spec:?}: grandchild deployed"); + let slot0 = result.state[&parent].storage[&U256::ZERO].present_value(); + assert_eq!( + slot0, + U256::from_be_slice(child.as_slice()), + "{spec:?}: slot 0 holds the child" + ); + } +} + +/// An inner gas limit above the CREATE transaction's intrinsic cost (97,198 here: MegaETH's +/// calldata storage surcharge nearly doubles the standard figure) but below what the +/// constructor's SSTORE and code deposit need: the sandbox halts out of gas, reported as +/// `ExecutionFailed`, nothing deployed. +#[test] +fn test_extreme_sandbox_out_of_gas_is_applied_execution_failed() { + const TIGHT_GAS: u64 = 110_000; + let (tx_bytes, signer) = create_pre_eip155_deploy_tx_with_value_and_gas_limit( + success_constructor(), + U256::ZERO, + TIGHT_GAS, + ); + let deploy_address = signer.create(0); + + for spec in SPECS { + let (result, events) = run_case(spec, &tx_bytes, signer, TIGHT_GAS, |_| {}); + + assert!( + result.result.is_success(), + "{spec:?}: outer call returns success with errorData; got {:?}; events {events:?}", + result.result + ); + assert_eq!(count(&events, |e| matches!(e, Ev::Create)), 1, "{spec:?}"); + assert!( + matches!( + events.iter().find(|e| matches!(e, Ev::CreateEnd { .. })), + Some(Ev::CreateEnd { ok: false, .. }) + ), + "{spec:?}: the CREATE frame halted" + ); + assert!(matches!( + last_end(&events), + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::ExecutionFailed, .. } + )); + assert!(!has_code(&result, deploy_address), "{spec:?}: nothing deployed"); + } +} + +/// A constructor that CALLs `KeylessDeploy` from inside the sandbox: interception is +/// depth-0 only, so the call reaches the contract's bytecode and reverts, and the observer +/// sees that nested call as an ordinary reverted frame. +#[test] +fn test_extreme_nested_keyless_call_reverts_inside_sandbox() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(nested_keyless_call_init()); + let deploy_address = signer.create(0); + let setup = |db: &mut MemoryDatabase| { + db.set_account_code(KEYLESS_DEPLOY_ADDRESS, KEYLESS_DEPLOY_CODE); + }; + + for spec in SPECS { + let (result, events) = run_case(spec, &tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, setup); + + assert!(result.result.is_success(), "{spec:?}"); + assert_eq!( + count(&events, |e| *e == Ev::Call { target: KEYLESS_DEPLOY_ADDRESS }), + 1, + "{spec:?}: the nested call is observed, not intercepted" + ); + assert_eq!( + count(&events, |e| *e == + Ev::CallEnd { target: KEYLESS_DEPLOY_ADDRESS, reverted: true }), + 1, + "{spec:?}: the nested call reverted" + ); + assert!(matches!( + last_end(&events), + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, .. } + )); + assert!(has_code(&result, deploy_address), "{spec:?}: the constructor still deployed"); + } +} diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 25a65d51..2b666729 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -142,11 +142,21 @@ pub(crate) fn constructor_calls_identity_precompile() -> Bytes { pub(crate) fn create_pre_eip155_deploy_tx_with_value( init_code: Bytes, value: U256, +) -> (Bytes, Address) { + create_pre_eip155_deploy_tx_with_value_and_gas_limit(init_code, value, SIGNED_TX_GAS_LIMIT) +} + +/// Like [`create_pre_eip155_deploy_tx_with_value`] with an explicit inner gas limit, for +/// shapes that need the sandbox to run out of gas. +pub(crate) fn create_pre_eip155_deploy_tx_with_value_and_gas_limit( + init_code: Bytes, + value: U256, + gas_limit: u64, ) -> (Bytes, Address) { let tx = TxLegacy { nonce: 0, gas_price: 100_000_000_000, - gas_limit: SIGNED_TX_GAS_LIMIT, + gas_limit, to: TxKind::Create, value, input: init_code, diff --git a/crates/mega-evm/tests/rex2/main.rs b/crates/mega-evm/tests/rex2/main.rs index f0272ed0..76bb726e 100644 --- a/crates/mega-evm/tests/rex2/main.rs +++ b/crates/mega-evm/tests/rex2/main.rs @@ -1,6 +1,7 @@ //! Tests for Rex2 hardfork features. mod keyless_deploy; +mod keyless_sandbox_extreme; mod keyless_sandbox_inspector; mod keyless_sandbox_observer; mod keyless_sandbox_support; From 8adcf03aa87e565f1a58cb0135596b8c2c0f252c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 22:48:47 +0800 Subject: [PATCH 18/28] test: pin caller/creator pranks on the sandbox inspector and the flat trace shape --- bin/mega-evme/src/common/trace.rs | 59 +++++ .../tests/rex2/keyless_sandbox_extreme.rs | 4 +- .../tests/rex2/keyless_sandbox_inspector.rs | 220 +++++++++++++++++- 3 files changed, 279 insertions(+), 4 deletions(-) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 254b116e..52f6cdd8 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -271,6 +271,8 @@ mod tests { use super::*; use alloy_consensus::{transaction::Recovered, Signed, TxLegacy}; use alloy_primitives::{address, hex, Address, Signature, TxKind, B256, U256}; + use alloy_rpc_types_eth::TransactionInfo; + use alloy_rpc_types_trace::parity::{Action, TraceOutput}; use alloy_sol_types::SolCall; use mega_evm::{ alloy_evm::EvmEnv, @@ -733,4 +735,61 @@ mod tests { "synthetic CREATE at 1, constructor at 2, KeylessDeploy code at 3" ); } + + /// The parity rendering (`trace_*` and `debug_traceTransaction` with `flatCallTracer`) of + /// the spliced trace: the sandbox CREATE is trace address `[0]` under the `KeylessDeploy` + /// CALL, every nested frame extends that prefix, and `subtraces` counts match the tree. + #[test] + fn test_keyless_flat_trace_addresses_nest_under_sandbox_create() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let parent = signer.create(0); + let child = parent.create(1); + let grandchild = child.create(1); + let (_frame, outer, _result, _db) = + traced_call_frame(tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |db| { + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + }); + + let traces = outer + .borrow() + .clone() + .into_parity_builder() + .into_localized_transaction_traces(TransactionInfo::default()); + + let shape: Vec<(Vec, &str, Option
, usize, bool)> = traces + .iter() + .map(|t| { + let (kind, to) = match &t.trace.action { + Action::Call(call) => ("call", Some(call.to)), + Action::Create(_) => ( + "create", + match &t.trace.result { + Some(TraceOutput::Create(out)) => Some(out.address), + _ => None, + }, + ), + other => panic!("unexpected action {other:?}"), + }; + ( + t.trace.trace_address.clone(), + kind, + to, + t.trace.subtraces, + t.trace.error.is_some(), + ) + }) + .collect(); + assert_eq!( + shape, + vec![ + (vec![], "call", Some(KEYLESS_DEPLOY_ADDRESS), 1, false), + (vec![0], "create", Some(parent), 2, false), + (vec![0, 0], "create", Some(child), 2, false), + (vec![0, 0, 0], "create", Some(grandchild), 0, false), + (vec![0, 0, 1], "call", Some(REVERTER), 0, true), + (vec![0, 1], "call", Some(REVERTER), 0, true), + ], + "flat traces: {traces:#?}" + ); + } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs index 0116052f..c3a32d03 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs @@ -356,8 +356,8 @@ fn test_extreme_deep_mixed_streams_every_nested_frame() { } } -/// An inner gas limit above the CREATE transaction's intrinsic cost (97,198 here: MegaETH's -/// calldata storage surcharge nearly doubles the standard figure) but below what the +/// An inner gas limit above the CREATE transaction's intrinsic cost (97,198 here: the calldata +/// storage surcharge nearly doubles the standard figure) but below what the /// constructor's SSTORE and code deposit need: the sandbox halts out of gas, reported as /// `ExecutionFailed`, nothing deployed. #[test] diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index 1ef9f8e2..9016321b 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -9,12 +9,16 @@ use mega_evm::{ decode_error_result, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, SandboxInspector, SandboxObserver, SandboxRejectKind, SandboxStartInfo, }, - test_utils::{BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase}, + test_utils::{ + deep_mixed_init, BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase, REVERTING_RUNTIME, + }, EmptyExternalEnv, EvmTxRuntimeLimits, LimitUsage, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, TestExternalEnvs, }; use revm::{ - bytecode::opcode::{CODECOPY, ISZERO, JUMPDEST, JUMPI, MLOAD, RETURN, SSTORE, STATICCALL}, + bytecode::opcode::{ + CALL, CODECOPY, ISZERO, JUMPDEST, JUMPI, MLOAD, MSTORE8, POP, RETURN, SSTORE, STATICCALL, + }, context::{ContextTr, JournalTr}, handler::EvmTr, inspector::NoOpInspector, @@ -37,6 +41,11 @@ use super::keyless_sandbox_support::{ const SUCCESS_TARGET: Address = address!("0000000000000000000000000000000000cccccc"); const OTHER_DEPLOY_ADDRESS: Address = address!("0000000000000000000000000000000000dddddd"); +/// Contract whose runtime stores `msg.sender` in slot 0: `CALLER, PUSH1 0, SSTORE, STOP`. +const CALLER_SINK: Address = address!("0000000000000000000000000000000000511111"); +const CALLER_SINK_RUNTIME: [u8; 5] = [0x33, 0x60, 0x00, 0x55, 0x00]; +/// The account a prank substitutes as `msg.sender` or as the creator of a nested CREATE. +const PRANKED: Address = address!("00000000000000000000000000000000009a9a9a"); const IDENTITY_INPUT: U256 = U256::from_limbs([0x11, 0, 0, 0]); const IDENTITY_OVERRIDE: U256 = U256::from_limbs([0x42, 0, 0, 0]); const JOURNAL_SLOT: U256 = U256::from_limbs([0x53, 0, 0, 0]); @@ -103,6 +112,27 @@ fn constructor_calls_success_target() -> Bytes { Bytes::from(code) } +/// Constructor that CALLs [`CALLER_SINK`] and returns a one-byte `STOP` runtime. +fn constructor_calls_caller_sink() -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(CALLER_SINK) + .push_number(50_000_u32) + .append(CALL) + .append(POP) + .push_number(0_u8) + .push_number(0_u8) + .append(MSTORE8) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + struct InspectorRunConfig<'a, I> { spec: MegaSpecId, db: &'a mut MemoryDatabase, @@ -425,6 +455,60 @@ impl SandboxInspector for JournalWriter { } } +/// Rewrites `msg.sender` of every CALL to `target` (a `prank`) and records the caller that +/// `call_end` is handed for that frame. +struct CallerPrank { + target: Address, + pranked: Address, + call_end_caller: Option
, +} + +impl SandboxInspector for CallerPrank { + fn call( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { + if inputs.target_address == self.target { + inputs.caller = self.pranked; + } + None + } + + fn call_end( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &CallInputs, + _outcome: &mut CallOutcome, + ) { + if inputs.target_address == self.target { + self.call_end_caller = Some(inputs.caller); + } + } +} + +/// Rewrites the creator of every CREATE issued by `creator` to `pranked`, leaving CREATEs +/// issued by anyone else (the sandbox's own top-level CREATE, deeper frames) untouched. +struct CreatorPrank { + creator: Address, + pranked: Address, + rewritten: usize, +} + +impl SandboxInspector for CreatorPrank { + fn create( + &mut self, + _context: &mut mega_evm::MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { + if inputs.caller == self.creator { + inputs.caller = self.pranked; + self.rewritten += 1; + } + None + } +} + fn slot_value(result: &ResultAndState, addr: Address, slot: U256) -> Option { result .state @@ -1318,3 +1402,135 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { } assert_parent_did_not_receive_sandbox_apply(&mismatch, signer, deploy_address, spec); } + +/// Runs `tx_bytes` with `inspector` attached, after `setup` has seeded the funded database. +fn run_pranked( + spec: MegaSpecId, + tx_bytes: &Bytes, + signer: Address, + inspector: Rc>, + setup: impl Fn(&mut MemoryDatabase), +) -> ResultAndState +where + I: SandboxInspector + 'static, +{ + let mut db = funded_db(signer); + setup(&mut db); + run_keyless_inspector(InspectorRunConfig { + spec, + db: &mut db, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(inspector), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }) +} + +fn address_word(address: Address) -> U256 { + U256::from_be_slice(address.as_slice()) +} + +/// A `call` hook that rewrites `inputs.caller` — a Foundry-style `prank`. The callee sees the +/// pranked `msg.sender`, `call_end` is handed the rewritten inputs, and the rest of the +/// deployment (address, signer accounting, apply) matches the unpranked control arm. +#[test] +fn test_inspector_call_prank_rewrites_msg_sender() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_calls_caller_sink()); + let deploy_address = signer.create(0); + let setup = |db: &mut MemoryDatabase| { + db.set_account_code(CALLER_SINK, Bytes::from_static(&CALLER_SINK_RUNTIME)); + }; + + for spec in SPECS { + let control = + run_pranked(spec, &tx_bytes, signer, Rc::new(RefCell::new(NopSandboxInspector)), setup); + assert_eq!( + slot_value(&control, CALLER_SINK, U256::ZERO), + Some(address_word(deploy_address)), + "{spec:?}: control arm: the constructor is the callee's msg.sender" + ); + assert!(account_has_code(&control, deploy_address), "{spec:?}: control arm deploys"); + assert_control_signer_applied(&control, signer, spec); + + let prank = Rc::new(RefCell::new(CallerPrank { + target: CALLER_SINK, + pranked: PRANKED, + call_end_caller: None, + })); + let pranked = run_pranked(spec, &tx_bytes, signer, Rc::clone(&prank), setup); + assert_eq!( + slot_value(&pranked, CALLER_SINK, U256::ZERO), + Some(address_word(PRANKED)), + "{spec:?}: the callee observed the pranked msg.sender" + ); + assert_eq!( + prank.borrow().call_end_caller, + Some(PRANKED), + "{spec:?}: call_end receives the rewritten inputs" + ); + assert!(account_has_code(&pranked, deploy_address), "{spec:?}: prank arm still deploys"); + assert_control_signer_applied(&pranked, signer, spec); + assert_eq!( + pranked.result.gas_used(), + control.result.gas_used(), + "{spec:?}: rewriting the caller does not change the outer gas" + ); + } +} + +/// A `create` hook that rewrites the creator of the nested CREATE: the child lands at the +/// pranked account's address and consumes that account's nonce instead of the parent's, the +/// parent stores the moved address, and CREATEs issued by other frames (the sandbox's own +/// top-level CREATE, the grandchild) are untouched. +#[test] +fn test_inspector_create_prank_moves_nested_child_to_pranked_creator() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let parent = signer.create(0); + let control_child = parent.create(1); + let pranked_child = PRANKED.create(0); + let grandchild = pranked_child.create(1); + let setup = |db: &mut MemoryDatabase| { + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + }; + + for spec in SPECS { + let control = + run_pranked(spec, &tx_bytes, signer, Rc::new(RefCell::new(NopSandboxInspector)), setup); + assert!(account_has_code(&control, parent), "{spec:?}: control arm deploys the parent"); + assert!(account_has_code(&control, control_child), "{spec:?}: control child address"); + assert_eq!( + slot_value(&control, parent, U256::ZERO), + Some(address_word(control_child)), + "{spec:?}: control arm stores the child at parent.create(1)" + ); + assert_eq!(control.state[&parent].info.nonce, 2, "{spec:?}: the parent created once"); + assert!(!control.state.contains_key(&PRANKED), "{spec:?}: control arm never touches it"); + + let prank = + Rc::new(RefCell::new(CreatorPrank { creator: parent, pranked: PRANKED, rewritten: 0 })); + let pranked = run_pranked(spec, &tx_bytes, signer, Rc::clone(&prank), setup); + assert_eq!(prank.borrow().rewritten, 1, "{spec:?}: only the parent's CREATE is rewritten"); + assert!(account_has_code(&pranked, parent), "{spec:?}: prank arm deploys the parent"); + assert!(account_has_code(&pranked, pranked_child), "{spec:?}: child moved to PRANKED"); + assert!(!account_has_code(&pranked, control_child), "{spec:?}: nothing at the old slot"); + assert!( + account_has_code(&pranked, grandchild), + "{spec:?}: grandchild under the moved child" + ); + assert_eq!( + slot_value(&pranked, parent, U256::ZERO), + Some(address_word(pranked_child)), + "{spec:?}: the constructor stored the moved address" + ); + assert_eq!( + pranked.state[&parent].info.nonce, 1, + "{spec:?}: the parent's nonce not consumed" + ); + assert_eq!( + pranked.state[&PRANKED].info.nonce, 1, + "{spec:?}: the pranked creator's nonce is" + ); + assert_control_signer_applied(&pranked, signer, spec); + } +} From 6e5a1a468587520b5bc44b1da95dcf2212e2d4bf Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:01:26 +0800 Subject: [PATCH 19/28] refactor(sandbox): collapse the keyless hook channels onto one inspector slot --- bin/mega-evme/src/common/trace.rs | 4 +- crates/mega-evm/src/block/executor.rs | 27 +-- crates/mega-evm/src/evm/AGENTS.md | 3 +- crates/mega-evm/src/evm/context.rs | 73 ++----- crates/mega-evm/src/evm/mod.rs | 27 +-- crates/mega-evm/src/sandbox/execution.rs | 36 +--- crates/mega-evm/src/sandbox/hooks.rs | 197 ------------------ crates/mega-evm/src/sandbox/inspector.rs | 53 +---- crates/mega-evm/src/sandbox/mod.rs | 9 +- crates/mega-evm/src/sandbox/observer.rs | 130 ++++++------ crates/mega-evm/src/sandbox/trace.rs | 5 +- .../tests/rex2/keyless_sandbox_inspector.rs | 41 ---- .../tests/rex2/keyless_sandbox_observer.rs | 37 +--- 13 files changed, 122 insertions(+), 520 deletions(-) delete mode 100644 crates/mega-evm/src/sandbox/hooks.rs diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 52f6cdd8..d42d1801 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -756,7 +756,9 @@ mod tests { .into_parity_builder() .into_localized_transaction_traces(TransactionInfo::default()); - let shape: Vec<(Vec, &str, Option
, usize, bool)> = traces + /// `(trace_address, action kind, target, subtraces, errored)` of one flat entry. + type FlatEntry = (Vec, &'static str, Option
, usize, bool); + let shape: Vec = traces .iter() .map(|t| { let (kind, to) = match &t.trace.action { diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index ff269c5e..0feec5d4 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -163,23 +163,11 @@ where O: crate::sandbox::SandboxObserver + crate::sandbox::SandboxObserver + 'static, + ExtEnvs: 'static, { self.evm.set_keyless_sandbox_observer(observer); } - /// Attaches an observer that implements [`crate::sandbox::SandboxObserver`] - /// only for this executor's `ExtEnvs`. - /// - /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer_for_parent_env`]. - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - self.evm.set_keyless_sandbox_observer_for_parent_env(observer); - } - /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_inspector`]. The inspector @@ -195,19 +183,6 @@ where self.evm.set_keyless_sandbox_inspector(inspector); } - /// Attaches an inspector that implements [`crate::sandbox::SandboxInspector`] - /// only for this executor's `ExtEnvs`. - /// - /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_inspector_for_parent_env`]. - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - self.evm.set_keyless_sandbox_inspector_for_parent_env(inspector); - } - /// Detaches any sandbox hook from both env-type slots. /// /// Forwards to [`crate::MegaEvm::clear_keyless_sandbox_hook`]. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 228f1b84..eaf3418b 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -26,7 +26,8 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, Two exclusive channels into nested keyless-deploy sandbox execution, attached on `MegaContext` (and forwarded from `MegaEvm` / `MegaBlockExecutor`). Read-only default: `SandboxObserver` cannot short-circuit `CALL`/`CREATE` and must not mutate interpreter or context state. Rewriting explicit: `SandboxInspector` forwards `&mut` inputs and override return values so interventions take effect inside the sandbox as they would on a top-level EVM. -`sandbox::KeylessSandboxHooks` restates the setters as a trait so a host held behind a generic EVM projection (a node's `ConfigureEvm::Evm`) can attach a hook through a bound. +Both channels share one type-erased slot (`Rc>>`, held twice: parent env type and `EmptyExternalEnv`); an observer is installed behind the crate-private `ReadOnlyHook` adapter, which forwards shared references and never answers a `CALL`/`CREATE` override. +Types generic over revm's `Inspector` get both channels through blanket impls; hosts behind a generic EVM projection (a node's `ConfigureEvm::Evm`) name the attach operation on their own configuration type instead of on mega-evm. With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` handle for the outer EVM plus a second one on the observer channel, and `splice_sandbox_traces` to graft the sandbox call tree under the intercepted `KeylessDeploy` CALL after execution; `mega-evme` and node tracing RPCs use the same implementation. Contract: diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index d2cfc1d6..f7fa039a 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -30,7 +30,7 @@ use revm::{ use crate::{ constants, is_system_originated, - sandbox::{SandboxHook, SandboxInspector, SandboxObserver}, + sandbox::{ReadOnlyHook, SandboxHookHandle, SandboxInspector, SandboxObserver}, AdditionalLimit, BucketId, DynamicGasCost, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvTypes, ExternalEnvs, MegaSpecId, TxRuntimeLimit, VolatileDataAccess, VolatileDataAccessTracker, VolatileDataAccessType, @@ -84,18 +84,19 @@ pub struct MegaContext { /// Changing `ExtEnvs` via [`Self::with_external_envs`] resets this field /// and the [`EmptyExternalEnv`] hook slot: the hook cannot be carried across /// an env-type change and must be attached after external environments are - /// assembled. Observer and inspector occupy the same slot exclusively. + /// assembled. Observer and inspector occupy the same slot exclusively; an + /// observer sits behind a read-only adapter. #[debug(ignore)] - pub(crate) keyless_sandbox_hook: Option>, + pub(crate) keyless_sandbox_hook: Option>, /// Hook handle typed against [`EmptyExternalEnv`]. /// /// Pre-REX4 sandboxes always execute with [`EmptyExternalEnv`]. Opcode-level /// hooks on that path use this slot so attaching a hook cannot change - /// sandbox env semantics. `None` when no hook is attached, or when the - /// caller used a `_for_parent_env` setter. + /// sandbox env semantics. Set and cleared together with + /// [`Self::keyless_sandbox_hook`]. #[debug(ignore)] - pub(crate) keyless_sandbox_hook_empty: Option>, + pub(crate) keyless_sandbox_hook_empty: Option>, /// The system address for the current block. /// Pre-REX5: always `MEGA_SYSTEM_ADDRESS` (the legacy hardcoded constant). @@ -496,11 +497,12 @@ impl MegaContext { /// Attaches an observer for nested sandbox execution on every spec. /// /// The observer must implement [`SandboxObserver`] for both this context's - /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored as two - /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes - /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent - /// env). [`crate::sandbox::InspectorSandboxObserver`] with a fully generic - /// inspector satisfies both bounds. + /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored, behind a + /// read-only adapter, as two type-erased slots so opcode-level hooks fire + /// for pre-REX4 sandboxes (always [`EmptyExternalEnv`]) and for REX4+ + /// sandboxes (shared parent env). A type that implements + /// [`revm::Inspector`] for every sandbox context lifetime satisfies both + /// bounds via the blanket impl. /// /// Attaching an observer does not change sandbox external-env semantics at /// any spec. Replaces any attached inspector. @@ -513,37 +515,20 @@ impl MegaContext { pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: SandboxObserver + SandboxObserver + 'static, + ExtEnvs: 'static, { match observer { Some(obs) => { let cloned = Rc::clone(&obs); let parent: Rc>> = cloned; let empty: Rc>> = obs; - self.keyless_sandbox_hook = Some(SandboxHook::Observer(parent)); - self.keyless_sandbox_hook_empty = Some(SandboxHook::Observer(empty)); + self.keyless_sandbox_hook = Some(ReadOnlyHook::handle(parent)); + self.keyless_sandbox_hook_empty = Some(ReadOnlyHook::handle(empty)); } None => self.clear_keyless_sandbox_hook(), } } - /// Attaches an observer that implements [`SandboxObserver`] only for this - /// context's `ExtEnvs`. - /// - /// Opcode-level observation is available on REX4+ sandboxes, which share - /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit - /// only `sandbox_start` / `sandbox_end` through this handle. - /// - /// Prefer [`Self::set_keyless_sandbox_observer`] when the observer - /// implements both env types. Use [`Self::clear_keyless_sandbox_hook`] - /// to detach. Replaces any attached inspector. - pub fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - self.keyless_sandbox_hook = observer.map(SandboxHook::Observer); - self.keyless_sandbox_hook_empty = None; - } - /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// /// The inspector must implement [`SandboxInspector`] for both this context's @@ -568,33 +553,15 @@ impl MegaContext { match inspector { Some(insp) => { let cloned = Rc::clone(&insp); - let parent: Rc>> = cloned; - let empty: Rc>> = insp; - self.keyless_sandbox_hook = Some(SandboxHook::Inspector(parent)); - self.keyless_sandbox_hook_empty = Some(SandboxHook::Inspector(empty)); + let parent: SandboxHookHandle = cloned; + let empty: SandboxHookHandle = insp; + self.keyless_sandbox_hook = Some(parent); + self.keyless_sandbox_hook_empty = Some(empty); } None => self.clear_keyless_sandbox_hook(), } } - /// Attaches an inspector that implements [`SandboxInspector`] only for this - /// context's `ExtEnvs`. - /// - /// Opcode-level inspection is available on REX4+ sandboxes, which share - /// the parent env. Pre-REX4 sandboxes keep [`EmptyExternalEnv`] and emit - /// only `sandbox_start` / `sandbox_end` through this handle. - /// - /// Prefer [`Self::set_keyless_sandbox_inspector`] when the inspector - /// implements both env types. Use [`Self::clear_keyless_sandbox_hook`] - /// to detach. Replaces any attached observer. - pub fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - self.keyless_sandbox_hook = inspector.map(SandboxHook::Inspector); - self.keyless_sandbox_hook_empty = None; - } - /// Detaches any sandbox hook from both env-type slots. /// /// Restores the no-hook sandbox path. Prefer this over passing `None` to diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 56380679..b3c6cb5e 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -246,23 +246,11 @@ impl MegaEvm { pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) where O: SandboxObserver + SandboxObserver + 'static, + ExtEnvs: 'static, { self.inner.ctx.set_keyless_sandbox_observer(observer); } - /// Attaches an observer that implements [`SandboxObserver`] only for this - /// EVM's `ExtEnvs`. - /// - /// Forwards to [`MegaContext::set_keyless_sandbox_observer_for_parent_env`]. - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - self.inner.ctx.set_keyless_sandbox_observer_for_parent_env(observer); - } - /// Attaches a rewriting inspector for nested sandbox execution on every spec. /// /// Forwards to [`MegaContext::set_keyless_sandbox_inspector`]. The inspector @@ -275,19 +263,6 @@ impl MegaEvm { self.inner.ctx.set_keyless_sandbox_inspector(inspector); } - /// Attaches an inspector that implements [`SandboxInspector`] only for this - /// EVM's `ExtEnvs`. - /// - /// Forwards to [`MegaContext::set_keyless_sandbox_inspector_for_parent_env`]. - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end`. - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - self.inner.ctx.set_keyless_sandbox_inspector_for_parent_env(inspector); - } - /// Detaches any sandbox hook from both env-type slots. /// /// Forwards to [`MegaContext::clear_keyless_sandbox_hook`]. diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index dc1bcf2c..8e33c2b6 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -68,11 +68,8 @@ use super::{ use super::{ error::{encode_error_result, KeylessDeployError}, - inspector::{InspectorBridge, SandboxHook}, - observer::{ - ObserverBridge, SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, - SandboxStartInfo, - }, + inspector::{InspectorBridge, SandboxHookHandle}, + observer::{SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, SandboxStartInfo}, state::SandboxDb, }; @@ -448,7 +445,7 @@ pub fn execute_keyless_deploy_call // fire exactly once on this path, covering every terminal arm below. let hook = ctx.keyless_sandbox_hook.clone(); if let Some(h) = &hook { - h.notify_start(&SandboxStartInfo { + h.borrow_mut().sandbox_start(&SandboxStartInfo { spec: ctx.spec, signer: deploy_signer, deploy_address, @@ -709,7 +706,7 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( sandbox_tx_limits: Option, block: BlockEnv, chain: L1BlockInfo, - hook: Option>, + hook: Option>, ) -> SandboxOutcome { let sandbox_ctx = match sandbox_tx_limits { Some(limits) => sandbox_ctx.with_tx_runtime_limits(limits), @@ -719,24 +716,9 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( let is_rex5_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX5); let is_rex6_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX6); match hook { - Some(SandboxHook::Observer(observer)) => { - let mut sandbox_evm = - MegaEvm::new(sandbox_ctx).with_inspector(ObserverBridge::new(observer)); - let result = sandbox_evm.transact_raw(sandbox_tx); - let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); - let volatile_accesses = - sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); - process_sandbox_transact_result( - result, - limit_usage, - volatile_accesses, - is_rex5_enabled, - is_rex6_enabled, - ) - } - Some(SandboxHook::Inspector(inspector)) => { + Some(hook) => { let mut sandbox_evm = - MegaEvm::new(sandbox_ctx).with_inspector(InspectorBridge::new(inspector)); + MegaEvm::new(sandbox_ctx).with_inspector(InspectorBridge::new(hook)); let result = sandbox_evm.transact_raw(sandbox_tx); let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); let volatile_accesses = @@ -770,11 +752,11 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( /// Delivers `sandbox_end` when a hook is attached. fn notify_sandbox_end( - hook: &Option>, + hook: &Option>, outcome: SandboxEndOutcome, ) { if let Some(h) = hook { - h.notify_end(&outcome); + h.borrow_mut().sandbox_end(&outcome); } } @@ -1848,7 +1830,7 @@ mod tests { Some(limits), block, chain, - observer.map(SandboxHook::Observer), + observer.map(crate::sandbox::ReadOnlyHook::handle), ) } diff --git a/crates/mega-evm/src/sandbox/hooks.rs b/crates/mega-evm/src/sandbox/hooks.rs deleted file mode 100644 index 97a8a127..00000000 --- a/crates/mega-evm/src/sandbox/hooks.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Trait view of the keyless sandbox hook setters. -//! -//! [`crate::MegaContext`], [`crate::MegaEvm`], and [`crate::MegaBlockExecutor`] each expose the -//! same setters as inherent methods. Callers that only hold the EVM behind a generic (for -//! example a node's `ConfigureEvm::Evm` projection) cannot name those inherent methods, so -//! [`KeylessSandboxHooks`] restates them as a trait bound. - -#[cfg(not(feature = "std"))] -use alloc as std; -use core::cell::RefCell; -use std::rc::Rc; - -use alloy_evm::Database; -use alloy_op_evm::block::receipt_builder::OpReceiptBuilder; - -use super::{SandboxInspector, SandboxObserver}; -use crate::{EmptyExternalEnv, ExternalEnvTypes, MegaBlockExecutor, MegaContext, MegaEvm}; - -/// Attaches or detaches the keyless sandbox hook on a host that runs sandboxes. -/// -/// Both channels share one slot: attaching either replaces the other. See -/// [`SandboxObserver`] for the read-only channel and [`SandboxInspector`] for the rewriting -/// channel. -pub trait KeylessSandboxHooks { - /// External environment types the host's parent context is typed against. - type ExtEnvs: ExternalEnvTypes; - - /// Attaches a read-only observer on every spec. - fn set_keyless_sandbox_observer(&mut self, observer: Option>>) - where - O: SandboxObserver + SandboxObserver + 'static; - - /// Attaches a read-only observer that only implements the parent env type. - /// - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end` through this handle. - fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ); - - /// Attaches a rewriting inspector on every spec. - fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) - where - I: SandboxInspector + SandboxInspector + 'static; - - /// Attaches a rewriting inspector that only implements the parent env type. - /// - /// Pre-REX4 sandboxes emit only `sandbox_start` / `sandbox_end` through this handle. - fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ); - - /// Detaches any sandbox hook from both env-type slots. - fn clear_keyless_sandbox_hook(&mut self); -} - -impl KeylessSandboxHooks for MegaContext { - type ExtEnvs = ExtEnvs; - - fn set_keyless_sandbox_observer(&mut self, observer: Option>>) - where - O: SandboxObserver + SandboxObserver + 'static, - { - MegaContext::set_keyless_sandbox_observer(self, observer); - } - - fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - MegaContext::set_keyless_sandbox_observer_for_parent_env(self, observer); - } - - fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) - where - I: SandboxInspector + SandboxInspector + 'static, - { - MegaContext::set_keyless_sandbox_inspector(self, inspector); - } - - fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - MegaContext::set_keyless_sandbox_inspector_for_parent_env(self, inspector); - } - - fn clear_keyless_sandbox_hook(&mut self) { - MegaContext::clear_keyless_sandbox_hook(self); - } -} - -impl KeylessSandboxHooks - for MegaEvm -{ - type ExtEnvs = ExtEnvs; - - fn set_keyless_sandbox_observer(&mut self, observer: Option>>) - where - O: SandboxObserver + SandboxObserver + 'static, - { - MegaEvm::set_keyless_sandbox_observer(self, observer); - } - - fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - MegaEvm::set_keyless_sandbox_observer_for_parent_env(self, observer); - } - - fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) - where - I: SandboxInspector + SandboxInspector + 'static, - { - MegaEvm::set_keyless_sandbox_inspector(self, inspector); - } - - fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - MegaEvm::set_keyless_sandbox_inspector_for_parent_env(self, inspector); - } - - fn clear_keyless_sandbox_hook(&mut self) { - MegaEvm::clear_keyless_sandbox_hook(self); - } -} - -impl KeylessSandboxHooks - for MegaBlockExecutor -{ - type ExtEnvs = E::ExtEnvs; - - fn set_keyless_sandbox_observer(&mut self, observer: Option>>) - where - O: SandboxObserver + SandboxObserver + 'static, - { - self.evm.set_keyless_sandbox_observer(observer); - } - - fn set_keyless_sandbox_observer_for_parent_env( - &mut self, - observer: Option>>>, - ) { - self.evm.set_keyless_sandbox_observer_for_parent_env(observer); - } - - fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) - where - I: SandboxInspector + SandboxInspector + 'static, - { - self.evm.set_keyless_sandbox_inspector(inspector); - } - - fn set_keyless_sandbox_inspector_for_parent_env( - &mut self, - inspector: Option>>>, - ) { - self.evm.set_keyless_sandbox_inspector_for_parent_env(inspector); - } - - fn clear_keyless_sandbox_hook(&mut self) { - self.evm.clear_keyless_sandbox_hook(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{test_utils::MemoryDatabase, MegaSpecId}; - - struct NopObserver; - - impl SandboxObserver for NopObserver {} - - fn attach_through_trait(host: &mut T) { - host.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); - host.clear_keyless_sandbox_hook(); - } - - #[test] - fn test_hooks_trait_reaches_context_and_evm() { - let mut db = MemoryDatabase::default(); - let mut ctx = MegaContext::new(&mut db, MegaSpecId::REX6); - attach_through_trait(&mut ctx); - - let mut evm = MegaEvm::new(ctx); - attach_through_trait(&mut evm); - evm.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); - assert!(evm.ctx.keyless_sandbox_hook.is_some()); - KeylessSandboxHooks::clear_keyless_sandbox_hook(&mut evm); - assert!(evm.ctx.keyless_sandbox_hook.is_none()); - } -} diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs index 1103f5ec..4b1ac394 100644 --- a/crates/mega-evm/src/sandbox/inspector.rs +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -4,7 +4,8 @@ //! Hook signatures match revm's [`Inspector`] on the sandbox EVM: `&mut` inputs and //! override return values are forwarded, so `CALL`/`CREATE` can be short-circuited and //! outcomes rewritten. [`InspectorBridge`] installs the handle as the sandbox EVM's -//! inspector. Attaching an inspector is exclusive with attaching an observer. +//! inspector. Both channels occupy the same type-erased slot: an observer is installed behind +//! a read-only adapter, so attaching either replaces the other. //! //! # Contract //! @@ -40,7 +41,7 @@ use revm::{ use crate::{ExternalEnvTypes, MegaContext}; use super::{ - observer::{SandboxEndOutcome, SandboxObserver, SandboxStartInfo}, + observer::{SandboxEndOutcome, SandboxStartInfo}, state::SandboxDb, }; @@ -368,51 +369,11 @@ impl Inspector, E>, EthInterprete } } -/// Exclusive hook slot for nested sandbox execution. +/// The type-erased hook slot for nested sandbox execution. /// -/// Either channel may occupy a slot; attaching one replaces the other. -pub(crate) enum SandboxHook { - /// Read-only observer channel. - Observer(Rc>>), - /// Rewriting inspector channel. - Inspector(Rc>>), -} - -impl Clone for SandboxHook { - fn clone(&self) -> Self { - match self { - Self::Observer(observer) => Self::Observer(Rc::clone(observer)), - Self::Inspector(inspector) => Self::Inspector(Rc::clone(inspector)), - } - } -} - -impl SandboxHook { - /// Delivers `sandbox_start` to the attached channel. - pub(crate) fn notify_start(&self, info: &SandboxStartInfo) { - match self { - Self::Observer(observer) => observer.borrow_mut().sandbox_start(info), - Self::Inspector(inspector) => inspector.borrow_mut().sandbox_start(info), - } - } - - /// Delivers `sandbox_end` to the attached channel. - pub(crate) fn notify_end(&self, outcome: &SandboxEndOutcome) { - match self { - Self::Observer(observer) => observer.borrow_mut().sandbox_end(outcome), - Self::Inspector(inspector) => inspector.borrow_mut().sandbox_end(outcome), - } - } -} - -impl core::fmt::Debug for SandboxHook { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Observer(_) => f.debug_tuple("Observer").finish_non_exhaustive(), - Self::Inspector(_) => f.debug_tuple("Inspector").finish_non_exhaustive(), - } - } -} +/// Both channels share it: an observer is installed behind +/// [`super::observer::ReadOnlyHook`], an inspector directly. Attaching one replaces the other. +pub(crate) type SandboxHookHandle = Rc>>; #[cfg(test)] mod tests { diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 1798d31d..e2e6cd4d 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -61,7 +61,6 @@ //! [`execute_keyless_deploy_call`] //! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution //! - `inspector` - Rewriting [`SandboxInspector`] channel into nested sandbox execution -//! - `hooks` - [`KeylessSandboxHooks`], the trait view of the hook setters for generic hosts //! - `trace` - Shared `revm-inspectors` splicing helpers (`inspectors` feature) //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal @@ -92,9 +91,9 @@ //! always run with [`crate::EmptyExternalEnv`] (minimum bucket capacity, no oracle //! data), and REX4+ sandboxes always share the parent env. Opcode-level hooks on //! pre-REX4 are delivered through a second slot typed against -//! [`crate::EmptyExternalEnv`]. Handles that only implement the parent env type can use -//! the `_for_parent_env` setters; pre-REX4 then emits only `sandbox_start` / -//! `sandbox_end`. +//! [`crate::EmptyExternalEnv`], so a hook implements its trait for both the parent env +//! type and [`crate::EmptyExternalEnv`]; revm inspectors that are generic over the +//! context satisfy both through the blanket impls. //! //! [`SandboxObserver`]: observer::SandboxObserver //! [`SandboxInspector`]: inspector::SandboxInspector @@ -131,7 +130,6 @@ mod error; mod execution; -mod hooks; mod inspector; mod observer; mod state; @@ -142,7 +140,6 @@ mod tx; pub use error::*; pub use execution::*; -pub use hooks::*; pub use inspector::*; pub use observer::*; pub use state::*; diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 5d502fb5..8d4b2b2d 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -9,18 +9,18 @@ //! A [`SandboxObserver`] sees every interpreter hook that revm's [`Inspector`] //! would see inside a sandbox, plus a paired [`SandboxObserver::sandbox_start`] / //! [`SandboxObserver::sandbox_end`] lifecycle. Observation cannot short-circuit -//! `CALL`/`CREATE`: those hooks have no return value, and [`ObserverBridge`] -//! always forwards `None` to the EVM. +//! `CALL`/`CREATE`: those hooks have no return value, and the sandbox installs +//! the observer behind `ReadOnlyHook`, which always answers `None` to the EVM. //! //! # Read-only contract //! //! Two layers close intervention: //! //! - **Structural.** [`SandboxObserver::call`] / [`SandboxObserver::create`] take shared references -//! to [`CallInputs`] / [`CreateInputs`]. [`InspectorSandboxObserver`] clones a temporary copy -//! when forwarding to an inner [`Inspector`]; mutations of that copy are discarded, as are `Some` -//! override outcomes. Combined with [`ObserverBridge`] always returning `None`, rewriting inputs -//! and short-circuiting the frame are both closed. +//! to [`CallInputs`] / [`CreateInputs`]. The blanket impl for revm [`Inspector`]s clones a +//! temporary copy when forwarding; mutations of that copy are discarded, as are `Some` override +//! outcomes. Combined with `ReadOnlyHook` always returning `None`, rewriting inputs and +//! short-circuiting the frame are both closed. //! - **Contractual.** [`SandboxObserver::step`] / [`SandboxObserver::step_end`] take `&mut //! Interpreter` and hooks take `&mut MegaContext` because those types are not cheaply cloneable. //! Implementations must not mutate them. Debug builds already trip conservation asserts on many @@ -43,15 +43,12 @@ //! //! Attaching an observer must not change sandbox env semantics at any spec. //! Pre-REX4 sandboxes always run with [`crate::EmptyExternalEnv`]; REX4+ -//! sandboxes always share the parent env. Callers that implement +//! sandboxes always share the parent env. An observer therefore implements //! [`SandboxObserver`] for both the parent env type and -//! [`crate::EmptyExternalEnv`] attach via -//! [`crate::MegaContext::set_keyless_sandbox_observer`], which stores two -//! type-erased handles so opcode-level hooks fire on both paths. -//! [`InspectorSandboxObserver`] with a fully generic inspector satisfies both -//! bounds. An observer that only implements the parent env type can attach -//! via [`crate::MegaContext::set_keyless_sandbox_observer_for_parent_env`]; -//! pre-REX4 then emits only `sandbox_start` / `sandbox_end`. +//! [`crate::EmptyExternalEnv`]; [`crate::MegaContext::set_keyless_sandbox_observer`] +//! stores two type-erased handles so opcode-level hooks fire on both paths. A +//! type that implements revm's [`Inspector`] for every sandbox context satisfies +//! both bounds through the blanket impl. #[cfg(not(feature = "std"))] use alloc as std; @@ -69,7 +66,10 @@ use revm::{ use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; -use super::state::SandboxDb; +use super::{ + inspector::{SandboxHookHandle, SandboxInspector}, + state::SandboxDb, +}; /// Context available when a sandbox is about to execute. #[non_exhaustive] @@ -154,8 +154,13 @@ impl SandboxEndOutcome { /// A compliant (read-only) observer does not change sandbox execution results. /// [`call`](Self::call) / [`create`](Self::create) cannot rewrite inputs or /// override the frame; [`step`](Self::step) and context arguments remain -/// contractually read-only. [`ObserverBridge`] always drops `call`/`create` -/// override outcomes. +/// contractually read-only. `ReadOnlyHook` never answers a `call`/`create` +/// override on the observer's behalf. +/// +/// Types that already implement [`Inspector`] for every sandbox context +/// lifetime receive a blanket impl that forwards each hook on a temporary +/// copy of the inputs. Local types that are not inspectors implement this +/// trait by hand. pub trait SandboxObserver { /// Called once after the sandbox interpreter is created, before the first opcode. #[inline] @@ -269,20 +274,7 @@ pub trait SandboxObserver { } } -/// Adapts any HRTB-compatible revm [`Inspector`] into a [`SandboxObserver`]. -/// -/// `call`/`create` clone a temporary input copy for the inner inspector; that -/// copy and any override outcome are discarded. [`SandboxObserver::sandbox_start`] -/// and [`SandboxObserver::sandbox_end`] keep their default empty implementations. -pub struct InspectorSandboxObserver(pub I); - -impl core::fmt::Debug for InspectorSandboxObserver { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("InspectorSandboxObserver").finish_non_exhaustive() - } -} - -impl SandboxObserver for InspectorSandboxObserver +impl SandboxObserver for I where E: ExternalEnvTypes, I: for<'a> Inspector, E>, EthInterpreter>, @@ -293,7 +285,7 @@ where interp: &mut Interpreter, context: &mut MegaContext, E>, ) { - self.0.initialize_interp(interp, context); + Inspector::initialize_interp(self, interp, context); } #[inline] @@ -302,7 +294,7 @@ where interp: &mut Interpreter, context: &mut MegaContext, E>, ) { - self.0.step(interp, context); + Inspector::step(self, interp, context); } #[inline] @@ -311,7 +303,7 @@ where interp: &mut Interpreter, context: &mut MegaContext, E>, ) { - self.0.step_end(interp, context); + Inspector::step_end(self, interp, context); } #[inline] @@ -321,13 +313,14 @@ where context: &mut MegaContext, E>, log: Log, ) { - self.0.log(interp, context, log); + Inspector::log(self, interp, context, log); } #[inline] fn call(&mut self, context: &mut MegaContext, E>, inputs: &CallInputs) { + // The inspector works on a copy: its mutations and any override are discarded. let mut inputs = inputs.clone(); - let _ = self.0.call(context, &mut inputs); + let _ = Inspector::call(self, context, &mut inputs); } #[inline] @@ -337,15 +330,15 @@ where inputs: &CallInputs, outcome: &CallOutcome, ) { - // Clone so a mutating inner inspector cannot write back into execution. + // Clone so a mutating inspector cannot write back into execution. let mut outcome = outcome.clone(); - self.0.call_end(context, inputs, &mut outcome); + Inspector::call_end(self, context, inputs, &mut outcome); } #[inline] fn create(&mut self, context: &mut MegaContext, E>, inputs: &CreateInputs) { let mut inputs = inputs.clone(); - let _ = self.0.create(context, &mut inputs); + let _ = Inspector::create(self, context, &mut inputs); } #[inline] @@ -355,41 +348,40 @@ where inputs: &CreateInputs, outcome: &CreateOutcome, ) { - // Clone so a mutating inner inspector cannot write back into execution. let mut outcome = outcome.clone(); - self.0.create_end(context, inputs, &mut outcome); + Inspector::create_end(self, context, inputs, &mut outcome); } #[inline] fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.0.selfdestruct(contract, target, value); + Inspector::selfdestruct(self, contract, target, value); } } -/// Inspector that forwards sandbox frames to a [`SandboxObserver`]. +/// Installs a [`SandboxObserver`] on the sandbox's hook slot without a way to intervene. /// -/// `call` and `create` always return `None`, so the observer has no channel -/// that can override execution. -pub(crate) struct ObserverBridge { +/// Every hook forwards a shared reference to the observer, `call` / `create` always answer +/// `None`, and `call_end` / `create_end` hand their outcomes over read-only, so the observer +/// channel is structurally unable to rewrite execution even though it shares the slot with +/// [`SandboxInspector`]. +pub(crate) struct ReadOnlyHook { observer: Rc>>, } -impl core::fmt::Debug for ObserverBridge { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("ObserverBridge").finish_non_exhaustive() +impl ReadOnlyHook { + /// Wraps a shared observer handle as a type-erased hook handle. + pub(crate) fn handle(observer: Rc>>) -> SandboxHookHandle { + Rc::new(RefCell::new(Self { observer })) } } -impl ObserverBridge { - /// Wraps a shared observer handle as a revm inspector. - pub(crate) fn new(observer: Rc>>) -> Self { - Self { observer } +impl core::fmt::Debug for ReadOnlyHook { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ReadOnlyHook").finish_non_exhaustive() } } -impl Inspector, E>, EthInterpreter> - for ObserverBridge -{ +impl SandboxInspector for ReadOnlyHook { #[inline] fn initialize_interp( &mut self, @@ -471,6 +463,16 @@ impl Inspector, E>, EthInterprete fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { self.observer.borrow_mut().selfdestruct(contract, target, value); } + + #[inline] + fn sandbox_start(&mut self, info: &SandboxStartInfo) { + self.observer.borrow_mut().sandbox_start(info); + } + + #[inline] + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + self.observer.borrow_mut().sandbox_end(outcome); + } } #[cfg(test)] @@ -505,19 +507,27 @@ mod tests { } #[test] - fn test_inspector_adapter_accepts_generic_inspector() { + fn test_generic_inspector_is_a_sandbox_observer() { fn assert_observer>(_: T) {} - assert_observer::(InspectorSandboxObserver(GenericInspector)); + assert_observer::(GenericInspector); + assert_observer::(NopObserver); } #[test] - fn test_inspector_adapter_satisfies_empty_and_parent_env_observer_bounds() { + fn test_blanket_observer_satisfies_empty_and_parent_env_bounds() { use crate::TestExternalEnvs; fn assert_dual + SandboxObserver>( _: T, ) { } - assert_dual(InspectorSandboxObserver(GenericInspector)); + assert_dual(GenericInspector); assert_dual(NopObserver); } + + #[test] + fn test_read_only_hook_is_a_sandbox_inspector_handle() { + let observer: Rc>> = + Rc::new(RefCell::new(NopObserver)); + let _handle: SandboxHookHandle = ReadOnlyHook::handle(observer); + } } diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index 937ef19d..36144a23 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -27,7 +27,6 @@ use revm_inspectors::tracing::{ TracingInspector, }; -use super::InspectorSandboxObserver; use crate::KEYLESS_DEPLOY_ADDRESS; /// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. @@ -60,8 +59,8 @@ impl SharedTracingInspector { } /// Observer handle for the read-only keyless sandbox channel. - pub fn as_sandbox_observer(&self) -> Rc>> { - Rc::new(RefCell::new(InspectorSandboxObserver(self.clone()))) + pub fn as_sandbox_observer(&self) -> Rc> { + Rc::new(RefCell::new(self.clone())) } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index 9016321b..98544bbe 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -1139,47 +1139,6 @@ fn test_inspector_pre_rex4_crowded_parent_env_keeps_empty_env_gas() { ); } -#[test] -fn test_inspector_for_parent_env_skips_pre_rex4_opcode_hooks() { - for spec in SPECS { - let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); - let mut db = funded_db(signer); - let recorder = Rc::new(RefCell::new(RecordingInspector::default())); - let inspector: Rc>> = recorder.clone(); - - let mut context = - MegaContext::new(&mut db, spec).with_external_envs(crowded_parent_env().into()); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context.set_keyless_sandbox_inspector_for_parent_env(Some(inspector)); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); - let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); - let events = recorder.borrow().events.clone(); - - assert!(result.result.is_success(), "{spec:?} deploy should succeed: {:?}", result.result); - let starts = events.iter().filter(|e| matches!(e, InspectedEvent::Start)).count(); - let ends = events.iter().filter(|e| matches!(e, InspectedEvent::End(_))).count(); - assert_eq!(starts, 1, "{spec:?}: sandbox_start once"); - assert_eq!(ends, 1, "{spec:?}: sandbox_end once"); - let has_opcode = - events.iter().any(|e| matches!(e, InspectedEvent::Step(_) | InspectedEvent::Create)); - if spec.is_enabled(MegaSpecId::REX4) { - assert!( - has_opcode, - "{spec:?}: REX4+ parent-env inspector sees opcode hooks: {events:?}" - ); - } else { - assert!( - !has_opcode, - "{spec:?}: pre-REX4 parent-env inspector must not emit opcode hooks: {events:?}" - ); - } - } -} - #[test] fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { let spec = MegaSpecId::REX5; diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 06ec19e1..ba9fd669 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -9,12 +9,11 @@ use mega_evm::{ constants, revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ - decode_error_result, InspectorSandboxObserver, KeylessDeployError, SandboxCompletionKind, - SandboxEndOutcome, SandboxObserver, SandboxRejectKind, SandboxStartInfo, + decode_error_result, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, + SandboxObserver, SandboxRejectKind, SandboxStartInfo, }, test_utils::{ErrorInjectingDatabase, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, - TestExternalEnvs, }; use revm::{ inspector::NoOpInspector, @@ -441,34 +440,6 @@ fn test_opcode_events_flow_on_pre_rex4_with_nonempty_parent_env() { } } -#[test] -fn test_parent_env_only_observer_skips_pre_rex4_opcode_hooks() { - let spec = MegaSpecId::REX2; - let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); - let mut db = funded_db(signer); - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); - let observer: Rc>> = recorder.clone(); - - let mut context = - MegaContext::new(&mut db, spec).with_external_envs(crowded_parent_env().into()); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context.set_keyless_sandbox_observer_for_parent_env(Some(observer)); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); - let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); - let events = recorder.borrow().events.clone(); - - assert!(result.result.is_success(), "deploy should succeed: {:?}", result.result); - assert_single_start_end_pair(&events); - assert!( - !events.iter().any(|e| matches!(e, ObservedEvent::Step(_) | ObservedEvent::Create)), - "parent-env-only observer must not emit opcode hooks on pre-REX4: {events:?}" - ); -} - #[test] fn test_observer_parity_success_across_specs() { for spec in SPECS { @@ -692,10 +663,10 @@ fn test_event_order_create_wraps_steps() { } #[test] -fn test_inspector_adapter_records_sandbox_create_for_generic_inspector() { +fn test_blanket_observer_records_sandbox_create_for_generic_inspector() { let tracer = GenericCreateCounter::default(); let creates = Rc::clone(&tracer.creates); - let observer = Rc::new(RefCell::new(InspectorSandboxObserver(tracer))); + let observer = Rc::new(RefCell::new(tracer)); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); From f663967314f9292cf8a2a76a398ccc0c223020aa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:13:24 +0800 Subject: [PATCH 20/28] refactor(sandbox): name the read-only hook constructor new --- crates/mega-evm/src/evm/context.rs | 8 ++++++-- crates/mega-evm/src/sandbox/execution.rs | 4 +++- crates/mega-evm/src/sandbox/observer.rs | 17 ++++++++--------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index f7fa039a..7b1e6426 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -522,8 +522,12 @@ impl MegaContext { let cloned = Rc::clone(&obs); let parent: Rc>> = cloned; let empty: Rc>> = obs; - self.keyless_sandbox_hook = Some(ReadOnlyHook::handle(parent)); - self.keyless_sandbox_hook_empty = Some(ReadOnlyHook::handle(empty)); + let parent: SandboxHookHandle = + Rc::new(RefCell::new(ReadOnlyHook::new(parent))); + let empty: SandboxHookHandle = + Rc::new(RefCell::new(ReadOnlyHook::new(empty))); + self.keyless_sandbox_hook = Some(parent); + self.keyless_sandbox_hook_empty = Some(empty); } None => self.clear_keyless_sandbox_hook(), } diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 8e33c2b6..ba5f1edc 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -1830,7 +1830,9 @@ mod tests { Some(limits), block, chain, - observer.map(crate::sandbox::ReadOnlyHook::handle), + observer.map(|observer| -> SandboxHookHandle { + Rc::new(RefCell::new(crate::sandbox::ReadOnlyHook::new(observer))) + }), ) } diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 8d4b2b2d..f4d670ab 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -66,10 +66,7 @@ use revm::{ use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; -use super::{ - inspector::{SandboxHookHandle, SandboxInspector}, - state::SandboxDb, -}; +use super::{inspector::SandboxInspector, state::SandboxDb}; /// Context available when a sandbox is about to execute. #[non_exhaustive] @@ -368,10 +365,10 @@ pub(crate) struct ReadOnlyHook { observer: Rc>>, } -impl ReadOnlyHook { - /// Wraps a shared observer handle as a type-erased hook handle. - pub(crate) fn handle(observer: Rc>>) -> SandboxHookHandle { - Rc::new(RefCell::new(Self { observer })) +impl ReadOnlyHook { + /// Wraps a shared observer handle. + pub(crate) fn new(observer: Rc>>) -> Self { + Self { observer } } } @@ -526,8 +523,10 @@ mod tests { #[test] fn test_read_only_hook_is_a_sandbox_inspector_handle() { + use super::super::inspector::SandboxHookHandle; let observer: Rc>> = Rc::new(RefCell::new(NopObserver)); - let _handle: SandboxHookHandle = ReadOnlyHook::handle(observer); + let _handle: SandboxHookHandle = + Rc::new(RefCell::new(ReadOnlyHook::new(observer))); } } From ce43e5b22c209ec300185106a71d4fc9ba05039c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:59:07 +0800 Subject: [PATCH 21/28] fix(sandbox): skip trace splicing into an empty outer and pin read-only override drops --- bin/mega-evme/src/common/trace.rs | 19 ++++ crates/mega-evm/src/sandbox/observer.rs | 13 ++- crates/mega-evm/src/sandbox/trace.rs | 14 ++- .../tests/rex2/keyless_sandbox_inspector.rs | 42 ++------ .../tests/rex2/keyless_sandbox_observer.rs | 102 ++++++++++++++++-- .../tests/rex2/keyless_sandbox_support.rs | 34 ++++++ 6 files changed, 176 insertions(+), 48 deletions(-) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index d42d1801..eba84d8e 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -736,6 +736,25 @@ mod tests { ); } + /// Splicing sandbox frames into an outer inspector that recorded nothing is a no-op: the + /// arena's default root is not a `KeylessDeploy` CALL, and hanging the sandbox under it + /// would invent a call tree the outer EVM never executed. + #[test] + fn test_splice_into_empty_outer_is_a_no_op() { + use mega_evm::sandbox::trace::sandbox_has_frames; + let outer = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let sandbox = + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + run_traced_keyless(outer, sandbox.clone()); + assert!(sandbox_has_frames(&sandbox.borrow()), "the sandbox recorded the CREATE"); + + let mut empty = TracingInspector::new(TracingInspectorConfig::all()); + splice_sandbox_traces(&mut empty, &sandbox.borrow()); + assert!(!sandbox_has_frames(&empty), "nothing was grafted under the default root"); + assert_eq!(empty.traces().nodes().len(), 1, "the arena keeps only its default root"); + } + /// The parity rendering (`trace_*` and `debug_traceTransaction` with `flatCallTracer`) of /// the spliced trace: the sandbox CREATE is trace address `[0]` under the `KeylessDeploy` /// CALL, every nested frame extends that prefix, and `subtraces` counts match the tree. diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index f4d670ab..1e9b6051 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -34,10 +34,15 @@ //! //! [`sandbox_start`](SandboxObserver::sandbox_start) and //! [`sandbox_end`](SandboxObserver::sandbox_end) fire exactly once per sandbox -//! attempt, and only when an observer is attached. A validate-reject path that -//! never constructs a sandbox EVM still delivers the pair. Reverted inner -//! frames still emit their events; whether sandbox state was applied to the -//! parent is reported by [`SandboxEndOutcome::state_applied`]. +//! attempt, and only when an observer is attached. An attempt begins once the +//! keyless payload has been decoded, its signer recovered, the deploy address +//! derived and found free, and the gas budget admitted; a call that is rejected +//! before that point emits no events. From then on the pair is guaranteed: a +//! sandbox transaction that revm's validation rejects without ever constructing +//! a sandbox EVM still delivers `sandbox_end` with +//! [`SandboxRejectKind::Rejected`]. Reverted inner frames still emit their +//! events; whether sandbox state was applied to the parent is reported by +//! [`SandboxEndOutcome::state_applied`]. //! //! # External-environment invariance //! diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index 36144a23..b0737ee5 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -117,7 +117,13 @@ where /// Returns true when `sandbox` recorded at least one completed frame. pub fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { - sandbox.traces().nodes().iter().any(|node| { + has_frames(sandbox) +} + +/// Whether `inspector` recorded any frame. A fresh or fused arena still holds one default +/// root node, so emptiness is judged by content, not by node count. +fn has_frames(inspector: &TracingInspector) -> bool { + inspector.traces().nodes().iter().any(|node| { node.trace.status.is_some() || !node.trace.steps.is_empty() || !node.children.is_empty() }) } @@ -134,8 +140,12 @@ pub fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { /// call-like opcode in its steps. An intercepted `KeylessDeploy` CALL records no bytecode, so /// a CREATE step is grafted onto the parent; without it the sandbox constructor steps would /// be omitted from struct-log output instead of nested in execution order. +/// +/// An `outer` that recorded no frames at all (already fused, or never run) is also left +/// untouched: its arena holds only the default root node, and grafting the sandbox under +/// that phantom root would invent a call tree the outer EVM never executed. pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &TracingInspector) { - if !sandbox_has_frames(sandbox) { + if !has_frames(sandbox) || !has_frames(outer) { return; } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index 98544bbe..93a23598 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -16,9 +16,7 @@ use mega_evm::{ MegaSpecId, TestExternalEnvs, }; use revm::{ - bytecode::opcode::{ - CALL, CODECOPY, ISZERO, JUMPDEST, JUMPI, MLOAD, MSTORE8, POP, RETURN, SSTORE, STATICCALL, - }, + bytecode::opcode::{CALL, CODECOPY, ISZERO, JUMPDEST, JUMPI, MSTORE8, POP, RETURN, SSTORE}, context::{ContextTr, JournalTr}, handler::EvmTr, inspector::NoOpInspector, @@ -30,13 +28,14 @@ use revm::{ use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; use super::keyless_sandbox_support::{ - assert_result_and_state_eq, assert_usage_eq, constructor_calls_reverter, - constructor_touches_sentinel, create_pre_eip155_deploy_tx, + assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_and_stores_return, + constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_outer_gas, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_usage, split_create_initcode, - success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_PRECOMPILE, - LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, MERGE_FAIL_SENTINEL, REVERTER, SPECS, + success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, IDENTITY_OVERRIDE, + IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, MERGE_FAIL_SENTINEL, + REVERTER, SPECS, }; const SUCCESS_TARGET: Address = address!("0000000000000000000000000000000000cccccc"); @@ -46,39 +45,10 @@ const CALLER_SINK: Address = address!("0000000000000000000000000000000000511111" const CALLER_SINK_RUNTIME: [u8; 5] = [0x33, 0x60, 0x00, 0x55, 0x00]; /// The account a prank substitutes as `msg.sender` or as the creator of a nested CREATE. const PRANKED: Address = address!("00000000000000000000000000000000009a9a9a"); -const IDENTITY_INPUT: U256 = U256::from_limbs([0x11, 0, 0, 0]); -const IDENTITY_OVERRIDE: U256 = U256::from_limbs([0x42, 0, 0, 0]); const JOURNAL_SLOT: U256 = U256::from_limbs([0x53, 0, 0, 0]); const JOURNAL_VALUE: U256 = U256::from_limbs([0x54, 0, 0, 0]); const STEP_GAS_SURCHARGE: u64 = 1_000; -fn constructor_calls_identity_and_stores_return() -> Bytes { - BytecodeBuilder::default() - .push_u256(IDENTITY_INPUT) - .push_number(0_u8) - .append(revm::bytecode::opcode::MSTORE) - .push_number(32_u8) - .push_number(0_u8) - .push_number(32_u8) - .push_number(0_u8) - .push_address(IDENTITY_PRECOMPILE) - .push_number(50_000_u32) - .append(STATICCALL) - .append(revm::bytecode::opcode::POP) - .push_number(0_u8) - .append(MLOAD) - .push_number(0_u8) - .append(SSTORE) - .push_number(1_u8) - .push_number(0_u8) - .push_number(0_u8) - .append(CODECOPY) - .push_number(1_u8) - .push_number(0_u8) - .append(RETURN) - .build() -} - fn constructor_calls_success_target() -> Bytes { let prefix = BytecodeBuilder::default() .push_number(0_u8) diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index ba9fd669..53fb0b53 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -19,21 +19,22 @@ use revm::{ inspector::NoOpInspector, interpreter::{ interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, - CreateInputs, CreateOutcome, Interpreter, + CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, InterpreterResult, }, Inspector, }; use super::keyless_sandbox_support::{ - assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_precompile, - constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, + assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_and_stores_return, + constructor_calls_identity_precompile, constructor_calls_reverter, + constructor_touches_sentinel, create_pre_eip155_deploy_tx, create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_override_u256, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_parent_env, run_keyless_with_parent_env_usage, run_keyless_with_usage, split_create_initcode, - success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_PRECOMPILE, - LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, SIGNED_TX_GAS_LIMIT, SPECS, - SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, + success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, IDENTITY_OVERRIDE, + IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, + SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -662,6 +663,95 @@ fn test_event_order_create_wraps_steps() { assert!(last_step_end < create_end, "step_end before create_end"); } +/// A revm inspector that answers every identity-precompile CALL with a fixed return. +/// +/// Attached through the read-only channel it must have no effect: the blanket +/// `SandboxObserver` impl hands it a copy of the inputs and discards the override. The +/// same type attached through the rewriting channel is the control arm proving the +/// override is real. +struct OverridingInspector; + +impl Inspector for OverridingInspector { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + (inputs.target_address == IDENTITY_PRECOMPILE).then(|| { + CallOutcome::new( + InterpreterResult::new( + InstructionResult::Return, + Bytes::from(IDENTITY_OVERRIDE.to_be_bytes::<32>()), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + ) + }) + } +} + +fn slot_zero(result: &ResultAndState, addr: Address) -> Option { + result + .state + .get(&addr) + .and_then(|account| account.storage.get(&U256::ZERO)) + .map(|slot| slot.present_value()) +} + +/// The read-only channel structurally drops `call` overrides: an inspector that short-circuits +/// the identity precompile changes nothing when attached as an observer (result, state, and +/// usage match the no-hook run and slot 0 holds the precompile's echo), while the same +/// inspector on the rewriting channel lands its override in slot 0. +#[test] +fn test_observer_channel_drops_call_overrides() { + let (tx_bytes, signer) = + create_pre_eip155_deploy_tx(constructor_calls_identity_and_stores_return()); + let deploy_address = signer.create(0); + + for spec in SPECS { + let mut db_base = funded_db(signer); + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_eq!( + slot_zero(&baseline, deploy_address), + Some(IDENTITY_INPUT), + "{spec:?}: the real precompile echoes the input" + ); + + let mut db_obs = funded_db(signer); + let (observed, observed_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_obs, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(Rc::new(RefCell::new(OverridingInspector))), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_result_and_state_eq(&observed, &baseline, &format!("{spec:?} observer override")); + assert_usage_eq(observed_usage, baseline_usage, &format!("{spec:?} observer override")); + + let mut db_insp = funded_db(signer); + let mut context = MegaContext::new(&mut db_insp, spec); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(OverridingInspector)))); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); + let rewritten = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("inspector channel"); + assert_eq!( + slot_zero(&rewritten, deploy_address), + Some(IDENTITY_OVERRIDE), + "{spec:?}: the rewriting channel lands the same inspector's override" + ); + } +} + #[test] fn test_blanket_observer_records_sandbox_create_for_generic_inspector() { let tracer = GenericCreateCounter::default(); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 2b666729..37b84e47 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -119,6 +119,40 @@ pub(crate) fn split_create_initcode() -> Bytes { .build() } +/// Word the identity-precompile constructor sends and expects back. +pub(crate) const IDENTITY_INPUT: U256 = U256::from_limbs([0x11, 0, 0, 0]); +/// Word a short-circuiting hook answers the identity call with instead. +pub(crate) const IDENTITY_OVERRIDE: U256 = U256::from_limbs([0x42, 0, 0, 0]); + +/// Constructor that STATICCALLs the identity precompile with [`IDENTITY_INPUT`] and stores +/// whatever comes back in slot 0, so a rewritten call outcome is visible in state. +pub(crate) fn constructor_calls_identity_and_stores_return() -> Bytes { + BytecodeBuilder::default() + .push_u256(IDENTITY_INPUT) + .push_number(0_u8) + .append(MSTORE) + .push_number(32_u8) + .push_number(0_u8) + .push_number(32_u8) + .push_number(0_u8) + .push_address(IDENTITY_PRECOMPILE) + .push_number(50_000_u32) + .append(STATICCALL) + .append(POP) + .push_number(0_u8) + .append(revm::bytecode::opcode::MLOAD) + .push_number(0_u8) + .append(revm::bytecode::opcode::SSTORE) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(revm::bytecode::opcode::CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() +} + pub(crate) fn constructor_calls_identity_precompile() -> Bytes { BytecodeBuilder::default() .push_number(0_u8) From 655770de6bbb0620888a91a6cd80d71cd1cd47e0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 10:46:07 +0800 Subject: [PATCH 22/28] fix(sandbox): skip splicing when the outer trace has no KeylessDeploy call Add in-crate tests for sandbox::trace: both hook channels graft the same tree, fuse, the splice guards, SELFDESTRUCT forwarding, the parity-config leaf case, and the MegaBlockExecutor hook forwarders. --- crates/mega-evm/src/sandbox/trace.rs | 21 +- .../tests/rex2/keyless_sandbox_trace.rs | 553 ++++++++++++++++++ crates/mega-evm/tests/rex2/main.rs | 2 + 3 files changed, 565 insertions(+), 11 deletions(-) create mode 100644 crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index b0737ee5..b17d12ec 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -141,22 +141,21 @@ fn has_frames(inspector: &TracingInspector) -> bool { /// a CREATE step is grafted onto the parent; without it the sandbox constructor steps would /// be omitted from struct-log output instead of nested in execution order. /// -/// An `outer` that recorded no frames at all (already fused, or never run) is also left -/// untouched: its arena holds only the default root node, and grafting the sandbox under -/// that phantom root would invent a call tree the outer EVM never executed. +/// An `outer` without a `KeylessDeploy` CALL frame is also left untouched. That covers an +/// arena that recorded no frames at all (already fused, or never run) and one that traced a +/// transaction which never entered the sandbox while `sandbox` still holds frames from an +/// earlier transaction; grafting under the default root or under an unrelated frame would +/// invent a call tree the outer EVM never executed. pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &TracingInspector) { if !has_frames(sandbox) || !has_frames(outer) { return; } - let parent_idx = outer - .traces() - .nodes() - .iter() - .rposition(|node| { - node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() - }) - .unwrap_or(0); + let Some(parent_idx) = outer.traces().nodes().iter().rposition(|node| { + node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() + }) else { + return; + }; let depth_offset = outer.traces().nodes()[parent_idx].trace.depth + 1; let base = outer.traces().nodes().len(); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs new file mode 100644 index 00000000..ce87ede0 --- /dev/null +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs @@ -0,0 +1,553 @@ +//! Tests for `sandbox::trace`: a `TracingInspector` shared between the outer EVM and the +//! keyless sandbox through either hook channel, and `splice_sandbox_traces`, which grafts the +//! sandbox frames under the intercepted `KeylessDeploy` CALL. +//! +//! Compiled only with the `inspectors` feature, which gates `sandbox::trace` itself. + +use std::convert::Infallible; + +use alloy_consensus::{transaction::Recovered, Signed, TxLegacy}; +use alloy_evm::{block::BlockExecutor, EvmEnv}; +use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; +use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall; +use mega_evm::{ + revm::{context::result::ResultAndState, InspectEvm}, + sandbox::trace::{sandbox_has_frames, splice_sandbox_traces, SharedTracingInspector}, + test_utils::{deep_mixed_init, BytecodeBuilder, MemoryDatabase, REVERTING_RUNTIME}, + BlockLimits, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaContext, + MegaEvm, MegaEvmFactory, MegaHaltReason, MegaHardforkConfig, MegaSpecId, MegaTransaction, + MegaTxEnvelope, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{OpCode, CALL, CREATE, PUSH0, SELFDESTRUCT, STOP}, + context::{BlockEnv, CfgEnv, TxEnv}, + database::State, +}; +use revm_inspectors::tracing::{ + types::{CallTraceNode, TraceMemberOrder}, + TracingInspector, TracingInspectorConfig, +}; + +use super::keyless_sandbox_support::{ + assert_result_and_state_eq, constructor_calls_reverter, create_pre_eip155_deploy_tx, + empty_code_constructor, funded_db, keyless_deploy_call_tx, success_constructor, + DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, REVERTER, SPECS, + TEST_CALLER, +}; + +/// A contract whose runtime is a single `STOP`, for transactions that never enter the sandbox. +const STOPPER: Address = address!("0000000000000000000000000000000000cccccc"); + +/// Which keyless sandbox channel the sandbox tracer is attached through. +#[derive(Clone, Copy, Debug)] +enum Channel { + Observer, + Inspector, +} + +fn tracer() -> SharedTracingInspector { + SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())) +} + +/// Init code that `SELFDESTRUCT`s the contract under construction with `address(0)` as the +/// beneficiary. +fn selfdestructing_constructor() -> Bytes { + BytecodeBuilder::default().append_many([PUSH0, PUSH0, SELFDESTRUCT]).build() +} + +/// Runs one keyless deploy of `tx_bytes` on a fresh `MegaEvm` with `outer` installed as the +/// EVM inspector and `sandbox` attached through `channel`. +fn trace_keyless_deploy( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx_bytes: Bytes, + outer: SharedTracingInspector, + sandbox: &SharedTracingInspector, + channel: Channel, +) -> ResultAndState { + 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); + }); + let mut evm = MegaEvm::new(context).with_inspector(outer); + match channel { + Channel::Observer => { + evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + } + Channel::Inspector => { + evm.set_keyless_sandbox_inspector(Some(sandbox.as_sandbox_observer())); + } + } + evm.inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) + .expect("keyless deploy transact") +} + +/// Runs a plain call to [`STOPPER`] with `outer` installed, so the arena holds a frame that is +/// not a `KeylessDeploy` CALL. +fn trace_plain_call(db: &mut MemoryDatabase, outer: SharedTracingInspector) { + db.set_account_code(STOPPER, Bytes::from_static(&[STOP])); + let mut context = MegaContext::new(db, MegaSpecId::REX5); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut evm = MegaEvm::new(context).with_inspector(outer); + let tx = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Call(STOPPER), + gas_limit: DEFAULT_OUTER_GAS_LIMIT, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let result = evm.inspect_tx(tx).expect("plain call transact"); + assert!(result.result.is_success(), "plain call must succeed: {:?}", result.result); +} + +/// Traces the three-level `deep_mixed_init` deployment through `channel` and returns the outer +/// tracer with the sandbox frames spliced in, plus the execution result and the deployer. +fn traced_deep_mixed( + spec: MegaSpecId, + channel: Channel, +) -> (SharedTracingInspector, SharedTracingInspector, ResultAndState, Address) { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let mut db = funded_db(signer); + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let outer = tracer(); + let sandbox = tracer(); + assert!(!sandbox_has_frames(&sandbox.borrow()), "{spec:?}: fresh sandbox tracer is empty"); + let result = trace_keyless_deploy(spec, &mut db, tx_bytes, outer.clone(), &sandbox, channel); + assert!(result.result.is_success(), "{spec:?} {channel:?}: {:?}", result.result); + assert!(sandbox_has_frames(&sandbox.borrow()), "{spec:?}: sandbox recorded the CREATE"); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + (outer, sandbox, result, signer) +} + +/// Checks that arena positions, parent links, child links, and depths agree for every node. +/// +/// Steps are recorded at the journal depth of the frame that executes them, one level below +/// the frame's own trace depth, and the splice preserves that relation for grafted frames. +fn assert_arena_consistent(nodes: &[CallTraceNode], case: &str) { + for (idx, node) in nodes.iter().enumerate() { + assert_eq!(node.idx, idx, "{case}: node idx must match its arena position"); + for step in &node.trace.steps { + assert_eq!( + step.depth, + node.trace.depth as u64 + 1, + "{case}: step {} of node {idx} must sit one level below its frame", + step.op + ); + } + for &child in &node.children { + assert_eq!(nodes[child].parent, Some(idx), "{case}: child {child} parent link"); + assert_eq!( + nodes[child].trace.depth, + node.trace.depth + 1, + "{case}: child {child} must sit one level below {idx}" + ); + } + if let Some(parent) = node.parent { + assert!( + nodes[parent].children.contains(&idx), + "{case}: parent {parent} must list child {idx}" + ); + } + } +} + +/// Asserts the spliced shape of `deep_mixed_init`: CALL `KeylessDeploy` → CREATE parent → +/// { CREATE child → { CREATE grandchild, reverted CALL }, reverted CALL }. +fn assert_deep_mixed_shape(outer: &SharedTracingInspector, signer: Address, case: &str) { + let parent_addr = signer.create(0); + let child_addr = parent_addr.create(1); + let grandchild_addr = child_addr.create(1); + + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, case); + assert_eq!(nodes.len(), 6, "{case}: outer CALL plus five sandbox frames"); + + let root = &nodes[0]; + assert_eq!( + root.trace.address, KEYLESS_DEPLOY_ADDRESS, + "{case}: root is the KeylessDeploy CALL" + ); + assert_eq!(root.trace.depth, 0, "{case}: root depth"); + assert!(!root.trace.kind.is_any_create(), "{case}: root is a CALL"); + assert_eq!(root.children.len(), 1, "{case}: the sandbox CREATE hangs under the root"); + assert!( + root.trace.steps.iter().any(|step| step.op.get() == CREATE), + "{case}: the intercepted CALL carries a synthetic CREATE step" + ); + assert!( + matches!(root.ordering.last(), Some(TraceMemberOrder::Call(0))), + "{case}: the grafted child is the last member of the root, got {:?}", + root.ordering.last() + ); + + let parent = &nodes[root.children[0]]; + assert!(parent.trace.kind.is_any_create(), "{case}: sandbox root is a CREATE"); + assert_eq!( + parent.trace.address, parent_addr, + "{case}: sandbox CREATE lands at signer.create(0)" + ); + assert_eq!(parent.trace.depth, 1, "{case}: sandbox CREATE sits under the CALL"); + assert!(parent.trace.success, "{case}: the deployment succeeds"); + assert_eq!(parent.children.len(), 2, "{case}: parent CREATEs a child and CALLs the reverter"); + + let child = &nodes[parent.children[0]]; + assert!(child.trace.kind.is_any_create(), "{case}: first parent child is the CREATE"); + assert_eq!(child.trace.address, child_addr, "{case}: child address"); + assert_eq!( + child.children.len(), + 2, + "{case}: child CREATEs a grandchild and CALLs the reverter" + ); + + let grandchild = &nodes[child.children[0]]; + assert!(grandchild.trace.kind.is_any_create(), "{case}: grandchild is a CREATE"); + assert_eq!(grandchild.trace.address, grandchild_addr, "{case}: grandchild address"); + assert_eq!(grandchild.trace.depth, 3, "{case}: grandchild depth"); + assert!(grandchild.children.is_empty(), "{case}: grandchild is a leaf"); + + for (label, idx) in [("child", child.children[1]), ("parent", parent.children[1])] { + let call = &nodes[idx]; + assert!(!call.trace.kind.is_any_create(), "{case}: {label}'s reverter frame is a CALL"); + assert_eq!(call.trace.address, REVERTER, "{case}: {label} calls the reverter"); + assert!(!call.trace.success, "{case}: {label}'s reverter call reverts"); + assert!(call.children.is_empty(), "{case}: reverter frame is a leaf"); + } +} + +#[test] +fn test_observer_channel_tracer_splices_deep_mixed_shape() { + for spec in SPECS { + let (outer, _sandbox, _result, signer) = traced_deep_mixed(spec, Channel::Observer); + assert_deep_mixed_shape(&outer, signer, &format!("observer {spec:?}")); + } +} + +#[test] +fn test_inspector_channel_tracer_splices_same_shape_and_result() { + for spec in SPECS { + let (obs_outer, _, obs_result, signer) = traced_deep_mixed(spec, Channel::Observer); + let (insp_outer, _, insp_result, insp_signer) = traced_deep_mixed(spec, Channel::Inspector); + assert_eq!(signer, insp_signer); + assert_deep_mixed_shape(&insp_outer, signer, &format!("inspector {spec:?}")); + assert_result_and_state_eq(&insp_result, &obs_result, &format!("inspector {spec:?}")); + + let obs_ref = obs_outer.borrow(); + let insp_ref = insp_outer.borrow(); + let shape = |nodes: &[CallTraceNode]| -> Vec<(usize, bool, Address, Option)> { + nodes + .iter() + .map(|n| (n.trace.depth, n.trace.kind.is_any_create(), n.trace.address, n.parent)) + .collect() + }; + assert_eq!( + shape(obs_ref.traces().nodes()), + shape(insp_ref.traces().nodes()), + "{spec:?}: both channels graft the same tree" + ); + } +} + +#[test] +fn test_fuse_resets_both_tracers() { + let (outer, sandbox, _result, _signer) = traced_deep_mixed(MegaSpecId::REX5, Channel::Observer); + assert!(sandbox_has_frames(&outer.borrow())); + assert!(sandbox_has_frames(&sandbox.borrow())); + + sandbox.fuse(); + outer.fuse(); + assert!(!sandbox_has_frames(&sandbox.borrow()), "fused sandbox tracer is empty"); + assert!(!sandbox_has_frames(&outer.borrow()), "fused outer tracer is empty"); + assert_eq!(outer.borrow().traces().nodes().len(), 1, "a fused arena keeps its default root"); +} + +/// Splicing into an outer tracer that recorded no frames, or one whose frames contain no +/// `KeylessDeploy` CALL, leaves the outer arena untouched even though the sandbox holds frames. +#[test] +fn test_splice_without_keyless_call_in_outer_is_a_no_op() { + let (_outer, sandbox, _result, _signer) = + traced_deep_mixed(MegaSpecId::REX5, Channel::Observer); + assert!(sandbox_has_frames(&sandbox.borrow())); + + let mut empty = TracingInspector::new(TracingInspectorConfig::all()); + splice_sandbox_traces(&mut empty, &sandbox.borrow()); + assert!(!sandbox_has_frames(&empty), "nothing is grafted under the default root"); + assert_eq!(empty.traces().nodes().len(), 1); + + let plain = tracer(); + let mut db = MemoryDatabase::default(); + trace_plain_call(&mut db, plain.clone()); + assert!(sandbox_has_frames(&plain.borrow()), "the plain call recorded a frame"); + let before = plain.borrow().traces().nodes().len(); + splice_sandbox_traces(&mut plain.borrow_mut(), &sandbox.borrow()); + let plain_ref = plain.borrow(); + let nodes = plain_ref.traces().nodes(); + assert_eq!(nodes.len(), before, "no sandbox frame is appended to an unrelated trace"); + assert_eq!(nodes[0].trace.address, STOPPER); + assert!(nodes[0].children.is_empty(), "the unrelated frame gains no child"); + assert!( + nodes[0].trace.steps.iter().all(|step| step.op.get() != CREATE), + "no synthetic CREATE step is added to an unrelated frame" + ); +} + +/// A parity-style tracer records no steps, so a leaf sandbox CREATE is a node with a status +/// but neither steps nor children. It still counts as a frame, is grafted, and the parent +/// receives the synthetic CREATE step that struct-log rendering needs. +#[test] +fn test_tracer_without_steps_still_splices_a_leaf_create() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let outer = SharedTracingInspector::new(TracingInspector::new( + TracingInspectorConfig::default_parity(), + )); + let sandbox = SharedTracingInspector::new(TracingInspector::new( + TracingInspectorConfig::default_parity(), + )); + let result = trace_keyless_deploy( + MegaSpecId::REX5, + &mut db, + tx_bytes, + outer.clone(), + &sandbox, + Channel::Observer, + ); + assert!(result.result.is_success(), "{:?}", result.result); + { + let sandbox_ref = sandbox.borrow(); + let create = &sandbox_ref.traces().nodes()[0]; + assert!(create.trace.kind.is_any_create()); + assert!(create.trace.status.is_some(), "the CREATE frame completed"); + assert!(create.trace.steps.is_empty(), "parity config records no steps"); + assert!(create.children.is_empty(), "success_constructor makes no calls"); + } + assert!(sandbox_has_frames(&sandbox.borrow()), "a completed leaf frame counts"); + assert!(sandbox_has_frames(&outer.borrow())); + + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "parity"); + assert_eq!(nodes.len(), 2); + assert_eq!(nodes[0].children, vec![1]); + assert_eq!(nodes[1].trace.address, signer.create(0)); + assert_eq!(nodes[0].trace.steps.len(), 1, "exactly the synthetic CREATE step"); + assert_eq!(nodes[0].trace.steps[0].op.get(), CREATE); + assert_eq!(nodes[0].trace.steps[0].contract, KEYLESS_DEPLOY_ADDRESS); + assert_eq!( + nodes[0].ordering, + vec![TraceMemberOrder::Step(0), TraceMemberOrder::Call(0)], + "the step precedes the child it introduces" + ); +} + +/// The synthetic CREATE step exists to pair one call-like step with each child. A parent that +/// already carries a call-like step per child gets no extra step. +#[test] +fn test_splice_adds_no_create_step_when_the_parent_has_a_call_like_step_per_child() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let outer = tracer(); + let sandbox = tracer(); + let result = trace_keyless_deploy( + MegaSpecId::REX5, + &mut db, + tx_bytes, + outer.clone(), + &sandbox, + Channel::Observer, + ); + assert!(result.result.is_success(), "{:?}", result.result); + + // Give the intercepted CALL one call-like step of its own before grafting. + let mut call_step = sandbox.borrow().traces().nodes()[0].trace.steps[0].clone(); + call_step.op = OpCode::new(CALL).expect("CALL is a defined opcode"); + call_step.depth = 1; + { + let mut outer_mut = outer.borrow_mut(); + let root = &mut outer_mut.traces_mut().nodes_mut()[0]; + assert!(root.trace.steps.is_empty(), "an intercepted CALL records no steps"); + root.trace.steps.push(call_step); + root.ordering.push(TraceMemberOrder::Step(0)); + } + + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "paired"); + assert_eq!(nodes[0].children, vec![1], "the sandbox CREATE is still grafted"); + assert_eq!(nodes[0].trace.steps.len(), 1, "no synthetic CREATE step is added"); + assert_eq!(nodes[0].trace.steps[0].op.get(), CALL); + assert_eq!(nodes[0].ordering, vec![TraceMemberOrder::Step(0), TraceMemberOrder::Call(0)]); +} + +/// A `SELFDESTRUCT` inside the sandbox reaches the sandbox tracer's `selfdestruct` hook +/// through both channels, as it does on a top-level EVM. +#[test] +fn test_sandbox_selfdestruct_reaches_the_tracer() { + for channel in [Channel::Observer, Channel::Inspector] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(selfdestructing_constructor()); + let mut db = funded_db(signer); + let outer = tracer(); + let sandbox = tracer(); + let result = trace_keyless_deploy( + MegaSpecId::REX5, + &mut db, + tx_bytes, + outer.clone(), + &sandbox, + channel, + ); + assert!(result.result.is_success(), "{channel:?}: {:?}", result.result); + + let sandbox_ref = sandbox.borrow(); + let create = sandbox_ref + .traces() + .nodes() + .iter() + .find(|node| node.trace.kind.is_any_create()) + .unwrap_or_else(|| panic!("{channel:?}: sandbox recorded the CREATE frame")); + assert_eq!( + create.trace.selfdestruct_address, + Some(signer.create(0)), + "{channel:?}: the tracer saw the constructor self-destruct" + ); + assert_eq!( + create.trace.selfdestruct_refund_target, + Some(Address::ZERO), + "{channel:?}: beneficiary as pushed by the constructor" + ); + } +} + +fn keyless_deploy_envelope(nonce: u64, tx_bytes: Bytes) -> Recovered { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: tx_bytes, + gasLimitOverride: U256::from(LARGE_GAS_LIMIT_OVERRIDE), + } + .abi_encode(); + let tx = TxLegacy { + chain_id: Some(1), + nonce, + gas_price: 0, + gas_limit: DEFAULT_OUTER_GAS_LIMIT, + to: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + value: U256::ZERO, + input: call_data.into(), + }; + let signed = Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO); + Recovered::new_unchecked(MegaTxEnvelope::Legacy(signed), TEST_CALLER) +} + +/// The `MegaBlockExecutor` forwarders: attach through the inspector channel, detach, then +/// attach through the observer channel, one keyless deployment per step of a single block. +#[test] +fn test_block_executor_forwards_sandbox_hooks() { + let (tx_a, signer_a) = create_pre_eip155_deploy_tx(success_constructor()); + let (tx_b, signer_b) = create_pre_eip155_deploy_tx(empty_code_constructor()); + let (tx_c, signer_c) = create_pre_eip155_deploy_tx(constructor_calls_reverter()); + assert_ne!(signer_a, signer_b); + assert_ne!(signer_b, signer_c); + + let mut db = MemoryDatabase::default(); + for signer in [signer_a, signer_b, signer_c] { + db.set_account_balance(signer, U256::from(LARGE_SIGNER_BALANCE)); + } + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let mut state = State::builder().with_database(&mut db).build(); + + let evm_factory = + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()); + let hardforks = MegaHardforkConfig::default().with_all_activated(); + let factory = + MegaBlockExecutorFactory::new(&hardforks, evm_factory, OpAlloyReceiptBuilder::default()); + + let mut cfg_env = CfgEnv::default(); + cfg_env.spec = MegaSpecId::REX5; + let block_env = BlockEnv { gas_limit: 4 * DEFAULT_OUTER_GAS_LIMIT, ..Default::default() }; + let evm_env = EvmEnv::new(cfg_env, block_env); + let block_ctx = + MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits()); + + let outer = tracer(); + let sandbox = tracer(); + let mut executor = + factory.create_executor_with_inspector(&mut state, block_ctx, evm_env, outer.clone()); + // `apply_pre_execution_changes` needs a configured SequencerRegistry on REX5+, and + // KeylessDeploy interception does not depend on the predeploy bytecode. + executor.evm.ctx.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + // Inspector channel: the sandbox CREATE is recorded and grafted under the CALL. + executor.set_keyless_sandbox_inspector(Some(sandbox.as_sandbox_observer())); + outer.fuse(); + executor + .execute_transaction_with_result_closure(&keyless_deploy_envelope(0, tx_a), |result| { + assert!(result.is_success(), "tx A: {result:?}"); + }) + .expect("tx A executes"); + assert!(sandbox_has_frames(&sandbox.borrow()), "tx A: sandbox recorded the CREATE"); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + { + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "tx A"); + assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS); + assert_eq!(nodes[0].children.len(), 1, "tx A: one grafted CREATE"); + let create = &nodes[nodes[0].children[0]]; + assert!(create.trace.kind.is_any_create()); + assert_eq!(create.trace.address, signer_a.create(0)); + assert!(create.trace.success); + } + sandbox.fuse(); + + // Detached: the sandbox still runs, nothing is recorded, and the splice is a no-op. + executor.clear_keyless_sandbox_hook(); + outer.fuse(); + executor + .execute_transaction_with_result_closure(&keyless_deploy_envelope(1, tx_b), |result| { + assert!(result.is_success(), "tx B: {result:?}"); + }) + .expect("tx B executes"); + assert!(!sandbox_has_frames(&sandbox.borrow()), "tx B: no hook, nothing recorded"); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + { + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS); + assert!(nodes[0].children.is_empty(), "tx B: nothing grafted"); + } + + // Observer channel: frames come back, including the constructor's reverted CALL. + executor.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + outer.fuse(); + executor + .execute_transaction_with_result_closure(&keyless_deploy_envelope(2, tx_c), |result| { + assert!(result.is_success(), "tx C: {result:?}"); + }) + .expect("tx C executes"); + assert!(sandbox_has_frames(&sandbox.borrow()), "tx C: sandbox recorded the CREATE"); + splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + { + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "tx C"); + assert_eq!(nodes[0].children.len(), 1, "tx C: one grafted CREATE"); + let create = &nodes[nodes[0].children[0]]; + assert_eq!(create.trace.address, signer_c.create(0)); + assert_eq!(create.children.len(), 1, "tx C: the constructor's CALL"); + let call = &nodes[create.children[0]]; + assert_eq!(call.trace.address, REVERTER); + assert!(!call.trace.success, "tx C: the reverter call reverts"); + assert_eq!(call.trace.depth, 2); + } +} diff --git a/crates/mega-evm/tests/rex2/main.rs b/crates/mega-evm/tests/rex2/main.rs index 76bb726e..5624cbf8 100644 --- a/crates/mega-evm/tests/rex2/main.rs +++ b/crates/mega-evm/tests/rex2/main.rs @@ -5,5 +5,7 @@ mod keyless_sandbox_extreme; mod keyless_sandbox_inspector; mod keyless_sandbox_observer; mod keyless_sandbox_support; +#[cfg(feature = "inspectors")] +mod keyless_sandbox_trace; mod oracle_hint; mod selfdestruct; From 0502ab24721867b260798a202a76edeade9bf860 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 11:52:50 +0800 Subject: [PATCH 23/28] test(sandbox): cover default hook bodies, slot clearing, and adapter Debug impls Defaults-only observer and inspector run a logging, calling, and self-destructing deployment with the same result, state, and usage as the no-hook path; set_keyless_sandbox_inspector(None) clears both slots; with_db keeps the hook. --- crates/mega-evm/src/evm/context.rs | 29 +++++++++ crates/mega-evm/src/sandbox/inspector.rs | 9 +++ crates/mega-evm/src/sandbox/observer.rs | 9 +++ .../tests/rex2/keyless_sandbox_inspector.rs | 42 +++++++++++-- .../tests/rex2/keyless_sandbox_observer.rs | 59 +++++++++++++++++-- .../tests/rex2/keyless_sandbox_support.rs | 8 ++- .../tests/rex2/keyless_sandbox_trace.rs | 16 ++--- 7 files changed, 152 insertions(+), 20 deletions(-) diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 7b1e6426..72a1262e 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -910,6 +910,10 @@ mod tests { impl SandboxObserver for NopObserver {} + struct NopInspector; + + impl crate::sandbox::SandboxInspector for NopInspector {} + #[test] fn test_with_cfg_updates_spec() { // Create context with initial spec @@ -1020,6 +1024,31 @@ mod tests { assert!(context.keyless_sandbox_hook_empty.is_none()); } + #[test] + fn test_set_keyless_sandbox_inspector_none_clears_both_slots() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + context.set_keyless_sandbox_inspector(None::>>); + assert!(context.keyless_sandbox_hook.is_none()); + assert!(context.keyless_sandbox_hook_empty.is_none()); + + context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopInspector)))); + assert!(context.keyless_sandbox_hook.is_some()); + assert!(context.keyless_sandbox_hook_empty.is_some()); + context.set_keyless_sandbox_observer(None::>>); + assert!(context.keyless_sandbox_hook.is_none()); + assert!(context.keyless_sandbox_hook_empty.is_none()); + } + + #[test] + fn test_with_db_keeps_the_sandbox_hook() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); + context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopInspector)))); + let context = context.with_db(EmptyDB::default()); + assert!(context.keyless_sandbox_hook.is_some()); + assert!(context.keyless_sandbox_hook_empty.is_some()); + } + #[test] fn test_mega_evm_clear_keyless_sandbox_hook() { use revm::handler::EvmTr; diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs index 4b1ac394..1306e594 100644 --- a/crates/mega-evm/src/sandbox/inspector.rs +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -414,4 +414,13 @@ mod tests { assert_dual(GenericInspector); assert_dual(LocalInspector); } + + #[test] + fn test_inspector_bridge_debug_names_the_bridge() { + use std::format; + let inspector: Rc>> = + Rc::new(RefCell::new(LocalInspector)); + let rendered = format!("{:?}", InspectorBridge::new(inspector)); + assert!(rendered.starts_with("InspectorBridge"), "{rendered}"); + } } diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 1e9b6051..d6b802a2 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -534,4 +534,13 @@ mod tests { let _handle: SandboxHookHandle = Rc::new(RefCell::new(ReadOnlyHook::new(observer))); } + + #[test] + fn test_read_only_hook_debug_names_the_adapter() { + use std::format; + let observer: Rc>> = + Rc::new(RefCell::new(NopObserver)); + let rendered = format!("{:?}", ReadOnlyHook::new(observer)); + assert!(rendered.starts_with("ReadOnlyHook"), "{rendered}"); + } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index 93a23598..9571a981 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -32,10 +32,10 @@ use super::keyless_sandbox_support::{ constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_outer_gas, parent_compute_gas_used, - revert_constructor, run_keyless, run_keyless_with_usage, split_create_initcode, - success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, IDENTITY_OVERRIDE, - IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, MERGE_FAIL_SENTINEL, - REVERTER, SPECS, + revert_constructor, run_keyless, run_keyless_with_usage, selfdestructing_constructor, + split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, + IDENTITY_OVERRIDE, IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, + MERGE_FAIL_SENTINEL, REVERTER, SPECS, }; const SUCCESS_TARGET: Address = address!("0000000000000000000000000000000000cccccc"); @@ -1463,3 +1463,37 @@ fn test_inspector_create_prank_moves_nested_child_to_pranked_creator() { assert_control_signer_applied(&pranked, signer, spec); } } + +/// The trait's default `selfdestruct` body is inert: a defaults-only inspector attached to a +/// constructor that self-destructs leaves result, state, and usage identical to the no-hook run. +#[test] +fn test_defaults_only_inspector_is_inert_on_selfdestruct() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(selfdestructing_constructor()); + let mut db_base = funded_db(signer); + let mut db_nop = db_base.clone(); + + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let (inspected, inspected_usage) = run_keyless_inspector_with_usage(InspectorRunConfig { + spec, + db: &mut db_nop, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + inspector: Some(Rc::new(RefCell::new(NopSandboxInspector))), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let case = format!("defaults-only inspector selfdestruct {spec:?}"); + assert!(baseline.result.is_success(), "{case}: {:?}", baseline.result); + assert_result_and_state_eq(&inspected, &baseline, &case); + assert_usage_eq(inspected_usage, baseline_usage, &case); + } +} diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 53fb0b53..38ea0a00 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -31,10 +31,11 @@ use super::keyless_sandbox_support::{ create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_override_u256, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_parent_env, - run_keyless_with_parent_env_usage, run_keyless_with_usage, split_create_initcode, - success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, IDENTITY_OVERRIDE, - IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, - SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, + run_keyless_with_parent_env_usage, run_keyless_with_usage, selfdestructing_constructor, + split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, + IDENTITY_OVERRIDE, IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, + REVERTER, SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, + SPLIT_CREATE_SLOT_VALUE, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -962,3 +963,53 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { other => panic!("expected NotApplied(ApplyFailed), got {other:?}"), } } + +/// An observer that overrides nothing: every hook is the trait's default. +struct DefaultsOnlyObserver; + +impl SandboxObserver for DefaultsOnlyObserver {} + +/// The trait's default hook bodies are inert: with a defaults-only observer attached, a +/// deployment that logs, calls, creates, and self-destructs inside the sandbox ends with the +/// same result, state, and resource usage as the no-hook run. +#[test] +fn test_defaults_only_observer_is_inert() { + let shapes: [(&str, Bytes); 2] = [ + ("deep mixed", mega_evm::test_utils::deep_mixed_init(REVERTER)), + ("selfdestruct", selfdestructing_constructor()), + ]; + for spec in SPECS { + for (name, init_code) in &shapes { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code.clone()); + let mut db_base = funded_db(signer); + db_base.set_account_code( + REVERTER, + Bytes::from_static(&mega_evm::test_utils::REVERTING_RUNTIME), + ); + let mut db_obs = db_base.clone(); + + let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_base, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let (observed, observed_usage) = run_keyless_with_usage(RunConfig { + spec, + db: &mut db_obs, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: Some(Rc::new(RefCell::new(DefaultsOnlyObserver))), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + let case = format!("defaults-only observer {name} {spec:?}"); + assert!(baseline.result.is_success(), "{case}: {:?}", baseline.result); + assert_result_and_state_eq(&observed, &baseline, &case); + assert_usage_eq(observed_usage, baseline_usage, &case); + } + } +} diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 37b84e47..22c9993e 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -14,7 +14,7 @@ use mega_evm::{ MIN_BUCKET_SIZE, }; use revm::{ - bytecode::opcode::{BALANCE, CALL, MSTORE, POP, PUSH0, RETURN, STATICCALL}, + bytecode::opcode::{BALANCE, CALL, MSTORE, POP, PUSH0, RETURN, SELFDESTRUCT, STATICCALL}, context::TxEnv, handler::EvmTr, inspector::NoOpInspector, @@ -70,6 +70,12 @@ pub(crate) fn empty_code_constructor() -> Bytes { BytecodeBuilder::default().append_many([PUSH0, PUSH0, RETURN]).build() } +/// Init code that `SELFDESTRUCT`s the contract under construction with `address(0)` as the +/// beneficiary, so the sandbox emits a `selfdestruct` hook and deploys nothing. +pub(crate) fn selfdestructing_constructor() -> Bytes { + BytecodeBuilder::default().append_many([PUSH0, PUSH0, SELFDESTRUCT]).build() +} + pub(crate) fn constructor_calls_reverter() -> Bytes { BytecodeBuilder::default() .push_number(0_u8) diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs index ce87ede0..6ff3af61 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs @@ -14,13 +14,13 @@ use alloy_sol_types::SolCall; use mega_evm::{ revm::{context::result::ResultAndState, InspectEvm}, sandbox::trace::{sandbox_has_frames, splice_sandbox_traces, SharedTracingInspector}, - test_utils::{deep_mixed_init, BytecodeBuilder, MemoryDatabase, REVERTING_RUNTIME}, + test_utils::{deep_mixed_init, MemoryDatabase, REVERTING_RUNTIME}, BlockLimits, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaContext, MegaEvm, MegaEvmFactory, MegaHaltReason, MegaHardforkConfig, MegaSpecId, MegaTransaction, MegaTxEnvelope, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, }; use revm::{ - bytecode::opcode::{OpCode, CALL, CREATE, PUSH0, SELFDESTRUCT, STOP}, + bytecode::opcode::{OpCode, CALL, CREATE, STOP}, context::{BlockEnv, CfgEnv, TxEnv}, database::State, }; @@ -31,9 +31,9 @@ use revm_inspectors::tracing::{ use super::keyless_sandbox_support::{ assert_result_and_state_eq, constructor_calls_reverter, create_pre_eip155_deploy_tx, - empty_code_constructor, funded_db, keyless_deploy_call_tx, success_constructor, - DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, REVERTER, SPECS, - TEST_CALLER, + empty_code_constructor, funded_db, keyless_deploy_call_tx, selfdestructing_constructor, + success_constructor, DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, + REVERTER, SPECS, TEST_CALLER, }; /// A contract whose runtime is a single `STOP`, for transactions that never enter the sandbox. @@ -50,12 +50,6 @@ fn tracer() -> SharedTracingInspector { SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())) } -/// Init code that `SELFDESTRUCT`s the contract under construction with `address(0)` as the -/// beneficiary. -fn selfdestructing_constructor() -> Bytes { - BytecodeBuilder::default().append_many([PUSH0, PUSH0, SELFDESTRUCT]).build() -} - /// Runs one keyless deploy of `tx_bytes` on a fresh `MegaEvm` with `outer` installed as the /// EVM inspector and `sandbox` attached through `channel`. fn trace_keyless_deploy( From f344f5c087c65c0046e6acc5d6ca7667cb3b63e5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 15:08:19 +0800 Subject: [PATCH 24/28] fix(sandbox): record each sandbox execution in its own trace arena and address the review findings Tracing: a hand-written SandboxTracer records one arena per sandbox execution keyed by the intercepted call (SandboxStartInfo::outer_call), the splice pairs and moves each arena under its KeylessDeploy CALL and drains the tracer, unclosed frames are closed as FatalExternalError, pre-REX5 empty-code logs are stripped, and the synthetic CREATE step reports the real remaining gas and the sandbox reservation. as_sandbox_observer is replaced by paired(). Channels: lifecycle events go to the same env slot as the opcode hooks, EmptyCode is reported on every spec, a create override returning no address is charged like AddressMismatch, and the hook setters take Rc> by value. Adds the keyless_sandbox_hook benchmark and updates the mega-evme call sites, docs, and tests. --- bin/mega-evme/src/common/trace.rs | 78 +-- bin/mega-evme/src/replay/cmd.rs | 6 +- bin/mega-evme/tests/replay_keyless.rs | 6 +- crates/mega-evm/benches/mega_bench.rs | 130 +++- crates/mega-evm/src/block/executor.rs | 4 +- crates/mega-evm/src/evm/AGENTS.md | 3 +- crates/mega-evm/src/evm/context.rs | 80 +-- crates/mega-evm/src/evm/mod.rs | 4 +- crates/mega-evm/src/sandbox/execution.rs | 205 +++++-- crates/mega-evm/src/sandbox/mod.rs | 5 +- crates/mega-evm/src/sandbox/observer.rs | 53 +- crates/mega-evm/src/sandbox/trace.rs | 406 ++++++++++--- crates/mega-evm/src/system/intercept.rs | 1 + .../tests/rex2/keyless_sandbox_extreme.rs | 2 +- .../tests/rex2/keyless_sandbox_inspector.rs | 46 +- .../tests/rex2/keyless_sandbox_observer.rs | 107 +++- .../tests/rex2/keyless_sandbox_support.rs | 8 +- .../tests/rex2/keyless_sandbox_trace.rs | 565 +++++++++++------- docs/mega-evme/tracing/call-tracer.md | 1 + docs/mega-evme/tracing/opcode-tracer.md | 2 + 20 files changed, 1214 insertions(+), 498 deletions(-) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index eba84d8e..eeb5ed20 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -1,6 +1,6 @@ //! Trace configuration for mega-evme -use std::path::PathBuf; +use std::{cell::RefCell, path::PathBuf, rc::Rc}; use alloy_primitives::Bytes; use alloy_rpc_types_trace::geth::{ @@ -17,7 +17,7 @@ use mega_evm::{ state::EvmState, ExecuteEvm, InspectEvm, }, - sandbox::trace::{splice_sandbox_traces, SharedTracingInspector}, + sandbox::trace::{paired, splice_sandbox_traces, SandboxTracer, SharedTracingInspector}, MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, }; use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; @@ -98,10 +98,14 @@ impl TraceArgs { self.trace } + /// The [`TracingInspectorConfig`] every mega-evme tracer records with: everything on. + pub fn inspector_config(&self) -> TracingInspectorConfig { + TracingInspectorConfig::all() + } + /// Creates a [`TracingInspector`] configured for full tracing pub fn create_inspector(&self) -> TracingInspector { - let config = TracingInspectorConfig::all(); - TracingInspector::new(config) + TracingInspector::new(self.inspector_config()) } /// Creates [`GethDefaultTracingOptions`] from CLI arguments @@ -217,11 +221,11 @@ impl TraceArgs { pub(crate) fn generate_trace_with_sandbox( &self, outer: &SharedTracingInspector, - sandbox: &SharedTracingInspector, + sandbox: &Rc>, result_and_state: &ResultAndState, prestate: impl DatabaseRef, ) -> String { - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); self.generate_trace(&outer.borrow(), result_and_state, prestate) } @@ -237,10 +241,9 @@ impl TraceArgs { { if self.is_tracing_enabled() { info!(tracer = ?self.tracer, "Evm executing with tracing"); - let outer = SharedTracingInspector::new(self.create_inspector()); - let sandbox = SharedTracingInspector::new(self.create_inspector()); + let (outer, sandbox) = paired(self.inspector_config()); let mut evm = MegaEvm::new(evm_context).with_inspector(outer.clone()); - evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + evm.set_keyless_sandbox_observer(Rc::clone(&sandbox)); let result_and_state = evm .inspect_tx(tx) @@ -387,7 +390,7 @@ mod tests { fn run_traced_keyless( outer: SharedTracingInspector, - sandbox: SharedTracingInspector, + sandbox: Rc>, ) -> (ResultAndState, MemoryDatabase, Address) { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); run_traced_keyless_bytes(outer, sandbox, tx_bytes, signer, LARGE_GAS_LIMIT_OVERRIDE, |_| {}) @@ -397,7 +400,7 @@ mod tests { /// to the funded database first (extra contracts the constructor calls into). fn run_traced_keyless_bytes( outer: SharedTracingInspector, - sandbox: SharedTracingInspector, + sandbox: Rc>, tx_bytes: Bytes, signer: Address, gas_limit_override: u64, @@ -408,7 +411,7 @@ mod tests { let result = { let context = keyless_context(&mut db); let mut evm = MegaEvm::new(context).with_inspector(outer); - evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + evm.set_keyless_sandbox_observer(sandbox); let result = evm .inspect_tx(keyless_deploy_tx_with_override(tx_bytes, gas_limit_override)) .expect("keyless deploy"); @@ -425,10 +428,7 @@ mod tests { gas_limit_override: u64, setup: impl Fn(&mut MemoryDatabase), ) -> (CallFrame, SharedTracingInspector, ResultAndState, MemoryDatabase) { - let outer = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (outer, sandbox) = paired(TracingInspectorConfig::all()); let (result_and_state, db, _signer) = run_traced_keyless_bytes( outer.clone(), sandbox.clone(), @@ -437,7 +437,7 @@ mod tests { gas_limit_override, setup, ); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let frame = outer.borrow().geth_builder().geth_call_traces( CallConfig { only_top_call: Some(false), with_log: Some(true) }, result_and_state.result.gas_used(), @@ -447,12 +447,9 @@ mod tests { #[test] fn test_keyless_call_trace_nests_sandbox_create() { - let outer = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (outer, sandbox) = paired(TracingInspectorConfig::all()); let (result_and_state, _db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); @@ -480,12 +477,9 @@ mod tests { #[test] fn test_keyless_opcode_trace_includes_sandbox_steps_in_execution_order() { - let outer = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (outer, sandbox) = paired(TracingInspectorConfig::all()); let (result_and_state, db, _signer) = run_traced_keyless(outer.clone(), sandbox.clone()); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let outer_ref = outer.borrow(); let geth_trace = outer_ref.geth_builder().geth_traces( @@ -556,10 +550,7 @@ mod tests { BlockLimits::no_limits(), ); - let outer = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + let (outer, sandbox) = paired(TracingInspectorConfig::all()); let mut executor = factory.create_executor_with_inspector(&mut state, block_ctx, evm_env, outer.clone()); // Skip `apply_pre_execution_changes`: it needs a configured SequencerRegistry on @@ -569,7 +560,7 @@ mod tests { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - executor.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + executor.set_keyless_sandbox_observer(Rc::clone(&sandbox)); let call_data = IKeylessDeploy::keylessDeployCall { keylessDeploymentTransaction: tx_bytes, @@ -591,7 +582,7 @@ mod tests { let outcome = executor.run_transaction(&recovered).expect("run keyless deploy"); assert!(outcome.inner.result.is_success(), "{:?}", outcome.inner.result); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); assert_eq!(nodes[0].trace.address, KEYLESS_DEPLOY_ADDRESS); @@ -736,23 +727,20 @@ mod tests { ); } - /// Splicing sandbox frames into an outer inspector that recorded nothing is a no-op: the - /// arena's default root is not a `KeylessDeploy` CALL, and hanging the sandbox under it - /// would invent a call tree the outer EVM never executed. + /// Splicing a recorded sandbox into an outer inspector that does not hold its + /// `KeylessDeploy` CALL drops the sandbox: the arena's default root is not that frame, and + /// hanging the sandbox under it would invent a call tree the outer EVM never executed. #[test] - fn test_splice_into_empty_outer_is_a_no_op() { - use mega_evm::sandbox::trace::sandbox_has_frames; - let outer = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); - let sandbox = - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())); + fn test_splice_into_empty_outer_drops_the_sandbox() { + let (outer, sandbox) = paired(TracingInspectorConfig::all()); run_traced_keyless(outer, sandbox.clone()); - assert!(sandbox_has_frames(&sandbox.borrow()), "the sandbox recorded the CREATE"); + assert_eq!(sandbox.borrow().pending(), 1, "the sandbox recorded the CREATE"); let mut empty = TracingInspector::new(TracingInspectorConfig::all()); - splice_sandbox_traces(&mut empty, &sandbox.borrow()); - assert!(!sandbox_has_frames(&empty), "nothing was grafted under the default root"); + splice_sandbox_traces(&mut empty, &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0, "the sandbox is consumed either way"); assert_eq!(empty.traces().nodes().len(), 1, "the arena keeps only its default root"); + assert!(empty.traces().nodes()[0].children.is_empty(), "nothing was grafted"); } /// The parity rendering (`trace_*` and `debug_traceTransaction` with `flatCallTracer`) of diff --git a/bin/mega-evme/src/replay/cmd.rs b/bin/mega-evme/src/replay/cmd.rs index ab27f7f4..d290911d 100644 --- a/bin/mega-evme/src/replay/cmd.rs +++ b/bin/mega-evme/src/replay/cmd.rs @@ -14,7 +14,7 @@ use mega_evm::{ primitives::eip4844, DatabaseRef, }, - sandbox::trace::SharedTracingInspector, + sandbox::trace::{SandboxTracer, SharedTracingInspector}, BlockLimits, EvmTxRuntimeLimits, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforks, MegaSpecId, }; @@ -475,7 +475,7 @@ impl Cmd { let sandbox = self .trace_args .is_tracing_enabled() - .then(|| SharedTracingInspector::new(self.trace_args.create_inspector())); + .then(|| SandboxTracer::handle(self.trace_args.inspector_config())); let mut state = StateBuilder::new().with_database(&mut database).with_bundle_update().build(); let mut block_executor = block_executor_factory.create_executor_with_inspector( @@ -527,7 +527,7 @@ impl Cmd { block_executor.inspector_mut().fuse(); if let Some(sandbox) = &sandbox { - block_executor.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); + block_executor.set_keyless_sandbox_observer(std::rc::Rc::clone(sandbox)); } let outcome = block_executor .run_transaction(wrapped_tx) diff --git a/bin/mega-evme/tests/replay_keyless.rs b/bin/mega-evme/tests/replay_keyless.rs index 49037c5e..0cb3832e 100644 --- a/bin/mega-evme/tests/replay_keyless.rs +++ b/bin/mega-evme/tests/replay_keyless.rs @@ -93,8 +93,10 @@ fn test_replay_keyless_opcode_tracer_includes_sandbox_steps() { // Synthetic CREATE grafted onto the intercepted CALL (depth 1, geth convention). assert_eq!(logs[0]["op"].as_str(), Some("CREATE")); assert_eq!(logs[0]["depth"].as_u64(), Some(1)); - assert_eq!(logs[0]["gas"].as_u64(), Some(636_184)); - assert_eq!(logs[0]["gasCost"].as_u64(), Some(0)); + // `gas` is the intercepting frame's remaining gas once the 100K dispatch overhead is + // charged; `gasCost` is the gas reserved for the sandbox (the caller's override on REX2). + assert_eq!(logs[0]["gas"].as_u64(), Some(536_184)); + assert_eq!(logs[0]["gasCost"].as_u64(), Some(26_018_741)); // Sandbox constructor starts on the next step at intercept-frame depth + 1. assert_eq!(logs[1]["op"].as_str(), Some("PUSH1")); diff --git a/crates/mega-evm/benches/mega_bench.rs b/crates/mega-evm/benches/mega_bench.rs index a29a3b28..151c5a50 100644 --- a/crates/mega-evm/benches/mega_bench.rs +++ b/crates/mega-evm/benches/mega_bench.rs @@ -19,21 +19,27 @@ #![allow(missing_docs)] use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization}; -use alloy_primitives::{address, Address, Bytes, U256}; +use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, revm::inspector::NoOpInspector, + sandbox::{SandboxDb, SandboxObserver}, test_utils::{BytecodeBuilder, MemoryDatabase}, - EmptyExternalEnv, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + EmptyExternalEnv, ExternalEnvTypes, IKeylessDeploy, MegaContext, MegaEvm, MegaSpecId, + MegaTransaction, KEYLESS_DEPLOY_ADDRESS, }; use revm::{ bytecode::opcode::{ - ADD, CALL, COINBASE, CREATE, CREATE2, DELEGATECALL, GAS, LOG0, LOG1, LOG2, LOG4, NUMBER, - POP, PUSH0, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, TIMESTAMP, + ADD, CALL, CODECOPY, COINBASE, CREATE, CREATE2, DELEGATECALL, GAS, LOG0, LOG1, LOG2, LOG4, + NUMBER, POP, PUSH0, RETURN, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, TIMESTAMP, }, context::tx::TxEnvBuilder, + interpreter::{interpreter::EthInterpreter, CallInputs, CreateInputs, Interpreter}, ExecuteEvm as _, }; +use std::{cell::RefCell, rc::Rc}; mod common; use common::{ @@ -950,6 +956,121 @@ fn bench_oracle_real_data(c: &mut Criterion) { group.finish(); } +// +// ============================================================================ +// Keyless Sandbox Hook Benchmark +// ============================================================================ +// +// A `KeylessDeploy` call whose constructor spends most of its gas on a compute +// loop, run with and without a sandbox observer attached. The `observer` row +// pays the per-opcode `InspectorBridge` → `ReadOnlyHook` → observer forwarding +// on top of the plain sandbox interception, which is what `no_hook` measures. +// + +const KEYLESS_SANDBOX_ITERATIONS: usize = 100; + +/// The cheapest observer that still exercises every forwarding hop. +#[derive(Default)] +struct CountingObserver { + steps: u64, + frames: u64, +} + +impl SandboxObserver for CountingObserver { + fn step( + &mut self, + _interp: &mut Interpreter, + _context: &mut MegaContext, E>, + ) { + self.steps += 1; + } + + fn call(&mut self, _context: &mut MegaContext, E>, _inputs: &CallInputs) { + self.frames += 1; + } + + fn create(&mut self, _context: &mut MegaContext, E>, _inputs: &CreateInputs) { + self.frames += 1; + } +} + +/// `keylessDeploy` calldata wrapping a pre-EIP-155 deployment of `init_code`, plus the +/// keyless signer it recovers to. +fn keyless_deploy_call_data(init_code: Bytes) -> (Bytes, Address) { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: 5_000_000, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes([0x22; 32]); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let signer = signed.recover_signer().expect("keyless signer recovers"); + let mut raw = Vec::new(); + signed.rlp_encode(&mut raw); + let call = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: raw.into(), + gasLimitOverride: U256::from(100_000_000_u64), + } + .abi_encode(); + (call.into(), signer) +} + +fn bench_keyless_sandbox_hook(c: &mut Criterion) { + let mut init_code = BytecodeBuilder::default(); + for _ in 0..KEYLESS_SANDBOX_ITERATIONS { + init_code = init_code.push_number(1u64).push_number(2u64).append(ADD).append(POP); + } + let init_code = init_code + .push_number(1u8) + .push_number(0u8) + .push_number(0u8) + .append(CODECOPY) + .push_number(1u8) + .push_number(0u8) + .append(RETURN) + .build(); + let (call_data, signer) = keyless_deploy_call_data(init_code); + + let mut group = c.benchmark_group("keyless_sandbox_hook"); + group.sample_size(10); + for (row, attach_observer) in [("rex5/no_hook", false), ("rex5/observer", true)] { + group.bench_function(row, |b| { + b.iter(|| { + let mut db = MemoryDatabase::default(); + db.set_account_balance(signer, U256::from(10).pow(U256::from(21))); + db.set_account_balance(CALLER, U256::from(10).pow(U256::from(18))); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX5); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + if attach_observer { + evm.set_keyless_sandbox_observer(Rc::new(RefCell::new( + CountingObserver::default(), + ))); + } + let tx = TxEnvBuilder::new() + .caller(CALLER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(FEATURE_GAS_LIMIT) + .data(call_data.clone()) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let result = evm.transact(tx).expect("keyless deploy executes"); + assert!(result.result.is_success(), "keyless deploy should succeed"); + black_box(result) + }) + }); + } + group.finish(); +} + criterion_group!( benches, bench_volatile_data, @@ -967,5 +1088,6 @@ criterion_group!( bench_eip7702_authlist, bench_staticcall_selfdestruct, bench_salt_dynamic_gas, + bench_keyless_sandbox_hook, ); criterion_main!(benches); diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 0feec5d4..d9f9b250 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -158,7 +158,7 @@ where /// must implement [`crate::sandbox::SandboxObserver`] for both this executor's /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use /// [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) where O: crate::sandbox::SandboxObserver + crate::sandbox::SandboxObserver @@ -174,7 +174,7 @@ where /// must implement [`crate::sandbox::SandboxInspector`] for both this executor's /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use /// [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) where I: crate::sandbox::SandboxInspector + crate::sandbox::SandboxInspector diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index eaf3418b..d943d6da 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -28,7 +28,8 @@ Read-only default: `SandboxObserver` cannot short-circuit `CALL`/`CREATE` and mu Rewriting explicit: `SandboxInspector` forwards `&mut` inputs and override return values so interventions take effect inside the sandbox as they would on a top-level EVM. Both channels share one type-erased slot (`Rc>>`, held twice: parent env type and `EmptyExternalEnv`); an observer is installed behind the crate-private `ReadOnlyHook` adapter, which forwards shared references and never answers a `CALL`/`CREATE` override. Types generic over revm's `Inspector` get both channels through blanket impls; hosts behind a generic EVM projection (a node's `ConfigureEvm::Evm`) name the attach operation on their own configuration type instead of on mega-evm. -With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` handle for the outer EVM plus a second one on the observer channel, and `splice_sandbox_traces` to graft the sandbox call tree under the intercepted `KeylessDeploy` CALL after execution; `mega-evme` and node tracing RPCs use the same implementation. +With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` for the outer EVM paired with a `SandboxTracer` on the observer channel, which records each sandbox execution in an arena of its own keyed by the intercepted call, and `splice_sandbox_traces`, which grafts those arenas under their `KeylessDeploy` CALL frames after execution and leaves the tracer empty; `mega-evme` and node tracing RPCs use the same implementation. +Lifecycle events (`sandbox_start` / `sandbox_end`) are delivered on the slot that also receives the sandbox's opcode-level hooks: `EmptyExternalEnv` pre-REX4, the parent env from REX4 on. Contract: 1. With no hook attached, the sandbox path is unchanged. diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 72a1262e..4b295518 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -507,30 +507,23 @@ impl MegaContext { /// Attaching an observer does not change sandbox external-env semantics at /// any spec. Replaces any attached inspector. /// - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. Passing `None` - /// also clears both slots, but type inference for `O` often fails on that - /// path. Observation is read-only: mutating interpreter or context state - /// through the hooks is undefined and may diverge consensus. There is no - /// take/drain API; recorded data stays in the caller's observer. - pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + /// [`Self::clear_keyless_sandbox_hook`] is the only way to detach. Observation + /// is read-only: mutating interpreter or context state through the hooks is + /// undefined and may diverge consensus. There is no take/drain API; recorded + /// data stays in the caller's observer. + pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) where O: SandboxObserver + SandboxObserver + 'static, ExtEnvs: 'static, { - match observer { - Some(obs) => { - let cloned = Rc::clone(&obs); - let parent: Rc>> = cloned; - let empty: Rc>> = obs; - let parent: SandboxHookHandle = - Rc::new(RefCell::new(ReadOnlyHook::new(parent))); - let empty: SandboxHookHandle = - Rc::new(RefCell::new(ReadOnlyHook::new(empty))); - self.keyless_sandbox_hook = Some(parent); - self.keyless_sandbox_hook_empty = Some(empty); - } - None => self.clear_keyless_sandbox_hook(), - } + let cloned = Rc::clone(&observer); + let parent: Rc>> = cloned; + let empty: Rc>> = observer; + let parent: SandboxHookHandle = Rc::new(RefCell::new(ReadOnlyHook::new(parent))); + let empty: SandboxHookHandle = + Rc::new(RefCell::new(ReadOnlyHook::new(empty))); + self.keyless_sandbox_hook = Some(parent); + self.keyless_sandbox_hook_empty = Some(empty); } /// Attaches a rewriting inspector for nested sandbox execution on every spec. @@ -548,29 +541,21 @@ impl MegaContext { /// usage are the post-intervention values. The channel is node-local and /// non-consensus. /// - /// Use [`Self::clear_keyless_sandbox_hook`] to detach. Passing `None` also - /// clears both slots, but type inference for `I` often fails on that path. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + /// [`Self::clear_keyless_sandbox_hook`] is the only way to detach. + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) where I: SandboxInspector + SandboxInspector + 'static, { - match inspector { - Some(insp) => { - let cloned = Rc::clone(&insp); - let parent: SandboxHookHandle = cloned; - let empty: SandboxHookHandle = insp; - self.keyless_sandbox_hook = Some(parent); - self.keyless_sandbox_hook_empty = Some(empty); - } - None => self.clear_keyless_sandbox_hook(), - } + let cloned = Rc::clone(&inspector); + let parent: SandboxHookHandle = cloned; + let empty: SandboxHookHandle = inspector; + self.keyless_sandbox_hook = Some(parent); + self.keyless_sandbox_hook_empty = Some(empty); } /// Detaches any sandbox hook from both env-type slots. /// - /// Restores the no-hook sandbox path. Prefer this over passing `None` to - /// [`Self::set_keyless_sandbox_observer`] or - /// [`Self::set_keyless_sandbox_inspector`] when the generic cannot be inferred. + /// Restores the no-hook sandbox path. pub fn clear_keyless_sandbox_hook(&mut self) { self.keyless_sandbox_hook = None; self.keyless_sandbox_hook_empty = None; @@ -1015,7 +1000,7 @@ mod tests { #[test] fn test_clear_keyless_sandbox_hook_clears_both_slots() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); @@ -1025,17 +1010,18 @@ mod tests { } #[test] - fn test_set_keyless_sandbox_inspector_none_clears_both_slots() { + fn test_attaching_one_channel_replaces_the_other() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); - context.set_keyless_sandbox_inspector(None::>>); - assert!(context.keyless_sandbox_hook.is_none()); - assert!(context.keyless_sandbox_hook_empty.is_none()); + let observer = Rc::new(RefCell::new(NopObserver)); + context.set_keyless_sandbox_observer(Rc::clone(&observer)); + assert_eq!(Rc::strong_count(&observer), 3, "both slots hold the observer"); - context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopInspector)))); + context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopInspector))); + assert_eq!(Rc::strong_count(&observer), 1, "the inspector replaced the observer"); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); - context.set_keyless_sandbox_observer(None::>>); + + context.clear_keyless_sandbox_hook(); assert!(context.keyless_sandbox_hook.is_none()); assert!(context.keyless_sandbox_hook_empty.is_none()); } @@ -1043,7 +1029,7 @@ mod tests { #[test] fn test_with_db_keeps_the_sandbox_hook() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(NopInspector)))); + context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopInspector))); let context = context.with_db(EmptyDB::default()); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); @@ -1054,7 +1040,7 @@ mod tests { use revm::handler::EvmTr; let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); let mut evm = crate::MegaEvm::new(context); evm.clear_keyless_sandbox_hook(); assert!(evm.ctx_ref().keyless_sandbox_hook.is_none()); @@ -1075,7 +1061,7 @@ mod tests { #[should_panic(expected = "set sandbox hook after external envs are wired")] fn test_with_external_envs_panics_in_debug_when_observer_is_attached() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Some(Rc::new(RefCell::new(NopObserver)))); + context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); let _ = context.with_external_envs(TestExternalEnvs::::new().into()); } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index b3c6cb5e..ed93d938 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -243,7 +243,7 @@ impl MegaEvm { /// must implement [`SandboxObserver`] for both this EVM's `ExtEnvs` and /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to /// detach. - pub fn set_keyless_sandbox_observer(&mut self, observer: Option>>) + pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) where O: SandboxObserver + SandboxObserver + 'static, ExtEnvs: 'static, @@ -256,7 +256,7 @@ impl MegaEvm { /// Forwards to [`MegaContext::set_keyless_sandbox_inspector`]. The inspector /// must implement [`SandboxInspector`] for both this EVM's `ExtEnvs` and /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Option>>) + pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) where I: SandboxInspector + SandboxInspector + 'static, { diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index ba5f1edc..c89a4945 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -56,9 +56,9 @@ use tracing::{error, warn}; use crate::{ constants, inspect_account_code_hash, mark_frame_result_as_exceeding_limit, AdditionalLimit, - EvmTxRuntimeLimits, ExternalEnvTypes, JournalInspectTr, LimitCheck, LimitUsage, MegaContext, - MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TxRuntimeLimit, VolatileDataAccess, - SANDBOX_TX_SOURCE_HASH, + EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvTypes, JournalInspectTr, LimitCheck, + LimitUsage, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TxRuntimeLimit, + VolatileDataAccess, SANDBOX_TX_SOURCE_HASH, }; use super::{ @@ -69,7 +69,10 @@ use super::{ use super::{ error::{encode_error_result, KeylessDeployError}, inspector::{InspectorBridge, SandboxHookHandle}, - observer::{SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, SandboxStartInfo}, + observer::{ + OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, + SandboxStartInfo, + }, state::SandboxDb, }; @@ -103,6 +106,7 @@ use super::{ pub fn execute_keyless_deploy_call( ctx: &mut MegaContext, call_inputs: &revm::interpreter::CallInputs, + depth: usize, tx_bytes: &Bytes, gas_limit_override: U256, ) -> FrameResult { @@ -431,6 +435,7 @@ pub fn execute_keyless_deploy_call // exit). The unconditional success follows from step 4b's // `gas_limit_override.min(gas.remaining())` cap — there is no intervening gas // movement between the cap and this debit. + let outer_gas_remaining = gas.remaining(); if ctx.spec.is_enabled(MegaSpecId::REX5) { let ok = gas.record_cost(effective_gas_limit); debug_assert!( @@ -443,15 +448,25 @@ pub fn execute_keyless_deploy_call // // INVARIANT: when a hook is attached, `sandbox_start` and `sandbox_end` // fire exactly once on this path, covering every terminal arm below. - let hook = ctx.keyless_sandbox_hook.clone(); - if let Some(h) = &hook { - h.borrow_mut().sandbox_start(&SandboxStartInfo { + // Lifecycle events go to the slot that also receives this sandbox's opcode-level + // hooks, so a hook with one impl per env sees the whole sandbox on one impl. + let lifecycle = LifecycleHook::select(ctx); + if lifecycle.is_attached() { + lifecycle.start(&SandboxStartInfo { spec: ctx.spec, signer: deploy_signer, deploy_address, gas_limit_override: gas_limit_override_u64, effective_gas_limit, tx_gas_limit, + outer_call: OuterCallInfo { + depth, + caller: call_inputs.caller, + gas_limit: call_inputs.gas_limit, + gas_remaining: outer_gas_remaining, + value: call_inputs.call_value(), + input: call_inputs.input.bytes(ctx), + }, }); } @@ -469,12 +484,9 @@ pub fn execute_keyless_deploy_call gas_used, &return_memory_offset, ) { - notify_sandbox_end( - &hook, - SandboxEndOutcome::NotApplied { - reason: SandboxRejectKind::PostAccountingHalt, - }, - ); + lifecycle.end(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::PostAccountingHalt, + }); return halt; } } @@ -485,21 +497,16 @@ pub fn execute_keyless_deploy_call // state on this path. if let SandboxCompletion::Deployed { deploy_address: deployed, .. } = &completion { if *deployed != deploy_address { - notify_sandbox_end( - &hook, - SandboxEndOutcome::NotApplied { - reason: SandboxRejectKind::AddressMismatch, - }, - ); + lifecycle.end(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::AddressMismatch, + }); return make_error!(KeylessDeployError::AddressMismatch); } } if let Err(e) = apply_sandbox_state(ctx, state, deploy_signer) { - notify_sandbox_end( - &hook, - SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::ApplyFailed }, - ); + lifecycle + .end(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::ApplyFailed }); return make_error!(e); } @@ -512,26 +519,20 @@ pub fn execute_keyless_deploy_call for log in logs { ctx.log(log); } - notify_sandbox_end( - &hook, - SandboxEndOutcome::Applied { - completion: SandboxCompletionKind::Deployed, - gas_used, - }, - ); + lifecycle.end(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::Deployed, + gas_used, + }); make_success!(gas_used, deployed) } SandboxCompletion::EmptyCode { gas_used, logs } => { for log in logs { ctx.log(log); } - notify_sandbox_end( - &hook, - SandboxEndOutcome::Applied { - completion: SandboxCompletionKind::EmptyCode, - gas_used, - }, - ); + lifecycle.end(SandboxEndOutcome::Applied { + completion: SandboxCompletionKind::EmptyCode, + gas_used, + }); make_execution_failure!( gas_used, KeylessDeployError::EmptyCodeDeployed { gas_used } @@ -542,17 +543,44 @@ pub fn execute_keyless_deploy_call // nonce bump from `make_create_frame`, plus any committed sandbox // writes) persists; outer caller sees the failure reason in // `errorData`. - notify_sandbox_end( - &hook, - SandboxEndOutcome::Applied { - completion: SandboxCompletionKind::ExecutionFailed, - gas_used, - }, - ); + // + // Pre-REX5 empty-code deploys travel this arm on the wire (logs + // dropped, frozen behavior); the hook still learns what the sandbox + // EVM did, so the completion kind is `EmptyCode` on every spec. + let completion = + if matches!(error, KeylessDeployError::EmptyCodeDeployed { .. }) { + SandboxCompletionKind::EmptyCode + } else { + SandboxCompletionKind::ExecutionFailed + }; + lifecycle.end(SandboxEndOutcome::Applied { completion, gas_used }); make_execution_failure!(gas_used, error) } } } + SandboxOutcome::NoContractCreated { gas_used, limit_usage, volatile_accesses } => { + // The sandbox ran a top-level create frame that produced no address, which only + // an inspector `create` override can do. The sandbox did execute, so charge it + // exactly like `AddressMismatch`: REX5+ books gas and usage, nothing is applied. + if ctx.spec.is_enabled(MegaSpecId::REX5) { + if let Some(halt) = apply_sandbox_post_accounting( + ctx, + &mut gas, + limit_usage, + volatile_accesses, + effective_gas_limit, + gas_used, + &return_memory_offset, + ) { + lifecycle.end(SandboxEndOutcome::NotApplied { + reason: SandboxRejectKind::PostAccountingHalt, + }); + return halt; + } + } + lifecycle.end(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }); + make_error!(KeylessDeployError::NoContractCreated) + } SandboxOutcome::Rejected(e) => { // Sandbox bailed before producing a frame — typically a validate-reject // (`FailedDeposit` → `InvalidTransaction`) or an internal error. Refund @@ -562,10 +590,7 @@ pub fn execute_keyless_deploy_call if ctx.spec.is_enabled(MegaSpecId::REX5) { gas.erase_cost(effective_gas_limit); } - notify_sandbox_end( - &hook, - SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }, - ); + lifecycle.end(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }); make_error!(e) } } @@ -750,13 +775,47 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( } } -/// Delivers `sandbox_end` when a hook is attached. -fn notify_sandbox_end( - hook: &Option>, - outcome: SandboxEndOutcome, -) { - if let Some(h) = hook { - h.borrow_mut().sandbox_end(&outcome); +/// The hook slot that receives one sandbox's lifecycle events. +/// +/// Mirrors the slot selection of `execute_keyless_deploy_sandbox`: REX4+ sandboxes share +/// the parent env and dispatch opcode-level hooks through the parent-env slot, pre-REX4 +/// sandboxes run on `EmptyExternalEnv` and dispatch through that slot. Lifecycle events +/// follow the same rule so a hook with one impl per env sees a sandbox on one impl only. +enum LifecycleHook { + Parent(Option>), + Empty(Option>), +} + +impl LifecycleHook { + fn select(ctx: &MegaContext) -> Self { + if ctx.spec.is_enabled(MegaSpecId::REX4) { + Self::Parent(ctx.keyless_sandbox_hook.clone()) + } else { + Self::Empty(ctx.keyless_sandbox_hook_empty.clone()) + } + } + + fn is_attached(&self) -> bool { + match self { + Self::Parent(hook) => hook.is_some(), + Self::Empty(hook) => hook.is_some(), + } + } + + fn start(&self, info: &SandboxStartInfo) { + match self { + Self::Parent(Some(hook)) => hook.borrow_mut().sandbox_start(info), + Self::Empty(Some(hook)) => hook.borrow_mut().sandbox_start(info), + Self::Parent(None) | Self::Empty(None) => {} + } + } + + fn end(&self, outcome: SandboxEndOutcome) { + match self { + Self::Parent(Some(hook)) => hook.borrow_mut().sandbox_end(&outcome), + Self::Empty(Some(hook)) => hook.borrow_mut().sandbox_end(&outcome), + Self::Parent(None) | Self::Empty(None) => {} + } } } @@ -794,6 +853,18 @@ pub enum SandboxOutcome { /// or `InternalError`). No state, resource usage, or volatile-access footprint /// applies — the outer caller must refund the full pre-debited reservation. Rejected(KeylessDeployError), + /// Sandbox EVM ran, but its top-level create frame produced no address. Only an + /// inspector `create` override returning `address: None` reaches this arm; the no-hook + /// path always creates. The sandbox executed, so gas and resource usage are charged + /// like `Completed`, while no state is applied. + NoContractCreated { + /// Gas consumed by the sandbox EVM execution. + gas_used: u64, + /// Resource usage from the sandbox's additional limit trackers. + limit_usage: LimitUsage, + /// Volatile-access footprint to merge into the parent after sandbox return. + volatile_accesses: VolatileDataAccess, + }, } /// Wire-shape dispatch for a completed sandbox execution. @@ -905,10 +976,15 @@ fn process_sandbox_transact_result( ExecutionResult::Success { gas_used, output, logs, .. } => { let revm::context::result::Output::Create(bytecode, Some(created_addr)) = output else { - // Contract creation didn't return an address — should never happen - // (the sandbox tx always asks for `TxKind::Create`), but we return - // a Rejected outcome instead of panicking to avoid crashing the node. - return SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated); + // Contract creation returned no address. The sandbox tx always asks for + // `TxKind::Create`, so revm never produces this on its own; an + // inspector `create` override answering `address: None` does. The + // sandbox executed, so its gas and usage are reported for charging. + return SandboxOutcome::NoContractCreated { + gas_used, + limit_usage, + volatile_accesses, + }; }; // REX6: also treat same-tx create + SELFDESTRUCT as empty-code. revm reports // `Success` with the constructor's returned bytecode, but @@ -1360,13 +1436,14 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated)), + matches!(out, SandboxOutcome::NoContractCreated { gas_used: 1, .. }), "unexpected: {out:?}", ); } - /// `Output::Create(_, None)` — Create succeeded but revm did not return an address. - /// Same defensive `NoContractCreated` shape as the Call branch. + /// `Output::Create(_, None)` — the create frame produced no address (an inspector `create` + /// override). Same `NoContractCreated` shape as the Call branch, carrying the gas used so + /// the caller can charge it. #[test] fn test_process_result_create_without_address_maps_to_no_contract_created() { let result: Result, FakeTxErr> = Ok(ResultAndState { @@ -1387,7 +1464,7 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated)), + matches!(out, SandboxOutcome::NoContractCreated { gas_used: 1, .. }), "unexpected: {out:?}", ); } @@ -1846,7 +1923,7 @@ mod tests { }); (created, slot) } - SandboxOutcome::Rejected(_) => (false, None), + SandboxOutcome::Rejected(_) | SandboxOutcome::NoContractCreated { .. } => (false, None), } } diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index e2e6cd4d..333735a5 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -61,7 +61,7 @@ //! [`execute_keyless_deploy_call`] //! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution //! - `inspector` - Rewriting [`SandboxInspector`] channel into nested sandbox execution -//! - `trace` - Shared `revm-inspectors` splicing helpers (`inspectors` feature) +//! - `trace` - Shared `revm-inspectors` recorder and splicing helpers (`inspectors` feature) //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal //! - `tx` - Transaction decoding and validation for pre-EIP-155 transactions @@ -93,7 +93,8 @@ //! pre-REX4 are delivered through a second slot typed against //! [`crate::EmptyExternalEnv`], so a hook implements its trait for both the parent env //! type and [`crate::EmptyExternalEnv`]; revm inspectors that are generic over the -//! context satisfy both through the blanket impls. +//! context satisfy both through the blanket impls. Lifecycle events follow the same +//! slot rule, so a hook with one impl per env sees a sandbox entirely on one impl. //! //! [`SandboxObserver`]: observer::SandboxObserver //! [`SandboxInspector`]: inspector::SandboxInspector diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index d6b802a2..971ea530 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -44,6 +44,11 @@ //! events; whether sandbox state was applied to the parent is reported by //! [`SandboxEndOutcome::state_applied`]. //! +//! Lifecycle events are delivered on the same slot as that sandbox's opcode-level +//! hooks: the [`crate::EmptyExternalEnv`] impl for pre-REX4 sandboxes, the parent-env +//! impl for REX4+ (see below). A type with one impl per env therefore always sees a +//! sandbox's `sandbox_start`, opcode hooks, and `sandbox_end` on the same impl. +//! //! # External-environment invariance //! //! Attaching an observer must not change sandbox env semantics at any spec. @@ -60,7 +65,7 @@ use alloc as std; use core::cell::RefCell; use std::rc::Rc; -use alloy_primitives::{Address, Log, U256}; +use alloy_primitives::{Address, Bytes, Log, U256}; use revm::{ interpreter::{ interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, @@ -94,6 +99,33 @@ pub struct SandboxStartInfo { pub effective_gas_limit: u64, /// Gas limit carried by the signed keyless transaction itself. pub tx_gas_limit: u64, + /// The intercepted `KeylessDeploy` call frame, as an inspector on the outer EVM saw it. + pub outer_call: OuterCallInfo, +} + +/// The intercepted `KeylessDeploy` call that started a sandbox, described with the fields an +/// inspector on the outer EVM records for that frame. +/// +/// A tracer that records the outer EVM and the sandbox separately uses this to pair a +/// sandbox with the outer frame it belongs under: the fields match what revm's `call` hook +/// received for the intercepted frame. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OuterCallInfo { + /// Call depth of the intercepted frame; `0` for a transaction's top-level call. + pub depth: usize, + /// `msg.sender` of the intercepted call. + pub caller: Address, + /// Gas limit of the intercepted call frame, as handed to it by the outer EVM. + pub gas_limit: u64, + /// Gas remaining in the intercepted frame when the sandbox started: after the dispatch + /// overhead and any REX5+ materialization charge, before the sandbox reservation was + /// debited. + pub gas_remaining: u64, + /// `msg.value` of the intercepted call. + pub value: U256, + /// Full calldata of the intercepted call. + pub input: Bytes, } /// Terminal outcome of one sandbox execution, delivered exactly once. @@ -102,7 +134,7 @@ pub struct SandboxStartInfo { pub enum SandboxEndOutcome { /// Sandbox completed and its state was applied to the parent journal. Applied { - /// Wire-shape completion reported to the outer caller. + /// How the sandbox EVM completed. completion: SandboxCompletionKind, /// Gas consumed by the sandbox EVM. gas_used: u64, @@ -115,14 +147,22 @@ pub enum SandboxEndOutcome { } /// Completion kind for a sandbox whose state was applied to the parent. +/// +/// The kind describes what the sandbox EVM did, on every spec alike. What the outer +/// caller is told differs by spec only for [`Self::EmptyCode`]. #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SandboxCompletionKind { /// Inner CREATE succeeded with non-empty runtime bytecode. Deployed, - /// Inner CREATE succeeded but returned empty runtime bytecode. + /// Inner CREATE ran to a successful exit but left no code at the deploy address: + /// it returned empty runtime bytecode, or (REX6+) the account self-destructed in the + /// same transaction. + /// + /// The outer caller sees `EmptyCodeDeployed` errorData on every spec. REX5+ forwards + /// the constructor's logs into the parent receipt; pre-REX5 drops them. EmptyCode, - /// Sandbox EVM execution failed (revert or halt) after producing mergeable state. + /// Sandbox EVM execution reverted or halted after producing mergeable state. ExecutionFailed, } @@ -130,7 +170,10 @@ pub enum SandboxCompletionKind { #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SandboxRejectKind { - /// Validate-reject or internal error before a mergeable frame was produced. + /// Validate-reject or internal error before a mergeable frame was produced, or a + /// top-level create result that carries no address. Only an inspector `create` + /// override produces the latter; it is charged like [`Self::AddressMismatch`] and + /// nothing is applied. Rejected, /// REX5+ post-execution resource accounting rejected the sandbox. PostAccountingHalt, diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index b17d12ec..f5941878 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -2,38 +2,64 @@ //! //! A [`TracingInspector`] attached to the outer EVM never sees sandbox frames: the sandbox //! is a separate EVM whose journal reports its top-level CREATE at depth 0, and recording that -//! into the outer arena would overwrite the CALL root. The pattern here is two inspectors — -//! the outer one installed on the EVM and a second one attached through the keyless sandbox -//! observer channel — followed by [`splice_sandbox_traces`], which grafts the sandbox arena -//! under the intercepted `KeylessDeploy` CALL after the transaction returns. +//! into the outer arena would overwrite the CALL root. The pattern here is two recorders: the +//! outer [`TracingInspector`] installed on the EVM, and a [`SandboxTracer`] attached through +//! the keyless sandbox observer channel, which records every sandbox execution into an arena +//! of its own. After the transaction returns, [`splice_sandbox_traces`] grafts each sandbox +//! arena under the outer `KeylessDeploy` CALL frame that started it. +//! +//! ```ignore +//! let (outer, sandbox) = paired(TracingInspectorConfig::all()); +//! let mut evm = MegaEvm::new(ctx).with_inspector(outer.clone()); +//! evm.set_keyless_sandbox_observer(sandbox.clone()); +//! let result = evm.inspect_tx(tx)?; +//! splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); +//! ``` +//! +//! When the outer inspector is owned elsewhere (a node's tracing RPC hands out a plain +//! [`TracingInspector`]), attach [`SandboxTracer::handle`] on its own and splice into that +//! inspector after each transaction. Splicing consumes the recorded sandboxes, so it is +//! idempotent and a tracer reused across transactions never carries frames over: a sandbox +//! whose outer frame is not in the arena being spliced into is dropped. //! //! Requires the `inspectors` feature. #[cfg(not(feature = "std"))] use alloc as std; use core::cell::{Ref, RefCell, RefMut}; -use std::{rc::Rc, vec::Vec}; +use std::{rc::Rc, vec, vec::Vec}; use alloy_primitives::{Address, Bytes, Log, U256}; use revm::{ bytecode::opcode::{self, OpCode}, context::ContextTr, inspector::JournalExt, - interpreter::{CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter}, + interpreter::{ + interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + InstructionResult, Interpreter, + }, Inspector, }; use revm_inspectors::tracing::{ - types::{CallTraceStep, TraceMemberOrder}, - TracingInspector, + types::{CallTraceNode, CallTraceStep, TraceMemberOrder}, + TracingInspector, TracingInspectorConfig, }; -use crate::KEYLESS_DEPLOY_ADDRESS; +use crate::{ExternalEnvTypes, MegaContext, MegaSpecId, KEYLESS_DEPLOY_ADDRESS}; + +use super::{ + observer::{OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, SandboxStartInfo}, + state::SandboxDb, + SandboxObserver, +}; /// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. /// -/// One handle serves as the outer inspector; a second, independent handle is attached to the -/// sandbox through [`Self::as_sandbox_observer`]. Both are `Clone`, so a caller can keep a -/// handle to read the recorded traces after execution. +/// Serves as the outer EVM inspector when the caller wants to keep a handle to read the +/// recorded traces after execution. It is the outer half of [`paired`]; the sandbox half is a +/// [`SandboxTracer`]. Do not attach a handle to this type through the sandbox observer +/// channel: the sandbox would record into the outer arena and overwrite the `KeylessDeploy` +/// CALL root with its own CREATE. #[derive(Clone, Debug)] pub struct SharedTracingInspector(Rc>); @@ -57,11 +83,6 @@ impl SharedTracingInspector { pub fn borrow_mut(&self) -> RefMut<'_, TracingInspector> { self.0.borrow_mut() } - - /// Observer handle for the read-only keyless sandbox channel. - pub fn as_sandbox_observer(&self) -> Rc> { - Rc::new(RefCell::new(self.clone())) - } } impl Inspector for SharedTracingInspector @@ -69,31 +90,31 @@ where CTX: ContextTr, { fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.borrow_mut().initialize_interp(interp, context); + Inspector::initialize_interp(&mut *self.0.borrow_mut(), interp, context); } fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.borrow_mut().step(interp, context); + Inspector::step(&mut *self.0.borrow_mut(), interp, context); } fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.borrow_mut().step_end(interp, context); + Inspector::step_end(&mut *self.0.borrow_mut(), interp, context); } fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { - self.0.borrow_mut().log(interp, context, log); + Inspector::log(&mut *self.0.borrow_mut(), interp, context, log); } fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { - self.0.borrow_mut().call(context, inputs) + Inspector::call(&mut *self.0.borrow_mut(), context, inputs) } fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.0.borrow_mut().call_end(context, inputs, outcome); + Inspector::call_end(&mut *self.0.borrow_mut(), context, inputs, outcome); } fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { - self.0.borrow_mut().create(context, inputs) + Inspector::create(&mut *self.0.borrow_mut(), context, inputs) } fn create_end( @@ -102,7 +123,7 @@ where inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { - self.0.borrow_mut().create_end(context, inputs, outcome); + Inspector::create_end(&mut *self.0.borrow_mut(), context, inputs, outcome); } fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { @@ -115,72 +136,277 @@ where } } -/// Returns true when `sandbox` recorded at least one completed frame. -pub fn sandbox_has_frames(sandbox: &TracingInspector) -> bool { - has_frames(sandbox) +/// Builds an outer inspector and a sandbox tracer with the same `config`, ready to be +/// attached to one EVM: the first as its inspector, the second through +/// `set_keyless_sandbox_observer`. +pub fn paired( + config: TracingInspectorConfig, +) -> (SharedTracingInspector, Rc>) { + (SharedTracingInspector::new(TracingInspector::new(config)), SandboxTracer::handle(config)) +} + +/// One recorded sandbox execution, waiting to be grafted under its outer frame. +#[derive(Debug)] +struct SandboxTrace { + /// Lifecycle context delivered at `sandbox_start`; carries the outer frame identity. + start: SandboxStartInfo, + /// Arena of the sandbox EVM, one per execution. + tracer: TracingInspector, + /// Whether the sandbox EVM entered a frame; a validate-reject leaves this `false`. + entered: bool, +} + +impl SandboxTrace { + /// Finalizes the arena once the sandbox reported its outcome. + fn finish(&mut self, outcome: &SandboxEndOutcome) { + if !self.entered { + return; + } + let nodes = self.tracer.traces_mut().nodes_mut(); + + // A frame the sandbox never closed (a database error aborted the sandbox from inside + // a nested frame, so revm unwound without `create_end`) would otherwise render as a + // successful CREATE with no gas used. Close it the way revm reports the abort. + for node in nodes.iter_mut() { + if node.trace.status.is_none() { + node.trace.status = Some(InstructionResult::FatalExternalError); + node.trace.success = false; + node.trace.gas_used = node.trace.gas_limit; + } + } + + // Pre-REX5, an empty-code deployment drops the constructor's logs from the receipt + // even though the CREATE frame completed successfully (frozen behavior). Geth's + // `withLog` rendering shows a log only when the receipt holds it, so strip them here. + let logs_dropped = matches!( + outcome, + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::EmptyCode, .. } + ) && !self.start.spec.is_enabled(MegaSpecId::REX5); + if logs_dropped { + for node in nodes.iter_mut() { + node.logs.clear(); + node.ordering.retain(|member| !matches!(member, TraceMemberOrder::Log(_))); + } + } + } +} + +/// Sandbox-side recorder for the keyless sandbox observer channel. +/// +/// Records each sandbox execution into a fresh [`TracingInspector`] arena and keeps it, +/// together with the identity of the outer `KeylessDeploy` frame that started it, until +/// [`splice_sandbox_traces`] grafts it into the outer trace. Attach it via +/// `set_keyless_sandbox_observer`; it implements [`SandboxObserver`] for every env type. +/// +/// Recording is per execution, so a tracer kept across transactions does not need a reset; +/// splicing drains what was recorded, and [`Self::clear`] discards it. +#[derive(Debug)] +pub struct SandboxTracer { + config: TracingInspectorConfig, + current: Option, + finished: Vec, +} + +impl SandboxTracer { + /// A tracer whose sandbox arenas use `config`. + pub fn new(config: TracingInspectorConfig) -> Self { + Self { config, current: None, finished: Vec::new() } + } + + /// [`Self::new`] wrapped for `set_keyless_sandbox_observer`. + pub fn handle(config: TracingInspectorConfig) -> Rc> { + Rc::new(RefCell::new(Self::new(config))) + } + + /// Number of recorded sandbox executions not yet spliced. + pub fn pending(&self) -> usize { + self.finished.len() + } + + /// Discards every recorded sandbox execution, including one in flight. + pub fn clear(&mut self) { + self.current = None; + self.finished.clear(); + } + + fn with_current(&mut self, f: impl FnOnce(&mut SandboxTrace) -> R) -> Option { + self.current.as_mut().map(f) + } +} + +impl SandboxObserver for SandboxTracer { + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.with_current(|t| Inspector::initialize_interp(&mut t.tracer, interp, context)); + } + + fn step( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.with_current(|t| Inspector::step(&mut t.tracer, interp, context)); + } + + fn step_end( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + ) { + self.with_current(|t| Inspector::step_end(&mut t.tracer, interp, context)); + } + + fn log( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, E>, + log: Log, + ) { + self.with_current(|t| Inspector::log(&mut t.tracer, interp, context, log)); + } + + fn call(&mut self, context: &mut MegaContext, E>, inputs: &CallInputs) { + self.with_current(|t| { + t.entered = true; + let mut inputs = inputs.clone(); + let _ = Inspector::call(&mut t.tracer, context, &mut inputs); + }); + } + + fn call_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CallInputs, + outcome: &CallOutcome, + ) { + self.with_current(|t| { + let mut outcome = outcome.clone(); + Inspector::call_end(&mut t.tracer, context, inputs, &mut outcome); + }); + } + + fn create(&mut self, context: &mut MegaContext, E>, inputs: &CreateInputs) { + self.with_current(|t| { + t.entered = true; + let mut inputs = inputs.clone(); + let _ = Inspector::create(&mut t.tracer, context, &mut inputs); + }); + } + + fn create_end( + &mut self, + context: &mut MegaContext, E>, + inputs: &CreateInputs, + outcome: &CreateOutcome, + ) { + self.with_current(|t| { + let mut outcome = outcome.clone(); + Inspector::create_end(&mut t.tracer, context, inputs, &mut outcome); + }); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.with_current(|t| { + , E>>>::selfdestruct( + &mut t.tracer, + contract, + target, + value, + ); + }); + } + + fn sandbox_start(&mut self, info: &SandboxStartInfo) { + debug_assert!(self.current.is_none(), "sandbox_start while a sandbox is in flight"); + self.current = Some(SandboxTrace { + start: info.clone(), + tracer: TracingInspector::new(self.config), + entered: false, + }); + } + + fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { + if let Some(mut trace) = self.current.take() { + trace.finish(outcome); + self.finished.push(trace); + } + } } -/// Whether `inspector` recorded any frame. A fresh or fused arena still holds one default -/// root node, so emptiness is judged by content, not by node count. -fn has_frames(inspector: &TracingInspector) -> bool { - inspector.traces().nodes().iter().any(|node| { - node.trace.status.is_some() || !node.trace.steps.is_empty() || !node.children.is_empty() - }) +/// Whether `node` is the outer frame described by `call`: the intercepted `KeylessDeploy` +/// CALL, matched on the fields revm's `call` hook recorded for it. +fn is_outer_frame(node: &CallTraceNode, call: &OuterCallInfo) -> bool { + let trace = &node.trace; + trace.address == KEYLESS_DEPLOY_ADDRESS && + !trace.kind.is_any_create() && + trace.depth == call.depth && + trace.caller == call.caller && + trace.gas_limit == call.gas_limit && + trace.value == call.value && + trace.data == call.input } -/// Attaches `sandbox` traces as children of the outer [`KEYLESS_DEPLOY_ADDRESS`] CALL. +/// Grafts every sandbox execution recorded by `sandbox` under the outer `KeylessDeploy` CALL +/// frame that started it, then leaves `sandbox` empty. +/// +/// Each recorded sandbox is paired with the first unclaimed outer frame that matches the +/// call the sandbox reported at `sandbox_start`; frames are matched in arena order, which is +/// execution order. A sandbox whose frame is not in `outer` (the outer inspector was fused +/// before splicing, or belongs to another transaction) is dropped. A sandbox that never +/// entered a frame (a validate-reject) claims its frame and grafts nothing. /// -/// Sandbox execution uses a fresh journal whose top-level CREATE is depth 0. Recording that -/// into the outer inspector would replace the CALL root, so the sandbox is traced on a -/// sibling inspector and grafted here. Depths and arena indices are rewritten so the Geth -/// call tree hangs the CREATE under the CALL. When the sandbox recorded nothing, `outer` is -/// left untouched. +/// Sandbox execution uses a fresh journal whose top-level CREATE is depth 0. Depths and arena +/// indices are rewritten so the Geth call tree hangs the CREATE under the CALL. /// /// Geth-style struct logs only descend into a child when the parent frame has a matching /// call-like opcode in its steps. An intercepted `KeylessDeploy` CALL records no bytecode, so /// a CREATE step is grafted onto the parent; without it the sandbox constructor steps would -/// be omitted from struct-log output instead of nested in execution order. -/// -/// An `outer` without a `KeylessDeploy` CALL frame is also left untouched. That covers an -/// arena that recorded no frames at all (already fused, or never run) and one that traced a -/// transaction which never entered the sandbox while `sandbox` still holds frames from an -/// earlier transaction; grafting under the default root or under an unrelated frame would -/// invent a call tree the outer EVM never executed. -pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &TracingInspector) { - if !has_frames(sandbox) || !has_frames(outer) { - return; - } - - let Some(parent_idx) = outer.traces().nodes().iter().rposition(|node| { - node.trace.address == KEYLESS_DEPLOY_ADDRESS && !node.trace.kind.is_any_create() - }) else { - return; - }; +/// be omitted from struct-log output instead of nested in execution order. That step reports +/// the outer frame's remaining gas at sandbox start as `gas` and the sandbox reservation as +/// `gasCost`; the contract never executed it. +pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &mut SandboxTracer) { + let original_len = outer.traces().nodes().len(); + let mut claimed = vec![false; original_len]; + + for trace in sandbox.finished.drain(..) { + let parent_idx = outer.traces().nodes()[..original_len] + .iter() + .enumerate() + .position(|(idx, node)| !claimed[idx] && is_outer_frame(node, &trace.start.outer_call)); + let Some(parent_idx) = parent_idx else { + continue; + }; + claimed[parent_idx] = true; + if trace.entered { + graft(outer, parent_idx, trace); + } + } +} + +/// Moves the arena of `trace` under `outer`'s node `parent_idx`. +fn graft(outer: &mut TracingInspector, parent_idx: usize, mut trace: SandboxTrace) { let depth_offset = outer.traces().nodes()[parent_idx].trace.depth + 1; let base = outer.traces().nodes().len(); - let remapped: Vec<_> = sandbox - .traces() - .nodes() - .iter() - .cloned() - .enumerate() - .map(|(old_idx, mut node)| { - node.idx = base + old_idx; - node.trace.depth += depth_offset; - for step in &mut node.trace.steps { - step.depth += depth_offset as u64; - } - node.parent = Some(match node.parent { - None => parent_idx, - Some(parent) => base + parent, - }); - for child in &mut node.children { - *child += base; - } - node - }) - .collect(); + let sandbox_nodes = core::mem::take(trace.tracer.traces_mut().nodes_mut()); + let remapped = sandbox_nodes.into_iter().enumerate().map(|(old_idx, mut node)| { + node.idx = base + old_idx; + node.trace.depth += depth_offset; + for step in &mut node.trace.steps { + step.depth += depth_offset as u64; + } + node.parent = Some(match node.parent { + None => parent_idx, + Some(parent) => base + parent, + }); + for child in &mut node.children { + *child += base; + } + node + }); outer.traces_mut().nodes_mut().extend(remapped); let parent = &mut outer.traces_mut().nodes_mut()[parent_idx]; @@ -194,8 +420,12 @@ pub fn splice_sandbox_traces(outer: &mut TracingInspector, sandbox: &TracingInsp let step_idx = parent.trace.steps.len(); let create_depth = parent.trace.depth as u64 + 1; let contract = parent.trace.address; - let gas_remaining = parent.trace.gas_limit; - parent.trace.steps.push(intercepted_create_step(create_depth, contract, gas_remaining)); + parent.trace.steps.push(intercepted_create_step( + create_depth, + contract, + trace.start.outer_call.gas_remaining, + trace.start.effective_gas_limit, + )); parent.ordering.push(TraceMemberOrder::Step(step_idx)); } parent.ordering.push(TraceMemberOrder::Call(child_slot)); @@ -216,7 +446,17 @@ fn is_calllike_opcode(op: u8) -> bool { /// Placeholder CREATE recorded on an intercepted `KeylessDeploy` CALL so struct-log output can /// descend into the spliced sandbox frame. -fn intercepted_create_step(depth: u64, contract: Address, gas_remaining: u64) -> CallTraceStep { +/// +/// `gas_remaining` is the outer frame's gas when the sandbox started; `gas_cost` is the gas +/// reserved for the sandbox, the way Geth reports the gas handed to a callee on the calling +/// step. Pre-REX5 the reservation is the caller's override and can exceed the outer frame's +/// gas; the sandbox pays for itself there. +fn intercepted_create_step( + depth: u64, + contract: Address, + gas_remaining: u64, + gas_cost: u64, +) -> CallTraceStep { CallTraceStep { depth, pc: 0, @@ -229,7 +469,7 @@ fn intercepted_create_step(depth: u64, contract: Address, gas_remaining: u64) -> gas_remaining, gas_refund_counter: 0, gas_used: 0, - gas_cost: 0, + gas_cost, storage_change: None, status: None, immediate_bytes: None, diff --git a/crates/mega-evm/src/system/intercept.rs b/crates/mega-evm/src/system/intercept.rs index 6420340e..8d59309a 100644 --- a/crates/mega-evm/src/system/intercept.rs +++ b/crates/mega-evm/src/system/intercept.rs @@ -317,6 +317,7 @@ impl SystemContractInterceptor::new().into()); } @@ -1302,7 +1328,7 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_inspector(Some(Rc::clone(&rec))); + context.set_keyless_sandbox_inspector(Rc::clone(&rec)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs index 38ea0a00..913a6ae9 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs @@ -535,9 +535,12 @@ fn test_sandbox_end_applied_execution_failed_on_constructor_revert() { } } +/// An empty-code deployment is reported as `EmptyCode` on every spec, while the outer +/// caller's wire shape is the frozen one: `EmptyCodeDeployed` errorData everywhere, with the +/// constructor's logs forwarded only from REX5 on. #[test] -fn test_sandbox_end_applied_empty_code_on_rex5() { - for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { +fn test_sandbox_end_applied_empty_code_on_every_spec() { + for spec in SPECS { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(empty_code_constructor()); let mut db = funded_db(signer); let (result, events) = run_with_recorder(spec, &mut db, tx_bytes, None); @@ -549,6 +552,100 @@ fn test_sandbox_end_applied_empty_code_on_rex5() { }) => {} other => panic!("{spec:?}: expected Applied(EmptyCode), got {other:?}"), } + let output = result.result.output().expect("empty-code outer output"); + let ret = IKeylessDeploy::keylessDeployCall::abi_decode_returns(output) + .expect("empty-code ABI return"); + assert_eq!(ret.deployedAddress, Address::ZERO, "{spec:?}: nothing deployed"); + assert!( + matches!( + decode_error_result(&ret.errorData), + Some(KeylessDeployError::EmptyCodeDeployed { .. }) + ), + "{spec:?}: the wire shape stays EmptyCodeDeployed" + ); + } +} + +/// Records, per env impl, which hooks a dual-impl observer received. +#[derive(Default)] +struct SplitImplObserver { + /// Events seen by the `EmptyExternalEnv` impl. + empty: Vec<&'static str>, + /// Events seen by the parent-env impl. + parent: Vec<&'static str>, +} + +impl SandboxObserver for SplitImplObserver { + fn step( + &mut self, + _interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext< + mega_evm::sandbox::SandboxDb<'_>, + mega_evm::EmptyExternalEnv, + >, + ) { + if self.empty.last() != Some(&"step") { + self.empty.push("step"); + } + } + + fn sandbox_start(&mut self, _info: &SandboxStartInfo) { + self.empty.push("start"); + } + + fn sandbox_end(&mut self, _outcome: &SandboxEndOutcome) { + self.empty.push("end"); + } +} + +impl SandboxObserver for SplitImplObserver { + fn step( + &mut self, + _interp: &mut Interpreter, + _context: &mut mega_evm::MegaContext< + mega_evm::sandbox::SandboxDb<'_>, + mega_evm::TestExternalEnvs, + >, + ) { + if self.parent.last() != Some(&"step") { + self.parent.push("step"); + } + } + + fn sandbox_start(&mut self, _info: &SandboxStartInfo) { + self.parent.push("start"); + } + + fn sandbox_end(&mut self, _outcome: &SandboxEndOutcome) { + self.parent.push("end"); + } +} + +/// With a non-empty parent env, an observer with one impl per env sees a sandbox's +/// lifecycle and opcode hooks on the same impl: `EmptyExternalEnv` pre-REX4, the parent env +/// from REX4 on. +#[test] +fn test_lifecycle_and_opcode_hooks_land_on_the_same_env_impl() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let observer = Rc::new(RefCell::new(SplitImplObserver::default())); + let result = run_keyless_with_parent_env( + spec, + &mut db, + tx_bytes, + crowded_parent_env(), + Some(Rc::clone(&observer)), + ); + assert!(result.result.is_success(), "{spec:?}: {:?}", result.result); + let observer = observer.borrow(); + let (used, idle, label) = if spec.is_enabled(MegaSpecId::REX4) { + (&observer.parent, &observer.empty, "parent env") + } else { + (&observer.empty, &observer.parent, "EmptyExternalEnv") + }; + assert_eq!(used, &["start", "step", "end"], "{spec:?}: whole sandbox on the {label} impl"); + assert!(idle.is_empty(), "{spec:?}: the other impl sees nothing, got {idle:?}"); } } @@ -741,7 +838,7 @@ fn test_observer_channel_drops_call_overrides() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_inspector(Some(Rc::new(RefCell::new(OverridingInspector)))); + context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(OverridingInspector))); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); let rewritten = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("inspector channel"); @@ -897,7 +994,7 @@ fn test_sandbox_start_info_saturates_gas_limit_override_above_u64_max() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(Some(Rc::clone(&recorder))); + context.set_keyless_sandbox_observer(Rc::clone(&recorder)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx_with_override_u256(tx_bytes, U256::MAX, DEFAULT_OUTER_GAS_LIMIT); @@ -936,7 +1033,7 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(Some(Rc::clone(&recorder))); + context.set_keyless_sandbox_observer(Rc::clone(&recorder)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 22c9993e..4509999a 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -305,7 +305,9 @@ where if let Some(limits) = config.tx_limits { context = context.with_tx_runtime_limits(limits); } - context.set_keyless_sandbox_observer(config.observer); + if let Some(observer) = config.observer { + context.set_keyless_sandbox_observer(observer); + } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx_with_outer_gas( config.tx_bytes, @@ -383,7 +385,9 @@ where chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(observer); + if let Some(observer) = observer { + context.set_keyless_sandbox_observer(observer); + } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("keyless deploy transact"); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs index 6ff3af61..d16e9770 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs @@ -1,10 +1,10 @@ -//! Tests for `sandbox::trace`: a `TracingInspector` shared between the outer EVM and the -//! keyless sandbox through either hook channel, and `splice_sandbox_traces`, which grafts the -//! sandbox frames under the intercepted `KeylessDeploy` CALL. +//! Tests for `sandbox::trace`: the paired outer inspector and `SandboxTracer`, and +//! `splice_sandbox_traces`, which grafts each recorded sandbox under the intercepted +//! `KeylessDeploy` CALL that started it. //! //! Compiled only with the `inspectors` feature, which gates `sandbox::trace` itself. -use std::convert::Infallible; +use std::{cell::RefCell, convert::Infallible, rc::Rc}; use alloy_consensus::{transaction::Recovered, Signed, TxLegacy}; use alloy_evm::{block::BlockExecutor, EvmEnv}; @@ -12,17 +12,28 @@ use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall; use mega_evm::{ - revm::{context::result::ResultAndState, InspectEvm}, - sandbox::trace::{sandbox_has_frames, splice_sandbox_traces, SharedTracingInspector}, - test_utils::{deep_mixed_init, MemoryDatabase, REVERTING_RUNTIME}, + constants::rex2::KEYLESS_DEPLOY_OVERHEAD_GAS, + revm::{ + context::result::{ExecutionResult, ResultAndState}, + InspectEvm, + }, + sandbox::{ + decode_error_result, + trace::{paired, splice_sandbox_traces, SandboxTracer, SharedTracingInspector}, + KeylessDeployError, + }, + test_utils::{ + deep_mixed_init, BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase, REVERTING_RUNTIME, + }, BlockLimits, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaContext, MegaEvm, MegaEvmFactory, MegaHaltReason, MegaHardforkConfig, MegaSpecId, MegaTransaction, MegaTxEnvelope, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, }; use revm::{ - bytecode::opcode::{OpCode, CALL, CREATE, STOP}, + bytecode::opcode::{OpCode, BALANCE, CALL, CODECOPY, CREATE, LOG0, POP, PUSH0, RETURN, STOP}, context::{BlockEnv, CfgEnv, TxEnv}, database::State, + interpreter::InstructionResult, }; use revm_inspectors::tracing::{ types::{CallTraceNode, TraceMemberOrder}, @@ -31,63 +42,90 @@ use revm_inspectors::tracing::{ use super::keyless_sandbox_support::{ assert_result_and_state_eq, constructor_calls_reverter, create_pre_eip155_deploy_tx, - empty_code_constructor, funded_db, keyless_deploy_call_tx, selfdestructing_constructor, - success_constructor, DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, - REVERTER, SPECS, TEST_CALLER, + empty_code_constructor, funded_db, keyless_deploy_call_tx, run_keyless, + selfdestructing_constructor, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, + LARGE_GAS_LIMIT_OVERRIDE, LARGE_SIGNER_BALANCE, MERGE_FAIL_SENTINEL, REVERTER, SPECS, + TEST_CALLER, }; /// A contract whose runtime is a single `STOP`, for transactions that never enter the sandbox. const STOPPER: Address = address!("0000000000000000000000000000000000cccccc"); +/// A helper whose runtime reads the balance of [`MERGE_FAIL_SENTINEL`], the account an +/// [`ErrorInjectingDatabase`] fails on. +const SENTINEL_READER: Address = address!("0000000000000000000000000000000000dddddd"); -/// Which keyless sandbox channel the sandbox tracer is attached through. -#[derive(Clone, Copy, Debug)] -enum Channel { - Observer, - Inspector, +type SandboxHandle = Rc>; + +fn all_config() -> TracingInspectorConfig { + TracingInspectorConfig::all() +} + +/// Init code that `CALL`s `target` and then deploys one byte of runtime code. +fn constructor_calls(target: Address) -> Bytes { + BytecodeBuilder::default() + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_number(0_u8) + .push_address(target) + .push_number(50_000_u32) + .append(CALL) + .append(POP) + .push_number(1_u8) + .push_number(0_u8) + .push_number(0_u8) + .append(CODECOPY) + .push_number(1_u8) + .push_number(0_u8) + .append(RETURN) + .build() } -fn tracer() -> SharedTracingInspector { - SharedTracingInspector::new(TracingInspector::new(TracingInspectorConfig::all())) +/// Init code that emits one empty `LOG0` and returns empty runtime code. +fn logging_empty_code_constructor() -> Bytes { + BytecodeBuilder::default().append_many([PUSH0, PUSH0, LOG0, PUSH0, PUSH0, RETURN]).build() +} + +fn configured_context( + db: DB, + spec: MegaSpecId, +) -> MegaContext { + 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); + }); + context } /// Runs one keyless deploy of `tx_bytes` on a fresh `MegaEvm` with `outer` installed as the -/// EVM inspector and `sandbox` attached through `channel`. +/// EVM inspector and `sandbox` attached through the observer channel. fn trace_keyless_deploy( spec: MegaSpecId, db: &mut MemoryDatabase, tx_bytes: Bytes, outer: SharedTracingInspector, - sandbox: &SharedTracingInspector, - channel: Channel, + sandbox: &SandboxHandle, ) -> ResultAndState { - 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); - }); + let context = configured_context(db, spec); let mut evm = MegaEvm::new(context).with_inspector(outer); - match channel { - Channel::Observer => { - evm.set_keyless_sandbox_observer(Some(sandbox.as_sandbox_observer())); - } - Channel::Inspector => { - evm.set_keyless_sandbox_inspector(Some(sandbox.as_sandbox_observer())); - } - } + evm.set_keyless_sandbox_observer(Rc::clone(sandbox)); evm.inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) .expect("keyless deploy transact") } -/// Runs a plain call to [`STOPPER`] with `outer` installed, so the arena holds a frame that is -/// not a `KeylessDeploy` CALL. -fn trace_plain_call(db: &mut MemoryDatabase, outer: SharedTracingInspector) { +/// Runs a plain call to [`STOPPER`] with `outer` installed and `sandbox` attached, so the +/// outer arena holds a frame that is not a `KeylessDeploy` CALL. +fn trace_plain_call( + db: &mut MemoryDatabase, + outer: SharedTracingInspector, + sandbox: &SandboxHandle, +) { db.set_account_code(STOPPER, Bytes::from_static(&[STOP])); - let mut context = MegaContext::new(db, MegaSpecId::REX5); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); + let context = configured_context(db, MegaSpecId::REX5); let mut evm = MegaEvm::new(context).with_inspector(outer); + evm.set_keyless_sandbox_observer(Rc::clone(sandbox)); let tx = TxEnv { caller: TEST_CALLER, kind: TxKind::Call(STOPPER), @@ -101,22 +139,21 @@ fn trace_plain_call(db: &mut MemoryDatabase, outer: SharedTracingInspector) { assert!(result.result.is_success(), "plain call must succeed: {:?}", result.result); } -/// Traces the three-level `deep_mixed_init` deployment through `channel` and returns the outer -/// tracer with the sandbox frames spliced in, plus the execution result and the deployer. +/// Traces the three-level `deep_mixed_init` deployment and splices it, returning the outer +/// tracer, the sandbox tracer, the execution result, and the deployer. fn traced_deep_mixed( spec: MegaSpecId, - channel: Channel, -) -> (SharedTracingInspector, SharedTracingInspector, ResultAndState, Address) { +) -> (SharedTracingInspector, SandboxHandle, ResultAndState, Address) { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); let mut db = funded_db(signer); db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); - let outer = tracer(); - let sandbox = tracer(); - assert!(!sandbox_has_frames(&sandbox.borrow()), "{spec:?}: fresh sandbox tracer is empty"); - let result = trace_keyless_deploy(spec, &mut db, tx_bytes, outer.clone(), &sandbox, channel); - assert!(result.result.is_success(), "{spec:?} {channel:?}: {:?}", result.result); - assert!(sandbox_has_frames(&sandbox.borrow()), "{spec:?}: sandbox recorded the CREATE"); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + let (outer, sandbox) = paired(all_config()); + assert_eq!(sandbox.borrow().pending(), 0, "{spec:?}: fresh tracer records nothing"); + let result = trace_keyless_deploy(spec, &mut db, tx_bytes, outer.clone(), &sandbox); + assert!(result.result.is_success(), "{spec:?}: {:?}", result.result); + assert_eq!(sandbox.borrow().pending(), 1, "{spec:?}: one sandbox execution recorded"); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0, "{spec:?}: splicing drains the tracer"); (outer, sandbox, result, signer) } @@ -172,9 +209,20 @@ fn assert_deep_mixed_shape(outer: &SharedTracingInspector, signer: Address, case assert_eq!(root.trace.depth, 0, "{case}: root depth"); assert!(!root.trace.kind.is_any_create(), "{case}: root is a CALL"); assert_eq!(root.children.len(), 1, "{case}: the sandbox CREATE hangs under the root"); + let synthetic = + root.trace.steps.iter().find(|step| step.op.get() == CREATE).unwrap_or_else(|| { + panic!("{case}: the intercepted CALL carries a synthetic CREATE step") + }); + assert_eq!(synthetic.contract, KEYLESS_DEPLOY_ADDRESS, "{case}: synthetic step contract"); + assert_eq!( + synthetic.gas_cost, LARGE_GAS_LIMIT_OVERRIDE, + "{case}: the synthetic step charges the sandbox reservation" + ); assert!( - root.trace.steps.iter().any(|step| step.op.get() == CREATE), - "{case}: the intercepted CALL carries a synthetic CREATE step" + synthetic.gas_remaining <= DEFAULT_OUTER_GAS_LIMIT - KEYLESS_DEPLOY_OVERHEAD_GAS && + synthetic.gas_remaining > DEFAULT_OUTER_GAS_LIMIT - 1_000_000, + "{case}: the synthetic step reports the outer frame's remaining gas, got {}", + synthetic.gas_remaining ); assert!( matches!(root.ordering.last(), Some(TraceMemberOrder::Call(0))), @@ -217,121 +265,226 @@ fn assert_deep_mixed_shape(outer: &SharedTracingInspector, signer: Address, case } #[test] -fn test_observer_channel_tracer_splices_deep_mixed_shape() { +fn test_tracer_splices_deep_mixed_shape_with_no_hook_parity() { for spec in SPECS { - let (outer, _sandbox, _result, signer) = traced_deep_mixed(spec, Channel::Observer); - assert_deep_mixed_shape(&outer, signer, &format!("observer {spec:?}")); + let (outer, _sandbox, result, signer) = traced_deep_mixed(spec); + assert_deep_mixed_shape(&outer, signer, &format!("{spec:?}")); + + let (tx_bytes, _) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let mut db = funded_db(signer); + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let baseline = run_keyless(RunConfig { + spec, + db: &mut db, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + observer: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_result_and_state_eq(&result, &baseline, &format!("traced {spec:?}")); } } +/// Splicing consumes what the tracer recorded: a second splice grafts nothing more, and +/// `fuse` on the outer handle starts the next transaction from a clean arena. #[test] -fn test_inspector_channel_tracer_splices_same_shape_and_result() { - for spec in SPECS { - let (obs_outer, _, obs_result, signer) = traced_deep_mixed(spec, Channel::Observer); - let (insp_outer, _, insp_result, insp_signer) = traced_deep_mixed(spec, Channel::Inspector); - assert_eq!(signer, insp_signer); - assert_deep_mixed_shape(&insp_outer, signer, &format!("inspector {spec:?}")); - assert_result_and_state_eq(&insp_result, &obs_result, &format!("inspector {spec:?}")); - - let obs_ref = obs_outer.borrow(); - let insp_ref = insp_outer.borrow(); - let shape = |nodes: &[CallTraceNode]| -> Vec<(usize, bool, Address, Option)> { - nodes - .iter() - .map(|n| (n.trace.depth, n.trace.kind.is_any_create(), n.trace.address, n.parent)) - .collect() - }; +fn test_splice_is_idempotent_and_fuse_resets_the_outer() { + let (outer, sandbox, _result, _signer) = traced_deep_mixed(MegaSpecId::REX5); + let before = outer.borrow().traces().nodes().len(); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_eq!(outer.borrow().traces().nodes().len(), before, "a second splice is a no-op"); + assert_eq!(outer.borrow().traces().nodes()[0].children.len(), 1, "no duplicate child"); + + outer.fuse(); + assert_eq!(outer.borrow().traces().nodes().len(), 1, "a fused arena keeps its default root"); + assert!(outer.borrow().traces().nodes()[0].children.is_empty()); +} + +/// One pair of handles serves consecutive transactions: each splice grafts only the +/// sandbox of the transaction just traced, and nothing from an earlier one. +#[test] +fn test_one_tracer_pair_across_two_transactions() { + let (outer, sandbox) = paired(all_config()); + + let (tx_a, signer_a) = create_pre_eip155_deploy_tx(deep_mixed_init(REVERTER)); + let mut db = funded_db(signer_a); + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let result = trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_a, outer.clone(), &sandbox); + assert!(result.result.is_success(), "{:?}", result.result); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_deep_mixed_shape(&outer, signer_a, "tx A"); + + outer.fuse(); + let (tx_b, signer_b) = create_pre_eip155_deploy_tx(success_constructor()); + assert_ne!(signer_a, signer_b); + let mut db = funded_db(signer_b); + let result = trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_b, outer.clone(), &sandbox); + assert!(result.result.is_success(), "{:?}", result.result); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + { + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "tx B"); assert_eq!( - shape(obs_ref.traces().nodes()), - shape(insp_ref.traces().nodes()), - "{spec:?}: both channels graft the same tree" + nodes.len(), + 2, + "tx B: the outer CALL and one sandbox CREATE, nothing from tx A" ); + assert_eq!(nodes[0].children, vec![1]); + assert_eq!(nodes[1].trace.address, signer_b.create(0)); + assert!(nodes[1].children.is_empty()); } } +/// A recorded sandbox whose outer frame is not in the arena being spliced into is dropped: +/// an empty outer, an outer holding an unrelated transaction, and an outer that was fused +/// before the splice all end up untouched, and the tracer is left empty either way. #[test] -fn test_fuse_resets_both_tracers() { - let (outer, sandbox, _result, _signer) = traced_deep_mixed(MegaSpecId::REX5, Channel::Observer); - assert!(sandbox_has_frames(&outer.borrow())); - assert!(sandbox_has_frames(&sandbox.borrow())); +fn test_splice_without_the_outer_frame_drops_the_sandbox() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); - sandbox.fuse(); + // Never spliced before the outer moved on to a plain transaction. + let (outer, sandbox) = paired(all_config()); + let mut db = funded_db(signer); + trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes.clone(), outer.clone(), &sandbox); + assert_eq!(sandbox.borrow().pending(), 1); outer.fuse(); - assert!(!sandbox_has_frames(&sandbox.borrow()), "fused sandbox tracer is empty"); - assert!(!sandbox_has_frames(&outer.borrow()), "fused outer tracer is empty"); - assert_eq!(outer.borrow().traces().nodes().len(), 1, "a fused arena keeps its default root"); + let mut db = MemoryDatabase::default(); + trace_plain_call(&mut db, outer.clone(), &sandbox); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0, "the stale sandbox is dropped, not kept"); + { + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes.len(), 1, "no sandbox frame is appended to an unrelated trace"); + assert_eq!(nodes[0].trace.address, STOPPER); + assert!(nodes[0].children.is_empty(), "the unrelated frame gains no child"); + assert!( + nodes[0].trace.steps.iter().all(|step| step.op.get() != CREATE), + "no synthetic CREATE step is added to an unrelated frame" + ); + } + + // Spliced into an arena that recorded nothing. + let (outer, sandbox) = paired(all_config()); + let mut db = funded_db(signer); + trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); + let mut empty = TracingInspector::new(all_config()); + splice_sandbox_traces(&mut empty, &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0); + assert_eq!(empty.traces().nodes().len(), 1, "the arena keeps only its default root"); + assert!(empty.traces().nodes()[0].children.is_empty()); } -/// Splicing into an outer tracer that recorded no frames, or one whose frames contain no -/// `KeylessDeploy` CALL, leaves the outer arena untouched even though the sandbox holds frames. +/// A database error inside a nested sandbox frame aborts the sandbox before the root CREATE +/// closes. The grafted frames report that abort instead of rendering as a success. #[test] -fn test_splice_without_keyless_call_in_outer_is_a_no_op() { - let (_outer, sandbox, _result, _signer) = - traced_deep_mixed(MegaSpecId::REX5, Channel::Observer); - assert!(sandbox_has_frames(&sandbox.borrow())); +fn test_sandbox_aborted_by_database_error_is_grafted_as_a_fatal_error() { + for config in [TracingInspectorConfig::all(), TracingInspectorConfig::default_parity()] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(constructor_calls(SENTINEL_READER)); + let mut inner = funded_db(signer); + inner.set_account_code( + SENTINEL_READER, + BytecodeBuilder::default() + .push_address(MERGE_FAIL_SENTINEL) + .append(BALANCE) + .append(POP) + .stop() + .build(), + ); + let mut db = ErrorInjectingDatabase::new(inner); + db.fail_on_account = Some(MERGE_FAIL_SENTINEL); + + let outer = SharedTracingInspector::new(TracingInspector::new(config)); + let sandbox = SandboxTracer::handle(config); + let context = configured_context(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context).with_inspector(outer.clone()); + evm.set_keyless_sandbox_observer(Rc::clone(&sandbox)); + let result = evm + .inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) + .expect("outer transact"); + let error = match &result.result { + ExecutionResult::Revert { output, .. } => decode_error_result(output), + other => panic!("the aborted sandbox reverts the outer call, got {other:?}"), + }; + assert!(matches!(error, Some(KeylessDeployError::InternalError)), "got {error:?}"); - let mut empty = TracingInspector::new(TracingInspectorConfig::all()); - splice_sandbox_traces(&mut empty, &sandbox.borrow()); - assert!(!sandbox_has_frames(&empty), "nothing is grafted under the default root"); - assert_eq!(empty.traces().nodes().len(), 1); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, "aborted"); + assert_eq!(nodes.len(), 3, "outer CALL, sandbox CREATE, helper CALL"); + assert!(!nodes[0].trace.success, "the outer CALL reverted"); - let plain = tracer(); - let mut db = MemoryDatabase::default(); - trace_plain_call(&mut db, plain.clone()); - assert!(sandbox_has_frames(&plain.borrow()), "the plain call recorded a frame"); - let before = plain.borrow().traces().nodes().len(); - splice_sandbox_traces(&mut plain.borrow_mut(), &sandbox.borrow()); - let plain_ref = plain.borrow(); - let nodes = plain_ref.traces().nodes(); - assert_eq!(nodes.len(), before, "no sandbox frame is appended to an unrelated trace"); - assert_eq!(nodes[0].trace.address, STOPPER); - assert!(nodes[0].children.is_empty(), "the unrelated frame gains no child"); - assert!( - nodes[0].trace.steps.iter().all(|step| step.op.get() != CREATE), - "no synthetic CREATE step is added to an unrelated frame" - ); + let create = &nodes[nodes[0].children[0]]; + assert!(create.trace.kind.is_any_create()); + assert_eq!(create.trace.status, Some(InstructionResult::FatalExternalError)); + assert!(!create.trace.success, "an unclosed CREATE is not a success"); + assert_eq!(create.trace.gas_used, create.trace.gas_limit, "an aborted frame keeps no gas"); + + let helper = &nodes[create.children[0]]; + assert_eq!(helper.trace.address, SENTINEL_READER); + assert_eq!(helper.trace.status, Some(InstructionResult::FatalExternalError)); + assert!(!helper.trace.success); + } +} + +/// Pre-REX5, an empty-code deployment drops the constructor's logs from the receipt; the +/// grafted CREATE carries no logs there, and keeps them from REX5 on where the receipt has +/// them. +#[test] +fn test_empty_code_logs_follow_the_receipt() { + for spec in SPECS { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(logging_empty_code_constructor()); + let mut db = funded_db(signer); + let (outer, sandbox) = paired(all_config()); + let result = trace_keyless_deploy(spec, &mut db, tx_bytes, outer.clone(), &sandbox); + assert!(result.result.is_success(), "{spec:?}: {:?}", result.result); + let receipt_logs = result.result.logs().len(); + + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_arena_consistent(nodes, &format!("{spec:?}")); + let create = &nodes[nodes[0].children[0]]; + assert!(create.trace.success, "{spec:?}: the constructor returned"); + let ordered_logs = + create.ordering.iter().filter(|m| matches!(m, TraceMemberOrder::Log(_))).count(); + if spec.is_enabled(MegaSpecId::REX5) { + assert_eq!(receipt_logs, 1, "{spec:?}: the receipt keeps the log"); + assert_eq!(create.logs.len(), 1, "{spec:?}: the trace shows the log"); + assert_eq!(ordered_logs, 1, "{spec:?}: the log stays in the member order"); + } else { + assert_eq!(receipt_logs, 0, "{spec:?}: the receipt dropped the log"); + assert!(create.logs.is_empty(), "{spec:?}: the trace shows no log the receipt lacks"); + assert_eq!(ordered_logs, 0, "{spec:?}: no dangling log member"); + } + } } /// A parity-style tracer records no steps, so a leaf sandbox CREATE is a node with a status -/// but neither steps nor children. It still counts as a frame, is grafted, and the parent -/// receives the synthetic CREATE step that struct-log rendering needs. +/// but neither steps nor children. It is still grafted, and the parent receives the +/// synthetic CREATE step that struct-log rendering needs. #[test] fn test_tracer_without_steps_still_splices_a_leaf_create() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let outer = SharedTracingInspector::new(TracingInspector::new( - TracingInspectorConfig::default_parity(), - )); - let sandbox = SharedTracingInspector::new(TracingInspector::new( - TracingInspectorConfig::default_parity(), - )); - let result = trace_keyless_deploy( - MegaSpecId::REX5, - &mut db, - tx_bytes, - outer.clone(), - &sandbox, - Channel::Observer, - ); + let (outer, sandbox) = paired(TracingInspectorConfig::default_parity()); + let result = trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); assert!(result.result.is_success(), "{:?}", result.result); - { - let sandbox_ref = sandbox.borrow(); - let create = &sandbox_ref.traces().nodes()[0]; - assert!(create.trace.kind.is_any_create()); - assert!(create.trace.status.is_some(), "the CREATE frame completed"); - assert!(create.trace.steps.is_empty(), "parity config records no steps"); - assert!(create.children.is_empty(), "success_constructor makes no calls"); - } - assert!(sandbox_has_frames(&sandbox.borrow()), "a completed leaf frame counts"); - assert!(sandbox_has_frames(&outer.borrow())); - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); assert_arena_consistent(nodes, "parity"); assert_eq!(nodes.len(), 2); assert_eq!(nodes[0].children, vec![1]); - assert_eq!(nodes[1].trace.address, signer.create(0)); + let create = &nodes[1]; + assert_eq!(create.trace.address, signer.create(0)); + assert!(create.trace.status.is_some(), "the CREATE frame completed"); + assert!(create.trace.steps.is_empty(), "parity config records no steps"); + assert!(create.children.is_empty(), "success_constructor makes no calls"); assert_eq!(nodes[0].trace.steps.len(), 1, "exactly the synthetic CREATE step"); assert_eq!(nodes[0].trace.steps[0].op.get(), CREATE); assert_eq!(nodes[0].trace.steps[0].contract, KEYLESS_DEPLOY_ADDRESS); @@ -346,24 +499,18 @@ fn test_tracer_without_steps_still_splices_a_leaf_create() { /// already carries a call-like step per child gets no extra step. #[test] fn test_splice_adds_no_create_step_when_the_parent_has_a_call_like_step_per_child() { + // Borrow a real step from a plain call's trace and relabel it as a CALL. + let (plain_outer, plain_sandbox) = paired(all_config()); + let mut db = MemoryDatabase::default(); + trace_plain_call(&mut db, plain_outer.clone(), &plain_sandbox); + let mut call_step = plain_outer.borrow().traces().nodes()[0].trace.steps[0].clone(); + call_step.op = OpCode::new(CALL).expect("CALL is a defined opcode"); + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let outer = tracer(); - let sandbox = tracer(); - let result = trace_keyless_deploy( - MegaSpecId::REX5, - &mut db, - tx_bytes, - outer.clone(), - &sandbox, - Channel::Observer, - ); + let (outer, sandbox) = paired(all_config()); + let result = trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); assert!(result.result.is_success(), "{:?}", result.result); - - // Give the intercepted CALL one call-like step of its own before grafting. - let mut call_step = sandbox.borrow().traces().nodes()[0].trace.steps[0].clone(); - call_step.op = OpCode::new(CALL).expect("CALL is a defined opcode"); - call_step.depth = 1; { let mut outer_mut = outer.borrow_mut(); let root = &mut outer_mut.traces_mut().nodes_mut()[0]; @@ -372,7 +519,7 @@ fn test_splice_adds_no_create_step_when_the_parent_has_a_call_like_step_per_chil root.ordering.push(TraceMemberOrder::Step(0)); } - splice_sandbox_traces(&mut outer.borrow_mut(), &sandbox.borrow()); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); let outer_ref = outer.borrow(); let nodes = outer_ref.traces().nodes(); assert_arena_consistent(nodes, "paired"); @@ -382,43 +529,30 @@ fn test_splice_adds_no_create_step_when_the_parent_has_a_call_like_step_per_chil assert_eq!(nodes[0].ordering, vec![TraceMemberOrder::Step(0), TraceMemberOrder::Call(0)]); } -/// A `SELFDESTRUCT` inside the sandbox reaches the sandbox tracer's `selfdestruct` hook -/// through both channels, as it does on a top-level EVM. +/// A `SELFDESTRUCT` inside the sandbox reaches the sandbox tracer's `selfdestruct` hook. #[test] fn test_sandbox_selfdestruct_reaches_the_tracer() { - for channel in [Channel::Observer, Channel::Inspector] { - let (tx_bytes, signer) = create_pre_eip155_deploy_tx(selfdestructing_constructor()); - let mut db = funded_db(signer); - let outer = tracer(); - let sandbox = tracer(); - let result = trace_keyless_deploy( - MegaSpecId::REX5, - &mut db, - tx_bytes, - outer.clone(), - &sandbox, - channel, - ); - assert!(result.result.is_success(), "{channel:?}: {:?}", result.result); - - let sandbox_ref = sandbox.borrow(); - let create = sandbox_ref - .traces() - .nodes() - .iter() - .find(|node| node.trace.kind.is_any_create()) - .unwrap_or_else(|| panic!("{channel:?}: sandbox recorded the CREATE frame")); - assert_eq!( - create.trace.selfdestruct_address, - Some(signer.create(0)), - "{channel:?}: the tracer saw the constructor self-destruct" - ); - assert_eq!( - create.trace.selfdestruct_refund_target, - Some(Address::ZERO), - "{channel:?}: beneficiary as pushed by the constructor" - ); - } + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(selfdestructing_constructor()); + let mut db = funded_db(signer); + let (outer, sandbox) = paired(all_config()); + let result = trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); + assert!(result.result.is_success(), "{:?}", result.result); + + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + let create = &nodes[nodes[0].children[0]]; + assert!(create.trace.kind.is_any_create()); + assert_eq!( + create.trace.selfdestruct_address, + Some(signer.create(0)), + "the tracer saw the constructor self-destruct" + ); + assert_eq!( + create.trace.selfdestruct_refund_target, + Some(Address::ZERO), + "beneficiary as pushed by the constructor" + ); } fn keyless_deploy_envelope(nonce: u64, tx_bytes: Bytes) -> Recovered { @@ -440,8 +574,9 @@ fn keyless_deploy_envelope(nonce: u64, tx_bytes: Bytes) -> Recovered Date: Thu, 3 Sep 2026 15:49:37 +0800 Subject: [PATCH 25/28] test(sandbox): pin the outer-frame pairing key and SandboxTracer::clear Build SandboxStartInfo only when a hook is attached, so the no-hook path never materializes the intercepted call's calldata. --- crates/mega-evm/src/sandbox/execution.rs | 49 ++++++++---------- .../tests/rex2/keyless_sandbox_trace.rs | 51 ++++++++++++++++++- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index c89a4945..640fd900 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -451,24 +451,22 @@ pub fn execute_keyless_deploy_call // Lifecycle events go to the slot that also receives this sandbox's opcode-level // hooks, so a hook with one impl per env sees the whole sandbox on one impl. let lifecycle = LifecycleHook::select(ctx); - if lifecycle.is_attached() { - lifecycle.start(&SandboxStartInfo { - spec: ctx.spec, - signer: deploy_signer, - deploy_address, - gas_limit_override: gas_limit_override_u64, - effective_gas_limit, - tx_gas_limit, - outer_call: OuterCallInfo { - depth, - caller: call_inputs.caller, - gas_limit: call_inputs.gas_limit, - gas_remaining: outer_gas_remaining, - value: call_inputs.call_value(), - input: call_inputs.input.bytes(ctx), - }, - }); - } + lifecycle.start(|| SandboxStartInfo { + spec: ctx.spec, + signer: deploy_signer, + deploy_address, + gas_limit_override: gas_limit_override_u64, + effective_gas_limit, + tx_gas_limit, + outer_call: OuterCallInfo { + depth, + caller: call_inputs.caller, + gas_limit: call_inputs.gas_limit, + gas_remaining: outer_gas_remaining, + value: call_inputs.call_value(), + input: call_inputs.input.bytes(ctx), + }, + }); match execute_keyless_deploy_sandbox(ctx, sandbox_tx, sandbox_tx_limits) { SandboxOutcome::Completed { state, completion, limit_usage, volatile_accesses } => { @@ -795,17 +793,12 @@ impl LifecycleHook { } } - fn is_attached(&self) -> bool { - match self { - Self::Parent(hook) => hook.is_some(), - Self::Empty(hook) => hook.is_some(), - } - } - - fn start(&self, info: &SandboxStartInfo) { + /// Delivers `sandbox_start` when a hook is attached; `info` is only built then, so the + /// no-hook path never materializes the intercepted call's calldata. + fn start(&self, info: impl FnOnce() -> SandboxStartInfo) { match self { - Self::Parent(Some(hook)) => hook.borrow_mut().sandbox_start(info), - Self::Empty(Some(hook)) => hook.borrow_mut().sandbox_start(info), + Self::Parent(Some(hook)) => hook.borrow_mut().sandbox_start(&info()), + Self::Empty(Some(hook)) => hook.borrow_mut().sandbox_start(&info()), Self::Parent(None) | Self::Empty(None) => {} } } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs index d16e9770..2b927d98 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs @@ -36,7 +36,7 @@ use revm::{ interpreter::InstructionResult, }; use revm_inspectors::tracing::{ - types::{CallTraceNode, TraceMemberOrder}, + types::{CallKind, CallTraceNode, TraceMemberOrder}, TracingInspector, TracingInspectorConfig, }; @@ -377,6 +377,55 @@ fn test_splice_without_the_outer_frame_drops_the_sandbox() { assert!(empty.traces().nodes()[0].children.is_empty()); } +/// `clear` discards recorded sandbox executions, so a later splice grafts nothing. +#[test] +fn test_clear_discards_recorded_sandboxes() { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let (outer, sandbox) = paired(all_config()); + trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); + assert_eq!(sandbox.borrow().pending(), 1); + + sandbox.borrow_mut().clear(); + assert_eq!(sandbox.borrow().pending(), 0, "clear drops the recording"); + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes.len(), 1, "nothing left to graft"); + assert!(nodes[0].children.is_empty()); +} + +/// The outer frame a sandbox is grafted under must match the intercepted call on every +/// field the outer inspector recorded for it; a frame that differs in any one of them is not +/// that call, and the sandbox is dropped rather than misplaced. +#[test] +fn test_outer_frame_pairing_requires_every_field() { + type Mutation = fn(&mut CallTraceNode); + let mutations: [(&str, Mutation); 7] = [ + ("address", |node| node.trace.address = STOPPER), + ("kind", |node| node.trace.kind = CallKind::Create), + ("depth", |node| node.trace.depth += 1), + ("caller", |node| node.trace.caller = STOPPER), + ("gas_limit", |node| node.trace.gas_limit += 1), + ("value", |node| node.trace.value = U256::from(1)), + ("data", |node| node.trace.data = Bytes::from_static(&[0xff])), + ]; + for (field, mutate) in mutations { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); + let mut db = funded_db(signer); + let (outer, sandbox) = paired(all_config()); + trace_keyless_deploy(MegaSpecId::REX5, &mut db, tx_bytes, outer.clone(), &sandbox); + mutate(&mut outer.borrow_mut().traces_mut().nodes_mut()[0]); + + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0, "{field}: the sandbox is consumed"); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes.len(), 1, "{field}: a frame differing in {field} is not the call"); + assert!(nodes[0].children.is_empty(), "{field}: nothing grafted"); + } +} + /// A database error inside a nested sandbox frame aborts the sandbox before the root CREATE /// closes. The grafted frames report that abort instead of rendering as a success. #[test] From f511680b00cc723af95992b68ef4c41adf191a82 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 17:01:04 +0800 Subject: [PATCH 26/28] fix(sandbox): keep the public keyless-deploy API shape and pin the tracer contract execute_keyless_deploy_call keeps its signature (the interceptor passes the depth through a crate-private helper); SandboxOutcome keeps its two variants (the no-address create result lives in a crate-private SandboxRun); OuterCallInfo names the calldata data, matching the trace node it is compared with. SandboxTracer documents the splice-or-clear-per-transaction contract and asserts it in debug builds. Tests cover the outer adapter's create/log/selfdestruct forwarding, the blanket channels' log/selfdestruct forwarding, and a validate-rejected sandbox. --- crates/mega-evm/src/sandbox/execution.rs | 98 +++++++++++----- crates/mega-evm/src/sandbox/observer.rs | 2 +- crates/mega-evm/src/sandbox/trace.rs | 16 ++- crates/mega-evm/src/system/intercept.rs | 10 +- .../tests/rex2/keyless_sandbox_trace.rs | 108 +++++++++++++++++- 5 files changed, 198 insertions(+), 36 deletions(-) diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 640fd900..5288b7d5 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -104,6 +104,18 @@ use super::{ /// section for the full payment invariants across the Rex5 defense layers (preflight, /// upfront cap, post-merge safety net, sandbox-`validate()` rejection). pub fn execute_keyless_deploy_call( + ctx: &mut MegaContext, + call_inputs: &revm::interpreter::CallInputs, + tx_bytes: &Bytes, + gas_limit_override: U256, +) -> FrameResult { + // The interceptor only dispatches top-level calls, so the intercepted frame is at depth 0. + execute_keyless_deploy_call_at_depth(ctx, call_inputs, 0, tx_bytes, gas_limit_override) +} + +/// [`execute_keyless_deploy_call`] with the intercepted frame's call depth, which the +/// interceptor knows and reports to hooks through `OuterCallInfo::depth`. +pub(crate) fn execute_keyless_deploy_call_at_depth( ctx: &mut MegaContext, call_inputs: &revm::interpreter::CallInputs, depth: usize, @@ -464,12 +476,17 @@ pub fn execute_keyless_deploy_call gas_limit: call_inputs.gas_limit, gas_remaining: outer_gas_remaining, value: call_inputs.call_value(), - input: call_inputs.input.bytes(ctx), + data: call_inputs.input.bytes(ctx), }, }); match execute_keyless_deploy_sandbox(ctx, sandbox_tx, sandbox_tx_limits) { - SandboxOutcome::Completed { state, completion, limit_usage, volatile_accesses } => { + SandboxRun::Outcome(SandboxOutcome::Completed { + state, + completion, + limit_usage, + volatile_accesses, + }) => { let gas_used = completion.gas_used(); if ctx.spec.is_enabled(MegaSpecId::REX5) { @@ -556,7 +573,7 @@ pub fn execute_keyless_deploy_call } } } - SandboxOutcome::NoContractCreated { gas_used, limit_usage, volatile_accesses } => { + SandboxRun::NoContractCreated { gas_used, limit_usage, volatile_accesses } => { // The sandbox ran a top-level create frame that produced no address, which only // an inspector `create` override can do. The sandbox did execute, so charge it // exactly like `AddressMismatch`: REX5+ books gas and usage, nothing is applied. @@ -579,7 +596,7 @@ pub fn execute_keyless_deploy_call lifecycle.end(SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }); make_error!(KeylessDeployError::NoContractCreated) } - SandboxOutcome::Rejected(e) => { + SandboxRun::Outcome(SandboxOutcome::Rejected(e)) => { // Sandbox bailed before producing a frame — typically a validate-reject // (`FailedDeposit` → `InvalidTransaction`) or an internal error. Refund // the full reservation; the materialization charge already applied @@ -637,7 +654,7 @@ pub(crate) fn execute_keyless_deploy_sandbox, sandbox_tx: MegaTransaction, sandbox_tx_limits: Option, -) -> SandboxOutcome { +) -> SandboxRun { let deploy_signer = sandbox_tx.caller(); let gas_limit = sandbox_tx.gas_limit(); let gas_price = sandbox_tx.gas_price(); @@ -683,7 +700,7 @@ pub(crate) fn execute_keyless_deploy_sandbox total, - None => return SandboxOutcome::Rejected(KeylessDeployError::InsufficientBalance), + None => return SandboxRun::rejected(KeylessDeployError::InsufficientBalance), } }; if signer_account.balance < total_cost { - return SandboxOutcome::Rejected(KeylessDeployError::InsufficientBalance); + return SandboxRun::rejected(KeylessDeployError::InsufficientBalance); } // Execute sandbox - using type-erased SandboxDb prevents infinite type instantiation. @@ -730,7 +747,7 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( block: BlockEnv, chain: L1BlockInfo, hook: Option>, -) -> SandboxOutcome { +) -> SandboxRun { let sandbox_ctx = match sandbox_tx_limits { Some(limits) => sandbox_ctx.with_tx_runtime_limits(limits), None => sandbox_ctx, @@ -846,6 +863,16 @@ pub enum SandboxOutcome { /// or `InternalError`). No state, resource usage, or volatile-access footprint /// applies — the outer caller must refund the full pre-debited reservation. Rejected(KeylessDeployError), +} + +/// What one sandbox run produced, as seen by the interceptor. +/// +/// Wraps the public [`SandboxOutcome`] with the one shape that is not an outcome the outer +/// caller can report as-is: a top-level create frame that produced no address. +#[derive(Debug)] +pub(crate) enum SandboxRun { + /// The sandbox completed or was rejected; see [`SandboxOutcome`]. + Outcome(SandboxOutcome), /// Sandbox EVM ran, but its top-level create frame produced no address. Only an /// inspector `create` override returning `address: None` reaches this arm; the no-hook /// path always creates. The sandbox executed, so gas and resource usage are charged @@ -860,6 +887,12 @@ pub enum SandboxOutcome { }, } +impl SandboxRun { + fn rejected(error: KeylessDeployError) -> Self { + Self::Outcome(SandboxOutcome::Rejected(error)) + } +} + /// Wire-shape dispatch for a completed sandbox execution. /// /// `Deployed` and `EmptyCode` both surface as success-shape outer returns: the @@ -956,12 +989,14 @@ fn process_sandbox_transact_result( volatile_accesses: VolatileDataAccess, is_rex5_enabled: bool, is_rex6_enabled: bool, -) -> SandboxOutcome { - let completed = |state, completion| SandboxOutcome::Completed { - state, - completion, - limit_usage, - volatile_accesses, +) -> SandboxRun { + let completed = |state, completion| { + SandboxRun::Outcome(SandboxOutcome::Completed { + state, + completion, + limit_usage, + volatile_accesses, + }) }; match result { @@ -973,7 +1008,7 @@ fn process_sandbox_transact_result( // `TxKind::Create`, so revm never produces this on its own; an // inspector `create` override answering `address: None` does. The // sandbox executed, so its gas and usage are reported for charging. - return SandboxOutcome::NoContractCreated { + return SandboxRun::NoContractCreated { gas_used, limit_usage, volatile_accesses, @@ -1031,7 +1066,7 @@ fn process_sandbox_transact_result( // so we drop the discarded sandbox state and surface as Rejected. if matches!(reason, MegaHaltReason::Base(op_revm::OpHaltReason::FailedDeposit)) { warn!(gas_used, "keyless deploy sandbox failed deposit (validation-reject)",); - return SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction); + return SandboxRun::rejected(KeylessDeployError::InvalidTransaction); } // The halt `reason` is dropped on the ABI wire (the Solidity error // carries only `gasUsed`) and `decode_error_result` synthesizes a @@ -1059,14 +1094,14 @@ fn process_sandbox_transact_result( error = %e, "keyless deploy sandbox transaction rejected during validation", ); - SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction) + SandboxRun::rejected(KeylessDeployError::InvalidTransaction) } Err(e) => { error!( error = %e, "keyless deploy sandbox failed with internal error", ); - SandboxOutcome::Rejected(KeylessDeployError::InternalError) + SandboxRun::rejected(KeylessDeployError::InternalError) } } } @@ -1429,7 +1464,7 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::NoContractCreated { gas_used: 1, .. }), + matches!(out, SandboxRun::NoContractCreated { gas_used: 1, .. }), "unexpected: {out:?}", ); } @@ -1457,7 +1492,7 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::NoContractCreated { gas_used: 1, .. }), + matches!(out, SandboxRun::NoContractCreated { gas_used: 1, .. }), "unexpected: {out:?}", ); } @@ -1478,7 +1513,10 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::Rejected(KeylessDeployError::InternalError)), + matches!( + out, + SandboxRun::Outcome(SandboxOutcome::Rejected(KeylessDeployError::InternalError)) + ), "unexpected: {out:?}", ); } @@ -1497,7 +1535,12 @@ mod tests { false, ); assert!( - matches!(out, SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction)), + matches!( + out, + SandboxRun::Outcome(SandboxOutcome::Rejected( + KeylessDeployError::InvalidTransaction + )) + ), "unexpected: {out:?}", ); } @@ -1894,7 +1937,7 @@ mod tests { let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(SPLIT_CREATE_COMPUTE_BUDGET); - run_sandbox_ctx( + match run_sandbox_ctx( context, tx, Some(limits), @@ -1903,7 +1946,10 @@ mod tests { observer.map(|observer| -> SandboxHookHandle { Rc::new(RefCell::new(crate::sandbox::ReadOnlyHook::new(observer))) }), - ) + ) { + SandboxRun::Outcome(outcome) => outcome, + other => panic!("split-create fixture produced no outcome: {other:?}"), + } } fn split_create_slot(outcome: &SandboxOutcome) -> (bool, Option) { @@ -1916,7 +1962,7 @@ mod tests { }); (created, slot) } - SandboxOutcome::Rejected(_) | SandboxOutcome::NoContractCreated { .. } => (false, None), + SandboxOutcome::Rejected(_) => (false, None), } } diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs index 971ea530..36d37601 100644 --- a/crates/mega-evm/src/sandbox/observer.rs +++ b/crates/mega-evm/src/sandbox/observer.rs @@ -125,7 +125,7 @@ pub struct OuterCallInfo { /// `msg.value` of the intercepted call. pub value: U256, /// Full calldata of the intercepted call. - pub input: Bytes, + pub data: Bytes, } /// Terminal outcome of one sandbox execution, delivered exactly once. diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index f5941878..dde546db 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -199,7 +199,12 @@ impl SandboxTrace { /// `set_keyless_sandbox_observer`; it implements [`SandboxObserver`] for every env type. /// /// Recording is per execution, so a tracer kept across transactions does not need a reset; -/// splicing drains what was recorded, and [`Self::clear`] discards it. +/// splicing drains what was recorded, and [`Self::clear`] discards it. Do one or the other +/// after every transaction, before the outer inspector moves on: pairing matches a recorded +/// sandbox to an outer frame by the fields of the intercepted call, and a recording left +/// behind would match a later call with the same caller, gas, value, and calldata (a retry +/// of the same payload that the interceptor turns down before it reaches the sandbox, for +/// instance) even though it belongs to an earlier transaction. #[derive(Debug)] pub struct SandboxTracer { config: TracingInspectorConfig, @@ -321,6 +326,13 @@ impl SandboxObserver for SandboxTracer { fn sandbox_start(&mut self, info: &SandboxStartInfo) { debug_assert!(self.current.is_none(), "sandbox_start while a sandbox is in flight"); + // The interceptor only sandboxes top-level calls, so a second execution always belongs + // to a later transaction: a recording still pending here was neither spliced nor + // cleared after the transaction that produced it. + debug_assert!( + self.finished.is_empty(), + "a recorded sandbox was neither spliced nor cleared before the next transaction" + ); self.current = Some(SandboxTrace { start: info.clone(), tracer: TracingInspector::new(self.config), @@ -346,7 +358,7 @@ fn is_outer_frame(node: &CallTraceNode, call: &OuterCallInfo) -> bool { trace.caller == call.caller && trace.gas_limit == call.gas_limit && trace.value == call.value && - trace.data == call.input + trace.data == call.data } /// Grafts every sandbox execution recorded by `sandbox` under the outer `KeylessDeploy` CALL diff --git a/crates/mega-evm/src/system/intercept.rs b/crates/mega-evm/src/system/intercept.rs index 8d59309a..0b4e086f 100644 --- a/crates/mega-evm/src/system/intercept.rs +++ b/crates/mega-evm/src/system/intercept.rs @@ -16,10 +16,10 @@ use revm::{ }; use crate::{ - sandbox::execute_keyless_deploy_call, ExternalEnvTypes, IKeylessDeploy, IMegaAccessControl, - IMegaLimitControl, IOracle, MegaContext, MegaSpecId, OracleEnv, ACCESS_CONTROL_ADDRESS, - DISABLED_BY_PARENT_REVERT_DATA, KEYLESS_DEPLOY_ADDRESS, LIMIT_CONTROL_ADDRESS, - ORACLE_CONTRACT_ADDRESS, + sandbox::execute_keyless_deploy_call_at_depth, ExternalEnvTypes, IKeylessDeploy, + IMegaAccessControl, IMegaLimitControl, IOracle, MegaContext, MegaSpecId, OracleEnv, + ACCESS_CONTROL_ADDRESS, DISABLED_BY_PARENT_REVERT_DATA, KEYLESS_DEPLOY_ADDRESS, + LIMIT_CONTROL_ADDRESS, ORACLE_CONTRACT_ADDRESS, }; /// The result of a system contract call interception attempt. @@ -314,7 +314,7 @@ impl SystemContractInterceptor decode_error_result(output), + other => panic!("an unfunded signer is rejected, got {other:?}"), + }; + assert!(matches!(error, Some(KeylessDeployError::InsufficientBalance)), "got {error:?}"); + assert_eq!(sandbox.borrow().pending(), 1, "the rejected sandbox is still recorded"); + + splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); + assert_eq!(sandbox.borrow().pending(), 0, "and consumed by the splice"); + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes.len(), 1, "nothing to graft: the sandbox never entered a frame"); + assert!(nodes[0].children.is_empty()); + assert!(nodes[0].trace.steps.is_empty(), "no synthetic CREATE step either"); +} + /// `clear` discards recorded sandbox executions, so a later splice grafts nothing. #[test] fn test_clear_discards_recorded_sandboxes() { @@ -578,6 +606,82 @@ fn test_splice_adds_no_create_step_when_the_parent_has_a_call_like_step_per_chil assert_eq!(nodes[0].ordering, vec![TraceMemberOrder::Step(0), TraceMemberOrder::Call(0)]); } +/// The outer adapter forwards every hook of a frame the outer EVM itself executes: a plain +/// CREATE transaction whose constructor logs and self-destructs lands as one CREATE node +/// with the log and the self-destruct recorded. +#[test] +fn test_outer_adapter_forwards_create_log_and_selfdestruct() { + let init_code = BytecodeBuilder::default() + .append_many([PUSH0, PUSH0, LOG0, PUSH0, PUSH0, SELFDESTRUCT]) + .build(); + let mut db = MemoryDatabase::default(); + let outer = SharedTracingInspector::new(TracingInspector::new(all_config())); + let context = configured_context(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context).with_inspector(outer.clone()); + let tx = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Create, + data: init_code, + gas_limit: DEFAULT_OUTER_GAS_LIMIT, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let result = evm.inspect_tx(tx).expect("create transact"); + assert!(result.result.is_success(), "{:?}", result.result); + + let outer_ref = outer.borrow(); + let nodes = outer_ref.traces().nodes(); + assert_eq!(nodes.len(), 1); + let create = &nodes[0]; + assert!(create.trace.kind.is_any_create(), "the root is the CREATE frame"); + assert_eq!(create.trace.address, TEST_CALLER.create(0)); + assert_eq!(create.logs.len(), 1, "the constructor's LOG0 was forwarded"); + assert_eq!(create.trace.selfdestruct_address, Some(TEST_CALLER.create(0))); + assert!(create.trace.steps.iter().any(|step| step.op.get() == SELFDESTRUCT)); +} + +/// A plain `TracingInspector` attached through either channel reaches the sandbox through +/// the blanket impls, including the `log` and `selfdestruct` hooks. +#[test] +fn test_blanket_channels_forward_log_and_selfdestruct_to_a_tracing_inspector() { + for attach_as_inspector in [false, true] { + for (name, init_code, expect_log, expect_selfdestruct) in [ + ("deep mixed", deep_mixed_init(REVERTER), true, false), + ("selfdestruct", selfdestructing_constructor(), false, true), + ] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); + let mut db = funded_db(signer); + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let tracer = Rc::new(RefCell::new(TracingInspector::new(all_config()))); + let context = configured_context(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + if attach_as_inspector { + evm.set_keyless_sandbox_inspector(Rc::clone(&tracer)); + } else { + evm.set_keyless_sandbox_observer(Rc::clone(&tracer)); + } + let result = evm + .inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) + .expect("keyless deploy transact"); + let case = format!("{name} inspector={attach_as_inspector}"); + assert!(result.result.is_success(), "{case}: {:?}", result.result); + + let tracer_ref = tracer.borrow(); + let root = &tracer_ref.traces().nodes()[0]; + assert!(root.trace.kind.is_any_create(), "{case}: sandbox CREATE recorded"); + let logs: usize = tracer_ref.traces().nodes().iter().map(|n| n.logs.len()).sum(); + assert_eq!(logs > 0, expect_log, "{case}: log forwarding, got {logs} logs"); + assert_eq!( + root.trace.selfdestruct_address.is_some(), + expect_selfdestruct, + "{case}: selfdestruct forwarding" + ); + } + } +} + /// A `SELFDESTRUCT` inside the sandbox reaches the sandbox tracer's `selfdestruct` hook. #[test] fn test_sandbox_selfdestruct_reaches_the_tracer() { From ab11c3bbf6ffebe6f9346a2ff120690a55bc5e84 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 8 Sep 2026 17:19:22 +0800 Subject: [PATCH 27/28] refactor(sandbox): collapse the hook channels into SandboxInspector Drop SandboxObserver, its blanket impl, and ReadOnlyHook: the read-only guarantee was partial, the outer EVM's inspector has no read-only variant, and enforcing it cloned every CALL/CREATE input and outcome on the tracing path. One trait, one setter (set_keyless_sandbox_hook) plus clear_keyless_sandbox_hook; the lifecycle types live in sandbox::inspector; SandboxTracer forwards without copies. A hook that returns None from call/create and leaves interpreter and context state alone observes without intervening. --- bin/mega-evme/src/common/trace.rs | 6 +- bin/mega-evme/src/replay/cmd.rs | 2 +- crates/mega-evm/benches/mega_bench.rs | 41 +- crates/mega-evm/src/block/executor.rs | 26 +- crates/mega-evm/src/evm/AGENTS.md | 18 +- crates/mega-evm/src/evm/context.rs | 93 +-- crates/mega-evm/src/evm/mod.rs | 28 +- crates/mega-evm/src/sandbox/execution.rs | 28 +- crates/mega-evm/src/sandbox/inspector.rs | 194 +++++- crates/mega-evm/src/sandbox/mod.rs | 38 +- crates/mega-evm/src/sandbox/observer.rs | 589 ------------------ crates/mega-evm/src/sandbox/trace.rs | 66 +- .../tests/rex2/keyless_sandbox_extreme.rs | 124 +--- ...ox_observer.rs => keyless_sandbox_hook.rs} | 159 ++--- .../tests/rex2/keyless_sandbox_inspector.rs | 43 +- .../tests/rex2/keyless_sandbox_support.rs | 18 +- .../tests/rex2/keyless_sandbox_trace.rs | 90 ++- crates/mega-evm/tests/rex2/main.rs | 2 +- 18 files changed, 426 insertions(+), 1139 deletions(-) delete mode 100644 crates/mega-evm/src/sandbox/observer.rs rename crates/mega-evm/tests/rex2/{keyless_sandbox_observer.rs => keyless_sandbox_hook.rs} (86%) diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index eeb5ed20..329c8772 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -243,7 +243,7 @@ impl TraceArgs { info!(tracer = ?self.tracer, "Evm executing with tracing"); let (outer, sandbox) = paired(self.inspector_config()); let mut evm = MegaEvm::new(evm_context).with_inspector(outer.clone()); - evm.set_keyless_sandbox_observer(Rc::clone(&sandbox)); + evm.set_keyless_sandbox_hook(Rc::clone(&sandbox)); let result_and_state = evm .inspect_tx(tx) @@ -411,7 +411,7 @@ mod tests { let result = { let context = keyless_context(&mut db); let mut evm = MegaEvm::new(context).with_inspector(outer); - evm.set_keyless_sandbox_observer(sandbox); + evm.set_keyless_sandbox_hook(sandbox); let result = evm .inspect_tx(keyless_deploy_tx_with_override(tx_bytes, gas_limit_override)) .expect("keyless deploy"); @@ -560,7 +560,7 @@ mod tests { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - executor.set_keyless_sandbox_observer(Rc::clone(&sandbox)); + executor.set_keyless_sandbox_hook(Rc::clone(&sandbox)); let call_data = IKeylessDeploy::keylessDeployCall { keylessDeploymentTransaction: tx_bytes, diff --git a/bin/mega-evme/src/replay/cmd.rs b/bin/mega-evme/src/replay/cmd.rs index d290911d..cda2fdad 100644 --- a/bin/mega-evme/src/replay/cmd.rs +++ b/bin/mega-evme/src/replay/cmd.rs @@ -527,7 +527,7 @@ impl Cmd { block_executor.inspector_mut().fuse(); if let Some(sandbox) = &sandbox { - block_executor.set_keyless_sandbox_observer(std::rc::Rc::clone(sandbox)); + block_executor.set_keyless_sandbox_hook(std::rc::Rc::clone(sandbox)); } let outcome = block_executor .run_transaction(wrapped_tx) diff --git a/crates/mega-evm/benches/mega_bench.rs b/crates/mega-evm/benches/mega_bench.rs index 151c5a50..43282cf4 100644 --- a/crates/mega-evm/benches/mega_bench.rs +++ b/crates/mega-evm/benches/mega_bench.rs @@ -25,7 +25,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use mega_evm::{ alloy_consensus::{Signed, TxLegacy}, revm::inspector::NoOpInspector, - sandbox::{SandboxDb, SandboxObserver}, + sandbox::{SandboxDb, SandboxInspector}, test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, ExternalEnvTypes, IKeylessDeploy, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, KEYLESS_DEPLOY_ADDRESS, @@ -36,7 +36,10 @@ use revm::{ NUMBER, POP, PUSH0, RETURN, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, TIMESTAMP, }, context::tx::TxEnvBuilder, - interpreter::{interpreter::EthInterpreter, CallInputs, CreateInputs, Interpreter}, + interpreter::{ + interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + Interpreter, + }, ExecuteEvm as _, }; use std::{cell::RefCell, rc::Rc}; @@ -962,21 +965,21 @@ fn bench_oracle_real_data(c: &mut Criterion) { // ============================================================================ // // A `KeylessDeploy` call whose constructor spends most of its gas on a compute -// loop, run with and without a sandbox observer attached. The `observer` row -// pays the per-opcode `InspectorBridge` → `ReadOnlyHook` → observer forwarding -// on top of the plain sandbox interception, which is what `no_hook` measures. +// loop, run with and without a sandbox hook attached. The `hook` row pays the +// per-opcode `InspectorBridge` → hook forwarding on top of the plain sandbox +// interception, which is what `no_hook` measures. // const KEYLESS_SANDBOX_ITERATIONS: usize = 100; -/// The cheapest observer that still exercises every forwarding hop. +/// The cheapest hook that still exercises every forwarding hop. #[derive(Default)] -struct CountingObserver { +struct CountingHook { steps: u64, frames: u64, } -impl SandboxObserver for CountingObserver { +impl SandboxInspector for CountingHook { fn step( &mut self, _interp: &mut Interpreter, @@ -985,12 +988,22 @@ impl SandboxObserver for CountingObserver { self.steps += 1; } - fn call(&mut self, _context: &mut MegaContext, E>, _inputs: &CallInputs) { + fn call( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &mut CallInputs, + ) -> Option { self.frames += 1; + None } - fn create(&mut self, _context: &mut MegaContext, E>, _inputs: &CreateInputs) { + fn create( + &mut self, + _context: &mut MegaContext, E>, + _inputs: &mut CreateInputs, + ) -> Option { self.frames += 1; + None } } @@ -1037,7 +1050,7 @@ fn bench_keyless_sandbox_hook(c: &mut Criterion) { let mut group = c.benchmark_group("keyless_sandbox_hook"); group.sample_size(10); - for (row, attach_observer) in [("rex5/no_hook", false), ("rex5/observer", true)] { + for (row, attach_hook) in [("rex5/no_hook", false), ("rex5/hook", true)] { group.bench_function(row, |b| { b.iter(|| { let mut db = MemoryDatabase::default(); @@ -1049,10 +1062,8 @@ fn bench_keyless_sandbox_hook(c: &mut Criterion) { chain.operator_fee_constant = Some(U256::ZERO); }); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - if attach_observer { - evm.set_keyless_sandbox_observer(Rc::new(RefCell::new( - CountingObserver::default(), - ))); + if attach_hook { + evm.set_keyless_sandbox_hook(Rc::new(RefCell::new(CountingHook::default()))); } let tx = TxEnvBuilder::new() .caller(CALLER) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index d9f9b250..a3c39db7 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -152,35 +152,19 @@ where self.evm.inspector() } - /// Attaches an observer for nested sandbox execution on every spec. + /// Attaches a hook for nested sandbox execution on every spec. /// - /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_observer`]. The observer - /// must implement [`crate::sandbox::SandboxObserver`] for both this executor's + /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_hook`]. The hook must + /// implement [`crate::sandbox::SandboxInspector`] for both this executor's /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use /// [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) - where - O: crate::sandbox::SandboxObserver - + crate::sandbox::SandboxObserver - + 'static, - ExtEnvs: 'static, - { - self.evm.set_keyless_sandbox_observer(observer); - } - - /// Attaches a rewriting inspector for nested sandbox execution on every spec. - /// - /// Forwards to [`crate::MegaEvm::set_keyless_sandbox_inspector`]. The inspector - /// must implement [`crate::sandbox::SandboxInspector`] for both this executor's - /// `ExtEnvs` and [`crate::EmptyExternalEnv`]. Use - /// [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) + pub fn set_keyless_sandbox_hook(&mut self, hook: Rc>) where I: crate::sandbox::SandboxInspector + crate::sandbox::SandboxInspector + 'static, { - self.evm.set_keyless_sandbox_inspector(inspector); + self.evm.set_keyless_sandbox_hook(hook); } /// Detaches any sandbox hook from both env-type slots. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index d943d6da..bd05ca1f 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -22,21 +22,19 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - `MegaEvm` methods read aggregate resource usage from `additional_limit` after execution. - Keep inspector and non-inspector paths behaviorally aligned. -## Keyless sandbox hooks -Two exclusive channels into nested keyless-deploy sandbox execution, attached on `MegaContext` (and forwarded from `MegaEvm` / `MegaBlockExecutor`). -Read-only default: `SandboxObserver` cannot short-circuit `CALL`/`CREATE` and must not mutate interpreter or context state. -Rewriting explicit: `SandboxInspector` forwards `&mut` inputs and override return values so interventions take effect inside the sandbox as they would on a top-level EVM. -Both channels share one type-erased slot (`Rc>>`, held twice: parent env type and `EmptyExternalEnv`); an observer is installed behind the crate-private `ReadOnlyHook` adapter, which forwards shared references and never answers a `CALL`/`CREATE` override. -Types generic over revm's `Inspector` get both channels through blanket impls; hosts behind a generic EVM projection (a node's `ConfigureEvm::Evm`) name the attach operation on their own configuration type instead of on mega-evm. -With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` for the outer EVM paired with a `SandboxTracer` on the observer channel, which records each sandbox execution in an arena of its own keyed by the intercepted call, and `splice_sandbox_traces`, which grafts those arenas under their `KeylessDeploy` CALL frames after execution and leaves the tracer empty; `mega-evme` and node tracing RPCs use the same implementation. -Lifecycle events (`sandbox_start` / `sandbox_end`) are delivered on the slot that also receives the sandbox's opcode-level hooks: `EmptyExternalEnv` pre-REX4, the parent env from REX4 on. +## Keyless sandbox hook +One hook channel into nested keyless-deploy sandbox execution: `SandboxInspector`, attached on `MegaContext` via `set_keyless_sandbox_hook` (forwarded from `MegaEvm` / `MegaBlockExecutor`) and detached via `clear_keyless_sandbox_hook`; setting a hook replaces the previous one. +Hook signatures match revm's `Inspector` on the sandbox EVM: `&mut` inputs and override return values are forwarded, so interventions take effect inside the sandbox as they would on a top-level EVM, and a hook that returns `None` from `call`/`create` and leaves interpreter and context state alone observes without intervening (the same read-only notion the outer EVM's inspector has). +The slot is one type-erased handle (`Rc>>`, held twice: parent env type and `EmptyExternalEnv`), and a sandbox's lifecycle events (`sandbox_start` / `sandbox_end`) go to the slot that also receives its opcode-level hooks: `EmptyExternalEnv` pre-REX4, the parent env from REX4 on. +Types generic over revm's `Inspector` get the hook through a blanket impl; hosts behind a generic EVM projection (a node's `ConfigureEvm::Evm`) name the attach operation on their own configuration type instead of on mega-evm. +With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` for the outer EVM paired with a `SandboxTracer` attached as the hook, which records each sandbox execution in an arena of its own keyed by the intercepted call, and `splice_sandbox_traces`, which grafts those arenas under their `KeylessDeploy` CALL frames after execution and leaves the tracer empty; `mega-evme` and node tracing RPCs use the same implementation. Contract: 1. With no hook attached, the sandbox path is unchanged. -2. Attaching the rewriting channel without intervening leaves result, state, gas, and usage identical to the unattached path. +2. Attaching a hook without intervening leaves result, state, gas, and usage identical to the unattached path. 3. Interventions take effect inside the sandbox as they would on a top-level EVM; reported `gas_used` and usage are the post-intervention values, and the parent frame records them as-is without a conservation check. Malformed synthetic outcomes, such as a `memory_offset` outside the frame's memory, panic exactly as they would on a top-level EVM; the sandbox neither isolates nor amplifies that. -4. The channel is node-local and non-consensus; an intervening node may diverge from the network, and the caller accepts that risk. +4. The hook is node-local and non-consensus; an intervening node may diverge from the network, and the caller accepts that risk. 5. Later specs measure interventions and refuse some shapes; integrators must not depend on this base being permissive. ## WHERE TO LOOK diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 4b295518..c3c4d2ff 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -30,7 +30,7 @@ use revm::{ use crate::{ constants, is_system_originated, - sandbox::{ReadOnlyHook, SandboxHookHandle, SandboxInspector, SandboxObserver}, + sandbox::{SandboxHookHandle, SandboxInspector}, AdditionalLimit, BucketId, DynamicGasCost, EmptyExternalEnv, EvmTxRuntimeLimits, ExternalEnvTypes, ExternalEnvs, MegaSpecId, TxRuntimeLimit, VolatileDataAccess, VolatileDataAccessTracker, VolatileDataAccessType, @@ -84,8 +84,7 @@ pub struct MegaContext { /// Changing `ExtEnvs` via [`Self::with_external_envs`] resets this field /// and the [`EmptyExternalEnv`] hook slot: the hook cannot be carried across /// an env-type change and must be attached after external environments are - /// assembled. Observer and inspector occupy the same slot exclusively; an - /// observer sits behind a read-only adapter. + /// assembled. Setting a hook replaces the previous one. #[debug(ignore)] pub(crate) keyless_sandbox_hook: Option>, @@ -494,61 +493,31 @@ impl MegaContext { self } - /// Attaches an observer for nested sandbox execution on every spec. + /// Attaches a hook for nested sandbox execution on every spec. /// - /// The observer must implement [`SandboxObserver`] for both this context's - /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored, behind a - /// read-only adapter, as two type-erased slots so opcode-level hooks fire - /// for pre-REX4 sandboxes (always [`EmptyExternalEnv`]) and for REX4+ - /// sandboxes (shared parent env). A type that implements - /// [`revm::Inspector`] for every sandbox context lifetime satisfies both - /// bounds via the blanket impl. - /// - /// Attaching an observer does not change sandbox external-env semantics at - /// any spec. Replaces any attached inspector. - /// - /// [`Self::clear_keyless_sandbox_hook`] is the only way to detach. Observation - /// is read-only: mutating interpreter or context state through the hooks is - /// undefined and may diverge consensus. There is no take/drain API; recorded - /// data stays in the caller's observer. - pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) - where - O: SandboxObserver + SandboxObserver + 'static, - ExtEnvs: 'static, - { - let cloned = Rc::clone(&observer); - let parent: Rc>> = cloned; - let empty: Rc>> = observer; - let parent: SandboxHookHandle = Rc::new(RefCell::new(ReadOnlyHook::new(parent))); - let empty: SandboxHookHandle = - Rc::new(RefCell::new(ReadOnlyHook::new(empty))); - self.keyless_sandbox_hook = Some(parent); - self.keyless_sandbox_hook_empty = Some(empty); - } - - /// Attaches a rewriting inspector for nested sandbox execution on every spec. - /// - /// The inspector must implement [`SandboxInspector`] for both this context's + /// The hook must implement [`SandboxInspector`] for both this context's /// `ExtEnvs` and [`EmptyExternalEnv`]. The same handle is stored as two - /// type-erased slots so opcode-level hooks fire for pre-REX4 sandboxes - /// (always [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent - /// env). A type that implements [`revm::Inspector`] for every sandbox - /// context lifetime satisfies both bounds via the blanket impl. - /// - /// Attaching an inspector does not change sandbox external-env semantics at - /// any spec. Replaces any attached observer. Interventions take effect + /// type-erased slots so hooks fire for pre-REX4 sandboxes (always + /// [`EmptyExternalEnv`]) and for REX4+ sandboxes (shared parent env); a + /// sandbox's lifecycle events go to the same slot as its opcode-level hooks. + /// A type that implements [`revm::Inspector`] for every sandbox context + /// lifetime satisfies both bounds via the blanket impl. + /// + /// Attaching a hook does not change sandbox external-env semantics at any + /// spec. Setting a hook replaces the previous one. Interventions take effect /// inside the sandbox as they would on a top-level EVM; reported gas and - /// usage are the post-intervention values. The channel is node-local and - /// non-consensus. + /// usage are the post-intervention values. The hook is node-local and + /// non-consensus. There is no take/drain API; recorded data stays in the + /// caller's hook. /// /// [`Self::clear_keyless_sandbox_hook`] is the only way to detach. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) + pub fn set_keyless_sandbox_hook(&mut self, hook: Rc>) where I: SandboxInspector + SandboxInspector + 'static, { - let cloned = Rc::clone(&inspector); + let cloned = Rc::clone(&hook); let parent: SandboxHookHandle = cloned; - let empty: SandboxHookHandle = inspector; + let empty: SandboxHookHandle = hook; self.keyless_sandbox_hook = Some(parent); self.keyless_sandbox_hook_empty = Some(empty); } @@ -891,10 +860,6 @@ mod tests { use crate::TestExternalEnvs; - struct NopObserver; - - impl SandboxObserver for NopObserver {} - struct NopInspector; impl crate::sandbox::SandboxInspector for NopInspector {} @@ -1000,7 +965,7 @@ mod tests { #[test] fn test_clear_keyless_sandbox_hook_clears_both_slots() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopInspector))); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); @@ -1010,14 +975,14 @@ mod tests { } #[test] - fn test_attaching_one_channel_replaces_the_other() { + fn test_setting_a_hook_replaces_the_previous_one() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - let observer = Rc::new(RefCell::new(NopObserver)); - context.set_keyless_sandbox_observer(Rc::clone(&observer)); - assert_eq!(Rc::strong_count(&observer), 3, "both slots hold the observer"); + let first = Rc::new(RefCell::new(NopInspector)); + context.set_keyless_sandbox_hook(Rc::clone(&first)); + assert_eq!(Rc::strong_count(&first), 3, "both slots hold the hook"); - context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopInspector))); - assert_eq!(Rc::strong_count(&observer), 1, "the inspector replaced the observer"); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopInspector))); + assert_eq!(Rc::strong_count(&first), 1, "the new hook replaced the first"); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); @@ -1029,7 +994,7 @@ mod tests { #[test] fn test_with_db_keeps_the_sandbox_hook() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopInspector))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopInspector))); let context = context.with_db(EmptyDB::default()); assert!(context.keyless_sandbox_hook.is_some()); assert!(context.keyless_sandbox_hook_empty.is_some()); @@ -1040,7 +1005,7 @@ mod tests { use revm::handler::EvmTr; let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopInspector))); let mut evm = crate::MegaEvm::new(context); evm.clear_keyless_sandbox_hook(); assert!(evm.ctx_ref().keyless_sandbox_hook.is_none()); @@ -1059,9 +1024,9 @@ mod tests { #[test] #[cfg(debug_assertions)] #[should_panic(expected = "set sandbox hook after external envs are wired")] - fn test_with_external_envs_panics_in_debug_when_observer_is_attached() { + fn test_with_external_envs_panics_in_debug_when_hook_is_attached() { let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_observer(Rc::new(RefCell::new(NopObserver))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopInspector))); let _ = context.with_external_envs(TestExternalEnvs::::new().into()); } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index ed93d938..b5417a0e 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -70,8 +70,8 @@ use revm::{ }; use crate::{ - sandbox::{SandboxInspector, SandboxObserver}, - BucketId, EmptyExternalEnv, ExternalEnvTypes, LimitUsage, MegaTransaction, + sandbox::SandboxInspector, BucketId, EmptyExternalEnv, ExternalEnvTypes, LimitUsage, + MegaTransaction, }; /// The main EVM implementation for the `MegaETH` chain. @@ -237,30 +237,16 @@ impl MegaEvm { } impl MegaEvm { - /// Attaches an observer for nested sandbox execution on every spec. + /// Attaches a hook for nested sandbox execution on every spec. /// - /// Forwards to [`MegaContext::set_keyless_sandbox_observer`]. The observer - /// must implement [`SandboxObserver`] for both this EVM's `ExtEnvs` and - /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to - /// detach. - pub fn set_keyless_sandbox_observer(&mut self, observer: Rc>) - where - O: SandboxObserver + SandboxObserver + 'static, - ExtEnvs: 'static, - { - self.inner.ctx.set_keyless_sandbox_observer(observer); - } - - /// Attaches a rewriting inspector for nested sandbox execution on every spec. - /// - /// Forwards to [`MegaContext::set_keyless_sandbox_inspector`]. The inspector - /// must implement [`SandboxInspector`] for both this EVM's `ExtEnvs` and + /// Forwards to [`MegaContext::set_keyless_sandbox_hook`]. The hook must + /// implement [`SandboxInspector`] for both this EVM's `ExtEnvs` and /// [`EmptyExternalEnv`]. Use [`Self::clear_keyless_sandbox_hook`] to detach. - pub fn set_keyless_sandbox_inspector(&mut self, inspector: Rc>) + pub fn set_keyless_sandbox_hook(&mut self, hook: Rc>) where I: SandboxInspector + SandboxInspector + 'static, { - self.inner.ctx.set_keyless_sandbox_inspector(inspector); + self.inner.ctx.set_keyless_sandbox_hook(hook); } /// Detaches any sandbox hook from both env-type slots. diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 5288b7d5..9b511b30 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -68,10 +68,9 @@ use super::{ use super::{ error::{encode_error_result, KeylessDeployError}, - inspector::{InspectorBridge, SandboxHookHandle}, - observer::{ - OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, SandboxRejectKind, - SandboxStartInfo, + inspector::{ + InspectorBridge, OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, + SandboxHookHandle, SandboxRejectKind, SandboxStartInfo, }, state::SandboxDb, }; @@ -1416,8 +1415,6 @@ mod tests { use revm::context::result::Output; use std::rc::Rc; - use super::super::observer::SandboxObserver; - /// Test error type that lets us drive `process_sandbox_transact_result`'s /// `Err` arms (which map to `SandboxOutcome::Rejected`) directly without /// standing up a full sandbox EVM. The two arms differ only by @@ -1862,7 +1859,7 @@ mod tests { struct SplitNopObserver; - impl SandboxObserver for SplitNopObserver {} + impl crate::sandbox::SandboxInspector for SplitNopObserver {} fn assert_sandbox_outcomes_eq(left: &SandboxOutcome, right: &SandboxOutcome, case: &str) { match (left, right) { @@ -1904,7 +1901,7 @@ mod tests { fn run_split_create_fixture( spec: MegaSpecId, - observer: Option>>>, + observer: Option>, ) -> SandboxOutcome { use crate::test_utils::MemoryDatabase; use revm::state::EvmState; @@ -1937,16 +1934,7 @@ mod tests { let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(SPLIT_CREATE_COMPUTE_BUDGET); - match run_sandbox_ctx( - context, - tx, - Some(limits), - block, - chain, - observer.map(|observer| -> SandboxHookHandle { - Rc::new(RefCell::new(crate::sandbox::ReadOnlyHook::new(observer))) - }), - ) { + match run_sandbox_ctx(context, tx, Some(limits), block, chain, observer) { SandboxRun::Outcome(outcome) => outcome, other => panic!("split-create fixture produced no outcome: {other:?}"), } @@ -1970,7 +1958,7 @@ mod tests { fn test_observer_parity_pre_rex5_split_create_commits_sstore() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4] { let baseline = run_split_create_fixture(spec, None); - let observer: Rc>> = + let observer: SandboxHookHandle = Rc::new(RefCell::new(SplitNopObserver)); let observed = run_split_create_fixture(spec, Some(observer)); @@ -1988,7 +1976,7 @@ mod tests { #[test] fn test_observer_parity_rex5_atomic_create_failure() { let baseline = run_split_create_fixture(MegaSpecId::REX5, None); - let observer: Rc>> = + let observer: SandboxHookHandle = Rc::new(RefCell::new(SplitNopObserver)); let observed = run_split_create_fixture(MegaSpecId::REX5, Some(observer)); diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs index 1306e594..7d34bf2f 100644 --- a/crates/mega-evm/src/sandbox/inspector.rs +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -1,35 +1,61 @@ -//! Rewriting inspector channel for nested sandbox execution. +//! Hook channel into nested sandbox execution. //! -//! A [`SandboxInspector`] is the intervention counterpart of [`super::SandboxObserver`]. -//! Hook signatures match revm's [`Inspector`] on the sandbox EVM: `&mut` inputs and -//! override return values are forwarded, so `CALL`/`CREATE` can be short-circuited and -//! outcomes rewritten. [`InspectorBridge`] installs the handle as the sandbox EVM's -//! inspector. Both channels occupy the same type-erased slot: an observer is installed behind -//! a read-only adapter, so attaching either replaces the other. +//! Keyless sandbox execution is otherwise invisible to a parent inspector. A +//! [`SandboxInspector`] attached via [`crate::MegaContext::set_keyless_sandbox_hook`] +//! sees every hook that revm's [`Inspector`] would see on the sandbox EVM, plus a paired +//! [`sandbox_start`](SandboxInspector::sandbox_start) / +//! [`sandbox_end`](SandboxInspector::sandbox_end) lifecycle. Hook signatures match revm's: +//! `&mut` inputs and override return values are forwarded, so a hook can rewrite inputs, +//! short-circuit `CALL`/`CREATE`, and rewrite outcomes exactly as it could on a top-level +//! EVM. A hook that only observes returns `None` from `call`/`create` (the default bodies) +//! and leaves interpreter and context state alone; that is what read-only means here, as it +//! does for the outer EVM's inspector. [`InspectorBridge`] installs the handle as the sandbox +//! EVM's inspector. //! //! # Contract //! //! 1. With no hook attached, the sandbox path is unchanged. -//! 2. Attaching this channel without intervening leaves result, state, gas, and usage identical to -//! the unattached path. +//! 2. Attaching a hook without intervening leaves result, state, gas, and usage identical to the +//! unattached path. //! 3. Interventions take effect inside the sandbox as they would on a top-level EVM. Reported //! `gas_used` and usage are the post-intervention values; the parent frame records them as-is //! and does not check conservation. Malformed synthetic outcomes, such as a `memory_offset` //! outside the frame's memory, panic exactly as they would on a top-level EVM; the sandbox //! neither isolates nor amplifies that. -//! 4. The channel is node-local and non-consensus. An intervening node may diverge from the -//! network; the caller accepts that risk. +//! 4. The hook is node-local and non-consensus. An intervening node may diverge from the network; +//! the caller accepts that risk. //! 5. Later specs measure interventions and refuse some shapes. Integrators must not depend on this //! base being permissive. //! -//! `sandbox_start` / `sandbox_end` are informational and cannot veto execution. +//! # Lifecycle +//! +//! `sandbox_start` and `sandbox_end` fire exactly once per sandbox attempt, and only when a +//! hook is attached. An attempt begins once the keyless payload has been decoded, its signer +//! recovered, the deploy address derived and found free, and the gas budget admitted; a call +//! that is rejected before that point emits no events. From then on the pair is guaranteed: a +//! sandbox transaction that revm's validation rejects without ever constructing a sandbox EVM +//! still delivers `sandbox_end` with [`SandboxRejectKind::Rejected`]. Reverted inner frames +//! still emit their events; whether sandbox state was applied to the parent is reported by +//! [`SandboxEndOutcome::state_applied`]. Both lifecycle hooks are informational and cannot +//! veto execution. +//! +//! # External-environment invariance +//! +//! Attaching a hook must not change sandbox env semantics at any spec. Pre-REX4 sandboxes +//! always run with [`crate::EmptyExternalEnv`]; REX4+ sandboxes always share the parent env. +//! A hook therefore implements [`SandboxInspector`] for both the parent env type and +//! [`crate::EmptyExternalEnv`]; the setter stores two type-erased handles so hooks fire on +//! both paths, and a sandbox's lifecycle events go to the same slot as its opcode-level +//! hooks (the [`crate::EmptyExternalEnv`] impl pre-REX4, the parent-env impl from REX4 on). +//! A type that implements revm's [`Inspector`] for every sandbox context satisfies both +//! bounds through the blanket impl. #[cfg(not(feature = "std"))] use alloc as std; use core::cell::RefCell; use std::rc::Rc; -use alloy_primitives::{Address, Log, U256}; +use alloy_primitives::{Address, Bytes, Log, U256}; use revm::{ interpreter::{ interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, @@ -38,19 +64,129 @@ use revm::{ Inspector, }; -use crate::{ExternalEnvTypes, MegaContext}; +use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; + +use super::state::SandboxDb; + +/// Context available when a sandbox is about to execute. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SandboxStartInfo { + /// Spec used by the parent context that started the sandbox. + pub spec: MegaSpecId, + /// Recovered signer of the keyless deployment transaction. + pub signer: Address, + /// Deterministic deploy address derived from the signer. + pub deploy_address: Address, + /// Caller-supplied gas limit override decoded from the payload. + /// + /// The ABI value is a `U256` and is saturating-converted to `u64`, so a + /// payload larger than [`u64::MAX`] is reported as [`u64::MAX`]. + pub gas_limit_override: u64, + /// Gas limit actually granted to the sandbox after outer-gas capping + /// (REX5+ caps to the outer frame's remaining gas; pre-REX5 equals + /// [`Self::gas_limit_override`]). + pub effective_gas_limit: u64, + /// Gas limit carried by the signed keyless transaction itself. + pub tx_gas_limit: u64, + /// The intercepted `KeylessDeploy` call frame, as an inspector on the outer EVM saw it. + pub outer_call: OuterCallInfo, +} -use super::{ - observer::{SandboxEndOutcome, SandboxStartInfo}, - state::SandboxDb, -}; +/// The intercepted `KeylessDeploy` call that started a sandbox, described with the fields an +/// inspector on the outer EVM records for that frame. +/// +/// A tracer that records the outer EVM and the sandbox separately uses this to pair a +/// sandbox with the outer frame it belongs under: the fields match what revm's `call` hook +/// received for the intercepted frame. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OuterCallInfo { + /// Call depth of the intercepted frame; `0` for a transaction's top-level call. + pub depth: usize, + /// `msg.sender` of the intercepted call. + pub caller: Address, + /// Gas limit of the intercepted call frame, as handed to it by the outer EVM. + pub gas_limit: u64, + /// Gas remaining in the intercepted frame when the sandbox started: after the dispatch + /// overhead and any REX5+ materialization charge, before the sandbox reservation was + /// debited. + pub gas_remaining: u64, + /// `msg.value` of the intercepted call. + pub value: U256, + /// Full calldata of the intercepted call. + pub data: Bytes, +} + +/// Terminal outcome of one sandbox execution, delivered exactly once. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SandboxEndOutcome { + /// Sandbox completed and its state was applied to the parent journal. + Applied { + /// How the sandbox EVM completed. + completion: SandboxCompletionKind, + /// Gas consumed by the sandbox EVM. + gas_used: u64, + }, + /// Sandbox ran but its state was not applied. + NotApplied { + /// Why the sandbox state was discarded. + reason: SandboxRejectKind, + }, +} + +/// Completion kind for a sandbox whose state was applied to the parent. +/// +/// The kind describes what the sandbox EVM did, on every spec alike. What the outer +/// caller is told differs by spec only for [`Self::EmptyCode`]. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxCompletionKind { + /// Inner CREATE succeeded with non-empty runtime bytecode. + Deployed, + /// Inner CREATE ran to a successful exit but left no code at the deploy address: + /// it returned empty runtime bytecode, or (REX6+) the account self-destructed in the + /// same transaction. + /// + /// The outer caller sees `EmptyCodeDeployed` errorData on every spec. REX5+ forwards + /// the constructor's logs into the parent receipt; pre-REX5 drops them. + EmptyCode, + /// Sandbox EVM execution reverted or halted after producing mergeable state. + ExecutionFailed, +} -/// Object-safe rewriting inspector for nested sandbox execution. +/// Reason a completed or aborted sandbox did not apply state to the parent. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SandboxRejectKind { + /// Validate-reject or internal error before a mergeable frame was produced, or a + /// top-level create result that carries no address. Only a hook's `create` override + /// produces the latter; it is charged like [`Self::AddressMismatch`] and nothing is + /// applied. + Rejected, + /// REX5+ post-execution resource accounting rejected the sandbox. + PostAccountingHalt, + /// Applying sandbox state to the parent journal failed. + ApplyFailed, + /// Deployed address did not match the derived address. + AddressMismatch, +} + +impl SandboxEndOutcome { + /// Returns `true` when sandbox state was applied to the parent journal. + pub fn state_applied(&self) -> bool { + matches!(self, Self::Applied { .. }) + } +} + +/// Object-safe hook into nested sandbox execution. /// /// Signatures match [`Inspector`]`<`[`MegaContext`]`<`[`SandboxDb`]`<'_>, ExtEnvs>, /// `[`EthInterpreter`]`>`. All hooks have empty / `None` defaults so adding a hook is not a -/// breaking change. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` keep the -/// trait object-safe. +/// breaking change, and a type that overrides nothing but the hooks it reads from observes +/// without intervening. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` +/// keep the trait object-safe. /// /// Types that already implement [`Inspector`] for every sandbox context lifetime /// receive a blanket [`SandboxInspector`] impl. Local types that are not inspectors @@ -369,10 +505,8 @@ impl Inspector, E>, EthInterprete } } -/// The type-erased hook slot for nested sandbox execution. -/// -/// Both channels share it: an observer is installed behind -/// [`super::observer::ReadOnlyHook`], an inspector directly. Attaching one replaces the other. +/// The type-erased hook slot for nested sandbox execution. Setting a hook replaces the +/// previous one. pub(crate) type SandboxHookHandle = Rc>>; #[cfg(test)] @@ -389,6 +523,16 @@ mod tests { impl Inspector for GenericInspector {} + #[test] + fn test_sandbox_end_outcome_reports_whether_state_was_applied() { + let applied = + SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, gas_used: 1 }; + assert!(applied.state_applied()); + + let not_applied = SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }; + assert!(!not_applied.state_applied()); + } + #[test] fn test_sandbox_inspector_is_object_safe() { let _boxed: Box> = Box::new(LocalInspector); diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 333735a5..27193ba4 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -59,33 +59,24 @@ //! //! - `execution` - Core sandbox execution logic and the main entry point //! [`execute_keyless_deploy_call`] -//! - `observer` - Read-only [`SandboxObserver`] channel into nested sandbox execution -//! - `inspector` - Rewriting [`SandboxInspector`] channel into nested sandbox execution +//! - `inspector` - The [`SandboxInspector`] hook channel into nested sandbox execution //! - `trace` - Shared `revm-inspectors` recorder and splicing helpers (`inspectors` feature) //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal //! - `tx` - Transaction decoding and validation for pre-EIP-155 transactions //! - `error` - Error types ([`KeylessDeployError`]) that map to Solidity errors in `IKeylessDeploy` //! -//! # Sandbox hooks +//! # Sandbox hook //! -//! Nested sandbox execution is otherwise invisible to a parent inspector. Two exclusive -//! channels can attach through [`crate::MegaContext`] (also forwarded from [`crate::MegaEvm`] -//! and [`crate::MegaBlockExecutor`]): -//! -//! - **Read-only (default).** [`SandboxObserver`] via -//! [`crate::MegaContext::set_keyless_sandbox_observer`]. Interpreter hooks plus a paired -//! `sandbox_start` / `sandbox_end` lifecycle. Cannot short-circuit `CALL`/`CREATE`. -//! - **Rewriting (explicit).** [`SandboxInspector`] via -//! [`crate::MegaContext::set_keyless_sandbox_inspector`]. Same lifecycle, but `&mut` inputs and -//! override return values are forwarded so interventions take effect inside the sandbox as they -//! would on a top-level EVM. -//! -//! A compliant (read-only) observer does not change execution results; no such -//! guarantee is made for an observer that mutates interpreter or context state. -//! Reverted inner frames still emit their events; whether sandbox state was applied -//! to the parent is reported by [`SandboxEndOutcome::state_applied`]. With no hook -//! attached the sandbox path is unchanged. +//! Nested sandbox execution is otherwise invisible to a parent inspector. A +//! [`SandboxInspector`] attaches through [`crate::MegaContext::set_keyless_sandbox_hook`] +//! (also forwarded from [`crate::MegaEvm`] and [`crate::MegaBlockExecutor`]): interpreter hooks +//! with revm's `&mut` inputs and override return values, plus a paired `sandbox_start` / +//! `sandbox_end` lifecycle. Interventions take effect inside the sandbox as they would on a +//! top-level EVM; a hook that returns `None` from `call`/`create` and leaves interpreter and +//! context state alone does not change execution results. Reverted inner frames still emit +//! their events; whether sandbox state was applied to the parent is reported by +//! [`SandboxEndOutcome::state_applied`]. With no hook attached the sandbox path is unchanged. //! //! Attaching a hook does not change sandbox external-env semantics: pre-REX4 sandboxes //! always run with [`crate::EmptyExternalEnv`] (minimum bucket capacity, no oracle @@ -93,12 +84,11 @@ //! pre-REX4 are delivered through a second slot typed against //! [`crate::EmptyExternalEnv`], so a hook implements its trait for both the parent env //! type and [`crate::EmptyExternalEnv`]; revm inspectors that are generic over the -//! context satisfy both through the blanket impls. Lifecycle events follow the same +//! context satisfy both through the blanket impl. Lifecycle events follow the same //! slot rule, so a hook with one impl per env sees a sandbox entirely on one impl. //! -//! [`SandboxObserver`]: observer::SandboxObserver //! [`SandboxInspector`]: inspector::SandboxInspector -//! [`SandboxEndOutcome::state_applied`]: observer::SandboxEndOutcome::state_applied +//! [`SandboxEndOutcome::state_applied`]: inspector::SandboxEndOutcome::state_applied //! //! # Type Erasure Strategy //! @@ -132,7 +122,6 @@ mod error; mod execution; mod inspector; -mod observer; mod state; mod state_merge; #[cfg(feature = "inspectors")] @@ -142,6 +131,5 @@ mod tx; pub use error::*; pub use execution::*; pub use inspector::*; -pub use observer::*; pub use state::*; pub use tx::*; diff --git a/crates/mega-evm/src/sandbox/observer.rs b/crates/mega-evm/src/sandbox/observer.rs deleted file mode 100644 index 36d37601..00000000 --- a/crates/mega-evm/src/sandbox/observer.rs +++ /dev/null @@ -1,589 +0,0 @@ -//! Read-only observation channel for nested sandbox execution. -//! -//! Keyless sandbox execution has two hook channels. This module is the -//! **read-only default**: attach via [`crate::MegaContext::set_keyless_sandbox_observer`]. -//! The rewriting channel is [`crate::sandbox::SandboxInspector`], attached explicitly -//! via [`crate::MegaContext::set_keyless_sandbox_inspector`]. The two occupy the same -//! slot; attaching one replaces the other. -//! -//! A [`SandboxObserver`] sees every interpreter hook that revm's [`Inspector`] -//! would see inside a sandbox, plus a paired [`SandboxObserver::sandbox_start`] / -//! [`SandboxObserver::sandbox_end`] lifecycle. Observation cannot short-circuit -//! `CALL`/`CREATE`: those hooks have no return value, and the sandbox installs -//! the observer behind `ReadOnlyHook`, which always answers `None` to the EVM. -//! -//! # Read-only contract -//! -//! Two layers close intervention: -//! -//! - **Structural.** [`SandboxObserver::call`] / [`SandboxObserver::create`] take shared references -//! to [`CallInputs`] / [`CreateInputs`]. The blanket impl for revm [`Inspector`]s clones a -//! temporary copy when forwarding; mutations of that copy are discarded, as are `Some` override -//! outcomes. Combined with `ReadOnlyHook` always returning `None`, rewriting inputs and -//! short-circuiting the frame are both closed. -//! - **Contractual.** [`SandboxObserver::step`] / [`SandboxObserver::step_end`] take `&mut -//! Interpreter` and hooks take `&mut MegaContext` because those types are not cheaply cloneable. -//! Implementations must not mutate them. Debug builds already trip conservation asserts on many -//! such mutations. -//! -//! Attaching a compliant (read-only) observer does not change sandbox execution -//! results. No such guarantee is made for an observer that mutates interpreter -//! or context state. -//! -//! # Lifecycle -//! -//! [`sandbox_start`](SandboxObserver::sandbox_start) and -//! [`sandbox_end`](SandboxObserver::sandbox_end) fire exactly once per sandbox -//! attempt, and only when an observer is attached. An attempt begins once the -//! keyless payload has been decoded, its signer recovered, the deploy address -//! derived and found free, and the gas budget admitted; a call that is rejected -//! before that point emits no events. From then on the pair is guaranteed: a -//! sandbox transaction that revm's validation rejects without ever constructing -//! a sandbox EVM still delivers `sandbox_end` with -//! [`SandboxRejectKind::Rejected`]. Reverted inner frames still emit their -//! events; whether sandbox state was applied to the parent is reported by -//! [`SandboxEndOutcome::state_applied`]. -//! -//! Lifecycle events are delivered on the same slot as that sandbox's opcode-level -//! hooks: the [`crate::EmptyExternalEnv`] impl for pre-REX4 sandboxes, the parent-env -//! impl for REX4+ (see below). A type with one impl per env therefore always sees a -//! sandbox's `sandbox_start`, opcode hooks, and `sandbox_end` on the same impl. -//! -//! # External-environment invariance -//! -//! Attaching an observer must not change sandbox env semantics at any spec. -//! Pre-REX4 sandboxes always run with [`crate::EmptyExternalEnv`]; REX4+ -//! sandboxes always share the parent env. An observer therefore implements -//! [`SandboxObserver`] for both the parent env type and -//! [`crate::EmptyExternalEnv`]; [`crate::MegaContext::set_keyless_sandbox_observer`] -//! stores two type-erased handles so opcode-level hooks fire on both paths. A -//! type that implements revm's [`Inspector`] for every sandbox context satisfies -//! both bounds through the blanket impl. - -#[cfg(not(feature = "std"))] -use alloc as std; -use core::cell::RefCell; -use std::rc::Rc; - -use alloy_primitives::{Address, Bytes, Log, U256}; -use revm::{ - interpreter::{ - interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, - Interpreter, - }, - Inspector, -}; - -use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; - -use super::{inspector::SandboxInspector, state::SandboxDb}; - -/// Context available when a sandbox is about to execute. -#[non_exhaustive] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SandboxStartInfo { - /// Spec used by the parent context that started the sandbox. - pub spec: MegaSpecId, - /// Recovered signer of the keyless deployment transaction. - pub signer: Address, - /// Deterministic deploy address derived from the signer. - pub deploy_address: Address, - /// Caller-supplied gas limit override decoded from the payload. - /// - /// The ABI value is a `U256` and is saturating-converted to `u64`, so a - /// payload larger than [`u64::MAX`] is reported as [`u64::MAX`]. - pub gas_limit_override: u64, - /// Gas limit actually granted to the sandbox after outer-gas capping - /// (REX5+ caps to the outer frame's remaining gas; pre-REX5 equals - /// [`Self::gas_limit_override`]). - pub effective_gas_limit: u64, - /// Gas limit carried by the signed keyless transaction itself. - pub tx_gas_limit: u64, - /// The intercepted `KeylessDeploy` call frame, as an inspector on the outer EVM saw it. - pub outer_call: OuterCallInfo, -} - -/// The intercepted `KeylessDeploy` call that started a sandbox, described with the fields an -/// inspector on the outer EVM records for that frame. -/// -/// A tracer that records the outer EVM and the sandbox separately uses this to pair a -/// sandbox with the outer frame it belongs under: the fields match what revm's `call` hook -/// received for the intercepted frame. -#[non_exhaustive] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OuterCallInfo { - /// Call depth of the intercepted frame; `0` for a transaction's top-level call. - pub depth: usize, - /// `msg.sender` of the intercepted call. - pub caller: Address, - /// Gas limit of the intercepted call frame, as handed to it by the outer EVM. - pub gas_limit: u64, - /// Gas remaining in the intercepted frame when the sandbox started: after the dispatch - /// overhead and any REX5+ materialization charge, before the sandbox reservation was - /// debited. - pub gas_remaining: u64, - /// `msg.value` of the intercepted call. - pub value: U256, - /// Full calldata of the intercepted call. - pub data: Bytes, -} - -/// Terminal outcome of one sandbox execution, delivered exactly once. -#[non_exhaustive] -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SandboxEndOutcome { - /// Sandbox completed and its state was applied to the parent journal. - Applied { - /// How the sandbox EVM completed. - completion: SandboxCompletionKind, - /// Gas consumed by the sandbox EVM. - gas_used: u64, - }, - /// Sandbox ran but its state was not applied. - NotApplied { - /// Why the sandbox state was discarded. - reason: SandboxRejectKind, - }, -} - -/// Completion kind for a sandbox whose state was applied to the parent. -/// -/// The kind describes what the sandbox EVM did, on every spec alike. What the outer -/// caller is told differs by spec only for [`Self::EmptyCode`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SandboxCompletionKind { - /// Inner CREATE succeeded with non-empty runtime bytecode. - Deployed, - /// Inner CREATE ran to a successful exit but left no code at the deploy address: - /// it returned empty runtime bytecode, or (REX6+) the account self-destructed in the - /// same transaction. - /// - /// The outer caller sees `EmptyCodeDeployed` errorData on every spec. REX5+ forwards - /// the constructor's logs into the parent receipt; pre-REX5 drops them. - EmptyCode, - /// Sandbox EVM execution reverted or halted after producing mergeable state. - ExecutionFailed, -} - -/// Reason a completed or aborted sandbox did not apply state to the parent. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SandboxRejectKind { - /// Validate-reject or internal error before a mergeable frame was produced, or a - /// top-level create result that carries no address. Only an inspector `create` - /// override produces the latter; it is charged like [`Self::AddressMismatch`] and - /// nothing is applied. - Rejected, - /// REX5+ post-execution resource accounting rejected the sandbox. - PostAccountingHalt, - /// Applying sandbox state to the parent journal failed. - ApplyFailed, - /// Deployed address did not match the derived address. - AddressMismatch, -} - -impl SandboxEndOutcome { - /// Returns `true` when sandbox state was applied to the parent journal. - pub fn state_applied(&self) -> bool { - matches!(self, Self::Applied { .. }) - } -} - -/// Object-safe observer for nested sandbox execution. -/// -/// All hooks have empty default implementations so adding a hook is not a -/// breaking change. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` -/// keep the trait object-safe. -/// -/// A compliant (read-only) observer does not change sandbox execution results. -/// [`call`](Self::call) / [`create`](Self::create) cannot rewrite inputs or -/// override the frame; [`step`](Self::step) and context arguments remain -/// contractually read-only. `ReadOnlyHook` never answers a `call`/`create` -/// override on the observer's behalf. -/// -/// Types that already implement [`Inspector`] for every sandbox context -/// lifetime receive a blanket impl that forwards each hook on a temporary -/// copy of the inputs. Local types that are not inspectors implement this -/// trait by hand. -pub trait SandboxObserver { - /// Called once after the sandbox interpreter is created, before the first opcode. - #[inline] - fn initialize_interp( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, ExtEnvs>, - ) { - let _ = interp; - let _ = context; - } - - /// Called before each opcode executes. - #[inline] - fn step( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, ExtEnvs>, - ) { - let _ = interp; - let _ = context; - } - - /// Called after each opcode executes. - #[inline] - fn step_end( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, ExtEnvs>, - ) { - let _ = interp; - let _ = context; - } - - /// Called when a log is emitted inside the sandbox. - #[inline] - fn log( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, ExtEnvs>, - log: Log, - ) { - let _ = interp; - let _ = context; - let _ = log; - } - - /// Called when a call frame is about to start. Cannot override the call. - #[inline] - fn call(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &CallInputs) { - let _ = context; - let _ = inputs; - } - - /// Called when a call frame has concluded. - #[inline] - fn call_end( - &mut self, - context: &mut MegaContext, ExtEnvs>, - inputs: &CallInputs, - outcome: &CallOutcome, - ) { - let _ = context; - let _ = inputs; - let _ = outcome; - } - - /// Called when a create frame is about to start. Cannot override the create. - #[inline] - fn create(&mut self, context: &mut MegaContext, ExtEnvs>, inputs: &CreateInputs) { - let _ = context; - let _ = inputs; - } - - /// Called when a create frame has concluded. - #[inline] - fn create_end( - &mut self, - context: &mut MegaContext, ExtEnvs>, - inputs: &CreateInputs, - outcome: &CreateOutcome, - ) { - let _ = context; - let _ = inputs; - let _ = outcome; - } - - /// Called when a contract self-destructs inside the sandbox. - #[inline] - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - let _ = contract; - let _ = target; - let _ = value; - } - - /// Called once immediately before sandbox execution starts. - #[inline] - fn sandbox_start(&mut self, info: &SandboxStartInfo) { - let _ = info; - } - - /// Called once with the terminal sandbox outcome. Paired with [`Self::sandbox_start`]. - /// - /// When an internal database error aborts sandbox execution, the opcode-level - /// event stream may truncate on an unclosed frame (the upstream inspect path - /// propagates the error with `?`). [`sandbox_end`](Self::sandbox_end) is still - /// delivered and is the terminus of the event stream. - #[inline] - fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { - let _ = outcome; - } -} - -impl SandboxObserver for I -where - E: ExternalEnvTypes, - I: for<'a> Inspector, E>, EthInterpreter>, -{ - #[inline] - fn initialize_interp( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - Inspector::initialize_interp(self, interp, context); - } - - #[inline] - fn step( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - Inspector::step(self, interp, context); - } - - #[inline] - fn step_end( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - Inspector::step_end(self, interp, context); - } - - #[inline] - fn log( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - log: Log, - ) { - Inspector::log(self, interp, context, log); - } - - #[inline] - fn call(&mut self, context: &mut MegaContext, E>, inputs: &CallInputs) { - // The inspector works on a copy: its mutations and any override are discarded. - let mut inputs = inputs.clone(); - let _ = Inspector::call(self, context, &mut inputs); - } - - #[inline] - fn call_end( - &mut self, - context: &mut MegaContext, E>, - inputs: &CallInputs, - outcome: &CallOutcome, - ) { - // Clone so a mutating inspector cannot write back into execution. - let mut outcome = outcome.clone(); - Inspector::call_end(self, context, inputs, &mut outcome); - } - - #[inline] - fn create(&mut self, context: &mut MegaContext, E>, inputs: &CreateInputs) { - let mut inputs = inputs.clone(); - let _ = Inspector::create(self, context, &mut inputs); - } - - #[inline] - fn create_end( - &mut self, - context: &mut MegaContext, E>, - inputs: &CreateInputs, - outcome: &CreateOutcome, - ) { - let mut outcome = outcome.clone(); - Inspector::create_end(self, context, inputs, &mut outcome); - } - - #[inline] - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - Inspector::selfdestruct(self, contract, target, value); - } -} - -/// Installs a [`SandboxObserver`] on the sandbox's hook slot without a way to intervene. -/// -/// Every hook forwards a shared reference to the observer, `call` / `create` always answer -/// `None`, and `call_end` / `create_end` hand their outcomes over read-only, so the observer -/// channel is structurally unable to rewrite execution even though it shares the slot with -/// [`SandboxInspector`]. -pub(crate) struct ReadOnlyHook { - observer: Rc>>, -} - -impl ReadOnlyHook { - /// Wraps a shared observer handle. - pub(crate) fn new(observer: Rc>>) -> Self { - Self { observer } - } -} - -impl core::fmt::Debug for ReadOnlyHook { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("ReadOnlyHook").finish_non_exhaustive() - } -} - -impl SandboxInspector for ReadOnlyHook { - #[inline] - fn initialize_interp( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - self.observer.borrow_mut().initialize_interp(interp, context); - } - - #[inline] - fn step( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - self.observer.borrow_mut().step(interp, context); - } - - #[inline] - fn step_end( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - ) { - self.observer.borrow_mut().step_end(interp, context); - } - - #[inline] - fn log( - &mut self, - interp: &mut Interpreter, - context: &mut MegaContext, E>, - log: Log, - ) { - self.observer.borrow_mut().log(interp, context, log); - } - - #[inline] - fn call( - &mut self, - context: &mut MegaContext, E>, - inputs: &mut CallInputs, - ) -> Option { - self.observer.borrow_mut().call(context, inputs); - None - } - - #[inline] - fn call_end( - &mut self, - context: &mut MegaContext, E>, - inputs: &CallInputs, - outcome: &mut CallOutcome, - ) { - self.observer.borrow_mut().call_end(context, inputs, outcome); - } - - #[inline] - fn create( - &mut self, - context: &mut MegaContext, E>, - inputs: &mut CreateInputs, - ) -> Option { - self.observer.borrow_mut().create(context, inputs); - None - } - - #[inline] - fn create_end( - &mut self, - context: &mut MegaContext, E>, - inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - self.observer.borrow_mut().create_end(context, inputs, outcome); - } - - #[inline] - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.observer.borrow_mut().selfdestruct(contract, target, value); - } - - #[inline] - fn sandbox_start(&mut self, info: &SandboxStartInfo) { - self.observer.borrow_mut().sandbox_start(info); - } - - #[inline] - fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { - self.observer.borrow_mut().sandbox_end(outcome); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::EmptyExternalEnv; - use std::boxed::Box; - - struct NopObserver; - - impl SandboxObserver for NopObserver {} - - struct GenericInspector; - - impl Inspector for GenericInspector {} - - #[test] - fn test_sandbox_observer_is_object_safe() { - let _boxed: Box> = Box::new(NopObserver); - let _rc: Rc>> = - Rc::new(RefCell::new(NopObserver)); - } - - #[test] - fn test_sandbox_end_outcome_reports_whether_state_was_applied() { - let applied = - SandboxEndOutcome::Applied { completion: SandboxCompletionKind::Deployed, gas_used: 1 }; - assert!(applied.state_applied()); - - let not_applied = SandboxEndOutcome::NotApplied { reason: SandboxRejectKind::Rejected }; - assert!(!not_applied.state_applied()); - } - - #[test] - fn test_generic_inspector_is_a_sandbox_observer() { - fn assert_observer>(_: T) {} - assert_observer::(GenericInspector); - assert_observer::(NopObserver); - } - - #[test] - fn test_blanket_observer_satisfies_empty_and_parent_env_bounds() { - use crate::TestExternalEnvs; - fn assert_dual + SandboxObserver>( - _: T, - ) { - } - assert_dual(GenericInspector); - assert_dual(NopObserver); - } - - #[test] - fn test_read_only_hook_is_a_sandbox_inspector_handle() { - use super::super::inspector::SandboxHookHandle; - let observer: Rc>> = - Rc::new(RefCell::new(NopObserver)); - let _handle: SandboxHookHandle = - Rc::new(RefCell::new(ReadOnlyHook::new(observer))); - } - - #[test] - fn test_read_only_hook_debug_names_the_adapter() { - use std::format; - let observer: Rc>> = - Rc::new(RefCell::new(NopObserver)); - let rendered = format!("{:?}", ReadOnlyHook::new(observer)); - assert!(rendered.starts_with("ReadOnlyHook"), "{rendered}"); - } -} diff --git a/crates/mega-evm/src/sandbox/trace.rs b/crates/mega-evm/src/sandbox/trace.rs index dde546db..9a4efa01 100644 --- a/crates/mega-evm/src/sandbox/trace.rs +++ b/crates/mega-evm/src/sandbox/trace.rs @@ -3,15 +3,15 @@ //! A [`TracingInspector`] attached to the outer EVM never sees sandbox frames: the sandbox //! is a separate EVM whose journal reports its top-level CREATE at depth 0, and recording that //! into the outer arena would overwrite the CALL root. The pattern here is two recorders: the -//! outer [`TracingInspector`] installed on the EVM, and a [`SandboxTracer`] attached through -//! the keyless sandbox observer channel, which records every sandbox execution into an arena -//! of its own. After the transaction returns, [`splice_sandbox_traces`] grafts each sandbox -//! arena under the outer `KeylessDeploy` CALL frame that started it. +//! outer [`TracingInspector`] installed on the EVM, and a [`SandboxTracer`] attached as the +//! keyless sandbox hook, which records every sandbox execution into an arena of its own. After the +//! transaction returns, [`splice_sandbox_traces`] grafts each sandbox arena under the outer +//! `KeylessDeploy` CALL frame that started it. //! //! ```ignore //! let (outer, sandbox) = paired(TracingInspectorConfig::all()); //! let mut evm = MegaEvm::new(ctx).with_inspector(outer.clone()); -//! evm.set_keyless_sandbox_observer(sandbox.clone()); +//! evm.set_keyless_sandbox_hook(sandbox.clone()); //! let result = evm.inspect_tx(tx)?; //! splice_sandbox_traces(&mut outer.borrow_mut(), &mut sandbox.borrow_mut()); //! ``` @@ -48,18 +48,19 @@ use revm_inspectors::tracing::{ use crate::{ExternalEnvTypes, MegaContext, MegaSpecId, KEYLESS_DEPLOY_ADDRESS}; use super::{ - observer::{OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, SandboxStartInfo}, + inspector::{ + OuterCallInfo, SandboxCompletionKind, SandboxEndOutcome, SandboxInspector, SandboxStartInfo, + }, state::SandboxDb, - SandboxObserver, }; /// [`Inspector`] adapter over a [`TracingInspector`] shared via [`Rc`]/[`RefCell`]. /// /// Serves as the outer EVM inspector when the caller wants to keep a handle to read the /// recorded traces after execution. It is the outer half of [`paired`]; the sandbox half is a -/// [`SandboxTracer`]. Do not attach a handle to this type through the sandbox observer -/// channel: the sandbox would record into the outer arena and overwrite the `KeylessDeploy` -/// CALL root with its own CREATE. +/// [`SandboxTracer`]. Do not attach a handle to this type as the sandbox hook: the sandbox +/// would record into the outer arena and overwrite the `KeylessDeploy` CALL root with its own +/// CREATE. #[derive(Clone, Debug)] pub struct SharedTracingInspector(Rc>); @@ -138,7 +139,7 @@ where /// Builds an outer inspector and a sandbox tracer with the same `config`, ready to be /// attached to one EVM: the first as its inspector, the second through -/// `set_keyless_sandbox_observer`. +/// `set_keyless_sandbox_hook`. pub fn paired( config: TracingInspectorConfig, ) -> (SharedTracingInspector, Rc>) { @@ -191,12 +192,13 @@ impl SandboxTrace { } } -/// Sandbox-side recorder for the keyless sandbox observer channel. +/// Sandbox-side recorder, attached as the keyless sandbox hook. /// /// Records each sandbox execution into a fresh [`TracingInspector`] arena and keeps it, /// together with the identity of the outer `KeylessDeploy` frame that started it, until /// [`splice_sandbox_traces`] grafts it into the outer trace. Attach it via -/// `set_keyless_sandbox_observer`; it implements [`SandboxObserver`] for every env type. +/// `set_keyless_sandbox_hook`; it implements [`SandboxInspector`] for every env type and never +/// intervenes: `call` / `create` answer `None` and inputs and outcomes pass through untouched. /// /// Recording is per execution, so a tracer kept across transactions does not need a reset; /// splicing drains what was recorded, and [`Self::clear`] discards it. Do one or the other @@ -218,7 +220,7 @@ impl SandboxTracer { Self { config, current: None, finished: Vec::new() } } - /// [`Self::new`] wrapped for `set_keyless_sandbox_observer`. + /// [`Self::new`] wrapped for `set_keyless_sandbox_hook`. pub fn handle(config: TracingInspectorConfig) -> Rc> { Rc::new(RefCell::new(Self::new(config))) } @@ -239,7 +241,7 @@ impl SandboxTracer { } } -impl SandboxObserver for SandboxTracer { +impl SandboxInspector for SandboxTracer { fn initialize_interp( &mut self, interp: &mut Interpreter, @@ -273,44 +275,46 @@ impl SandboxObserver for SandboxTracer { self.with_current(|t| Inspector::log(&mut t.tracer, interp, context, log)); } - fn call(&mut self, context: &mut MegaContext, E>, inputs: &CallInputs) { + fn call( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CallInputs, + ) -> Option { self.with_current(|t| { t.entered = true; - let mut inputs = inputs.clone(); - let _ = Inspector::call(&mut t.tracer, context, &mut inputs); + let _ = Inspector::call(&mut t.tracer, context, inputs); }); + None } fn call_end( &mut self, context: &mut MegaContext, E>, inputs: &CallInputs, - outcome: &CallOutcome, + outcome: &mut CallOutcome, ) { - self.with_current(|t| { - let mut outcome = outcome.clone(); - Inspector::call_end(&mut t.tracer, context, inputs, &mut outcome); - }); + self.with_current(|t| Inspector::call_end(&mut t.tracer, context, inputs, outcome)); } - fn create(&mut self, context: &mut MegaContext, E>, inputs: &CreateInputs) { + fn create( + &mut self, + context: &mut MegaContext, E>, + inputs: &mut CreateInputs, + ) -> Option { self.with_current(|t| { t.entered = true; - let mut inputs = inputs.clone(); - let _ = Inspector::create(&mut t.tracer, context, &mut inputs); + let _ = Inspector::create(&mut t.tracer, context, inputs); }); + None } fn create_end( &mut self, context: &mut MegaContext, E>, inputs: &CreateInputs, - outcome: &CreateOutcome, + outcome: &mut CreateOutcome, ) { - self.with_current(|t| { - let mut outcome = outcome.clone(); - Inspector::create_end(&mut t.tracer, context, inputs, &mut outcome); - }); + self.with_current(|t| Inspector::create_end(&mut t.tracer, context, inputs, outcome)); } fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs index a6e25172..bec7fca7 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs @@ -1,9 +1,9 @@ -//! Corner-case sandbox shapes seen through the read-only observer channel. +//! Corner-case sandbox shapes seen through a non-intervening sandbox hook. //! //! Each case uses one of the init codes in `mega_evm::test_utils::keyless` — the same bytes //! the offline state-test corpus and the end-to-end suite run — and pins two things on every -//! spec: the exact event stream the observer receives, and that attaching the observer does -//! not change the result, state, or usage of the transaction. +//! spec: the exact event stream the hook receives, and that attaching the hook does not +//! change the result, state, or usage of the transaction. use std::{cell::RefCell, rc::Rc}; @@ -13,7 +13,7 @@ use mega_evm::{ interpreter::EthInterpreter, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, }, - sandbox::{SandboxCompletionKind, SandboxEndOutcome, SandboxInspector, SandboxObserver}, + sandbox::{SandboxCompletionKind, SandboxEndOutcome, SandboxInspector}, test_utils::{ deep_mixed_init, nested_keyless_call_init, revert_after_create_init, MemoryDatabase, REVERTING_RUNTIME, @@ -23,9 +23,9 @@ use mega_evm::{ use crate::keyless_sandbox_support::{ assert_result_and_state_eq, assert_usage_eq, create_pre_eip155_deploy_tx, - create_pre_eip155_deploy_tx_with_value_and_gas_limit, funded_db, - keyless_deploy_call_tx_with_outer_gas, run_keyless_with_usage, success_constructor, RunConfig, - DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, REVERTER, SPECS, + create_pre_eip155_deploy_tx_with_value_and_gas_limit, funded_db, run_keyless_with_usage, + success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, LARGE_GAS_LIMIT_OVERRIDE, REVERTER, + SPECS, }; /// The frame-level events these cases assert on. @@ -44,60 +44,8 @@ struct Recorder { events: Vec, } -impl SandboxObserver for Recorder { - fn call( - &mut self, - _context: &mut MegaContext, E>, - inputs: &CallInputs, - ) { - self.events.push(Ev::Call { target: inputs.target_address }); - } - - fn call_end( - &mut self, - _context: &mut MegaContext, E>, - inputs: &CallInputs, - outcome: &CallOutcome, - ) { - self.events.push(Ev::CallEnd { - target: inputs.target_address, - reverted: outcome.result.is_revert(), - }); - } - - fn create( - &mut self, - _context: &mut MegaContext, E>, - _inputs: &CreateInputs, - ) { - self.events.push(Ev::Create); - } - - fn create_end( - &mut self, - _context: &mut MegaContext, E>, - _inputs: &CreateInputs, - outcome: &CreateOutcome, - ) { - self.events.push(Ev::CreateEnd { ok: outcome.result.is_ok(), address: outcome.address }); - } - - fn log( - &mut self, - _interp: &mut Interpreter, - _context: &mut MegaContext, E>, - _log: alloy_primitives::Log, - ) { - self.events.push(Ev::Log); - } - - fn sandbox_end(&mut self, outcome: &SandboxEndOutcome) { - self.events.push(Ev::End(outcome.clone())); - } -} - -/// The same recorder on the rewriting channel: records identically and never intervenes, so -/// both channels must produce the same stream and the same execution. +/// Records every hook and never intervenes: `call` / `create` answer `None`, inputs and +/// outcomes pass through untouched. impl SandboxInspector for Recorder { fn call( &mut self, @@ -152,47 +100,9 @@ impl SandboxInspector for Recorder { } } -/// Runs `tx_bytes` with the recorder attached through the rewriting channel. -fn run_through_inspector_channel( - spec: MegaSpecId, - tx_bytes: &Bytes, - signer: Address, - gas_limit_override: u64, - setup: &impl Fn(&mut MemoryDatabase), -) -> ( - mega_evm::revm::context::result::ResultAndState, - mega_evm::LimitUsage, - Vec, -) { - use mega_evm::{ - revm::{inspector::NoOpInspector, ExecuteEvm}, - MegaEvm, - }; - let recorder = Rc::new(RefCell::new(Recorder::default())); - let mut db = funded_db(signer); - setup(&mut db); - let mut context = MegaContext::new(&mut db, spec); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context.set_keyless_sandbox_inspector(Rc::clone(&recorder)); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx_with_outer_gas( - tx_bytes.clone(), - gas_limit_override, - DEFAULT_OUTER_GAS_LIMIT, - ); - let result = evm.transact(tx).expect("keyless deploy through the inspector channel"); - let usage = evm.ctx.additional_limit.borrow().get_usage(); - let events = recorder.borrow().events.clone(); - (result, usage, events) -} - -/// One keyless deploy of `tx_bytes` under `spec`: once with the recorder on the read-only -/// channel, once with no hook, and once with the recorder on the rewriting channel. The three -/// runs must agree on result, state, and usage, and both channels must stream the same -/// events. Returns the observed run and the event stream. +/// One keyless deploy of `tx_bytes` under `spec`: once with the recorder attached and once +/// with no hook. The two runs must agree on result, state, and usage. Returns the observed +/// run and the event stream. fn run_case( spec: MegaSpecId, tx_bytes: &Bytes, @@ -210,7 +120,7 @@ fn run_case( db: &mut db, tx_bytes: tx_bytes.clone(), gas_limit_override, - observer: Some(Rc::clone(&recorder)), + hook: Some(Rc::clone(&recorder)), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -222,7 +132,7 @@ fn run_case( db: &mut db, tx_bytes: tx_bytes.clone(), gas_limit_override, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -231,12 +141,6 @@ fn run_case( assert_usage_eq(observed_usage, baseline_usage, &case); let events = recorder.borrow().events.clone(); - let (rewritten, rewritten_usage, rewritten_events) = - run_through_inspector_channel(spec, tx_bytes, signer, gas_limit_override, &setup); - assert_result_and_state_eq(&rewritten, &baseline, &format!("{case} (inspector channel)")); - assert_usage_eq(rewritten_usage, baseline_usage, &format!("{case} (inspector channel)")); - assert_eq!(rewritten_events, events, "{case}: both channels must stream the same frame events"); - (observed, events) } diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs similarity index 86% rename from crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs rename to crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs index 913a6ae9..191ccd27 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_observer.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs @@ -1,5 +1,5 @@ -//! Sandbox observer: parity with the no-observer path, event-stream contracts, -//! and HRTB adapter coverage for generic inspectors. +//! Non-intervening sandbox hooks: parity with the no-hook path, event-stream and lifecycle +//! contracts, and blanket-impl coverage for generic inspectors. use std::{cell::RefCell, rc::Rc}; @@ -10,7 +10,7 @@ use mega_evm::{ revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ decode_error_result, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, - SandboxObserver, SandboxRejectKind, SandboxStartInfo, + SandboxInspector, SandboxRejectKind, SandboxStartInfo, }, test_utils::{ErrorInjectingDatabase, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, @@ -19,23 +19,21 @@ use revm::{ inspector::NoOpInspector, interpreter::{ interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, - CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, InterpreterResult, + CreateInputs, CreateOutcome, Interpreter, }, Inspector, }; use super::keyless_sandbox_support::{ - assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_and_stores_return, - constructor_calls_identity_precompile, constructor_calls_reverter, - constructor_touches_sentinel, create_pre_eip155_deploy_tx, + assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_precompile, + constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_override_u256, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_parent_env, run_keyless_with_parent_env_usage, run_keyless_with_usage, selfdestructing_constructor, - split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, - IDENTITY_OVERRIDE, IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, - REVERTER, SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, - SPLIT_CREATE_SLOT_VALUE, + split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, + IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, + SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -69,7 +67,7 @@ struct RecordingObserver { events: Vec, } -impl SandboxObserver for RecordingObserver { +impl SandboxInspector for RecordingObserver { fn initialize_interp( &mut self, _interp: &mut Interpreter, @@ -97,16 +95,17 @@ impl SandboxObserver for RecordingObserver { fn call( &mut self, _context: &mut mega_evm::MegaContext, E>, - inputs: &CallInputs, - ) { + inputs: &mut CallInputs, + ) -> Option { self.events.push(ObservedEvent::Call { target: inputs.target_address }); + None } fn call_end( &mut self, _context: &mut mega_evm::MegaContext, E>, inputs: &CallInputs, - _outcome: &CallOutcome, + _outcome: &mut CallOutcome, ) { self.events.push(ObservedEvent::CallEnd { target: inputs.target_address }); } @@ -114,16 +113,17 @@ impl SandboxObserver for RecordingObserver { fn create( &mut self, _context: &mut mega_evm::MegaContext, E>, - _inputs: &CreateInputs, - ) { + _inputs: &mut CreateInputs, + ) -> Option { self.events.push(ObservedEvent::Create); + None } fn create_end( &mut self, _context: &mut mega_evm::MegaContext, E>, _inputs: &CreateInputs, - _outcome: &CreateOutcome, + _outcome: &mut CreateOutcome, ) { self.events.push(ObservedEvent::CreateEnd); } @@ -269,7 +269,7 @@ fn parity_pair( db: &mut db_obs, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(observer), + hook: Some(observer), tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -278,7 +278,7 @@ fn parity_pair( db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -299,7 +299,7 @@ fn run_with_recorder( db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(observer), + hook: Some(observer), tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -575,7 +575,7 @@ struct SplitImplObserver { parent: Vec<&'static str>, } -impl SandboxObserver for SplitImplObserver { +impl SandboxInspector for SplitImplObserver { fn step( &mut self, _interp: &mut Interpreter, @@ -598,7 +598,7 @@ impl SandboxObserver for SplitImplObserver { } } -impl SandboxObserver for SplitImplObserver { +impl SandboxInspector for SplitImplObserver { fn step( &mut self, _interp: &mut Interpreter, @@ -761,97 +761,8 @@ fn test_event_order_create_wraps_steps() { assert!(last_step_end < create_end, "step_end before create_end"); } -/// A revm inspector that answers every identity-precompile CALL with a fixed return. -/// -/// Attached through the read-only channel it must have no effect: the blanket -/// `SandboxObserver` impl hands it a copy of the inputs and discards the override. The -/// same type attached through the rewriting channel is the control arm proving the -/// override is real. -struct OverridingInspector; - -impl Inspector for OverridingInspector { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - (inputs.target_address == IDENTITY_PRECOMPILE).then(|| { - CallOutcome::new( - InterpreterResult::new( - InstructionResult::Return, - Bytes::from(IDENTITY_OVERRIDE.to_be_bytes::<32>()), - Gas::new(inputs.gas_limit), - ), - inputs.return_memory_offset.clone(), - ) - }) - } -} - -fn slot_zero(result: &ResultAndState, addr: Address) -> Option { - result - .state - .get(&addr) - .and_then(|account| account.storage.get(&U256::ZERO)) - .map(|slot| slot.present_value()) -} - -/// The read-only channel structurally drops `call` overrides: an inspector that short-circuits -/// the identity precompile changes nothing when attached as an observer (result, state, and -/// usage match the no-hook run and slot 0 holds the precompile's echo), while the same -/// inspector on the rewriting channel lands its override in slot 0. -#[test] -fn test_observer_channel_drops_call_overrides() { - let (tx_bytes, signer) = - create_pre_eip155_deploy_tx(constructor_calls_identity_and_stores_return()); - let deploy_address = signer.create(0); - - for spec in SPECS { - let mut db_base = funded_db(signer); - let (baseline, baseline_usage) = run_keyless_with_usage(RunConfig { - spec, - db: &mut db_base, - tx_bytes: tx_bytes.clone(), - gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, - tx_limits: None, - outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, - }); - assert_eq!( - slot_zero(&baseline, deploy_address), - Some(IDENTITY_INPUT), - "{spec:?}: the real precompile echoes the input" - ); - - let mut db_obs = funded_db(signer); - let (observed, observed_usage) = run_keyless_with_usage(RunConfig { - spec, - db: &mut db_obs, - tx_bytes: tx_bytes.clone(), - gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(Rc::new(RefCell::new(OverridingInspector))), - tx_limits: None, - outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, - }); - assert_result_and_state_eq(&observed, &baseline, &format!("{spec:?} observer override")); - assert_usage_eq(observed_usage, baseline_usage, &format!("{spec:?} observer override")); - - let mut db_insp = funded_db(signer); - let mut context = MegaContext::new(&mut db_insp, spec); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(OverridingInspector))); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); - let rewritten = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("inspector channel"); - assert_eq!( - slot_zero(&rewritten, deploy_address), - Some(IDENTITY_OVERRIDE), - "{spec:?}: the rewriting channel lands the same inspector's override" - ); - } -} - #[test] -fn test_blanket_observer_records_sandbox_create_for_generic_inspector() { +fn test_blanket_impl_records_sandbox_create_for_generic_inspector() { let tracer = GenericCreateCounter::default(); let creates = Rc::clone(&tracer.creates); let observer = Rc::new(RefCell::new(tracer)); @@ -863,7 +774,7 @@ fn test_blanket_observer_records_sandbox_create_for_generic_inspector() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(observer), + hook: Some(observer), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -876,7 +787,7 @@ fn test_blanket_observer_records_sandbox_create_for_generic_inspector() { } #[test] -fn test_no_observer_skips_lifecycle_hooks() { +fn test_no_hook_skips_lifecycle_hooks() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); let result = run_keyless(RunConfig { @@ -884,7 +795,7 @@ fn test_no_observer_skips_lifecycle_hooks() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -905,7 +816,7 @@ fn test_sandbox_start_info_rex5_caps_effective_gas_limit_below_override() { db: &mut db, tx_bytes, gas_limit_override: OVERRIDE, - observer: Some(Rc::clone(&recorder)), + hook: Some(Rc::clone(&recorder)), tx_limits: None, outer_gas_limit: OUTER_GAS, }); @@ -960,7 +871,7 @@ fn test_observer_parity_split_create_through_interceptor() { db: &mut db_obs, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(Rc::clone(&recorder)), + hook: Some(Rc::clone(&recorder)), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -969,7 +880,7 @@ fn test_observer_parity_split_create_through_interceptor() { db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -994,7 +905,7 @@ fn test_sandbox_start_info_saturates_gas_limit_override_above_u64_max() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(Rc::clone(&recorder)); + context.set_keyless_sandbox_hook(Rc::clone(&recorder)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx_with_override_u256(tx_bytes, U256::MAX, DEFAULT_OUTER_GAS_LIMIT); @@ -1033,7 +944,7 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(Rc::clone(&recorder)); + context.set_keyless_sandbox_hook(Rc::clone(&recorder)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); @@ -1064,7 +975,7 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { /// An observer that overrides nothing: every hook is the trait's default. struct DefaultsOnlyObserver; -impl SandboxObserver for DefaultsOnlyObserver {} +impl SandboxInspector for DefaultsOnlyObserver {} /// The trait's default hook bodies are inert: with a defaults-only observer attached, a /// deployment that logs, calls, creates, and self-destructs inside the sandbox ends with the @@ -1090,7 +1001,7 @@ fn test_defaults_only_observer_is_inert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1099,7 +1010,7 @@ fn test_defaults_only_observer_is_inert() { db: &mut db_obs, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: Some(Rc::new(RefCell::new(DefaultsOnlyObserver))), + hook: Some(Rc::new(RefCell::new(DefaultsOnlyObserver))), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index 5bf7d46d..bd24a8d9 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -1,4 +1,4 @@ -//! Rewriting sandbox inspector channel: intervention cases with a no-intervention control arm. +//! Sandbox hook interventions: each case with a no-intervention control arm. use std::{cell::RefCell, rc::Rc}; @@ -7,7 +7,7 @@ use mega_evm::{ revm::context::result::{ExecutionResult, ResultAndState}, sandbox::{ decode_error_result, KeylessDeployError, SandboxCompletionKind, SandboxEndOutcome, - SandboxInspector, SandboxObserver, SandboxRejectKind, SandboxStartInfo, + SandboxInspector, SandboxRejectKind, SandboxStartInfo, }, test_utils::{ deep_mixed_init, BytecodeBuilder, ErrorInjectingDatabase, MemoryDatabase, REVERTING_RUNTIME, @@ -135,7 +135,7 @@ where context = context.with_tx_runtime_limits(limits); } if let Some(inspector) = config.inspector { - context.set_keyless_sandbox_inspector(inspector); + context.set_keyless_sandbox_hook(inspector); } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx_with_outer_gas( @@ -164,7 +164,7 @@ where chain.operator_fee_constant = Some(U256::ZERO); }); if let Some(inspector) = inspector { - context.set_keyless_sandbox_inspector(inspector); + context.set_keyless_sandbox_hook(inspector); } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); @@ -251,7 +251,7 @@ struct RecordingObserver { events: Vec, } -impl SandboxObserver for RecordingObserver { +impl SandboxInspector for RecordingObserver { fn step( &mut self, interp: &mut Interpreter, @@ -263,9 +263,10 @@ impl SandboxObserver for RecordingObserver { fn create( &mut self, _context: &mut mega_evm::MegaContext, E>, - _inputs: &revm::interpreter::CreateInputs, - ) { + _inputs: &mut revm::interpreter::CreateInputs, + ) -> Option { self.events.push(InspectedEvent::Create); + None } fn sandbox_start(&mut self, _info: &SandboxStartInfo) { @@ -671,7 +672,7 @@ fn test_inspector_no_intervention_parity_across_specs_and_constructors() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -728,7 +729,7 @@ fn test_inspector_call_short_circuit_writes_override_and_still_emits_call_end() db: &mut db_ctrl, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -999,7 +1000,7 @@ fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1030,7 +1031,7 @@ fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1136,7 +1137,7 @@ fn test_inspector_pre_rex4_crowded_parent_env_keeps_empty_env_gas() { } #[test] -fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { +fn test_setting_a_hook_replaces_the_previous_and_clear_restores_parity() { let spec = MegaSpecId::REX5; let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); @@ -1148,8 +1149,8 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_observer(Rc::clone(&observer)); - context.set_keyless_sandbox_inspector(Rc::clone(&inspector)); + context.set_keyless_sandbox_hook(Rc::clone(&observer)); + context.set_keyless_sandbox_hook(Rc::clone(&inspector)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); @@ -1171,8 +1172,8 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_inspector(Rc::clone(&inspector)); - context.set_keyless_sandbox_observer(Rc::clone(&observer)); + context.set_keyless_sandbox_hook(Rc::clone(&inspector)); + context.set_keyless_sandbox_hook(Rc::clone(&observer)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); @@ -1193,7 +1194,7 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopSandboxInspector))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopSandboxInspector))); context.clear_keyless_sandbox_hook(); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); @@ -1204,7 +1205,7 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1217,7 +1218,7 @@ fn test_sandbox_hook_slots_are_exclusive_and_clear_restores_parity() { #[should_panic(expected = "set sandbox hook after external envs are wired")] fn test_with_external_envs_panics_in_debug_when_inspector_is_attached() { let mut context = MegaContext::new(revm::database::EmptyDB::default(), MegaSpecId::REX4); - context.set_keyless_sandbox_inspector(Rc::new(RefCell::new(NopSandboxInspector))); + context.set_keyless_sandbox_hook(Rc::new(RefCell::new(NopSandboxInspector))); let _ = context.with_external_envs(TestExternalEnvs::::new().into()); } @@ -1328,7 +1329,7 @@ fn test_inspector_reaches_all_seven_sandbox_end_outcomes() { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_inspector(Rc::clone(&rec)); + context.set_keyless_sandbox_hook(Rc::clone(&rec)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("outer transact"); @@ -1504,7 +1505,7 @@ fn test_defaults_only_inspector_is_inert_on_selfdestruct() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 4509999a..3f3f34f1 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -7,7 +7,7 @@ use alloy_sol_types::SolCall; use mega_evm::{ alloy_consensus::{Signed, TxLegacy}, revm::context::result::ResultAndState, - sandbox::SandboxObserver, + sandbox::SandboxInspector, test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, LimitUsage, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, @@ -279,14 +279,14 @@ pub(crate) struct RunConfig<'a, O> { pub db: &'a mut MemoryDatabase, pub tx_bytes: Bytes, pub gas_limit_override: u64, - pub observer: Option>>, + pub hook: Option>>, pub tx_limits: Option, pub outer_gas_limit: u64, } pub(crate) fn run_keyless(config: RunConfig<'_, O>) -> ResultAndState where - O: SandboxObserver + 'static, + O: SandboxInspector + 'static, { run_keyless_with_usage(config).0 } @@ -295,7 +295,7 @@ pub(crate) fn run_keyless_with_usage( config: RunConfig<'_, O>, ) -> (ResultAndState, LimitUsage) where - O: SandboxObserver + 'static, + O: SandboxInspector + 'static, { let mut context = MegaContext::new(config.db, config.spec); context.modify_chain(|chain| { @@ -305,8 +305,8 @@ where if let Some(limits) = config.tx_limits { context = context.with_tx_runtime_limits(limits); } - if let Some(observer) = config.observer { - context.set_keyless_sandbox_observer(observer); + if let Some(hook) = config.hook { + context.set_keyless_sandbox_hook(hook); } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx_with_outer_gas( @@ -365,7 +365,7 @@ pub(crate) fn run_keyless_with_parent_env( observer: Option>>, ) -> ResultAndState where - O: SandboxObserver + SandboxObserver + 'static, + O: SandboxInspector + SandboxInspector + 'static, { run_keyless_with_parent_env_usage(spec, db, tx_bytes, env, observer).0 } @@ -378,7 +378,7 @@ pub(crate) fn run_keyless_with_parent_env_usage( observer: Option>>, ) -> (ResultAndState, LimitUsage) where - O: SandboxObserver + SandboxObserver + 'static, + O: SandboxInspector + SandboxInspector + 'static, { let mut context = MegaContext::new(db, spec).with_external_envs(env.into()); context.modify_chain(|chain| { @@ -386,7 +386,7 @@ where chain.operator_fee_constant = Some(U256::ZERO); }); if let Some(observer) = observer { - context.set_keyless_sandbox_observer(observer); + context.set_keyless_sandbox_hook(observer); } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs index 563293c1..4d5f564b 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_trace.rs @@ -103,7 +103,7 @@ fn configured_context( } /// Runs one keyless deploy of `tx_bytes` on a fresh `MegaEvm` with `outer` installed as the -/// EVM inspector and `sandbox` attached through the observer channel. +/// EVM inspector and `sandbox` attached as the sandbox hook. fn trace_keyless_deploy( spec: MegaSpecId, db: &mut MemoryDatabase, @@ -113,7 +113,7 @@ fn trace_keyless_deploy( ) -> ResultAndState { let context = configured_context(db, spec); let mut evm = MegaEvm::new(context).with_inspector(outer); - evm.set_keyless_sandbox_observer(Rc::clone(sandbox)); + evm.set_keyless_sandbox_hook(Rc::clone(sandbox)); evm.inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) .expect("keyless deploy transact") } @@ -128,7 +128,7 @@ fn trace_plain_call( db.set_account_code(STOPPER, Bytes::from_static(&[STOP])); let context = configured_context(db, MegaSpecId::REX5); let mut evm = MegaEvm::new(context).with_inspector(outer); - evm.set_keyless_sandbox_observer(Rc::clone(sandbox)); + evm.set_keyless_sandbox_hook(Rc::clone(sandbox)); let tx = TxEnv { caller: TEST_CALLER, kind: TxKind::Call(STOPPER), @@ -281,7 +281,7 @@ fn test_tracer_splices_deep_mixed_shape_with_no_hook_parity() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - observer: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -477,7 +477,7 @@ fn test_sandbox_aborted_by_database_error_is_grafted_as_a_fatal_error() { let sandbox = SandboxTracer::handle(config); let context = configured_context(&mut db, MegaSpecId::REX5); let mut evm = MegaEvm::new(context).with_inspector(outer.clone()); - evm.set_keyless_sandbox_observer(Rc::clone(&sandbox)); + evm.set_keyless_sandbox_hook(Rc::clone(&sandbox)); let result = evm .inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) .expect("outer transact"); @@ -642,43 +642,36 @@ fn test_outer_adapter_forwards_create_log_and_selfdestruct() { assert!(create.trace.steps.iter().any(|step| step.op.get() == SELFDESTRUCT)); } -/// A plain `TracingInspector` attached through either channel reaches the sandbox through -/// the blanket impls, including the `log` and `selfdestruct` hooks. +/// A plain `TracingInspector` attached as the hook reaches the sandbox through the blanket +/// impl, including the `log` and `selfdestruct` hooks. #[test] -fn test_blanket_channels_forward_log_and_selfdestruct_to_a_tracing_inspector() { - for attach_as_inspector in [false, true] { - for (name, init_code, expect_log, expect_selfdestruct) in [ - ("deep mixed", deep_mixed_init(REVERTER), true, false), - ("selfdestruct", selfdestructing_constructor(), false, true), - ] { - let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); - let mut db = funded_db(signer); - db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); - let tracer = Rc::new(RefCell::new(TracingInspector::new(all_config()))); - let context = configured_context(&mut db, MegaSpecId::REX5); - let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); - if attach_as_inspector { - evm.set_keyless_sandbox_inspector(Rc::clone(&tracer)); - } else { - evm.set_keyless_sandbox_observer(Rc::clone(&tracer)); - } - let result = evm - .inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) - .expect("keyless deploy transact"); - let case = format!("{name} inspector={attach_as_inspector}"); - assert!(result.result.is_success(), "{case}: {:?}", result.result); - - let tracer_ref = tracer.borrow(); - let root = &tracer_ref.traces().nodes()[0]; - assert!(root.trace.kind.is_any_create(), "{case}: sandbox CREATE recorded"); - let logs: usize = tracer_ref.traces().nodes().iter().map(|n| n.logs.len()).sum(); - assert_eq!(logs > 0, expect_log, "{case}: log forwarding, got {logs} logs"); - assert_eq!( - root.trace.selfdestruct_address.is_some(), - expect_selfdestruct, - "{case}: selfdestruct forwarding" - ); - } +fn test_blanket_impl_forwards_log_and_selfdestruct_to_a_tracing_inspector() { + for (name, init_code, expect_log, expect_selfdestruct) in [ + ("deep mixed", deep_mixed_init(REVERTER), true, false), + ("selfdestruct", selfdestructing_constructor(), false, true), + ] { + let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); + let mut db = funded_db(signer); + db.set_account_code(REVERTER, Bytes::from_static(&REVERTING_RUNTIME)); + let tracer = Rc::new(RefCell::new(TracingInspector::new(all_config()))); + let context = configured_context(&mut db, MegaSpecId::REX5); + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + evm.set_keyless_sandbox_hook(Rc::clone(&tracer)); + let result = evm + .inspect_tx(keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE)) + .expect("keyless deploy transact"); + assert!(result.result.is_success(), "{name}: {:?}", result.result); + + let tracer_ref = tracer.borrow(); + let root = &tracer_ref.traces().nodes()[0]; + assert!(root.trace.kind.is_any_create(), "{name}: sandbox CREATE recorded"); + let logs: usize = tracer_ref.traces().nodes().iter().map(|n| n.logs.len()).sum(); + assert_eq!(logs > 0, expect_log, "{name}: log forwarding, got {logs} logs"); + assert_eq!( + root.trace.selfdestruct_address.is_some(), + expect_selfdestruct, + "{name}: selfdestruct forwarding" + ); } } @@ -727,9 +720,8 @@ fn keyless_deploy_envelope(nonce: u64, tx_bytes: Bytes) -> Recovered Date: Tue, 8 Sep 2026 17:42:57 +0800 Subject: [PATCH 28/28] docs(sandbox): finish the single-hook wording and keep the blanket override control Non-intervention now spells out unchanged inputs and outcomes; observer wording in comments, messages, and test names becomes hook; a generic revm Inspector attached through the blanket impl is pinned to land its call override. --- crates/mega-evm/src/evm/AGENTS.md | 4 +- crates/mega-evm/src/sandbox/execution.rs | 27 ++- crates/mega-evm/src/sandbox/inspector.rs | 17 +- crates/mega-evm/src/sandbox/mod.rs | 8 +- .../tests/rex2/keyless_sandbox_extreme.rs | 4 +- .../tests/rex2/keyless_sandbox_hook.rs | 174 ++++++++++++------ .../tests/rex2/keyless_sandbox_inspector.rs | 38 ++-- .../tests/rex2/keyless_sandbox_support.rs | 12 +- 8 files changed, 176 insertions(+), 108 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index bd05ca1f..cd8d866c 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -23,8 +23,8 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - Keep inspector and non-inspector paths behaviorally aligned. ## Keyless sandbox hook -One hook channel into nested keyless-deploy sandbox execution: `SandboxInspector`, attached on `MegaContext` via `set_keyless_sandbox_hook` (forwarded from `MegaEvm` / `MegaBlockExecutor`) and detached via `clear_keyless_sandbox_hook`; setting a hook replaces the previous one. -Hook signatures match revm's `Inspector` on the sandbox EVM: `&mut` inputs and override return values are forwarded, so interventions take effect inside the sandbox as they would on a top-level EVM, and a hook that returns `None` from `call`/`create` and leaves interpreter and context state alone observes without intervening (the same read-only notion the outer EVM's inspector has). +One hook into nested keyless-deploy sandbox execution: `SandboxInspector`, attached on `MegaContext` via `set_keyless_sandbox_hook` (forwarded from `MegaEvm` / `MegaBlockExecutor`) and detached via `clear_keyless_sandbox_hook`; setting a hook replaces the previous one. +Hook signatures match revm's `Inspector` on the sandbox EVM: `&mut` inputs and override return values are forwarded, so interventions take effect inside the sandbox as they would on a top-level EVM, and a hook that returns `None` from `call`/`create` and leaves its `&mut` inputs, outcomes, interpreter, and context unchanged observes without intervening (the same read-only notion the outer EVM's inspector has). The slot is one type-erased handle (`Rc>>`, held twice: parent env type and `EmptyExternalEnv`), and a sandbox's lifecycle events (`sandbox_start` / `sandbox_end`) go to the slot that also receives its opcode-level hooks: `EmptyExternalEnv` pre-REX4, the parent env from REX4 on. Types generic over revm's `Inspector` get the hook through a blanket impl; hosts behind a generic EVM projection (a node's `ConfigureEvm::Evm`) name the attach operation on their own configuration type instead of on mega-evm. With the `inspectors` feature, `sandbox::trace` ships the shared `revm-inspectors` pattern: a `SharedTracingInspector` for the outer EVM paired with a `SandboxTracer` attached as the hook, which records each sandbox execution in an arena of its own keyed by the intercepted call, and `splice_sandbox_traces`, which grafts those arenas under their `KeylessDeploy` CALL frames after execution and leaves the tracer empty; `mega-evme` and node tracing RPCs use the same implementation. diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 9b511b30..f547709b 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -772,7 +772,7 @@ fn run_sandbox_ctx<'db, ExtEnvs: ExternalEnvTypes>( } None => { // Preserve the existing zero-observation path for every caller which - // has not attached a sandbox observer. + // has not attached a sandbox hook. let mut sandbox_evm = MegaEvm::new(sandbox_ctx); let result = sandbox_evm.transact_raw(sandbox_tx); let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage(); @@ -1857,9 +1857,9 @@ mod tests { ]) } - struct SplitNopObserver; + struct SplitNopHook; - impl crate::sandbox::SandboxInspector for SplitNopObserver {} + impl crate::sandbox::SandboxInspector for SplitNopHook {} fn assert_sandbox_outcomes_eq(left: &SandboxOutcome, right: &SandboxOutcome, case: &str) { match (left, right) { @@ -1887,7 +1887,7 @@ mod tests { assert_eq!(left_state.len(), right_state.len(), "{case}: account count"); for (addr, account) in left_state { let other = right_state.get(addr).unwrap_or_else(|| { - panic!("{case}: missing account {addr:?} in observer state") + panic!("{case}: missing account {addr:?} in hooked state") }); assert_eq!(account, other, "{case}: account {addr:?}"); } @@ -1901,7 +1901,7 @@ mod tests { fn run_split_create_fixture( spec: MegaSpecId, - observer: Option>, + hook: Option>, ) -> SandboxOutcome { use crate::test_utils::MemoryDatabase; use revm::state::EvmState; @@ -1934,7 +1934,7 @@ mod tests { let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(SPLIT_CREATE_COMPUTE_BUDGET); - match run_sandbox_ctx(context, tx, Some(limits), block, chain, observer) { + match run_sandbox_ctx(context, tx, Some(limits), block, chain, hook) { SandboxRun::Outcome(outcome) => outcome, other => panic!("split-create fixture produced no outcome: {other:?}"), } @@ -1955,12 +1955,12 @@ mod tests { } #[test] - fn test_observer_parity_pre_rex5_split_create_commits_sstore() { + fn test_hook_parity_pre_rex5_split_create_commits_sstore() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3, MegaSpecId::REX4] { let baseline = run_split_create_fixture(spec, None); - let observer: SandboxHookHandle = - Rc::new(RefCell::new(SplitNopObserver)); - let observed = run_split_create_fixture(spec, Some(observer)); + let hook: SandboxHookHandle = + Rc::new(RefCell::new(SplitNopHook)); + let observed = run_split_create_fixture(spec, Some(hook)); let (created, slot) = split_create_slot(&baseline); assert!(created, "{spec:?}: pre-REX5 split CREATE must leave the account created"); @@ -1974,11 +1974,10 @@ mod tests { } #[test] - fn test_observer_parity_rex5_atomic_create_failure() { + fn test_hook_parity_rex5_atomic_create_failure() { let baseline = run_split_create_fixture(MegaSpecId::REX5, None); - let observer: SandboxHookHandle = - Rc::new(RefCell::new(SplitNopObserver)); - let observed = run_split_create_fixture(MegaSpecId::REX5, Some(observer)); + let hook: SandboxHookHandle = Rc::new(RefCell::new(SplitNopHook)); + let observed = run_split_create_fixture(MegaSpecId::REX5, Some(hook)); let (created, slot) = split_create_slot(&baseline); assert!(!created, "REX5 atomic CREATE failure must not leave a created account"); diff --git a/crates/mega-evm/src/sandbox/inspector.rs b/crates/mega-evm/src/sandbox/inspector.rs index 7d34bf2f..044c9389 100644 --- a/crates/mega-evm/src/sandbox/inspector.rs +++ b/crates/mega-evm/src/sandbox/inspector.rs @@ -1,4 +1,4 @@ -//! Hook channel into nested sandbox execution. +//! Hook into nested sandbox execution. //! //! Keyless sandbox execution is otherwise invisible to a parent inspector. A //! [`SandboxInspector`] attached via [`crate::MegaContext::set_keyless_sandbox_hook`] @@ -7,10 +7,10 @@ //! [`sandbox_end`](SandboxInspector::sandbox_end) lifecycle. Hook signatures match revm's: //! `&mut` inputs and override return values are forwarded, so a hook can rewrite inputs, //! short-circuit `CALL`/`CREATE`, and rewrite outcomes exactly as it could on a top-level -//! EVM. A hook that only observes returns `None` from `call`/`create` (the default bodies) -//! and leaves interpreter and context state alone; that is what read-only means here, as it -//! does for the outer EVM's inspector. [`InspectorBridge`] installs the handle as the sandbox -//! EVM's inspector. +//! EVM. A hook that only observes returns `None` from `call`/`create` (the default bodies), +//! leaves the `&mut` inputs and outcomes it is handed unchanged, and leaves interpreter and +//! context state alone; that is what read-only means here, as it does for the outer EVM's +//! inspector. [`InspectorBridge`] installs the handle as the sandbox EVM's inspector. //! //! # Contract //! @@ -184,9 +184,10 @@ impl SandboxEndOutcome { /// /// Signatures match [`Inspector`]`<`[`MegaContext`]`<`[`SandboxDb`]`<'_>, ExtEnvs>, /// `[`EthInterpreter`]`>`. All hooks have empty / `None` defaults so adding a hook is not a -/// breaking change, and a type that overrides nothing but the hooks it reads from observes -/// without intervening. Method-level lifetimes on [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` -/// keep the trait object-safe. +/// breaking change; a hook that only reads, returning `None` from `call` / `create` and leaving +/// its `&mut` inputs, outcomes, interpreter, and context unchanged, observes without +/// intervening. Method-level lifetimes on +/// [`MegaContext`]`<`[`SandboxDb`]`<'_>, _>` keep the trait object-safe. /// /// Types that already implement [`Inspector`] for every sandbox context lifetime /// receive a blanket [`SandboxInspector`] impl. Local types that are not inspectors diff --git a/crates/mega-evm/src/sandbox/mod.rs b/crates/mega-evm/src/sandbox/mod.rs index 27193ba4..20154e56 100644 --- a/crates/mega-evm/src/sandbox/mod.rs +++ b/crates/mega-evm/src/sandbox/mod.rs @@ -59,7 +59,7 @@ //! //! - `execution` - Core sandbox execution logic and the main entry point //! [`execute_keyless_deploy_call`] -//! - `inspector` - The [`SandboxInspector`] hook channel into nested sandbox execution +//! - `inspector` - The [`SandboxInspector`] hook into nested sandbox execution //! - `trace` - Shared `revm-inspectors` recorder and splicing helpers (`inspectors` feature) //! - `state` - Type-erased database wrapper ([`SandboxDb`]) for isolated execution //! - `state_merge` - Replay-safe merge of sandbox state into the parent journal @@ -73,9 +73,9 @@ //! (also forwarded from [`crate::MegaEvm`] and [`crate::MegaBlockExecutor`]): interpreter hooks //! with revm's `&mut` inputs and override return values, plus a paired `sandbox_start` / //! `sandbox_end` lifecycle. Interventions take effect inside the sandbox as they would on a -//! top-level EVM; a hook that returns `None` from `call`/`create` and leaves interpreter and -//! context state alone does not change execution results. Reverted inner frames still emit -//! their events; whether sandbox state was applied to the parent is reported by +//! top-level EVM; a hook that returns `None` from `call`/`create` and leaves its `&mut` inputs, +//! outcomes, interpreter, and context unchanged does not change execution results. Reverted inner +//! frames still emit their events; whether sandbox state was applied to the parent is reported by //! [`SandboxEndOutcome::state_applied`]. With no hook attached the sandbox path is unchanged. //! //! Attaching a hook does not change sandbox external-env semantics: pre-REX4 sandboxes diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs index bec7fca7..f68d2d17 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_extreme.rs @@ -167,7 +167,7 @@ fn count(events: &[Ev], pred: impl Fn(&Ev) -> bool) -> usize { } /// The constructor CREATEs a child and then REVERTs: the nested CREATE streams through the -/// observer, the sandbox ends as `ExecutionFailed`, and the parent journal receives neither +/// hook, the sandbox ends as `ExecutionFailed`, and the parent journal receives neither /// contract. #[test] fn test_extreme_revert_after_create_streams_child_then_rolls_back() { @@ -299,7 +299,7 @@ fn test_extreme_sandbox_out_of_gas_is_applied_execution_failed() { } /// A constructor that CALLs `KeylessDeploy` from inside the sandbox: interception is -/// depth-0 only, so the call reaches the contract's bytecode and reverts, and the observer +/// depth-0 only, so the call reaches the contract's bytecode and reverts, and the hook /// sees that nested call as an ordinary reverted frame. #[test] fn test_extreme_nested_keyless_call_reverts_inside_sandbox() { diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs index 191ccd27..7abd2ed8 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_hook.rs @@ -19,21 +19,23 @@ use revm::{ inspector::NoOpInspector, interpreter::{ interpreter::EthInterpreter, interpreter_types::Jumps, CallInputs, CallOutcome, - CreateInputs, CreateOutcome, Interpreter, + CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, InterpreterResult, }, Inspector, }; use super::keyless_sandbox_support::{ - assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_precompile, - constructor_calls_reverter, constructor_touches_sentinel, create_pre_eip155_deploy_tx, + assert_result_and_state_eq, assert_usage_eq, constructor_calls_identity_and_stores_return, + constructor_calls_identity_precompile, constructor_calls_reverter, + constructor_touches_sentinel, create_pre_eip155_deploy_tx, create_pre_eip155_deploy_tx_with_value, crowded_parent_env, empty_code_constructor, funded_db, keyless_deploy_call_tx, keyless_deploy_call_tx_with_override_u256, parent_compute_gas_used, revert_constructor, run_keyless, run_keyless_with_parent_env, run_keyless_with_parent_env_usage, run_keyless_with_usage, selfdestructing_constructor, - split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, - IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, REVERTER, - SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, SPLIT_CREATE_SLOT_VALUE, + split_create_initcode, success_constructor, RunConfig, DEFAULT_OUTER_GAS_LIMIT, IDENTITY_INPUT, + IDENTITY_OVERRIDE, IDENTITY_PRECOMPILE, LARGE_GAS_LIMIT_OVERRIDE, MERGE_FAIL_SENTINEL, + REVERTER, SIGNED_TX_GAS_LIMIT, SPECS, SPLIT_CREATE_CODE_LEN, SPLIT_CREATE_SLOT, + SPLIT_CREATE_SLOT_VALUE, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -63,11 +65,11 @@ enum ObservedEvent { } #[derive(Default)] -struct RecordingObserver { +struct RecordingHook { events: Vec, } -impl SandboxInspector for RecordingObserver { +impl SandboxInspector for RecordingHook { fn initialize_interp( &mut self, _interp: &mut Interpreter, @@ -262,14 +264,14 @@ fn parity_pair( let (tx_bytes, signer) = create_pre_eip155_deploy_tx(init_code); let mut db_obs = if fund_signer { funded_db(signer) } else { MemoryDatabase::default() }; let mut db_base = db_obs.clone(); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let hook = Rc::new(RefCell::new(RecordingHook::default())); let (observed, observed_usage) = run_keyless_with_usage(RunConfig { spec, db: &mut db_obs, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: Some(observer), + hook: Some(hook), tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -278,7 +280,7 @@ fn parity_pair( db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -292,14 +294,14 @@ fn run_with_recorder( tx_bytes: Bytes, tx_limits: Option, ) -> (ResultAndState, Vec) { - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); - let observer = Rc::clone(&recorder); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); + let hook = Rc::clone(&recorder); let result = run_keyless(RunConfig { spec, db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: Some(observer), + hook: Some(hook), tx_limits, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -344,12 +346,12 @@ fn assert_call_create_balanced(events: &[ObservedEvent]) { } #[test] -fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { +fn test_hook_parity_pre_rex4_with_nonempty_parent_env() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db_obs = funded_db(signer); let mut db_base = db_obs.clone(); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let hook = Rc::new(RefCell::new(RecordingHook::default())); let env = crowded_parent_env(); let (observed, observed_usage) = run_keyless_with_parent_env_usage( @@ -357,14 +359,14 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { &mut db_obs, tx_bytes.clone(), env.clone(), - Some(observer), + Some(hook), ); let (baseline, baseline_usage) = run_keyless_with_parent_env_usage( spec, &mut db_base, tx_bytes, env, - None::>>, + None::>>, ); assert!( @@ -379,12 +381,12 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env() { } #[test] -fn test_observer_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert() { +fn test_hook_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(revert_constructor()); let mut db_obs = funded_db(signer); let mut db_base = db_obs.clone(); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let hook = Rc::new(RefCell::new(RecordingHook::default())); let env = crowded_parent_env(); let (observed, observed_usage) = run_keyless_with_parent_env_usage( @@ -392,14 +394,14 @@ fn test_observer_parity_pre_rex4_with_nonempty_parent_env_on_constructor_revert( &mut db_obs, tx_bytes.clone(), env.clone(), - Some(observer), + Some(hook), ); let (baseline, baseline_usage) = run_keyless_with_parent_env_usage( spec, &mut db_base, tx_bytes, env, - None::>>, + None::>>, ); assert!( @@ -418,7 +420,7 @@ fn test_opcode_events_flow_on_pre_rex4_with_nonempty_parent_env() { for spec in [MegaSpecId::REX2, MegaSpecId::REX3] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); let result = run_keyless_with_parent_env( spec, &mut db, @@ -443,21 +445,21 @@ fn test_opcode_events_flow_on_pre_rex4_with_nonempty_parent_env() { } #[test] -fn test_observer_parity_success_across_specs() { +fn test_hook_parity_success_across_specs() { for spec in SPECS { parity_pair(spec, success_constructor(), true, None, &format!("success {spec:?}")); } } #[test] -fn test_observer_parity_constructor_revert_across_specs() { +fn test_hook_parity_constructor_revert_across_specs() { for spec in SPECS { parity_pair(spec, revert_constructor(), true, None, &format!("revert {spec:?}")); } } #[test] -fn test_observer_parity_rex5_resource_limit_halt() { +fn test_hook_parity_rex5_resource_limit_halt() { for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let used = parent_compute_gas_used(spec, signer, tx_bytes); @@ -566,16 +568,16 @@ fn test_sandbox_end_applied_empty_code_on_every_spec() { } } -/// Records, per env impl, which hooks a dual-impl observer received. +/// Records, per env impl, which hooks a dual-impl hook type received. #[derive(Default)] -struct SplitImplObserver { +struct SplitImplHook { /// Events seen by the `EmptyExternalEnv` impl. empty: Vec<&'static str>, /// Events seen by the parent-env impl. parent: Vec<&'static str>, } -impl SandboxInspector for SplitImplObserver { +impl SandboxInspector for SplitImplHook { fn step( &mut self, _interp: &mut Interpreter, @@ -598,7 +600,7 @@ impl SandboxInspector for SplitImplObserver { } } -impl SandboxInspector for SplitImplObserver { +impl SandboxInspector for SplitImplHook { fn step( &mut self, _interp: &mut Interpreter, @@ -621,7 +623,7 @@ impl SandboxInspector for SplitImplObserver { } } -/// With a non-empty parent env, an observer with one impl per env sees a sandbox's +/// With a non-empty parent env, a hook with one impl per env sees a sandbox's /// lifecycle and opcode hooks on the same impl: `EmptyExternalEnv` pre-REX4, the parent env /// from REX4 on. #[test] @@ -629,20 +631,20 @@ fn test_lifecycle_and_opcode_hooks_land_on_the_same_env_impl() { for spec in SPECS { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let observer = Rc::new(RefCell::new(SplitImplObserver::default())); + let hook = Rc::new(RefCell::new(SplitImplHook::default())); let result = run_keyless_with_parent_env( spec, &mut db, tx_bytes, crowded_parent_env(), - Some(Rc::clone(&observer)), + Some(Rc::clone(&hook)), ); assert!(result.result.is_success(), "{spec:?}: {:?}", result.result); - let observer = observer.borrow(); + let hook = hook.borrow(); let (used, idle, label) = if spec.is_enabled(MegaSpecId::REX4) { - (&observer.parent, &observer.empty, "parent env") + (&hook.parent, &hook.empty, "parent env") } else { - (&observer.empty, &observer.parent, "EmptyExternalEnv") + (&hook.empty, &hook.parent, "EmptyExternalEnv") }; assert_eq!(used, &["start", "step", "end"], "{spec:?}: whole sandbox on the {label} impl"); assert!(idle.is_empty(), "{spec:?}: the other impl sees nothing, got {idle:?}"); @@ -765,7 +767,7 @@ fn test_event_order_create_wraps_steps() { fn test_blanket_impl_records_sandbox_create_for_generic_inspector() { let tracer = GenericCreateCounter::default(); let creates = Rc::clone(&tracer.creates); - let observer = Rc::new(RefCell::new(tracer)); + let hook = Rc::new(RefCell::new(tracer)); let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); @@ -774,7 +776,7 @@ fn test_blanket_impl_records_sandbox_create_for_generic_inspector() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: Some(observer), + hook: Some(hook), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -795,7 +797,7 @@ fn test_no_hook_skips_lifecycle_hooks() { db: &mut db, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -810,7 +812,7 @@ fn test_sandbox_start_info_rex5_caps_effective_gas_limit_below_override() { for spec in [MegaSpecId::REX5, MegaSpecId::REX6] { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); let result = run_keyless(RunConfig { spec, db: &mut db, @@ -854,7 +856,7 @@ fn test_sandbox_start_info_rex5_caps_effective_gas_limit_below_override() { } #[test] -fn test_observer_parity_split_create_through_interceptor() { +fn test_hook_parity_split_create_through_interceptor() { // Parent 1M compute is not forwarded into pre-REX5 sandboxes; the sandbox // runs at the 200M spec default. Do not set a parent limit — the initcode // itself must overflow that 200M default. EVM gas and compute gas are @@ -864,7 +866,7 @@ fn test_observer_parity_split_create_through_interceptor() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(split_create_initcode()); let mut db_obs = funded_db(signer); let mut db_base = db_obs.clone(); - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); let (observed, observed_usage) = run_keyless_with_usage(RunConfig { spec, @@ -880,7 +882,7 @@ fn test_observer_parity_split_create_through_interceptor() { db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -898,7 +900,7 @@ fn test_observer_parity_split_create_through_interceptor() { fn test_sandbox_start_info_saturates_gas_limit_override_above_u64_max() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db = funded_db(signer); - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); let mut context = MegaContext::new(&mut db, MegaSpecId::REX5); context.modify_chain(|chain| { @@ -938,7 +940,7 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { // Occupancy / sandbox execution load the sentinel once; merge inspects it again. db.fail_on_account_skip = 1; - let recorder = Rc::new(RefCell::new(RecordingObserver::default())); + let recorder = Rc::new(RefCell::new(RecordingHook::default())); let mut context = MegaContext::new(&mut db, spec); context.modify_chain(|chain| { chain.operator_fee_scalar = Some(U256::ZERO); @@ -972,16 +974,82 @@ fn test_sandbox_end_not_applied_apply_failed_on_merge_db_error() { } } -/// An observer that overrides nothing: every hook is the trait's default. -struct DefaultsOnlyObserver; +/// A revm inspector that answers every identity-precompile CALL with a fixed return. +struct OverridingInspector; + +impl Inspector for OverridingInspector { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + (inputs.target_address == IDENTITY_PRECOMPILE).then(|| { + CallOutcome::new( + InterpreterResult::new( + InstructionResult::Return, + Bytes::from(IDENTITY_OVERRIDE.to_be_bytes::<32>()), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + ) + }) + } +} + +fn slot_zero(result: &ResultAndState, addr: Address) -> Option { + result.state.get(&addr).and_then(|a| a.storage.get(&U256::ZERO)).map(|s| s.present_value()) +} + +/// A generic revm `Inspector` attached through the blanket impl lands its `call` override +/// inside the sandbox: the constructor stores what the short-circuited identity call returned. +#[test] +fn test_blanket_impl_forwards_call_overrides() { + for spec in SPECS { + let (tx_bytes, signer) = + create_pre_eip155_deploy_tx(constructor_calls_identity_and_stores_return()); + let deploy_address = signer.create(0); + let mut db_ctrl = funded_db(signer); + let mut db_hooked = db_ctrl.clone(); + + let control = run_keyless(RunConfig { + spec, + db: &mut db_ctrl, + tx_bytes: tx_bytes.clone(), + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + hook: None::>>, + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert_eq!( + slot_zero(&control, deploy_address), + Some(IDENTITY_INPUT), + "{spec:?}: without a hook the identity precompile echoes the input" + ); + + let hooked = run_keyless(RunConfig { + spec, + db: &mut db_hooked, + tx_bytes, + gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, + hook: Some(Rc::new(RefCell::new(OverridingInspector))), + tx_limits: None, + outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, + }); + assert!(hooked.result.is_success(), "{spec:?}: {:?}", hooked.result); + assert_eq!( + slot_zero(&hooked, deploy_address), + Some(IDENTITY_OVERRIDE), + "{spec:?}: the blanket impl forwards the override" + ); + } +} + +/// A hook that overrides nothing: every method is the trait's default. +struct DefaultsOnlyHook; -impl SandboxInspector for DefaultsOnlyObserver {} +impl SandboxInspector for DefaultsOnlyHook {} -/// The trait's default hook bodies are inert: with a defaults-only observer attached, a +/// The trait's default method bodies are inert: with a defaults-only hook attached, a /// deployment that logs, calls, creates, and self-destructs inside the sandbox ends with the /// same result, state, and resource usage as the no-hook run. #[test] -fn test_defaults_only_observer_is_inert() { +fn test_defaults_only_hook_is_inert() { let shapes: [(&str, Bytes); 2] = [ ("deep mixed", mega_evm::test_utils::deep_mixed_init(REVERTER)), ("selfdestruct", selfdestructing_constructor()), @@ -1001,7 +1069,7 @@ fn test_defaults_only_observer_is_inert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1010,11 +1078,11 @@ fn test_defaults_only_observer_is_inert() { db: &mut db_obs, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: Some(Rc::new(RefCell::new(DefaultsOnlyObserver))), + hook: Some(Rc::new(RefCell::new(DefaultsOnlyHook))), tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); - let case = format!("defaults-only observer {name} {spec:?}"); + let case = format!("defaults-only hook {name} {spec:?}"); assert!(baseline.result.is_success(), "{case}: {:?}", baseline.result); assert_result_and_state_eq(&observed, &baseline, &case); assert_usage_eq(observed_usage, baseline_usage, &case); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs index bd24a8d9..ad1c86b6 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_inspector.rs @@ -247,11 +247,11 @@ impl SandboxInspector for RecordingInspector { } #[derive(Default)] -struct RecordingObserver { +struct RecordingHook { events: Vec, } -impl SandboxInspector for RecordingObserver { +impl SandboxInspector for RecordingHook { fn step( &mut self, interp: &mut Interpreter, @@ -672,7 +672,7 @@ fn test_inspector_no_intervention_parity_across_specs_and_constructors() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -729,7 +729,7 @@ fn test_inspector_call_short_circuit_writes_override_and_still_emits_call_end() db: &mut db_ctrl, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1000,7 +1000,7 @@ fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1031,7 +1031,7 @@ fn test_inspector_journal_write_commits_on_success_and_rolls_back_on_revert() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1142,30 +1142,30 @@ fn test_setting_a_hook_replaces_the_previous_and_clear_restores_parity() { let (tx_bytes, signer) = create_pre_eip155_deploy_tx(success_constructor()); let mut db_obs_then_insp = funded_db(signer); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let hook = Rc::new(RefCell::new(RecordingHook::default())); let inspector = Rc::new(RefCell::new(RecordingInspector::default())); let mut context = MegaContext::new(&mut db_obs_then_insp, spec); context.modify_chain(|chain| { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - context.set_keyless_sandbox_hook(Rc::clone(&observer)); + context.set_keyless_sandbox_hook(Rc::clone(&hook)); context.set_keyless_sandbox_hook(Rc::clone(&inspector)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); assert!( - observer.borrow().events.is_empty(), - "observer must be displaced by inspector: {:?}", - observer.borrow().events + hook.borrow().events.is_empty(), + "the first hook must be displaced by the second: {:?}", + hook.borrow().events ); assert!( inspector.borrow().events.iter().any(|e| matches!(e, InspectedEvent::Start)), - "inspector that replaced observer must receive events" + "the hook that replaced the first must receive events" ); let mut db_insp_then_obs = funded_db(signer); - let observer = Rc::new(RefCell::new(RecordingObserver::default())); + let hook = Rc::new(RefCell::new(RecordingHook::default())); let inspector = Rc::new(RefCell::new(RecordingInspector::default())); let mut context = MegaContext::new(&mut db_insp_then_obs, spec); context.modify_chain(|chain| { @@ -1173,18 +1173,18 @@ fn test_setting_a_hook_replaces_the_previous_and_clear_restores_parity() { chain.operator_fee_constant = Some(U256::ZERO); }); context.set_keyless_sandbox_hook(Rc::clone(&inspector)); - context.set_keyless_sandbox_hook(Rc::clone(&observer)); + context.set_keyless_sandbox_hook(Rc::clone(&hook)); let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes.clone(), LARGE_GAS_LIMIT_OVERRIDE); alloy_evm::Evm::transact_raw(&mut evm, tx).expect("transact"); assert!( inspector.borrow().events.is_empty(), - "inspector must be displaced by observer: {:?}", + "the first hook must be displaced by the second: {:?}", inspector.borrow().events ); assert!( - observer.borrow().events.iter().any(|e| matches!(e, InspectedEvent::Start)), - "observer that replaced inspector must receive events" + hook.borrow().events.iter().any(|e| matches!(e, InspectedEvent::Start)), + "the hook that replaced the first must receive events" ); let mut db_cleared = funded_db(signer); @@ -1205,7 +1205,7 @@ fn test_setting_a_hook_replaces_the_previous_and_clear_restores_parity() { db: &mut db_base, tx_bytes, gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); @@ -1505,7 +1505,7 @@ fn test_defaults_only_inspector_is_inert_on_selfdestruct() { db: &mut db_base, tx_bytes: tx_bytes.clone(), gas_limit_override: LARGE_GAS_LIMIT_OVERRIDE, - hook: None::>>, + hook: None::>>, tx_limits: None, outer_gas_limit: DEFAULT_OUTER_GAS_LIMIT, }); diff --git a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs index 3f3f34f1..556cd03e 100644 --- a/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs +++ b/crates/mega-evm/tests/rex2/keyless_sandbox_support.rs @@ -1,4 +1,4 @@ -//! Shared fixtures for keyless-deploy sandbox observer and inspector tests. +//! Shared fixtures for the keyless-deploy sandbox hook tests. use std::{cell::RefCell, rc::Rc}; @@ -362,12 +362,12 @@ pub(crate) fn run_keyless_with_parent_env( db: &mut MemoryDatabase, tx_bytes: Bytes, env: TestExternalEnvs, - observer: Option>>, + hook: Option>>, ) -> ResultAndState where O: SandboxInspector + SandboxInspector + 'static, { - run_keyless_with_parent_env_usage(spec, db, tx_bytes, env, observer).0 + run_keyless_with_parent_env_usage(spec, db, tx_bytes, env, hook).0 } pub(crate) fn run_keyless_with_parent_env_usage( @@ -375,7 +375,7 @@ pub(crate) fn run_keyless_with_parent_env_usage( db: &mut MemoryDatabase, tx_bytes: Bytes, env: TestExternalEnvs, - observer: Option>>, + hook: Option>>, ) -> (ResultAndState, LimitUsage) where O: SandboxInspector + SandboxInspector + 'static, @@ -385,8 +385,8 @@ where chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); }); - if let Some(observer) = observer { - context.set_keyless_sandbox_hook(observer); + if let Some(hook) = hook { + context.set_keyless_sandbox_hook(hook); } let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); let tx = keyless_deploy_call_tx(tx_bytes, LARGE_GAS_LIMIT_OVERRIDE);