From 56cfdc5f8c263ad6467a2746333653ef33fe47be Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 14:54:16 +0800 Subject: [PATCH 001/208] bench: add cheap-opcode interpreter hotloop workload to transact --- crates/mega-evm/benches/transact.rs | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/benches/transact.rs b/crates/mega-evm/benches/transact.rs index b32abddb..8c30516d 100644 --- a/crates/mega-evm/benches/transact.rs +++ b/crates/mega-evm/benches/transact.rs @@ -86,10 +86,56 @@ fn bench_weth9_transfer(c: &mut Criterion) { group.finish(); } +/// Builds a tight countdown loop of cheap opcodes: +/// +/// ```text +/// PUSH3 iterations +/// loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI +/// STOP +/// ``` +/// +/// Each iteration executes 7 opcodes for 26 gas (JUMPDEST 1 + PUSH1 3 + SWAP1 3 + +/// SUB 3 + DUP1 3 + PUSH1 3 + JUMPI 10), all from the cheap-opcode family that +/// dominates real interpreter workloads. +fn hotloop_code(iterations: u32) -> Bytes { + let mut code = Vec::with_capacity(14); + // PUSH3 + code.push(0x62); + code.extend_from_slice(&iterations.to_be_bytes()[1..4]); + // loop target is the JUMPDEST right after the initial PUSH3 (offset 4). + let loop_target = code.len() as u8; + code.push(0x5b); // JUMPDEST + code.push(0x60); // PUSH1 + code.push(0x01); + code.push(0x90); // SWAP1 + code.push(0x03); // SUB + code.push(0x80); // DUP1 + code.push(0x60); // PUSH1 + code.push(loop_target); + code.push(0x57); // JUMPI + code.push(0x00); // STOP + Bytes::from(code) +} + +/// Benchmark a cheap-opcode-dense interpreter hot loop (~700k executed opcodes, +/// ~2.6M gas), the workload shape where per-opcode gas-accounting overhead is +/// the dominant tax. +fn bench_interpreter_hotloop(c: &mut Criterion) { + let mut group = c.benchmark_group("interpreter_hotloop"); + // Gas price is zero, so the caller needs no balance. Callee holds the loop body. + let workload = Workload::single( + vec![Account::new(CALLEE).code(hotloop_code(100_000))], + TxSpec::call(CALLER, CALLEE), + ); + register_all(&mut group, &workload); + group.finish(); +} + criterion_group!( benches, bench_empty_transaction, bench_simple_ether_transfer, - bench_weth9_transfer + bench_weth9_transfer, + bench_interpreter_hotloop ); criterion_main!(benches); From c520902943144cdf39491f65850f11ce11518a5b Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 15:26:58 +0800 Subject: [PATCH 002/208] feat(rex7): settle compute gas at checkpoints Plain opcodes in the REX7 instruction table run revm's raw instructions with no per-opcode recording; compute gas settles as an interpreter-gas delta at each checkpoint (storage-gas opcodes, CALL/CREATE family, volatile opcodes, frame entry/resume/exit). Per-transaction totals are unchanged; a limit exceed now surfaces at the next checkpoint. Specs <= REX6 are untouched. --- crates/mega-evm/src/evm/instructions.rs | 369 +++++++++++++++++++++++- crates/mega-evm/src/limit/limit.rs | 131 ++++++++- 2 files changed, 482 insertions(+), 18 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index db6b23ff..16b2acf7 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -165,6 +165,16 @@ use revm::{ /// usage. CREATE2 is the one real behavior change: REX6+ short-circuits to `create_rex6`, which /// folds the memory-expansion gas into the single post-body recording instead of recording it as /// a separate eager entry as REX5 did. +/// - **REX7** (extends REX6): switches to **checkpoint compute-gas settlement**. The plain opcodes +/// are revm's own instructions with no recording wrapper at all; compute gas settles as an +/// interpreter-gas delta at each checkpoint — the storage-gas opcodes, the CALL / CREATE family, +/// the volatile opcodes, and frame entry / resume / exit. Per-transaction totals are unchanged; a +/// limit exceed surfaces at the next checkpoint rather than at the opcode that crossed it. +/// - Volatile opcodes: `volatile_data_ext::*_checkpoint` (raw instruction + segment settlement + +/// detention cap) in place of the `compute_gas_ext` delegation +/// - Storage-gas, CALL-family, CREATE and SELFDESTRUCT: the REX6 handler chains, settling from +/// the checkpoint baseline internally +/// - Every other opcode: revm's raw instruction /// /// Note: chains terminating at `storage_gas_ext` (rather than `compute_gas_ext`) reflect the /// canonical metering order above — `storage_gas_ext::*` records compute gas internally via @@ -577,22 +587,94 @@ macro_rules! set_halt_action { mod rex7 { use super::*; - /// Returns the instruction table for the `REX7` spec. - /// - /// Changes from Rex6: none yet. + /// Returns the instruction table for the `REX7` spec — **checkpoint compute-gas accounting**. + /// + /// Unlike every earlier custom table, the plain opcodes are revm's own instructions with no + /// per-opcode gas recording at all: the interpreter's gas counter is the accounting source, + /// and compute gas settles as a segment delta at each checkpoint. The checkpoints are exactly + /// the positions that have to stay wrapped anyway: + /// + /// - the storage-gas opcodes (SSTORE, LOG0–LOG4, SELFDESTRUCT) and the CALL / CREATE family — + /// the same handler chains as Rex6, whose [`record_storage_compute_gas!`] settles from the + /// checkpoint baseline instead of a per-opcode capture; + /// - the volatile / detention opcodes — `*_checkpoint` variants that run the raw instruction, + /// settle the segment, then apply the detention cap; + /// - frame entry / resume and frame exit — `AdditionalLimit::before_frame_run` opens the window + /// and `after_frame_run_instructions` settles the tail segment. + /// + /// Per-transaction totals telescope to the same sums as per-opcode recording. What differs is + /// where a limit-exceeding transaction halts: the exceed surfaces at the next checkpoint + /// rather than at the opcode that crossed the limit. + /// + /// The Rex6 behavior differences (canonical metering order, `create_rex6` dispatch, + /// SELFDESTRUCT existing-target accounting, CALL-family EIP-7702 delegate resolution on the + /// disabled path) live as internal `spec.is_enabled(MegaSpecId::REX6)` dispatch inside the + /// shared handlers reused here, so they carry over unchanged. + /// + /// `H` is `Sized` here — unlike the earlier tables — because the base table comes from revm's + /// own [`instructions::instruction_table`], whose bound it is. The only caller instantiates it + /// with [`MegaContext`], so nothing is lost. pub(super) const fn instruction_table< WIRE: InterpreterTypes, - H: HostExt + ContextTr + JournalInspectTr + ?Sized, + H: HostExt + ContextTr + JournalInspectTr, >() -> [Instruction; 256] where WIRE::Stack: StackInspectTr, { - rex6::instruction_table::() + use revm::bytecode::opcode::*; + let mut table = instructions::instruction_table::(); + + // revm's table wires these four ahead of the fork that activates them; every `MegaSpecId` + // maps to a pre-activation Ethereum spec, and no `MegaETH` table has ever dispatched them. + // Restore the unknown-opcode handler so the checkpoint table's opcode set is the same one + // Rex6 exposes. + table[DUPN as usize] = Instruction::new(control::unknown); + table[SWAPN as usize] = Instruction::new(control::unknown); + table[EXCHANGE as usize] = Instruction::new(control::unknown); + table[SLOTNUM as usize] = Instruction::new(control::unknown); + + // Volatile / detention checkpoints: raw instruction, segment settlement, detention cap. + table[BALANCE as usize] = Instruction::new(volatile_data_ext::balance_checkpoint); + table[EXTCODESIZE as usize] = Instruction::new(volatile_data_ext::extcodesize_checkpoint); + table[EXTCODECOPY as usize] = Instruction::new(volatile_data_ext::extcodecopy_checkpoint); + table[EXTCODEHASH as usize] = Instruction::new(volatile_data_ext::extcodehash_checkpoint); + table[BLOCKHASH as usize] = Instruction::new(volatile_data_ext::blockhash_checkpoint); + table[COINBASE as usize] = Instruction::new(volatile_data_ext::coinbase_checkpoint); + table[TIMESTAMP as usize] = Instruction::new(volatile_data_ext::timestamp_checkpoint); + table[NUMBER as usize] = Instruction::new(volatile_data_ext::block_number_checkpoint); + table[DIFFICULTY as usize] = Instruction::new(volatile_data_ext::difficulty_checkpoint); + table[GASLIMIT as usize] = Instruction::new(volatile_data_ext::gas_limit_opcode_checkpoint); + table[BASEFEE as usize] = Instruction::new(volatile_data_ext::basefee_checkpoint); + table[BLOBBASEFEE as usize] = Instruction::new(volatile_data_ext::blobbasefee_checkpoint); + table[BLOBHASH as usize] = Instruction::new(volatile_data_ext::blobhash_checkpoint); + table[SELFBALANCE as usize] = Instruction::new(volatile_data_ext::selfbalance_checkpoint); + table[SLOAD as usize] = Instruction::new(volatile_data_ext::sload_checkpoint); + + // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains unchanged. Under + // Rex7 they settle from the checkpoint baseline internally. + table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); + table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); + table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); + table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); + table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); + table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); + table[CREATE as usize] = Instruction::new(forward_gas_ext::create); + table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); + table[CALL as usize] = Instruction::new(volatile_data_ext::call); + table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); + table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); + table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); + table[SELFDESTRUCT as usize] = + Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); + + table } /// Returns the static gas table for the `REX7` spec. /// - /// The instruction table is unchanged from Rex6, so the zeroed set is too. + /// The volatile-guarded set is unchanged from Rex6 — the checkpoint handlers guard and charge + /// exactly the opcodes their per-opcode counterparts did — so the zeroed set is too. The plain + /// opcodes keep revm's entries: their pre-charge is what the segment delta measures. pub(super) const fn gas_table(table: GasTable) -> GasTable { rex6::gas_table(table) } @@ -758,6 +840,10 @@ macro_rules! run_inner_instruction_or_abort { /// CREATE2 differs only by folding its memory-expansion gas into this single window instead of /// recording it separately. /// +/// Under REX7 checkpoint accounting the window instead opens at the previous checkpoint, so the +/// same recording also settles the unwrapped plain opcodes that ran since; see the macro body for +/// why the exclusions stay exact and why the static gas is no longer added back. +/// /// On exceeding the compute-gas limit, halts the interpreter and returns from the enclosing /// instruction handler. The early return mirrors [`compute_gas!`] so a trailing statement after /// this macro (e.g. the pre-REX5 `resize_gas` late-record in `storage_gas_ext::create`) is only @@ -765,9 +851,24 @@ macro_rules! run_inner_instruction_or_abort { /// add gas to the tracker after the OOG was already set. macro_rules! record_storage_compute_gas { ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr) => {{ + let spec = $context.host.spec_id(); + let is_rex6 = spec.is_enabled(MegaSpecId::REX6); + let is_checkpoint_accounting = spec.is_enabled(MegaSpecId::REX7); let gas_after = $context.interpreter.gas.remaining(); - let mut gas_used = (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) - .saturating_sub($storage_charged); + // Under checkpoint accounting the window opens at the last checkpoint — frame entry / + // resume, or the previous checkpoint opcode — instead of at this handler's own + // `$gas_before` capture, so the plain opcodes that ran since settle here in the same + // recording. Only plain opcodes can run inside that extra span, so no storage gas and no + // forwarded child gas hides in it and the exclusions below stay exact. The window then + // also contains the interpreter's static-gas pre-charge for this opcode, which the + // per-opcode form has to add back because its capture sits after it. + let mut gas_used = if is_checkpoint_accounting { + let baseline = $context.host.additional_limit().borrow().checkpoint_baseline(); + baseline.saturating_sub(gas_after).saturating_sub($storage_charged) + } else { + (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) + .saturating_sub($storage_charged) + }; // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit @@ -776,7 +877,7 @@ macro_rules! record_storage_compute_gas { let mut forwarded_child_gas: u64 = 0; match $context.interpreter.bytecode.action() { Some(InterpreterAction::NewFrame(FrameInput::Call(call_inputs))) => { - let stipend_from_revm = if $context.host.spec_id().is_enabled(MegaSpecId::REX5) && + let stipend_from_revm = if spec.is_enabled(MegaSpecId::REX5) && matches!(call_inputs.scheme, CallScheme::Call | CallScheme::CallCode) && call_inputs.transfers_value() { @@ -797,9 +898,13 @@ macro_rules! record_storage_compute_gas { // On a compute-limit halt the pending child `NewFrame` is discarded (the child never runs), // but revm already deducted the forwarded gas and the outer `forward_gas_ext` erase is // skipped on this abort path. REX6+: return that gas to the parent before halting. - let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); let exceeding_result = { let mut additional_limit = $context.host.additional_limit().borrow_mut(); + // Re-open the settlement window at this opcode's exit before recording, so neither a + // halt here nor the frame-final settlement can bill this segment twice. + if is_checkpoint_accounting { + additional_limit.sync_checkpoint_baseline(gas_after); + } if additional_limit.record_compute_gas(gas_used) { None } else { @@ -1803,6 +1908,223 @@ pub mod volatile_data_ext { wrap_call_volatile_check!(static_call, STATICCALL, forward_gas_ext::static_call); wrap_call_volatile_check!(delegate_call, DELEGATECALL, forward_gas_ext::delegate_call); wrap_call_volatile_check!(call_code, CALLCODE, forward_gas_ext::call_code); + + /* Checkpoint variants of the volatile handlers (REX7+). + + Under checkpoint accounting the volatile opcodes stay wrapped — they are checkpoints — but + they run revm's raw instruction and settle the whole open segment, measured on the + interpreter's own gas counter, in one recording, instead of delegating to a per-opcode + `compute_gas_ext` wrapper. Wherever the opcode's static gas is charged it lands inside that + segment, so the settlement adds nothing back; each handler keeps charging it at the position + its per-opcode counterpart does, because that position decides what an underfunded frame has + already done when it halts. + + The settlement runs before `apply_compute_gas_limit!`, so a REX4+ relative detention cap is + still derived from fully settled usage at the access point. + + The frozen detention-window tripwire the per-opcode conditional wrapper carries is not + repeated here: it watches for historical transactions whose replay would diverge across a revm + bump, and no such transaction can exist for a spec with no activation history. */ + + /// Settles the open checkpoint segment at the interpreter's current gas, re-opens the window, + /// and halts — returning from the enclosing handler — when a limit surfaces. + /// + /// A frame-local exceed reports as a revert, which the enclosing handler's tail would have + /// treated as a normal (non-halting) outcome and still followed with the detention cap, so the + /// cap is applied here before returning. A TX-level exceed reports as an out-of-gas halt, which + /// that tail short-circuits, so the cap is not applied on that path. + macro_rules! settle_checkpoint_compute_gas { + ($context:expr) => { + let exceeding_result = { + let gas_after = $context.interpreter.gas.remaining(); + let mut additional_limit = $context.host.additional_limit().borrow_mut(); + if additional_limit.settle_checkpoint(gas_after) { + None + } else { + Some(additional_limit.exceeding_instruction_result()) + } + }; + if let Some(result) = exceeding_result { + set_halt_action!($context.interpreter, result); + if !result.is_halt() { + apply_compute_gas_limit!($context); + } + return Err(result); + } + }; + } + + /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: disabled guard, static gas ahead + /// of the raw instruction (the position these opcodes' revm bodies charge from), segment + /// settlement, detention cap. + macro_rules! wrap_checkpoint_detain_gas_unconditional { + ($fn_name:ident, $opcode:ident, $original_fn:path, $access_type:expr) => { + #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: raw instruction, segment settlement, gas detention.")] + #[inline] + pub fn $fn_name( + context: InstructionContext<'_, H, WIRE>, + ) -> InstructionExecResult { + if context.host.volatile_access_disabled() { + revert_volatile_access_disabled!(context, $opcode, $access_type); + } + charge_static_gas!(context, $opcode); + + run_inner_instruction_or_abort!($original_fn, context, inner_outcome); + settle_checkpoint_compute_gas!(context); + apply_compute_gas_limit!(context); + inner_outcome + } + }; + } + + /// Checkpoint form of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw instruction, + /// static gas after it (the position these opcodes' revm bodies charge from, so an underfunded + /// frame has already popped its operands and marked its access), segment settlement, detention + /// cap. + macro_rules! wrap_checkpoint_detain_gas_conditional { + ($fn_name:ident, $opcode:ident, $original_fn:path) => { + #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: raw instruction, segment settlement, gas detention.")] + #[inline] + pub fn $fn_name, H: HostExt + ?Sized>( + context: InstructionContext<'_, H, WIRE>, + ) -> InstructionExecResult { + if let Some(addr_word) = context.interpreter.stack.inspect::<0>() { + let target: Address = addr_word.into_address(); + let beneficiary = context.host.beneficiary_address(); + if target == beneficiary && context.host.volatile_access_disabled() { + revert_volatile_access_disabled!( + context, + $opcode, + VolatileDataAccessType::Beneficiary + ); + } + } + + run_inner_instruction_or_abort!($original_fn, context, inner_outcome); + charge_static_gas!(context, $opcode); + settle_checkpoint_compute_gas!(context); + apply_compute_gas_limit!(context); + inner_outcome + } + }; + } + + wrap_checkpoint_detain_gas_unconditional!( + timestamp_checkpoint, + TIMESTAMP, + instructions::block_info::timestamp, + VolatileDataAccessType::Timestamp + ); + wrap_checkpoint_detain_gas_unconditional!( + block_number_checkpoint, + NUMBER, + instructions::block_info::block_number, + VolatileDataAccessType::BlockNumber + ); + wrap_checkpoint_detain_gas_unconditional!( + difficulty_checkpoint, + DIFFICULTY, + instructions::block_info::difficulty, + VolatileDataAccessType::Difficulty + ); + wrap_checkpoint_detain_gas_unconditional!( + gas_limit_opcode_checkpoint, + GASLIMIT, + instructions::block_info::gaslimit, + VolatileDataAccessType::GasLimit + ); + wrap_checkpoint_detain_gas_unconditional!( + basefee_checkpoint, + BASEFEE, + instructions::block_info::basefee, + VolatileDataAccessType::BaseFee + ); + wrap_checkpoint_detain_gas_unconditional!( + coinbase_checkpoint, + COINBASE, + instructions::block_info::coinbase, + VolatileDataAccessType::Coinbase + ); + wrap_checkpoint_detain_gas_unconditional!( + blockhash_checkpoint, + BLOCKHASH, + instructions::host::blockhash, + VolatileDataAccessType::BlockHash + ); + wrap_checkpoint_detain_gas_unconditional!( + blobbasefee_checkpoint, + BLOBBASEFEE, + instructions::block_info::blob_basefee, + VolatileDataAccessType::BlobBaseFee + ); + wrap_checkpoint_detain_gas_unconditional!( + blobhash_checkpoint, + BLOBHASH, + instructions::tx_info::blob_hash, + VolatileDataAccessType::BlobHash + ); + + wrap_checkpoint_detain_gas_conditional!( + balance_checkpoint, + BALANCE, + instructions::host::balance + ); + wrap_checkpoint_detain_gas_conditional!( + extcodesize_checkpoint, + EXTCODESIZE, + instructions::host::extcodesize + ); + wrap_checkpoint_detain_gas_conditional!( + extcodecopy_checkpoint, + EXTCODECOPY, + instructions::host::extcodecopy + ); + wrap_checkpoint_detain_gas_conditional!( + extcodehash_checkpoint, + EXTCODEHASH, + instructions::host::extcodehash + ); + + /// `SLOAD` as a checkpoint. Same oracle-volatile handling as [`sload`], but the raw revm + /// instruction runs unwrapped and the open segment settles here. + #[inline] + pub fn sload_checkpoint( + context: InstructionContext<'_, H, WIRE>, + ) -> InstructionExecResult { + let target = context.interpreter.input.target_address(); + if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { + revert_volatile_access_disabled!(context, SLOAD, VolatileDataAccessType::Oracle); + } + + run_inner_instruction_or_abort!(instructions::host::sload, context, inner_outcome); + charge_static_gas!(context, SLOAD); + settle_checkpoint_compute_gas!(context); + apply_compute_gas_limit!(context); + inner_outcome + } + + /// `SELFBALANCE` as a checkpoint. Same beneficiary-volatile handling as [`selfbalance`], but + /// the raw revm instruction runs unwrapped and the open segment settles here. + #[inline] + pub fn selfbalance_checkpoint( + context: InstructionContext<'_, H, WIRE>, + ) -> InstructionExecResult { + let target = context.interpreter.input.target_address(); + let beneficiary = context.host.beneficiary_address(); + if target == beneficiary && context.host.volatile_access_disabled() { + revert_volatile_access_disabled!( + context, + SELFBALANCE, + VolatileDataAccessType::Beneficiary + ); + } + charge_static_gas!(context, SELFBALANCE); + + run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); + settle_checkpoint_compute_gas!(context); + apply_compute_gas_limit!(context); + inner_outcome + } } /// Extends opcodes with additional limit (kv update limit, data limit, etc.) enforcement. @@ -2607,7 +2929,19 @@ pub mod storage_gas_ext { }; let drained = context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); - gas!(context.interpreter, cost - drained); + let storage_charged = cost - drained; + gas!(context.interpreter, storage_charged); + + // Under checkpoint accounting this storage debit sits inside the window that the + // trailing compute recording in `compute_gas_ext::selfdestruct_self_charged` closes, + // and that recording has no storage term of its own — exclude the debit by lowering + // the open baseline here. + { + let mut additional_limit = context.host.additional_limit().borrow_mut(); + if additional_limit.checkpoint_accounting() { + additional_limit.deduct_checkpoint_baseline(storage_charged); + } + } // Record resource usage for new beneficiary account context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); @@ -2958,8 +3292,19 @@ pub mod compute_gas_ext { } let pre_charged = if SELF_CHARGES_STATIC_GAS { 0 } else { const { static_gas(opcode::SELFDESTRUCT) } }; - let gas_used = pre_charged + gas_before.saturating_sub(context.interpreter.gas.remaining()); + let gas_after = context.interpreter.gas.remaining(); let mut additional_limit = context.host.additional_limit().borrow_mut(); + // Under checkpoint accounting the window opens at the previous checkpoint, so this + // recording also settles the unwrapped plain opcodes that ran since. Wherever the opcode's + // static gas was charged it lands inside that window, so nothing is added back; the + // beneficiary-creation storage charge already lowered the baseline by its own amount. + let gas_used = if additional_limit.checkpoint_accounting() { + let used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); + additional_limit.sync_checkpoint_baseline(gas_after); + used + } else { + pre_charged + gas_before.saturating_sub(gas_after) + }; if !additional_limit.record_compute_gas_all_dims(gas_used) { // A successful inner SELFDESTRUCT has already set its return action, which the halt // replaces; the `Err` is what stops the interpreter loop. diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 98251938..7be501e5 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -107,6 +107,22 @@ pub struct AdditionalLimit { /// A tracker for the `STORAGE_CALL_STIPEND` granted to value-transferring calls (REX4+). pub(crate) storage_call_stipend: storage_call_stipend::StorageCallStipendTracker, + + /// REX7+: whether compute gas settles at checkpoints rather than per opcode. + /// + /// When set, plain opcodes run unwrapped and record nothing; the interpreter's own gas + /// counter is read at each checkpoint and the whole segment since the previous one is + /// recorded in a single call. + checkpoint_accounting: bool, + + /// Interpreter gas remaining at the start of the current unsettled segment — the previous + /// checkpoint, or the frame entry / resume that opened the window. Only meaningful while a + /// frame is running and only when [`checkpoint_accounting`](Self::checkpoint_accounting) is + /// active. Re-synced at every [`before_frame_run`](Self::before_frame_run) (which covers both + /// frame entry and every resume after a child frame's outcome is merged back) and at every + /// checkpoint settlement, and lowered by storage-gas charge sites that debit interpreter gas + /// inside an open window. + checkpoint_baseline: u64, } /// The usage of the additional limits. @@ -134,6 +150,8 @@ impl AdditionalLimit { kv_update: kv_update::KVUpdateTracker::new(spec, limits.tx_kv_updates_limit), compute_gas: compute_gas::ComputeGasTracker::new(spec, limits.tx_compute_gas_limit), storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), + checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), + checkpoint_baseline: 0, } } } @@ -175,6 +193,53 @@ impl AdditionalLimit { self.data_size.reset(); self.kv_update.reset(); self.storage_call_stipend.reset(); + self.checkpoint_baseline = 0; + } + + /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. + #[inline] + pub(crate) fn checkpoint_accounting(&self) -> bool { + self.checkpoint_accounting + } + + /// Interpreter gas remaining at the start of the current unsettled segment. + /// + /// Settlement sites that need to subtract their own storage gas or forwarded child gas read + /// this instead of a per-opcode `gas_before` capture, so the measured delta covers every + /// unwrapped plain opcode executed since the previous checkpoint. + #[inline] + pub(crate) fn checkpoint_baseline(&self) -> u64 { + self.checkpoint_baseline + } + + /// Re-opens the settlement window at `remaining`, without recording anything. + /// + /// Used by settlement sites that compute their own segment amount; every such site must + /// call this once it has recorded, so a later settlement cannot bill the segment twice. + #[inline] + pub(crate) fn sync_checkpoint_baseline(&mut self, remaining: u64) { + self.checkpoint_baseline = remaining; + } + + /// Lowers the open window's baseline by `amount`, excluding a storage-gas debit from the + /// segment that the next settlement will measure. + /// + /// Charge sites that debit storage gas to interpreter gas while a window is open, and whose + /// settlement site does not receive the charged amount directly, use this instead: the + /// settlement then takes `baseline − remaining` with no storage term of its own. + #[inline] + pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { + self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); + } + + /// Settles the open segment against `gas_remaining`, re-opens the window there, and returns + /// `false` when a limit — including a non-compute exceed latched since the previous + /// checkpoint — surfaces. + #[inline] + pub(crate) fn settle_checkpoint(&mut self, gas_remaining: u64) -> bool { + let gas_used = self.checkpoint_baseline.saturating_sub(gas_remaining); + self.checkpoint_baseline = gas_remaining; + self.record_compute_gas(gas_used) } /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every @@ -418,8 +483,33 @@ impl AdditionalLimit { /// This runs on every metered opcode, so it is the hottest hook in the whole tracker. /// `#[inline]` lets the record + within-limit check fold directly into the per-opcode /// wrapper, removing a call across the `RefMut` boundary. + /// + /// Surfacing an exceed here is what turns a latched non-compute overflow into a halt, so + /// the call sites are also the positions a halt can land on: every metered opcode under + /// per-opcode accounting, and every checkpoint under checkpoint accounting. #[inline] pub(crate) fn record_compute_gas(&mut self, compute_gas_used: u64) -> bool { + self.record_compute_gas_impl::(compute_gas_used) + } + + /// Records the compute gas used without the latch-protocol guard. + /// + /// The guard in [`record_compute_gas_impl`](Self::record_compute_gas_impl) asserts that no + /// non-compute dimension is over limit without having latched, which holds at every position + /// an opcode can record from. It does not hold at a frame's final settlement: a pre-inner + /// recorder whose opcode then failed (SELFDESTRUCT's beneficiary accounting) deliberately + /// leaves its usage unlatched, and the frame is about to pop and discard it. Recording it + /// through the guarded entry point would trip the assert on that path. + #[inline] + pub(crate) fn record_compute_gas_unguarded(&mut self, compute_gas_used: u64) -> bool { + self.record_compute_gas_impl::(compute_gas_used) + } + + #[inline] + fn record_compute_gas_impl( + &mut self, + compute_gas_used: u64, + ) -> bool { // Record unconditionally, even when another dimension has already latched an exceed: // the compute work was performed, and the recorded total feeds the transaction outcome // and block-level compute accounting. Skipping the record would under-report compute @@ -437,13 +527,15 @@ impl AdditionalLimit { // only if every non-compute mutation site already latched its own exceed. If a // non-compute dimension is over limit but not yet latched, some mutation site is missing // its `check_limit()` — catch it here in tests, not in production. The sub-tracker - // `check_limit()` calls are non-mutating, so this compiles out of release builds. (The - // one pre-inner recorder, SELFDESTRUCT, routes through `record_compute_gas_all_dims`, not - // this method, so it never trips this.) + // `check_limit()` calls are non-mutating, so this compiles out of release builds. The one + // pre-inner recorder, SELFDESTRUCT, routes through `record_compute_gas_all_dims`, not this + // method, so it never trips this; the frame-final settlement, which can observe that same + // recorder's usage after its opcode failed, opts out via `GUARD_LATCH_PROTOCOL`. debug_assert!( - !self.data_size.check_limit().exceeded_limit() && - !self.kv_update.check_limit().exceeded_limit() && - !self.state_growth.check_limit().exceeded_limit(), + !GUARD_LATCH_PROTOCOL || + (!self.data_size.check_limit().exceeded_limit() && + !self.kv_update.check_limit().exceeded_limit() && + !self.state_growth.check_limit().exceeded_limit()), "non-compute limit exceeded without latching: a mutation site is missing check_limit()", ); // Recording compute gas can only change the compute-gas dimension, so check just that one @@ -670,6 +762,15 @@ impl AdditionalLimit { &mut self, frame: &EthFrame, ) -> Option { + // Checkpoint accounting: open the settlement window at the frame's current gas. This hook + // runs both at frame entry and at every resume after a child frame's outcome — including + // the gas it returned — has been merged back into this frame's interpreter, so the window + // always starts at an instruction boundary with the interpreter's counter in its real, + // post-merge state. + if self.checkpoint_accounting { + self.checkpoint_baseline = frame.interpreter.gas.remaining(); + } + self.state_growth.before_frame_run(frame); self.data_size.before_frame_run(frame); self.kv_update.before_frame_run(frame); @@ -719,6 +820,24 @@ impl AdditionalLimit { frame: &'a EthFrame, action: &'a mut InterpreterAction, ) { + // Checkpoint accounting: the frame has produced its final action, so settle the tail + // segment — everything since the last checkpoint — against the interpreter's gas counter. + // `frame.interpreter.gas` still holds the loop-exit value here (the code-deposit storage + // charge applied by the execution-layer hook mutates only the action's gas copy), so the + // delta telescopes over exactly the unwrapped plain opcodes that ran since. A checkpoint + // that already settled and halted leaves `baseline == remaining` (delta 0), and a CALL + // abort path's forwarded-gas `erase_cost` can only raise `remaining` above the baseline, + // which the saturation turns into 0. Any exceed recorded here is latched, and the frame + // result marking below / in `before_frame_return_result` surfaces it. + if self.checkpoint_accounting { + if let InterpreterAction::Return(_) = action { + let remaining = frame.interpreter.gas.remaining(); + let gas_used = self.checkpoint_baseline.saturating_sub(remaining); + self.checkpoint_baseline = remaining; + let _ = self.record_compute_gas_unguarded(gas_used); + } + } + self.state_growth.after_frame_run(frame, action); self.data_size.after_frame_run(frame, action); self.kv_update.after_frame_run(frame, action); From 035426bd32f5b65c750d796ecb18f171966549d5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 15:26:58 +0800 Subject: [PATCH 003/208] test(rex7): pin REX6/REX7 checkpoint settlement parity Covers plain segments, SSTORE/LOG, SLOAD, the CALL family (success, revert, nested), CREATE/CREATE2, SELFDESTRUCT, volatile detention below the cap and the GAS reading, each with minimum and scaled SALT buckets. Also pins the two places the models differ: checkpoint-coarsened halts and out-of-gas frames. --- .../tests/rex7/checkpoint_settlement.rs | 542 ++++++++++++++++++ crates/mega-evm/tests/rex7/common.rs | 134 +++++ crates/mega-evm/tests/rex7/main.rs | 5 + 3 files changed, 681 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/checkpoint_settlement.rs create mode 100644 crates/mega-evm/tests/rex7/common.rs diff --git a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs new file mode 100644 index 00000000..41deb3ca --- /dev/null +++ b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs @@ -0,0 +1,542 @@ +//! REX7 checkpoint compute-gas settlement. +//! +//! Under checkpoint accounting the plain opcodes run revm's raw instructions with no per-opcode +//! recording; compute gas settles as an interpreter-gas delta at each checkpoint — the storage-gas +//! opcodes, the CALL / CREATE family, the volatile opcodes, and frame entry / resume / exit. +//! +//! The property these tests pin is the **precision invariant**: for a transaction that stays +//! inside every per-tx limit, the settled totals are bit-identical to per-opcode recording, so +//! REX6 and REX7 produce the same compute gas, the same four-dimension usage, the same receipt +//! `gas_used`, and the same execution result. The interpreter's gas counter meters every opcode +//! anyway, so summing it by segment reproduces the per-opcode sum exactly. +//! +//! The two places where the models are *not* identical are pinned at the bottom of this file: +//! a limit crossing inside a plain-opcode segment surfaces at the next checkpoint rather than at +//! the crossing opcode, and a frame that halts out of gas settles its burned remainder as compute +//! gas. + +use crate::common::{ + transact, transact_default, transact_with_bucket_capacity, Outcome, CALLEE, CALLER, CONTRACT, + EMPTY_TARGET, ONE_ETH, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{ + ADD, BALANCE, CALL, CALLCODE, CREATE, CREATE2, DELEGATECALL, DUP1, EXTCODEHASH, EXTCODESIZE, + GAS, JUMPDEST, JUMPI, LOG1, MUL, POP, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, SUB, + SWAP1, TIMESTAMP, +}; + +/// A third contract, so a CALL chain can reach depth 2. +const INNER: Address = address!("0000000000000000000000000000000000300004"); + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// A SALT bucket capacity four times the minimum, so every SALT-scaled storage-gas charge +/// (`SSTORE` set, new account, contract creation) is non-zero and the settlement sites that have +/// to exclude those charges from the compute window are actually exercised. +const SCALED_BUCKET_CAPACITY: u64 = 4 * mega_evm::MIN_BUCKET_SIZE as u64; + +/// Asserts that `build_db()` executes identically under REX6 (per-opcode recording) and REX7 +/// (checkpoint settlement): same success, same result, same four-dimension usage, same `gas_used`. +/// +/// Every case is run twice — once with minimum SALT buckets and once with +/// [`SCALED_BUCKET_CAPACITY`] — so the storage-gas exclusions are checked with a non-zero charge +/// as well. The returned outcomes are the minimum-bucket ones. +fn assert_settlement_parity( + label: &str, + expect_success: bool, + build_db: impl Fn() -> MemoryDatabase, +) -> (Outcome, Outcome) { + assert_settlement_parity_with_limits( + label, + expect_success, + build_db, + EvmTxRuntimeLimits::from_spec, + ) +} + +/// [`assert_settlement_parity`] with per-spec runtime limits. +fn assert_settlement_parity_with_limits( + label: &str, + expect_success: bool, + build_db: impl Fn() -> MemoryDatabase, + limits: impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + assert_outcomes_match(label, expect_success, &r6, &r7); + + let scaled_label = alloc_scaled_label(label); + let s6 = transact_with_bucket_capacity( + MegaSpecId::REX6, + build_db(), + limits(MegaSpecId::REX6), + SCALED_BUCKET_CAPACITY, + ); + let s7 = transact_with_bucket_capacity( + MegaSpecId::REX7, + build_db(), + limits(MegaSpecId::REX7), + SCALED_BUCKET_CAPACITY, + ); + assert_outcomes_match(&scaled_label, expect_success, &s6, &s7); + + (r6, r7) +} + +fn alloc_scaled_label(label: &str) -> String { + format!("{label} (scaled SALT buckets)") +} + +fn assert_outcomes_match(label: &str, expect_success: bool, r6: &Outcome, r7: &Outcome) { + assert_eq!( + r6.is_success(), + expect_success, + "{label}: REX6 success expectation mismatch; got {:?}", + r6.result + ); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "{label}: execution result must be identical; REX6={:?} REX7={:?}", + r6.result, + r7.result + ); + assert_eq!( + r6.compute_gas, r7.compute_gas, + "{label}: checkpoint settlement must telescope to the per-opcode compute-gas sum; \ + REX6={} REX7={}", + r6.compute_gas, r7.compute_gas + ); + assert_eq!( + r6.gas_used, r7.gas_used, + "{label}: receipt gas_used must be unchanged; REX6={} REX7={}", + r6.gas_used, r7.gas_used + ); + assert_eq!( + (r6.data_size, r6.kv_updates, r6.state_growth), + (r7.data_size, r7.kv_updates, r7.state_growth), + "{label}: the non-compute dimensions must be unchanged", + ); +} + +/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body: +/// +/// ```text +/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP +/// ``` +/// +/// `prefix` is prepended verbatim and participates in the jump-target offset. +fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own under checkpoint +/// accounting. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// A pure arithmetic loop settles only at the frame-exit checkpoint, and that single settlement +/// must equal the sum the per-opcode wrappers would have recorded opcode by opcode. +#[test] +fn test_plain_arithmetic_loop_settles_to_the_per_opcode_sum() { + let code = countdown_loop_code(&[], 500); + let (r6, _) = assert_settlement_parity("plain loop", true, || base_db(code.clone())); + assert!(r6.compute_gas > 10_000, "the loop must be substantial; compute={}", r6.compute_gas); +} + +/// A segment that runs plain opcodes, hits a mid-code checkpoint, then runs more plain opcodes +/// before the frame exits: the two settlements must partition the frame's gas exactly. +#[test] +fn test_plain_segments_around_a_mid_code_checkpoint() { + let code = plain_filler(BytecodeBuilder::default(), 40) + .push_u256(U256::from(99u64)) + .push_u256(U256::from(7u64)) + .append(SSTORE); + let code = plain_filler(code, 40).append(STOP).build(); + assert_settlement_parity("plain | SSTORE | plain", true, || base_db(code.clone())); +} + +/// SSTORE and LOG both charge storage gas inside their compute window and subtract it back out. +/// Interleaving them with plain opcodes checks that the subtraction stays exact once the window +/// also spans the plain opcodes before them. +#[test] +fn test_sstore_and_log_mixed_with_plain_opcodes() { + let code = plain_filler(BytecodeBuilder::default(), 20) + .sstore(U256::from(1), U256::from(0x11)) + .mstore(0, [0x22u8; 32]) + .push_number(0xabcu64) // topic0 + .push_number(32u64) // len + .push_number(0u64) // offset + .append(LOG1); + let code = plain_filler(code, 20).sstore(U256::from(2), U256::from(0x33)).append(STOP).build(); + let (r6, _) = assert_settlement_parity("SSTORE + LOG mix", true, || base_db(code.clone())); + assert!(r6.data_size > 0, "the log and stores must register data size"); + assert!(r6.kv_updates > 0, "the stores must register KV updates"); +} + +/// A cold then warm SLOAD, each a checkpoint, with plain opcodes between them. +#[test] +fn test_sload_checkpoints_with_plain_opcodes_between() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_u256(U256::from(3)) + .append(SLOAD) + .append(POP); + let code = plain_filler(code, 10) + .push_u256(U256::from(3)) + .append(SLOAD) + .append(POP) + .append(STOP) + .build(); + assert_settlement_parity("SLOAD checkpoints", true, || base_db(code.clone())); +} + +/// Bytecode for a CALL to `target` forwarding `gas_limit` and `value`. +fn call_code(target: Address, value: u64, gas_limit: u64) -> BytecodeBuilder { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(value) + .push_address(target) + .push_number(gas_limit) + .append(CALL) + .append(POP) +} + +/// A CALL sub-frame that succeeds: the caller's segment settles at the CALL checkpoint (before +/// `frame_init`), the callee settles its own segments, and the caller's window re-opens at the +/// resume with the callee's returned gas already merged back. +#[test] +fn test_call_subframe_success() { + let callee = plain_filler(BytecodeBuilder::default(), 15) + .sstore(U256::from(5), U256::from(0x77)) + .append(STOP) + .build(); + let code = plain_filler(call_code(CALLEE, 0, 1_000_000), 15).append(STOP).build(); + assert_settlement_parity("CALL success", true, || { + base_db(code.clone()).account_code(CALLEE, callee.clone()) + }); +} + +/// A CALL sub-frame that reverts: the callee's segments still settle (compute gas is persistent +/// even when the frame's state changes are dropped), and the returned gas re-opens the caller's +/// window at the resume. +#[test] +fn test_call_subframe_revert() { + let callee = plain_filler(BytecodeBuilder::default(), 15) + .sstore(U256::from(5), U256::from(0x77)) + .revert() + .build(); + let code = plain_filler(call_code(CALLEE, 0, 1_000_000), 15).append(STOP).build(); + let (r6, _) = assert_settlement_parity("CALL revert", true, || { + base_db(code.clone()).account_code(CALLEE, callee.clone()) + }); + assert!(r6.compute_gas > 0); +} + +/// A value-transferring CALL to an empty account: the new-account storage gas is charged inside +/// the CALL's compute window and subtracted back out, with the window now also spanning the plain +/// opcodes ahead of it. +#[test] +fn test_call_value_transfer_to_empty_account() { + let code = plain_filler(call_code(EMPTY_TARGET, 1, 1_000_000), 10).append(STOP).build(); + assert_settlement_parity("CALL value transfer", true, || base_db(code.clone())); +} + +/// Two nested CALL frames, so the resume path runs at two depths. +#[test] +fn test_nested_call_frames() { + let inner = plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(9), U256::from(0x99)) + .append(STOP) + .build(); + let callee = plain_filler(call_code(INNER, 0, 500_000), 10).append(STOP).build(); + let code = plain_filler(call_code(CALLEE, 0, 2_000_000), 10).append(STOP).build(); + assert_settlement_parity("nested CALL", true, || { + base_db(code.clone()) + .account_code(CALLEE, callee.clone()) + .account_code(INNER, inner.clone()) + }); +} + +/// DELEGATECALL, STATICCALL and CALLCODE all reach the same checkpoint chain as CALL. +#[test] +fn test_delegatecall_staticcall_callcode_frames() { + let callee = plain_filler(BytecodeBuilder::default(), 10).append(STOP).build(); + for (label, opcode, value_operand) in [ + ("DELEGATECALL", DELEGATECALL, false), + ("STATICCALL", STATICCALL, false), + ("CALLCODE", CALLCODE, true), + ] { + let mut builder = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64); // argsOffset + if value_operand { + builder = builder.push_number(0u64); + } + let code = plain_filler( + builder.push_address(CALLEE).push_number(500_000u64).append(opcode).append(POP), + 10, + ) + .append(STOP) + .build(); + assert_settlement_parity(label, true, || { + base_db(code.clone()).account_code(CALLEE, callee.clone()) + }); + } +} + +/// Initcode that deploys `runtime` as the created contract's code. +fn deploying_initcode(runtime: &[u8]) -> Vec { + BytecodeBuilder::default().return_with_data(runtime).build_vec() +} + +/// A CREATE whose child frame really runs initcode: the create frame gets its own window at entry +/// and its own tail settlement at exit, and the code-deposit accounting runs on top of both. +#[test] +fn test_create_child_frame() { + let runtime = plain_filler(BytecodeBuilder::default(), 4).append(STOP).build_vec(); + let initcode = deploying_initcode(&runtime); + let len = initcode.len() as u64; + let code = plain_filler(BytecodeBuilder::default(), 10) + .mstore(0, &initcode) + .push_number(len) // length + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP); + let code = plain_filler(code, 10).append(STOP).build(); + assert_settlement_parity("CREATE", true, || base_db(code.clone())); +} + +/// CREATE2 folds its memory-expansion gas into the same single window, which under checkpoint +/// accounting also spans the plain opcodes before it. +#[test] +fn test_create2_child_frame() { + let runtime = plain_filler(BytecodeBuilder::default(), 4).append(STOP).build_vec(); + let initcode = deploying_initcode(&runtime); + let len = initcode.len() as u64; + let code = plain_filler(BytecodeBuilder::default(), 10) + .mstore(0, &initcode) + .push_number(0x5a5au64) // salt + .push_number(len) // length + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE2) + .append(POP); + let code = plain_filler(code, 10).append(STOP).build(); + assert_settlement_parity("CREATE2", true, || base_db(code.clone())); +} + +/// SELFDESTRUCT to an empty beneficiary charges new-account storage gas from a site that is not +/// the settlement site, so the open window's baseline has to be lowered by exactly that charge. +#[test] +fn test_selfdestruct_new_beneficiary_excludes_its_storage_gas() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_address(EMPTY_TARGET) + .append(SELFDESTRUCT) + .build(); + assert_settlement_parity("SELFDESTRUCT new beneficiary", true, || base_db(code.clone())); +} + +/// SELFDESTRUCT to an existing beneficiary takes the other arm (no storage-gas charge, REX6+ +/// account-write accounting only). +#[test] +fn test_selfdestruct_existing_beneficiary() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_address(CALLEE) + .append(SELFDESTRUCT) + .build(); + let callee = BytecodeBuilder::default().append(STOP).build(); + assert_settlement_parity("SELFDESTRUCT existing beneficiary", true, || { + base_db(code.clone()).account_code(CALLEE, callee.clone()) + }); +} + +/// TIMESTAMP marks block-environment access and lowers the compute-gas limit to +/// `usage_at_access + cap`. With the cap comfortably above what the rest of the transaction +/// spends, detention is engaged but never binding — and the detention cap must be derived from +/// fully settled usage, so REX6 and REX7 must still agree bit for bit. +#[test] +fn test_block_env_detention_below_the_cap() { + let code = countdown_loop_code(&[TIMESTAMP, POP], 200); + let limits = |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = 1_000_000; + limits + }; + assert_settlement_parity_with_limits( + "TIMESTAMP detention", + true, + || base_db(code.clone()), + limits, + ); +} + +/// The beneficiary-conditional volatile checkpoints (BALANCE / EXTCODESIZE / EXTCODEHASH) charge +/// their static gas after the raw instruction, which under checkpoint accounting lands inside the +/// settled segment rather than being added back. +#[test] +fn test_conditional_volatile_checkpoints() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_address(CALLEE) + .append(BALANCE) + .append(POP) + .push_address(CALLEE) + .append(EXTCODESIZE) + .append(POP) + .push_address(CALLEE) + .append(EXTCODEHASH) + .append(POP); + let code = plain_filler(code, 10).append(STOP).build(); + let callee = BytecodeBuilder::default().append(STOP).build(); + assert_settlement_parity("conditional volatile", true, || { + base_db(code.clone()).account_code(CALLEE, callee.clone()) + }); +} + +/// `GAS` is not a checkpoint: it runs raw and reads the interpreter's own counter. The settlement +/// must not perturb that counter, so a contract that stores its `GAS` reading must store the same +/// value under both specs. +#[test] +fn test_gas_opcode_reads_the_same_remaining_gas() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .append(GAS) + .push_u256(U256::from(4)) + .append(SSTORE) + .append(STOP) + .build(); + let (r6, r7) = assert_settlement_parity("GAS reading", true, || base_db(code.clone())); + let slot = U256::from(4); + assert_eq!( + r6.storage_value(CONTRACT, slot), + r7.storage_value(CONTRACT, slot), + "GAS must observe the same interpreter gas under both accounting models", + ); + assert!(!r6.storage_value(CONTRACT, slot).is_zero(), "the GAS reading must be non-zero"); +} + +/// A long straight run of arithmetic with no checkpoint at all, so the whole frame is one segment +/// settled once at the frame-exit checkpoint. +#[test] +fn test_single_segment_frame() { + let mut builder = BytecodeBuilder::default().push_number(7u64); + for _ in 0..200 { + builder = builder.push_number(3u64).append(ADD).push_number(2u64).append(MUL); + } + let code = builder.append(POP).append(STOP).build(); + assert_settlement_parity("single segment", true, || base_db(code.clone())); +} + +/// Pushes the operands of an `SSTORE(slot=7, value=99)` and stops before the SSTORE byte, so a run +/// measures the compute gas accumulated up to the opcode under test. +fn plain_run_then_sstore_code(pairs: usize, include_sstore: bool) -> Bytes { + let builder = plain_filler(BytecodeBuilder::default(), pairs) + .push_u256(U256::from(99u64)) + .push_u256(U256::from(7u64)); + let builder = if include_sstore { builder.append(SSTORE) } else { builder }; + builder.append(STOP).build() +} + +/// The one enforcement difference this ticket's model has: a compute-gas crossing inside a +/// plain-opcode segment is not caught at the crossing opcode — nothing is metered there — but at +/// the next checkpoint. Both specs halt; REX7 records the whole segment up to that checkpoint, +/// which is exactly what an unconstrained run records at the same point. +#[test] +fn test_compute_limit_crossing_surfaces_at_the_next_checkpoint() { + let code = plain_run_then_sstore_code(200, true); + let usage_before_sstore = + transact_default(MegaSpecId::REX7, base_db(plain_run_then_sstore_code(200, false))) + .compute_gas; + // Trip the limit partway through the plain run, well before the SSTORE checkpoint. + let intrinsic = + transact_default(MegaSpecId::REX7, base_db(plain_run_then_sstore_code(0, false))) + .compute_gas; + let compute_limit = intrinsic + (usage_before_sstore - intrinsic) / 2; + let limits = + |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); + + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code.clone()), limits(MegaSpecId::REX7)); + let r7_unconstrained = transact_default(MegaSpecId::REX7, base_db(code)); + + assert!(!r6.is_success(), "REX6 must halt on the tight compute limit; got {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must halt on the tight compute limit; got {:?}", r7.result); + assert!( + r6.compute_gas <= compute_limit + 12, + "REX6 halts at the crossing opcode; compute={} limit={compute_limit}", + r6.compute_gas + ); + assert_eq!( + r7.compute_gas, r7_unconstrained.compute_gas, + "REX7 settles the whole segment through the SSTORE checkpoint", + ); + assert!( + r7.compute_gas > r6.compute_gas, + "checkpoint enforcement overshoots per-opcode enforcement; REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); +} + +/// The second difference: a frame that halts out of EVM gas has its remaining budget zeroed by the +/// interpreter before the frame-exit settlement reads the counter, so the burned remainder settles +/// as compute gas. Per-opcode recording attributes nothing to the failing opcode and nothing to +/// the burn, so REX7 reports strictly more compute gas for such a frame. +/// +/// The direction is the safe one — compute usage is over-reported, never under-reported — and the +/// halt itself is identical. +#[test] +fn test_out_of_gas_frame_settles_its_burned_gas_as_compute() { + // A callee that runs out of the gas its caller forwarded. + let callee = countdown_loop_code(&[], 10_000); + let code = call_code(CALLEE, 0, 5_000).append(STOP).build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let r6 = transact_default(MegaSpecId::REX6, build_db()); + let r7 = transact_default(MegaSpecId::REX7, build_db()); + + assert!(r6.is_success(), "the outer transaction survives the callee's OOG: {:?}", r6.result); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the halt itself is unchanged", + ); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas is unchanged"); + assert!( + r7.compute_gas > r6.compute_gas, + "the burned remainder settles as compute gas under REX7; REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); +} diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs new file mode 100644 index 00000000..f66e1a10 --- /dev/null +++ b/crates/mega-evm/tests/rex7/common.rs @@ -0,0 +1,134 @@ +//! Shared helpers for the REX7 test suite. + +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::MemoryDatabase, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaHaltReason, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, +}; +use revm::{ + context::{result::ExecutionResult, tx::TxEnvBuilder}, + handler::EvmTr, + state::EvmState, +}; + +/// Transaction sender. +pub(crate) const CALLER: Address = address!("0000000000000000000000000000000000300000"); +/// Contract invoked by the transaction; its code exercises the opcodes under test. +pub(crate) const CONTRACT: Address = address!("0000000000000000000000000000000000300001"); +/// A second contract, used as the target of internal CALL-family frames. +pub(crate) const CALLEE: Address = address!("0000000000000000000000000000000000300002"); +/// A spare empty address used as a value-transfer / SELFDESTRUCT target. +pub(crate) const EMPTY_TARGET: Address = address!("0000000000000000000000000000000000300003"); + +/// One ether, in wei. +pub(crate) const ONE_ETH: u128 = 1_000_000_000_000_000_000; + +/// The post-transaction readings compared across specs. +pub(crate) struct Outcome { + pub(crate) result: ExecutionResult, + /// Post-tx compute-gas tracker reading (`get_usage().compute_gas`). + pub(crate) compute_gas: u64, + /// Post-tx data-size tracker reading (`get_usage().data_size`). + pub(crate) data_size: u64, + /// Post-tx KV-update tracker reading (`get_usage().kv_updates`). + pub(crate) kv_updates: u64, + /// Post-tx state-growth tracker reading (`get_usage().state_growth`). + pub(crate) state_growth: u64, + /// Receipt `gas_used` (combined compute + storage EVM gas). + pub(crate) gas_used: u64, + /// The state the transaction produced. + pub(crate) state: EvmState, +} + +impl Outcome { + pub(crate) fn is_success(&self) -> bool { + self.result.is_success() + } + + /// Reads a storage slot out of the produced state, defaulting to zero when the transaction + /// never touched it. + pub(crate) fn storage_value(&self, address: Address, slot: U256) -> U256 { + self.state + .get(&address) + .and_then(|account| account.storage.get(&slot)) + .map(|value| value.present_value()) + .unwrap_or_default() + } +} + +/// Runs a single transaction that calls [`CONTRACT`] under `spec` with the given DB and runtime +/// limits, returning the execution result plus the post-tx tracker readings and `gas_used`. +pub(crate) fn transact( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, +) -> Outcome { + let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(100_000_000).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); + let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + let gas_used = result.result.tx_gas_used(); + Outcome { + result: result.result, + compute_gas: usage.compute_gas, + data_size: usage.data_size, + kv_updates: usage.kv_updates, + state_growth: usage.state_growth, + gas_used, + state: result.state, + } +} + +/// Runs [`transact`] with the spec's default runtime limits. +pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome { + transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) +} + +/// [`transact`] with every SALT bucket reporting `bucket_capacity`. +/// +/// The SALT-scaled storage-gas charges (`SSTORE` set, new account, contract creation) are +/// `base × (capacity / MIN_BUCKET_SIZE − 1)`, so only a capacity above +/// [`mega_evm::MIN_BUCKET_SIZE`] makes them non-zero and exercises the paths that have to +/// exclude them from the compute-gas window. +pub(crate) fn transact_with_bucket_capacity( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + bucket_capacity: u64, +) -> Outcome { + let envs = TestExternalEnvs::default().with_default_bucket_capacity(bucket_capacity); + let mut context = MegaContext::new(&mut db, spec) + .with_external_envs(envs.into()) + .with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(100_000_000).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); + let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + let gas_used = result.result.tx_gas_used(); + Outcome { + result: result.result, + compute_gas: usage.compute_gas, + data_size: usage.data_size, + kv_updates: usage.kv_updates, + state_growth: usage.state_growth, + gas_used, + state: result.state, + } +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index da0c0c50..fa0bd395 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -1,3 +1,8 @@ //! Tests for the `REX7` spec. +//! +//! - `checkpoint_settlement` — checkpoint compute-gas settlement: per-transaction totals stay +//! bit-identical to per-opcode recording, and the two places where the models diverge. +mod checkpoint_settlement; +mod common; mod modexp_gas; From 9d7d2a1aa90d5ea2f670612798c63169409ddbb6 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:25:32 +0800 Subject: [PATCH 004/208] feat(rex7): enforce compute limits with the V0 gas clamp At every checkpoint and frame entry/resume the interpreter's visible gas is clamped to the compute headroom -- the tighter of the frame-local budget and the TX-level detained limit -- and the hidden remainder is recorded together with the constraint that bound it. revm's own per-opcode gas check then stops a crossing opcode at the clamp boundary before it executes, so a plain-opcode segment is bounded with no per-opcode accounting at all. Checkpoint handlers gain a prologue (settle the open segment, restore the clamp so CALL forwarding, GAS and storage charges observe the true counter) and an epilogue (re-clamp against the possibly detained headroom). GAS joins the checkpoint set so the clamp stays unobservable. The frame's final result restores the hidden gas and reclassifies a clamp-induced out-of-gas as the compute exceed it stands for: frame-local binding reverts to the parent, TX-level binding halts with the gas rescued, and detention keeps its VolatileDataAccessOutOfGas attribution. Transactions that stay inside every limit remain bit-identical to per-opcode accounting; a crossing now halts one opcode earlier, with that opcode's cost excluded from the recorded usage. Specs <= REX6 are unchanged. --- crates/mega-evm/src/evm/execution.rs | 7 +- crates/mega-evm/src/evm/instructions.rs | 298 ++++++++++++++++------- crates/mega-evm/src/limit/compute_gas.rs | 25 ++ crates/mega-evm/src/limit/limit.rs | 187 +++++++++++--- 4 files changed, 389 insertions(+), 128 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index a275b949..ee3718c2 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -419,7 +419,7 @@ impl MegaEvm { #[inline] fn before_frame_run( ctx: &MegaContext, - frame: &EthFrame, + frame: &mut EthFrame, ) -> Result, ContextDbError>> { // Check if the additional limit is already exceeded, if so, we should immediately stop // and synthesize an interpreter action. @@ -456,6 +456,11 @@ impl MegaEvm { let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); if let InterpreterAction::Return(interpreter_result) = action { + // REX7 V0 clamp: hand any clamp-hidden gas back to the result — and latch a + // clamp-induced out-of-gas as the compute exceed it stands for — before the + // code-deposit charge below observes the result's gas. + ctx.additional_limit.borrow_mut().restore_clamp_into_result(interpreter_result); + // Charge storage gas cost for the number of bytes if frame.data.is_create() && interpreter_result.is_ok() { let code_deposit_storage_gas = constants::mini_rex::CODEDEPOSIT_STORAGE_GAS * diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 16b2acf7..0c5fa5ee 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -650,8 +650,12 @@ mod rex7 { table[SELFBALANCE as usize] = Instruction::new(volatile_data_ext::selfbalance_checkpoint); table[SLOAD as usize] = Instruction::new(volatile_data_ext::sload_checkpoint); + // V0 gas-clamp enforcement: `GAS` has to be a checkpoint so the clamp is restored before + // the counter is observed. + table[GAS as usize] = Instruction::new(compute_gas_ext::gas_checkpoint); + // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains unchanged. Under - // Rex7 they settle from the checkpoint baseline internally. + // Rex7 they open with a checkpoint prologue and close with an epilogue. table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); @@ -818,6 +822,88 @@ macro_rules! run_inner_instruction_or_abort { }; } +/// REX7 checkpoint prologue. Runs at the top of every checkpoint handler, before any gas capture or +/// gas-consuming work: +/// +/// 1. Settles the open plain-opcode segment — `baseline − remaining`, both readings on the clamped +/// counter, telescoping over exactly the unwrapped opcodes since the last checkpoint. +/// 2. Restores the clamp-hidden gas, so the checkpoint's body runs on the **true** counter: the +/// CALL-family forwarding math, the `GAS` opcode's pushed value and the storage-gas charges all +/// observe real gas, which is what keeps the clamp unobservable to a transaction that never +/// exceeds a limit. +/// 3. Re-opens the settlement window at the restored counter. +/// +/// Halts — returning from the enclosing handler — when the settlement surfaces a limit exceed, +/// including one latched earlier by a non-compute mutation site. The restore has already happened +/// on that path, so the frame result carries true gas. No-op before REX7. +macro_rules! checkpoint_prologue { + ($context:expr) => { + if $context.host.spec_id().is_enabled(MegaSpecId::REX7) { + let exceeding_result = { + let mut additional_limit = $context.host.additional_limit().borrow_mut(); + let remaining = $context.interpreter.gas.remaining(); + let segment = additional_limit.checkpoint_baseline().saturating_sub(remaining); + let hidden = additional_limit.checkpoint_restore_hidden(); + $context.interpreter.gas.erase_cost(hidden); + additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); + if additional_limit.record_compute_gas(segment) { + None + } else { + Some(additional_limit.exceeding_instruction_result()) + } + }; + if let Some(result) = exceeding_result { + set_halt_action!($context.interpreter, result); + return Err(result); + } + } + }; +} + +/// REX7 checkpoint epilogue: re-applies the V0 gas clamp from the freshly settled usage — including +/// any detention cap the checkpoint just installed — and re-opens the settlement window on the +/// clamped counter. +/// +/// Only applies when the frame keeps executing. A checkpoint that published an action has either +/// suspended into a child frame (the resume clamps in `AdditionalLimit::before_frame_run`) or ended +/// the frame (the frame's final result restores instead), and clamping either would strand hidden +/// gas across the boundary. No-op before REX7. +macro_rules! checkpoint_epilogue { + ($context:expr) => { + if $context.host.spec_id().is_enabled(MegaSpecId::REX7) && + $context.interpreter.bytecode.action().is_none() + { + let mut additional_limit = $context.host.additional_limit().borrow_mut(); + let hide = + additional_limit.checkpoint_clamp_amount($context.interpreter.gas.remaining()); + if hide > 0 { + let clamped = $context.interpreter.gas.record_regular_cost(hide); + debug_assert!(clamped, "clamp amount exceeds remaining gas"); + } + additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); + } + }; +} + +/// Records a checkpoint opcode's own body gas (`$gas_before − remaining`) and re-opens the +/// settlement window, enforcing the compute-gas limit exactly as the per-opcode wrappers do. +/// +/// Used by the REX7 checkpoint handlers whose bodies can never spawn a child frame (the volatile +/// opcodes, `SLOAD`, `SELFBALANCE`, `GAS`). The CALL / CREATE and storage-gas bodies use +/// [`record_storage_compute_gas!`] instead, which additionally excludes storage charges and +/// forwarded child gas. +macro_rules! record_checkpoint_body_compute_gas { + ($context:expr, $gas_before:expr) => { + let gas_after = $context.interpreter.gas.remaining(); + let gas_used = $gas_before.saturating_sub(gas_after); + { + let mut additional_limit = $context.host.additional_limit().borrow_mut(); + additional_limit.sync_checkpoint_baseline(gas_after); + compute_gas!($context.interpreter, additional_limit, gas_used); + } + }; +} + /// Records an opcode's compute gas in a single measurement window and enforces the compute-gas /// limit. The REX6 storage-affecting handlers invoke it directly with the storage gas they /// charged; plain opcodes use the leaner inline recording in @@ -840,9 +926,8 @@ macro_rules! run_inner_instruction_or_abort { /// CREATE2 differs only by folding its memory-expansion gas into this single window instead of /// recording it separately. /// -/// Under REX7 checkpoint accounting the window instead opens at the previous checkpoint, so the -/// same recording also settles the unwrapped plain opcodes that ran since; see the macro body for -/// why the exclusions stay exact and why the static gas is no longer added back. +/// Under REX7 checkpoint accounting the window is the same one — [`checkpoint_prologue!`] runs +/// ahead of the `$gas_before` capture — but the static gas is not added back: see the macro body. /// /// On exceeding the compute-gas limit, halts the interpreter and returns from the enclosing /// instruction handler. The early return mirrors [`compute_gas!`] so a trailing statement after @@ -855,16 +940,18 @@ macro_rules! record_storage_compute_gas { let is_rex6 = spec.is_enabled(MegaSpecId::REX6); let is_checkpoint_accounting = spec.is_enabled(MegaSpecId::REX7); let gas_after = $context.interpreter.gas.remaining(); - // Under checkpoint accounting the window opens at the last checkpoint — frame entry / - // resume, or the previous checkpoint opcode — instead of at this handler's own - // `$gas_before` capture, so the plain opcodes that ran since settle here in the same - // recording. Only plain opcodes can run inside that extra span, so no storage gas and no - // forwarded child gas hides in it and the exclusions below stay exact. The window then - // also contains the interpreter's static-gas pre-charge for this opcode, which the - // per-opcode form has to add back because its capture sits after it. + // The per-opcode `$gas_before` window applies on every spec: under checkpoint accounting + // the plain segment ahead of this opcode was already settled by + // [`checkpoint_prologue!`], which also restored the gas clamp, so `$gas_before` + // (captured after the prologue) lives on the true counter and measures the same + // span it measures everywhere else. + // + // What the two differ on is the opcode's static gas. Whoever charges it — the interpreter + // before dispatch, or an outer volatile wrapper — does so ahead of the prologue, so under + // checkpoint accounting it is already inside the settled segment and adding it back here + // would bill it twice. let mut gas_used = if is_checkpoint_accounting { - let baseline = $context.host.additional_limit().borrow().checkpoint_baseline(); - baseline.saturating_sub(gas_after).saturating_sub($storage_charged) + $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged) } else { (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) .saturating_sub($storage_charged) @@ -1216,8 +1303,19 @@ pub mod forward_gas_ext { /// - `$wrapped_fn`: Path to the wrapped instruction implementation /// - `$has_transfer_logic`: Expression to determine if value is being transferred (e.g., /// `has_transfer` or `false`) + /// + /// The `@checkpoint_tail` variant additionally re-applies the REX7 gas clamp on the way out. It + /// is used by `CREATE` / `CREATE2`, whose table entries dispatch straight here; the CALL family + /// is wrapped once more by `volatile_data_ext::wrap_call_volatile_check`, which owns the + /// epilogue so that it lands after the detention cap that wrapper installs. macro_rules! wrap_gas_cap { ($fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr) => { + wrap_gas_cap!(@inner $fn_name, $opcode_name, $wrapped_fn, $has_transfer_logic, false); + }; + (@checkpoint_tail $fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr) => { + wrap_gas_cap!(@inner $fn_name, $opcode_name, $wrapped_fn, $has_transfer_logic, true); + }; + (@inner $fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr, $checkpoint_tail:literal) => { #[doc = concat!("`", $opcode_name, "` opcode with 98/100 gas forwarding rule.")] #[inline] pub fn $fn_name< @@ -1301,6 +1399,9 @@ pub mod forward_gas_ext { } _ => {} } + if $checkpoint_tail { + checkpoint_epilogue!(context); + } inner_outcome } }; @@ -1333,8 +1434,12 @@ pub mod forward_gas_ext { wrap_gas_cap!(call_code, "CALLCODE", storage_gas_ext::call_code, check_call_has_transfer); wrap_gas_cap!(delegate_call, "DELEGATECALL", storage_gas_ext::delegate_call, no_transfer); wrap_gas_cap!(static_call, "STATICCALL", storage_gas_ext::static_call, no_transfer); - wrap_gas_cap!(create, "CREATE", storage_gas_ext::create::, no_transfer); - wrap_gas_cap!(create2, "CREATE2", storage_gas_ext::create::, no_transfer); + wrap_gas_cap!( + @checkpoint_tail create, "CREATE", storage_gas_ext::create::, no_transfer + ); + wrap_gas_cap!( + @checkpoint_tail create2, "CREATE2", storage_gas_ext::create::, no_transfer + ); } /** Volatile data access opcode handlers with compute gas limit enforcement. @@ -1897,6 +2002,11 @@ pub mod volatile_data_ext { // not interpreter state, so it is safe in any interpreter state (including // `NewFrame` after a successful CALL). apply_compute_gas_limit!(context); + // REX7: re-clamp for a CALL that never published a child frame (an insufficient balance + // or depth rejection pushes 0 and lets the frame keep running). The epilogue is what + // keeps the following plain segment bounded, and it sits after the cap above so a CALL + // that just marked beneficiary access clamps against the detained headroom. + checkpoint_epilogue!(context); inner_outcome } }; @@ -1911,55 +2021,26 @@ pub mod volatile_data_ext { /* Checkpoint variants of the volatile handlers (REX7+). - Under checkpoint accounting the volatile opcodes stay wrapped — they are checkpoints — but - they run revm's raw instruction and settle the whole open segment, measured on the - interpreter's own gas counter, in one recording, instead of delegating to a per-opcode - `compute_gas_ext` wrapper. Wherever the opcode's static gas is charged it lands inside that - segment, so the settlement adds nothing back; each handler keeps charging it at the position - its per-opcode counterpart does, because that position decides what an underfunded frame has - already done when it halts. + Under checkpoint accounting the volatile opcodes stay wrapped — they are checkpoints. The + prologue settles the open plain segment and restores the gas clamp, revm's raw instruction runs + on the true counter, the body's own gas is recorded per opcode, the detention cap is applied + from the fully settled usage exactly as the per-opcode order applies it, and the epilogue + re-clamps against the possibly-lowered headroom. - The settlement runs before `apply_compute_gas_limit!`, so a REX4+ relative detention cap is - still derived from fully settled usage at the access point. + Each handler keeps charging the opcode's static gas at the position its per-opcode counterpart + charges it, because that position decides what an underfunded frame has already done when it + halts. The frozen detention-window tripwire the per-opcode conditional wrapper carries is not repeated here: it watches for historical transactions whose replay would diverge across a revm bump, and no such transaction can exist for a spec with no activation history. */ - /// Settles the open checkpoint segment at the interpreter's current gas, re-opens the window, - /// and halts — returning from the enclosing handler — when a limit surfaces. - /// - /// A frame-local exceed reports as a revert, which the enclosing handler's tail would have - /// treated as a normal (non-halting) outcome and still followed with the detention cap, so the - /// cap is applied here before returning. A TX-level exceed reports as an out-of-gas halt, which - /// that tail short-circuits, so the cap is not applied on that path. - macro_rules! settle_checkpoint_compute_gas { - ($context:expr) => { - let exceeding_result = { - let gas_after = $context.interpreter.gas.remaining(); - let mut additional_limit = $context.host.additional_limit().borrow_mut(); - if additional_limit.settle_checkpoint(gas_after) { - None - } else { - Some(additional_limit.exceeding_instruction_result()) - } - }; - if let Some(result) = exceeding_result { - set_halt_action!($context.interpreter, result); - if !result.is_halt() { - apply_compute_gas_limit!($context); - } - return Err(result); - } - }; - } - - /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: disabled guard, static gas ahead - /// of the raw instruction (the position these opcodes' revm bodies charge from), segment - /// settlement, detention cap. + /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: disabled guard, prologue, static + /// gas ahead of the raw instruction (the position these opcodes' revm bodies charge from), + /// body recording, detention cap, epilogue. macro_rules! wrap_checkpoint_detain_gas_unconditional { ($fn_name:ident, $opcode:ident, $original_fn:path, $access_type:expr) => { - #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: raw instruction, segment settlement, gas detention.")] + #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: segment settlement, raw instruction, gas detention, re-clamp.")] #[inline] pub fn $fn_name( context: InstructionContext<'_, H, WIRE>, @@ -1967,23 +2048,26 @@ pub mod volatile_data_ext { if context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, $opcode, $access_type); } + checkpoint_prologue!(context); + let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); - settle_checkpoint_compute_gas!(context); + record_checkpoint_body_compute_gas!(context, gas_before); apply_compute_gas_limit!(context); + checkpoint_epilogue!(context); inner_outcome } }; } - /// Checkpoint form of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw instruction, - /// static gas after it (the position these opcodes' revm bodies charge from, so an underfunded - /// frame has already popped its operands and marked its access), segment settlement, detention - /// cap. + /// Checkpoint form of [`wrap_op_detain_gas_conditional`]: beneficiary peek, prologue, raw + /// instruction, static gas after it (the position these opcodes' revm bodies charge from, so an + /// underfunded frame has already popped its operands and marked its access), body recording, + /// detention cap, epilogue. macro_rules! wrap_checkpoint_detain_gas_conditional { ($fn_name:ident, $opcode:ident, $original_fn:path) => { - #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: raw instruction, segment settlement, gas detention.")] + #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: segment settlement, raw instruction, gas detention, re-clamp.")] #[inline] pub fn $fn_name, H: HostExt + ?Sized>( context: InstructionContext<'_, H, WIRE>, @@ -1999,11 +2083,14 @@ pub mod volatile_data_ext { ); } } + checkpoint_prologue!(context); + let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); charge_static_gas!(context, $opcode); - settle_checkpoint_compute_gas!(context); + record_checkpoint_body_compute_gas!(context, gas_before); apply_compute_gas_limit!(context); + checkpoint_epilogue!(context); inner_outcome } }; @@ -2086,7 +2173,7 @@ pub mod volatile_data_ext { ); /// `SLOAD` as a checkpoint. Same oracle-volatile handling as [`sload`], but the raw revm - /// instruction runs unwrapped and the open segment settles here. + /// instruction runs unwrapped and the open segment settles in the prologue. #[inline] pub fn sload_checkpoint( context: InstructionContext<'_, H, WIRE>, @@ -2095,16 +2182,19 @@ pub mod volatile_data_ext { if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, SLOAD, VolatileDataAccessType::Oracle); } + checkpoint_prologue!(context); + let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!(instructions::host::sload, context, inner_outcome); charge_static_gas!(context, SLOAD); - settle_checkpoint_compute_gas!(context); + record_checkpoint_body_compute_gas!(context, gas_before); apply_compute_gas_limit!(context); + checkpoint_epilogue!(context); inner_outcome } /// `SELFBALANCE` as a checkpoint. Same beneficiary-volatile handling as [`selfbalance`], but - /// the raw revm instruction runs unwrapped and the open segment settles here. + /// the raw revm instruction runs unwrapped and the open segment settles in the prologue. #[inline] pub fn selfbalance_checkpoint( context: InstructionContext<'_, H, WIRE>, @@ -2118,11 +2208,14 @@ pub mod volatile_data_ext { VolatileDataAccessType::Beneficiary ); } + checkpoint_prologue!(context); + let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); - settle_checkpoint_compute_gas!(context); + record_checkpoint_body_compute_gas!(context, gas_before); apply_compute_gas_limit!(context); + checkpoint_epilogue!(context); inner_outcome } } @@ -2183,6 +2276,9 @@ pub mod additional_limit_ext { set_halt_action!(context.interpreter, result); return Err(result); } + drop(additional_limit); + // REX7: re-clamp once every dimension this opcode touches has been recorded. + checkpoint_epilogue!(context); inner_outcome } @@ -2220,6 +2316,9 @@ pub mod additional_limit_ext { set_halt_action!(context.interpreter, result); return Err(result); } + drop(additional_limit); + // REX7: re-clamp once every dimension this opcode touches has been recorded. + checkpoint_epilogue!(context); inner_outcome } } @@ -2310,6 +2409,9 @@ pub mod storage_gas_ext { >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { + // REX7: settle the open segment and restore the clamp before any gas observation, + // so the storage charge and the body's 63/64 forwarding math see the true counter. + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers all of the // opcode's compute work. let gas_before = context.interpreter.gas.remaining(); @@ -2664,6 +2766,10 @@ pub mod storage_gas_ext { return Err(InstructionResult::StateChangeDuringStaticCall); } + // REX7: settle the open segment and restore the clamp before any gas observation, so the + // memory expansion, the storage charge and the body's forwarding math see the true counter. + checkpoint_prologue!(context); + // Captured before any gas movement so the single compute window covers the wrapper-side // CREATE2 memory expansion as well as the inner opcode. let gas_before = context.interpreter.gas.remaining(); @@ -2745,6 +2851,8 @@ pub mod storage_gas_ext { >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { + // REX7: settle the open segment and restore the clamp before any gas observation. + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); let Some(len) = context.interpreter.stack.inspect::<1>() else { @@ -2805,6 +2913,8 @@ pub mod storage_gas_ext { >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { + // REX7: settle the open segment and restore the clamp before any gas observation. + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); // The address to the underlying execution contract state @@ -2881,6 +2991,11 @@ pub mod storage_gas_ext { >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { + // REX7: settle the open segment and restore the clamp before any gas observation — the + // beneficiary-creation storage charge below and the inner opcode both run on the true + // counter, which is what keeps the storage charge outside every compute window. + checkpoint_prologue!(context); + // Inside a static frame, revm's inner SELFDESTRUCT halts on the // static-context check without changing state. Skip the mega host work below // (two account inspections, SALT account-creation pricing, the storage-gas @@ -2929,19 +3044,7 @@ pub mod storage_gas_ext { }; let drained = context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); - let storage_charged = cost - drained; - gas!(context.interpreter, storage_charged); - - // Under checkpoint accounting this storage debit sits inside the window that the - // trailing compute recording in `compute_gas_ext::selfdestruct_self_charged` closes, - // and that recording has no storage term of its own — exclude the debit by lowering - // the open baseline here. - { - let mut additional_limit = context.host.additional_limit().borrow_mut(); - if additional_limit.checkpoint_accounting() { - additional_limit.deduct_checkpoint_baseline(storage_charged); - } - } + gas!(context.interpreter, cost - drained); // Record resource usage for new beneficiary account context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); @@ -3294,17 +3397,14 @@ pub mod compute_gas_ext { if SELF_CHARGES_STATIC_GAS { 0 } else { const { static_gas(opcode::SELFDESTRUCT) } }; let gas_after = context.interpreter.gas.remaining(); let mut additional_limit = context.host.additional_limit().borrow_mut(); - // Under checkpoint accounting the window opens at the previous checkpoint, so this - // recording also settles the unwrapped plain opcodes that ran since. Wherever the opcode's - // static gas was charged it lands inside that window, so nothing is added back; the - // beneficiary-creation storage charge already lowered the baseline by its own amount. - let gas_used = if additional_limit.checkpoint_accounting() { - let used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); + // The per-opcode `gas_before` window applies on every spec. Under checkpoint accounting the + // plain segment ahead of this opcode was already settled by the `checkpoint_prologue!` in + // `storage_gas_ext::selfdestruct`, which also restored the clamp; the window is re-opened + // here so the frame's final settlement cannot bill this body a second time. + let gas_used = pre_charged + gas_before.saturating_sub(gas_after); + if additional_limit.checkpoint_accounting() { additional_limit.sync_checkpoint_baseline(gas_after); - used - } else { - pre_charged + gas_before.saturating_sub(gas_after) - }; + } if !additional_limit.record_compute_gas_all_dims(gas_used) { // A successful inner SELFDESTRUCT has already set its return action, which the halt // replaces; the `Err` is what stops the interpreter loop. @@ -3314,6 +3414,24 @@ pub mod compute_gas_ext { } inner_outcome } + + /// `GAS` as a REX7 checkpoint. + /// + /// `GAS` has to be a checkpoint under V0 clamp enforcement even though it charges nothing but + /// its static gas: the prologue hands the clamp-hidden gas back before the raw instruction + /// reads the counter, so the value pushed on the stack is the true remaining and the clamp + /// stays invisible to any transaction that never exceeds a limit. + #[inline] + pub fn gas_checkpoint( + context: InstructionContext<'_, H, WIRE>, + ) -> InstructionExecResult { + checkpoint_prologue!(context); + let gas_before = context.interpreter.gas.remaining(); + run_inner_instruction_or_abort!(instructions::system::gas, context, inner_outcome); + record_checkpoint_body_compute_gas!(context, gas_before); + checkpoint_epilogue!(context); + inner_outcome + } } /// Trait to inspect the stack elements. diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index d37e27ca..90f42953 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -110,6 +110,31 @@ impl ComputeGasTracker { self.detained_limit } + /// Returns the base (undetained) TX compute gas limit. + pub(crate) fn base_tx_limit(&self) -> u64 { + self.frame_tracker.tx_limit() + } + + /// Returns the compute gas headroom the V0 gas clamp may leave visible to the interpreter, + /// and whether the binding constraint is the frame-local budget (`true`) or the TX-level + /// (possibly detained) limit (`false`). + /// + /// The headroom is the tighter of the current frame's remaining compute budget (Rex4+) and + /// the TX-level remaining under the effective (possibly detained) limit — the same pair + /// [`check_limit`](TxRuntimeLimit::check_limit) enforces. Gas hidden beyond this headroom is + /// therefore reachable only by a transaction that would exceed one of those two limits. + #[inline] + pub(crate) fn clamp_headroom(&self) -> (u64, bool) { + let tx_remaining = self.tx_limit().saturating_sub(self.tx_usage()); + if self.rex4_enabled { + let frame_remaining = self.frame_tracker.current_frame_remaining(); + if frame_remaining < tx_remaining { + return (frame_remaining, true); + } + } + (tx_remaining, false) + } + /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained /// limit is tighter than the base TX limit AND actual usage exceeds it. pub(crate) fn is_detained_exceed(&self) -> bool { diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 7be501e5..d85e39e6 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -120,9 +120,35 @@ pub struct AdditionalLimit { /// frame is running and only when [`checkpoint_accounting`](Self::checkpoint_accounting) is /// active. Re-synced at every [`before_frame_run`](Self::before_frame_run) (which covers both /// frame entry and every resume after a child frame's outcome is merged back) and at every - /// checkpoint settlement, and lowered by storage-gas charge sites that debit interpreter gas - /// inside an open window. + /// checkpoint prologue and body recording. checkpoint_baseline: u64, + + /// V0 gas-clamp enforcement (REX7+): the part of the executing frame's interpreter gas hidden + /// from the interpreter, so that revm's own per-opcode gas checks enforce the compute headroom + /// inside plain-opcode segments at no per-opcode cost. + /// + /// Non-zero only while the current frame is inside a plain segment: every checkpoint restores + /// it before running its body — so CALL forwarding, `GAS` and storage charges observe the true + /// counter — and re-applies it on the way out, and the frame's final result restores it via + /// [`restore_clamp_into_result`](Self::restore_clamp_into_result). + clamp_hidden: u64, + + /// Whether the headroom that bound the last clamp was the frame-local compute budget (`true`) + /// or the TX-level (possibly detained) limit (`false`). + /// + /// This decides how a clamp-induced out-of-gas is reclassified: a frame-local exceed reverts + /// to the parent, a TX-level exceed halts the transaction. + clamp_frame_local: bool, + + /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level + /// constraint. + /// + /// [`ComputeGasTracker::is_detained_exceed`] requires `used > detained_limit`, which a + /// clamp-stopped transaction never reaches — the crossing opcode is stopped before it + /// executes, so usage stays at or below the limit. The halt-reason attribution consults + /// this flag instead, keeping the reported reason `VolatileDataAccessOutOfGas` exactly as + /// per-opcode enforcement reports it. + clamp_latched_detained: bool, } /// The usage of the additional limits. @@ -152,6 +178,9 @@ impl AdditionalLimit { storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), checkpoint_baseline: 0, + clamp_hidden: 0, + clamp_frame_local: false, + clamp_latched_detained: false, } } } @@ -194,6 +223,9 @@ impl AdditionalLimit { self.kv_update.reset(); self.storage_call_stipend.reset(); self.checkpoint_baseline = 0; + self.clamp_hidden = 0; + self.clamp_frame_local = false; + self.clamp_latched_detained = false; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -221,25 +253,90 @@ impl AdditionalLimit { self.checkpoint_baseline = remaining; } - /// Lowers the open window's baseline by `amount`, excluding a storage-gas debit from the - /// segment that the next settlement will measure. + /// Takes the outstanding clamp-hidden gas so the caller can hand it back to the interpreter. + /// + /// Every checkpoint prologue calls this before running its body, and the frame's final result + /// calls it before the result propagates, so the clamp is never observable outside a plain + /// segment. + #[inline] + pub(crate) fn checkpoint_restore_hidden(&mut self) -> u64 { + core::mem::take(&mut self.clamp_hidden) + } + + /// Computes how much interpreter gas to hide so the visible remaining equals the compute + /// headroom, records it as outstanding, and returns it for the caller to debit from the + /// interpreter's counter. /// - /// Charge sites that debit storage gas to interpreter gas while a window is open, and whose - /// settlement site does not receive the charged amount directly, use this instead: the - /// settlement then takes `baseline − remaining` with no storage term of its own. + /// Returns 0 when clamping does not apply: the transaction is exempt from per-tx metering, or + /// a limit has already been latched (the enclosing site halts on it instead). #[inline] - pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { - self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); + pub(crate) fn checkpoint_clamp_amount(&mut self, remaining: u64) -> u64 { + debug_assert_eq!(self.clamp_hidden, 0, "clamp applied while a clamp is outstanding"); + if !self.has_exceeded_limit.within_limit() { + return 0; + } + let (headroom, frame_local) = self.compute_gas.clamp_headroom(); + let hide = remaining.saturating_sub(headroom); + self.clamp_hidden = hide; + self.clamp_frame_local = frame_local; + hide } - /// Settles the open segment against `gas_remaining`, re-opens the window there, and returns - /// `false` when a limit — including a non-compute exceed latched since the previous - /// checkpoint — surfaces. + /// Latches a clamp-induced out-of-gas as a compute gas limit exceed. + /// + /// The crossing opcode never executed — revm's own gas check stopped it at the clamp boundary — + /// so its cost is not in the recorded usage and an ordinary [`check_limit`](Self::check_limit) + /// pass sees usage at or below the limit. The latch is therefore stamped directly, with + /// `frame_local` taken from the constraint that bound the clamp, so the existing frame-result + /// machinery (frame-local absorb to revert; TX-level mark plus gas rescue) produces the halt + /// shape it produces for every other compute exceed. #[inline] - pub(crate) fn settle_checkpoint(&mut self, gas_remaining: u64) -> bool { - let gas_used = self.checkpoint_baseline.saturating_sub(gas_remaining); - self.checkpoint_baseline = gas_remaining; - self.record_compute_gas(gas_used) + fn latch_clamp_exceed(&mut self) { + if !self.has_exceeded_limit.within_limit() { + return; + } + self.has_exceeded_limit = LimitCheck::ExceedsLimit { + kind: super::LimitKind::ComputeGas, + frame_local: self.clamp_frame_local, + limit: self.compute_gas.tx_limit(), + used: self.compute_gas.tx_usage(), + }; + // Preserve the volatile-detention attribution: when the binding TX-level constraint at + // clamp time was the detained limit, the halt must classify as `VolatileDataAccessOutOfGas` + // exactly as per-opcode enforcement classifies it. + self.clamp_latched_detained = !self.clamp_frame_local && + self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); + } + + /// Restores any outstanding V0 clamp into the frame's final interpreter result, and latches a + /// clamp-induced out-of-gas as a compute exceed. + /// + /// Must run before anything reads or charges the result's gas — in particular before the + /// execution-layer code-deposit storage charge, which would otherwise observe the clamped copy + /// and mis-fire an out-of-gas on a CREATE frame that is nowhere near its limits. + /// + /// A clamp can only be outstanding when the frame ended inside a plain-opcode segment, because + /// every checkpoint prologue restores it before its body. An out-of-gas exit from such a + /// segment is a clamp artifact: the true counter held `hidden` more gas than the + /// interpreter could see, and the crossing opcode was stopped at the clamp boundary *before + /// executing* — exactly the V0 enforcement point. When the crossing opcode would have + /// exceeded the true remaining as well, the compute classification still wins: the two are + /// indistinguishable here, and attributing the halt to the resource limit keeps the + /// sender's remaining gas refundable. + pub(crate) fn restore_clamp_into_result(&mut self, result: &mut InterpreterResult) { + if !self.checkpoint_accounting { + return; + } + let hidden = self.checkpoint_restore_hidden(); + if hidden == 0 { + return; + } + result.gas.erase_cost(hidden); + // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every other + // result either is unrelated to gas or cannot arise from a plain opcode. + if matches!(result.result, InstructionResult::OutOfGas | InstructionResult::MemoryOOG) { + self.latch_clamp_exceed(); + } } /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every @@ -383,10 +480,15 @@ impl AdditionalLimit { &self, access_type: VolatileDataAccess, ) -> Option { - self.compute_gas.is_detained_exceed().then(|| MegaHaltReason::VolatileDataAccessOutOfGas { - access_type, - limit: self.compute_gas.detained_limit(), - actual: self.compute_gas.tx_usage(), + // `is_detained_exceed` covers per-opcode enforcement, where usage crossed the detained + // limit. `clamp_latched_detained` covers V0 clamp enforcement, where the crossing opcode + // was stopped before executing and usage therefore stays at or below the limit. + (self.compute_gas.is_detained_exceed() || self.clamp_latched_detained).then(|| { + MegaHaltReason::VolatileDataAccessOutOfGas { + access_type, + limit: self.compute_gas.detained_limit(), + actual: self.compute_gas.tx_usage(), + } }) } @@ -760,17 +862,8 @@ impl AdditionalLimit { /// indicating that the limit is exceeded. pub(crate) fn before_frame_run( &mut self, - frame: &EthFrame, + frame: &mut EthFrame, ) -> Option { - // Checkpoint accounting: open the settlement window at the frame's current gas. This hook - // runs both at frame entry and at every resume after a child frame's outcome — including - // the gas it returned — has been merged back into this frame's interpreter, so the window - // always starts at an instruction boundary with the interpreter's counter in its real, - // post-merge state. - if self.checkpoint_accounting { - self.checkpoint_baseline = frame.interpreter.gas.remaining(); - } - self.state_growth.before_frame_run(frame); self.data_size.before_frame_run(frame); self.kv_update.before_frame_run(frame); @@ -784,6 +877,23 @@ impl AdditionalLimit { output, )); } + + // Checkpoint accounting: apply the V0 gas clamp and open the settlement window at the + // frame's clamped gas. This hook runs both at frame entry and at every resume after a child + // frame's outcome — including the gas it returned — has been merged back into this frame's + // interpreter, so the window always starts at an instruction boundary with the + // interpreter's counter in its real, post-merge state. No clamp can be outstanding + // here: every suspension point (the CALL / CREATE checkpoint prologue) and every + // frame end restores it first. + if self.checkpoint_accounting { + debug_assert_eq!(self.clamp_hidden, 0, "frame resumed with a clamp outstanding"); + let hide = self.checkpoint_clamp_amount(frame.interpreter.gas.remaining()); + if hide > 0 { + let clamped = frame.interpreter.gas.record_regular_cost(hide); + debug_assert!(clamped, "clamp amount exceeds remaining gas"); + } + self.checkpoint_baseline = frame.interpreter.gas.remaining(); + } None } @@ -822,13 +932,16 @@ impl AdditionalLimit { ) { // Checkpoint accounting: the frame has produced its final action, so settle the tail // segment — everything since the last checkpoint — against the interpreter's gas counter. - // `frame.interpreter.gas` still holds the loop-exit value here (the code-deposit storage - // charge applied by the execution-layer hook mutates only the action's gas copy), so the - // delta telescopes over exactly the unwrapped plain opcodes that ran since. A checkpoint - // that already settled and halted leaves `baseline == remaining` (delta 0), and a CALL - // abort path's forwarded-gas `erase_cost` can only raise `remaining` above the baseline, - // which the saturation turns into 0. Any exceed recorded here is latched, and the frame - // result marking below / in `before_frame_return_result` surfaces it. + // `frame.interpreter.gas` still holds the loop-exit value here (the clamp restore and the + // code-deposit storage charge both mutate only the action's gas copy), and both it and the + // baseline live in the same clamped domain, so the delta telescopes over exactly the + // unwrapped plain opcodes that ran since. A checkpoint that already settled and halted + // leaves `baseline == remaining` (delta 0), and a CALL abort path's forwarded-gas + // `erase_cost` can only raise `remaining` above the baseline, which the saturation turns + // into 0. Any exceed recorded here is latched, and the frame result marking below / in + // `before_frame_return_result` surfaces it. The clamp restore itself already happened, in + // `restore_clamp_into_result`, before the execution-layer hook charged code-deposit storage + // gas against the action's gas. if self.checkpoint_accounting { if let InterpreterAction::Return(_) = action { let remaining = frame.interpreter.gas.remaining(); From dc541a58af03383ab773c03e5bfe26da8dd67524 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:25:40 +0800 Subject: [PATCH 005/208] test(rex7): cover V0 gas-clamp enforcement Pins that the clamp is unobservable through GAS, that a crossing opcode is stopped before it executes with its cost excluded from usage, that a detention cap is enforced inside a checkpoint-free loop, and that a clamp-induced out-of-gas is reclassified by whichever constraint bound the clamp (frame-local revert, TX-level halt with rescue, volatile-detention attribution) including the double-exceed corner where the compute classification wins. The checkpoint-settlement suite's enforcement case is updated from the checkpoint-deferred halt to the V0 halt position. --- .../tests/rex7/checkpoint_settlement.rs | 46 +- crates/mega-evm/tests/rex7/common.rs | 25 +- crates/mega-evm/tests/rex7/main.rs | 3 + crates/mega-evm/tests/rex7/v0_clamp.rs | 444 ++++++++++++++++++ 4 files changed, 498 insertions(+), 20 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/v0_clamp.rs diff --git a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs index 41deb3ca..2dde394f 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs @@ -11,9 +11,9 @@ //! anyway, so summing it by segment reproduces the per-opcode sum exactly. //! //! The two places where the models are *not* identical are pinned at the bottom of this file: -//! a limit crossing inside a plain-opcode segment surfaces at the next checkpoint rather than at -//! the crossing opcode, and a frame that halts out of gas settles its burned remainder as compute -//! gas. +//! a limit crossing inside a plain-opcode segment halts *before* the crossing opcode rather than +//! after it, and a frame that halts out of gas settles its burned remainder as compute gas. The +//! enforcement mechanism behind the first — the V0 gas clamp — has its own suite in `v0_clamp`. use crate::common::{ transact, transact_default, transact_with_bucket_capacity, Outcome, CALLEE, CALLER, CONTRACT, @@ -425,9 +425,9 @@ fn test_conditional_volatile_checkpoints() { }); } -/// `GAS` is not a checkpoint: it runs raw and reads the interpreter's own counter. The settlement -/// must not perturb that counter, so a contract that stores its `GAS` reading must store the same -/// value under both specs. +/// `GAS` reads the interpreter's own counter, so neither the settlement nor the gas clamp may +/// perturb what it observes: a contract that stores its `GAS` reading must store the same value +/// under both specs. #[test] fn test_gas_opcode_reads_the_same_remaining_gas() { let code = plain_filler(BytecodeBuilder::default(), 10) @@ -468,12 +468,14 @@ fn plain_run_then_sstore_code(pairs: usize, include_sstore: bool) -> Bytes { builder.append(STOP).build() } -/// The one enforcement difference this ticket's model has: a compute-gas crossing inside a -/// plain-opcode segment is not caught at the crossing opcode — nothing is metered there — but at -/// the next checkpoint. Both specs halt; REX7 records the whole segment up to that checkpoint, -/// which is exactly what an unconstrained run records at the same point. +/// The one enforcement difference this model has: a compute-gas crossing inside a plain-opcode +/// segment is not caught *at* the crossing opcode — nothing is metered there — but *before* it, by +/// the V0 gas clamp, which leaves the interpreter only as much visible gas as the compute headroom +/// allows. Both specs halt, and both halt in the middle of the plain run without ever reaching the +/// SSTORE checkpoint downstream; REX6 executes the crossing opcode and records it, so its usage +/// ends up over the limit, while REX7 stops one opcode earlier and its usage stays at the limit. #[test] -fn test_compute_limit_crossing_surfaces_at_the_next_checkpoint() { +fn test_compute_limit_crossing_halts_before_the_crossing_opcode() { let code = plain_run_then_sstore_code(200, true); let usage_before_sstore = transact_default(MegaSpecId::REX7, base_db(plain_run_then_sstore_code(200, false))) @@ -487,26 +489,32 @@ fn test_compute_limit_crossing_surfaces_at_the_next_checkpoint() { |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); - let r7 = transact(MegaSpecId::REX7, base_db(code.clone()), limits(MegaSpecId::REX7)); - let r7_unconstrained = transact_default(MegaSpecId::REX7, base_db(code)); + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); assert!(!r6.is_success(), "REX6 must halt on the tight compute limit; got {:?}", r6.result); assert!(!r7.is_success(), "REX7 must halt on the tight compute limit; got {:?}", r7.result); assert!( - r6.compute_gas <= compute_limit + 12, - "REX6 halts at the crossing opcode; compute={} limit={compute_limit}", + r6.compute_gas > compute_limit, + "REX6 records the crossing opcode it just executed; compute={} limit={compute_limit}", r6.compute_gas ); assert_eq!( - r7.compute_gas, r7_unconstrained.compute_gas, - "REX7 settles the whole segment through the SSTORE checkpoint", + r7.compute_gas, compute_limit, + "REX7 stops at the clamp boundary, with the crossing opcode's cost excluded", ); assert!( - r7.compute_gas > r6.compute_gas, - "checkpoint enforcement overshoots per-opcode enforcement; REX6={} REX7={}", + r7.compute_gas < r6.compute_gas, + "clamp enforcement must be at least as tight as per-opcode enforcement; REX6={} REX7={}", r6.compute_gas, r7.compute_gas ); + let slot = U256::from(7u64); + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.storage_value(CONTRACT, slot).is_zero(), + "{label}: the halt lands inside the plain run, so the SSTORE downstream never executes", + ); + } } /// The second difference: a frame that halts out of EVM gas has its remaining budget zeroed by the diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index f66e1a10..9b0d5cb6 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -45,6 +45,14 @@ impl Outcome { self.result.is_success() } + /// The halt reason, or a panic with `label` when the transaction did not halt. + pub(crate) fn halt_reason(&self, label: &str) -> &MegaHaltReason { + match &self.result { + ExecutionResult::Halt { reason, .. } => reason, + other => panic!("{label}: expected a halt, got {other:?}"), + } + } + /// Reads a storage slot out of the produced state, defaulting to zero when the transaction /// never touched it. pub(crate) fn storage_value(&self, address: Address, slot: U256) -> U256 { @@ -56,12 +64,27 @@ impl Outcome { } } +/// The transaction gas limit [`transact`] runs with — high enough that EVM gas is never the +/// binding constraint. +pub(crate) const DEFAULT_TX_GAS_LIMIT: u64 = 100_000_000; + /// Runs a single transaction that calls [`CONTRACT`] under `spec` with the given DB and runtime /// limits, returning the execution result plus the post-tx tracker readings and `gas_used`. pub(crate) fn transact( + spec: MegaSpecId, + db: MemoryDatabase, + limits: EvmTxRuntimeLimits, +) -> Outcome { + transact_with_gas_limit(spec, db, limits, DEFAULT_TX_GAS_LIMIT) +} + +/// [`transact`] with an explicit transaction gas limit, for cases that need EVM gas itself to run +/// out. +pub(crate) fn transact_with_gas_limit( spec: MegaSpecId, mut db: MemoryDatabase, limits: EvmTxRuntimeLimits, + gas_limit: u64, ) -> Outcome { let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); context.modify_chain(|chain| { @@ -69,7 +92,7 @@ pub(crate) fn transact( chain.operator_fee_constant = Some(U256::from(0)); }); let tx = - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(100_000_000).build_fill(); + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index fa0bd395..edd2739c 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -2,7 +2,10 @@ //! //! - `checkpoint_settlement` — checkpoint compute-gas settlement: per-transaction totals stay //! bit-identical to per-opcode recording, and the two places where the models diverge. +//! - `v0_clamp` — V0 gas-clamp enforcement: a crossing opcode is stopped before it executes, and +//! the resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. mod checkpoint_settlement; mod common; mod modexp_gas; +mod v0_clamp; diff --git a/crates/mega-evm/tests/rex7/v0_clamp.rs b/crates/mega-evm/tests/rex7/v0_clamp.rs new file mode 100644 index 00000000..29000935 --- /dev/null +++ b/crates/mega-evm/tests/rex7/v0_clamp.rs @@ -0,0 +1,444 @@ +//! REX7 V0 gas-clamp enforcement. +//! +//! Plain opcodes under checkpoint accounting record nothing, so nothing checks a limit while a +//! plain segment runs. Enforcement instead comes from the interpreter itself: at every checkpoint +//! and frame entry / resume the visible remaining gas is clamped down to the compute headroom — the +//! tighter of the frame-local budget and the TX-level (possibly detained) limit — and the hidden +//! remainder is remembered along with the constraint that bound it. revm's own per-opcode gas check +//! then stops a crossing opcode at the clamp boundary *before it executes*, and the frame's final +//! result restores the hidden gas and reclassifies the out-of-gas as the limit exceed it stands +//! for. +//! +//! These tests pin the three sides of that mechanism: +//! +//! - **Unobservable while within limits**: `GAS` reads the true counter even under an active clamp, +//! so a transaction that never exceeds a limit is bit-identical to per-opcode accounting. +//! - **Exact enforcement**: the crossing opcode never runs, its cost never enters the recorded +//! usage, and usage therefore stops at or below the limit — including inside a checkpoint-free +//! arithmetic loop, where deferring to the next checkpoint would overshoot by the whole loop. +//! - **Faithful reclassification**: frame-local exceeds revert to the parent, TX-level exceeds halt +//! the transaction with the remaining gas rescued, and a detention exceed keeps reporting +//! `VolatileDataAccessOutOfGas`. + +use crate::common::{ + transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CONTRACT, ONE_ETH, +}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, +}; +use revm::bytecode::opcode::{ + CALL, DUP1, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, TIMESTAMP, +}; + +/// Slot the outer contract stores the CALL success flag into. +const CALL_RESULT_SLOT: u64 = 0x10; +/// Slot a callee writes to, so a reverted sub-frame can be told from a committed one. +const CALLEE_SLOT: u64 = 0x11; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(crate::common::CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body: +/// +/// ```text +/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP +/// ``` +/// +/// Each iteration runs seven plain opcodes for 26 gas. `prefix` is prepended verbatim and +/// participates in the jump-target offset. +fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// Per-spec runtime limits with the TX compute gas limit replaced. +fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) +} + +/// Per-spec runtime limits with the block-environment detention cap replaced. +fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = cap; + limits + } +} + +/// The compute gas a transaction running `code` uses when nothing constrains it. +fn unconstrained_compute_gas(code: Bytes) -> u64 { + transact_default(MegaSpecId::REX7, base_db(code)).compute_gas +} + +/// `GAS` must observe the true remaining gas even while a clamp is outstanding — the checkpoint +/// prologue restores the hidden gas before the raw instruction reads the counter. +/// +/// A tight detention cap keeps the clamp active for the whole post-access run while the transaction +/// itself stays far inside every limit, so the stored reading, the compute total and the receipt +/// must all match per-opcode REX6, where no clamp exists at all. +#[test] +fn test_clamp_is_unobservable_via_the_gas_opcode() { + let code = plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .append(GAS) + .push_u256(U256::from(CALL_RESULT_SLOT)) + .append(SSTORE) + .append(STOP) + .build(); + // A cap two orders of magnitude below the frame's remaining EVM gas, so the clamp is active at + // the GAS opcode, but well above what the rest of this transaction spends, so nothing is ever + // exceeded. + let limits = detention_cap(1_000_000); + + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + + let slot = U256::from(CALL_RESULT_SLOT); + let r7_reading = r7.storage_value(CONTRACT, slot); + assert!(!r7_reading.is_zero(), "the GAS reading must be non-zero"); + assert_eq!( + r6.storage_value(CONTRACT, slot), + r7_reading, + "GAS must push the true remaining gas, not the clamped value", + ); + assert_eq!(r6.compute_gas, r7.compute_gas, "compute totals must be identical"); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas must be identical"); +} + +/// The crossing opcode is stopped before it executes, so its cost never enters the recorded usage. +/// +/// The limit is placed partway through a straight plain-opcode run. REX6 executes the crossing +/// opcode and only then records it, so its usage ends up strictly over the limit; REX7 clamps the +/// visible gas to the headroom, so revm rejects the crossing opcode at the boundary and usage stops +/// exactly at the limit. +#[test] +fn test_crossing_opcode_is_stopped_before_it_executes() { + let code = plain_filler(BytecodeBuilder::default(), 200).append(STOP).build(); + let intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + let full_run = unconstrained_compute_gas(code.clone()); + // Trip the limit halfway through the plain run. + let limit = intrinsic + (full_run - intrinsic) / 2; + let limits = compute_limit(limit); + + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); + + assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must stop on the tight compute limit: {:?}", r7.result); + + assert!( + r6.compute_gas > limit, + "REX6 records the crossing opcode before halting; compute={} limit={limit}", + r6.compute_gas + ); + assert_eq!( + r7.compute_gas, limit, + "REX7 must stop exactly at the limit: the crossing opcode never runs, and the headroom it \ + could not pay for is what the frame burns", + ); +} + +/// A TX-level crossing halts the transaction, and the gas the clamp was hiding is rescued for the +/// sender rather than burned. +/// +/// The top-level frame's compute budget equals the TX-level remaining, so the TX limit is what +/// binds and the halt must propagate. +#[test] +fn test_tx_level_clamp_exceed_halts_with_the_hidden_gas_rescued() { + // ~260k gas of plain opcodes with no checkpoint inside the loop at all. + let code = countdown_loop_code(&[], 10_000); + let intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + let limit = intrinsic + 5_000; + + let r7 = transact(MegaSpecId::REX7, base_db(code), compute_limit(limit)(MegaSpecId::REX7)); + + assert!(!r7.is_success(), "the tight compute limit must halt the transaction: {:?}", r7.result); + assert!( + matches!(r7.halt_reason("REX7"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "a TX-level clamp exceed must report the compute-gas limit; got {:?}", + r7.halt_reason("REX7"), + ); + assert_eq!(r7.compute_gas, limit, "usage must stop at the limit, not past it"); + assert!( + r7.gas_used < 200_000, + "the clamp-hidden gas must be rescued, not burned; gas_used={}", + r7.gas_used + ); +} + +/// A frame-local crossing reverts the sub-frame and lets the caller continue. +/// +/// A nested frame's compute budget is 98/100 of its parent's remaining budget, so it is always +/// tighter than the TX-level remaining — the clamp binds frame-locally, and the clamp-induced +/// out-of-gas must be reclassified into the ordinary frame-local revert rather than a TX halt. +#[test] +fn test_frame_local_clamp_exceed_reverts_to_the_parent() { + // The callee writes a slot and then burns far more compute than its frame budget allows. The + // write is passed as the loop's prefix so the loop's jump target accounts for it. + let prologue = + BytecodeBuilder::default().sstore(U256::from(CALLEE_SLOT), U256::from(0x77)).build_vec(); + let callee = countdown_loop_code(&prologue, 10_000); + + // The caller returns the CALL's success flag. A nested frame may consume up to 98/100 of its + // parent's compute budget, so the caller's own tail has to be cheap enough to fit in the + // remainder — a storage write would push the caller over its budget too. + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000_000u64) // gas + .append(CALL) + .push_number(0u64) // memory offset + .append(MSTORE) + .push_number(32u64) // length + .push_number(0u64) // offset + .append(RETURN) + .build(); + + let intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + // Enough headroom for the caller's own work and the callee's SSTORE, far short of its loop. + let limits = compute_limit(intrinsic + 100_000); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.is_success(), + "{label}: the outer transaction survives a frame-local exceed: {:?}", + r.result + ); + assert_eq!( + r.result.output().map(|o| U256::from_be_slice(o)), + Some(U256::ZERO), + "{label}: the CALL must report failure", + ); + assert!( + r.storage_value(CALLEE, U256::from(CALLEE_SLOT)).is_zero(), + "{label}: the reverted sub-frame's storage write must be discarded", + ); + } + assert!( + r7.compute_gas < r6.compute_gas, + "REX7 stops the callee before the crossing opcode, so it records less than REX6; \ + REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); +} + +/// A detention crossing keeps its `VolatileDataAccessOutOfGas` attribution. +/// +/// Detention lowers the TX-level limit to `usage_at_access + cap`, so it is the TX-level constraint +/// that binds. The usual detained-exceed predicate needs usage to have crossed the detained limit, +/// which clamp enforcement never lets happen — the attribution has to survive on the clamp's own +/// record of what bound it. +#[test] +fn test_detention_clamp_exceed_keeps_the_volatile_attribution() { + let code = countdown_loop_code(&[TIMESTAMP, POP], 10_000); + let limits = detention_cap(1_000); + + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); + + assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must halt on the detention cap: {:?}", r7.result); + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + matches!(r.halt_reason(label), MegaHaltReason::VolatileDataAccessOutOfGas { .. }), + "{label}: the halt must be attributed to volatile detention; got {:?}", + r.halt_reason(label), + ); + } +} + +/// The clamp bounds a detention cap inside a checkpoint-free arithmetic loop — the shape that makes +/// checkpoint-deferred enforcement unbounded, since the loop body contains no checkpoint at all and +/// the whole ~260k-gas loop would otherwise run to completion before anything checked. +#[test] +fn test_clamp_bounds_detention_inside_a_checkpoint_free_loop() { + let cap = 1_000; + let code = countdown_loop_code(&[TIMESTAMP, POP], 10_000); + let intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + let limits = detention_cap(cap); + + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code.clone()), limits(MegaSpecId::REX7)); + let unconstrained = unconstrained_compute_gas(code); + + // The detained limit is `usage_at_access + cap`; the access happens two opcodes in, so + // `intrinsic + TIMESTAMP + cap` bounds it from above. + let detained_upper = intrinsic + 2 + cap; + assert!( + unconstrained > 250_000, + "the loop must be far larger than the cap to make the test meaningful; loop={unconstrained}" + ); + assert!( + r7.compute_gas <= detained_upper, + "REX7 must not overshoot the detention cap; compute={} cap≈{detained_upper}", + r7.compute_gas + ); + // Per-opcode enforcement stops within one opcode of the cap; the clamp must not stop earlier. + assert!( + r7.compute_gas + 32 >= r6.compute_gas, + "REX7 must stop at the clamp boundary, not before it; REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); +} + +/// The adjudicated double-exceed corner: when the crossing opcode outruns both the true EVM +/// remaining and the compute headroom, the compute classification wins. +/// +/// A memory expansion far larger than the transaction's whole gas limit is unaffordable either way. +/// REX6 reports revm's memory out-of-gas and burns the frame; REX7 reports the compute-gas limit +/// and rescues the clamp-hidden remainder for the sender. The two are indistinguishable at the +/// frame boundary — an out-of-gas carries no opcode cost — and this direction favours the sender +/// without opening anything new: a caller that wants to avoid the burn can already REVERT. +#[test] +fn test_double_exceed_prefers_the_compute_classification() { + // A ~7.5 MB memory offset: the expansion costs on the order of 10^8 gas, well past the + // transaction's gas limit below. + let code = plain_filler(BytecodeBuilder::default(), 5) + .push_number(0u64) // value + .push_number(7_500_000u64) // offset + .append(MSTORE) + .append(STOP) + .build(); + let gas_limit = 1_000_000; + let intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + // Headroom well below the frame's true remaining, so the clamp is outstanding at the MSTORE. + let limits = compute_limit(intrinsic + 1_000); + + let r6 = transact_with_gas_limit( + MegaSpecId::REX6, + base_db(code.clone()), + limits(MegaSpecId::REX6), + gas_limit, + ); + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(code), + limits(MegaSpecId::REX7), + gas_limit, + ); + + assert!(!r6.is_success(), "REX6 must fail on the unaffordable expansion: {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must fail on the unaffordable expansion: {:?}", r7.result); + assert!( + matches!(r6.halt_reason("REX6"), MegaHaltReason::Base(_)), + "REX6 reports revm's own out-of-gas; got {:?}", + r6.halt_reason("REX6"), + ); + assert!( + matches!(r7.halt_reason("REX7"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "REX7 must classify the double exceed as a compute exceed; got {:?}", + r7.halt_reason("REX7"), + ); + assert_eq!(r6.gas_used, gas_limit, "REX6 burns the whole gas limit"); + assert!( + r7.gas_used < gas_limit, + "REX7 must rescue the clamp-hidden gas; gas_used={} limit={gas_limit}", + r7.gas_used + ); +} + +/// Every checkpoint kind has to restore the clamp before its body runs and re-apply it afterwards, +/// or the segments around it would either observe clamped gas or run unbounded. Exercising them in +/// one transaction that stays inside every limit pins the round trip: any asymmetry between the +/// restore and the re-clamp shows up as a compute-gas or receipt difference against REX6. +#[test] +fn test_clamp_round_trips_through_every_checkpoint_kind() { + let callee = plain_filler(BytecodeBuilder::default(), 5).append(STOP).build(); + let code = plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .append(GAS) + .append(POP) + .sstore(U256::from(1), U256::from(0x22)) + .push_u256(U256::from(1)) + .append(revm::bytecode::opcode::SLOAD) + .append(POP) + .push_address(CALLEE) + .append(revm::bytecode::opcode::BALANCE) + .append(POP); + let code = plain_filler(code, 5) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(500_000u64) // gas + .append(CALL) + .append(POP); + let code = plain_filler(code, 5) + .mstore(0, [0x33u8; 32]) + .push_number(0xabcu64) // topic0 + .push_number(32u64) // len + .push_number(0u64) // offset + .append(revm::bytecode::opcode::LOG1) + .append(STOP) + .build(); + + // A detention cap that engages at the TIMESTAMP but is never binding, so the clamp is + // outstanding across every later checkpoint. + let limits = detention_cap(1_000_000); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + assert_outcomes_identical(&r6, &r7); +} + +fn assert_outcomes_identical(r6: &Outcome, r7: &Outcome) { + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "execution result must be identical", + ); + assert_eq!(r6.compute_gas, r7.compute_gas, "compute gas must be identical"); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be identical"); + assert_eq!( + (r6.data_size, r6.kv_updates, r6.state_growth), + (r7.data_size, r7.kv_updates, r7.state_growth), + "the non-compute dimensions must be identical", + ); +} From 603af8477ae4cbbbe3460f84687f92c5c9ab1e6c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 16:47:55 +0800 Subject: [PATCH 006/208] fix(rex7): apply the detention cap on a frame-local checkpoint exceed A frame-local compute exceed reports as a revert, which the per-opcode layering carries past the detention tail rather than returning on, so the cap is installed even though the frame is about to unwind; a TX-level exceed reports as an out-of-gas halt, which that layering short-circuits on. The volatile checkpoint handlers now reproduce both arms when recording their own body, instead of returning on either. Adds a REX6/REX7 parity test for a volatile checkpoint whose own body crosses the compute limit, covering the halt, the recorded usage and the resulting detained limit together. --- crates/mega-evm/src/evm/instructions.rs | 32 +++++++++-- crates/mega-evm/tests/rex7/common.rs | 15 +++++- crates/mega-evm/tests/rex7/v0_clamp.rs | 72 ++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 0c5fa5ee..f40f01e9 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -902,6 +902,30 @@ macro_rules! record_checkpoint_body_compute_gas { compute_gas!($context.interpreter, additional_limit, gas_used); } }; + // Variant for the volatile checkpoints, whose tail installs the detention cap. A frame-local + // exceed reports as a revert, which the per-opcode layering carries past the cap application + // rather than returning on, so the cap is applied here before returning. A TX-level exceed + // reports as an out-of-gas halt, which that layering does short-circuit — no cap on that path. + ($context:expr, $gas_before:expr, detention_tail) => { + let gas_after = $context.interpreter.gas.remaining(); + let gas_used = $gas_before.saturating_sub(gas_after); + let exceeding_result = { + let mut additional_limit = $context.host.additional_limit().borrow_mut(); + additional_limit.sync_checkpoint_baseline(gas_after); + if additional_limit.record_compute_gas(gas_used) { + None + } else { + Some(additional_limit.exceeding_instruction_result()) + } + }; + if let Some(result) = exceeding_result { + set_halt_action!($context.interpreter, result); + if !result.is_halt() { + apply_compute_gas_limit!($context); + } + return Err(result); + } + }; } /// Records an opcode's compute gas in a single measurement window and enforces the compute-gas @@ -2053,7 +2077,7 @@ pub mod volatile_data_ext { charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); - record_checkpoint_body_compute_gas!(context, gas_before); + record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); checkpoint_epilogue!(context); inner_outcome @@ -2088,7 +2112,7 @@ pub mod volatile_data_ext { run_inner_instruction_or_abort!($original_fn, context, inner_outcome); charge_static_gas!(context, $opcode); - record_checkpoint_body_compute_gas!(context, gas_before); + record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); checkpoint_epilogue!(context); inner_outcome @@ -2187,7 +2211,7 @@ pub mod volatile_data_ext { run_inner_instruction_or_abort!(instructions::host::sload, context, inner_outcome); charge_static_gas!(context, SLOAD); - record_checkpoint_body_compute_gas!(context, gas_before); + record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); checkpoint_epilogue!(context); inner_outcome @@ -2213,7 +2237,7 @@ pub mod volatile_data_ext { charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); - record_checkpoint_body_compute_gas!(context, gas_before); + record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); checkpoint_epilogue!(context); inner_outcome diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 9b0d5cb6..5e2392df 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -36,6 +36,9 @@ pub(crate) struct Outcome { pub(crate) state_growth: u64, /// Receipt `gas_used` (combined compute + storage EVM gas). pub(crate) gas_used: u64, + /// Post-tx detained compute gas limit — equal to the configured TX limit unless volatile + /// access lowered it. + pub(crate) detained_compute_gas_limit: u64, /// The state the transaction produced. pub(crate) state: EvmState, } @@ -98,7 +101,10 @@ pub(crate) fn transact_with_gas_limit( let mut evm = MegaEvm::new(context); let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + let (usage, detained_compute_gas_limit) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) + }; let gas_used = result.result.tx_gas_used(); Outcome { result: result.result, @@ -107,6 +113,7 @@ pub(crate) fn transact_with_gas_limit( kv_updates: usage.kv_updates, state_growth: usage.state_growth, gas_used, + detained_compute_gas_limit, state: result.state, } } @@ -143,7 +150,10 @@ pub(crate) fn transact_with_bucket_capacity( let mut evm = MegaEvm::new(context); let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let usage = evm.ctx_ref().additional_limit.borrow().get_usage(); + let (usage, detained_compute_gas_limit) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) + }; let gas_used = result.result.tx_gas_used(); Outcome { result: result.result, @@ -152,6 +162,7 @@ pub(crate) fn transact_with_bucket_capacity( kv_updates: usage.kv_updates, state_growth: usage.state_growth, gas_used, + detained_compute_gas_limit, state: result.state, } } diff --git a/crates/mega-evm/tests/rex7/v0_clamp.rs b/crates/mega-evm/tests/rex7/v0_clamp.rs index 29000935..2e8c2343 100644 --- a/crates/mega-evm/tests/rex7/v0_clamp.rs +++ b/crates/mega-evm/tests/rex7/v0_clamp.rs @@ -23,13 +23,14 @@ use crate::common::{ transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CONTRACT, ONE_ETH, }; -use alloy_primitives::{Bytes, U256}; +use alloy_primitives::{Address, Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, }; use revm::bytecode::opcode::{ - CALL, DUP1, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, TIMESTAMP, + CALL, DUP1, EXTCODECOPY, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, + TIMESTAMP, }; /// Slot the outer contract stores the CALL success flag into. @@ -442,3 +443,70 @@ fn assert_outcomes_identical(r6: &Outcome, r7: &Outcome) { "the non-compute dimensions must be identical", ); } + +/// A volatile checkpoint whose own body crosses the compute limit must behave identically under +/// both accounting models. +/// +/// The prologue restores the clamp before the body runs, so an `EXTCODECOPY` large enough to cross +/// the limit is metered on the true counter and recorded per opcode exactly as REX6 records it — +/// the clamp plays no part. What the checkpoint form has to reproduce is the tail: the detention +/// cap is applied on a frame-local exceed (a revert the per-opcode layering carries past the cap) +/// and skipped on a TX-level exceed (an out-of-gas halt that layering short-circuits on). Pinning +/// the halt, the recorded usage and the resulting detained limit together covers both the metering +/// and that ordering. +#[test] +fn test_volatile_body_crossing_the_limit_matches_per_opcode() { + // ~1.5 MB of EXTCODECOPY against the block beneficiary: the copy plus the memory expansion cost + // millions of gas, and the account load marks beneficiary access. + let callee = BytecodeBuilder::default() + .push_number(1_500_000u64) // length + .push_number(0u64) // offset + .push_number(0u64) // destOffset + .push_address(Address::ZERO) // the default block beneficiary + .append(EXTCODECOPY) + .append(STOP) + .build(); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000_000u64) // gas + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let full = transact_default(MegaSpecId::REX7, build_db()).compute_gas; + assert!(full > 4_000_000, "the copy must dominate the transaction; compute={full}"); + + // Just under what the transaction needs, so the crossing lands inside the EXTCODECOPY body + // rather than in a plain segment. + let tx_limit = full - full / 100; + let limits = move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(tx_limit); + limits.block_env_access_compute_gas_limit = 1_000; + limits + }; + + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + + assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the halt must be identical", + ); + assert_eq!( + r6.compute_gas, r7.compute_gas, + "the body is metered on the true counter under both models; REX6={} REX7={}", + r6.compute_gas, r7.compute_gas + ); + assert_eq!( + r6.detained_compute_gas_limit, r7.detained_compute_gas_limit, + "the detention tail must fire — or not fire — at the same point under both models", + ); +} From fc53eed7908b7b3931ad9ea977fd935a04c16daa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:04:52 +0800 Subject: [PATCH 007/208] docs(rex7): document checkpoint compute gas accounting Record REX7 checkpoint settlement and V0 gas-clamp enforcement on the upgrade page, gate matching rules under details on compute-gas and related metering pages, and update AGENTS.md protocol wording. --- AGENTS.md | 5 +- docs/spec/evm/compute-gas.md | 54 ++++++++++++ docs/spec/evm/dual-gas-model.md | 11 +++ docs/spec/evm/gas-detention.md | 10 +++ docs/spec/evm/resource-accounting.md | 1 + docs/spec/evm/resource-limits.md | 1 + docs/spec/hardfork-spec.md | 9 +- docs/spec/overview.md | 2 +- docs/spec/upgrades/overview.md | 2 +- docs/spec/upgrades/rex7.md | 119 ++++++++++++++++++++++++--- 10 files changed, 197 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4024f7e0..0c086a9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,7 +114,8 @@ Consequently: MegaETH separates EVM gas into two independent dimensions tracked during execution: - **Compute gas**: Measures pure computational cost. - Every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. + Through REX6 every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. + REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a V0 gas clamp. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). @@ -222,7 +223,7 @@ Correctness of the other three dimensions (data size, KV updates, state growth) 1. **Every non-compute mutation site must latch.** Any code that records data-size/KV/state-growth usage during execution (`on_sstore`, `on_log`, `record_oracle_hint_bytes`, the frame-lifecycle hooks) must run `check_limit()` itself, latching any exceed into `has_exceeded_limit`. - The latch is surfaced by the leading short-circuit of the next `record_compute_gas` call, so the halt lands on the same opcode as the pre-protocol fan-out did. + The latch is surfaced by the leading short-circuit of the next `record_compute_gas` call (through REX6, that is the next metered opcode; under REX7 checkpoint accounting it is the next checkpoint), so the halt lands on the same site as the pre-protocol fan-out did. 2. **Pre-inner recorders must NOT latch.** A site that records usage _before_ its inner instruction executes (currently SELFDESTRUCT's two beneficiary recorders: empty-beneficiary creation and the REX6+ existing-beneficiary credit) must record without latching: the inner instruction can still fail, the frame then discards the usage, and an early latch would stick and rewrite the frame's real result. Such opcodes use a trailing all-dimension check (`record_compute_gas_all_dims`) that runs only after the inner instruction succeeds. diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 5a1069d9..151fe335 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -444,6 +444,59 @@ The transaction's standard EVM `gas_limit` remains the only bound that can halt A node MUST record compute gas before evaluating any exceed, including an exceed already latched on another resource dimension. The compute work was performed, and the recorded total feeds the transaction outcome and the block-level compute accounting even for a transaction halted on a different dimension. +
+Rex7 (unstable): checkpoint settlement and gas-clamp enforcement + +Rex7 replaces per-opcode recording for plain opcodes with checkpoint settlement, and enforces compute-gas and detention limits inside plain segments by clamping interpreter-visible gas. +The full previous/new pairing is on the [Rex7 Network Upgrade](../upgrades/rex7.md) page; the normative rules for implementers follow. + +#### Checkpoint set + +A node MUST settle compute gas at each of the following **checkpoints**, and MUST NOT open a per-opcode measurement window for any other opcode: + +- storage-gas opcodes: `SSTORE`, `LOG0`–`LOG4`, `SELFDESTRUCT`; +- call-family opcodes: `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`; +- create opcodes: `CREATE`, `CREATE2`; +- volatile / detention-guarded opcodes: the unconditional block-environment set, the beneficiary-conditional set, and oracle-conditional `SLOAD` (same membership as the Volatile class and the call-family / `SELFDESTRUCT` beneficiary guards above); +- the `GAS` opcode; +- frame entry, frame resume after a child returns, and frame exit. + +Plain opcodes between checkpoints MUST run without recording compute gas when they finish. + +#### Segment settlement + +At each checkpoint a node MUST: + +1. Settle the open plain-opcode segment as the interpreter-gas delta since the previous checkpoint or frame open/resume, applying the same storage-gas and forwarded-child exclusions as the checkpoint opcode's measurement window under this page's stable rules. +2. Record that segment amount as compute gas and evaluate the compute-gas limit (and any latched non-compute resource-limit exceed) at that checkpoint — the latch-surface point is the next checkpoint rather than the next per-opcode recording site. +3. Record the checkpoint opcode's own body under the measurement-window rules for its metering class, then re-open the settlement window. + +Non-opcode recording sites on this page (intrinsic gas, precompiles, code deposit, KeylessDeploy) are unchanged. + +For every transaction that stays within every runtime resource limit, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. + +#### Gas-clamp enforcement + +After settlement and body recording at a checkpoint (and at frame entry and resume), a node MUST clamp the interpreter-visible remaining gas to the remaining compute headroom — the minimum of the current frame's remaining per-frame compute budget and the transaction-level remaining budget under the effective limit (including detention) — and MUST restore the hidden amount before the next checkpoint body, before `GAS` is observed, before call-gas forwarding, and before storage-gas charges. + +Inside a plain-opcode segment: + +- An opcode that would cost more than the clamped visible remainder MUST NOT execute. +- The frame's final result MUST restore the hidden gas. +- The node MUST reclassify that out-of-gas as the resource-limit exceed the clamp stood for: frame-local budget → frame revert with `MegaLimitExceeded`; transaction-level compute → transaction halt with `OutOfGas` and rescued remaining gas; detained limit → transaction halt with `VolatileDataAccessOutOfGas` and rescued remaining gas. + +Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. + +When the crossing opcode would exhaust both the true remaining EVM gas and the compute headroom, a node MUST attribute the halt to the compute-gas or detention limit (with rescue) rather than to ordinary EVM out-of-gas. + +#### Exceptional-halt frame carve-out + +When a frame ends in an exceptional halt — including ordinary out-of-gas and memory out-of-gas — the interpreter zeros the frame's remaining gas before frame-exit settlement. +A node MUST settle that entire burned remainder as compute gas at frame exit. +Under per-opcode recording through Rex6 neither the failing opcode nor the burn is attributed to compute gas, so a transaction that contains an inner out-of-gas call frame MAY report a strictly higher compute-gas total under Rex7 while EVM gas and the receipt remain identical. + +
+ #### Keyless Deploy Exceed When recording the [KeylessDeploy](../system-contracts/keyless-deploy.md) dispatch overhead exceeds a compute gas limit, the outcome follows the frame-local / transaction-level split above, but the two branches are not observably the same: @@ -572,3 +625,4 @@ System-granted gas leaks to the sender, who recovers gas that was never theirs t - [Rex4](../upgrades/rex4.md) — introduced the per-call-frame compute gas budget; made gas detention caps relative to usage at the access point; added beneficiary volatile-access guards to the `CALL` family, `SELFDESTRUCT`, and `SELFBALANCE`. - [Rex5](../upgrades/rex5.md) — excluded the `CALL_STIPEND` from the forwarded-gas deduction; moved `CREATE2` memory-expansion recording ahead of the storage-gas charge; made contract-creation code-deposit compute gas atomic with the deployment commit; refined precompile compute-gas recording and bounded it by the remaining compute budget; added the `SELFDESTRUCT` empty-beneficiary storage-gas charge; removed `CALLCODE` from the cold first-touch charge and added `SELFDESTRUCT`'s beneficiary to it; stopped following EIP-7702 delegation in the pre-execution inspection, restoring inherited warmth for delegates. - [Rex6](../upgrades/rex6.md) — unified the measurement window across all storage-affecting opcodes and folded `CREATE2` memory expansion into it, ending the two-window exception; returned forwarded gas to the failing frame on a compute-gas exceed; rescued the unused envelope on a keyless-deploy dispatch exceed; made beneficiary detection delegation-aware, returning `CALLCODE` call targets to the cold first-touch charge; exempted system-originated transactions from the compute gas limit and gas detention. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit. diff --git a/docs/spec/evm/dual-gas-model.md b/docs/spec/evm/dual-gas-model.md index 374eb057..60043b5e 100644 --- a/docs/spec/evm/dual-gas-model.md +++ b/docs/spec/evm/dual-gas-model.md @@ -99,6 +99,16 @@ When more than one dimension is over its limit on that opcode, the reported dime A node MUST record an opcode's compute gas in exactly one step, after the opcode body has fully executed — with no `CREATE2` exception. The no-record rule when the body does not run to completion is specified in [Single-Record Rule](compute-gas.md#single-record-rule). +
+Rex7 (unstable): checkpoint settlement of compute gas + +Under Rex7, the metering order above continues to govern every **checkpoint** opcode — the storage-affecting set listed in this section, the volatile / detention-guarded set, and `GAS` — and those checkpoints still charge storage gas before the body and record compute gas after it. +Plain opcodes between checkpoints MUST NOT record compute gas when they finish; their compute gas settles as an interpreter-gas segment delta at the next checkpoint or at frame entry, resume, or exit. +Limit enforcement inside a plain-opcode segment uses gas clamping rather than a post-opcode record step: a crossing opcode is stopped before it executes, and its cost is excluded from recorded usage. +See [Compute Gas Accounting](compute-gas.md) and the [Rex7 Network Upgrade](../upgrades/rex7.md) for the full checkpoint set, clamp rules, and the exceptional-halt frame carve-out. + +
+ ### Storage Gas [Storage gas](../glossary.md#storage-gas) is an additional charge for operations that impose persistent storage burden on nodes. @@ -305,3 +315,4 @@ For the historical evolution of storage gas formulas and constants across specs: - [Rex4](../upgrades/rex4.md) — storage gas stipend for value transfers - [Rex5](../upgrades/rex5.md) — reworked the storage gas stipend into a separated-allowance model, derived the top-level contract-creation storage-gas address from the sender's current state nonce, and made contract-creation code-deposit compute gas atomic with the deployment commit - [Rex6](../upgrades/rex6.md) — unified per-opcode gas metering order (compute gas recorded once, after the opcode body, with no `CREATE2` exception); system-originated transactions charge dynamic storage gas at minimum bucket capacity; forwarded gas and the KeylessDeploy envelope are returned on a compute-gas exceed rather than spent +- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; clamps interpreter-visible gas between checkpoints so compute and detention limits stop a crossing opcode before it executes diff --git a/docs/spec/evm/gas-detention.md b/docs/spec/evm/gas-detention.md index 5217179b..d8c5bdac 100644 --- a/docs/spec/evm/gas-detention.md +++ b/docs/spec/evm/gas-detention.md @@ -113,6 +113,15 @@ When a volatile-data trigger occurs, the node MUST perform the following steps i After detention has been applied, any subsequent execution step that would cause `compute_gas_used` to exceed the effective detained limit MUST halt the transaction with `VolatileDataAccessOutOfGas`. +
+Rex7 (unstable): clamp-based detention enforcement inside plain segments + +Under Rex7, after a detention cap has been installed the remaining compute headroom includes that detained limit, and the gas clamp applied at checkpoints and frame boundaries restricts interpreter-visible gas to that headroom. +A plain-opcode segment that would cross the detained limit is therefore stopped at the clamp boundary before the crossing opcode executes, reclassified as `VolatileDataAccessOutOfGas`, with remaining gas rescued for the sender — the same halt reason and refund shape as through Rex6, but without executing the crossing opcode or recording its cost. +See [Compute Gas Accounting](compute-gas.md) and the [Rex7 Network Upgrade](../upgrades/rex7.md). + +
+ The detained compute-gas limit MUST NOT halt a [system-originated transaction](../system-contracts/system-tx.md#system-originated-transaction-metering-exemption). Volatile-data accesses by such a transaction are still tracked, but the detention cap is not enforced against it; its standard EVM `gas_limit` remains the only halting bound. @@ -204,3 +213,4 @@ Gas detention semantics evolved across specs: - [Rex3](../upgrades/rex3.md) — raised oracle cap to 20M and changed oracle detection from CALL-based to SLOAD-based - [Rex4](../upgrades/rex4.md) — changes absolute detention to relative detention and adds additional beneficiary-triggered behavior - [Rex6](../upgrades/rex6.md) — adds a beneficiary-detention trigger for an applied EIP-7702 authorization whose authority equals the block beneficiary; resolves a CALL-family target's EIP-7702 delegation one hop before the beneficiary comparison, so a call through a delegator whose delegate is the beneficiary triggers detention (through Rex5 only the raw target is compared); and stops enforcing the detention cap against system-originated transactions, whose volatile accesses are still tracked +- [Rex7](../upgrades/rex7.md) _(unstable)_ — enforces the detained limit inside plain-opcode segments by gas clamping, stopping a crossing opcode before it executes while preserving `VolatileDataAccessOutOfGas` and gas rescue diff --git a/docs/spec/evm/resource-accounting.md b/docs/spec/evm/resource-accounting.md index 354006bd..6a63ae2c 100644 --- a/docs/spec/evm/resource-accounting.md +++ b/docs/spec/evm/resource-accounting.md @@ -273,3 +273,4 @@ This page describes the current accounting behavior. - [Rex6](../upgrades/rex6.md) — counted the account-info write of a `SELFDESTRUCT` balance credit to an already-existing beneficiary: through Rex5 only a `SELFDESTRUCT` that created a new beneficiary was metered, so a balance credit to an existing beneficiary (which does not flow through the frame-initialization or caller-dedup path) recorded nothing. - [Rex6](../upgrades/rex6.md) — added a per-log data-size base: through Rex5, an empty `LOG0` contributed zero data size because the log address was not counted. - [Rex6](../upgrades/rex6.md) — deduplicated the value self-transfer account-info write: when a value-transferring call's target equals its caller, the caller-side and target-side writes refer to the same account, but through Rex5 the data-size and KV-update charges were recorded for both, over-counting the one account (it never under-charges). This extends the Rex5 caller-account deduplication above to the self-transfer case. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — does not change data-size, KV-update, or state-growth counting; compute-gas settlement moves to checkpoints (see [Compute Gas Accounting](compute-gas.md)). diff --git a/docs/spec/evm/resource-limits.md b/docs/spec/evm/resource-limits.md index 1ceedc5c..969826f7 100644 --- a/docs/spec/evm/resource-limits.md +++ b/docs/spec/evm/resource-limits.md @@ -246,3 +246,4 @@ Including failed transactions ensures the sender always pays for consumed resour - [Rex4](../upgrades/rex4.md) — added per-call-frame runtime budgets; intrinsic resource costs (always deducted before execution) are now reflected in the top-level frame budget before it is forwarded to child frames. - [Rex5](../upgrades/rex5.md) — bounded a precompile invocation's compute-gas consumption by the remaining compute-gas budget, failing the precompile with `PrecompileOOG` rather than letting it overshoot the budget. - [Rex6](../upgrades/rex6.md) — moved EIP-7702 authority state-growth resolution from pre-execution (after the caller nonce bump) to validation, and added dynamic SALT account-creation gas for each net-new applied authority to the pre-frame intrinsic gas deduction; removed the keyless-deploy exception to gas preservation, so remaining gas is now rescued on every transaction-level exceed; and stopped enforcing the four runtime transaction-level limits against system-originated transactions, whose usage is still recorded. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — does not change the limit ceilings or the success/failed/skipped/rejected outcomes; a compute-gas or detention exceed inside a plain-opcode segment is stopped before the crossing opcode executes (see [Compute Gas Accounting](compute-gas.md)). diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index ff10aee4..b717ea36 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -150,7 +150,10 @@ _See [Rex6 Network Upgrade](upgrades/rex6.md) for full details._ ### REX7 -REX7 is the current **unstable** spec under active development. -It introduces no behavioral change over REX6 yet; its semantics may change at any time before it is frozen. +REX7 is the current **unstable** spec under active development; its semantics may change at any time before it is frozen. -_See [Rex7 Network Upgrade](upgrades/rex7.md) for the current state._ +- **Checkpoint-settled compute gas** — Plain opcodes record no compute gas between checkpoints; settlement runs at storage-gas opcodes, the CALL / CREATE family, volatile opcodes, `GAS`, and frame entry / resume / exit. +- **Gas-clamp enforcement** — Between checkpoints the interpreter-visible remaining gas is clamped to the remaining compute headroom, so a compute-gas or detention exceed stops the crossing opcode before it executes (zero overshoot; crossing cost excluded from recorded usage). +- **Exceptional-halt frame carve-out** — A frame that ends in an exceptional halt (including out-of-gas) settles its burned remainder as compute gas, so nested out-of-gas calls may report higher compute usage than REX6 while EVM gas and the receipt stay the same. + +_See [Rex7 Network Upgrade](upgrades/rex7.md) for the full previous/new pairing._ diff --git a/docs/spec/overview.md b/docs/spec/overview.md index bd84fd7e..4eaa0dc9 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -71,7 +71,7 @@ Contracts deployed under a given spec will continue to behave identically, regar - **REX4** — Per-call-frame resource budgets, relative gas detention, [storage gas stipend](glossary.md#storage-gas-stipend), MegaAccessControl and MegaLimitControl system contracts. - **REX5** — SequencerRegistry system contract, Oracle v2.0.0 with dynamic system address, caller-account update deduplication, storage-gas-stipend separated-allowance model, value-transfer CALL/CALLCODE parent compute-gas attribution, CREATE code-deposit compute-gas atomicity, EIP-2935/EIP-4788 pre-block gas floor with fail-closed block rejection, CREATE2 empty-initcode short-circuit, KeylessDeploy trailing-bytes rejection and empty-code log forwarding. - **REX6** — Unified per-opcode gas metering order, consolidated EIP-7702 authorization accounting, CREATE-frame accounting corrections, KeylessDeploy sandbox hardening, post-execution fee-reward accounting, system-originated transaction metering exemption, extended beneficiary detention coverage, and SequencerRegistry v2.0.0 rotation hardening. -- **REX7** — The **unstable** spec, currently open for development. No behavioral change over REX6 yet. +- **REX7** — The **unstable** spec, currently open for development. Checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints. See [Hardforks and Specs](hardfork-spec.md) for full details. diff --git a/docs/spec/upgrades/overview.md b/docs/spec/upgrades/overview.md index d544ca07..8898a870 100644 --- a/docs/spec/upgrades/overview.md +++ b/docs/spec/upgrades/overview.md @@ -153,7 +153,7 @@ Not yet scheduled {% endtabs %} Unstable; under active development. -No behavioral change over Rex6 yet. +Checkpoint-settled [compute gas](../glossary.md#compute-gas) accounting with gas-clamp enforcement: plain opcodes record nothing between checkpoints; within-limit transactions stay bit-identical to Rex6; a compute-gas or detention exceed inside a plain segment stops the crossing opcode before it executes. ## How to Read These Pages diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index dcdaba79..aa58f935 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -1,5 +1,5 @@ --- -description: Rex7 network upgrade — the current unstable spec, open for development and carrying no behavioral change over Rex6 yet. +description: Rex7 network upgrade — checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints, within-limit transactions stay bit-identical to Rex6, and limit-exceeding opcodes are stopped before they execute. --- # Rex7 Network Upgrade @@ -14,34 +14,133 @@ Anything recorded on this page may change before Rex7 is frozen, and nothing her ## Summary -Rex7 is the spec currently open for development. -It inherits every [Rex6](rex6.md) behavior and, as of this page, changes none of them: a transaction executed under Rex7 produces the same result as the same transaction executed under Rex6. +Rex7 changes how a node records and enforces [compute gas](../glossary.md#compute-gas) during execution. -Rex7 exists so that new behavior has somewhere to land. -[Rex6](rex6.md) is frozen — its semantics are fixed and may no longer be modified — so any change to gas costs, opcode behavior, resource accounting, or a system contract must be introduced under Rex7. +Through [Rex6](rex6.md), every metered opcode records its own compute gas after it finishes, and a compute-limit exceed is evaluated at that opcode. +Rex7 replaces that per-opcode recording for ordinary opcodes with **checkpoint settlement**: plain opcodes run without a compute-gas recording step, and the node settles the compute gas of an entire segment when it reaches a checkpoint. + +Rex7 also introduces **gas-clamp enforcement**: between checkpoints the node restricts the interpreter-visible remaining gas to the remaining compute headroom, so the inherited EVM's own per-opcode gas check stops a limit-crossing opcode before that opcode executes. + +For a transaction that never crosses a compute-gas, detention, or other resource limit, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. +For a transaction that does cross a compute-gas or detention limit inside a plain-opcode segment, the halt lands before the crossing opcode rather than after it, the crossing opcode's cost is excluded from recorded compute usage, and remaining gas remains refundable under the same rescue rules as other transaction-level compute-limit halts. + +One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire burned EVM-gas budget as compute gas at frame exit, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. ## What Changed -Nothing yet. +### Checkpoint-Settled Compute Gas Accounting + +#### Previous behavior + +From [MiniRex](minirex.md) through [Rex6](rex6.md), a node records compute gas at every metered opcode: + +- Each opcode belongs to a metering class defined in [Compute Gas Accounting](../evm/compute-gas.md). +- After the opcode body completes (or at the equivalent single measurement window for storage-affecting opcodes), the node records `(gas_before − gas_after)` less any storage-gas and forwarded-child exclusions, and evaluates the compute-gas limit. +- Plain opcodes (arithmetic, stack, memory, jumps, and similar) each open and close their own measurement window. +- A compute-gas or detention exceed is evaluated after the opcode that crossed the limit has finished, so that opcode's cost is included in recorded usage and the recorded total can land strictly above the limit. + +Frame entry, frame resume, and frame exit do not themselves settle a multi-opcode segment; they only participate in per-frame budget push/pop and in the non-opcode recording sites listed in [Compute Gas Accounting](../evm/compute-gas.md#non-opcode-recording-sites). + +#### New behavior + +Under Rex7, a node MUST settle compute gas at **checkpoints** rather than after every plain opcode. + +A **checkpoint** is any of the following: + +1. A storage-gas opcode: `SSTORE`, `LOG0`–`LOG4`, `SELFDESTRUCT`. +2. A call-family opcode: `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`. +3. A create opcode: `CREATE`, `CREATE2`. +4. A volatile / detention-guarded opcode: the unconditional block-environment set (`BLOCKHASH`, `COINBASE`, `TIMESTAMP`, `NUMBER`, `DIFFICULTY` / `PREVRANDAO`, `GASLIMIT`, `BASEFEE`, `BLOBBASEFEE`, `BLOBHASH`), the beneficiary-conditional set (`BALANCE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, `SELFBALANCE`), and oracle-conditional `SLOAD`. +5. The `GAS` opcode. +6. Frame entry, frame resume after a child returns, and frame exit. + +Every other opcode is a **plain opcode** for settlement purposes. +A plain opcode MUST NOT open a compute-gas measurement window of its own and MUST NOT record compute gas when it finishes. + +At each checkpoint a node MUST: + +1. Settle the open plain-opcode segment as the interpreter-gas delta since the previous checkpoint (or since the frame opened / resumed), excluding storage gas and forwarded child gas that the checkpoint itself charges or forwards under the same exclusion rules as [Compute Gas Accounting](../evm/compute-gas.md). +2. Record that segment amount as compute gas and evaluate the compute-gas limit (and any latched non-compute resource-limit exceed) at that checkpoint. +3. Record the checkpoint opcode's own body compute gas under the same measurement-window rules that apply through Rex6 for that opcode class, then re-open the settlement window for the next segment. + +Non-opcode recording sites (transaction intrinsic gas, precompiles, contract-creation code deposit, KeylessDeploy overhead and sandbox merge) are unchanged. + +**Precision invariant.** +For every transaction that stays within every runtime resource limit, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. +The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed. -Each change landed under Rex7 will be recorded here as a **Previous behavior** / **New behavior** pair, in the order it is specified. +**Exceptional-halt frame carve-out.** +When a frame ends in an exceptional halt — including ordinary out-of-gas and memory out-of-gas — the interpreter zeros the frame's remaining gas before the frame-exit settlement runs. +A node MUST therefore settle the entire burned remainder of that frame's budget as compute gas at frame exit. +Under per-opcode recording through Rex6, neither the failing opcode nor the burn is attributed to compute gas. +Consequently, a transaction that contains an inner call frame which runs out of gas MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. + +### Gas-Clamp Enforcement + +#### Previous behavior + +Through Rex6, the compute-gas limit and the detained compute-gas limit are enforced when an opcode records its compute gas after it has finished. +The crossing opcode therefore executes fully, its cost is recorded, and recorded usage can land strictly above the limit (overshoot of one opcode). +Frame-local budget exceeds become frame reverts with `MegaLimitExceeded`; transaction-level and detention exceeds become transaction halts with remaining gas rescued for the sender. + +#### New behavior + +Under Rex7, a node MUST enforce compute-gas and detention limits inside plain-opcode segments by **clamping** the interpreter-visible remaining gas. + +At each checkpoint, after settlement and after the checkpoint body has recorded its own compute gas (and after any detention cap the checkpoint installs), and again at frame entry and resume, a node MUST: + +1. Compute the remaining compute headroom as the minimum of the current frame's remaining per-frame compute budget and the transaction-level remaining budget under the effective limit (including detention). +2. Hide any interpreter remaining gas above that headroom from the interpreter, remembering both the hidden amount and which constraint bound the clamp (frame-local budget vs transaction-level / detained limit). +3. Leave the true remaining gas available again before the next checkpoint body runs, before `GAS` is observed, before call-gas forwarding is computed, and before storage-gas charges are taken, so those sites always see the unclamped counter. + +Inside a plain-opcode segment only plain opcodes run, so the inherited EVM's ordinary per-opcode gas check is the enforcement tool: + +- When an opcode would cost more gas than the clamped visible remainder, the opcode MUST NOT execute. +- The frame's final result MUST restore the hidden gas into the gas counter. +- The node MUST reclassify that out-of-gas as the resource-limit exceed that the clamp stood for: + - **Frame-local binding** → the frame reverts with `MegaLimitExceeded(uint8 kind, uint64 limit)`, and unspent gas returns to the parent through ordinary frame accounting. + - **Transaction-level compute binding** → the transaction halts with `OutOfGas`, and remaining gas is rescued and refunded to the sender. + - **Detained-limit binding** → the transaction halts with `VolatileDataAccessOutOfGas`, with the same gas rescue. + +Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. +Recorded usage at a clamp-induced halt therefore ends at the limit (or strictly below it if settlement had not yet closed a partial segment), not strictly above it. + +**Double-exceed preference.** +When the crossing opcode would have exhausted both the true remaining EVM gas and the compute headroom at the same point, a node MUST attribute the halt to the compute-gas (or detention) limit rather than to ordinary EVM out-of-gas, so remaining gas stays refundable under the rescue rules. +The two cases are indistinguishable once the frame has already reported out-of-gas, and the compute classification is the one that preserves the sender refund. + +**Within-limit observability.** +For a transaction that never crosses a compute or detention limit, the clamp MUST be unobservable: `GAS` returns the true remaining gas, call forwarding and storage-gas charges see the true counter, and gas, receipt, and state match Rex6. ## Developer Impact -None. +Rex7 is not scheduled on any network. +Its semantics may still change before it is frozen. -Rex7 is not scheduled on any network, and it is behaviorally identical to Rex6, so no contract, tool, or integration needs to do anything today. +Contracts and tools that assume per-opcode compute-gas attribution for every instruction MUST treat that assumption as false under Rex7: only checkpoints settle compute gas during execution, and a plain-opcode segment has no intermediate recording. + +Contracts that stay within every resource limit see no behavioral change relative to Rex6. +Contracts that trip the compute-gas or detention limit inside a plain-opcode segment halt one opcode earlier than under Rex6, with the crossing opcode excluded from recorded compute usage and with remaining gas still refundable on a transaction-level halt. + +A parent that calls into a child which runs out of ordinary EVM gas may observe a higher transaction-level compute-gas total under Rex7 than under Rex6; the receipt `gas_used` and the execution success or failure of the outer transaction are unchanged by that carve-out alone. ## Safety and Compatibility Rex7 changes nothing about how blocks under earlier specs are executed. -Every spec through Rex6 is frozen: a node replaying historical blocks resolves each block's spec from its timestamp and applies that spec's semantics, unaffected by Rex7's existence. +Every spec through Rex6 remains frozen: a node replaying historical blocks resolves each block's spec from its timestamp and applies that spec's semantics. Because Rex7 is unstable, its semantics may change in either direction until it is frozen. Any node, tool, or test fixture pinned to Rex7 must expect its results to move. A deployment that needs stable semantics must select a frozen spec explicitly rather than relying on the latest one. +The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and recorded usage does not pass the limit by that opcode's cost. +The exceptional-halt frame carve-out is the only path on which Rex7 can report more compute gas than Rex6 for the same inputs; it over-reports rather than under-reports. + ## References - [Hardforks and Specs](../hardfork-spec.md) — how specs are versioned, frozen, and activated. - [Rex6 Network Upgrade](rex6.md) — the frozen spec Rex7 inherits from. +- [Compute Gas Accounting](../evm/compute-gas.md) — measurement windows, metering classes, and exceed behavior (Rex7 details on that page). +- [Dual Gas Model](../evm/dual-gas-model.md) — total gas, storage gas, and metering order. +- [Gas Detention](../evm/gas-detention.md) — detained compute-gas caps. +- [Multidimensional Resource Limits](../evm/resource-limits.md) — transaction- and frame-level limit outcomes. From 66dd95665d0a58eb1593052ed750d90e577aaa63 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:11:31 +0800 Subject: [PATCH 008/208] test(rex7): cover interceptor and precompile resume settlement --- crates/mega-evm/tests/rex7/common.rs | 95 +++- .../mega-evm/tests/rex7/interceptor_resume.rs | 421 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 3 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 crates/mega-evm/tests/rex7/interceptor_resume.rs diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 5e2392df..066df1de 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -6,7 +6,7 @@ use mega_evm::{ MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, }; use revm::{ - context::{result::ExecutionResult, tx::TxEnvBuilder}, + context::{result::ExecutionResult, tx::TxEnvBuilder, TxEnv}, handler::EvmTr, state::EvmState, }; @@ -123,6 +123,99 @@ pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) } +/// The transaction shape [`transact`] runs: a plain call from [`CALLER`] to [`CONTRACT`]. +pub(crate) fn default_tx() -> TxEnv { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill() +} + +/// The external environment [`transact_tx`] runs with when a test does not need SALT buckets or +/// oracle storage of its own. Equivalent to the empty environment the other helpers use: every +/// bucket reports the minimum capacity and the oracle has no data. +pub(crate) fn default_envs() -> TestExternalEnvs { + TestExternalEnvs::new() +} + +/// The general entry point: an explicit transaction and an explicit external environment. +/// +/// The other helpers in this module fix the transaction to a plain call into [`CONTRACT`]; the +/// shapes that need a different one — EIP-7702 authorizations, system-originated callers, direct +/// calls into a system contract — build their own `TxEnv` and come through here. `envs` is borrowed +/// so a test can read back what execution recorded into it (oracle hints, for instance). +pub(crate) fn transact_tx( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + tx: TxEnv, + envs: &TestExternalEnvs, +) -> Outcome { + let mut context = MegaContext::new(&mut db, spec) + .with_external_envs(envs.into()) + .with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); + let (usage, detained_compute_gas_limit) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) + }; + let gas_used = result.result.tx_gas_used(); + Outcome { + result: result.result, + compute_gas: usage.compute_gas, + data_size: usage.data_size, + kv_updates: usage.kv_updates, + state_growth: usage.state_growth, + gas_used, + detained_compute_gas_limit, + state: result.state, + } +} + +/// Asserts that two outcomes are indistinguishable: same execution result, same four-dimension +/// usage, same receipt `gas_used`, and the same detained compute-gas limit. +/// +/// This is the precision invariant in assertion form — what a transaction that stays inside every +/// per-tx limit must produce under both accounting models. +pub(crate) fn assert_outcomes_identical(label: &str, r6: &Outcome, r7: &Outcome) { + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "{label}: execution result must be identical; REX6={:?} REX7={:?}", + r6.result, + r7.result + ); + assert_eq!( + r6.compute_gas, r7.compute_gas, + "{label}: compute gas must be identical; REX6={} REX7={}", + r6.compute_gas, r7.compute_gas + ); + assert_eq!( + r6.gas_used, r7.gas_used, + "{label}: receipt gas_used must be identical; REX6={} REX7={}", + r6.gas_used, r7.gas_used + ); + assert_eq!( + (r6.data_size, r6.kv_updates, r6.state_growth), + (r7.data_size, r7.kv_updates, r7.state_growth), + "{label}: the non-compute dimensions must be identical", + ); + assert_eq!( + r6.detained_compute_gas_limit, r7.detained_compute_gas_limit, + "{label}: the detained compute-gas limit must be identical; REX6={} REX7={}", + r6.detained_compute_gas_limit, r7.detained_compute_gas_limit + ); +} + /// [`transact`] with every SALT bucket reporting `bucket_capacity`. /// /// The SALT-scaled storage-gas charges (`SSTORE` set, new account, contract creation) are diff --git a/crates/mega-evm/tests/rex7/interceptor_resume.rs b/crates/mega-evm/tests/rex7/interceptor_resume.rs new file mode 100644 index 00000000..0eafcea8 --- /dev/null +++ b/crates/mega-evm/tests/rex7/interceptor_resume.rs @@ -0,0 +1,421 @@ +//! REX7 checkpoint settlement across a CALL that never runs a child frame. +//! +//! Two call targets return to their caller without an EVM frame ever being created for them: +//! +//! - a **system contract interceptor**, which short-circuits inside `frame_init` and hands back a +//! synthetic `FrameResult` carrying the full forwarded gas; +//! - a **precompile**, which revm executes inside `frame_init` and returns as a result rather than +//! as a frame to run. +//! +//! Both take the CALL checkpoint on the way out and the frame-resume clamp on the way back, but +//! neither runs `AdditionalLimit::before_frame_init` against a real child. That makes them the two +//! places where the caller's segment settlement and the clamp round trip have to work without any +//! child-frame bookkeeping to lean on. +//! +//! What these tests pin: +//! +//! - the caller's open segment is settled **before** the interceptor reads the tracker, so a system +//! contract that reports remaining compute gas reports the same number it reports under +//! per-opcode accounting; +//! - the clamp is restored across the boundary, so the forwarded gas returns intact and the receipt +//! does not depend on how much gas was forwarded; +//! - the caller's window re-opens on resume, so a limit crossing in the segment *after* the return +//! is still stopped at the clamp boundary rather than overshooting to the next checkpoint. + +use crate::common::{ + assert_outcomes_identical, transact, transact_default, Outcome, CALLEE, CALLER, CONTRACT, + ONE_ETH, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IMegaLimitControl, MegaHaltReason, MegaSpecId, LIMIT_CONTROL_ADDRESS, + LIMIT_CONTROL_CODE, +}; +use revm::bytecode::opcode::{CALL, MLOAD, POP, SSTORE, STOP, TIMESTAMP}; + +/// The identity precompile: returns its input unchanged, and is executed inside `frame_init`. +const IDENTITY_PRECOMPILE: Address = address!("0000000000000000000000000000000000000004"); + +/// Slot the contract stores the value it observed through the CALL into. +const OBSERVED_SLOT: u64 = 0x20; +/// Slot a downstream checkpoint writes, so a halt before it is observable in state. +const DOWNSTREAM_SLOT: u64 = 0x21; + +/// Memory offset the CALL's return data lands at, clear of the calldata at `0x00`. +const RET_OFFSET: u64 = 0x40; + +/// Gas forwarded to the call target unless a test varies it. +const FORWARDED_GAS: u64 = 1_000_000; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// Per-spec runtime limits with the TX compute gas limit replaced. +fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) +} + +/// Per-spec runtime limits with the block-environment detention cap replaced. +fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = cap; + limits + } +} + +/// A CALL to `target` forwarding `forwarded_gas`, with `args_size` bytes of calldata taken from +/// `mem[0..]` and 32 bytes of return data written to `mem[RET_OFFSET..]`. +fn call_with_return_data( + builder: BytecodeBuilder, + target: Address, + args_size: u64, + forwarded_gas: u64, +) -> BytecodeBuilder { + builder + .push_number(32u64) // retSize + .push_number(RET_OFFSET) // retOffset + .push_number(args_size) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(forwarded_gas) + .append(CALL) + .append(POP) +} + +/// Everything up to and including the CALL into `MegaLimitControl.remainingComputeGas()`: +/// a plain run, the calldata write, and the CALL itself. +fn interceptor_prefix(prologue_volatile: bool, forwarded_gas: u64) -> BytecodeBuilder { + let mut builder = plain_filler(BytecodeBuilder::default(), 5); + if prologue_volatile { + builder = builder.append(TIMESTAMP).append(POP); + } + let builder = + plain_filler(builder, 5).mstore(0, IMegaLimitControl::remainingComputeGasCall::SELECTOR); + call_with_return_data(builder, LIMIT_CONTROL_ADDRESS, 4, forwarded_gas) +} + +/// Everything up to and including the CALL into the identity precompile. +fn precompile_prefix(prologue_volatile: bool, forwarded_gas: u64) -> BytecodeBuilder { + let mut builder = plain_filler(BytecodeBuilder::default(), 5); + if prologue_volatile { + builder = builder.append(TIMESTAMP).append(POP); + } + let builder = plain_filler(builder, 5).mstore(0, [0x5au8; 32]); + call_with_return_data(builder, IDENTITY_PRECOMPILE, 32, forwarded_gas) +} + +/// Stores the 32 bytes the CALL returned into [`OBSERVED_SLOT`]. +fn store_returned_word(builder: BytecodeBuilder) -> BytecodeBuilder { + builder + .push_number(RET_OFFSET) + .append(MLOAD) + .push_u256(U256::from(OBSERVED_SLOT)) + .append(SSTORE) +} + +/// The compute gas a transaction running `code` uses when nothing constrains it. +fn unconstrained_compute_gas(code: Bytes) -> u64 { + transact_default(MegaSpecId::REX7, base_db(code)).compute_gas +} + +/// Runs `code` under both specs with `limits` and returns `(REX6, REX7)`. +fn run_both( + code: &Bytes, + limits: &impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code.clone()), limits(MegaSpecId::REX7)); + (r6, r7) +} + +/// The interceptor reports the caller's remaining compute gas straight out of the tracker, so the +/// number it returns is a direct readout of how much of the caller's execution had been settled at +/// the moment `frame_init` ran. +/// +/// Under per-opcode accounting every opcode ahead of the CALL is already recorded. Under checkpoint +/// accounting the whole plain segment ahead of it is still open until the CALL's checkpoint +/// prologue settles it — which runs at the CALL opcode, before `frame_init`. If that settlement +/// were deferred (to the resume, or to the frame's tail), the contract would observe more remaining +/// compute gas than REX6 reports. Comparing the stored word is what pins the ordering. +#[test] +fn test_interceptor_observes_the_settled_remaining_compute_gas() { + let code = plain_filler( + store_returned_word(plain_filler(interceptor_prefix(false, FORWARDED_GAS), 10)), + 10, + ) + .append(STOP) + .build(); + let (r6, r7) = run_both(&code, &EvmTxRuntimeLimits::from_spec); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + + let slot = U256::from(OBSERVED_SLOT); + let observed = r7.storage_value(CONTRACT, slot); + assert!(!observed.is_zero(), "the interceptor must have returned a remaining-gas reading"); + assert_eq!( + r6.storage_value(CONTRACT, slot), + observed, + "the interceptor must observe the caller's segment already settled", + ); + assert_outcomes_identical("limit-control interception", &r6, &r7); +} + +/// The same readout while a clamp is outstanding. +/// +/// A detention cap engaged before the CALL leaves the interpreter running on clamped gas through +/// the plain segment ahead of it. The CALL checkpoint has to restore the hidden gas before +/// `frame_init` runs, or the interceptor and the forwarding math would both be computed against a +/// counter missing the hidden remainder. +#[test] +fn test_interceptor_observes_the_same_reading_under_an_active_clamp() { + let code = store_returned_word(plain_filler(interceptor_prefix(true, FORWARDED_GAS), 10)) + .append(STOP) + .build(); + // Well above what this transaction spends, so detention is engaged but never binding. + let (r6, r7) = run_both(&code, &detention_cap(1_000_000)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + + let slot = U256::from(OBSERVED_SLOT); + let observed = r7.storage_value(CONTRACT, slot); + assert!(!observed.is_zero(), "the interceptor must have returned a remaining-gas reading"); + assert_eq!( + r6.storage_value(CONTRACT, slot), + observed, + "an outstanding clamp must not change what the interceptor observes", + ); + assert_outcomes_identical("limit-control interception under a clamp", &r6, &r7); +} + +/// Gas leakage path 1 — the system contract interception short-circuit. +/// +/// The synthetic result carries `Gas::new(call_inputs.gas_limit)`: nothing is spent, so every gas +/// unit forwarded comes back. The receipt is therefore independent of the forwarded amount, and +/// that independence is what catches a clamp leak — if the clamp were still outstanding across +/// `frame_init`, or if the resume restored the forwarded amount rather than the hidden one, the two +/// runs below would not cost the same. +#[test] +fn test_interception_returns_the_forwarded_gas_regardless_of_the_amount() { + let build = |forwarded| { + store_returned_word(plain_filler(interceptor_prefix(true, forwarded), 10)) + .append(STOP) + .build() + }; + // Two PUSH3 operands, so the programs are byte-for-byte the same length and the only difference + // is how much gas the interceptor is handed. + let small = build(0x10_0000u64); + let large = build(0x50_0000u64); + assert_eq!(small.len(), large.len(), "the two programs must differ only in the operand"); + // A detention cap nowhere near binding, so a clamp is outstanding at the CALL in both REX7 + // arms. + let limits = detention_cap(2_000_000); + + let (small6, small7) = run_both(&small, &limits); + let (large6, large7) = run_both(&large, &limits); + + for (label, r) in [ + ("small REX6", &small6), + ("small REX7", &small7), + ("large REX6", &large6), + ("large REX7", &large7), + ] { + assert!(r.is_success(), "{label}: must succeed: {:?}", r.result); + } + assert_eq!( + small7.gas_used, large7.gas_used, + "REX7: the interception must return the forwarded gas intact; 1M forwarded={} 5M \ + forwarded={}", + small7.gas_used, large7.gas_used + ); + assert_eq!( + small6.gas_used, large6.gas_used, + "REX6: same invariant, as the baseline the REX7 arm has to reproduce", + ); + assert_outcomes_identical("1M forwarded to the interceptor", &small6, &small7); + assert_outcomes_identical("5M forwarded to the interceptor", &large6, &large7); +} + +/// Enforcement after an interceptor resume: the caller's settlement window re-opens at the resume, +/// so a crossing in the segment that follows is stopped at the clamp boundary. +/// +/// The limit is placed inside the tail plain run, after the CALL has already returned. REX6 +/// executes the crossing opcode and records it, so its usage ends up over the limit; REX7 stops +/// exactly at the limit and the downstream SSTORE never runs. +#[test] +fn test_crossing_after_an_interceptor_resume_stops_at_the_clamp_boundary() { + let tail_pairs = 200; + let code = plain_filler(interceptor_prefix(false, FORWARDED_GAS), tail_pairs) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + // The same program truncated at the resume, and at the end of the tail: the crossing goes + // halfway between them, which is inside the tail and clear of both checkpoints. + let at_resume = + unconstrained_compute_gas(interceptor_prefix(false, FORWARDED_GAS).append(STOP).build()); + let after_tail = unconstrained_compute_gas( + plain_filler(interceptor_prefix(false, FORWARDED_GAS), tail_pairs).append(STOP).build(), + ); + assert!(after_tail > at_resume, "the tail must cost something; {at_resume} -> {after_tail}"); + let limit = at_resume + (after_tail - at_resume) / 2; + + let (r6, r7) = run_both(&code, &compute_limit(limit)); + + assert!(!r6.is_success(), "REX6 must halt on the tight compute limit: {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must halt on the tight compute limit: {:?}", r7.result); + assert!( + r6.compute_gas > limit, + "REX6 records the crossing opcode before halting; compute={} limit={limit}", + r6.compute_gas + ); + assert_eq!( + r7.compute_gas, limit, + "REX7 must stop exactly at the limit in the segment opened by the resume", + ); + // The top-level frame's compute budget equals the TX-level remaining, and the clamp breaks that + // tie towards the TX-level constraint, so a REX7 crossing reports the compute-gas limit. + // (REX6's per-opcode check tries the frame budget first and reports the tie as a + // frame-local exceed, which the top-level frame absorbs into a revert — the models classify + // the tie differently.) + assert!( + matches!(r7.halt_reason("REX7"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "REX7: the halt must report the compute-gas limit; got {:?}", + r7.halt_reason("REX7"), + ); + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.storage_value(CONTRACT, U256::from(DOWNSTREAM_SLOT)).is_zero(), + "{label}: the stop lands inside the tail, so the SSTORE after it never runs", + ); + } +} + +/// A precompile is executed inside `frame_init` and returns as a result, so like an interceptor it +/// resumes the caller without a child frame ever running. Unlike an interceptor it does spend gas, +/// so the resume merges a partially consumed budget back into the caller. +#[test] +fn test_precompile_resume_matches_per_opcode_accounting() { + let code = plain_filler( + store_returned_word(plain_filler(precompile_prefix(false, FORWARDED_GAS), 10)), + 10, + ) + .append(STOP) + .build(); + let (r6, r7) = run_both(&code, &EvmTxRuntimeLimits::from_spec); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + assert_eq!( + r7.storage_value(CONTRACT, U256::from(OBSERVED_SLOT)), + U256::from_be_bytes([0x5au8; 32]), + "the identity precompile must have returned its input", + ); + assert_outcomes_identical("identity precompile", &r6, &r7); +} + +/// The precompile resume with a clamp outstanding across the CALL. +#[test] +fn test_precompile_resume_under_an_active_clamp() { + let code = store_returned_word(plain_filler(precompile_prefix(true, FORWARDED_GAS), 10)) + .append(STOP) + .build(); + let (r6, r7) = run_both(&code, &detention_cap(1_000_000)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + assert_outcomes_identical("identity precompile under a clamp", &r6, &r7); +} + +/// Enforcement after a precompile resume, the counterpart of the interceptor case: the precompile +/// consumed part of the forwarded gas, so the resume re-clamps against a counter the child moved. +#[test] +fn test_crossing_after_a_precompile_resume_stops_at_the_clamp_boundary() { + let tail_pairs = 200; + let code = plain_filler(precompile_prefix(false, FORWARDED_GAS), tail_pairs) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + let at_resume = + unconstrained_compute_gas(precompile_prefix(false, FORWARDED_GAS).append(STOP).build()); + let after_tail = unconstrained_compute_gas( + plain_filler(precompile_prefix(false, FORWARDED_GAS), tail_pairs).append(STOP).build(), + ); + let limit = at_resume + (after_tail - at_resume) / 2; + + let (r6, r7) = run_both(&code, &compute_limit(limit)); + + assert!(!r6.is_success(), "REX6 must halt on the tight compute limit: {:?}", r6.result); + assert!(!r7.is_success(), "REX7 must halt on the tight compute limit: {:?}", r7.result); + assert!( + r6.compute_gas > limit, + "REX6 records the crossing opcode before halting; compute={} limit={limit}", + r6.compute_gas + ); + assert_eq!( + r7.compute_gas, limit, + "REX7 must stop exactly at the limit in the segment opened by the resume", + ); + assert!( + r7.storage_value(CONTRACT, U256::from(DOWNSTREAM_SLOT)).is_zero(), + "the halt lands inside the tail, so the SSTORE after it never runs", + ); +} + +/// The interception resume one frame down: the caller is a sub-frame, so the settlement and the +/// re-clamp happen against a frame-local compute budget rather than the TX-level remaining. +#[test] +fn test_nested_frame_interceptor_resume_matches_per_opcode_accounting() { + let callee = + plain_filler(store_returned_word(plain_filler(interceptor_prefix(false, 200_000), 10)), 10) + .append(STOP) + .build(); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(2_000_000u64) // gas + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let r6 = transact_default(MegaSpecId::REX6, build_db()); + let r7 = transact_default(MegaSpecId::REX7, build_db()); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + let slot = U256::from(OBSERVED_SLOT); + assert!( + !r7.storage_value(CALLEE, slot).is_zero(), + "the nested interception must have returned a remaining-gas reading", + ); + assert_eq!( + r6.storage_value(CALLEE, slot), + r7.storage_value(CALLEE, slot), + "a nested caller must observe the same settled remaining compute gas", + ); + assert_outcomes_identical("nested interception", &r6, &r7); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index edd2739c..03364b24 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -7,5 +7,6 @@ mod checkpoint_settlement; mod common; +mod interceptor_resume; mod modexp_gas; mod v0_clamp; From 40d7ce93bacce09cc311d1aa981f9edecdd9279c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:15:36 +0800 Subject: [PATCH 009/208] test(rex7): pin where a latched non-compute exceed surfaces --- crates/mega-evm/tests/rex7/latch_surfacing.rs | 331 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 332 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/latch_surfacing.rs diff --git a/crates/mega-evm/tests/rex7/latch_surfacing.rs b/crates/mega-evm/tests/rex7/latch_surfacing.rs new file mode 100644 index 00000000..6fc8bad7 --- /dev/null +++ b/crates/mega-evm/tests/rex7/latch_surfacing.rs @@ -0,0 +1,331 @@ +//! REX7: where a latched non-compute limit exceed surfaces. +//! +//! Only the compute dimension is checked on the hot path. The other three — data size, KV updates, +//! state growth — are recorded at their own mutation sites, and each site latches the exceed into +//! `has_exceeded_limit` itself. The latch becomes a stop at the next position that consults it, +//! which under per-opcode accounting is the next metered opcode and under checkpoint accounting is +//! the next checkpoint. +//! +//! Every non-compute mutation site reachable from bytecode is *itself* a checkpoint (SSTORE, the +//! LOG family, SELFDESTRUCT, the CALL / CREATE family), and each of those settles its own body +//! after the mutation has run. The two surfacing rules therefore land on the same opcode, and these +//! tests pin that they do — by construction, not by coincidence: +//! +//! - the recorded compute gas is exactly what a run truncated at the mutation site records, so the +//! plain segment *after* the site never executed; +//! - the checkpoint downstream of that segment never ran, which its absent storage write shows; +//! - the reported dimension, limit and usage are identical to per-opcode accounting. +//! +//! The oracle-hint case at the bottom covers the one non-compute mutation site that is not an +//! opcode: it records inside `frame_init`, one step past the CALL checkpoint that settled the +//! caller's segment. + +use crate::common::{ + assert_outcomes_identical, transact, transact_default, transact_tx, Outcome, CALLER, CONTRACT, + DEFAULT_TX_GAS_LIMIT, ONE_ETH, +}; +use alloy_primitives::{Bytes, B256, U256}; +use alloy_sol_types::{SolCall as _, SolError as _}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IOracle, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, + TestExternalEnvs, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2, +}; +use revm::{ + bytecode::opcode::{CALL, LOG1, POP, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, +}; + +/// Slot written by the checkpoint downstream of the latching site; its absence proves the stop +/// landed at the site and not after it. +const DOWNSTREAM_SLOT: u64 = 0x31; +/// Slot written by the latching SSTORE itself. +const LATCHING_SLOT: u64 = 0x30; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// The plain segment placed between the latching site and the checkpoint downstream of it. Long +/// enough that including it in the recorded compute gas would be unmistakable. +const GAP_PAIRS: usize = 40; + +/// The dimension a failed transaction blamed, read out of whichever failure shape it produced. +/// +/// A TX-level exceed halts and carries the dimension in the halt reason; a frame-local exceed is +/// absorbed into a revert carrying `MegaLimitExceeded(uint8 kind, uint64 limit)`. Both are in +/// scope here, since which one a given limit produces is not what these tests are about. +fn blamed_dimension(label: &str, outcome: &Outcome) -> LimitKind { + match &outcome.result { + ExecutionResult::Halt { reason, .. } => match reason { + MegaHaltReason::DataLimitExceeded { .. } => LimitKind::DataSize, + MegaHaltReason::KVUpdateLimitExceeded { .. } => LimitKind::KVUpdate, + MegaHaltReason::ComputeGasLimitExceeded { .. } => LimitKind::ComputeGas, + MegaHaltReason::StateGrowthLimitExceeded { .. } => LimitKind::StateGrowth, + other => panic!("{label}: not a limit halt: {other:?}"), + }, + ExecutionResult::Revert { output, .. } => { + let decoded = MegaLimitExceeded::abi_decode(output) + .unwrap_or_else(|e| panic!("{label}: revert data is not MegaLimitExceeded: {e}")); + LimitKind::from_u8(decoded.kind) + .unwrap_or_else(|| panic!("{label}: unknown limit kind {}", decoded.kind)) + } + other => panic!("{label}: expected a limit failure, got {other:?}"), + } +} + +/// Runs `code` under both specs with `limits` and returns `(REX6, REX7)`. +fn run_both( + code: &Bytes, + limits: &impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code.clone()), limits(MegaSpecId::REX7)); + (r6, r7) +} + +/// The shared body of the three per-dimension cases. +/// +/// `full` runs the latching site, a plain gap, and a downstream SSTORE. `truncated` is the same +/// program cut off immediately after the latching site. Asserting the failing run's compute gas +/// against the truncated run's is what pins the stop to the site: the gap contributes nothing. +fn assert_stops_at_the_latching_site( + label: &str, + full: Bytes, + truncated: Bytes, + expected: LimitKind, + limits: impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let at_site = transact_default(MegaSpecId::REX7, base_db(truncated)).compute_gas; + let (r6, r7) = run_both(&full, &limits); + + for (spec, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + !r.is_success(), + "{label}/{spec}: the tight limit must stop the tx: {:?}", + r.result + ); + assert_eq!( + blamed_dimension(&format!("{label}/{spec}"), r), + expected, + "{label}/{spec}: the wrong dimension was blamed", + ); + assert!( + r.storage_value(CONTRACT, U256::from(DOWNSTREAM_SLOT)).is_zero(), + "{label}/{spec}: the checkpoint downstream of the gap must never run", + ); + } + assert_eq!( + r7.compute_gas, at_site, + "{label}: REX7 must stop at the latching site — the plain gap after it must contribute no \ + compute gas; stopped at {} vs {at_site} recorded up to the site", + r7.compute_gas + ); + assert_outcomes_identical(label, &r6, &r7); + (r6, r7) +} + +/// Data size: `on_log` records the log's topics and payload, then latches. LOG1 is a checkpoint, so +/// its own trailing settlement surfaces the latch on the spot. +#[test] +fn test_data_size_latch_stops_at_the_log_that_recorded_it() { + let log = |builder: BytecodeBuilder| { + builder + .mstore(0, [0x11u8; 32]) + .push_number(0xabcu64) // topic0 + .push_number(32u64) // len + .push_number(0u64) // offset + .append(LOG1) + }; + let truncated = log(plain_filler(BytecodeBuilder::default(), 10)).append(STOP).build(); + let full = plain_filler(log(plain_filler(BytecodeBuilder::default(), 10)), GAP_PAIRS) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + + // One byte under what the log needs, so the log's own recording is what overflows. + let before = transact_default(MegaSpecId::REX7, base_db(truncated.clone())); + let intrinsic = transact_default( + MegaSpecId::REX7, + base_db(BytecodeBuilder::default().append(STOP).build()), + ) + .data_size; + assert!( + intrinsic < before.data_size, + "the log must be what pushes data size past the intrinsic footprint; {intrinsic} vs {}", + before.data_size + ); + let limit = before.data_size - 1; + + assert_stops_at_the_latching_site( + "data size / LOG1", + full, + truncated, + LimitKind::DataSize, + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_data_size_limit(limit), + ); +} + +/// KV updates: `on_sstore` records the storage write, then latches. SSTORE is a checkpoint, so the +/// stop lands on it. +#[test] +fn test_kv_update_latch_stops_at_the_sstore_that_recorded_it() { + let truncated = plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(LATCHING_SLOT), U256::from(0x77)) + .append(STOP) + .build(); + let full = plain_filler( + plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(LATCHING_SLOT), U256::from(0x77)), + GAP_PAIRS, + ) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + + let before = transact_default(MegaSpecId::REX7, base_db(truncated.clone())); + let intrinsic = transact_default( + MegaSpecId::REX7, + base_db(BytecodeBuilder::default().append(STOP).build()), + ) + .kv_updates; + assert!( + intrinsic < before.kv_updates, + "the store must be what pushes KV updates past the intrinsic footprint; {intrinsic} vs {}", + before.kv_updates + ); + let limit = before.kv_updates - 1; + + let (_, r7) = assert_stops_at_the_latching_site( + "KV updates / SSTORE", + full, + truncated, + LimitKind::KVUpdate, + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_kv_updates_limit(limit), + ); + // The store's KV usage is frame-discardable, so popping the stopped frame takes it back out + // again — the post-transaction reading is the intrinsic footprint, under the limit that the + // store transiently crossed. + assert!( + r7.kv_updates <= limit, + "the stopped frame's KV usage must be discarded; usage={} limit={limit}", + r7.kv_updates + ); +} + +/// State growth: the same SSTORE records a net-new storage slot. With only the state-growth limit +/// tightened, that is the dimension `check_limit` reports. +#[test] +fn test_state_growth_latch_stops_at_the_sstore_that_recorded_it() { + let truncated = plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(LATCHING_SLOT), U256::from(0x77)) + .append(STOP) + .build(); + let full = plain_filler( + plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(LATCHING_SLOT), U256::from(0x77)), + GAP_PAIRS, + ) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + + let before = transact_default(MegaSpecId::REX7, base_db(truncated.clone())); + assert!(before.state_growth > 0, "the store must create a net-new slot"); + let limit = before.state_growth - 1; + + assert_stops_at_the_latching_site( + "state growth / SSTORE", + full, + truncated, + LimitKind::StateGrowth, + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_state_growth_limit(limit), + ); +} + +/// The one non-compute mutation site that is not an opcode: the oracle-hint interceptor meters the +/// payload inside `frame_init`, one step past the CALL checkpoint. +/// +/// On overflow the interceptor deliberately synthesizes nothing and returns `None`, leaving +/// `before_frame_init` to produce the canonical TX-level halt. Under checkpoint accounting the +/// caller's segment was already settled by the CALL's own checkpoint, which runs before +/// `frame_init` — so the halt reports the same usage REX6 reports, and the caller's plain segment +/// ahead of the CALL is fully accounted for despite the frame never starting. +#[test] +fn test_oracle_hint_data_size_latch_halts_at_the_frame_boundary() { + let payload = vec![0u8; 256]; + let calldata = + IOracle::sendHintCall { topic: B256::repeat_byte(0x5a), data: payload.into() }.abi_encode(); + let len = calldata.len() as u64; + let mut builder = plain_filler(BytecodeBuilder::default(), 20); + builder = builder.mstore(0, &calldata); + let code = builder + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(len) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(ORACLE_CONTRACT_ADDRESS) + .push_number(1_000_000u64) // gas + .append(CALL) + .append(POP) + .sstore(U256::from(DOWNSTREAM_SLOT), U256::from(1)) + .append(STOP) + .build(); + let build_db = + || base_db(code.clone()).account_code(ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2); + + // Enough for the calldata footprint but not for the hint payload the interceptor meters on top. + let unconstrained = transact_default(MegaSpecId::REX7, build_db()); + assert!( + unconstrained.is_success(), + "the unconstrained run must succeed: {:?}", + unconstrained.result + ); + let limit = unconstrained.data_size - len; + let limits = move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_data_size_limit(limit); + + let envs = TestExternalEnvs::new(); + let tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill() + }; + let r6 = transact_tx(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6), tx(), &envs); + let hints_after_rex6 = envs.recorded_hints().len(); + let r7 = transact_tx(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7), tx(), &envs); + let hints_after_rex7 = envs.recorded_hints().len(); + + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!(!r.is_success(), "{label}: the tight data-size limit must halt: {:?}", r.result); + assert_eq!( + blamed_dimension(label, r), + LimitKind::DataSize, + "{label}: the halt must blame data size", + ); + assert!( + r.storage_value(CONTRACT, U256::from(DOWNSTREAM_SLOT)).is_zero(), + "{label}: the store after the CALL must never run", + ); + } + assert_eq!( + (hints_after_rex6, hints_after_rex7), + (0, 0), + "an over-budget hint must never reach the oracle backend under either spec", + ); + assert_outcomes_identical("oracle hint data-size overflow", &r6, &r7); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 03364b24..26f9f39f 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -8,5 +8,6 @@ mod checkpoint_settlement; mod common; mod interceptor_resume; +mod latch_surfacing; mod modexp_gas; mod v0_clamp; From 4f7e6ddf904ee8ac7cfe4a788f5b43082df686d2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:18:35 +0800 Subject: [PATCH 010/208] test(rex7): cover the three gas-leakage paths under an active clamp --- crates/mega-evm/tests/rex7/gas_leakage.rs | 397 ++++++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 398 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/gas_leakage.rs diff --git a/crates/mega-evm/tests/rex7/gas_leakage.rs b/crates/mega-evm/tests/rex7/gas_leakage.rs new file mode 100644 index 00000000..7cb35001 --- /dev/null +++ b/crates/mega-evm/tests/rex7/gas_leakage.rs @@ -0,0 +1,397 @@ +//! REX7: the three gas-leakage paths, exercised with a clamp outstanding. +//! +//! Any mechanism that hides, grants or adjusts gas per frame has to be unwound on every way out of +//! a frame, or system-held gas leaks back to the parent or the sender. The V0 clamp is such a +//! mechanism — it hides part of the interpreter's gas — and the three paths that have to handle it +//! are the ones the leakage checklist names: +//! +//! 1. **System contract interception** short-circuits `frame_init` and synthesizes a result with no +//! child frame. The clamp must already be restored when the CALL checkpoint publishes the frame, +//! and the caller's counter must come back whole on resume. +//! 2. **Gas rescue on a TX-level exceed** captures the frame's remaining gas for the sender. It +//! must capture the true remaining — neither the clamped view (which would burn the hidden gas) +//! nor the sum of both (which would refund it twice). +//! 3. **Frame return** hands the frame's gas back to its parent. The restore must happen before +//! anything reads or charges that gas, and identically on success, on revert and on a limit +//! exceed. +//! +//! The probes are chosen so a leak changes an observable, not just an internal: the parent's own +//! `GAS` reading after the child returns, the receipt's independence from the transaction gas +//! limit, and whether a code deposit that costs more than the clamp left visible can be paid at +//! all. + +use crate::common::{ + assert_outcomes_identical, transact, transact_with_gas_limit, Outcome, CALLEE, CALLER, + CONTRACT, ONE_ETH, +}; +use alloy_primitives::{Bytes, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IMegaLimitControl, MegaHaltReason, MegaSpecId, LIMIT_CONTROL_ADDRESS, + LIMIT_CONTROL_CODE, +}; +use revm::bytecode::opcode::{ + CALL, CREATE, DUP1, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, + TIMESTAMP, +}; + +/// Slot the caller stores its post-return `GAS` reading into. +const GAS_READING_SLOT: u64 = 0x40; +/// Slot a callee writes, so a committed sub-frame can be told from a reverted one. +const CALLEE_SLOT: u64 = 0x41; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body. +fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// Per-spec runtime limits with the TX compute gas limit replaced. +fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) +} + +/// Per-spec runtime limits with the block-environment detention cap replaced. +fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = cap; + limits + } +} + +/// A CALL to `target` forwarding `gas`, with no arguments and no return data, followed by the +/// caller reading `GAS` and storing it. +/// +/// The stored reading is the probe: it is the caller's own view of its counter after the child's +/// gas has been merged back, so any gas the child failed to hand back — or handed back twice — +/// shows up in it. +fn call_then_store_gas(target: revm::primitives::Address, forwarded: u64) -> Bytes { + plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(forwarded) + .append(CALL) + .append(POP) + .append(GAS) + .push_u256(U256::from(GAS_READING_SLOT)) + .append(SSTORE) + .append(STOP) + .build() +} + +/// Runs `code` under both specs with `limits` and returns `(REX6, REX7)`. +fn run_both( + build_db: impl Fn() -> MemoryDatabase, + limits: &impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + (r6, r7) +} + +/// Asserts both arms succeeded and read back the same post-return `GAS` value. +fn assert_same_gas_reading(label: &str, r6: &Outcome, r7: &Outcome) { + assert!(r6.is_success(), "{label}/REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "{label}/REX7 must succeed: {:?}", r7.result); + let slot = U256::from(GAS_READING_SLOT); + let reading = r7.storage_value(CONTRACT, slot); + assert!(!reading.is_zero(), "{label}: the GAS reading must be non-zero"); + assert_eq!( + r6.storage_value(CONTRACT, slot), + reading, + "{label}: the caller's counter after the return must match per-opcode accounting", + ); + assert_outcomes_identical(label, r6, r7); +} + +/// Leakage path 1 — the interception short-circuit, probed from the caller's own counter. +/// +/// The interceptor produces a synthetic result without a child frame ever existing, so nothing on +/// that path unwinds a clamp. The clamp therefore has to be already restored when the CALL +/// checkpoint publishes the frame, and re-applied only once the caller resumes. Reading `GAS` right +/// after the CALL is the caller's direct view of that: a clamp that survived into `frame_init`, or +/// one restored twice, moves this reading. +#[test] +fn test_interception_short_circuit_leaves_the_callers_counter_whole() { + let code = plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .mstore(0, IMegaLimitControl::remainingComputeGasCall::SELECTOR) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(4u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(LIMIT_CONTROL_ADDRESS) + .push_number(1_000_000u64) + .append(CALL) + .append(POP) + .append(GAS) + .push_u256(U256::from(GAS_READING_SLOT)) + .append(SSTORE) + .append(STOP) + .build(); + let (r6, r7) = run_both(|| base_db(code.clone()), &detention_cap(1_000_000)); + assert_same_gas_reading("interception short-circuit", &r6, &r7); +} + +/// Leakage path 2 — the TX-level rescue, probed by varying the transaction gas limit. +/// +/// A TX-level compute exceed stops at the compute limit, so how much EVM gas the transaction was +/// given cannot change what it consumed. The clamp-hidden amount, on the other hand, is exactly +/// `gas_limit − headroom` and moves one-for-one with the gas limit — so if the rescue captured the +/// clamped view (burning the hidden gas) or the true remaining plus the hidden amount (refunding it +/// twice), the receipt would track the gas limit. It must not. +#[test] +fn test_tx_level_rescue_is_independent_of_the_transaction_gas_limit() { + let code = countdown_loop_code(&[], 10_000); + let intrinsic = transact( + MegaSpecId::REX7, + base_db(BytecodeBuilder::default().append(STOP).build()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + ) + .compute_gas; + let limit = intrinsic + 5_000; + let limits = compute_limit(limit); + + let mut readings = Vec::new(); + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + let small = transact_with_gas_limit(spec, base_db(code.clone()), limits(spec), 1_000_000); + let large = transact_with_gas_limit(spec, base_db(code.clone()), limits(spec), 50_000_000); + assert!(!small.is_success(), "{spec:?}: the tight compute limit must stop the tx"); + assert!(!large.is_success(), "{spec:?}: the tight compute limit must stop the tx"); + assert_eq!( + small.compute_gas, large.compute_gas, + "{spec:?}: the stop point must not depend on the transaction gas limit", + ); + assert_eq!( + small.gas_used, large.gas_used, + "{spec:?}: the rescued gas must be the true remaining, so the receipt cannot track the \ + transaction gas limit; 1M limit -> {} and 50M limit -> {}", + small.gas_used, large.gas_used + ); + readings.push((small.compute_gas, small.gas_used)); + } + let (r7_compute, r7_gas_used) = readings[1]; + assert_eq!(r7_compute, limit, "REX7 stops exactly at the compute limit"); + assert!( + r7_gas_used < 1_000_000, + "the clamp-hidden gas must reach the sender, not the burn; gas_used={r7_gas_used}", + ); +} + +/// The same rescue probe with gas detention as the binding constraint, so the reclassification path +/// (`VolatileDataAccessOutOfGas` on a clamp-latched detention exceed) is the one under test. +#[test] +fn test_detained_rescue_is_independent_of_the_transaction_gas_limit() { + let code = countdown_loop_code(&[TIMESTAMP, POP], 10_000); + let limits = detention_cap(1_000); + + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + let small = transact_with_gas_limit(spec, base_db(code.clone()), limits(spec), 1_000_000); + let large = transact_with_gas_limit(spec, base_db(code.clone()), limits(spec), 50_000_000); + for (label, r) in [("1M", &small), ("50M", &large)] { + assert!(!r.is_success(), "{spec:?}/{label}: the detention cap must stop the tx"); + assert!( + matches!(r.halt_reason(label), MegaHaltReason::VolatileDataAccessOutOfGas { .. }), + "{spec:?}/{label}: the halt must keep the volatile attribution; got {:?}", + r.halt_reason(label), + ); + } + assert_eq!( + small.gas_used, large.gas_used, + "{spec:?}: a detained stop must rescue the true remaining, so the receipt cannot track \ + the transaction gas limit; 1M limit -> {} and 50M limit -> {}", + small.gas_used, large.gas_used + ); + } +} + +/// Leakage path 3 — frame return, on the success arm. +/// +/// The callee ends inside a plain segment, so its clamp is still outstanding when the frame +/// produces its result. Restoring it there is what lets the unspent remainder flow back to the +/// caller; the caller's `GAS` reading is what shows whether it did. +#[test] +fn test_frame_return_restores_the_clamp_on_success() { + let callee = plain_filler(BytecodeBuilder::default(), 20).append(STOP).build(); + let code = call_then_store_gas(CALLEE, 1_000_000); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let (r6, r7) = run_both(build_db, &detention_cap(1_000_000)); + assert_same_gas_reading("frame return / success", &r6, &r7); +} + +/// The same probe on the revert arm: the unwinding has to be unconditional, or one of the two exit +/// paths leaks. +#[test] +fn test_frame_return_restores_the_clamp_on_revert() { + let callee = plain_filler(BytecodeBuilder::default(), 20) + .sstore(U256::from(CALLEE_SLOT), U256::from(0x77)) + .revert() + .build(); + let code = call_then_store_gas(CALLEE, 1_000_000); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let (r6, r7) = run_both(build_db, &detention_cap(1_000_000)); + assert_same_gas_reading("frame return / revert", &r6, &r7); + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.storage_value(CALLEE, U256::from(CALLEE_SLOT)).is_zero(), + "{label}: the reverted sub-frame's write must be discarded", + ); + } +} + +/// Frame return on the exceed arm: the callee outruns its own frame-local compute budget, so its +/// clamp turns into a fake out-of-gas that is restored and reclassified into a revert. The gas the +/// clamp was hiding still belongs to the caller. +/// +/// The two models stop the callee at different points — REX7 stops the crossing opcode before it +/// runs — so the caller resumes with different amounts and the readings are not comparable. What is +/// comparable is that the caller survives, sees a failed CALL, and is left with gas of the right +/// order rather than a burned or doubled counter. +#[test] +fn test_frame_local_exceed_returns_the_hidden_gas_to_the_parent() { + let prologue = + BytecodeBuilder::default().sstore(U256::from(CALLEE_SLOT), U256::from(0x77)).build_vec(); + let callee = countdown_loop_code(&prologue, 10_000); + let code = plain_filler(BytecodeBuilder::default(), 5) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000_000u64) + .append(CALL) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let intrinsic = transact( + MegaSpecId::REX7, + base_db(BytecodeBuilder::default().append(STOP).build()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + ) + .compute_gas; + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let (r6, r7) = run_both(build_db, &compute_limit(intrinsic + 100_000)); + + for (label, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.is_success(), + "{label}: the caller survives a frame-local exceed: {:?}", + r.result + ); + assert_eq!( + r.result.output().map(|o| U256::from_be_slice(o)), + Some(U256::ZERO), + "{label}: the CALL must report failure", + ); + assert!( + r.storage_value(CALLEE, U256::from(CALLEE_SLOT)).is_zero(), + "{label}: the reverted sub-frame's write must be discarded", + ); + } + // The caller forwarded (63/64 of) tens of millions of gas and got a failed call back. Only the + // callee's actual work may be gone: a clamp that was not restored would have stranded the + // hidden millions in the child. + assert!( + r7.gas_used < 1_000_000, + "the gas the clamp hid inside the callee must return to the caller; gas_used={}", + r7.gas_used + ); + assert!( + r7.gas_used < r6.gas_used + 100_000, + "REX7 must not consume materially more than per-opcode accounting; REX6={} REX7={}", + r6.gas_used, + r7.gas_used + ); +} + +/// Frame return, ordering arm: the clamp must be restored *before* the code-deposit charge. +/// +/// The deposit costs `CODEDEPOSIT_STORAGE_GAS` (10,000) per byte of deployed code, while the +/// compute it records is 200 per byte. Sizing the detention cap between the two — comfortably above +/// the deposit's compute, far below its EVM gas — leaves a CREATE that can only be paid for out of +/// the counter the clamp was hiding. If the charge saw the clamped copy, this deployment would fail +/// out of gas despite the transaction being nowhere near any limit. +#[test] +fn test_create_return_restores_the_clamp_before_the_code_deposit_charge() { + let runtime = vec![STOP; 100]; + let initcode = BytecodeBuilder::default().return_with_data(&runtime).build_vec(); + let len = initcode.len() as u64; + let code = plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .mstore(0, &initcode) + .push_number(len) // length + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + // 100 bytes of runtime code: 20,000 compute for the deposit, 1,000,000 EVM gas for it. + let deposit_compute = runtime.len() as u64 * 200; + let deposit_evm_gas = runtime.len() as u64 * 10_000; + let cap = 80_000; + assert!( + deposit_compute < cap && cap < deposit_evm_gas, + "the cap has to sit between the deposit's compute and its EVM gas", + ); + + let (r6, r7) = run_both(|| base_db(code.clone()), &detention_cap(cap)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + let created = + r7.result.output().map(|o| U256::from_be_slice(o)).expect("CREATE must return output"); + assert!(!created.is_zero(), "the CREATE must have succeeded; a zero address means it OOG'd"); + assert_eq!( + r6.result.output().map(|o| U256::from_be_slice(o)), + Some(created), + "both models must deploy to the same address", + ); + assert_outcomes_identical("CREATE under a clamp", &r6, &r7); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 26f9f39f..499b0e84 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -7,6 +7,7 @@ mod checkpoint_settlement; mod common; +mod gas_leakage; mod interceptor_resume; mod latch_surfacing; mod modexp_gas; From 4a209944835a561a151e57b119f6745b974bfc80 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:21:48 +0800 Subject: [PATCH 011/208] test(rex7): extend REX6/REX7 parity to system-path transaction shapes --- crates/mega-evm/tests/rex7/common.rs | 9 - crates/mega-evm/tests/rex7/main.rs | 1 + crates/mega-evm/tests/rex7/parity_shapes.rs | 469 ++++++++++++++++++++ 3 files changed, 470 insertions(+), 9 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/parity_shapes.rs diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 066df1de..c3b3d8ed 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -123,15 +123,6 @@ pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) } -/// The transaction shape [`transact`] runs: a plain call from [`CALLER`] to [`CONTRACT`]. -pub(crate) fn default_tx() -> TxEnv { - TxEnvBuilder::default() - .caller(CALLER) - .call(CONTRACT) - .gas_limit(DEFAULT_TX_GAS_LIMIT) - .build_fill() -} - /// The external environment [`transact_tx`] runs with when a test does not need SALT buckets or /// oracle storage of its own. Equivalent to the empty environment the other helpers use: every /// bucket reports the minimum capacity and the oracle has no data. diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 499b0e84..ed499d68 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -11,4 +11,5 @@ mod gas_leakage; mod interceptor_resume; mod latch_surfacing; mod modexp_gas; +mod parity_shapes; mod v0_clamp; diff --git a/crates/mega-evm/tests/rex7/parity_shapes.rs b/crates/mega-evm/tests/rex7/parity_shapes.rs new file mode 100644 index 00000000..7855077a --- /dev/null +++ b/crates/mega-evm/tests/rex7/parity_shapes.rs @@ -0,0 +1,469 @@ +//! REX6 ↔ REX7 bit-for-bit parity on the transaction shapes the settlement suite does not reach. +//! +//! The precision invariant is that a transaction which stays inside every per-tx limit is +//! indistinguishable under the two accounting models. `checkpoint_settlement` establishes that for +//! bytecode shapes reached from a plain call; the shapes here are the ones that enter or leave the +//! interpreter through a different door: +//! +//! - **EIP-7702 authorizations** — accounted in validate / pre-execution, before any frame exists, +//! and able to re-derive the beneficiary detention cap from usage the checkpoint model settles +//! differently; +//! - **`KeylessDeploy`** — a system contract intercepted at depth 0, whose sandbox runs a whole +//! nested transaction and merges its usage back; +//! - **system-originated transactions** — exempt from per-tx metering, which also switches the +//! clamp off entirely, so an exempt transaction must run to completion under a limit that would +//! stop a user transaction; +//! - **the REX5 storage-call stipend** — a per-frame allowance drawn only at the storage-gas +//! surcharge sites, which are exactly the checkpoints; +//! - **oracle hints** — metered from inside `frame_init`, one step past the CALL checkpoint. +//! +//! Every case asserts the full outcome tuple: execution result, compute gas, all four dimensions, +//! receipt `gas_used` and the detained compute-gas limit. + +use std::vec::Vec; + +use crate::common::{ + assert_outcomes_identical, default_envs, transact_tx, Outcome, CALLEE, CALLER, CONTRACT, + DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, +}; +use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, IOracle, MegaSpecId, TestExternalEnvs, + KEYLESS_DEPLOY_ADDRESS, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2, +}; +use revm::{ + bytecode::opcode::{CALL, LOG1, POP, SLOAD, STOP, TIMESTAMP}, + context::{tx::TxEnvBuilder, TxEnv}, +}; + +/// The protocol's own system caller (EIP-4788 / EIP-2935 pre-block calls). A transaction from this +/// address is system-originated, and REX6+ exempts it from per-tx metering. +const PROTOCOL_SYSTEM_CALLER: Address = address!("fffffffffffffffffffffffffffffffffffffffe"); + +/// The address authorizations in this file delegate to. +const DELEGATE: Address = address!("0000000000000000000000000000000000330001"); +/// An authority that already exists in state. +const EXISTING_AUTHORITY: Address = address!("0000000000000000000000000000000000330002"); +/// An authority that does not exist yet, so applying its authorization grows state. +const NEW_AUTHORITY: Address = address!("0000000000000000000000000000000000330003"); + +const KEYLESS_RELAYER: Address = address!("0000000000000000000000000000000000330004"); + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// A mixed body: plain opcodes around one of every checkpoint family that does not need operands +/// from the caller — a storage read, a storage write, a log, and a volatile opcode. +fn mixed_checkpoint_body() -> Bytes { + let builder = plain_filler(BytecodeBuilder::default(), 10) + .append(TIMESTAMP) + .append(POP) + .push_u256(U256::from(3)) + .append(SLOAD) + .append(POP) + .sstore(U256::from(1), U256::from(0x11)); + let builder = plain_filler(builder, 10) + .mstore(0, [0x22u8; 32]) + .push_number(0xabcu64) // topic0 + .push_number(32u64) // len + .push_number(0u64) // offset + .append(LOG1); + plain_filler(builder, 10).append(STOP).build() +} + +/// Runs `tx` under both specs against a freshly built database and asserts the two are +/// indistinguishable. Returns `(REX6, REX7)` for any case-specific assertions on top. +fn assert_parity( + label: &str, + build_db: impl Fn() -> MemoryDatabase, + build_tx: impl Fn() -> TxEnv, + limits: impl Fn(MegaSpecId) -> EvmTxRuntimeLimits, +) -> (Outcome, Outcome) { + let envs6 = default_envs(); + let r6 = + transact_tx(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6), build_tx(), &envs6); + let envs7 = default_envs(); + let r7 = + transact_tx(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7), build_tx(), &envs7); + assert_outcomes_identical(label, &r6, &r7); + (r6, r7) +} + +fn recovered_auth(authority: Address, nonce: u64) -> RecoveredAuthorization { + RecoveredAuthorization::new_unchecked( + Authorization { chain_id: U256::from(1), address: DELEGATE, nonce }, + RecoveredAuthority::Valid(authority), + ) +} + +/// EIP-7702 authorization accounting happens in `validate` / pre-execution — before the first +/// frame, and therefore before any checkpoint exists. It also charges dynamic SALT account-creation +/// gas into the transaction's intrinsic gas, which the first frame's settlement window then has to +/// open on top of. One net-new authority and one existing one exercise both arms of +/// `on_rex6_eip7702_authority_applied`. +#[test] +fn test_eip7702_authorization_accounting_matches_per_opcode() { + let code = mixed_checkpoint_body(); + let build_db = || { + base_db(code.clone()) + .account_balance(EXISTING_AUTHORITY, U256::from(1u64)) + .account_code(DELEGATE, BytecodeBuilder::default().append(STOP).build()) + }; + let build_tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .chain_id(Some(1)) + .authorization_list_recovered(Vec::from([ + recovered_auth(EXISTING_AUTHORITY, 0), + recovered_auth(NEW_AUTHORITY, 0), + ])) + .build_fill() + }; + + let (_, r7) = + assert_parity("EIP-7702 authorizations", build_db, build_tx, EvmTxRuntimeLimits::from_spec); + assert!(r7.is_success(), "the authorized transaction must succeed: {:?}", r7.result); + assert!( + r7.state_growth > 0, + "the net-new authority must register state growth; growth={}", + r7.state_growth + ); +} + +/// The same shape with the authorizations applied under an engaged detention cap. The cap is +/// re-derived from settled usage when an applied authority is the block beneficiary, so this also +/// checks that a cap installed outside any frame lands on the same number under both models. +#[test] +fn test_eip7702_authorization_under_detention_matches_per_opcode() { + let code = mixed_checkpoint_body(); + let build_db = || { + base_db(code.clone()) + .account_balance(EXISTING_AUTHORITY, U256::from(1u64)) + .account_code(DELEGATE, BytecodeBuilder::default().append(STOP).build()) + }; + let build_tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .chain_id(Some(1)) + .authorization_list_recovered(Vec::from([recovered_auth(NEW_AUTHORITY, 0)])) + .build_fill() + }; + let limits = |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = 1_000_000; + limits + }; + + let (_, r7) = assert_parity("EIP-7702 under detention", build_db, build_tx, limits); + assert!(r7.is_success(), "the authorized transaction must succeed: {:?}", r7.result); +} + +/// Builds a deterministic pre-EIP-155 keyless deployment transaction. +fn keyless_tx_bytes(init_code: Bytes) -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: 200_000, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// `KeylessDeploy` is intercepted at depth 0, so the interception happens before any frame — and +/// therefore before any checkpoint — has been created. Its sandbox then runs a whole nested +/// transaction under the same spec, with its own tracker, and merges the usage back. +/// +/// Two accounting models have to agree across all of that: the sandbox's own checkpoint settlement, +/// the merge, and the outer transaction's view of it. +#[test] +fn test_keyless_deploy_sandbox_accounting_matches_per_opcode() { + // Initcode that runs some plain opcodes and a storage write, then deploys a small runtime. + let runtime = BytecodeBuilder::default().append(STOP).build_vec(); + let init_code = plain_filler(BytecodeBuilder::default(), 10) + .sstore(U256::from(7), U256::from(0x99)) + .return_with_data(&runtime) + .build(); + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(init_code), + gasLimitOverride: U256::from(1_000_000u64), + } + .abi_encode(); + + let build_db = + || MemoryDatabase::default().account_balance(KEYLESS_RELAYER, U256::from(10 * ONE_ETH)); + let build_tx = || { + TxEnvBuilder::default() + .caller(KEYLESS_RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(30_000_000) + .chain_id(Some(1)) + .data(Bytes::from(call_data.clone())) + .build_fill() + }; + + let (_, r7) = + assert_parity("keyless deploy sandbox", build_db, build_tx, EvmTxRuntimeLimits::from_spec); + assert!(r7.is_success(), "the keyless deployment must succeed: {:?}", r7.result); + let returns = IKeylessDeploy::keylessDeployCall::abi_decode_returns( + r7.result.output().expect("the interceptor must return data"), + ) + .expect("the output must decode as keylessDeployReturn"); + assert!( + !returns.deployedAddress.is_zero(), + "the sandbox must report a deployed address; errorData={}", + returns.errorData + ); +} + +/// The same keyless deployment under a detention cap engaged by the sandboxed code, so the sandbox +/// runs with a clamp of its own. +#[test] +fn test_keyless_deploy_sandbox_under_detention_matches_per_opcode() { + let runtime = BytecodeBuilder::default().append(STOP).build_vec(); + let init_code = plain_filler(BytecodeBuilder::default(), 5) + .append(TIMESTAMP) + .append(POP) + .return_with_data(&runtime) + .build(); + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(init_code), + gasLimitOverride: U256::from(1_000_000u64), + } + .abi_encode(); + + let build_db = + || MemoryDatabase::default().account_balance(KEYLESS_RELAYER, U256::from(10 * ONE_ETH)); + let build_tx = || { + TxEnvBuilder::default() + .caller(KEYLESS_RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(30_000_000) + .chain_id(Some(1)) + .data(Bytes::from(call_data.clone())) + .build_fill() + }; + let limits = |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = 500_000; + limits + }; + + let (_, r7) = assert_parity("keyless deploy under detention", build_db, build_tx, limits); + assert!(r7.is_success(), "the keyless deployment must succeed: {:?}", r7.result); +} + +/// A system-originated transaction is exempt from per-tx metering, and the exemption also switches +/// the clamp off: `checkpoint_clamp_amount` refuses to hide anything once the tracker is not in the +/// `WithinLimit` state. +/// +/// The compute limit here is far below what the transaction spends, so a user transaction running +/// the same code would be stopped. The exempt one must run to completion under both models — and +/// still report the same compute usage, since the recording continues while only the halt decision +/// is suppressed. +#[test] +fn test_system_originated_transaction_is_unclamped_under_both_models() { + let code = { + let mut code = Vec::new(); + code.extend_from_slice(&[0x61, 0x03, 0xe8]); // PUSH2 1000 + let target = code.len() as u8; + code.extend_from_slice(&[0x5b, 0x60, 0x01, 0x90, 0x03, 0x80, 0x60, target, 0x57, 0x00]); + Bytes::from(code) + }; + let build_db = || { + MemoryDatabase::default() + .account_code(CONTRACT, code.clone()) + .account_balance(PROTOCOL_SYSTEM_CALLER, U256::from(ONE_ETH)) + }; + let build_tx = || { + TxEnvBuilder::default() + .caller(PROTOCOL_SYSTEM_CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .gas_price(0) + .build_fill() + }; + // Well under the loop's cost: binding for a user transaction, ignored for this one. + let limits = |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(25_000); + + let (_, r7) = assert_parity("system-originated exemption", build_db, build_tx, limits); + assert!(r7.is_success(), "an exempt transaction must not be stopped: {:?}", r7.result); + assert!( + r7.compute_gas > 25_000, + "the exempt transaction must have spent past the limit it ignores; compute={}", + r7.compute_gas + ); +} + +/// The REX5 storage-call stipend is a per-frame allowance drawn only at MegaETH's storage-gas +/// surcharge sites — which are exactly the checkpoints. A value-transferring internal CALL into a +/// callee that logs and writes storage draws on it at three of them. +/// +/// Under checkpoint accounting the same sites do the drawing, but the compute they record is now a +/// segment delta rather than a per-opcode capture, so the subtraction of the drawn storage gas has +/// to land on the same number. +#[test] +fn test_storage_call_stipend_allowance_matches_per_opcode() { + // The callee is reached by a value-transferring CALL with no gas of its own beyond the stipend + // revm adds, so its storage work is paid for out of the allowance. + let callee = BytecodeBuilder::default() + .mstore(0, [0x33u8; 32]) + .push_number(0xdefu64) // topic0 + .push_number(32u64) // len + .push_number(0u64) // offset + .append(LOG1) + .append(STOP) + .build(); + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(1u64) // value — arms the stipend + .push_address(CALLEE) + .push_number(0u64) // gas — only the stipend is available + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let build_tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill() + }; + + let (_, r7) = + assert_parity("storage-call stipend", build_db, build_tx, EvmTxRuntimeLimits::from_spec); + assert!(r7.is_success(), "the stipend-funded call must succeed: {:?}", r7.result); + assert_eq!( + r7.result.logs().len(), + 1, + "the callee's log must have been emitted out of the stipend allowance; logs={:?}", + r7.result.logs() + ); +} + +/// The stipend's other arm: a value transfer to an account that does not exist yet, so the +/// new-account materialisation surcharge is what draws on the allowance. +#[test] +fn test_storage_call_stipend_new_account_matches_per_opcode() { + let code = plain_filler(BytecodeBuilder::default(), 10) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(1u64) // value + .push_address(EMPTY_TARGET) + .push_number(0u64) // gas + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill() + }; + + let (_, r7) = assert_parity( + "storage-call stipend / new account", + || base_db(code.clone()), + build_tx, + EvmTxRuntimeLimits::from_spec, + ); + assert!(r7.is_success(), "the value transfer must succeed: {:?}", r7.result); +} + +/// The oracle-hint site on its success arm: the payload is metered into the data-size lane from +/// inside `frame_init`, then forwarded to the backend, then the inner Oracle frame runs. +/// +/// Under checkpoint accounting the caller's segment was settled at the CALL checkpoint one step +/// earlier, so both the metering and the forwarding observe the same state they observe under +/// per-opcode accounting — and the hint that reaches the backend has to be identical. +#[test] +fn test_oracle_hint_forwarding_matches_per_opcode() { + let payload = Bytes::from(vec![0xa5u8; 96]); + let topic = B256::repeat_byte(0x5a); + let calldata = IOracle::sendHintCall { topic, data: payload.clone() }.abi_encode(); + let len = calldata.len() as u64; + let code = plain_filler(BytecodeBuilder::default(), 10) + .mstore(0, &calldata) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(len) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(ORACLE_CONTRACT_ADDRESS) + .push_number(1_000_000u64) // gas + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = + || base_db(code.clone()).account_code(ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2); + let tx = || { + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill() + }; + + let envs6 = TestExternalEnvs::new(); + let r6 = transact_tx( + MegaSpecId::REX6, + build_db(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + tx(), + &envs6, + ); + let envs7 = TestExternalEnvs::new(); + let r7 = transact_tx( + MegaSpecId::REX7, + build_db(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + tx(), + &envs7, + ); + + let hints6 = envs6.recorded_hints(); + let hints7 = envs7.recorded_hints(); + assert_eq!(hints7.len(), 1, "the hint must have reached the backend; got {hints7:?}"); + assert_eq!(hints6, hints7, "the forwarded hint must be identical under both models"); + assert_eq!(hints7[0].data, payload, "the payload must survive intact"); + assert_outcomes_identical("oracle hint forwarding", &r6, &r7); +} From dad83f735b59597caea06c156d46cd036c9eace6 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:27:13 +0800 Subject: [PATCH 012/208] test(rex7): sweep the double-exceed corner across the knife edge --- .../tests/rex7/double_exceed_corner.rs | 315 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 316 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/double_exceed_corner.rs diff --git a/crates/mega-evm/tests/rex7/double_exceed_corner.rs b/crates/mega-evm/tests/rex7/double_exceed_corner.rs new file mode 100644 index 00000000..d62e1ddb --- /dev/null +++ b/crates/mega-evm/tests/rex7/double_exceed_corner.rs @@ -0,0 +1,315 @@ +//! REX7: the double-exceed corner, swept one gas at a time across the knife edge. +//! +//! The corner is the single opcode whose cost outruns *both* the true EVM remaining and the compute +//! headroom. The adjudication is that the compute classification wins: the transaction reports the +//! resource limit and the sender keeps the remaining gas, instead of revm's out-of-gas burning the +//! frame. The reason it can be adjudicated at all is that the two are indistinguishable at the +//! frame boundary — an out-of-gas carries no opcode cost — so there is nothing to tell them apart +//! with. +//! +//! A single case at the corner cannot show that the rule is *stable*: pick the transaction gas +//! limit one gas differently and the crossing opcode may become affordable in true gas while still +//! crossing the compute headroom. These tests calibrate the exact gas limit at which the crossing +//! opcode becomes affordable and sweep ±3 gas around it, asserting that +//! +//! - REX7 reports the same classification on every point of the sweep, and rescues the same amount +//! at every point — the receipt does not notice the edge at all; +//! - the window really does straddle the edge, which the REX6 arm shows by flipping from a burned +//! out-of-gas to a resource stop partway through. +//! +//! The calibration runs a probe truncated just before the crossing opcode and reads two numbers off +//! it: the compute gas recorded there (which fixes where to put the compute limit) and the +//! receipt's `gas_used` (the EVM gas spent there, which fixes the transaction gas limit at which +//! the crossing opcode is exactly affordable). The two are not the same number — MegaETH's +//! intrinsic transaction gas is larger than the intrinsic compute it records — so both have to be +//! measured. + +use crate::common::{ + transact_default, transact_with_gas_limit, Outcome, CALLER, CONTRACT, ONE_ETH, +}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, +}; +use revm::bytecode::opcode::{CALL, MSTORE, POP, RETURN, STOP, TIMESTAMP}; + +/// Memory offset the crossing MSTORE writes to. Far enough out that the expansion dominates the +/// opcode's cost, close enough that the cost stays in the hundreds of gas. +const CROSSING_OFFSET: u64 = 0x2000; + +/// How far either side of the knife edge to sweep. +const SWEEP: i64 = 3; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// The run leading up to the crossing MSTORE: an optional volatile access, a plain segment, and the +/// MSTORE's two stack operands. Everything here is cheap and fully paid for in every sweep point. +fn approach(volatile: bool) -> BytecodeBuilder { + let mut builder = BytecodeBuilder::default(); + if volatile { + builder = builder.append(TIMESTAMP).append(POP); + } + plain_filler(builder, 20).push_number(0u64).push_number(CROSSING_OFFSET) +} + +/// Runs `code` with nothing constraining it, for calibration. +fn unconstrained(code: Bytes) -> Outcome { + let outcome = transact_default(MegaSpecId::REX7, base_db(code)); + assert!(outcome.is_success(), "the calibration run must succeed: {:?}", outcome.result); + outcome +} + +/// The compute gas `code` records when neither EVM gas nor any resource limit constrains it. +fn unconstrained_compute_gas(code: Bytes) -> u64 { + unconstrained(code).compute_gas +} + +/// The calibration a sweep runs against. +struct KnifeEdge { + /// Compute gas recorded up to (not including) the crossing opcode. + compute_before: u64, + /// The crossing opcode's own cost. + cost: u64, + /// The transaction gas limit at which the crossing opcode is exactly affordable. + gas_limit_at_edge: u64, +} + +fn calibrate(volatile: bool) -> KnifeEdge { + let before = unconstrained(approach(volatile).append(STOP).build()); + let after = approach(volatile).append(MSTORE).append(STOP).build(); + let cost = unconstrained_compute_gas(after) - before.compute_gas; + assert!( + cost > 100, + "the crossing opcode must be expensive enough to sweep around; cost={cost}" + ); + KnifeEdge { + compute_before: before.compute_gas, + cost, + gas_limit_at_edge: before.gas_used + cost, + } +} + +/// The TX-level corner: the compute headroom at the MSTORE is half its cost, so the clamp always +/// stops it, while the transaction gas limit sweeps from one gas short of affording it to two gas +/// more than enough. +#[test] +fn test_tx_level_double_exceed_classification_is_stable_across_the_knife_edge() { + let edge = calibrate(false); + let code = approach(false).append(MSTORE).append(STOP).build(); + // Headroom strictly between zero and the opcode's cost: the clamp is outstanding at the MSTORE + // on every sweep point, and the MSTORE never fits inside it. + let limit = edge.compute_before + edge.cost / 2; + let limits = |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit); + + let mut rex7_gas_used = Vec::new(); + let mut rex6_burned = Vec::new(); + for delta in -SWEEP..=SWEEP { + let gas_limit = (edge.gas_limit_at_edge as i64 + delta) as u64; + let label = format!("edge{delta:+}"); + let r6 = transact_with_gas_limit( + MegaSpecId::REX6, + base_db(code.clone()), + limits(MegaSpecId::REX6), + gas_limit, + ); + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(code.clone()), + limits(MegaSpecId::REX7), + gas_limit, + ); + + assert!(!r7.is_success(), "{label}/REX7 must stop: {:?}", r7.result); + assert!( + matches!(r7.halt_reason(&label), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "{label}: REX7 must classify the corner as a compute exceed on every sweep point; got \ + {:?}", + r7.halt_reason(&label), + ); + // The crossing opcode never ran, so its cost is not in the usage: the recorded total sits + // at or under the limit, and within one crossing-opcode cost of it — the headroom the + // opcode could not pay for is what the frame leaves unspent. + assert!( + r7.compute_gas <= limit && r7.compute_gas + edge.cost > limit, + "{label}: REX7 must stop at the clamp boundary; compute={} limit={limit} cost={}", + r7.compute_gas, + edge.cost, + ); + assert!( + r7.gas_used < gas_limit, + "{label}: REX7 must rescue rather than burn; gas_used={} gas_limit={gas_limit}", + r7.gas_used + ); + rex7_gas_used.push(r7.gas_used); + + assert!(!r6.is_success(), "{label}/REX6 must stop: {:?}", r6.result); + rex6_burned.push(r6.gas_used == gas_limit); + } + + let first = rex7_gas_used[0]; + assert!( + rex7_gas_used.iter().all(|&used| used == first), + "the rescued amount must not notice the edge; gas_used across the sweep = {rex7_gas_used:?}", + ); + // The sweep has to actually straddle the edge, or the stability claim is vacuous: per-opcode + // accounting burns the frame below the edge and stops on the resource limit above it. + assert!( + rex6_burned.contains(&true) && rex6_burned.contains(&false), + "the sweep must straddle the knife edge; REX6 burn pattern = {rex6_burned:?}", + ); +} + +/// The same sweep with gas detention as the binding constraint: the classification that has to stay +/// stable is `VolatileDataAccessOutOfGas`, which the clamp reconstructs from what bound it rather +/// than from usage having crossed the detained limit. +#[test] +fn test_detained_double_exceed_classification_is_stable_across_the_knife_edge() { + let edge = calibrate(true); + let code = approach(true).append(MSTORE).append(STOP).build(); + // The cap is relative to usage at the access, which happens two opcodes in. Sizing it as + // "everything between the access and the MSTORE, plus half the MSTORE" puts the detained + // headroom at the MSTORE at half the opcode's cost, exactly as in the TX-level case. + let at_access = unconstrained_compute_gas( + BytecodeBuilder::default().append(TIMESTAMP).append(STOP).build(), + ); + let cap = edge.compute_before - at_access + edge.cost / 2; + let limits = move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = cap; + limits + }; + + let mut rex7_gas_used = Vec::new(); + let mut rex6_burned = Vec::new(); + for delta in -SWEEP..=SWEEP { + let gas_limit = (edge.gas_limit_at_edge as i64 + delta) as u64; + let label = format!("edge{delta:+}"); + let r6 = transact_with_gas_limit( + MegaSpecId::REX6, + base_db(code.clone()), + limits(MegaSpecId::REX6), + gas_limit, + ); + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(code.clone()), + limits(MegaSpecId::REX7), + gas_limit, + ); + + assert!(!r7.is_success(), "{label}/REX7 must stop: {:?}", r7.result); + assert!( + matches!(r7.halt_reason(&label), MegaHaltReason::VolatileDataAccessOutOfGas { .. }), + "{label}: a detained corner must keep the volatile attribution on every sweep point; \ + got {:?}", + r7.halt_reason(&label), + ); + assert!( + r7.gas_used < gas_limit, + "{label}: REX7 must rescue rather than burn; gas_used={} gas_limit={gas_limit}", + r7.gas_used + ); + rex7_gas_used.push(r7.gas_used); + + assert!(!r6.is_success(), "{label}/REX6 must stop: {:?}", r6.result); + rex6_burned.push(r6.gas_used == gas_limit); + } + + let first = rex7_gas_used[0]; + assert!( + rex7_gas_used.iter().all(|&used| used == first), + "the rescued amount must not notice the edge; gas_used across the sweep = {rex7_gas_used:?}", + ); + assert!( + rex6_burned.contains(&true) && rex6_burned.contains(&false), + "the sweep must straddle the knife edge; REX6 burn pattern = {rex6_burned:?}", + ); +} + +/// The corner one frame down, where the clamp is bound frame-locally rather than TX-level. +/// +/// A frame-local exceed is absorbed into a revert, so the caller survives and sees a failed CALL — +/// and it must keep doing so across the edge, where the crossing opcode flips from unaffordable to +/// affordable in the child's true gas. The caller's own budget is untouched throughout, so the +/// transaction itself must succeed on every sweep point. +#[test] +fn test_frame_local_double_exceed_classification_is_stable_across_the_knife_edge() { + let callee_code = approach(false).append(MSTORE).append(STOP).build(); + let callee_before = approach(false).append(STOP).build(); + // The callee is a whole transaction's worth of work when run on its own, so calibrating it that + // way gives the EVM gas it needs before the MSTORE; inside a frame the intrinsic part is not + // charged again, so the edge is calibrated by sweeping a wide enough window instead. + let callee_intrinsic = + unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); + let before_in_frame = unconstrained_compute_gas(callee_before) - callee_intrinsic; + let cost = unconstrained_compute_gas(callee_code.clone()) - + unconstrained_compute_gas(approach(false).append(STOP).build()); + let forwarded_at_edge = before_in_frame + cost; + + let caller = |forwarded: u64| { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(crate::common::CALLEE) + .push_number(forwarded) + .append(CALL) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build() + }; + + let mut succeeded = Vec::new(); + for delta in -SWEEP..=SWEEP { + let forwarded = (forwarded_at_edge as i64 + delta) as u64; + let label = format!("edge{delta:+}"); + let code = caller(forwarded); + let build_db = + || base_db(code.clone()).account_code(crate::common::CALLEE, callee_code.clone()); + let r6 = transact_default(MegaSpecId::REX6, build_db()); + let r7 = transact_default(MegaSpecId::REX7, build_db()); + + for (spec, r) in [("REX6", &r6), ("REX7", &r7)] { + assert!( + r.is_success(), + "{label}/{spec}: a sub-frame running out of gas must not stop the transaction: \ + {:?}", + r.result + ); + } + let call_ok = |r: &Outcome| { + r.result.output().map(|o| U256::from_be_slice(o)) == Some(U256::from(1u64)) + }; + assert_eq!( + call_ok(&r6), + call_ok(&r7), + "{label}: both models must agree on whether the sub-frame survived", + ); + succeeded.push(call_ok(&r7)); + } + // The window straddles the point where the forwarded gas starts covering the crossing opcode. + assert!( + succeeded.contains(&true) && succeeded.contains(&false), + "the sweep must straddle the sub-frame's knife edge; outcomes = {succeeded:?}", + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index ed499d68..e949c3a0 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -7,6 +7,7 @@ mod checkpoint_settlement; mod common; +mod double_exceed_corner; mod gas_leakage; mod interceptor_resume; mod latch_surfacing; From 789d67cb228ab2ea1cccc38b206606e9211f4069 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:28:54 +0800 Subject: [PATCH 013/208] test(rex7): add a parity case for every checkpoint opcode --- .../tests/rex7/checkpoint_families.rs | 203 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 204 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/checkpoint_families.rs diff --git a/crates/mega-evm/tests/rex7/checkpoint_families.rs b/crates/mega-evm/tests/rex7/checkpoint_families.rs new file mode 100644 index 00000000..5289f17c --- /dev/null +++ b/crates/mega-evm/tests/rex7/checkpoint_families.rs @@ -0,0 +1,203 @@ +//! REX7: one parity case per checkpoint opcode the REX7 table wires. +//! +//! `checkpoint_settlement` covers the checkpoint families through representative members — one +//! LOG, one SLOAD, the CALL family, CREATE / CREATE2, SELFDESTRUCT, a few volatile opcodes. This +//! file closes the set: every opcode the REX7 instruction table replaces with a checkpoint handler +//! gets its own parity case, so a handler wired with the wrong settlement macro — or left out of a +//! future table edit — fails here rather than only in whichever downstream test happened to use it. +//! +//! Each opcode is run twice: once plainly, and once with a detention cap engaged before it so a +//! clamp is outstanding when its prologue runs. Both runs must be indistinguishable from REX6. + +use crate::common::{assert_outcomes_identical, transact, CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{ + BALANCE, BASEFEE, BLOBBASEFEE, BLOBHASH, BLOCKHASH, COINBASE, DIFFICULTY, EXTCODECOPY, + EXTCODEHASH, EXTCODESIZE, GAS, GASLIMIT, LOG0, LOG1, LOG2, LOG3, LOG4, NUMBER, POP, + SELFBALANCE, SLOAD, STOP, TIMESTAMP, +}; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, BytecodeBuilder::default().append(STOP).build()) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// Wraps `snippet` in plain segments on both sides, optionally engaging a detention cap first, so +/// the checkpoint under test has an open segment to settle and a clamp to restore. +fn program(snippet: impl Fn(BytecodeBuilder) -> BytecodeBuilder, volatile_prologue: bool) -> Bytes { + let mut builder = BytecodeBuilder::default(); + if volatile_prologue { + builder = builder.append(TIMESTAMP).append(POP); + } + let builder = snippet(plain_filler(builder, 5)); + plain_filler(builder, 5).append(STOP).build() +} + +/// Runs one checkpoint opcode under both specs, plainly and with a clamp outstanding. +fn assert_checkpoint_parity(label: &str, snippet: impl Fn(BytecodeBuilder) -> BytecodeBuilder) { + for (arm, volatile_prologue, cap) in + [("plain", false, u64::MAX), ("under a clamp", true, 1_000_000)] + { + let code = program(&snippet, volatile_prologue); + let limits = |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + if cap != u64::MAX { + limits.block_env_access_compute_gas_limit = cap; + } + limits + }; + let r6 = transact(MegaSpecId::REX6, base_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); + let label = format!("{label} ({arm})"); + assert!(r6.is_success(), "{label}: REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "{label}: REX7 must succeed: {:?}", r7.result); + assert_outcomes_identical(&label, &r6, &r7); + } +} + +/// Pushes `n` LOG topics, then the payload length and offset, in the order the opcode pops them. +fn log_operands(builder: BytecodeBuilder, topics: usize) -> BytecodeBuilder { + let mut builder = builder.mstore(0, [0x44u8; 32]); + for topic in (0..topics).rev() { + builder = builder.push_number(0xabc0u64 + topic as u64); + } + builder.push_number(32u64).push_number(0u64) +} + +/// The block-environment opcodes: each marks volatile access, settles its segment, then installs +/// the detention cap. They take no operands and push one word. +#[test] +fn test_block_env_checkpoints_match_per_opcode() { + for (label, opcode) in [ + ("COINBASE", COINBASE), + ("TIMESTAMP", TIMESTAMP), + ("NUMBER", NUMBER), + ("DIFFICULTY", DIFFICULTY), + ("GASLIMIT", GASLIMIT), + ("BASEFEE", BASEFEE), + ("BLOBBASEFEE", BLOBBASEFEE), + ("SELFBALANCE", SELFBALANCE), + ] { + assert_checkpoint_parity(label, |builder| builder.append(opcode).append(POP)); + } +} + +/// The operand-taking volatile checkpoints. +#[test] +fn test_operand_taking_volatile_checkpoints_match_per_opcode() { + assert_checkpoint_parity("BLOCKHASH", |builder| { + builder.push_number(0u64).append(BLOCKHASH).append(POP) + }); + assert_checkpoint_parity("BLOBHASH", |builder| { + builder.push_number(0u64).append(BLOBHASH).append(POP) + }); + assert_checkpoint_parity("BALANCE", |builder| { + builder.push_address(CALLEE).append(BALANCE).append(POP) + }); + assert_checkpoint_parity("EXTCODESIZE", |builder| { + builder.push_address(CALLEE).append(EXTCODESIZE).append(POP) + }); + assert_checkpoint_parity("EXTCODEHASH", |builder| { + builder.push_address(CALLEE).append(EXTCODEHASH).append(POP) + }); + assert_checkpoint_parity("EXTCODECOPY", |builder| { + builder + .push_number(32u64) // length + .push_number(0u64) // offset + .push_number(0u64) // destOffset + .push_address(CALLEE) + .append(EXTCODECOPY) + }); + assert_checkpoint_parity("SLOAD", |builder| { + builder.push_u256(U256::from(3)).append(SLOAD).append(POP) + }); +} + +/// `GAS` is a checkpoint only because of the clamp: it has to restore the hidden gas before revm's +/// instruction reads the counter. +#[test] +fn test_gas_checkpoint_matches_per_opcode() { + assert_checkpoint_parity("GAS", |builder| builder.append(GAS).append(POP)); +} + +/// Every LOG arity: the storage-gas surcharge scales with the topic count, and each arity is a +/// separate table entry. +#[test] +fn test_every_log_arity_matches_per_opcode() { + for (label, opcode, topics) in [ + ("LOG0", LOG0, 0), + ("LOG1", LOG1, 1), + ("LOG2", LOG2, 2), + ("LOG3", LOG3, 3), + ("LOG4", LOG4, 4), + ] { + assert_checkpoint_parity(label, move |builder| { + log_operands(builder, topics).append(opcode) + }); + } +} + +/// SSTORE across the three write shapes its storage-gas charge distinguishes: a first write to a +/// fresh slot, an overwrite of that slot, and a write back to zero. +#[test] +fn test_sstore_write_shapes_match_per_opcode() { + assert_checkpoint_parity("SSTORE zero -> non-zero", |builder| { + builder.sstore(U256::from(0x50), U256::from(0x11)) + }); + assert_checkpoint_parity("SSTORE non-zero -> non-zero", |builder| { + builder + .sstore(U256::from(0x50), U256::from(0x11)) + .sstore(U256::from(0x50), U256::from(0x22)) + }); + assert_checkpoint_parity("SSTORE non-zero -> zero", |builder| { + builder.sstore(U256::from(0x50), U256::from(0x11)).sstore(U256::from(0x50), U256::ZERO) + }); + assert_checkpoint_parity("SSTORE then SLOAD of the same slot", |builder| { + builder + .sstore(U256::from(0x50), U256::from(0x11)) + .push_u256(U256::from(0x50)) + .append(SLOAD) + .append(POP) + }); + assert_checkpoint_parity("two SSTOREs with a plain gap", |builder| { + let builder = builder.sstore(U256::from(0x50), U256::from(0x11)); + plain_filler(builder, 10).sstore(U256::from(0x51), U256::from(0x22)) + }); +} + +/// Back-to-back checkpoints with no plain opcode between them: the settlement window has to open +/// and close on a zero-length segment without billing anything twice. +#[test] +fn test_adjacent_checkpoints_match_per_opcode() { + assert_checkpoint_parity("TIMESTAMP NUMBER COINBASE", |builder| { + builder + .append(TIMESTAMP) + .append(POP) + .append(NUMBER) + .append(POP) + .append(COINBASE) + .append(POP) + }); + assert_checkpoint_parity("GAS GAS", |builder| { + builder.append(GAS).append(POP).append(GAS).append(POP) + }); + assert_checkpoint_parity("SSTORE SSTORE", |builder| { + builder.sstore(U256::from(0x60), U256::from(1)).sstore(U256::from(0x61), U256::from(2)) + }); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index e949c3a0..11eeddf8 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -5,6 +5,7 @@ //! - `v0_clamp` — V0 gas-clamp enforcement: a crossing opcode is stopped before it executes, and //! the resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. +mod checkpoint_families; mod checkpoint_settlement; mod common; mod double_exceed_corner; From 0fdb5760ca7f36830b7905f2800cd6280c5da96a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:30:48 +0800 Subject: [PATCH 014/208] test(rex7): satisfy clippy doc-markdown in the new suites --- crates/mega-evm/tests/rex7/double_exceed_corner.rs | 2 +- crates/mega-evm/tests/rex7/parity_shapes.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/tests/rex7/double_exceed_corner.rs b/crates/mega-evm/tests/rex7/double_exceed_corner.rs index d62e1ddb..aac01651 100644 --- a/crates/mega-evm/tests/rex7/double_exceed_corner.rs +++ b/crates/mega-evm/tests/rex7/double_exceed_corner.rs @@ -20,7 +20,7 @@ //! The calibration runs a probe truncated just before the crossing opcode and reads two numbers off //! it: the compute gas recorded there (which fixes where to put the compute limit) and the //! receipt's `gas_used` (the EVM gas spent there, which fixes the transaction gas limit at which -//! the crossing opcode is exactly affordable). The two are not the same number — MegaETH's +//! the crossing opcode is exactly affordable). The two are not the same number — `MegaETH`'s //! intrinsic transaction gas is larger than the intrinsic compute it records — so both have to be //! measured. diff --git a/crates/mega-evm/tests/rex7/parity_shapes.rs b/crates/mega-evm/tests/rex7/parity_shapes.rs index 7855077a..73a3b146 100644 --- a/crates/mega-evm/tests/rex7/parity_shapes.rs +++ b/crates/mega-evm/tests/rex7/parity_shapes.rs @@ -324,7 +324,7 @@ fn test_system_originated_transaction_is_unclamped_under_both_models() { ); } -/// The REX5 storage-call stipend is a per-frame allowance drawn only at MegaETH's storage-gas +/// The REX5 storage-call stipend is a per-frame allowance drawn only at `MegaETH`'s storage-gas /// surcharge sites — which are exactly the checkpoints. A value-transferring internal CALL into a /// callee that logs and writes storage draws on it at three of them. /// From e481864cc144a4adb3f9c4baae13453d3ca2dd96 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:36:59 +0800 Subject: [PATCH 015/208] test(rex7): describe the new suites in the module docs --- crates/mega-evm/tests/rex7/main.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 11eeddf8..ffc5d561 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -4,6 +4,19 @@ //! bit-identical to per-opcode recording, and the two places where the models diverge. //! - `v0_clamp` — V0 gas-clamp enforcement: a crossing opcode is stopped before it executes, and //! the resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. +//! - `checkpoint_families` — one parity case per checkpoint opcode the REX7 table wires, so the set +//! is covered exhaustively rather than through representatives. +//! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a +//! system contract interceptor's synthetic result, and a precompile. +//! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a +//! stop. +//! - `gas_leakage` — the three paths a per-frame gas mechanism can leak through (interception, +//! TX-level rescue, frame return), each with a clamp outstanding. +//! - `parity_shapes` — parity on the transaction shapes that enter through a different door: +//! EIP-7702 authorizations, the `KeylessDeploy` sandbox, system-originated (exempt) transactions, +//! the REX5 storage-call stipend, and oracle hints. +//! - `double_exceed_corner` — the adjudicated corner swept one gas at a time, so the classification +//! is shown to be stable rather than merely correct at one point. mod checkpoint_families; mod checkpoint_settlement; From efc378c91dea556b7b495d653f876db917c1a329 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:42:04 +0800 Subject: [PATCH 016/208] test(rex7): engage the clamp in the frame-local corner sweep --- .../tests/rex7/double_exceed_corner.rs | 92 +++++++++++++------ 1 file changed, 63 insertions(+), 29 deletions(-) diff --git a/crates/mega-evm/tests/rex7/double_exceed_corner.rs b/crates/mega-evm/tests/rex7/double_exceed_corner.rs index aac01651..73293188 100644 --- a/crates/mega-evm/tests/rex7/double_exceed_corner.rs +++ b/crates/mega-evm/tests/rex7/double_exceed_corner.rs @@ -25,7 +25,7 @@ //! measured. use crate::common::{ - transact_default, transact_with_gas_limit, Outcome, CALLER, CONTRACT, ONE_ETH, + transact, transact_default, transact_with_gas_limit, Outcome, CALLER, CONTRACT, ONE_ETH, }; use alloy_primitives::{Bytes, U256}; use mega_evm::{ @@ -67,13 +67,18 @@ fn approach(volatile: bool) -> BytecodeBuilder { plain_filler(builder, 20).push_number(0u64).push_number(CROSSING_OFFSET) } -/// Runs `code` with nothing constraining it, for calibration. -fn unconstrained(code: Bytes) -> Outcome { - let outcome = transact_default(MegaSpecId::REX7, base_db(code)); +/// Runs `db` with nothing constraining it, for calibration. +fn unconstrained_db(db: MemoryDatabase) -> Outcome { + let outcome = transact_default(MegaSpecId::REX7, db); assert!(outcome.is_success(), "the calibration run must succeed: {:?}", outcome.result); outcome } +/// Runs `code` against the default database with nothing constraining it, for calibration. +fn unconstrained(code: Bytes) -> Outcome { + unconstrained_db(base_db(code)) +} + /// The compute gas `code` records when neither EVM gas nor any resource limit constrains it. fn unconstrained_compute_gas(code: Bytes) -> u64 { unconstrained(code).compute_gas @@ -243,22 +248,26 @@ fn test_detained_double_exceed_classification_is_stable_across_the_knife_edge() /// The corner one frame down, where the clamp is bound frame-locally rather than TX-level. /// -/// A frame-local exceed is absorbed into a revert, so the caller survives and sees a failed CALL — -/// and it must keep doing so across the edge, where the crossing opcode flips from unaffordable to -/// affordable in the child's true gas. The caller's own budget is untouched throughout, so the -/// transaction itself must succeed on every sweep point. +/// A nested frame's compute budget is always strictly tighter than the TX-level remaining (98/100 +/// of its parent's), so the clamp inside a sub-frame is always bound frame-locally — and a +/// frame-local exceed is absorbed into a revert rather than halting the transaction. With the +/// compute limit set so the child's headroom runs out inside the crossing opcode, that absorption +/// has to hold on every sweep point, including where the child's *true* forwarded gas flips from +/// too little to enough. +/// +/// The control arm — the same sweep with nothing constraining compute — is what shows the window +/// straddles a real edge: there the sub-frame's outcome does flip. #[test] fn test_frame_local_double_exceed_classification_is_stable_across_the_knife_edge() { let callee_code = approach(false).append(MSTORE).append(STOP).build(); let callee_before = approach(false).append(STOP).build(); - // The callee is a whole transaction's worth of work when run on its own, so calibrating it that - // way gives the EVM gas it needs before the MSTORE; inside a frame the intrinsic part is not - // charged again, so the edge is calibrated by sweeping a wide enough window instead. + // Calibrating the callee as a standalone transaction gives its work up to the MSTORE once the + // intrinsic part — which a sub-frame does not pay again — is taken back out. let callee_intrinsic = unconstrained_compute_gas(BytecodeBuilder::default().append(STOP).build()); - let before_in_frame = unconstrained_compute_gas(callee_before) - callee_intrinsic; - let cost = unconstrained_compute_gas(callee_code.clone()) - - unconstrained_compute_gas(approach(false).append(STOP).build()); + let callee_before_compute = unconstrained_compute_gas(callee_before); + let before_in_frame = callee_before_compute - callee_intrinsic; + let cost = unconstrained_compute_gas(callee_code.clone()) - callee_before_compute; let forwarded_at_edge = before_in_frame + cost; let caller = |forwarded: u64| { @@ -278,18 +287,44 @@ fn test_frame_local_double_exceed_classification_is_stable_across_the_knife_edge .append(RETURN) .build() }; + let build_db = |code: &Bytes| { + base_db(code.clone()).account_code(crate::common::CALLEE, callee_code.clone()) + }; + let call_succeeded = + |r: &Outcome| r.result.output().map(|o| U256::from_be_slice(o)) == Some(U256::from(1u64)); - let mut succeeded = Vec::new(); + // A compute limit half a crossing-opcode short of what the whole transaction needs when the + // sub-frame completes: the child's own budget is what runs out, and it runs out inside the + // MSTORE. + let generous = caller(forwarded_at_edge + SWEEP as u64); + let whole_tx = unconstrained_db(build_db(&generous)); + assert!(call_succeeded(&whole_tx), "the calibration run's sub-frame must complete"); + let limit = whole_tx.compute_gas - cost / 2; + let limits = |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit); + + let mut control_outcomes = Vec::new(); for delta in -SWEEP..=SWEEP { let forwarded = (forwarded_at_edge as i64 + delta) as u64; let label = format!("edge{delta:+}"); let code = caller(forwarded); - let build_db = - || base_db(code.clone()).account_code(crate::common::CALLEE, callee_code.clone()); - let r6 = transact_default(MegaSpecId::REX6, build_db()); - let r7 = transact_default(MegaSpecId::REX7, build_db()); - for (spec, r) in [("REX6", &r6), ("REX7", &r7)] { + // Treatment: the child's compute headroom is what the crossing opcode cannot pay for. + let r7 = transact(MegaSpecId::REX7, build_db(&code), limits(MegaSpecId::REX7)); + assert!( + r7.is_success(), + "{label}: a frame-local exceed must be absorbed into a revert, not halt the \ + transaction: {:?}", + r7.result + ); + assert!( + !call_succeeded(&r7), + "{label}: the sub-frame must report failure on every sweep point", + ); + + // Control: nothing constrains compute, so only the forwarded gas decides. + let c6 = transact_default(MegaSpecId::REX6, build_db(&code)); + let c7 = transact_default(MegaSpecId::REX7, build_db(&code)); + for (spec, r) in [("REX6", &c6), ("REX7", &c7)] { assert!( r.is_success(), "{label}/{spec}: a sub-frame running out of gas must not stop the transaction: \ @@ -297,19 +332,18 @@ fn test_frame_local_double_exceed_classification_is_stable_across_the_knife_edge r.result ); } - let call_ok = |r: &Outcome| { - r.result.output().map(|o| U256::from_be_slice(o)) == Some(U256::from(1u64)) - }; assert_eq!( - call_ok(&r6), - call_ok(&r7), + call_succeeded(&c6), + call_succeeded(&c7), "{label}: both models must agree on whether the sub-frame survived", ); - succeeded.push(call_ok(&r7)); + control_outcomes.push(call_succeeded(&c7)); } - // The window straddles the point where the forwarded gas starts covering the crossing opcode. + // The window straddles the point where the forwarded gas starts covering the crossing opcode, + // so the stability asserted above is a statement about a real edge. assert!( - succeeded.contains(&true) && succeeded.contains(&false), - "the sweep must straddle the sub-frame's knife edge; outcomes = {succeeded:?}", + control_outcomes.contains(&true) && control_outcomes.contains(&false), + "the sweep must straddle the sub-frame's knife edge; control outcomes = \ + {control_outcomes:?}", ); } From 6648de8576657fe56c7e2116e0e133a2748fccff Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 11 Aug 2026 17:49:17 +0800 Subject: [PATCH 017/208] docs(rex7): correct halt-field actual/limit contract and top-frame tie-break MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document that per-opcode enforcement (through Rex6) reports actual > limit while gas-clamp enforcement (Rex7+) reports actual ≤ limit on compute and detention halts. Normatively state that equal frame and TX remaining headroom binds the clamp to the TX level (halt + rescue), unlike Rex6's frame-local revert classification at the top frame. --- crates/mega-evm/src/evm/result.rs | 20 ++++++++++++++++---- docs/spec/evm/compute-gas.md | 4 ++++ docs/spec/upgrades/rex7.md | 6 ++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index e865360a..8ae66054 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -109,9 +109,15 @@ pub enum MegaHaltReason { }, /// Compute gas limit exceeded ComputeGasLimitExceeded { - /// The configured compute gas limit + /// The configured compute gas limit that was exceeded. + /// + /// Relation to `actual` depends on the enforcement model: + /// - Per-opcode enforcement (through Rex6): the crossing opcode has already recorded its + /// cost, so `actual > limit`. + /// - Gas-clamp enforcement (Rex7+): the crossing opcode is stopped before it executes and + /// its cost is not recorded, so `actual ≤ limit`. limit: u64, - /// The actual compute gas usage + /// The actual compute gas usage at the halt. actual: u64, }, /// State growth limit exceeded @@ -136,9 +142,15 @@ pub enum MegaHaltReason { access_type: VolatileDataAccess, /// The effective detained compute gas limit that was exceeded. /// In REX4+ this is `usage_at_access + cap` (relative); pre-REX4 it equals the raw cap - /// (absolute). Always satisfies `actual > limit`. + /// (absolute). + /// + /// Relation to `actual` depends on the enforcement model: + /// - Per-opcode enforcement (through Rex6): the crossing opcode has already recorded its + /// cost, so `actual > limit`. + /// - Gas-clamp enforcement (Rex7+): the crossing opcode is stopped before it executes and + /// its cost is not recorded, so `actual ≤ limit`. limit: u64, - /// The actual compute gas usage + /// The actual compute gas usage at the halt. actual: u64, }, } diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 151fe335..38de1dc6 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -487,6 +487,10 @@ Inside a plain-opcode segment: Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. +When the current frame's remaining per-frame compute budget equals the transaction-level remaining budget, a node MUST bind the clamp to the transaction-level constraint (including detention when detention is the effective transaction-level bound). +A clamp-induced exceed under that binding MUST halt the transaction with gas rescue; a node MUST NOT classify the equality as frame-local. +Through Rex6, the same equality is classified by the per-opcode check as a frame-local exceed; at the top-level frame that surfaces as a revert rather than a halt. + When the crossing opcode would exhaust both the true remaining EVM gas and the compute headroom, a node MUST attribute the halt to the compute-gas or detention limit (with rescue) rather than to ordinary EVM out-of-gas. #### Exceptional-halt frame carve-out diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index aa58f935..d3c5f667 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -105,6 +105,12 @@ Inside a plain-opcode segment only plain opcodes run, so the inherited EVM's ord Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. Recorded usage at a clamp-induced halt therefore ends at the limit (or strictly below it if settlement had not yet closed a partial segment), not strictly above it. +**Top-frame headroom tie-break.** +At the top-level frame the remaining per-frame compute budget equals the transaction-level remaining budget whenever both are still governed by the same base limit. +When those two remaining amounts are equal, a node MUST bind the clamp to the transaction-level constraint (or to the detained limit when detention is the effective transaction-level bound). +A clamp-induced exceed under that binding MUST halt the transaction with gas rescue; a node MUST NOT classify the equality as frame-local. +Through Rex6, the same equality is classified by the per-opcode check as a frame-local exceed, which the top-level frame absorbs into a revert rather than a halt. + **Double-exceed preference.** When the crossing opcode would have exhausted both the true remaining EVM gas and the compute headroom at the same point, a node MUST attribute the halt to the compute-gas (or detention) limit rather than to ordinary EVM out-of-gas, so remaining gas stays refundable under the rescue rules. The two cases are indistinguishable once the frame has already reported out-of-gas, and the compute classification is the one that preserves the sender refund. From ab45e02b0c8c16eeb221d7ffa47794c687a4c3ef Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 00:55:44 +0800 Subject: [PATCH 018/208] fix(rex7): bind the V0 gas clamp on an explicit lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clamp used a zero hidden amount as the sentinel for "no clamp", which also happens to be what an exactly-equal clamp hides. A segment whose true remaining matched the compute headroom therefore enforced the limit but was never reclassified: the crossing opcode's ordinary out-of-gas propagated as an EVM out-of-gas, with no gas rescue and no MegaLimitExceeded payload. Record the clamp as state instead — present exactly while it binds, carrying the constraint it was bound to — so the equal case reclassifies like every other clamp, and a segment whose own gas runs out first records no clamp at all and keeps the EVM's own out-of-gas. --- crates/mega-evm/src/evm/execution.rs | 8 +- crates/mega-evm/src/limit/compute_gas.rs | 30 ++- crates/mega-evm/src/limit/limit.rs | 119 ++++++------ .../tests/rex7/clamp_classification.rs | 171 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 3 + 5 files changed, 266 insertions(+), 65 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/clamp_classification.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index ee3718c2..767e7794 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -456,10 +456,10 @@ impl MegaEvm { let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); if let InterpreterAction::Return(interpreter_result) = action { - // REX7 V0 clamp: hand any clamp-hidden gas back to the result — and latch a - // clamp-induced out-of-gas as the compute exceed it stands for — before the - // code-deposit charge below observes the result's gas. - ctx.additional_limit.borrow_mut().restore_clamp_into_result(interpreter_result); + // REX7: hand any clamp-hidden gas back to the result and latch a clamp-induced + // out-of-gas as the compute exceed it stands for, before the code-deposit charge below + // observes the result's gas. + ctx.additional_limit.borrow_mut().settle_frame_final_result(interpreter_result); // Charge storage gas cost for the number of bytes if frame.data.is_create() && interpreter_result.is_ok() { diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 90f42953..86264157 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -6,6 +6,20 @@ use super::{ }; use crate::{JournalInspectTr, MegaSpecId}; +/// The constraint that bounds the V0 gas clamp for one plain-opcode segment. +/// +/// Captured when the clamp is applied, so a clamp-induced out-of-gas can be classified against the +/// constraint that was in force at the time rather than against whatever the tracker looks like +/// once the frame has already failed. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ClampBinding { + /// The compute headroom the interpreter is allowed to keep seeing. + pub(crate) headroom: u64, + /// `true` when the current frame's compute budget is what binds, `false` when the TX-level + /// (possibly detained) limit is. + pub(crate) frame_local: bool, +} + /// A frame-limit-based compute gas tracker using `FrameLimitTracker`. /// /// Unlike the other trackers (`DataSizeTracker`, `KVUpdateTracker`, `StateGrowthTracker`), compute @@ -115,24 +129,26 @@ impl ComputeGasTracker { self.frame_tracker.tx_limit() } - /// Returns the compute gas headroom the V0 gas clamp may leave visible to the interpreter, - /// and whether the binding constraint is the frame-local budget (`true`) or the TX-level - /// (possibly detained) limit (`false`). + /// Returns the constraint the V0 gas clamp must bind to at this point in the transaction. /// /// The headroom is the tighter of the current frame's remaining compute budget (Rex4+) and /// the TX-level remaining under the effective (possibly detained) limit — the same pair /// [`check_limit`](TxRuntimeLimit::check_limit) enforces. Gas hidden beyond this headroom is /// therefore reachable only by a transaction that would exceed one of those two limits. + /// Equal remainders bind to the TX-level constraint, so a top-level frame — where the two are + /// equal whenever the same base limit still governs both — halts with gas rescue rather than + /// absorbing the exceed into a revert. #[inline] - pub(crate) fn clamp_headroom(&self) -> (u64, bool) { - let tx_remaining = self.tx_limit().saturating_sub(self.tx_usage()); + pub(crate) fn clamp_binding(&self) -> ClampBinding { + let tx_limit = self.tx_limit(); + let tx_remaining = tx_limit.saturating_sub(self.tx_usage()); if self.rex4_enabled { let frame_remaining = self.frame_tracker.current_frame_remaining(); if frame_remaining < tx_remaining { - return (frame_remaining, true); + return ClampBinding { headroom: frame_remaining, frame_local: true }; } } - (tx_remaining, false) + ClampBinding { headroom: tx_remaining, frame_local: false } } /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index d85e39e6..88473068 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -123,22 +123,15 @@ pub struct AdditionalLimit { /// checkpoint prologue and body recording. checkpoint_baseline: u64, - /// V0 gas-clamp enforcement (REX7+): the part of the executing frame's interpreter gas hidden - /// from the interpreter, so that revm's own per-opcode gas checks enforce the compute headroom - /// inside plain-opcode segments at no per-opcode cost. + /// V0 gas-clamp enforcement (REX7+): the clamp in force for the plain-opcode segment the + /// current frame is inside, so that revm's own per-opcode gas checks enforce the compute + /// headroom at no per-opcode cost. /// - /// Non-zero only while the current frame is inside a plain segment: every checkpoint restores - /// it before running its body — so CALL forwarding, `GAS` and storage charges observe the true - /// counter — and re-applies it on the way out, and the frame's final result restores it via - /// [`restore_clamp_into_result`](Self::restore_clamp_into_result). - clamp_hidden: u64, - - /// Whether the headroom that bound the last clamp was the frame-local compute budget (`true`) - /// or the TX-level (possibly detained) limit (`false`). - /// - /// This decides how a clamp-induced out-of-gas is reclassified: a frame-local exceed reverts - /// to the parent, a TX-level exceed halts the transaction. - clamp_frame_local: bool, + /// Present only while the current frame is inside a plain segment: every checkpoint takes it + /// before running its body — so CALL forwarding, `GAS` and storage charges observe the true + /// counter — and re-applies it on the way out, and the frame's final result takes it via + /// [`settle_frame_final_result`](Self::settle_frame_final_result). + clamp: Option, /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level /// constraint. @@ -151,6 +144,21 @@ pub struct AdditionalLimit { clamp_latched_detained: bool, } +/// A V0 gas clamp in force for one plain-opcode segment (REX7+). +/// +/// The clamp is a lifecycle, not an amount. It is recorded exactly while it **binds** — while the +/// interpreter's true remaining gas was at or above the compute headroom when the segment opened — +/// and a `hidden` of zero is a binding clamp whose two budgets happened to coincide, not the +/// absence of one. When the frame's own gas would run out ahead of the compute headroom no clamp +/// is recorded at all, and an out-of-gas inside that segment stays the EVM's own. +#[derive(Clone, Copy, Debug)] +struct ClampState { + /// Interpreter gas hidden from the interpreter for this segment. + hidden: u64, + /// The constraint the clamp was bound to, captured at the moment it was applied. + binding: compute_gas::ClampBinding, +} + /// The usage of the additional limits. #[derive(Clone, Copy, Debug, Default)] pub struct LimitUsage { @@ -178,8 +186,7 @@ impl AdditionalLimit { storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), checkpoint_baseline: 0, - clamp_hidden: 0, - clamp_frame_local: false, + clamp: None, clamp_latched_detained: false, } } @@ -223,8 +230,7 @@ impl AdditionalLimit { self.kv_update.reset(); self.storage_call_stipend.reset(); self.checkpoint_baseline = 0; - self.clamp_hidden = 0; - self.clamp_frame_local = false; + self.clamp = None; self.clamp_latched_detained = false; } @@ -253,33 +259,41 @@ impl AdditionalLimit { self.checkpoint_baseline = remaining; } - /// Takes the outstanding clamp-hidden gas so the caller can hand it back to the interpreter. + /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, + /// returning that amount. /// /// Every checkpoint prologue calls this before running its body, and the frame's final result /// calls it before the result propagates, so the clamp is never observable outside a plain /// segment. #[inline] pub(crate) fn checkpoint_restore_hidden(&mut self) -> u64 { - core::mem::take(&mut self.clamp_hidden) + self.clamp.take().map_or(0, |clamp| clamp.hidden) } - /// Computes how much interpreter gas to hide so the visible remaining equals the compute - /// headroom, records it as outstanding, and returns it for the caller to debit from the - /// interpreter's counter. + /// Applies the V0 gas clamp for the segment that starts at `remaining`, and returns the amount + /// the caller must debit from the interpreter's counter. + /// + /// The clamp is recorded — and the segment therefore enforces the compute limit — whenever the + /// true remaining reaches the compute headroom, including when the two are exactly equal and + /// nothing needs to be hidden. When the frame's own gas would run out first, no clamp is + /// recorded: an out-of-gas in that segment is the EVM's own, and reclassifying it as a compute + /// exceed would rescue gas the transaction never had a claim to. /// - /// Returns 0 when clamping does not apply: the transaction is exempt from per-tx metering, or - /// a limit has already been latched (the enclosing site halts on it instead). + /// Records nothing and returns 0 when clamping does not apply: the transaction is exempt from + /// per-tx metering, or a limit has already been latched (the enclosing site halts on it + /// instead). #[inline] pub(crate) fn checkpoint_clamp_amount(&mut self, remaining: u64) -> u64 { - debug_assert_eq!(self.clamp_hidden, 0, "clamp applied while a clamp is outstanding"); + debug_assert!(self.clamp.is_none(), "clamp applied while a clamp is outstanding"); if !self.has_exceeded_limit.within_limit() { return 0; } - let (headroom, frame_local) = self.compute_gas.clamp_headroom(); - let hide = remaining.saturating_sub(headroom); - self.clamp_hidden = hide; - self.clamp_frame_local = frame_local; - hide + let binding = self.compute_gas.clamp_binding(); + let Some(hidden) = remaining.checked_sub(binding.headroom) else { + return 0; + }; + self.clamp = Some(ClampState { hidden, binding }); + hidden } /// Latches a clamp-induced out-of-gas as a compute gas limit exceed. @@ -291,20 +305,20 @@ impl AdditionalLimit { /// machinery (frame-local absorb to revert; TX-level mark plus gas rescue) produces the halt /// shape it produces for every other compute exceed. #[inline] - fn latch_clamp_exceed(&mut self) { + fn latch_clamp_exceed(&mut self, binding: &compute_gas::ClampBinding) { if !self.has_exceeded_limit.within_limit() { return; } self.has_exceeded_limit = LimitCheck::ExceedsLimit { kind: super::LimitKind::ComputeGas, - frame_local: self.clamp_frame_local, + frame_local: binding.frame_local, limit: self.compute_gas.tx_limit(), used: self.compute_gas.tx_usage(), }; // Preserve the volatile-detention attribution: when the binding TX-level constraint at // clamp time was the detained limit, the halt must classify as `VolatileDataAccessOutOfGas` // exactly as per-opcode enforcement classifies it. - self.clamp_latched_detained = !self.clamp_frame_local && + self.clamp_latched_detained = !binding.frame_local && self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); } @@ -316,26 +330,23 @@ impl AdditionalLimit { /// and mis-fire an out-of-gas on a CREATE frame that is nowhere near its limits. /// /// A clamp can only be outstanding when the frame ended inside a plain-opcode segment, because - /// every checkpoint prologue restores it before its body. An out-of-gas exit from such a - /// segment is a clamp artifact: the true counter held `hidden` more gas than the - /// interpreter could see, and the crossing opcode was stopped at the clamp boundary *before - /// executing* — exactly the V0 enforcement point. When the crossing opcode would have - /// exceeded the true remaining as well, the compute classification still wins: the two are - /// indistinguishable here, and attributing the halt to the resource limit keeps the - /// sender's remaining gas refundable. - pub(crate) fn restore_clamp_into_result(&mut self, result: &mut InterpreterResult) { + /// every checkpoint prologue takes it before its body. An out-of-gas exit from such a segment + /// is a clamp artifact: the true counter held `hidden` more gas than the interpreter could see, + /// and the crossing opcode was stopped at the clamp boundary *before executing* — exactly the + /// V0 enforcement point. When the crossing opcode would have exceeded the true remaining as + /// well, the compute classification still wins: the two are indistinguishable here, and + /// attributing the halt to the resource limit keeps the sender's remaining gas refundable. + pub(crate) fn settle_frame_final_result(&mut self, result: &mut InterpreterResult) { if !self.checkpoint_accounting { return; } - let hidden = self.checkpoint_restore_hidden(); - if hidden == 0 { - return; - } - result.gas.erase_cost(hidden); - // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every other - // result either is unrelated to gas or cannot arise from a plain opcode. - if matches!(result.result, InstructionResult::OutOfGas | InstructionResult::MemoryOOG) { - self.latch_clamp_exceed(); + if let Some(clamp) = self.clamp.take() { + result.gas.erase_cost(clamp.hidden); + // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every + // other result either is unrelated to gas or cannot arise from a plain opcode. + if matches!(result.result, InstructionResult::OutOfGas | InstructionResult::MemoryOOG) { + self.latch_clamp_exceed(&clamp.binding); + } } } @@ -886,7 +897,7 @@ impl AdditionalLimit { // here: every suspension point (the CALL / CREATE checkpoint prologue) and every // frame end restores it first. if self.checkpoint_accounting { - debug_assert_eq!(self.clamp_hidden, 0, "frame resumed with a clamp outstanding"); + debug_assert!(self.clamp.is_none(), "frame resumed with a clamp outstanding"); let hide = self.checkpoint_clamp_amount(frame.interpreter.gas.remaining()); if hide > 0 { let clamped = frame.interpreter.gas.record_regular_cost(hide); @@ -940,7 +951,7 @@ impl AdditionalLimit { // `erase_cost` can only raise `remaining` above the baseline, which the saturation turns // into 0. Any exceed recorded here is latched, and the frame result marking below / in // `before_frame_return_result` surfaces it. The clamp restore itself already happened, in - // `restore_clamp_into_result`, before the execution-layer hook charged code-deposit storage + // `settle_frame_final_result`, before the execution-layer hook charged code-deposit storage // gas against the action's gas. if self.checkpoint_accounting { if let InterpreterAction::Return(_) = action { diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs new file mode 100644 index 00000000..a775b343 --- /dev/null +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -0,0 +1,171 @@ +//! REX7 gas-clamp classification and the payload a clamp-induced exceed reports. +//! +//! The clamp is a lifecycle, not an amount: it is applied at a checkpoint, it binds the segment +//! that follows to one specific constraint, and it is consumed at the next checkpoint or at frame +//! exit. Whether the interpreter's true remaining happened to sit *above* the compute headroom or +//! exactly *on* it changes how much gets hidden — zero in the second case — but not whether the +//! clamp is in force. Both are the compute limit doing the stopping, and both must be reported as +//! such; only a frame whose own EVM gas runs out first is an ordinary out-of-gas. + +use crate::common::{transact_default, transact_with_gas_limit, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, +}; +use revm::bytecode::opcode::{MSTORE, POP, STOP}; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// Per-spec runtime limits with the TX compute gas limit replaced. +fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own, five gas each. +fn plain_filler(pairs: usize) -> Vec { + let mut builder = BytecodeBuilder::default(); + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder.build_vec() +} + +// --------------------------------------------------------------------------------------------- +// The equal-value clamp: hidden == 0 and the clamp still binds. +// --------------------------------------------------------------------------------------------- + +/// Memory offset the calibrated `MSTORE` writes at — 32 KiB, so its expansion cost is thousands of +/// gas and the knife edge is not sensitive to a one-gas miscount anywhere else. +const MSTORE_OFFSET: u64 = 0x8000; + +/// The two shapes the knife-edge calibration needs: everything up to the `MSTORE`'s operands, and +/// the same thing with the `MSTORE` itself. +fn knife_edge_shapes() -> (Bytes, Bytes) { + let operands = |mut code: Vec| { + let mut builder = BytecodeBuilder::default(); + builder = builder.push_number(0u64).push_number(MSTORE_OFFSET); + code.extend_from_slice(&builder.build_vec()); + code + }; + let mut before = operands(plain_filler(20)); + before.push(STOP); + let mut full = operands(plain_filler(20)); + full.push(MSTORE); + full.push(STOP); + (Bytes::from(before), Bytes::from(full)) +} + +/// The calibrated knife edge: the exact transaction gas limit and compute gas limit that leave the +/// crossing `MSTORE` one gas short on *both* budgets at once. +struct KnifeEdge { + code: Bytes, + gas_limit: u64, + compute_limit: u64, + /// Compute gas the transaction has recorded when the `MSTORE` is reached. + compute_before: u64, +} + +fn calibrate_knife_edge() -> KnifeEdge { + let (before_code, full_code) = knife_edge_shapes(); + let before = transact_default(MegaSpecId::REX7, base_db(before_code)); + let full = transact_default(MegaSpecId::REX7, base_db(full_code.clone())); + assert!(before.is_success(), "calibration run must succeed: {:?}", before.result); + assert!(full.is_success(), "calibration run must succeed: {:?}", full.result); + + let mstore_cost = full.compute_gas - before.compute_gas; + assert!(mstore_cost > 1, "the MSTORE must have a real expansion cost, got {mstore_cost}"); + KnifeEdge { + code: full_code, + // One gas short of the MSTORE on the EVM's own counter... + gas_limit: before.gas_used + mstore_cost - 1, + // ...and one gas short of it on the compute headroom, so the two coincide exactly and the + // clamp hides nothing at all. + compute_limit: before.compute_gas + mstore_cost - 1, + compute_before: before.compute_gas, + } +} + +/// An exact-value clamp — true remaining equal to the compute headroom, nothing hidden — is still +/// the compute limit doing the stopping, and must be reported as a compute exceed rather than as +/// an ordinary EVM out-of-gas. +/// +/// This is the double-exceed preference at its knife edge: the crossing opcode exhausts both +/// budgets at the same gas, and the compute classification is the one that keeps the sender's +/// remaining gas refundable. +#[test] +fn test_exact_value_clamp_is_still_a_compute_exceed() { + let edge = calibrate_knife_edge(); + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(edge.code.clone()), + compute_limit(edge.compute_limit)(MegaSpecId::REX7), + edge.gas_limit, + ); + + match r7.halt_reason("REX7") { + MegaHaltReason::ComputeGasLimitExceeded { limit, actual } => { + assert_eq!(*limit, edge.compute_limit, "the reported limit is the TX compute limit"); + assert!( + *actual <= edge.compute_limit, + "the crossing opcode never ran, so usage cannot be past the limit; got {actual}", + ); + } + other => panic!( + "an equal-value clamp must classify as a compute exceed, not an ordinary \ + out-of-gas; got {other:?}", + ), + } +} + +/// The neighbouring points on either side of the knife edge classify the way the equal point does +/// or the way an ordinary out-of-gas does, and nothing in between. +/// +/// One gas more of transaction gas puts the true remaining strictly above the headroom, so the +/// clamp hides one gas — the case that already worked. One gas more of compute limit puts the +/// headroom strictly above the true remaining, so the frame's own gas is what runs out and the +/// halt is an ordinary out-of-gas with no compute attribution. +#[test] +fn test_knife_edge_neighbours_classify_by_which_budget_binds() { + let edge = calibrate_knife_edge(); + + let hidden_one = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(edge.code.clone()), + compute_limit(edge.compute_limit)(MegaSpecId::REX7), + edge.gas_limit + 1, + ); + assert!( + matches!( + hidden_one.halt_reason("hidden=1"), + MegaHaltReason::ComputeGasLimitExceeded { .. } + ), + "one gas above the edge the clamp hides one gas and binds; got {:?}", + hidden_one.result + ); + + let gas_bound = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(edge.code.clone()), + compute_limit(edge.compute_limit + 1)(MegaSpecId::REX7), + edge.gas_limit, + ); + assert!( + !matches!( + gas_bound.halt_reason("gas-bound"), + MegaHaltReason::ComputeGasLimitExceeded { .. } + ), + "one gas of headroom above the true remaining makes this the EVM's own out-of-gas; \ + got {:?}", + gas_bound.result + ); + assert!( + gas_bound.compute_gas > edge.compute_before, + "the EVM out-of-gas burns the frame's remainder, which settles as compute", + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index ffc5d561..27d66790 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -4,6 +4,8 @@ //! bit-identical to per-opcode recording, and the two places where the models diverge. //! - `v0_clamp` — V0 gas-clamp enforcement: a crossing opcode is stopped before it executes, and //! the resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. +//! - `clamp_classification` — which constraint a clamp binds to, including the exact-value case, +//! and the ABI payload / halt fields a clamp-induced exceed reports. //! - `checkpoint_families` — one parity case per checkpoint opcode the REX7 table wires, so the set //! is covered exhaustively rather than through representatives. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a @@ -20,6 +22,7 @@ mod checkpoint_families; mod checkpoint_settlement; +mod clamp_classification; mod common; mod double_exceed_corner; mod gas_leakage; From 49647658aaadf5fd3c349412d01ad634c8797213 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:09:37 +0800 Subject: [PATCH 019/208] fix(rex7): settle every exceptional halt's burned remainder as compute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The frame-exit settlement read the interpreter's counter, and the interpreter zeroes that counter only for a plain out-of-gas. Memory OOG, stack underflow/overflow, invalid jump and unknown opcode all keep their loop-exit reading and have their remainder burned later by the frame-return rules, so the settlement saw almost none of it: a transaction that burned its whole million-gas envelope on a memory OOG reported 21,009 compute gas, and that figure feeds the block-level compute accounting. Drive the settlement off the halt classification instead, and cover the whole remainder the frame still held at the last checkpoint, including gas the V0 clamp was hiding from the interpreter. The burn is recorded outside limit enforcement. It is gas the EVM destroyed rather than work the network performed, and it is bounded by the sender's gas envelope rather than by the compute limit, so enforcing it would turn an ordinary EVM halt into a resource-limit failure with the remaining gas rescued — changing a receipt the carve-out requires to stay identical. No enforcement is lost: the executed part of an exceptionally halted frame's tail is bounded by the clamp or by a frame gas remainder that was already under the headroom. --- crates/mega-evm/src/limit/compute_gas.rs | 42 ++- crates/mega-evm/src/limit/limit.rs | 63 +++- crates/mega-evm/tests/compute_gas/main.rs | 42 ++- .../mega-evm/tests/compute_gas/snapshot.txt | 2 +- .../mega-evm/tests/rex7/exceptional_halt.rs | 282 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 3 + 6 files changed, 410 insertions(+), 24 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/exceptional_halt.rs diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 86264157..47fab923 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -52,6 +52,15 @@ pub(crate) struct ComputeGasTracker { /// The effective compute gas limit, which may be dynamically lowered by gas detention /// (volatile data access). Always <= `frame_tracker.tx_limit()`. detained_limit: u64, + /// Compute gas settled from the burned remainders of exceptionally halted frames (REX7+). + /// + /// Recorded into the TX-level lane of `frame_tracker`, so it shows up in the transaction's + /// reported compute total and in block-level accounting, and subtracted back out of every + /// limit comparison. A burned remainder is gas the EVM destroyed, not work the network + /// performed; letting it trip a limit would turn an ordinary EVM halt into a resource-limit + /// failure with the remaining gas rescued for the sender, changing a receipt that must stay + /// identical to per-opcode accounting. Always 0 before REX7. + burned: u64, frame_tracker: FrameLimitTracker<()>, } @@ -59,6 +68,7 @@ impl ComputeGasTracker { pub(crate) fn new(spec: MegaSpecId, tx_limit: u64) -> Self { Self { detained_limit: tx_limit, + burned: 0, frame_tracker: FrameLimitTracker::new(spec, tx_limit), rex1_enabled: spec.is_enabled(MegaSpecId::REX1), rex4_enabled: spec.is_enabled(MegaSpecId::REX4), @@ -88,7 +98,7 @@ impl ComputeGasTracker { pub(crate) fn set_detained_limit(&mut self, cap: u64) { let new_limit = if self.rex4_enabled { // REX4+: cap is relative to current usage (limits post-access computation) - self.tx_usage().saturating_add(cap) + self.enforced_tx_usage().saturating_add(cap) } else { // Pre-REX4: cap is absolute cap @@ -111,7 +121,7 @@ impl ComputeGasTracker { /// At that point `frame_stack.last()` is the caller's frame, so /// `current_frame_remaining()` gives the caller's remaining compute gas. pub(crate) fn current_call_remaining(&self) -> u64 { - let tx_remaining = self.tx_limit().saturating_sub(self.tx_usage()); + let tx_remaining = self.tx_limit().saturating_sub(self.enforced_tx_usage()); if self.rex4_enabled { self.frame_tracker.current_frame_remaining().min(tx_remaining) } else { @@ -141,7 +151,7 @@ impl ComputeGasTracker { #[inline] pub(crate) fn clamp_binding(&self) -> ClampBinding { let tx_limit = self.tx_limit(); - let tx_remaining = tx_limit.saturating_sub(self.tx_usage()); + let tx_remaining = tx_limit.saturating_sub(self.enforced_tx_usage()); if self.rex4_enabled { let frame_remaining = self.frame_tracker.current_frame_remaining(); if frame_remaining < tx_remaining { @@ -154,7 +164,7 @@ impl ComputeGasTracker { /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained /// limit is tighter than the base TX limit AND actual usage exceeds it. pub(crate) fn is_detained_exceed(&self) -> bool { - let used = self.tx_usage(); + let used = self.enforced_tx_usage(); used > self.detained_limit && self.detained_limit < self.frame_tracker.tx_limit() } @@ -187,6 +197,21 @@ impl ComputeGasTracker { } } + /// Records a burned remainder from an exceptionally halted frame (REX7+). + /// + /// Counts toward the transaction's reported compute total and block-level accounting, and is + /// excluded from every limit comparison — see [`burned`](Self::burned). + pub(crate) fn record_burned_gas(&mut self, amount: u64) { + self.burned = self.burned.saturating_add(amount); + self.frame_tracker.add_tx_persistent(amount); + } + + /// Total recorded usage minus the burned remainders that must not enforce. + #[inline] + fn enforced_tx_usage(&self) -> u64 { + self.frame_tracker.net_usage().saturating_sub(self.burned) + } + /// Merges external persistent usage into the TX-level entry. /// /// Used by `KeylessDeploy` (REX5+) to propagate sandbox compute gas consumption @@ -213,6 +238,7 @@ impl TxRuntimeLimit for ComputeGasTracker { #[inline] fn reset(&mut self) { self.frame_tracker.reset(); + self.burned = 0; // Rex1+: reset detained limit to original TX limit between transactions. // Pre-Rex1: the detained limit persists across transactions. if self.rex1_enabled { @@ -244,14 +270,16 @@ impl TxRuntimeLimit for ComputeGasTracker { // So TX-level detained check must still run even when frame check is within limit. } // TX-level detained check (all specs): total usage vs effective limit (min of tx/detained). + // The comparison runs on enforced usage — burned remainders are excluded — while the + // reported `used` is the full settled total, so a halt reason states the usage the + // transaction actually ends with. The two coincide on every spec before REX7. let limit = self.tx_limit(); - let used = self.tx_usage(); - if used > limit { + if self.enforced_tx_usage() > limit { LimitCheck::ExceedsLimit { kind: LimitKind::ComputeGas, frame_local: false, limit, - used, + used: self.tx_usage(), } } else { LimitCheck::WithinLimit diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 88473068..f80e23ae 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -133,6 +133,15 @@ pub struct AdditionalLimit { /// [`settle_frame_final_result`](Self::settle_frame_final_result). clamp: Option, + /// The clamp-hidden gas [`settle_frame_final_result`](Self::settle_frame_final_result) just + /// handed back to the frame's result, carried to the frame-exit settlement in + /// [`after_frame_run_instructions`](Self::after_frame_run_instructions). + /// + /// The two hooks are split by the execution layer's code-deposit charge, which has to observe + /// unclamped gas; the settlement that follows still needs to know how much of the frame's true + /// remainder the interpreter could not see. Written and consumed on the same frame exit. + restored_clamp_hidden: u64, + /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level /// constraint. /// @@ -187,6 +196,7 @@ impl AdditionalLimit { checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), checkpoint_baseline: 0, clamp: None, + restored_clamp_hidden: 0, clamp_latched_detained: false, } } @@ -231,6 +241,7 @@ impl AdditionalLimit { self.storage_call_stipend.reset(); self.checkpoint_baseline = 0; self.clamp = None; + self.restored_clamp_hidden = 0; self.clamp_latched_detained = false; } @@ -322,8 +333,9 @@ impl AdditionalLimit { self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); } - /// Restores any outstanding V0 clamp into the frame's final interpreter result, and latches a - /// clamp-induced out-of-gas as a compute exceed. + /// Finalises the compute accounting a frame's own result decides: restores any outstanding V0 + /// clamp, latches a clamp-induced out-of-gas as a compute exceed, and settles an exceptional + /// halt's burned remainder. /// /// Must run before anything reads or charges the result's gas — in particular before the /// execution-layer code-deposit storage charge, which would otherwise observe the clamped copy @@ -340,8 +352,12 @@ impl AdditionalLimit { if !self.checkpoint_accounting { return; } + self.restored_clamp_hidden = 0; if let Some(clamp) = self.clamp.take() { result.gas.erase_cost(clamp.hidden); + // Handed to the frame-exit settlement, which runs after the execution layer's + // code-deposit charge and can no longer see the clamp itself. + self.restored_clamp_hidden = clamp.hidden; // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every // other result either is unrelated to gas or cannot arise from a plain opcode. if matches!(result.result, InstructionResult::OutOfGas | InstructionResult::MemoryOOG) { @@ -953,12 +969,22 @@ impl AdditionalLimit { // `before_frame_return_result` surfaces it. The clamp restore itself already happened, in // `settle_frame_final_result`, before the execution-layer hook charged code-deposit storage // gas against the action's gas. + // + // A frame that ended in an exceptional halt takes the burn branch instead: it returns none + // of its remaining budget, so the whole remainder — not just what the counter shows — + // settles, and it settles outside limit enforcement. See `settle_exceptional_halt_burn`. if self.checkpoint_accounting { - if let InterpreterAction::Return(_) = action { + if let InterpreterAction::Return(interpreter_result) = action { + let exceptional_halt = !interpreter_result.result.is_ok_or_revert(); + let hidden = core::mem::take(&mut self.restored_clamp_hidden); let remaining = frame.interpreter.gas.remaining(); - let gas_used = self.checkpoint_baseline.saturating_sub(remaining); + if exceptional_halt && !self.limit_exceeded() { + self.settle_exceptional_halt_burn(hidden); + } else { + let gas_used = self.checkpoint_baseline.saturating_sub(remaining); + let _ = self.record_compute_gas_unguarded(gas_used); + } self.checkpoint_baseline = remaining; - let _ = self.record_compute_gas_unguarded(gas_used); } } @@ -1052,6 +1078,33 @@ impl AdditionalLimit { } } + /// Settles the entire remainder an exceptionally halted frame burns, as compute gas. + /// + /// An exceptional halt returns no gas: the top-level frame's whole envelope is spent by the + /// transaction's final gas accounting, and an inner frame's remainder is simply never handed + /// back to its caller. The interpreter zeroes its own counter only for a plain `OutOfGas`, + /// so the frame-exit delta cannot see the burn on any other classification. This settles it + /// directly instead: everything the frame still held at the last checkpoint — the open segment + /// (`baseline`, measured against a zero remainder) plus `hidden`, the part of the true + /// remainder the clamp was keeping out of the interpreter's sight. + /// + /// The whole amount goes to the tracker's non-enforcing lane, not just the part beyond the + /// compute headroom. Nothing is lost by that: a segment that runs under a clamp can never + /// consume past the headroom (the clamp is the enforcement), and a segment with no clamp is + /// bounded by a frame gas remainder that was already below the headroom — so the executed part + /// of an exceptionally halted frame's tail could not have exceeded a limit either way. What + /// enforcing the *burn* would do is turn an ordinary EVM halt into a resource-limit failure + /// with the remaining gas rescued for the sender, which is exactly the receipt change the + /// exceptional-halt carve-out forbids. + /// + /// Not reached when a resource limit is already latched: that path burns nothing, because the + /// frame either reverts to its parent (frame-local) or halts the transaction with its gas + /// rescued (TX-level) — including a clamp-induced out-of-gas, which + /// [`settle_frame_final_result`](Self::settle_frame_final_result) latches just before this. + fn settle_exceptional_halt_burn(&mut self, hidden: u64) { + self.compute_gas.record_burned_gas(self.checkpoint_baseline.saturating_add(hidden)); + } + /// Merges resource usage from a sandbox execution into this tracker. /// /// Used by `KeylessDeploy` (REX5+) to propagate sandbox resource consumption diff --git a/crates/mega-evm/tests/compute_gas/main.rs b/crates/mega-evm/tests/compute_gas/main.rs index 4cae4622..b2e0c0ab 100644 --- a/crates/mega-evm/tests/compute_gas/main.rs +++ b/crates/mega-evm/tests/compute_gas/main.rs @@ -849,33 +849,53 @@ fn test_compute_gas_snapshot_matches() { } } -/// Rex7 is the unstable spec and carries no behavior of its own yet: it delegates its instruction -/// table, runtime limits, and precompile set to Rex6 unchanged. +/// Rex7's checkpoint settlement is a precision-preserving change: every program that stays inside +/// its resource limits records the same compute gas, spends the same EVM gas, and ends the same +/// way as it does under Rex6. /// /// The snapshot alone does not pin this. Its rows differ by the spec-name column, so a Rex7 row /// that drifted from its Rex6 counterpart would still render as a well-formed snapshot and could be /// blessed by a regeneration. Comparing the readings directly makes the first accidental Rex7 -/// divergence a failure. When Rex7 gains its first deliberate behavior change, this test is -/// expected to fail and should be narrowed to the corpus entries that behavior does not reach. +/// divergence a failure. +/// +/// The one sanctioned divergence is the exceptional-halt carve-out: a frame that halts +/// exceptionally returns none of its remaining budget, and Rex7 settles that burned remainder as +/// compute gas where per-opcode recording attributes nothing to it. That moves compute gas upward +/// only — the receipt and the outcome still have to match exactly. `tests/rex7/exceptional_halt.rs` +/// pins the settled amount itself. #[test] fn test_rex7_matches_rex6_on_every_program() { for program in corpus() { let rex6 = transact(MegaSpecId::REX6, (program.build_db)()); let rex7 = transact(MegaSpecId::REX7, (program.build_db)()); assert_eq!( - (rex7.compute_gas, rex7.gas_used, &rex7.outcome), - (rex6.compute_gas, rex6.gas_used, &rex6.outcome), - "{}: Rex7 must be behaviorally identical to Rex6 \ - (Rex6: compute_gas={} gas_used={} outcome={}; \ - Rex7: compute_gas={} gas_used={} outcome={})", + (rex7.gas_used, &rex7.outcome), + (rex6.gas_used, &rex6.outcome), + "{}: Rex7 must spend the same gas and end the same way as Rex6 \ + (Rex6: gas_used={} outcome={}; Rex7: gas_used={} outcome={})", program.name, - rex6.compute_gas, rex6.gas_used, rex6.outcome, - rex7.compute_gas, rex7.gas_used, rex7.outcome, ); + if rex7.outcome.starts_with("halt ") { + assert!( + rex7.compute_gas >= rex6.compute_gas, + "{}: the exceptional-halt carve-out only ever moves compute gas up \ + (Rex6={} Rex7={})", + program.name, + rex6.compute_gas, + rex7.compute_gas, + ); + continue; + } + assert_eq!( + rex7.compute_gas, rex6.compute_gas, + "{}: checkpoint settlement must telescope to the per-opcode sum \ + (Rex6={} Rex7={})", + program.name, rex6.compute_gas, rex7.compute_gas, + ); } } diff --git a/crates/mega-evm/tests/compute_gas/snapshot.txt b/crates/mega-evm/tests/compute_gas/snapshot.txt index d6e976fb..c417e4b2 100644 --- a/crates/mega-evm/tests/compute_gas/snapshot.txt +++ b/crates/mega-evm/tests/compute_gas/snapshot.txt @@ -274,7 +274,7 @@ create2_oversized_initcode Rex3 21012 100000000 halt Ba create2_oversized_initcode Rex4 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode Rex5 763907 100000000 halt Base(Base(CreateInitCodeSizeLimit)) create2_oversized_initcode Rex6 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) -create2_oversized_initcode Rex7 21012 100000000 halt Base(Base(CreateInitCodeSizeLimit)) +create2_oversized_initcode Rex7 99961000 100000000 halt Base(Base(CreateInitCodeSizeLimit)) selfdestruct_to_empty Equivalence 0 53603 success selfdestruct_to_empty MiniRex 21003 100000000 halt Base(Base(InvalidFEOpcode)) diff --git a/crates/mega-evm/tests/rex7/exceptional_halt.rs b/crates/mega-evm/tests/rex7/exceptional_halt.rs new file mode 100644 index 00000000..db189e6c --- /dev/null +++ b/crates/mega-evm/tests/rex7/exceptional_halt.rs @@ -0,0 +1,282 @@ +//! REX7 exceptional-halt frame settlement. +//! +//! A frame that ends in an exceptional halt never returns its remaining budget: the top-level +//! frame's whole envelope is spent by the transaction's final gas accounting, and an inner frame's +//! remainder is simply not handed back to its caller. REX7 settles that burned remainder as compute +//! gas at frame exit, so the recorded compute total covers the entire budget the sender's gas paid +//! for. Per-opcode recording through REX6 attributes neither the failing opcode nor the burn, so it +//! reports strictly less. +//! +//! The interpreter only zeroes its own counter for a plain `OutOfGas`; every other exceptional halt +//! keeps the loop-exit reading and has its remainder burned later, by the frame-return rules. The +//! settlement therefore cannot be read off the counter — it has to be driven by the halt +//! classification, which is what these tests sweep. +//! +//! The invariant each case pins is the same one in both frame positions. These transactions spend +//! EVM gas on exactly two things: compute, and the transaction-intrinsic storage gas that is +//! excluded from compute accounting by definition. So "the whole burned budget settled as compute" +//! is `compute_gas == gas_used − intrinsic storage gas`. At the top level that covers the entire +//! transaction envelope; in the nested shape it covers the caller's own consumption plus the whole +//! budget it forwarded, because the halting callee returns none of it. +//! +//! `MemoryLimitOOG` is not in the sweep: it needs revm's `memory_limit` cfg, which this workspace +//! does not enable, so no bytecode can reach it. + +use crate::common::{ + transact, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH, +}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{ + ADD, CALL, DUP1, JUMP, JUMPDEST, JUMPI, MSTORE, POP, STOP, SUB, SWAP1, +}; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// The two transaction-intrinsic readings every case below calibrates against, measured from a +/// transaction that runs a single `STOP`: the total EVM gas the receipt charges before the frame +/// does anything (`gas`), and the part of it that counts as compute (`compute`). +/// +/// The difference is the intrinsic storage gas — charged to EVM gas but excluded from compute +/// accounting. Measuring it keeps the budget identity exact rather than pinned to a constant. +struct Intrinsic { + gas: u64, + compute: u64, +} + +impl Intrinsic { + fn measure(spec: MegaSpecId) -> Self { + let code = BytecodeBuilder::default().append(STOP).build(); + let outcome = transact_with_gas_limit( + spec, + base_db(code), + EvmTxRuntimeLimits::from_spec(spec), + 1_000_000, + ); + Self { gas: outcome.gas_used, compute: outcome.compute_gas } + } + + /// The storage gas the receipt carries that compute accounting never sees. + fn storage_gas(&self) -> u64 { + self.gas - self.compute + } +} + +/// A countdown loop of cheap plain opcodes, sized to outrun any budget below its own cost. +fn countdown_loop_code(iterations: u16) -> Vec { + let mut code = vec![0x61]; // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + code +} + +/// One exceptional-halt shape. +struct HaltCase { + /// Case name, used in assertion messages. + name: &'static str, + /// Bytecode that ends the frame it runs in with an exceptional halt. + code: Vec, + /// The budget the halting frame is given — large enough for the shape to reach its halt with + /// gas to spare, so the burned remainder is substantial. + frame_gas: u64, +} + +/// Every exceptional-halt classification a plain-opcode segment can produce, other than the +/// `memory_limit` cfg-gated one. +fn halt_cases() -> Vec { + vec![ + // Plain out-of-gas: the interpreter zeroes its own counter here, so this is the one shape + // that already settled its burn before the classification-driven settlement existed. + HaltCase { name: "plain OOG", code: countdown_loop_code(10_000), frame_gas: 60_000 }, + // Memory out-of-gas: expanding to one MiB costs ~2.2M gas, far past the budget. + HaltCase { + name: "memory OOG", + code: BytecodeBuilder::default() + .push_number(0u64) + .push_number(0x10_0000u64) + .append(MSTORE) + .append(STOP) + .build_vec(), + frame_gas: 60_000, + }, + // Stack underflow: ADD with nothing on the stack. + HaltCase { + name: "stack underflow", + code: BytecodeBuilder::default().append(ADD).append(STOP).build_vec(), + frame_gas: 60_000, + }, + // Stack overflow: each iteration pushes one word and jumps back, so the stack passes 1024 + // long before the budget runs out. + HaltCase { + name: "stack overflow", + code: vec![JUMPDEST, 0x60, 0x01, 0x60, 0x00, JUMP], + frame_gas: 60_000, + }, + // Invalid jump: a destination that is not a JUMPDEST. + HaltCase { + name: "invalid jump", + code: BytecodeBuilder::default().push_number(0xffu64).append(JUMP).build_vec(), + frame_gas: 60_000, + }, + // Unknown opcode: 0x0c is unassigned on every spec this table covers. + HaltCase { name: "unknown opcode", code: vec![0x0c], frame_gas: 60_000 }, + ] +} + +/// Runs `code` as the transaction's direct target, with exactly `frame_gas` beyond intrinsic. +fn top_level(spec: MegaSpecId, code: &[u8], frame_gas: u64) -> Outcome { + transact_with_gas_limit( + spec, + base_db(Bytes::copy_from_slice(code)), + EvmTxRuntimeLimits::from_spec(spec), + Intrinsic::measure(spec).gas + frame_gas, + ) +} + +/// Runs `code` in an inner frame that [`CONTRACT`] calls with exactly `frame_gas` forwarded, then +/// pops the failure flag and stops — so the caller survives its callee's halt. +fn nested(spec: MegaSpecId, code: &[u8], frame_gas: u64) -> Outcome { + let caller = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(frame_gas) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = base_db(caller).account_code(CALLEE, Bytes::copy_from_slice(code)); + transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) +} + +/// The frame's entire budget must settle as compute gas, in both frame positions, for every +/// exceptional-halt classification. +#[test] +fn test_every_exceptional_halt_settles_its_burned_budget_as_compute() { + /// Runs one halt shape in one frame position. + type Runner = fn(MegaSpecId, &[u8], u64) -> Outcome; + + let storage_gas = Intrinsic::measure(MegaSpecId::REX7).storage_gas(); + for case in halt_cases() { + for (position, run) in [("top-level", top_level as Runner), ("nested", nested as Runner)] { + let label = format!("{} ({position})", case.name); + let r6 = run(MegaSpecId::REX6, &case.code, case.frame_gas); + let r7 = run(MegaSpecId::REX7, &case.code, case.frame_gas); + + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "{label}: the halt itself must be unchanged", + ); + assert_eq!( + r6.gas_used, r7.gas_used, + "{label}: receipt gas_used must be unchanged; REX6={} REX7={}", + r6.gas_used, r7.gas_used + ); + assert_eq!( + r7.compute_gas, + r7.gas_used - storage_gas, + "{label}: REX7 must settle the whole burned budget as compute; \ + compute={} gas_used={} intrinsic storage gas={storage_gas}", + r7.compute_gas, + r7.gas_used, + ); + assert!( + r6.compute_gas < r7.compute_gas, + "{label}: per-opcode recording attributes neither the failing opcode nor the \ + burn, so it must report strictly less; REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); + } + } +} + +/// A frame that halts exceptionally while a non-zero gas clamp is outstanding burns the hidden gas +/// too, so the settlement has to cover the true remainder, not just the visible one. +/// +/// A tight compute limit keeps a large amount hidden; the frame then hits a stack underflow, which +/// is not a gas shortage at all and must not be reclassified into a compute exceed. +#[test] +fn test_exceptional_halt_under_an_active_clamp_settles_the_hidden_gas_too() { + let code = BytecodeBuilder::default().append(ADD).append(STOP).build(); + let intrinsic = Intrinsic::measure(MegaSpecId::REX7); + let limits = |spec| { + EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(intrinsic.compute + 1_000) + }; + let gas_limit = intrinsic.gas + 500_000; + + let r6 = transact_with_gas_limit( + MegaSpecId::REX6, + base_db(code.clone()), + limits(MegaSpecId::REX6), + gas_limit, + ); + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(code), + limits(MegaSpecId::REX7), + gas_limit, + ); + + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the stack underflow must not be reclassified as a resource-limit exceed", + ); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); + assert_eq!( + r7.compute_gas, + intrinsic.compute + 500_000, + "the clamp hides most of the frame's budget, and all of it is still burned", + ); +} + +/// The burn settlement must not retroactively fail the transaction. +/// +/// The burned budget is whatever the sender's gas envelope allowed, not what the compute limit +/// allowed, so the settlement can push the recorded total past the compute limit. Turning that into +/// a compute-limit halt would rescue gas the EVM already burned and change the receipt, which the +/// exceptional-halt carve-out must not do. +#[test] +fn test_burn_settlement_does_not_retroactively_halt_on_the_compute_limit() { + let code = BytecodeBuilder::default().append(ADD).append(STOP).build(); + let intrinsic = Intrinsic::measure(MegaSpecId::REX7); + let compute_limit = intrinsic.compute + 1_000; + let limits = + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(compute_limit); + + let r7 = + transact_with_gas_limit(MegaSpecId::REX7, base_db(code), limits, intrinsic.gas + 500_000); + + let reason = format!("{:?}", r7.halt_reason("REX7")); + assert!( + reason.contains("StackUnderflow"), + "the halt must stay the EVM's own, not become a compute-limit exceed; got {reason}", + ); + assert!( + r7.compute_gas > compute_limit, + "the settled burn is expected to exceed the compute limit here; compute={} limit={}", + r7.compute_gas, + compute_limit + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 27d66790..8a0847bb 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -19,12 +19,15 @@ //! the REX5 storage-call stipend, and oracle hints. //! - `double_exceed_corner` — the adjudicated corner swept one gas at a time, so the classification //! is shown to be stable rather than merely correct at one point. +//! - `exceptional_halt` — every exceptional-halt classification, in both frame positions: the +//! frame's whole burned budget settles as compute gas without changing the receipt. mod checkpoint_families; mod checkpoint_settlement; mod clamp_classification; mod common; mod double_exceed_corner; +mod exceptional_halt; mod gas_leakage; mod interceptor_resume; mod latch_surfacing; From 1b3008e73806751e3f490641c375aa139370be22 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:10:53 +0800 Subject: [PATCH 020/208] fix(rex7): report the binding budget in a clamp-induced exceed A clamp bound to a sub-frame's compute budget latched the transaction-level limit into the exceed. The frame-local revert then carried that number in its MegaLimitExceeded payload, where the calling contract can decode it and branch on it: the same nested call that reverts with limit=956851 under per-opcode enforcement reverted with limit=1000000 under the clamp. Carry the binding constraint's own limit on the clamp and latch that, so both paths report the budget that actually stopped execution. --- crates/mega-evm/src/limit/compute_gas.rs | 13 ++- crates/mega-evm/src/limit/frame_limit.rs | 11 ++ crates/mega-evm/src/limit/limit.rs | 11 +- .../tests/rex7/clamp_classification.rs | 107 +++++++++++++++++- 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 47fab923..d6e61fef 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -18,6 +18,11 @@ pub(crate) struct ClampBinding { /// `true` when the current frame's compute budget is what binds, `false` when the TX-level /// (possibly detained) limit is. pub(crate) frame_local: bool, + /// The binding constraint's own limit. A clamp-induced exceed reports this — as + /// `MegaLimitExceeded.limit` in the frame-local revert payload, or as + /// `ComputeGasLimitExceeded.limit` in the transaction halt — so it has to be the budget that + /// actually stopped execution, exactly as the non-clamp check path reports it. + pub(crate) limit: u64, } /// A frame-limit-based compute gas tracker using `FrameLimitTracker`. @@ -155,10 +160,14 @@ impl ComputeGasTracker { if self.rex4_enabled { let frame_remaining = self.frame_tracker.current_frame_remaining(); if frame_remaining < tx_remaining { - return ClampBinding { headroom: frame_remaining, frame_local: true }; + return ClampBinding { + headroom: frame_remaining, + frame_local: true, + limit: self.frame_tracker.current_frame_limit(), + }; } } - ClampBinding { headroom: tx_remaining, frame_local: false } + ClampBinding { headroom: tx_remaining, frame_local: false, limit: tx_limit } } /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained diff --git a/crates/mega-evm/src/limit/frame_limit.rs b/crates/mega-evm/src/limit/frame_limit.rs index 03d1a34b..8d05ac6b 100644 --- a/crates/mega-evm/src/limit/frame_limit.rs +++ b/crates/mega-evm/src/limit/frame_limit.rs @@ -236,6 +236,17 @@ impl FrameLimitTracker { } } + /// Returns the budget of the current frame, in the same form + /// [`exceeds_current_frame_limit`](Self::exceeds_current_frame_limit) reports it on an exceed. + /// + /// If the frame stack is empty (before the first frame is pushed), returns the TX-level limit. + pub(crate) fn current_frame_limit(&self) -> u64 { + match self.frame_stack.last() { + Some(entry) => entry.limit, + None => self.tx_entry.limit, + } + } + /// Returns a mutable reference to the current (top) frame entry. pub(crate) fn frame_mut(&mut self) -> Option<&mut FrameLimitEntry> { self.frame_stack.last_mut() diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index f80e23ae..887bda1d 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -311,10 +311,11 @@ impl AdditionalLimit { /// /// The crossing opcode never executed — revm's own gas check stopped it at the clamp boundary — /// so its cost is not in the recorded usage and an ordinary [`check_limit`](Self::check_limit) - /// pass sees usage at or below the limit. The latch is therefore stamped directly, with - /// `frame_local` taken from the constraint that bound the clamp, so the existing frame-result - /// machinery (frame-local absorb to revert; TX-level mark plus gas rescue) produces the halt - /// shape it produces for every other compute exceed. + /// pass sees usage at or below the limit. The latch is therefore stamped directly, from the + /// constraint that bound the clamp: `frame_local` decides the shape the existing frame-result + /// machinery produces (frame-local absorb to revert; TX-level mark plus gas rescue), and the + /// constraint's own `limit` is what that shape reports — the sub-frame budget for a frame-local + /// binding, the effective TX limit otherwise, matching what the non-clamp check path writes. #[inline] fn latch_clamp_exceed(&mut self, binding: &compute_gas::ClampBinding) { if !self.has_exceeded_limit.within_limit() { @@ -323,7 +324,7 @@ impl AdditionalLimit { self.has_exceeded_limit = LimitCheck::ExceedsLimit { kind: super::LimitKind::ComputeGas, frame_local: binding.frame_local, - limit: self.compute_gas.tx_limit(), + limit: binding.limit, used: self.compute_gas.tx_usage(), }; // Preserve the volatile-detention attribution: when the binding TX-level constraint at diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index a775b343..b0a5a79d 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -6,14 +6,28 @@ //! exactly *on* it changes how much gets hidden — zero in the second case — but not whether the //! clamp is in force. Both are the compute limit doing the stopping, and both must be reported as //! such; only a frame whose own EVM gas runs out first is an ordinary out-of-gas. +//! +//! The payload has to match too. A frame-local binding reverts with +//! `MegaLimitExceeded(uint8 kind, uint64 limit)`, which the caller can decode and branch on, so its +//! `limit` must be the sub-frame budget that actually bound the clamp rather than the +//! transaction-level limit. -use crate::common::{transact_default, transact_with_gas_limit, CALLER, CONTRACT, ONE_ETH}; +use crate::common::{ + transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH, +}; use alloy_primitives::{Bytes, U256}; +use alloy_sol_types::SolError; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, + EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{ + CALL, DUP1, JUMPDEST, JUMPI, MSTORE, POP, RETURN, RETURNDATACOPY, RETURNDATASIZE, STOP, + SUB, SWAP1, + }, + context::result::ExecutionResult, }; -use revm::bytecode::opcode::{MSTORE, POP, STOP}; fn base_db(code: Bytes) -> MemoryDatabase { MemoryDatabase::default() @@ -169,3 +183,90 @@ fn test_knife_edge_neighbours_classify_by_which_budget_binds() { "the EVM out-of-gas burns the frame's remainder, which settles as compute", ); } + +// --------------------------------------------------------------------------------------------- +// The payload a clamp-induced exceed reports. +// --------------------------------------------------------------------------------------------- + +/// A countdown loop of cheap plain opcodes, prefixed verbatim. +fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// A caller that CALLs [`CALLEE`] and returns the sub-frame's return data verbatim, so the +/// `MegaLimitExceeded` payload the sub-frame reverted with is observable from the receipt. +fn call_and_return_revert_data(gas: u64) -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(gas) + .append(CALL) + .append(POP) + .append(RETURNDATASIZE) + .push_number(0u64) // dataOffset + .push_number(0u64) // destOffset + .append(RETURNDATACOPY) + .append(RETURNDATASIZE) + .push_number(0u64) // offset + .append(RETURN) + .build() +} + +/// Decodes the `MegaLimitExceeded` payload a successful transaction returned. +fn decode_limit_exceeded(label: &str, outcome: &Outcome) -> MegaLimitExceeded { + let output = match &outcome.result { + ExecutionResult::Success { output, .. } => output.data().clone(), + other => panic!("{label}: expected success carrying the sub-frame payload, got {other:?}"), + }; + MegaLimitExceeded::abi_decode(&output) + .unwrap_or_else(|e| panic!("{label}: return data is not MegaLimitExceeded: {e}")) +} + +/// A frame-local clamp exceed must report the sub-frame budget that bound it, byte for byte the +/// same payload per-opcode enforcement produces. +/// +/// The revert data is visible to the calling contract, which can decode `limit` and branch on it, +/// so a transaction-level value here is not a diagnostic difference — it is a different observable +/// return value for the same execution. +#[test] +fn test_frame_local_clamp_exceed_reports_the_sub_frame_budget() { + let callee = countdown_loop_code(&[], 40_000); + let code = call_and_return_revert_data(50_000_000); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let limits = compute_limit(1_000_000); + + let r6 = transact(MegaSpecId::REX6, build_db(), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, build_db(), limits(MegaSpecId::REX7)); + + let d6 = decode_limit_exceeded("REX6", &r6); + let d7 = decode_limit_exceeded("REX7", &r7); + + assert_eq!(d6.kind, LimitKind::ComputeGas.as_u8(), "REX6 must blame compute gas"); + assert_eq!(d7.kind, d6.kind, "REX7 must blame the same dimension"); + assert_eq!( + d7.limit, d6.limit, + "the ABI-visible limit must be the sub-frame budget on both specs; REX6={} REX7={}", + d6.limit, d7.limit + ); + assert!( + d7.limit < 1_000_000, + "the sub-frame budget is a fraction of the TX limit, not the TX limit itself; got {}", + d7.limit + ); +} From a3d41e1c5ea59298adb75cefc7eb5a9c873ce51e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:12:24 +0800 Subject: [PATCH 021/208] fix(rex7): report the final compute usage in a clamp-induced halt The clamp exceed is latched at the frame's final result, and the frame-exit settlement that closes the partial plain segment runs after it. The latch is sticky, so the halt reason kept the pre-settlement snapshot: a transaction ending on 21,500 compute gas reported ComputeGasLimitExceeded.actual = 21,000. Re-read the usage from the tracker once the settlement has closed, which is what the detention path already effectively does by rebuilding its reason from live usage. --- crates/mega-evm/src/limit/limit.rs | 27 ++++++++++ .../tests/rex7/clamp_classification.rs | 53 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 887bda1d..c122874a 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -984,6 +984,7 @@ impl AdditionalLimit { } else { let gas_used = self.checkpoint_baseline.saturating_sub(remaining); let _ = self.record_compute_gas_unguarded(gas_used); + self.refresh_latched_compute_usage(); } self.checkpoint_baseline = remaining; } @@ -1079,6 +1080,32 @@ impl AdditionalLimit { } } + /// Re-reads a latched TX-level compute exceed's usage from the tracker (REX7+). + /// + /// A clamp-induced exceed is latched at the frame's final result, before the frame-exit + /// settlement closes the plain segment the crossing opcode stopped inside. The latch is sticky, + /// so the halt reason built later from [`check_limit`](Self::check_limit) would otherwise + /// report the usage as it stood one settlement short of final — which is not the number the + /// transaction's compute total ends on. The detention path never had this problem: it rebuilds + /// its halt reason from live tracker usage. + /// + /// Only TX-level exceeds are refreshed. A frame-local exceed's `used` is the frame's own + /// figure, which the frame-local revert payload does not carry, so rewriting it with a + /// transaction-level total would only blur what it means. + #[inline] + fn refresh_latched_compute_usage(&mut self) { + let usage = self.compute_gas.tx_usage(); + if let LimitCheck::ExceedsLimit { + kind: super::LimitKind::ComputeGas, + frame_local: false, + used, + .. + } = &mut self.has_exceeded_limit + { + *used = usage; + } + } + /// Settles the entire remainder an exceptionally halted frame burns, as compute gas. /// /// An exceptional halt returns no gas: the top-level frame's whole envelope is spent by the diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index b0a5a79d..55caa9af 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -10,7 +10,9 @@ //! The payload has to match too. A frame-local binding reverts with //! `MegaLimitExceeded(uint8 kind, uint64 limit)`, which the caller can decode and branch on, so its //! `limit` must be the sub-frame budget that actually bound the clamp rather than the -//! transaction-level limit. +//! transaction-level limit. A transaction-level binding halts with `ComputeGasLimitExceeded`, whose +//! `actual` must be the compute usage the transaction ends with — including the frame-exit +//! settlement that runs after the exceed is latched. use crate::common::{ transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH, @@ -125,9 +127,9 @@ fn test_exact_value_clamp_is_still_a_compute_exceed() { match r7.halt_reason("REX7") { MegaHaltReason::ComputeGasLimitExceeded { limit, actual } => { assert_eq!(*limit, edge.compute_limit, "the reported limit is the TX compute limit"); - assert!( - *actual <= edge.compute_limit, - "the crossing opcode never ran, so usage cannot be past the limit; got {actual}", + assert_eq!( + *actual, r7.compute_gas, + "the reported usage must be the transaction's final compute usage", ); } other => panic!( @@ -270,3 +272,46 @@ fn test_frame_local_clamp_exceed_reports_the_sub_frame_budget() { d7.limit ); } + +/// A transaction-level clamp exceed must report the usage the transaction actually ends with. +/// +/// The exceed is latched at the frame's final result, but the frame-exit settlement that closes +/// the partial plain segment runs after that. A halt reason frozen at latch time reports a usage +/// the transaction never had. +#[test] +fn test_tx_level_clamp_halt_reports_the_final_usage() { + let mut code = plain_filler(200); + code.push(STOP); + let code = Bytes::from(code); + + // The unconstrained run tells us both ends of the plain segment; putting the limit in the + // middle of it guarantees the halt lands with a partial segment still unsettled. + let free = transact_default(MegaSpecId::REX7, base_db(code.clone())); + assert!(free.is_success(), "the unconstrained run must succeed: {:?}", free.result); + let intrinsic = transact_default(MegaSpecId::REX7, base_db(Bytes::from(vec![STOP]))); + let midpoint = (intrinsic.compute_gas + free.compute_gas) / 2; + + let r7 = transact(MegaSpecId::REX7, base_db(code), compute_limit(midpoint)(MegaSpecId::REX7)); + + let (limit, actual) = match r7.halt_reason("REX7") { + MegaHaltReason::ComputeGasLimitExceeded { limit, actual } => (*limit, *actual), + other => panic!("expected a compute-gas halt, got {other:?}"), + }; + assert_eq!(limit, midpoint, "the reported limit is the configured TX compute limit"); + assert_eq!( + r7.compute_gas, midpoint, + "clamp enforcement stops the crossing opcode, so usage lands exactly on the limit", + ); + assert_eq!( + actual, r7.compute_gas, + "the reported usage must be the tracker's final reading, not a pre-settlement snapshot; \ + reported={actual} tracker={}", + r7.compute_gas + ); + assert_eq!( + r7.gas_used, + r7.compute_gas + (intrinsic.gas_used - intrinsic.compute_gas), + "the receipt must charge exactly the compute the transaction was allowed plus the \ + intrinsic storage gas", + ); +} From 4a4f53c85c8ffb13f3477e14d0c0c2ddf0690eda Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:14:23 +0800 Subject: [PATCH 022/208] test(rex7): compare state in the shared parity assertion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The helper's contract said the two runs must be indistinguishable, and the precision invariant names state explicitly, but the assertion never looked at it: two specs producing the same result and the same usage from different account or storage state passed. Compare a normalised view — account info, code, status flags, and each slot's original/present pair. Raw EvmState carries journal bookkeeping (`transaction_id`, per-slot `is_cold`) that identical runs can legitimately differ on. --- crates/mega-evm/tests/rex7/common.rs | 70 +++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index c3b3d8ed..4f52d7f9 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -1,6 +1,6 @@ //! Shared helpers for the REX7 test suite. -use alloy_primitives::{address, Address, Bytes, U256}; +use alloy_primitives::{address, Address, Bytes, B256, U256}; use mega_evm::{ test_utils::MemoryDatabase, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, @@ -10,6 +10,7 @@ use revm::{ handler::EvmTr, state::EvmState, }; +use std::collections::BTreeMap; /// Transaction sender. pub(crate) const CALLER: Address = address!("0000000000000000000000000000000000300000"); @@ -172,8 +173,57 @@ pub(crate) fn transact_tx( } } +/// The part of an account a transaction's state actually asserts. +/// +/// Raw [`EvmState`] cannot be compared directly: `Account::transaction_id` and each storage slot's +/// `is_cold` are journal bookkeeping with no consensus meaning, and two runs that produce identical +/// state can still differ there. This keeps the account info, the deployed code, the status flags +/// that decide how the account is applied, and every slot's original/present pair. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct AccountView { + balance: U256, + nonce: u64, + code_hash: B256, + code: Bytes, + touched: bool, + created: bool, + selfdestructed: bool, + loaded_as_not_existing: bool, + storage: BTreeMap, +} + +/// Normalises an [`EvmState`] into a stable, order-independent view. +pub(crate) fn state_view(state: &EvmState) -> BTreeMap { + state + .iter() + .map(|(address, account)| { + let view = AccountView { + balance: account.info.balance, + nonce: account.info.nonce, + code_hash: account.info.code_hash, + code: account + .info + .code + .as_ref() + .map(|code| code.original_bytes()) + .unwrap_or_default(), + touched: account.is_touched(), + created: account.is_created(), + selfdestructed: account.is_selfdestructed(), + loaded_as_not_existing: account.is_loaded_as_not_existing(), + storage: account + .storage + .iter() + .map(|(slot, value)| (*slot, (value.original_value, value.present_value))) + .collect(), + }; + (*address, view) + }) + .collect() +} + /// Asserts that two outcomes are indistinguishable: same execution result, same four-dimension -/// usage, same receipt `gas_used`, and the same detained compute-gas limit. +/// usage, same receipt `gas_used`, the same detained compute-gas limit, and the same state. /// /// This is the precision invariant in assertion form — what a transaction that stays inside every /// per-tx limit must produce under both accounting models. @@ -205,6 +255,22 @@ pub(crate) fn assert_outcomes_identical(label: &str, r6: &Outcome, r7: &Outcome) "{label}: the detained compute-gas limit must be identical; REX6={} REX7={}", r6.detained_compute_gas_limit, r7.detained_compute_gas_limit ); + let (s6, s7) = (state_view(&r6.state), state_view(&r7.state)); + if s6 != s7 { + // Report the first address the two disagree on; dumping both whole states buries it. + let mut addresses: Vec<&Address> = s6.keys().chain(s7.keys()).collect(); + addresses.sort_unstable(); + addresses.dedup(); + let culprit = addresses + .into_iter() + .find(|address| s6.get(*address) != s7.get(*address)) + .expect("the maps differ, so some address must"); + panic!( + "{label}: the produced state must be identical; {culprit} is\n REX6: {:?}\n REX7: {:?}", + s6.get(culprit), + s7.get(culprit), + ); + } } /// [`transact`] with every SALT bucket reporting `bucket_capacity`. From c3a82301e3065d54adb44bbc7b5aa4cc016b301d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:17:20 +0800 Subject: [PATCH 023/208] docs(rex7): align the carve-out and clamp rules with the fixed behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The exceptional-halt carve-out was written around the interpreter zeroing its own gas counter, which it does only for ordinary out-of-gas, and said nothing about whether the burned remainder enforces. State the rule by halt classification, and state that the burn is reported but never evaluated against a limit. The clamp section now says when the clamp is in force — an exact equality binds and hides nothing — and pins the two fields a clamp-induced exceed reports: the binding constraint's own limit, and the transaction's final compute usage rather than a pre-settlement snapshot. --- docs/spec/evm/compute-gas.md | 20 +++++++++++++++++--- docs/spec/upgrades/rex7.md | 29 +++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 38de1dc6..b42ca17a 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -479,13 +479,20 @@ For every transaction that stays within every runtime resource limit, a node MUS After settlement and body recording at a checkpoint (and at frame entry and resume), a node MUST clamp the interpreter-visible remaining gas to the remaining compute headroom — the minimum of the current frame's remaining per-frame compute budget and the transaction-level remaining budget under the effective limit (including detention) — and MUST restore the hidden amount before the next checkpoint body, before `GAS` is observed, before call-gas forwarding, and before storage-gas charges. +The clamp is in force for the segment that follows whenever the true remaining gas is at or above the headroom, and a node MUST remember which constraint bound it along with that constraint's own limit value. +An exact equality is a binding clamp that hides nothing, not the absence of a clamp. +When the true remaining gas is below the headroom, no clamp is in force and an out-of-gas inside the segment is the inherited EVM's own. + Inside a plain-opcode segment: - An opcode that would cost more than the clamped visible remainder MUST NOT execute. - The frame's final result MUST restore the hidden gas. - The node MUST reclassify that out-of-gas as the resource-limit exceed the clamp stood for: frame-local budget → frame revert with `MegaLimitExceeded`; transaction-level compute → transaction halt with `OutOfGas` and rescued remaining gas; detained limit → transaction halt with `VolatileDataAccessOutOfGas` and rescued remaining gas. +The `limit` reported by either shape MUST be the constraint that bound the clamp — the frame's own compute budget for a frame-local binding, the effective transaction-level limit otherwise — matching what the per-opcode check path on this page reports. + Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. +The `actual` a transaction-level clamp halt reports MUST be the transaction's final compute usage, after the frame-exit settlement has closed the partial segment the crossing opcode stopped inside. When the current frame's remaining per-frame compute budget equals the transaction-level remaining budget, a node MUST bind the clamp to the transaction-level constraint (including detention when detention is the effective transaction-level bound). A clamp-induced exceed under that binding MUST halt the transaction with gas rescue; a node MUST NOT classify the equality as frame-local. @@ -495,9 +502,16 @@ When the crossing opcode would exhaust both the true remaining EVM gas and the c #### Exceptional-halt frame carve-out -When a frame ends in an exceptional halt — including ordinary out-of-gas and memory out-of-gas — the interpreter zeros the frame's remaining gas before frame-exit settlement. -A node MUST settle that entire burned remainder as compute gas at frame exit. -Under per-opcode recording through Rex6 neither the failing opcode nor the burn is attributed to compute gas, so a transaction that contains an inner out-of-gas call frame MAY report a strictly higher compute-gas total under Rex7 while EVM gas and the receipt remain identical. +A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. +A node MUST settle that entire burned remainder as compute gas at frame exit: the open plain-opcode segment measured against a zero remainder, plus any gas the clamp was hiding. +The rule is driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. +Under per-opcode recording through Rex6 neither the failing opcode nor the burn is attributed to compute gas, so a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a strictly higher compute-gas total under Rex7 while EVM gas and the receipt remain identical. + +A node MUST NOT evaluate any resource limit against the burned remainder: it is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already burned and change the receipt this carve-out requires to stay identical. +The usage a limit is evaluated against therefore excludes the burn, while the reported compute-gas total and the block-level compute accounting include it. +Nothing is lost by the exclusion: the executed part of an exceptionally halted frame's tail is bounded either by the clamp or by a frame gas remainder that was already below the headroom. + +A clamp-induced out-of-gas is not an exceptional halt for this rule — the crossing opcode never executed and the remaining gas is rescued rather than burned. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index d3c5f667..f5db7714 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -70,10 +70,21 @@ For every transaction that stays within every runtime resource limit, a node MUS The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed. **Exceptional-halt frame carve-out.** -When a frame ends in an exceptional halt — including ordinary out-of-gas and memory out-of-gas — the interpreter zeros the frame's remaining gas before the frame-exit settlement runs. -A node MUST therefore settle the entire burned remainder of that frame's budget as compute gas at frame exit. +A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. +The top-level frame's whole envelope is spent by the transaction's final gas accounting, and an inner frame's remainder is never handed back to its caller. +A node MUST settle that entire burned remainder as compute gas at frame exit: the open plain-opcode segment measured against a zero remainder, plus any gas the clamp was hiding from the interpreter. +The rule is driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. + Under per-opcode recording through Rex6, neither the failing opcode nor the burn is attributed to compute gas. -Consequently, a transaction that contains an inner call frame which runs out of gas MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. +Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. + +A node MUST NOT evaluate any resource limit against the burned remainder. +The burn is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the recorded total past that limit; halting on it would rescue gas the EVM has already burned and change a receipt this carve-out requires to stay identical. +The usage a limit is evaluated against therefore excludes the burn, while the transaction's reported compute-gas total and the block-level compute accounting include it. +Nothing is lost by that exclusion: the part of an exceptionally halted frame's tail that was actually executed is bounded either by the gas clamp or by a frame gas remainder that was already below the compute headroom, so it could not have exceeded a limit in the first place. + +A clamp-induced out-of-gas is not an exceptional halt for this rule. +The crossing opcode was stopped before it executed and the remaining gas is rescued for the sender rather than burned, so the reclassification rules below apply instead. ### Gas-Clamp Enforcement @@ -90,8 +101,9 @@ Under Rex7, a node MUST enforce compute-gas and detention limits inside plain-op At each checkpoint, after settlement and after the checkpoint body has recorded its own compute gas (and after any detention cap the checkpoint installs), and again at frame entry and resume, a node MUST: 1. Compute the remaining compute headroom as the minimum of the current frame's remaining per-frame compute budget and the transaction-level remaining budget under the effective limit (including detention). -2. Hide any interpreter remaining gas above that headroom from the interpreter, remembering both the hidden amount and which constraint bound the clamp (frame-local budget vs transaction-level / detained limit). -3. Leave the true remaining gas available again before the next checkpoint body runs, before `GAS` is observed, before call-gas forwarding is computed, and before storage-gas charges are taken, so those sites always see the unclamped counter. +2. When the interpreter's true remaining gas is at or above that headroom, put the clamp in force for the segment that follows: hide the excess from the interpreter and remember which constraint bound the clamp — the frame-local budget or the transaction-level / detained limit — together with that constraint's own limit value. Equality is a binding clamp that hides nothing, not the absence of a clamp. +3. When the true remaining gas is below the headroom, no clamp is in force: the frame's own gas runs out ahead of the compute headroom, and an out-of-gas inside the segment is the inherited EVM's own rather than a resource-limit exceed. +4. Leave the true remaining gas available again before the next checkpoint body runs, before `GAS` is observed, before call-gas forwarding is computed, and before storage-gas charges are taken, so those sites always see the unclamped counter. Inside a plain-opcode segment only plain opcodes run, so the inherited EVM's ordinary per-opcode gas check is the enforcement tool: @@ -102,8 +114,12 @@ Inside a plain-opcode segment only plain opcodes run, so the inherited EVM's ord - **Transaction-level compute binding** → the transaction halts with `OutOfGas`, and remaining gas is rescued and refunded to the sender. - **Detained-limit binding** → the transaction halts with `VolatileDataAccessOutOfGas`, with the same gas rescue. +The reported `limit` MUST be the constraint that bound the clamp, not whichever limit is largest or most convenient: the frame's own compute budget for a frame-local binding, the effective transaction-level limit otherwise. +The revert payload is visible to the calling contract, so a frame-local exceed that reported the transaction-level limit would be a different observable return value for the same execution, not merely a different diagnostic. + Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. Recorded usage at a clamp-induced halt therefore ends at the limit (or strictly below it if settlement had not yet closed a partial segment), not strictly above it. +The `actual` a transaction-level clamp halt reports MUST be that final usage — the frame-exit settlement closes the partial segment after the exceed is identified, and a node MUST NOT report the usage as it stood before that settlement. **Top-frame headroom tie-break.** At the top-level frame the remaining per-frame compute budget equals the transaction-level remaining budget whenever both are still governed by the same base limit. @@ -128,7 +144,8 @@ Contracts and tools that assume per-opcode compute-gas attribution for every ins Contracts that stay within every resource limit see no behavioral change relative to Rex6. Contracts that trip the compute-gas or detention limit inside a plain-opcode segment halt one opcode earlier than under Rex6, with the crossing opcode excluded from recorded compute usage and with remaining gas still refundable on a transaction-level halt. -A parent that calls into a child which runs out of ordinary EVM gas may observe a higher transaction-level compute-gas total under Rex7 than under Rex6; the receipt `gas_used` and the execution success or failure of the outer transaction are unchanged by that carve-out alone. +A transaction that halts exceptionally, or that calls into a child frame which does, may report a higher transaction-level compute-gas total under Rex7 than under Rex6 — for any exceptional halt, not just out-of-gas. +The receipt `gas_used`, the halt or revert reported, and the execution success or failure of the outer transaction are unchanged by that carve-out: the burned remainder is reported, never enforced. ## Safety and Compatibility From 918868015f7cacc753e00d7e5919476510fc2f4a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:19:49 +0800 Subject: [PATCH 024/208] docs(rex7): correct the frame-exit hook docstrings after the split --- crates/mega-evm/src/limit/limit.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index c122874a..37597ef6 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -334,9 +334,9 @@ impl AdditionalLimit { self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); } - /// Finalises the compute accounting a frame's own result decides: restores any outstanding V0 - /// clamp, latches a clamp-induced out-of-gas as a compute exceed, and settles an exceptional - /// halt's burned remainder. + /// Finalises what the frame's own result decides about the clamp: restores any outstanding V0 + /// clamp into the result's gas, latches a clamp-induced out-of-gas as the compute exceed it + /// stands for, and records how much was hidden for the frame-exit settlement that follows. /// /// Must run before anything reads or charges the result's gas — in particular before the /// execution-layer code-deposit storage charge, which would otherwise observe the clamped copy @@ -1137,6 +1137,10 @@ impl AdditionalLimit { /// /// Used by `KeylessDeploy` (REX5+) to propagate sandbox resource consumption /// back to the parent transaction. + /// + /// [`LimitUsage`] carries one compute-gas total, so a REX7 sandbox that halted exceptionally + /// merges its burned remainder as ordinary enforcing usage rather than into the parent's + /// non-enforcing lane. The amount is bounded by the sandbox's gas reservation. pub(crate) fn merge_usage(&mut self, usage: LimitUsage) { self.compute_gas.merge_persistent_usage(usage.compute_gas); self.data_size.merge_persistent_usage(usage.data_size); From 3ba8273b2e138b3d9732025056f6eed38c780b4b Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:22:09 +0800 Subject: [PATCH 025/208] docs: note the REX7 exceptional-halt burn lane in AGENTS.md --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 0c086a9a..1d1f87d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,6 +116,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi - **Compute gas**: Measures pure computational cost. Through REX6 every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a V0 gas clamp. + A REX7 frame that ends in an exceptional halt additionally settles its whole burned remainder into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — the burn is destroyed gas, not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). From d7ba3cc2a88e9a1c59146eb0e237b34715349257 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 01:24:26 +0800 Subject: [PATCH 026/208] test(rex7): explain the callee loop size in the payload case --- crates/mega-evm/tests/rex7/clamp_classification.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index 55caa9af..2ef235d3 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -248,6 +248,7 @@ fn decode_limit_exceeded(label: &str, outcome: &Outcome) -> MegaLimitExceeded { /// return value for the same execution. #[test] fn test_frame_local_clamp_exceed_reports_the_sub_frame_budget() { + // Enough iterations to outrun the sub-frame's 98/100 share of a one-million compute budget. let callee = countdown_loop_code(&[], 40_000); let code = call_and_return_revert_data(50_000_000); let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); From ae0ad80eb6a01b2b28da4f2463fd174e4c43a5bc Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:01:43 +0800 Subject: [PATCH 027/208] fix(rex7): enforce the work an exceptionally halted frame performed An exceptional halt settled its whole open segment plus the clamp-hidden gas into the non-enforcing lane, so the opcodes the frame had already run stopped counting against the parent frame and the transaction. Code that keeps executing after absorbing the failure could then spend the same compute headroom a second time. Split the settlement in two: the executed tail settles through the ordinary enforcing path at frame exit, and only the remainder the frame destroys goes to the non-enforcing lane. The destroyed part is read from the frame's final result after action processing, which is also the first point the classification is final -- revm's create-return can still turn a successful constructor into a code-deposit out-of-gas, an EIP-3541 reject or a runtime code-size reject. The reported total is unchanged for every shape that was already correct; what moves is which half of it enforces. --- crates/mega-evm/src/limit/compute_gas.rs | 37 ++++-- crates/mega-evm/src/limit/limit.rs | 144 ++++++++++++++--------- 2 files changed, 118 insertions(+), 63 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index d6e61fef..4dffb7c9 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -57,14 +57,21 @@ pub(crate) struct ComputeGasTracker { /// The effective compute gas limit, which may be dynamically lowered by gas detention /// (volatile data access). Always <= `frame_tracker.tx_limit()`. detained_limit: u64, - /// Compute gas settled from the burned remainders of exceptionally halted frames (REX7+). + /// Compute gas settled from the **destroyed** remainders of exceptionally halted frames + /// (REX7+). /// /// Recorded into the TX-level lane of `frame_tracker`, so it shows up in the transaction's /// reported compute total and in block-level accounting, and subtracted back out of every - /// limit comparison. A burned remainder is gas the EVM destroyed, not work the network - /// performed; letting it trip a limit would turn an ordinary EVM halt into a resource-limit - /// failure with the remaining gas rescued for the sender, changing a receipt that must stay - /// identical to per-opcode accounting. Always 0 before REX7. + /// limit comparison. A destroyed remainder is gas the EVM threw away without executing + /// anything for it; letting it trip a limit would turn an ordinary EVM halt into a + /// resource-limit failure with the remaining gas rescued for the sender, changing a receipt + /// that must stay identical to per-opcode accounting. + /// + /// Only the remainder lands here. The work an exceptionally halted frame actually performed + /// before it failed is recorded through [`record_gas_used`](Self::record_gas_used) like any + /// other work, so it shrinks the parent frame's and the transaction's budgets — otherwise the + /// code that runs after the failed frame returns could spend the same headroom twice. Always 0 + /// before REX7. burned: u64, frame_tracker: FrameLimitTracker<()>, } @@ -206,7 +213,7 @@ impl ComputeGasTracker { } } - /// Records a burned remainder from an exceptionally halted frame (REX7+). + /// Records the destroyed remainder of an exceptionally halted frame (REX7+). /// /// Counts toward the transaction's reported compute total and block-level accounting, and is /// excluded from every limit comparison — see [`burned`](Self::burned). @@ -215,7 +222,23 @@ impl ComputeGasTracker { self.frame_tracker.add_tx_persistent(amount); } - /// Total recorded usage minus the burned remainders that must not enforce. + /// Reclassifies `amount` of already-merged usage as a destroyed remainder (REX7+). + /// + /// The sandbox path merges one compute total through + /// [`merge_persistent_usage`](Self::merge_persistent_usage) and then declares how much of it + /// the sandbox destroyed, rather than adding the amount a second time. + pub(crate) fn merge_burned_usage(&mut self, amount: u64) { + self.burned = self.burned.saturating_add(amount); + } + + /// The destroyed remainders inside [`tx_usage`](TxRuntimeLimit::tx_usage) — see + /// [`burned`](Self::burned). + #[inline] + pub(crate) fn burned_usage(&self) -> u64 { + self.burned + } + + /// Total recorded usage minus the destroyed remainders that must not enforce. #[inline] fn enforced_tx_usage(&self) -> u64 { self.frame_tracker.net_usage().saturating_sub(self.burned) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 37597ef6..6f4ecb85 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -133,15 +133,6 @@ pub struct AdditionalLimit { /// [`settle_frame_final_result`](Self::settle_frame_final_result). clamp: Option, - /// The clamp-hidden gas [`settle_frame_final_result`](Self::settle_frame_final_result) just - /// handed back to the frame's result, carried to the frame-exit settlement in - /// [`after_frame_run_instructions`](Self::after_frame_run_instructions). - /// - /// The two hooks are split by the execution layer's code-deposit charge, which has to observe - /// unclamped gas; the settlement that follows still needs to know how much of the frame's true - /// remainder the interpreter could not see. Written and consumed on the same frame exit. - restored_clamp_hidden: u64, - /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level /// constraint. /// @@ -196,7 +187,6 @@ impl AdditionalLimit { checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), checkpoint_baseline: 0, clamp: None, - restored_clamp_hidden: 0, clamp_latched_detained: false, } } @@ -241,7 +231,6 @@ impl AdditionalLimit { self.storage_call_stipend.reset(); self.checkpoint_baseline = 0; self.clamp = None; - self.restored_clamp_hidden = 0; self.clamp_latched_detained = false; } @@ -270,6 +259,24 @@ impl AdditionalLimit { self.checkpoint_baseline = remaining; } + /// Moves the open segment's baseline down by `amount` of `MegaETH` storage gas just charged to + /// the interpreter, so the charge sits outside the segment rather than inside it. + /// + /// A checkpoint body normally subtracts its own storage charge when it closes its measurement + /// window. A body that aborts — a static-context `LOG`, a `SELFDESTRUCT` whose inner + /// instruction runs out of gas — never reaches that subtraction, and the frame-exit settlement + /// that follows would then bill the charge as compute. Excluding it from the baseline as it is + /// charged makes the exclusion hold on both paths; on the normal path the body's own window + /// re-syncs the baseline afterwards, so this is invisible there. + /// + /// No-op before REX7, where nothing measures against a baseline. + #[inline] + pub(crate) fn exclude_storage_gas_from_segment(&mut self, amount: u64) { + if self.checkpoint_accounting { + self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); + } + } + /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, /// returning that amount. /// @@ -335,12 +342,14 @@ impl AdditionalLimit { } /// Finalises what the frame's own result decides about the clamp: restores any outstanding V0 - /// clamp into the result's gas, latches a clamp-induced out-of-gas as the compute exceed it - /// stands for, and records how much was hidden for the frame-exit settlement that follows. + /// clamp into the result's gas and latches a clamp-induced out-of-gas as the compute exceed it + /// stands for. /// /// Must run before anything reads or charges the result's gas — in particular before the /// execution-layer code-deposit storage charge, which would otherwise observe the clamped copy - /// and mis-fire an out-of-gas on a CREATE frame that is nowhere near its limits. + /// and mis-fire an out-of-gas on a CREATE frame that is nowhere near its limits. It is also + /// what puts the result's gas into the true domain, which is where the destroyed remainder of + /// an exceptionally halted frame is later read from. /// /// A clamp can only be outstanding when the frame ended inside a plain-opcode segment, because /// every checkpoint prologue takes it before its body. An out-of-gas exit from such a segment @@ -353,12 +362,8 @@ impl AdditionalLimit { if !self.checkpoint_accounting { return; } - self.restored_clamp_hidden = 0; if let Some(clamp) = self.clamp.take() { result.gas.erase_cost(clamp.hidden); - // Handed to the frame-exit settlement, which runs after the execution layer's - // code-deposit charge and can no longer see the clamp itself. - self.restored_clamp_hidden = clamp.hidden; // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every // other result either is unrelated to gas or cannot arise from a plain opcode. if matches!(result.result, InstructionResult::OutOfGas | InstructionResult::MemoryOOG) { @@ -400,6 +405,18 @@ impl AdditionalLimit { self.has_exceeded_limit = LimitCheck::Exempt; } + /// The part of [`get_usage`](Self::get_usage)'s `compute_gas` that exceptionally halted frames + /// destroyed rather than performed (REX7+, always 0 before). + /// + /// Reported and accounted like the rest of the total, never enforced. A caller that merges + /// this transaction's usage into another tracker — today the `KeylessDeploy` sandbox boundary — + /// has to carry it alongside the total, or the receiving tracker re-enforces gas the EVM + /// already destroyed. + #[inline] + pub(crate) fn burned_compute_gas(&self) -> u64 { + self.compute_gas.burned_usage() + } + /// Gets the usage of the additional limits. #[inline] pub fn get_usage(&self) -> LimitUsage { @@ -928,8 +945,9 @@ impl AdditionalLimit { /// Hook called after frame action processing in `frame_run`. /// /// Records compute gas cost induced in frame action processing (e.g., code deposit cost), - /// marks the frame result as exceeding limit if needed, and rescues gas if a TX-level limit - /// was exceeded (before any inspector callback that might modify gas). + /// marks the frame result as exceeding limit if needed, settles an exceptionally halted + /// frame's destroyed remainder (REX7+), and rescues gas if a TX-level limit was exceeded + /// (before any inspector callback that might modify gas). pub(crate) fn after_frame_run( &mut self, result: &mut FrameResult, @@ -945,6 +963,7 @@ impl AdditionalLimit { ); } } + self.settle_exceptional_halt_burn(result); // Rescue gas if a TX-level additional limit has been exceeded. // This must happen before any inspector callback (`frame_end`) that might modify // the gas via `spend_all()`, so the correct `gas.remaining()` value is captured. @@ -971,21 +990,19 @@ impl AdditionalLimit { // `settle_frame_final_result`, before the execution-layer hook charged code-deposit storage // gas against the action's gas. // - // A frame that ended in an exceptional halt takes the burn branch instead: it returns none - // of its remaining budget, so the whole remainder — not just what the counter shows — - // settles, and it settles outside limit enforcement. See `settle_exceptional_halt_burn`. + // This delta is the work the frame *performed*, so it settles the same way — through the + // enforcing path — however the frame ended. A frame that halts exceptionally still ran the + // opcodes ahead of its failure, and a parent frame keeps executing after absorbing that + // failure; leaving the executed tail out of enforcement would let the code after the failed + // frame spend the same headroom a second time. What such a frame additionally destroys — + // the budget it never gets to spend — is settled after action processing, outside + // enforcement, by `settle_exceptional_halt_burn`. if self.checkpoint_accounting { - if let InterpreterAction::Return(interpreter_result) = action { - let exceptional_halt = !interpreter_result.result.is_ok_or_revert(); - let hidden = core::mem::take(&mut self.restored_clamp_hidden); + if let InterpreterAction::Return(_) = action { let remaining = frame.interpreter.gas.remaining(); - if exceptional_halt && !self.limit_exceeded() { - self.settle_exceptional_halt_burn(hidden); - } else { - let gas_used = self.checkpoint_baseline.saturating_sub(remaining); - let _ = self.record_compute_gas_unguarded(gas_used); - self.refresh_latched_compute_usage(); - } + let gas_used = self.checkpoint_baseline.saturating_sub(remaining); + let _ = self.record_compute_gas_unguarded(gas_used); + self.refresh_latched_compute_usage(); self.checkpoint_baseline = remaining; } } @@ -1106,31 +1123,43 @@ impl AdditionalLimit { } } - /// Settles the entire remainder an exceptionally halted frame burns, as compute gas. + /// Settles the remainder an exceptionally halted frame destroys, as non-enforcing compute gas + /// (REX7+). /// /// An exceptional halt returns no gas: the top-level frame's whole envelope is spent by the /// transaction's final gas accounting, and an inner frame's remainder is simply never handed - /// back to its caller. The interpreter zeroes its own counter only for a plain `OutOfGas`, - /// so the frame-exit delta cannot see the burn on any other classification. This settles it - /// directly instead: everything the frame still held at the last checkpoint — the open segment - /// (`baseline`, measured against a zero remainder) plus `hidden`, the part of the true - /// remainder the clamp was keeping out of the interpreter's sight. - /// - /// The whole amount goes to the tracker's non-enforcing lane, not just the part beyond the - /// compute headroom. Nothing is lost by that: a segment that runs under a clamp can never - /// consume past the headroom (the clamp is the enforcement), and a segment with no clamp is - /// bounded by a frame gas remainder that was already below the headroom — so the executed part - /// of an exceptionally halted frame's tail could not have exceeded a limit either way. What - /// enforcing the *burn* would do is turn an ordinary EVM halt into a resource-limit failure - /// with the remaining gas rescued for the sender, which is exactly the receipt change the + /// back to its caller. The interpreter zeroes its own counter only for a plain `OutOfGas`, so + /// the frame-exit delta cannot see that destroyed budget on any other classification. The + /// result's own gas can: by the time this runs, + /// [`settle_frame_final_result`](Self::settle_frame_final_result) has handed back whatever the + /// V0 clamp was hiding and the code-deposit storage charge has been taken, so + /// `result.gas().remaining()` is exactly what the frame still held and will not get to keep. + /// + /// Runs **after** action processing, which is the first point the classification is final: + /// revm's create-return can still turn a successful constructor into a canonical code-deposit + /// out-of-gas, an EIP-3541 reject or a runtime code-size reject, and each of those destroys the + /// frame's remainder just like a halt from the interpreter loop. + /// + /// Only the destroyed part goes to the tracker's non-enforcing lane — the work performed ahead + /// of the failure already settled through the enforcing path in + /// [`after_frame_run_instructions`](Self::after_frame_run_instructions). Enforcing the + /// destroyed part would turn an ordinary EVM halt into a resource-limit failure with the + /// remaining gas rescued for the sender, which is exactly the receipt change the /// exceptional-halt carve-out forbids. /// - /// Not reached when a resource limit is already latched: that path burns nothing, because the - /// frame either reverts to its parent (frame-local) or halts the transaction with its gas + /// Not reached when a resource limit is already latched: that path destroys nothing, because + /// the frame either reverts to its parent (frame-local) or halts the transaction with its gas /// rescued (TX-level) — including a clamp-induced out-of-gas, which - /// [`settle_frame_final_result`](Self::settle_frame_final_result) latches just before this. - fn settle_exceptional_halt_burn(&mut self, hidden: u64) { - self.compute_gas.record_burned_gas(self.checkpoint_baseline.saturating_add(hidden)); + /// [`settle_frame_final_result`](Self::settle_frame_final_result) latches earlier in this + /// frame exit. + fn settle_exceptional_halt_burn(&mut self, result: &FrameResult) { + if !self.checkpoint_accounting || + self.limit_exceeded() || + result.instruction_result().is_ok_or_revert() + { + return; + } + self.compute_gas.record_burned_gas(result.gas().remaining()); } /// Merges resource usage from a sandbox execution into this tracker. @@ -1138,11 +1167,14 @@ impl AdditionalLimit { /// Used by `KeylessDeploy` (REX5+) to propagate sandbox resource consumption /// back to the parent transaction. /// - /// [`LimitUsage`] carries one compute-gas total, so a REX7 sandbox that halted exceptionally - /// merges its burned remainder as ordinary enforcing usage rather than into the parent's - /// non-enforcing lane. The amount is bounded by the sandbox's gas reservation. - pub(crate) fn merge_usage(&mut self, usage: LimitUsage) { + /// `burned_compute_gas` is the part of `usage.compute_gas` the sandbox destroyed rather than + /// performed (REX7+, always 0 before). It is already inside the merged total, so it is only + /// reclassified here — the parent reports it and never enforces it, exactly as the sandbox + /// did. Merging it as ordinary usage instead would let a sandbox frame's ordinary EVM halt + /// fail the outer transaction on a resource limit. + pub(crate) fn merge_usage(&mut self, usage: LimitUsage, burned_compute_gas: u64) { self.compute_gas.merge_persistent_usage(usage.compute_gas); + self.compute_gas.merge_burned_usage(burned_compute_gas); self.data_size.merge_persistent_usage(usage.data_size); self.kv_update.merge_persistent_usage(usage.kv_updates); self.state_growth.merge_persistent_usage(usage.state_growth); From 113fd7b1936ac4760d344fa73eecc787e02672a7 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:01:50 +0800 Subject: [PATCH 028/208] fix(rex7): keep an aborted checkpoint's storage charge out of compute A checkpoint body charges its storage gas before running the raw opcode and subtracts it back out when it records its own compute window. A body that halts in between -- LOG in a static frame, SELFDESTRUCT whose inner instruction runs out of gas -- never reaches that subtraction, so the frame-exit settlement reported the charge as compute gas. Exclude the charge from the open segment as it is made, at every site that debits MegaETH storage gas from inside a checkpoint body. The normal path re-syncs the segment right afterwards, so nothing changes there. --- crates/mega-evm/src/evm/instructions.rs | 53 ++++++++++++++++++++----- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index f40f01e9..7a566ada 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -168,8 +168,12 @@ use revm::{ /// - **REX7** (extends REX6): switches to **checkpoint compute-gas settlement**. The plain opcodes /// are revm's own instructions with no recording wrapper at all; compute gas settles as an /// interpreter-gas delta at each checkpoint — the storage-gas opcodes, the CALL / CREATE family, -/// the volatile opcodes, and frame entry / resume / exit. Per-transaction totals are unchanged; a -/// limit exceed surfaces at the next checkpoint rather than at the opcode that crossed it. +/// the volatile opcodes, and frame entry / resume / exit. Per-transaction totals are unchanged +/// for a transaction that stays inside every limit and never halts exceptionally; a frame that +/// does halt exceptionally additionally reports the budget it destroyed, which is enforced +/// against nothing. Enforcement inside a plain segment is the V0 gas clamp, which stops the +/// crossing opcode before it executes; an exceed detected by a settlement instead surfaces at the +/// checkpoint that settled it rather than at the opcode that crossed the limit. /// - Volatile opcodes: `volatile_data_ext::*_checkpoint` (raw instruction + segment settlement + /// detention cap) in place of the `compute_gas_ext` delegation /// - Storage-gas, CALL-family, CREATE and SELFDESTRUCT: the REX6 handler chains, settling from @@ -928,6 +932,29 @@ macro_rules! record_checkpoint_body_compute_gas { }; } +/// Charges `$amount` of `MegaETH` storage gas to the interpreter's counter and keeps it out of the +/// REX7 settlement segment that is currently open, returning the amount charged. +/// +/// Storage gas is never compute gas. A checkpoint body normally subtracts its own charge when +/// [`record_storage_compute_gas!`] closes the body's measurement window — but a body that halts +/// before reaching that macro (a static-context `LOG`, an inner instruction that runs out of gas) +/// leaves the frame-exit settlement measuring a segment the charge is still inside, which would +/// report storage gas as compute gas. Excluding it from the segment as it is charged makes the +/// exclusion hold on both paths; on the normal path the body's own window re-syncs the segment +/// afterwards, so this is invisible there. +/// +/// Returns `Err(OutOfGas)` from the enclosing handler when the frame cannot afford the charge, +/// exactly as a bare `gas!` would — with nothing debited and so nothing to exclude. No-op before +/// REX7, where nothing measures against a segment. +macro_rules! charge_storage_gas { + ($context:expr, $amount:expr) => {{ + let amount: u64 = $amount; + gas!($context.interpreter, amount); + $context.host.additional_limit().borrow_mut().exclude_storage_gas_from_segment(amount); + amount + }}; +} + /// Records an opcode's compute gas in a single measurement window and enforces the compute-gas /// limit. The REX6 storage-affecting handlers invoke it directly with the storage gas they /// charged; plain opcodes use the leaner inline recording in @@ -2478,9 +2505,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(new_account_storage_gas); - let charged = new_account_storage_gas - drained; - gas!(context.interpreter, charged); - charged + charge_storage_gas!(context, new_account_storage_gas - drained) } else { 0 }; @@ -2826,8 +2851,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(create_contract_storage_gas); - let storage_charged = create_contract_storage_gas - drained; - gas!(context.interpreter, storage_charged); + let storage_charged = charge_storage_gas!(context, create_contract_storage_gas - drained); // Run the raw inner create opcode (no `compute_gas_ext` wrapper — REX6 records compute gas // once below). @@ -2905,6 +2929,15 @@ pub mod storage_gas_ext { // storage cost. let storage_charged = log_storage_cost.expect("gas_or_fail! above halts and returns on None"); + // The `gas_or_fail!` above is the storage-gas charge, so it gets the same segment + // exclusion `charge_storage_gas!` applies at every other charge site: the raw opcode below + // can halt (a static frame rejects `LOG` outright) before the recording that would + // otherwise subtract it. + context + .host + .additional_limit() + .borrow_mut() + .exclude_storage_gas_from_segment(storage_charged); // Run the raw opcode and record compute gas once after the body completes (canonical // metering order). Byte-equivalent to the pre-REX6 per-`N` `compute_gas_ext::logK` @@ -2973,9 +3006,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(sstore_set_storage_gas); - let charged = sstore_set_storage_gas - drained; - gas!(context.interpreter, charged); - charged + charge_storage_gas!(context, sstore_set_storage_gas - drained) } else { 0 }; @@ -3068,7 +3099,7 @@ pub mod storage_gas_ext { }; let drained = context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); - gas!(context.interpreter, cost - drained); + charge_storage_gas!(context, cost - drained); // Record resource usage for new beneficiary account context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); From 9c6b609a2ad9a50f9e0dae689c92986a97da61eb Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:01:59 +0800 Subject: [PATCH 029/208] fix(rex7): keep the sandbox's compute-gas split across the merge The KeylessDeploy sandbox exported one compute total, whose REX7 reading already includes the remainders its exceptionally halted frames destroyed. The parent merged that as ordinary usage and then ran a post-merge limit check, so a burn that the sandbox itself never enforced became enforcing the moment it crossed the boundary -- turning a constructor's ordinary EVM halt into an outer ComputeGasLimitExceeded with the gas rescued. Carry the split across in SandboxUsage and merge the two lanes separately, so the parent reports the sandbox's whole total and enforces only the part the sandbox performed. --- crates/mega-evm/src/sandbox/execution.rs | 47 ++++++++++++++++++------ 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 177653a1..7edb82ef 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -648,7 +648,13 @@ fn run_sandbox_ctx( 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 limit_usage = { + let additional_limit = sandbox_evm.ctx.additional_limit.borrow(); + SandboxUsage { + usage: additional_limit.get_usage(), + burned_compute_gas: additional_limit.burned_compute_gas(), + } + }; let volatile_accesses = sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed(); process_sandbox_transact_result( @@ -686,7 +692,7 @@ pub enum SandboxOutcome { /// Wire-shape dispatch for what the outer caller should report. completion: SandboxCompletion, /// Resource usage from the sandbox's additional limit trackers. - limit_usage: LimitUsage, + limit_usage: SandboxUsage, /// Volatile-access footprint to merge into the parent after sandbox return. volatile_accesses: VolatileDataAccess, }, @@ -696,6 +702,22 @@ pub enum SandboxOutcome { Rejected(KeylessDeployError), } +/// Resource usage a completed sandbox hands to the parent, with Rex7's compute-gas split intact. +/// +/// [`LimitUsage`] carries one number per dimension, which is all the parent needs for three of +/// them. Compute gas needs two: the parent must report the sandbox's whole total but must enforce +/// only the part the sandbox performed, exactly as the sandbox itself did. Collapsing the two at +/// this boundary would let an ordinary EVM halt inside a sandboxed constructor fail the outer +/// transaction on a resource limit. +#[derive(Debug, Clone, Copy, Default)] +pub struct SandboxUsage { + /// Full reported usage. `compute_gas` includes `burned_compute_gas`. + pub usage: LimitUsage, + /// The part of `usage.compute_gas` the sandbox destroyed rather than performed — the + /// remainders of exceptionally halted sandbox frames (Rex7+, always 0 before). + pub burned_compute_gas: u64, +} + /// Wire-shape dispatch for a completed sandbox execution. /// /// `Deployed` and `EmptyCode` both surface as success-shape outer returns: the @@ -788,7 +810,7 @@ impl SandboxCompletion { /// surface (`Deployed { addr }` returned for create+SELFDESTRUCT) for replay parity. fn process_sandbox_transact_result( result: Result, E>, - limit_usage: LimitUsage, + limit_usage: SandboxUsage, volatile_accesses: VolatileDataAccess, is_rex5_enabled: bool, is_rex6_enabled: bool, @@ -930,7 +952,7 @@ fn process_sandbox_transact_result( fn apply_sandbox_post_accounting( ctx: &MegaContext, gas: &mut Gas, - limit_usage: LimitUsage, + limit_usage: SandboxUsage, volatile_accesses: VolatileDataAccess, reservation: u64, sandbox_gas_used: u64, @@ -1003,15 +1025,18 @@ fn charge_caller_materialization_pre_sandbox( ctx: &MegaContext, - limit_usage: LimitUsage, + limit_usage: SandboxUsage, ) { - ctx.additional_limit.borrow_mut().merge_usage(limit_usage); + ctx.additional_limit + .borrow_mut() + .merge_usage(limit_usage.usage, limit_usage.burned_compute_gas); } /// Returns the unused portion of the sandbox's pre-debited gas reservation to the @@ -1254,7 +1279,7 @@ mod tests { }); let out = process_sandbox_transact_result( result, - LimitUsage::default(), + SandboxUsage::default(), VolatileDataAccess::empty(), true, false, @@ -1280,7 +1305,7 @@ mod tests { }); let out = process_sandbox_transact_result( result, - LimitUsage::default(), + SandboxUsage::default(), VolatileDataAccess::empty(), true, false, @@ -1301,7 +1326,7 @@ mod tests { Err(FakeTxErr { is_tx: false, msg: "db blew up" }); let out = process_sandbox_transact_result( result, - LimitUsage::default(), + SandboxUsage::default(), VolatileDataAccess::empty(), true, false, @@ -1320,7 +1345,7 @@ mod tests { Err(FakeTxErr { is_tx: true, msg: "intrinsic gas too low" }); let out = process_sandbox_transact_result( result, - LimitUsage::default(), + SandboxUsage::default(), VolatileDataAccess::empty(), true, false, From 201fa4ec46d6c3ca120d9346b712630b4bee4a8e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:02:00 +0800 Subject: [PATCH 030/208] docs(rex7): state what a clamp-induced exceed's actual can exceed The clamp stops the crossing opcode before it executes, so the usage being enforced stays at or below the limit -- but the reported actual is the transaction's full total, which also carries the remainders of any frame that halted exceptionally earlier. Those are reported and never enforced, so actual can be larger than limit. --- crates/mega-evm/src/evm/result.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 8ae66054..0969d9da 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -115,7 +115,10 @@ pub enum MegaHaltReason { /// - Per-opcode enforcement (through Rex6): the crossing opcode has already recorded its /// cost, so `actual > limit`. /// - Gas-clamp enforcement (Rex7+): the crossing opcode is stopped before it executes and - /// its cost is not recorded, so `actual ≤ limit`. + /// its cost is not recorded, so the *enforced* usage stays at or below `limit`. `actual` + /// is the transaction's full reported total, which also carries the remainders of any + /// frame that halted exceptionally earlier in the transaction — those are reported but + /// never enforced, and they can push `actual` above `limit`. limit: u64, /// The actual compute gas usage at the halt. actual: u64, @@ -148,7 +151,10 @@ pub enum MegaHaltReason { /// - Per-opcode enforcement (through Rex6): the crossing opcode has already recorded its /// cost, so `actual > limit`. /// - Gas-clamp enforcement (Rex7+): the crossing opcode is stopped before it executes and - /// its cost is not recorded, so `actual ≤ limit`. + /// its cost is not recorded, so the *enforced* usage stays at or below `limit`. `actual` + /// is the transaction's full reported total, which also carries the remainders of any + /// frame that halted exceptionally earlier in the transaction — those are reported but + /// never enforced, and they can push `actual` above `limit`. limit: u64, /// The actual compute gas usage at the halt. actual: u64, From db8a414eb3048f2f588b3e06f8556bb1999f24ce Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:02:07 +0800 Subject: [PATCH 031/208] test(rex7): pin which half of an exceptional frame enforces Covers the four faces the executed half has to bind (transaction compute limit, the caller's remaining-budget reading, the detention cap base), the two boundaries that decide what belongs to which half (a checkpoint's storage charge, revm's post-action create rejects), the sandbox merge, and the inspected execution loop. The code-deposit claim in the compute-gas suite moves from an EIP-3541 rejection to an empty deploy: under REX7 a failed deposit destroys the CREATE frame's whole remainder, which dwarfs the charge under test. --- crates/mega-evm/tests/compute_gas/claims.rs | 37 +- crates/mega-evm/tests/rex7/burn_split.rs | 712 ++++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + 3 files changed, 738 insertions(+), 15 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/burn_split.rs diff --git a/crates/mega-evm/tests/compute_gas/claims.rs b/crates/mega-evm/tests/compute_gas/claims.rs index 7f4fa05f..b70aff44 100644 --- a/crates/mega-evm/tests/compute_gas/claims.rs +++ b/crates/mega-evm/tests/compute_gas/claims.rs @@ -819,24 +819,31 @@ fn test_first_call_to_the_beneficiary_is_charged_cold_from_minirex() { /// Pins "Code Deposit": the deposit's compute gas is recorded exactly once when the deposit /// occurs, and nothing is recorded when it does not. /// -/// The two initcodes have identical length and opcode sequence and differ only in the byte they -/// store, so every other cost in the transaction cancels and the difference between the two -/// recorded totals is the code-deposit charge alone. `0xEF` makes EIP-3541 reject the runtime -/// code, so the deposit never happens. +/// The two initcodes have identical length and opcode sequence and differ only in the length they +/// return, so every other cost in the transaction cancels — the `MSTORE8` has already expanded +/// memory past both `RETURN` windows — and the difference between the two recorded totals is the +/// code-deposit charge alone. +/// +/// The zero-length return is what makes "the deposit does not happen" observable on every spec: +/// the CREATE still succeeds, so the frame returns its unspent budget to the caller. A CREATE that +/// fails the deposit instead (EIP-3541, the code-size limit, an unaffordable code-deposit charge) +/// is an exceptional halt that returns nothing, and Rex7 settles that destroyed budget as compute +/// gas — a much larger number than the charge under test. Those shapes are pinned by the Rex7 +/// exceptional-halt suite rather than here. /// /// Two different mechanisms produce this number — `MiniRex` through Rex4 measure it over the /// frame-action window, Rex5+ pre-charge the canonical amount before the checkpoint commits — so /// the assertion runs on every tracked spec to keep them agreeing. #[test] fn test_code_deposit_recorded_only_when_deposit_occurs() { - /// `PUSH1 , PUSH0, MSTORE8, PUSH1 32, PUSH0, RETURN` — returns 32 bytes of runtime - /// code whose first byte is `first`. - fn initcode(first: u8) -> [u8; 8] { - [0x60, first, 0x5f, 0x53, 0x60, 0x20, 0x5f, 0xf3] + /// `PUSH1 0, PUSH0, MSTORE8, PUSH1 , PUSH0, RETURN` — returns `len` bytes of runtime + /// code. + fn initcode(len: u8) -> [u8; 8] { + [0x60, 0x00, 0x5f, 0x53, 0x60, len, 0x5f, 0xf3] } - fn creator(first: u8) -> MemoryDatabase { - let code = initcode(first); + fn creator(len: u8) -> MemoryDatabase { + let code = initcode(len); base_db( BytecodeBuilder::default() .mstore(0, code) @@ -856,10 +863,10 @@ fn test_code_deposit_recorded_only_when_deposit_occurs() { if !spec.is_enabled(MegaSpecId::MINI_REX) { continue; // Equivalence records no compute gas at all. } - let deposited = transact(spec, creator(0x00)); - let skipped = transact(spec, creator(0xef)); - // Both transactions succeed: the EIP-3541 rejection fails the CREATE (it pushes zero), - // not the transaction. A non-success outcome means the fixture itself drifted. + let deposited = transact(spec, creator(32)); + let skipped = transact(spec, creator(0)); + // Both transactions succeed: an empty deploy leaves the CREATE successful (it pushes the + // created address). A non-success outcome means the fixture itself drifted. assert_eq!(deposited.outcome, "success", "{spec_name}: depositing run should succeed"); assert_eq!(skipped.outcome, "success", "{spec_name}: skipped-deposit run should succeed"); let (deposited, skipped) = (deposited.compute_gas, skipped.compute_gas); @@ -872,7 +879,7 @@ fn test_code_deposit_recorded_only_when_deposit_occurs() { }); assert_eq!( delta, EXPECTED_DEPOSIT_GAS, - "{spec_name}: the only compute gas separating a deposit from an EIP-3541 rejection \ + "{spec_name}: the only compute gas separating a 32-byte deposit from an empty one \ must be the code-deposit charge (deposited={deposited} skipped={skipped})" ); } diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs new file mode 100644 index 00000000..6fc2b5e8 --- /dev/null +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -0,0 +1,712 @@ +//! REX7 splits an exceptionally halted frame into the work it performed and the budget it +//! destroyed. +//! +//! The two halves are accounted differently, and both halves have to be right: +//! +//! - **Executed** — everything the frame ran before it failed. It settles through the ordinary +//! enforcing path, so it shrinks the parent frame's budget, the transaction's compute budget, the +//! reading `MegaLimitControl.remainingComputeGas` returns, and the base a detention cap is built +//! on. A parent frame keeps executing after it absorbs a failed child; if the child's work left +//! enforcement, the code that follows could spend the same headroom a second time. +//! - **Destroyed** — the budget the frame never gets to spend, and never hands back. It is reported +//! and block-accounted but never enforced: halting on it would turn an ordinary EVM halt into a +//! resource-limit failure with the gas rescued, which is the receipt change the exceptional-halt +//! carve-out forbids. +//! +//! Two boundaries decide what belongs to which half, and both are exercised here: +//! +//! - the **storage gas** a checkpoint body charged before aborting is neither — it is storage gas, +//! and the body never reached the recording that would have subtracted it; +//! - the classification is only final **after action processing**, because revm's create-return can +//! still turn a successful constructor into a code-deposit out-of-gas, an EIP-3541 reject or a +//! runtime code-size reject. +//! +//! [`exceptional_halt`](crate::exceptional_halt) covers the reported totals; this module covers +//! which side of the enforcing boundary each part lands on. + +use crate::common::{ + transact, transact_default, transact_tx, transact_with_bucket_capacity, + transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, +}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + alloy_op_evm::OpTxError, + constants::mini_rex::{CODEDEPOSIT_STORAGE_GAS, LOG_DATA_STORAGE_GAS, MAX_CONTRACT_SIZE}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EVMError, EvmTxRuntimeLimits, IKeylessDeploy, IMegaLimitControl, MegaContext, MegaEvm, + MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, KEYLESS_DEPLOY_ADDRESS, + LIMIT_CONTROL_ADDRESS, +}; +use revm::{ + bytecode::opcode::{ + ADD, CALL, CREATE, LOG0, MLOAD, MSTORE8, POP, RETURN, SSTORE, STATICCALL, STOP, TIMESTAMP, + }, + context::{result::ResultAndState, tx::TxEnvBuilder, CfgEnv}, + handler::EvmTr, + inspector::NoOpInspector, +}; +use std::{convert::Infallible, vec::Vec}; + +/// Relayer that sends the keyless-deploy transactions. +const KEYLESS_RELAYER: Address = address!("0000000000000000000000000000000000340004"); + +/// Storage slot the caller writes its `remainingComputeGas` readings to. +const BEFORE_SLOT: u64 = 0xb0; +/// Second `remainingComputeGas` reading slot. +const AFTER_SLOT: u64 = 0xb1; + +/// Plain-opcode pairs the failing child runs before it underflows. Chosen large enough that the +/// work it performs dominates every other term in these fixtures. +const CHILD_PAIRS: usize = 1_000; +/// Compute gas one `PUSH1 1; POP` pair costs: `PUSH1` is 3, `POP` is 2. +const PAIR_GAS: u64 = 5; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// `pairs` PUSH1/POP pairs — plain opcodes that settle only at the next checkpoint. +fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + +/// A callee that performs [`CHILD_PAIRS`] pairs of real work and then ends its frame with a stack +/// underflow — an exceptional halt that is not a gas shortage, so the interpreter keeps its +/// counter and nothing about the failure is a resource-limit exceed. +fn working_then_underflowing_callee() -> Bytes { + plain_filler(BytecodeBuilder::default(), CHILD_PAIRS).append(ADD).append(STOP).build() +} + +/// A CALL into [`CALLEE`] forwarding `gas`, with the success flag popped so the caller survives +/// whatever the callee did. +fn call_callee(builder: BytecodeBuilder, gas: u64) -> BytecodeBuilder { + builder + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(gas) + .append(CALL) + .append(POP) +} + +/// A STATICCALL into `MegaLimitControl.remainingComputeGas()` whose returned word is written to +/// `slot`. The selector is written to memory first; the reading comes back into offset 0 as well. +fn store_remaining_compute_gas(builder: BytecodeBuilder, slot: u64) -> BytecodeBuilder { + builder + .mstore(0, IMegaLimitControl::remainingComputeGasCall::SELECTOR) + .push_number(32u64) // retSize + .push_number(0u64) // retOffset + .push_number(4u64) // argsSize + .push_number(0u64) // argsOffset + .push_address(LIMIT_CONTROL_ADDRESS) + .push_number(1_000_000u64) + .append(STATICCALL) + .append(POP) + .push_number(0u64) + .append(MLOAD) + .push_u256(U256::from(slot)) + .append(SSTORE) +} + +/// The caller shape every blocker-A case shares: work, an exceptional child, then the same amount +/// of work again. Whether the transaction survives the second half is what the child's executed +/// work decides. +fn work_call_work(child_gas: u64, tail_pairs: usize) -> Bytes { + let builder = plain_filler(BytecodeBuilder::default(), 10); + let builder = call_callee(builder, child_gas); + plain_filler(builder, tail_pairs).append(STOP).build() +} + +fn caller_db(caller_code: Bytes) -> MemoryDatabase { + base_db(caller_code).account_code(CALLEE, working_then_underflowing_callee()) +} + +/// The work an exceptionally halted child performed still binds the transaction's compute limit. +/// +/// This is the shape a fail-open shows up in: the child runs [`CHILD_PAIRS`] pairs of plain +/// opcodes and then underflows, the caller absorbs the failure and runs the same amount of work +/// again. Per-opcode accounting charges the child's work as it happens, so REX6's total is the +/// calibration point — set the limit one below it and the transaction must not finish. REX7 has to +/// stop as well: the child's executed work is real work, whatever the frame did afterwards. +#[test] +fn test_executed_work_of_an_exceptional_child_still_binds_the_tx_limit() { + let code = work_call_work(1_000_000, CHILD_PAIRS); + + // Calibrate against REX6 running the same program with nothing in its way. + let unconstrained = transact_default(MegaSpecId::REX6, caller_db(code.clone())); + assert!( + unconstrained.is_success(), + "the calibration run must succeed: {:?}", + unconstrained.result + ); + let limit = unconstrained.compute_gas - 1; + assert!( + limit > u64::try_from(CHILD_PAIRS).unwrap() * PAIR_GAS, + "the fixture must do more work than the child alone, or the limit proves nothing", + ); + + let limits = |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit); + let r6 = transact(MegaSpecId::REX6, caller_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, caller_db(code), limits(MegaSpecId::REX7)); + + assert!(!r6.is_success(), "REX6 must stop at the limit: {:?}", r6.result); + assert!( + !r7.is_success(), + "REX7 must stop at the same limit — the child's executed work is enforced even though its \ + frame ended in an exceptional halt; got {:?} with compute={}", + r7.result, + r7.compute_gas, + ); +} + +/// The same shape from the caller's own point of view: `MegaLimitControl.remainingComputeGas()` +/// reports the minimum of the caller's per-frame budget and the transaction-level remaining, so +/// one reading pins both. The drop across the failing child must cover the work the child did. +#[test] +fn test_exceptional_child_shrinks_the_callers_remaining_compute_budget() { + let builder = store_remaining_compute_gas(BytecodeBuilder::default(), BEFORE_SLOT); + let builder = call_callee(builder, 1_000_000); + let code = store_remaining_compute_gas(builder, AFTER_SLOT).append(STOP).build(); + + let child_work = u64::try_from(CHILD_PAIRS).unwrap() * PAIR_GAS; + let mut drops = Vec::new(); + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + let outcome = transact_default(spec, caller_db(code.clone())); + assert!( + outcome.is_success(), + "{spec:?}: the caller must survive its child: {:?}", + outcome.result, + ); + let before: u64 = outcome + .storage_value(CONTRACT, U256::from(BEFORE_SLOT)) + .try_into() + .expect("a compute-gas reading fits in u64"); + let after: u64 = outcome + .storage_value(CONTRACT, U256::from(AFTER_SLOT)) + .try_into() + .expect("a compute-gas reading fits in u64"); + assert!(before > after, "{spec:?}: the reading must fall across the child"); + let drop = before - after; + assert!( + drop >= child_work, + "{spec:?}: the caller's remaining budget must fall by at least the child's executed \ + work; drop={drop} child work={child_work}", + ); + drops.push(drop); + } + // Both models see the same child work; the only slack is which opcodes each attributes to the + // failing frame, so the two readings must agree to within one opcode's static gas. + let (r6, r7) = (drops[0], drops[1]); + assert!( + r7.abs_diff(r6) <= 32, + "the two models must charge the caller the same for a failed child; REX6={r6} REX7={r7}", + ); +} + +/// A detention cap is built relative to the usage already enforced at the access point +/// (`usage + cap`), so a fail-open on an exceptional child does not just widen the compute limit — +/// it widens every cap installed afterwards. Reading the post-transaction detained limit back is a +/// direct check on the base the cap was built from. +#[test] +fn test_detention_cap_after_an_exceptional_child_counts_its_executed_work() { + let builder = plain_filler(BytecodeBuilder::default(), 10); + let builder = call_callee(builder, 1_000_000); + let code = builder.append(TIMESTAMP).append(POP).append(STOP).build(); + + let limits = |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = 1_000_000; + limits + }; + let r6 = transact(MegaSpecId::REX6, caller_db(code.clone()), limits(MegaSpecId::REX6)); + let r7 = transact(MegaSpecId::REX7, caller_db(code), limits(MegaSpecId::REX7)); + + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert!(r7.is_success(), "REX7 must succeed: {:?}", r7.result); + assert!( + r7.detained_compute_gas_limit.abs_diff(r6.detained_compute_gas_limit) <= 32, + "the cap must be built on the same enforced usage under both models; REX6={} REX7={}", + r6.detained_compute_gas_limit, + r7.detained_compute_gas_limit, + ); +} + +/// The transaction-wide identity the storage-exclusion cases assert: every EVM gas a transaction +/// spends is either compute gas or `MegaETH` storage gas, so +/// `compute_gas == gas_used − storage gas`. Measuring the transaction-intrinsic part from a bare +/// `STOP` keeps it exact rather than pinned to a constant. +fn intrinsic_storage_gas(spec: MegaSpecId) -> u64 { + let outcome = transact_with_gas_limit( + spec, + base_db(BytecodeBuilder::default().append(STOP).build()), + EvmTxRuntimeLimits::from_spec(spec), + 1_000_000, + ); + outcome.gas_used - outcome.compute_gas +} + +/// A checkpoint body charges its storage gas before running the raw opcode, and subtracts it back +/// out when it records its own compute window. A body that halts in between never reaches that +/// subtraction — so the charge has to leave the open segment as it is made, or the frame-exit +/// settlement reports storage gas as compute gas. +/// +/// `LOG0` in a static frame is the shape that isolates it: the storage surcharge is a flat +/// per-byte rate that is already paid when revm rejects the state change. +#[test] +fn test_aborted_log_checkpoint_does_not_report_its_storage_charge_as_compute() { + const LOG_BYTES: u64 = 32; + let callee = BytecodeBuilder::default() + .push_number(LOG_BYTES) // len + .push_number(0u64) // offset + .append(LOG0) + .append(STOP) + .build(); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_address(CALLEE) + .push_number(77_777u64) + .append(STATICCALL) + .append(POP) + .append(STOP) + .build(); + + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let r7 = transact_default(MegaSpecId::REX7, db()); + assert!( + r7.is_success(), + "the caller must survive the static-context rejection: {:?}", + r7.result, + ); + + let log_storage_gas = LOG_DATA_STORAGE_GAS * LOG_BYTES; + assert_eq!( + r7.compute_gas, + r7.gas_used - intrinsic_storage_gas(MegaSpecId::REX7) - log_storage_gas, + "the LOG storage surcharge is storage gas on the halting path too; compute={} gas_used={}", + r7.compute_gas, + r7.gas_used, + ); +} + +/// The same exclusion for the other storage-charging checkpoint family that can abort after +/// charging: `SSTORE`. Its surcharge is SALT-scaled, so it is only non-zero above the minimum +/// bucket size — the elevated capacity is what makes this case exist at all. +/// +/// The surcharge is measured from a control run that performs the same write outside a static +/// frame, so the assertion states the amount rather than assuming a constant: the aborted body +/// pays exactly that much storage gas, and none of it may reach the compute total. +#[test] +fn test_aborted_sstore_checkpoint_does_not_report_its_storage_charge_as_compute() { + /// Twice the minimum bucket size, so the SALT multiplier makes the `SSTORE` set charge + /// non-zero. + const BUCKET_CAPACITY: u64 = 2 * mega_evm::MIN_BUCKET_SIZE as u64; + + let callee = BytecodeBuilder::default().sstore(U256::from(9), U256::from(0x77)).build(); + let caller = |call_opcode: u8| { + let mut builder = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64); // argsOffset + if call_opcode == CALL { + builder = builder.push_number(0u64); // value + } + builder + .push_address(CALLEE) + .push_number(10_000_000u64) + .append(call_opcode) + .append(POP) + .append(STOP) + .build() + }; + + let run = |call_opcode| { + transact_with_bucket_capacity( + MegaSpecId::REX7, + base_db(caller(call_opcode)).account_code(CALLEE, callee.clone()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + BUCKET_CAPACITY, + ) + }; + let intrinsic_storage = intrinsic_storage_gas(MegaSpecId::REX7); + + // Control: the same write in a frame that is allowed to make it. Its non-intrinsic storage gas + // is the surcharge the aborted run below also pays, before revm rejects the state change. + let committed = run(CALL); + assert!(committed.is_success(), "the control write must succeed: {:?}", committed.result); + let surcharge = committed.gas_used - committed.compute_gas - intrinsic_storage; + assert!( + surcharge > 0, + "the elevated bucket capacity must make the SSTORE set charge non-zero; gas_used={} \ + compute={}", + committed.gas_used, + committed.compute_gas, + ); + + let aborted = run(STATICCALL); + assert!( + aborted.is_success(), + "the caller must survive the static-context rejection: {:?}", + aborted.result, + ); + assert_eq!( + aborted.compute_gas, + aborted.gas_used - intrinsic_storage - surcharge, + "the SSTORE surcharge stays storage gas when the body it paid for never runs; compute={} \ + gas_used={} surcharge={surcharge}", + aborted.compute_gas, + aborted.gas_used, + ); +} + +/// Init code returning `len` bytes of runtime code whose first byte is `first`. +fn deploying_initcode(first: u8, len: u64) -> Bytes { + BytecodeBuilder::default() + .push_number(u64::from(first)) + .push_number(0u64) + .append(MSTORE8) + .push_number(len) + .push_number(0u64) + .append(RETURN) + .build() +} + +/// A contract whose body CREATEs `initcode` with all the gas it has, then stops. +fn creator_code(initcode: &Bytes) -> Bytes { + BytecodeBuilder::default() + .mstore(0, initcode.as_ref()) + .push_number(initcode.len() as u64) // length + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build() +} + +/// Runs a REX7 transaction into [`CONTRACT`] with an explicit gas limit and, optionally, a lowered +/// `limit_contract_code_size`. +/// +/// The shared helpers in [`crate::common`] all take the context's default configuration, which +/// pins the contract-size limit to `MegaETH`'s 512 KiB. Reaching revm's size reject needs a +/// smaller one, so this builds the context itself. +fn transact_create_reject( + mut db: MemoryDatabase, + gas_limit: u64, + code_size_limit: Option, +) -> Outcome { + let mut cfg = CfgEnv::default(); + cfg.spec = MegaSpecId::REX7; + cfg.limit_contract_code_size = code_size_limit.or(Some(MAX_CONTRACT_SIZE)); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); + let (usage, detained_compute_gas_limit) = { + let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); + (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) + }; + let gas_used = result.result.tx_gas_used(); + Outcome { + result: result.result, + compute_gas: usage.compute_gas, + data_size: usage.data_size, + kv_updates: usage.kv_updates, + state_growth: usage.state_growth, + gas_used, + detained_compute_gas_limit, + state: result.state, + } +} + +/// Runtime length the CREATE cases deploy — small enough that the per-byte code-deposit storage +/// charge stays affordable at the gas limits below. +const RUNTIME_LEN: u64 = 100; + +/// revm's per-byte code-deposit gas (`revm::interpreter::gas::CODEDEPOSIT`). +const CANONICAL_CODE_DEPOSIT_GAS: u64 = 200; + +/// The transaction-wide gas identity for the CREATE fixtures: the receipt's EVM gas is compute gas +/// plus `MegaETH` storage gas, and the only storage gas beyond the transaction intrinsic is the +/// per-byte code-deposit charge the execution layer takes before revm's create-return runs. +/// +/// It holds whether or not the deposit is ultimately rejected — which is the point. A reject +/// destroys the CREATE frame's whole remainder, and that destroyed budget is EVM gas the receipt +/// charges, so it has to appear in the compute total like any other exceptionally halted frame's. +fn assert_create_gas_identity(label: &str, outcome: &Outcome, intrinsic_storage: u64) { + let code_deposit_storage = CODEDEPOSIT_STORAGE_GAS * RUNTIME_LEN; + assert!( + outcome.is_success(), + "{label}: the creator must survive the CREATE: {:?}", + outcome.result, + ); + assert!( + outcome.gas_used > intrinsic_storage + code_deposit_storage, + "{label}: the fixture must reach the create-return, not run out paying the code-deposit \ + storage charge; gas_used={}", + outcome.gas_used, + ); + assert_eq!( + outcome.compute_gas, + outcome.gas_used - intrinsic_storage - code_deposit_storage, + "{label}: every EVM gas the transaction spent must be compute gas or storage gas; \ + compute={} gas_used={} code-deposit storage={code_deposit_storage}", + outcome.compute_gas, + outcome.gas_used, + ); +} + +/// revm's create-return rejects a successful constructor's runtime code **after** action +/// processing. EIP-3541 and the code-size limit are the two rejects that need no gas pressure at +/// all: the constructor returned normally, the frame's result was `Return` when the frame-exit +/// settlement ran, and only the create-return turned it into a halt that destroys the frame's +/// whole remainder. +/// +/// The code-size case runs against a lowered `limit_contract_code_size`. `MegaETH`'s own per-byte +/// code-deposit storage charge is 10,000 gas, so a runtime code long enough to pass the 512 KiB +/// consensus limit would need billions of gas to reach the reject and would run out paying that +/// charge first — reaching revm's size check at all needs a configured limit, not a longer +/// contract. +#[test] +fn test_create_rejected_after_action_processing_settles_its_destroyed_remainder() { + let intrinsic_storage = intrinsic_storage_gas(MegaSpecId::REX7); + let deployed = transact_create_reject( + base_db(creator_code(&deploying_initcode(0x00, RUNTIME_LEN))), + DEFAULT_TX_GAS_LIMIT, + None, + ); + assert_create_gas_identity("successful deposit", &deployed, intrinsic_storage); + + for (label, first, code_size_limit) in [ + // Runtime code starting with 0xEF: EIP-3541 rejects the deposit. + ("EIP-3541", 0xefu8, None), + // Runtime code past a configured contract-size limit. + ("code size", 0x00, Some(RUNTIME_LEN as usize - 1)), + ] { + let rejected = transact_create_reject( + base_db(creator_code(&deploying_initcode(first, RUNTIME_LEN))), + DEFAULT_TX_GAS_LIMIT, + code_size_limit, + ); + assert_create_gas_identity(label, &rejected, intrinsic_storage); + assert!( + rejected.gas_used > deployed.gas_used, + "{label}: the reject must destroy the CREATE frame's remainder, so it costs strictly \ + more than the deposit it replaced; rejected={} deployed={}", + rejected.gas_used, + deployed.gas_used, + ); + } +} + +/// The third post-action reject is the canonical code-deposit charge itself running out of gas — +/// the one that exists only inside a narrow gas window: too little gas and the frame fails earlier, +/// paying `MegaETH`'s own per-byte code-deposit storage charge; too much and the deposit goes +/// through. +/// +/// Sweeping across that window covers it without pinning the boundary. What every point has to +/// satisfy is that no EVM gas goes missing: the receipt's gas is compute gas plus storage gas, and +/// the only storage gas a run can carry beyond the transaction intrinsic is the per-byte +/// code-deposit charge — all of it, or none of it, depending on whether the frame could afford it. +/// A destroyed CREATE remainder that never reached the compute total would show up here as a third +/// value. +#[test] +fn test_create_code_deposit_out_of_gas_settles_its_destroyed_remainder() { + let intrinsic_storage = intrinsic_storage_gas(MegaSpecId::REX7); + let code_deposit_storage = CODEDEPOSIT_STORAGE_GAS * RUNTIME_LEN; + let initcode = deploying_initcode(0x00, RUNTIME_LEN); + let run = |gas_limit| transact_create_reject(base_db(creator_code(&initcode)), gas_limit, None); + + let deployed = run(DEFAULT_TX_GAS_LIMIT); + assert_create_gas_identity("successful deposit", &deployed, intrinsic_storage); + + // A successful deposit costs a fixed amount, and the frame that pays it keeps back the 2% the + // creator retained — so the window where the canonical charge alone is unaffordable sits just + // above that fixed cost, sized by the charge itself. + let canonical_deposit = CANONICAL_CODE_DEPOSIT_GAS * RUNTIME_LEN; + let mut code_deposit_oog_points = 0; + for step in 0..=canonical_deposit / 1_000 { + let gas_limit = deployed.gas_used + step * 1_000; + let outcome = run(gas_limit); + assert!( + outcome.is_success(), + "gas_limit={gas_limit}: the creator must survive the CREATE: {:?}", + outcome.result, + ); + let storage = outcome + .gas_used + .checked_sub(outcome.compute_gas) + .and_then(|total| total.checked_sub(intrinsic_storage)) + .unwrap_or_else(|| { + panic!( + "gas_limit={gas_limit}: compute gas exceeds the receipt's non-intrinsic gas; \ + compute={} gas_used={}", + outcome.compute_gas, outcome.gas_used, + ) + }); + assert!( + storage == 0 || storage == code_deposit_storage, + "gas_limit={gas_limit}: the only storage gas past the intrinsic is the per-byte \ + code-deposit charge, taken in full or not at all — anything else is EVM gas missing \ + from the compute total; storage={storage} compute={} gas_used={}", + outcome.compute_gas, + outcome.gas_used, + ); + // The charge was affordable but the deposit still did not happen: revm's create-return + // rejected it, after action processing, for the canonical code-deposit gas. + if storage == code_deposit_storage && outcome.gas_used != deployed.gas_used { + code_deposit_oog_points += 1; + } + } + assert!( + code_deposit_oog_points > 0, + "the sweep must contain at least one canonical code-deposit out-of-gas; a successful \ + deposit costs the same {} gas at every limit above the window", + deployed.gas_used, + ); +} + +/// Runs the blocker-A shape through `inspect_frame_run` instead of `frame_run`. +/// +/// The two loops are hand-maintained copies of the same body, and the split is settled across both +/// of their hooks — the executed tail before action processing, the destroyed remainder after. A +/// drop of either on the inspected copy alone would silently re-open the fail-open for any node +/// running with a tracer attached. +fn transact_inspected(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits, inspected: bool) -> u64 { + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + // Both arms must produce the same `MegaEvm` type, so build the inspected one by toggling the + // inspector flag rather than by changing the inspector type. + let mut evm = MegaEvm::new(context).with_inspector(NoOpInspector); + if !inspected { + alloy_evm::Evm::set_inspector_enabled(&mut evm, false); + } + let result: Result, EVMError> = + alloy_evm::Evm::transact_raw(&mut evm, tx); + result.expect("tx should not surface EVMError"); + let usage = EvmTr::ctx_ref(&evm).additional_limit.borrow().get_usage(); + usage.compute_gas +} + +/// The inspected execution loop must split an exceptional frame exactly like the plain one. +#[test] +fn test_the_split_is_the_same_under_an_inspector() { + let db = || caller_db(work_call_work(1_000_000, CHILD_PAIRS)); + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + assert_eq!( + transact_inspected(db(), limits, false), + transact_inspected(db(), limits, true), + "an attached inspector must not move the executed / destroyed split", + ); +} + +/// Builds a deterministic pre-EIP-155 keyless deployment transaction. +fn keyless_tx_bytes(init_code: Bytes) -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: 200_000, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// The `KeylessDeploy` sandbox runs a whole nested transaction with its own tracker and merges the +/// usage back, so the executed / destroyed split has to survive that boundary. A sandbox whose +/// constructor halts exceptionally reports its destroyed remainder like any other frame; if the +/// merge dropped the classification, the parent would enforce it — and a constructor's ordinary EVM +/// halt would rewrite the outer transaction into a compute-limit exceed with the gas rescued. +/// +/// The parent's compute limit is set well below the sandbox's gas override so the destroyed +/// remainder alone would be enough to trip it. +#[test] +fn test_sandbox_destroyed_remainder_stays_non_enforcing_across_the_merge() { + // `ADD` on an empty stack: the constructor halts immediately, so almost the whole sandbox + // envelope is destroyed rather than performed. + let init_code = BytecodeBuilder::default().append(ADD).append(STOP).build(); + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(init_code), + gasLimitOverride: U256::from(1_000_000u64), + } + .abi_encode(); + + let build_tx = || { + TxEnvBuilder::default() + .caller(KEYLESS_RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(30_000_000) + .chain_id(Some(1)) + .data(Bytes::from(call_data.clone())) + .build_fill() + }; + let limits = |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(300_000); + let run = |spec| { + let db = + MemoryDatabase::default().account_balance(KEYLESS_RELAYER, U256::from(10 * ONE_ETH)); + transact_tx(spec, db, limits(spec), build_tx(), &crate::common::default_envs()) + }; + + let r6 = run(MegaSpecId::REX6); + let r7 = run(MegaSpecId::REX7); + + assert!( + r6.is_success(), + "REX6 returns the constructor failure through the keyless-deploy wire contract: {:?}", + r6.result, + ); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the outer transaction must keep the wire contract REX6 defines — the sandbox's destroyed \ + budget is reported, never enforced, on either side of the merge", + ); + assert!( + r7.compute_gas > 300_000, + "the sandbox's destroyed remainder must still be reported past the limit; compute={}", + r7.compute_gas, + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 8a0847bb..dee25dd0 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -21,7 +21,11 @@ //! is shown to be stable rather than merely correct at one point. //! - `exceptional_halt` — every exceptional-halt classification, in both frame positions: the //! frame's whole burned budget settles as compute gas without changing the receipt. +//! - `burn_split` — which half of that budget enforces: the work the frame performed does, the +//! remainder it destroyed does not, and both boundaries (a checkpoint's storage charge, revm's +//! post-action create rejects) land on the right side. +mod burn_split; mod checkpoint_families; mod checkpoint_settlement; mod clamp_classification; From b16e94adf66a58c61cd568fb4371593dd454ca27 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 09:04:41 +0800 Subject: [PATCH 032/208] docs(rex7): specify the executed / destroyed split of an exceptional frame Rewrites the carve-out in the upgrade page and the compute-gas spec: which half enforces, where the split is taken from, what happens at the sandbox boundary, and the one shape the split cannot recover -- an ordinary out-of-gas with no clamp in force, whose zeroed counter makes the whole segment measure as executed. Also corrects the clamp contract: what a clamp keeps at or below the limit is the enforced usage, and the reported actual can be larger because it carries earlier frames' destroyed remainders. --- AGENTS.md | 3 ++- docs/spec/evm/compute-gas.md | 27 ++++++++++++++++------ docs/spec/upgrades/rex7.md | 43 ++++++++++++++++++++++++------------ 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1d1f87d4..476900ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,7 +116,8 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi - **Compute gas**: Measures pure computational cost. Through REX6 every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a V0 gas clamp. - A REX7 frame that ends in an exceptional halt additionally settles its whole burned remainder into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — the burn is destroyed gas, not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. + A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. + The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index b42ca17a..3a36e054 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -503,15 +503,28 @@ When the crossing opcode would exhaust both the true remaining EVM gas and the c #### Exceptional-halt frame carve-out A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. -A node MUST settle that entire burned remainder as compute gas at frame exit: the open plain-opcode segment measured against a zero remainder, plus any gas the clamp was hiding. -The rule is driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. -Under per-opcode recording through Rex6 neither the failing opcode nor the burn is attributed to compute gas, so a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a strictly higher compute-gas total under Rex7 while EVM gas and the receipt remain identical. +A node MUST settle that whole budget as compute gas, split into two parts that are accounted differently: -A node MUST NOT evaluate any resource limit against the burned remainder: it is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already burned and change the receipt this carve-out requires to stay identical. -The usage a limit is evaluated against therefore excludes the burn, while the reported compute-gas total and the block-level compute accounting include it. -Nothing is lost by the exclusion: the executed part of an exceptionally halted frame's tail is bounded either by the clamp or by a frame gas remainder that was already below the headroom. +- **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. +- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it. -A clamp-induced out-of-gas is not an exceptional halt for this rule — the crossing opcode never executed and the remaining gas is rescued rather than burned. +The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. +The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. + +The split MUST be driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. +That zeroing has one consequence a node MUST accept: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero when the frame exits, so the whole segment measures as executed and is enforced in full. +A node MUST NOT try to recover the split in that case. +It is the one shape where Rex7 enforcement is stricter than per-opcode enforcement through Rex6, which attributes the failing opcode to neither part. + +A node MUST take the split from the frame's **final** result, after the create-return processing that can still turn a successful constructor into a canonical code-deposit out-of-gas, an EIP-3541 reject or a runtime code-size reject. +Each of those destroys the frame's remainder just as a halt from the interpreter loop does. + +Under per-opcode recording through Rex6 neither the failing opcode nor the destroyed remainder is attributed to compute gas, so a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a strictly higher compute-gas total under Rex7 while EVM gas and the receipt remain identical. + +A clamp-induced out-of-gas is not an exceptional halt for this rule — the crossing opcode never executed and the remaining gas is rescued rather than destroyed. +A frame whose exit latches a resource-limit exceed destroys nothing either: it reverts to its parent (frame-local) or halts the transaction with its gas rescued (transaction-level). + +When a nested execution merges its usage into an outer one — the [KeylessDeploy](../system-contracts/keyless-deploy.md) sandbox is the only such boundary — a node MUST carry the split across it, reporting the inner total in full while enforcing only the executed part. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index f5db7714..f3acada6 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -24,7 +24,8 @@ Rex7 also introduces **gas-clamp enforcement**: between checkpoints the node res For a transaction that never crosses a compute-gas, detention, or other resource limit, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. For a transaction that does cross a compute-gas or detention limit inside a plain-opcode segment, the halt lands before the crossing opcode rather than after it, the crossing opcode's cost is excluded from recorded compute usage, and remaining gas remains refundable under the same rescue rules as other transaction-level compute-limit halts. -One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire burned EVM-gas budget as compute gas at frame exit, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. +One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. +That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. ## What Changed @@ -72,19 +73,29 @@ The interpreter's gas counter already meters every opcode; settling by segment r **Exceptional-halt frame carve-out.** A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. The top-level frame's whole envelope is spent by the transaction's final gas accounting, and an inner frame's remainder is never handed back to its caller. -A node MUST settle that entire burned remainder as compute gas at frame exit: the open plain-opcode segment measured against a zero remainder, plus any gas the clamp was hiding from the interpreter. -The rule is driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. +A node MUST settle that whole budget as compute gas, in two parts that are accounted differently. -Under per-opcode recording through Rex6, neither the failing opcode nor the burn is attributed to compute gas. -Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. +The **executed** part is the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. +A node MUST record it through the ordinary path, so it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against — exactly as the same opcodes would if the frame had returned normally. +A parent frame keeps executing after it absorbs a failed child; excluding the child's work from enforcement would let the code that follows spend the same compute headroom a second time. + +The **destroyed** part is whatever the frame still held when its result became final, including any gas the clamp was hiding from the interpreter. +A node MUST record it in the transaction's reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it. +It is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the reported total past that limit; halting on it would rescue gas the EVM has already destroyed and change a receipt this carve-out requires to stay identical. + +The split MUST be driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. +That zeroing has one consequence a node MUST accept rather than work around: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero at frame exit, so the whole segment measures as executed and is enforced in full. +This is the one shape where Rex7 enforcement is stricter than Rex6's, which attributes the failing opcode to neither part. -A node MUST NOT evaluate any resource limit against the burned remainder. -The burn is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the recorded total past that limit; halting on it would rescue gas the EVM has already burned and change a receipt this carve-out requires to stay identical. -The usage a limit is evaluated against therefore excludes the burn, while the transaction's reported compute-gas total and the block-level compute accounting include it. -Nothing is lost by that exclusion: the part of an exceptionally halted frame's tail that was actually executed is bounded either by the gas clamp or by a frame gas remainder that was already below the compute headroom, so it could not have exceeded a limit in the first place. +A node MUST take the split from the frame's **final** result, after the create-return processing that can still turn a successful constructor into a canonical code-deposit out-of-gas, an EIP-3541 reject or a runtime code-size reject — each of which destroys the frame's remainder just as a halt from the interpreter loop does. +When a nested execution merges its usage into an outer one, which today is only the `KeylessDeploy` sandbox boundary, a node MUST carry the split across that boundary: the outer transaction reports the inner total in full and enforces only its executed part. + +Under per-opcode recording through Rex6, neither the failing opcode nor the destroyed remainder is attributed to compute gas. +Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. A clamp-induced out-of-gas is not an exceptional halt for this rule. -The crossing opcode was stopped before it executed and the remaining gas is rescued for the sender rather than burned, so the reclassification rules below apply instead. +The crossing opcode was stopped before it executed and the remaining gas is rescued for the sender rather than destroyed, so the reclassification rules below apply instead. +A frame whose exit latches a resource-limit exceed destroys nothing either: it reverts to its parent, or halts the transaction with its gas rescued. ### Gas-Clamp Enforcement @@ -118,8 +129,10 @@ The reported `limit` MUST be the constraint that bound the clamp, not whichever The revert payload is visible to the calling contract, so a frame-local exceed that reported the transaction-level limit would be a different observable return value for the same execution, not merely a different diagnostic. Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. -Recorded usage at a clamp-induced halt therefore ends at the limit (or strictly below it if settlement had not yet closed a partial segment), not strictly above it. -The `actual` a transaction-level clamp halt reports MUST be that final usage — the frame-exit settlement closes the partial segment after the exceed is identified, and a node MUST NOT report the usage as it stood before that settlement. +The usage the clamp **enforces** therefore ends at or below the limit, not strictly above it. +The `actual` a transaction-level clamp halt reports MUST be the transaction's final reported compute usage — the frame-exit settlement closes the partial segment after the exceed is identified, and a node MUST NOT report the usage as it stood before that settlement. +Reported usage is not the same quantity as enforced usage: it also carries the destroyed remainders of any frame that halted exceptionally earlier in the transaction, which are reported and never enforced. +A node MUST NOT assume `actual` is at most `limit`. **Top-frame headroom tie-break.** At the top-level frame the remaining per-frame compute budget equals the transaction-level remaining budget whenever both are still governed by the same base limit. @@ -145,7 +158,8 @@ Contracts that stay within every resource limit see no behavioral change relativ Contracts that trip the compute-gas or detention limit inside a plain-opcode segment halt one opcode earlier than under Rex6, with the crossing opcode excluded from recorded compute usage and with remaining gas still refundable on a transaction-level halt. A transaction that halts exceptionally, or that calls into a child frame which does, may report a higher transaction-level compute-gas total under Rex7 than under Rex6 — for any exceptional halt, not just out-of-gas. -The receipt `gas_used`, the halt or revert reported, and the execution success or failure of the outer transaction are unchanged by that carve-out: the burned remainder is reported, never enforced. +The receipt `gas_used`, the halt or revert reported, and the execution success or failure of the outer transaction are unchanged by the destroyed half of that carve-out: it is reported, never enforced. +The executed half does enforce, so a contract that calls into a failing child and keeps working can trip a resource limit at the same point it would under Rex6 — and, for a child that ran out of gas with no clamp in force, marginally earlier. ## Safety and Compatibility @@ -156,8 +170,9 @@ Because Rex7 is unstable, its semantics may change in either direction until it Any node, tool, or test fixture pinned to Rex7 must expect its results to move. A deployment that needs stable semantics must select a frozen spec explicitly rather than relying on the latest one. -The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and recorded usage does not pass the limit by that opcode's cost. +The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and enforced usage does not pass the limit by that opcode's cost. The exceptional-halt frame carve-out is the only path on which Rex7 can report more compute gas than Rex6 for the same inputs; it over-reports rather than under-reports. +Its enforcing half is never looser than Rex6's, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. ## References From 476c7609c8c538d846112129c4b69d6524e699b9 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 10:37:16 +0800 Subject: [PATCH 033/208] fix(rex7): stop enforcing destroyed compute gas at the block limit The executed / destroyed split stopped at the transaction tracker. The outcome carried one compute number -- the full reported total, destroyed remainders included -- and the block limiter accumulated it into the one counter it compares against the block compute-gas limit. A transaction that destroyed a large gas envelope while performing almost no work therefore closed the block's compute capacity for the transactions behind it, re-enforcing at block level exactly what the transaction level had excluded. Carry the destroyed part through MegaTransactionOutcome into the commit path, and give the limiter two counters: block_compute_gas_used keeps reporting every transaction's whole total, while a new enforced counter carries only the work performed and is what admission, the block-full predicate and the ComputeGasLimit error read. Nothing is destroyed before Rex7, so the two counters advance in lockstep on every frozen spec. --- crates/mega-evm/src/block/executor.rs | 7 ++ crates/mega-evm/src/block/limit.rs | 127 ++++++++++++++++++++++-- crates/mega-evm/src/block/result.rs | 2 + crates/mega-evm/src/evm/mod.rs | 2 + crates/mega-evm/src/evm/result.rs | 13 +++ crates/mega-evm/tests/mutation/block.rs | 21 +++- 6 files changed, 160 insertions(+), 12 deletions(-) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index ed89c762..7e32f103 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -598,6 +598,7 @@ where data_size, kv_updates, compute_gas_used, + compute_gas_destroyed, state_growth_used, }, } = result; @@ -616,6 +617,11 @@ where // Accumulate post-execution resource usage into block-level counters. This does not // validate limits; over-limit enforcement happens in `pre_execution_check` before the // next transaction. The deposit-nonce record doubles as the deposit signal here. + // + // Compute gas crosses this boundary as the pair execution produced it — the full reported + // total and the destroyed part of it — so the limiter can report one and enforce the + // other. Collapsing them here would hand the block a single number that is right for + // reporting and wrong for admission. self.block_limiter.post_execution_update_raw( result.tx_gas_used(), tx_size, @@ -623,6 +629,7 @@ where data_size, kv_updates, compute_gas_used, + compute_gas_destroyed, state_growth_used, depositor.is_some(), ); diff --git a/crates/mega-evm/src/block/limit.rs b/crates/mega-evm/src/block/limit.rs index 58b6b23d..05026140 100644 --- a/crates/mega-evm/src/block/limit.rs +++ b/crates/mega-evm/src/block/limit.rs @@ -128,6 +128,8 @@ //! - Accumulates resource usage from the executed transaction into block-level counters //! - Does not validate post-execution limits; over-limit enforcement happens before admitting //! the next transaction in [`BlockLimiter::pre_execution_check`] +//! - Compute gas accumulates into two counters, one reported and one enforced; see +//! [`BlockLimiter::block_compute_gas_used`] //! //! 4. **Commit transaction** - [`crate::MegaBlockExecutor::commit_execution_outcome`] //! - Include in block (with success or failed receipt) @@ -604,6 +606,7 @@ impl BlockLimits { block_tx_size_used: 0, block_da_size_used: 0, block_compute_gas_used: 0, + block_compute_gas_enforced: 0, block_state_growth_used: 0, } } @@ -653,7 +656,9 @@ impl BlockLimits { /// let outcome = execute_transaction(tx); /// /// // Post-execution update (the executor commit path drives this internally) -/// limiter.post_execution_update_raw(gas, tx_size, da_size, data, kv, compute, growth, is_deposit); +/// limiter.post_execution_update_raw( +/// gas, tx_size, da_size, data, kv, compute, destroyed_compute, growth, is_deposit, +/// ); /// } /// ``` #[derive(Debug, Clone)] @@ -681,9 +686,25 @@ pub struct BlockLimiter { /// This tracks the total number of SSTORE operations across all transactions. pub block_kv_updates_used: u64, - /// Cumulative compute gas consumed by all transactions in the block. + /// Cumulative compute gas consumed by all transactions in the block, as reported. + /// + /// This is the block's public compute-gas statistic: every transaction's full reported total, + /// including the remainders Rex7+ exceptionally halted frames destroyed rather than performed. + /// Admission does not read it — + /// [`block_compute_gas_enforced`](Self::block_compute_gas_enforced) is the counter the + /// block compute-gas limit is evaluated against. pub block_compute_gas_used: u64, + /// The part of [`block_compute_gas_used`](Self::block_compute_gas_used) the block enforces: + /// the same total with each transaction's destroyed remainder subtracted. + /// + /// Destroyed gas is not work the network performed, and no resource limit is evaluated against + /// it at any level. A transaction whose reported total dwarfs its executed work would + /// otherwise close the block's compute capacity for everyone behind it while having computed + /// almost nothing. Before Rex7 nothing is ever destroyed, so this counter and the reported one + /// advance in lockstep. + pub block_compute_gas_enforced: u64, + /// Cumulative state growth consumed by all transactions in the block. pub block_state_growth_used: u64, } @@ -709,6 +730,7 @@ impl BlockLimiter { block_tx_size_used: 0, block_da_size_used: 0, block_compute_gas_used: 0, + block_compute_gas_enforced: 0, block_state_growth_used: 0, } } @@ -866,12 +888,14 @@ impl BlockLimiter { })); } - // Check block-level compute gas limit - if self.block_compute_gas_used >= self.limits.block_compute_gas_limit { + // Check block-level compute gas limit. The enforced counter is the one compared, and so + // the one the error reports: destroyed remainders are reported in + // `block_compute_gas_used` but never close the block's compute capacity. + if self.block_compute_gas_enforced >= self.limits.block_compute_gas_limit { return Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { hash: tx_hash, error: Box::new(MegaBlockLimitExceededError::ComputeGasLimit { - block_used: self.block_compute_gas_used, + block_used: self.block_compute_gas_enforced, limit: self.limits.block_compute_gas_limit, }), })); @@ -900,6 +924,12 @@ impl BlockLimiter { /// the transaction may push the block over a limit, which is intentional to maximize block /// utilization. `is_deposit` gates only the DA-size counter: deposits are exempt from DA /// accounting. + /// + /// Compute gas arrives as two numbers, not one: `compute_gas_used` is the transaction's full + /// reported total and `compute_gas_destroyed` is the part of it that Rex7+ exceptionally + /// halted frames destroyed rather than performed (0 before Rex7). The reported total lands in + /// the public statistic and the difference in the counter the block compute-gas limit is + /// evaluated against. #[allow(clippy::too_many_arguments)] pub fn post_execution_update_raw( &mut self, @@ -909,6 +939,7 @@ impl BlockLimiter { tx_data: u64, kv_updates: u64, compute_gas_used: u64, + compute_gas_destroyed: u64, state_growth_used: u64, is_deposit: bool, ) { @@ -934,8 +965,11 @@ impl BlockLimiter { self.block_kv_updates_used = self.block_kv_updates_used.saturating_add(kv_updates); // Block compute gas limit, no need to check here since we allow the last transaction to - // exceed the limit. + // exceed the limit. Only the executed part advances the enforced counter. self.block_compute_gas_used = self.block_compute_gas_used.saturating_add(compute_gas_used); + self.block_compute_gas_enforced = self + .block_compute_gas_enforced + .saturating_add(compute_gas_used.saturating_sub(compute_gas_destroyed)); // Block state growth limit, no need to check here since we allow the last transaction to // exceed the limit. @@ -944,13 +978,16 @@ impl BlockLimiter { } /// Returns true if any block-level limit has been reached or exceeded. + /// + /// Compute gas answers on the enforced counter, matching what + /// [`pre_execution_check`](Self::pre_execution_check) would reject the next transaction on. pub fn is_block_limit_reached(&self) -> bool { self.block_gas_used >= self.limits.block_gas_limit || self.block_tx_size_used >= self.limits.block_txs_encode_size_limit || self.block_da_size_used >= self.limits.block_da_size_limit || self.block_data_used >= self.limits.block_txs_data_limit || self.block_kv_updates_used >= self.limits.block_kv_update_limit || - self.block_compute_gas_used >= self.limits.block_compute_gas_limit || + self.block_compute_gas_enforced >= self.limits.block_compute_gas_limit || self.block_state_growth_used >= self.limits.block_state_growth_limit } } @@ -1014,6 +1051,7 @@ mod tests { limiter.block_data_used = u64::MAX - 1; limiter.block_kv_updates_used = u64::MAX - 1; limiter.block_compute_gas_used = u64::MAX - 1; + limiter.block_compute_gas_enforced = u64::MAX - 1; limiter.block_state_growth_used = u64::MAX - 1; limiter.post_execution_update_raw( @@ -1023,6 +1061,7 @@ mod tests { u64::MAX, u64::MAX, u64::MAX, + 0, u64::MAX, false, ); @@ -1033,6 +1072,7 @@ mod tests { assert_eq!(limiter.block_data_used, u64::MAX); assert_eq!(limiter.block_kv_updates_used, u64::MAX); assert_eq!(limiter.block_compute_gas_used, u64::MAX); + assert_eq!(limiter.block_compute_gas_enforced, u64::MAX); assert_eq!(limiter.block_state_growth_used, u64::MAX); } @@ -1043,8 +1083,79 @@ mod tests { let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); limiter.block_da_size_used = 100; - limiter.post_execution_update_raw(0, 0, u64::MAX, 0, 0, 0, 0, true); + limiter.post_execution_update_raw(0, 0, u64::MAX, 0, 0, 0, 0, 0, true); assert_eq!(limiter.block_da_size_used, 100); } + + /// The two compute-gas counters accumulate different things: the reported one takes the + /// transaction's whole total, the enforced one only the part the transaction performed. A + /// destroyed remainder that leaked into the enforced counter would close the block's compute + /// capacity for work that never happened. + #[test] + fn test_post_execution_update_raw_splits_the_compute_gas_lanes() { + let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); + + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 1_000_000, 900_000, 0, false); + assert_eq!(limiter.block_compute_gas_used, 1_000_000, "the report takes the whole total"); + assert_eq!(limiter.block_compute_gas_enforced, 100_000, "enforcement takes only the work"); + + // A second transaction that destroyed nothing advances both counters by the same amount. + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 50_000, 0, 0, false); + assert_eq!(limiter.block_compute_gas_used, 1_050_000); + assert_eq!(limiter.block_compute_gas_enforced, 150_000); + } + + /// Nothing is ever destroyed before Rex7, so a block of transactions that report a zero + /// destroyed part leaves the two counters equal at every step — the pre-Rex7 behaviour, which + /// the split must reproduce byte for byte. + #[test] + fn test_compute_gas_lanes_coincide_without_a_destroyed_part() { + let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); + + for compute in [21_000, 500, 1_234_567, 0] { + limiter.post_execution_update_raw(0, 0, 0, 0, 0, compute, 0, 0, false); + assert_eq!( + limiter.block_compute_gas_used, limiter.block_compute_gas_enforced, + "with nothing destroyed the reported and enforced counters must not diverge" + ); + } + } + + /// Admission compares the enforced counter, and the error it raises must state that same + /// number: a rejected transaction's operator reads `block_used` to understand what filled the + /// block, and the reported total would name a budget the block never spent. + #[test] + fn test_block_compute_gas_admission_reads_the_enforced_counter() { + let mut limits = BlockLimits::no_limits(); + limits.block_compute_gas_limit = 1_000_000; + let mut limiter = BlockLimiter::new(limits); + + // One transaction reporting far past the block limit, having performed almost none of it. + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 5_000_000, 4_950_000, 0, false); + assert!( + limiter.block_compute_gas_used > limits.block_compute_gas_limit, + "the reported total must carry the destroyed remainder past the limit" + ); + assert!( + !limiter.is_block_limit_reached(), + "a destroyed remainder must not fill the block's compute capacity" + ); + assert!( + limiter.pre_execution_check(B256::ZERO, 0, 0, 0, false).is_ok(), + "the next transaction must still be admitted" + ); + + // Executed work fills it, and the error names the enforced counter. + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 950_000, 0, 0, false); + assert!(limiter.is_block_limit_reached(), "executed work does fill the block"); + let error = limiter + .pre_execution_check(B256::ZERO, 0, 0, 0, false) + .expect_err("a full block must reject the next transaction"); + let message = format!("{error:?}"); + assert!( + message.contains("ComputeGasLimit") && message.contains("block_used: 1000000"), + "the error must report the counter that was compared, got {message}" + ); + } } diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 8fcf9743..2387ce2a 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -280,6 +280,7 @@ mod tests { data_size: 1, kv_updates: 2, compute_gas_used: 3, + compute_gas_destroyed: 1, state_growth_used: 4, }; @@ -301,6 +302,7 @@ mod tests { // One hop for the resource dimensions (`Copy` scalars may leave through a deref). let kv: u64 = outcome.kv_updates; assert_eq!((kv, outcome.compute_gas_used, outcome.state_growth_used), (2, 3, 4)); + assert_eq!(outcome.compute_gas_destroyed, 1); } #[test] diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index a536920d..b8173420 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -367,6 +367,7 @@ where data_size, kv_updates, compute_gas_used: compute_gas, + compute_gas_destroyed: additional_limit.burned_compute_gas(), state_growth_used: state_growth, }) } @@ -398,6 +399,7 @@ where data_size, kv_updates, compute_gas_used: compute_gas, + compute_gas_destroyed: additional_limit.burned_compute_gas(), state_growth_used: state_growth, }) } diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 0969d9da..a14d4358 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -29,7 +29,20 @@ pub struct MegaTransactionOutcome { /// The number of KV updates. pub kv_updates: u64, /// The compute gas used. + /// + /// This is the transaction's full reported total, which under Rex7+ also carries whatever an + /// exceptionally halted frame destroyed rather than performed. It is the number to report and + /// to accumulate into block-level compute accounting; it is not the number to compare against + /// a limit — see [`compute_gas_destroyed`](Self::compute_gas_destroyed). pub compute_gas_used: u64, + /// The part of [`compute_gas_used`](Self::compute_gas_used) that exceptionally halted frames + /// destroyed rather than performed (Rex7+, always 0 before). + /// + /// Destroyed gas is not work the network did, so no resource limit is evaluated against it. + /// The transaction's own limits already excluded it while executing; a consumer that + /// accumulates this outcome into a further limit — today the block compute-gas counter — must + /// subtract it too, and compare `compute_gas_used - compute_gas_destroyed` instead. + pub compute_gas_destroyed: u64, /// The state growth used. pub state_growth_used: u64, } diff --git a/crates/mega-evm/tests/mutation/block.rs b/crates/mega-evm/tests/mutation/block.rs index 727e4881..d66eb34c 100644 --- a/crates/mega-evm/tests/mutation/block.rs +++ b/crates/mega-evm/tests/mutation/block.rs @@ -425,10 +425,10 @@ fn test_pre_execution_check_block_da_size_boundary() { fn test_post_execution_update_raw_da_gated_by_deposit_flag() { let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); - limiter.post_execution_update_raw(0, 0, 1_234, 0, 0, 0, 0, false); + limiter.post_execution_update_raw(0, 0, 1_234, 0, 0, 0, 0, 0, false); assert_eq!(limiter.block_da_size_used, 1_234, "a non-deposit call must accumulate da_size"); - limiter.post_execution_update_raw(0, 0, 5_000, 0, 0, 0, 0, true); + limiter.post_execution_update_raw(0, 0, 5_000, 0, 0, 0, 0, 0, true); assert_eq!( limiter.block_da_size_used, 1_234, "a deposit call must leave the da counter untouched" @@ -457,6 +457,7 @@ fn test_is_block_limit_reached_all_below_is_false() { limiter.block_data_used = 9; limiter.block_kv_updates_used = 9; limiter.block_compute_gas_used = 9; + limiter.block_compute_gas_enforced = 9; limiter.block_state_growth_used = 9; assert!( @@ -487,6 +488,7 @@ macro_rules! only_dimension_at_limit { limiter.block_data_used = 0; limiter.block_kv_updates_used = 0; limiter.block_compute_gas_used = 0; + limiter.block_compute_gas_enforced = 0; limiter.block_state_growth_used = 0; // ...except the one under test, which sits exactly at its (5) limit. limiter.$used_field = 5; @@ -524,10 +526,21 @@ fn test_is_block_limit_reached_kv_updates_dimension() { assert!(limiter.is_block_limit_reached(), "kv updates at limit ⇒ block full"); } +/// Compute gas is the one dimension whose clause reads a counter other than the `*_used` one: +/// admission is evaluated against the enforced counter, so the reported total sitting at the +/// limit must leave the block open. #[test] fn test_is_block_limit_reached_compute_gas_dimension() { - let limiter = only_dimension_at_limit!(block_compute_gas_limit, block_compute_gas_used); - assert!(limiter.is_block_limit_reached(), "compute gas at limit ⇒ block full"); + let limiter = only_dimension_at_limit!(block_compute_gas_limit, block_compute_gas_enforced); + assert!(limiter.is_block_limit_reached(), "enforced compute gas at limit ⇒ block full"); + + let mut reported_only = + only_dimension_at_limit!(block_compute_gas_limit, block_compute_gas_used); + reported_only.block_compute_gas_enforced = 0; + assert!( + !reported_only.is_block_limit_reached(), + "a reported total at the limit with nothing enforced must leave the block open" + ); } #[test] From 432c4ce6dc3d90f6f86facf2d7f329c9bc73d50f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 10:41:02 +0800 Subject: [PATCH 034/208] test(rex7): admit a cheap transaction behind a destroyed remainder Drives both block compute-gas counters through the real run/commit path in the two shapes that destroy a remainder: an ordinary frame that halts on its first opcode, and one nested inside the KeylessDeploy sandbox, where the outer transaction succeeds and nothing in its result hints that a remainder was destroyed at all. Each asserts the block reports the whole total, enforces only the work performed, and still admits the cheap transaction behind it. Two more pin the other directions: executed work does still close a block, and Rex6 keeps the two counters identical. --- .../tests/block_executor/compute_gas_lanes.rs | 324 ++++++++++++++++++ crates/mega-evm/tests/block_executor/main.rs | 1 + 2 files changed, 325 insertions(+) create mode 100644 crates/mega-evm/tests/block_executor/compute_gas_lanes.rs diff --git a/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs b/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs new file mode 100644 index 00000000..9a3895ca --- /dev/null +++ b/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs @@ -0,0 +1,324 @@ +//! The Rex7 executed / destroyed compute-gas split, as the block sees it. +//! +//! A frame that halts exceptionally destroys the budget it was still holding. Rex7 reports that +//! remainder as compute gas but enforces no limit against it — the transaction level already +//! settled that, and the block level has to reach the same answer, because a transaction that +//! destroyed a large gas envelope while performing almost no work would otherwise close the +//! block's compute capacity for every transaction behind it. +//! +//! So the block keeps two compute-gas counters: `block_compute_gas_used` reports every +//! transaction's whole total, and `block_compute_gas_enforced` carries only the work performed and +//! is what admission compares. These tests drive both counters through the real commit path, in +//! the two shapes that produce a destroyed remainder — an ordinary exceptional frame, and one +//! nested inside the `KeylessDeploy` sandbox, which merges a whole separate tracker back across a +//! boundary the classification has to survive. +//! +//! Nothing is destroyed before Rex7, so the frozen specs are pinned here too: the two counters +//! must not diverge by so much as a gas unit under Rex6. + +use std::convert::Infallible; + +use alloy_evm::{block::BlockExecutor, EvmEnv, EvmFactory}; +use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; +use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{transaction::Recovered, Signed, TxLegacy}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + BlockLimits, IKeylessDeploy, MegaBlockExecutionCtx, MegaBlockExecutor, MegaEvmFactory, + MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{ADD, STOP}, + context::BlockEnv, + database::State, +}; + +/// Sends every transaction in these tests. +const CALLER: Address = address!("2000000000000000000000000000000000000002"); +/// `ADD` on an empty stack: the call halts on its first opcode, so nearly the whole envelope it +/// was forwarded is destroyed rather than performed. +const HALTING: Address = address!("1000000000000000000000000000000000000001"); +/// `STOP`: the cheap transaction that has to still fit in the block afterwards. +const CHEAP: Address = address!("1000000000000000000000000000000000000002"); + +/// Gas envelope the halting transaction destroys. Large enough that its reported compute total +/// alone dwarfs [`BLOCK_COMPUTE_GAS_LIMIT`], while the work it performed before failing — one +/// three-gas `ADD` on top of the intrinsic cost — stays far below it. +const HALTING_TX_GAS_LIMIT: u64 = 5_000_000; + +/// Gas envelope the `KeylessDeploy` sandbox destroys, passed as the call's gas-limit override. +const SANDBOX_GAS_OVERRIDE: u64 = 4_000_000; + +/// The block compute-gas ceiling these tests build around: above what any of these transactions +/// executes, below what the halting ones report. +const BLOCK_COMPUTE_GAS_LIMIT: u64 = 1_000_000; + +/// Builds a legacy transaction from `CALLER`. +fn envelope(nonce: u64, gas_limit: u64, to: Address, input: Bytes) -> MegaTxEnvelope { + let tx = TxLegacy { + chain_id: Some(8453), + nonce, + gas_price: 1_000_000, + gas_limit, + to: TxKind::Call(to), + value: U256::ZERO, + input, + }; + MegaTxEnvelope::Legacy(Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)) +} + +/// The pre-EIP-155 deployment transaction the `KeylessDeploy` sandbox replays, carrying a +/// constructor that halts on its first opcode. +fn keyless_deploy_call_data() -> Bytes { + let init_code = BytecodeBuilder::default().append(ADD).append(STOP).build(); + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: 200_000, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes([0x33; 32]); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut encoded = Vec::new(); + signed.rlp_encode(&mut encoded); + + Bytes::from( + IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: Bytes::from(encoded), + gasLimitOverride: U256::from(SANDBOX_GAS_OVERRIDE), + } + .abi_encode(), + ) +} + +/// The database every test here runs against: the two callees plus a funded sender. +fn build_db() -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_code(HALTING, BytecodeBuilder::default().append(ADD).append(STOP).build()); + db.set_account_code(CHEAP, BytecodeBuilder::default().stop().build()); + db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000_000u64)); + db +} + +/// What one committed transaction contributed, and where the block's counters stood afterwards. +#[derive(Debug, Clone, Copy)] +struct Contribution { + /// Whether the transaction's own execution result reports success. + succeeded: bool, + /// The transaction's full reported compute total. + reported: u64, + /// The part of `reported` its exceptionally halted frames destroyed. + destroyed: u64, + /// The block's reported compute counter after the commit. + block_reported: u64, + /// The block's enforced compute counter after the commit. + block_enforced: u64, + /// Whether the block considers itself full after the commit. + block_full: bool, +} + +/// Runs `txs` through one block at `spec`, committing each in turn, and returns what each +/// contributed. Stops at the first transaction the block refuses to admit, so the returned vector +/// is shorter than `txs` exactly when the block closed early. +fn run_block( + spec: MegaSpecId, + block_compute_gas_limit: u64, + txs: &[MegaTxEnvelope], +) -> Vec { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let external_envs = TestExternalEnvs::::new(); + let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs); + + let mut cfg_env = revm::context::CfgEnv::default(); + cfg_env.spec = spec; + let block_env = BlockEnv { + number: U256::from(1000), + timestamp: U256::from(1_800_000_000), + gas_limit: 30_000_000, + ..Default::default() + }; + let evm = evm_factory.create_evm(&mut state, EvmEnv::new(cfg_env, block_env)); + + let block_ctx = MegaBlockExecutionCtx::new( + B256::ZERO, + None, + Bytes::new(), + BlockLimits::no_limits().with_block_compute_gas_limit(block_compute_gas_limit), + ); + let chain_spec = MegaHardforkConfig::default().with_all_activated_through(spec); + let mut executor = + MegaBlockExecutor::new(evm, block_ctx, chain_spec, OpAlloyReceiptBuilder::default()); + + let mut contributions = Vec::new(); + for tx in txs { + let Ok(outcome) = executor.run_transaction(Recovered::new_unchecked(tx, CALLER)) else { + break; + }; + let succeeded = outcome.result.is_success(); + let reported = outcome.compute_gas_used; + let destroyed = outcome.compute_gas_destroyed; + executor.commit_transaction_outcome(outcome).expect("the commit must be admitted too"); + + let limiter = &executor.block_limiter; + contributions.push(Contribution { + succeeded, + reported, + destroyed, + block_reported: limiter.block_compute_gas_used, + block_enforced: limiter.block_compute_gas_enforced, + block_full: limiter.is_block_limit_reached(), + }); + } + + let (_, receipts) = executor.finish().expect("the block must finish"); + assert_eq!( + receipts.receipts.len(), + contributions.len(), + "every admitted transaction must have produced a receipt" + ); + contributions +} + +/// Asserts the shape both destroyed-remainder tests need from their first transaction: it reported +/// past the block's compute ceiling, but performed far too little to have earned that. +fn assert_reports_past_the_limit_without_performing_it(label: &str, first: &Contribution) { + assert!(first.destroyed > 0, "{label}: the frame must have destroyed a remainder"); + assert!( + first.reported > BLOCK_COMPUTE_GAS_LIMIT, + "{label}: the reported total must exceed the block limit, got {}", + first.reported, + ); + assert!( + first.reported - first.destroyed < BLOCK_COMPUTE_GAS_LIMIT, + "{label}: the work performed must stay under the block limit, got {}", + first.reported - first.destroyed, + ); +} + +/// An ordinary exceptional frame: a call that halts on its first opcode with a large envelope +/// still in hand. +/// +/// The block must report what the transaction reported — destroyed remainder included, which is +/// what makes the reported counter cross the ceiling — and must still admit the cheap transaction +/// behind it, because the enforced counter only ever saw the three gas the `ADD` charged before +/// underflowing. +#[test] +fn test_rex7_destroyed_remainder_reports_at_block_level_without_closing_the_block() { + let txs = [ + envelope(0, HALTING_TX_GAS_LIMIT, HALTING, Bytes::new()), + envelope(1, 100_000, CHEAP, Bytes::new()), + ]; + let block = run_block(MegaSpecId::REX7, BLOCK_COMPUTE_GAS_LIMIT, &txs); + + assert_eq!(block.len(), 2, "the cheap transaction must still fit in the block"); + let [halting, cheap] = [block[0], block[1]]; + + assert!(!halting.succeeded, "the first transaction must halt"); + assert_reports_past_the_limit_without_performing_it("ordinary frame", &halting); + + assert_eq!( + halting.block_reported, halting.reported, + "the block's reported statistic must carry the destroyed remainder" + ); + assert_eq!( + halting.block_enforced, + halting.reported - halting.destroyed, + "the block must enforce only the work performed" + ); + assert!( + !halting.block_full, + "a destroyed remainder must not fill the block's compute capacity" + ); + + assert!(cheap.succeeded, "the second transaction must execute normally"); + assert_eq!( + cheap.block_reported, + halting.reported + cheap.reported, + "the reported counter keeps accumulating whole totals" + ); + assert_eq!( + cheap.block_enforced, + halting.block_enforced + cheap.reported, + "a transaction that destroys nothing advances both counters by the same amount" + ); +} + +/// The same shape, one boundary deeper: the frame that halts lives inside the `KeylessDeploy` +/// sandbox, whose usage is merged back into the outer transaction through a separate tracker. +/// +/// The outer transaction succeeds — the sandbox reports a failed deployment through the +/// keyless-deploy wire contract rather than failing itself — so nothing about the outer result +/// hints that a remainder was destroyed. If the merge or the outcome dropped the classification, +/// the block would silently enforce a sandbox's destroyed budget against every transaction behind +/// it. +#[test] +fn test_rex7_sandbox_destroyed_remainder_does_not_close_the_block_either() { + let txs = [ + envelope(0, 30_000_000, KEYLESS_DEPLOY_ADDRESS, keyless_deploy_call_data()), + envelope(1, 100_000, CHEAP, Bytes::new()), + ]; + let block = run_block(MegaSpecId::REX7, BLOCK_COMPUTE_GAS_LIMIT, &txs); + + assert_eq!(block.len(), 2, "the cheap transaction must still fit in the block"); + let [sandbox, cheap] = [block[0], block[1]]; + + assert!(sandbox.succeeded, "the keyless deploy reports the constructor failure, it is not one"); + assert_reports_past_the_limit_without_performing_it("sandbox frame", &sandbox); + + assert_eq!( + sandbox.block_reported, sandbox.reported, + "the block's reported statistic must carry the sandbox's destroyed remainder" + ); + assert_eq!( + sandbox.block_enforced, + sandbox.reported - sandbox.destroyed, + "the block must enforce only the work the sandbox performed" + ); + assert!(!sandbox.block_full, "a sandbox's destroyed remainder must not fill the block"); + assert!(cheap.succeeded, "the second transaction must execute normally"); +} + +/// Executed work still fills the block: the split is a classification, not a way out of the block +/// compute-gas limit. +#[test] +fn test_rex7_executed_work_still_closes_the_block() { + // A ceiling below what a single halting transaction's `ADD`-plus-intrinsic work costs. + let txs = [ + envelope(0, HALTING_TX_GAS_LIMIT, HALTING, Bytes::new()), + envelope(1, 100_000, CHEAP, Bytes::new()), + ]; + let block = run_block(MegaSpecId::REX7, 1, &txs); + + assert_eq!(block.len(), 1, "the block must close once the enforced counter reaches the limit"); + assert!(block[0].block_full, "the work performed does fill a block this small"); +} + +/// Rex6 destroys nothing, so the enforced counter must track the reported one exactly — through a +/// block that mixes a successful transaction with one that halts on its first opcode, the shape +/// that diverges under Rex7. +#[test] +fn test_rex6_block_compute_counters_never_diverge() { + let txs = [ + envelope(0, HALTING_TX_GAS_LIMIT, HALTING, Bytes::new()), + envelope(1, 100_000, CHEAP, Bytes::new()), + envelope(2, HALTING_TX_GAS_LIMIT, HALTING, Bytes::new()), + ]; + let block = run_block(MegaSpecId::REX6, u64::MAX, &txs); + + assert_eq!(block.len(), 3, "no transaction here approaches an unlimited block"); + for (index, contribution) in block.iter().enumerate() { + assert_eq!( + contribution.destroyed, 0, + "tx {index}: no spec before Rex7 destroys compute gas" + ); + assert_eq!( + contribution.block_reported, contribution.block_enforced, + "tx {index}: the two counters must stay identical on a frozen spec" + ); + } +} diff --git a/crates/mega-evm/tests/block_executor/main.rs b/crates/mega-evm/tests/block_executor/main.rs index 663e9301..5538bff0 100644 --- a/crates/mega-evm/tests/block_executor/main.rs +++ b/crates/mega-evm/tests/block_executor/main.rs @@ -2,6 +2,7 @@ mod accessed_block_hashes; mod block_limits; +mod compute_gas_lanes; mod deposit_da_exemption; mod inspector; mod sequencer_registry; From bcff49cfb76fc276cfa8ddc04a4a76b0c6dac590 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 10:42:03 +0800 Subject: [PATCH 035/208] docs: split block compute accounting into reported and enforced State the block-level half of the exceptional-halt carve-out: a node that tracks cumulative block compute gas tracks two readings, reports the one that carries destroyed remainders and compares the one that does not. The carve-out already said no resource limit is evaluated against a destroyed remainder; it did not say which counter a block-level ceiling is allowed to read, which is where the rule was lost. --- AGENTS.md | 1 + docs/spec/evm/compute-gas.md | 2 +- docs/spec/evm/resource-limits.md | 8 +++++++- docs/spec/upgrades/rex7.md | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 476900ff..88a83c4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a V0 gas clamp. A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. + The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 3a36e054..54cb1bb6 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -506,7 +506,7 @@ A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of- A node MUST settle that whole budget as compute gas, split into two parts that are accounted differently: - **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. -- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it. +- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. diff --git a/docs/spec/evm/resource-limits.md b/docs/spec/evm/resource-limits.md index 969826f7..131d18a3 100644 --- a/docs/spec/evm/resource-limits.md +++ b/docs/spec/evm/resource-limits.md @@ -131,6 +131,12 @@ Subsequent candidate transactions MUST be skipped before execution once the bloc Although block compute gas usage MAY be tracked, the protocol does not impose a separate block-level compute gas cap. +From [Rex7](../upgrades/rex7.md) onward, a node that tracks cumulative block compute gas MUST track it as two readings, because the [exceptional-halt frame carve-out](compute-gas.md#exceptional-halt-frame-carve-out) makes them differ. +The **reported** reading accumulates each transaction's full compute-gas total, destroyed remainders included; it is the block's compute-gas statistic. +The **enforced** reading accumulates only the part each transaction performed, and is the only one a node MAY compare against a configured block compute-gas ceiling, and the only one such a ceiling's rejection MUST report as the block's usage. +Comparing the reported reading instead would let a transaction that destroyed a large gas envelope while performing almost no work close the block's compute capacity for every transaction behind it. +Before Rex7 nothing is destroyed, so the two readings coincide. + ### Two-Phase Block Building Workflow When constructing a block, a node or sequencer MUST process candidate transactions in the following order: @@ -246,4 +252,4 @@ Including failed transactions ensures the sender always pays for consumed resour - [Rex4](../upgrades/rex4.md) — added per-call-frame runtime budgets; intrinsic resource costs (always deducted before execution) are now reflected in the top-level frame budget before it is forwarded to child frames. - [Rex5](../upgrades/rex5.md) — bounded a precompile invocation's compute-gas consumption by the remaining compute-gas budget, failing the precompile with `PrecompileOOG` rather than letting it overshoot the budget. - [Rex6](../upgrades/rex6.md) — moved EIP-7702 authority state-growth resolution from pre-execution (after the caller nonce bump) to validation, and added dynamic SALT account-creation gas for each net-new applied authority to the pre-frame intrinsic gas deduction; removed the keyless-deploy exception to gas preservation, so remaining gas is now rescued on every transaction-level exceed; and stopped enforcing the four runtime transaction-level limits against system-originated transactions, whose usage is still recorded. -- [Rex7](../upgrades/rex7.md) _(unstable)_ — does not change the limit ceilings or the success/failed/skipped/rejected outcomes; a compute-gas or detention exceed inside a plain-opcode segment is stopped before the crossing opcode executes (see [Compute Gas Accounting](compute-gas.md)). +- [Rex7](../upgrades/rex7.md) _(unstable)_ — does not change the limit ceilings or the success/failed/skipped/rejected outcomes; a compute-gas or detention exceed inside a plain-opcode segment is stopped before the crossing opcode executes, and cumulative block compute gas splits into a reported and an enforced reading (see [Compute Gas Accounting](compute-gas.md)). diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index f3acada6..dbc284e8 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -80,7 +80,7 @@ A node MUST record it through the ordinary path, so it counts toward the transac A parent frame keeps executing after it absorbs a failed child; excluding the child's work from enforcement would let the code that follows spend the same compute headroom a second time. The **destroyed** part is whatever the frame still held when its result became final, including any gas the clamp was hiding from the interpreter. -A node MUST record it in the transaction's reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it. +A node MUST record it in the transaction's reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it — at transaction level or at block level, where a destroyed remainder that counted toward admission would close the block's compute capacity for the transactions behind it (see [Resource Limits](../evm/resource-limits.md)). It is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the reported total past that limit; halting on it would rescue gas the EVM has already destroyed and change a receipt this carve-out requires to stay identical. The split MUST be driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. From cbf4f4d6b2723d210354bc98202faca20de458e3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 10:49:16 +0800 Subject: [PATCH 036/208] docs: say which compute counter a block ComputeGasLimit rejection reports --- crates/mega-evm/src/block/result.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 2387ce2a..3aa3c6f7 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -170,7 +170,11 @@ pub enum MegaBlockLimitExceededError { /// Block compute gas limit reached. #[error("Block compute gas limit reached: block_used={block_used} >= limit={limit}")] ComputeGasLimit { - /// Compute gas used by block so far + /// Compute gas used by block so far, as the limit measures it. + /// + /// This is the enforced reading — the counter that was actually compared — so it excludes + /// the remainders Rex7+ exceptionally halted frames destroyed. The block's full reported + /// compute statistic, which includes them, can be higher. block_used: u64, /// Block compute gas limit limit: u64, From 54d95b4dfa06a5e9b2487a20ed19389cb6fdf219 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 12 Aug 2026 11:18:53 +0800 Subject: [PATCH 037/208] docs(rex7): qualify precision invariant to exclude exceptional-halt frames The within-limit bit-identical claim conflicted with the exceptional-halt carve-out: a StackUnderflow inside all resource limits can still diverge on reported compute total. Require that no frame ends in an exceptional halt before asserting compute-total / four-dimension parity with Rex6. --- docs/spec/evm/compute-gas.md | 2 +- docs/spec/upgrades/overview.md | 2 +- docs/spec/upgrades/rex7.md | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 54cb1bb6..ed6e7035 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -473,7 +473,7 @@ At each checkpoint a node MUST: Non-opcode recording sites on this page (intrinsic gas, precompiles, code deposit, KeylessDeploy) are unchanged. -For every transaction that stays within every runtime resource limit, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. +For every transaction that stays within every runtime resource limit and in which no frame ends in an exceptional halt, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. #### Gas-clamp enforcement diff --git a/docs/spec/upgrades/overview.md b/docs/spec/upgrades/overview.md index 8898a870..d19dad89 100644 --- a/docs/spec/upgrades/overview.md +++ b/docs/spec/upgrades/overview.md @@ -153,7 +153,7 @@ Not yet scheduled {% endtabs %} Unstable; under active development. -Checkpoint-settled [compute gas](../glossary.md#compute-gas) accounting with gas-clamp enforcement: plain opcodes record nothing between checkpoints; within-limit transactions stay bit-identical to Rex6; a compute-gas or detention exceed inside a plain segment stops the crossing opcode before it executes. +Checkpoint-settled [compute gas](../glossary.md#compute-gas) accounting with gas-clamp enforcement: plain opcodes record nothing between checkpoints; within-limit transactions that never end a frame in an exceptional halt stay bit-identical to Rex6; a compute-gas or detention exceed inside a plain segment stops the crossing opcode before it executes. ## How to Read These Pages diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index dbc284e8..a2132726 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -1,5 +1,5 @@ --- -description: Rex7 network upgrade — checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints, within-limit transactions stay bit-identical to Rex6, and limit-exceeding opcodes are stopped before they execute. +description: Rex7 network upgrade — checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints, within-limit transactions that never end a frame in an exceptional halt stay bit-identical to Rex6, and limit-exceeding opcodes are stopped before they execute. --- # Rex7 Network Upgrade @@ -21,7 +21,7 @@ Rex7 replaces that per-opcode recording for ordinary opcodes with **checkpoint s Rex7 also introduces **gas-clamp enforcement**: between checkpoints the node restricts the interpreter-visible remaining gas to the remaining compute headroom, so the inherited EVM's own per-opcode gas check stops a limit-crossing opcode before that opcode executes. -For a transaction that never crosses a compute-gas, detention, or other resource limit, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. +For a transaction that never crosses a compute-gas, detention, or other resource limit and in which no frame ends in an exceptional halt, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. For a transaction that does cross a compute-gas or detention limit inside a plain-opcode segment, the halt lands before the crossing opcode rather than after it, the crossing opcode's cost is excluded from recorded compute usage, and remaining gas remains refundable under the same rescue rules as other transaction-level compute-limit halts. One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. @@ -67,8 +67,8 @@ At each checkpoint a node MUST: Non-opcode recording sites (transaction intrinsic gas, precompiles, contract-creation code deposit, KeylessDeploy overhead and sandbox merge) are unchanged. **Precision invariant.** -For every transaction that stays within every runtime resource limit, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. -The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed. +For every transaction that stays within every runtime resource limit and in which no frame ends in an exceptional halt, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. +The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed and no frame ends in an exceptional halt. **Exceptional-halt frame carve-out.** A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. @@ -154,7 +154,7 @@ Its semantics may still change before it is frozen. Contracts and tools that assume per-opcode compute-gas attribution for every instruction MUST treat that assumption as false under Rex7: only checkpoints settle compute gas during execution, and a plain-opcode segment has no intermediate recording. -Contracts that stay within every resource limit see no behavioral change relative to Rex6. +Contracts that stay within every resource limit and never end a frame in an exceptional halt see no behavioral change relative to Rex6. Contracts that trip the compute-gas or detention limit inside a plain-opcode segment halt one opcode earlier than under Rex6, with the crossing opcode excluded from recorded compute usage and with remaining gas still refundable on a transaction-level halt. A transaction that halts exceptionally, or that calls into a child frame which does, may report a higher transaction-level compute-gas total under Rex7 than under Rex6 — for any exceptional halt, not just out-of-gas. From d35d293774e1e267b762d160ecbf29c74e962c67 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 13 Aug 2026 11:14:21 +0800 Subject: [PATCH 038/208] perf(evm): monomorphize checkpoint gating off the frozen-spec hot path --- crates/mega-evm/src/evm/execution.rs | 4 +- crates/mega-evm/src/evm/instructions.rs | 272 ++++++++++++++++-------- 2 files changed, 181 insertions(+), 95 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 53c4718f..e6e4b5ee 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -459,7 +459,9 @@ impl MegaEvm { // REX7: hand any clamp-hidden gas back to the result and latch a clamp-induced // out-of-gas as the compute exceed it stands for, before the code-deposit charge below // observes the result's gas. - ctx.additional_limit.borrow_mut().settle_frame_final_result(interpreter_result); + if ctx.spec.is_enabled(MegaSpecId::REX7) { + ctx.additional_limit.borrow_mut().settle_frame_final_result(interpreter_result); + } // Charge storage gas cost for the number of bytes if frame.data.is_create() && interpreter_result.is_ok() { diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 7a566ada..c25d23f8 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -313,9 +313,10 @@ mod rex { let mut table = mini_rex::instruction_table::(); // Mini-Rex mistakenly not modifying these three call-like opcodes. They are fixed in Rex - table[CALLCODE as usize] = Instruction::new(forward_gas_ext::call_code); - table[DELEGATECALL as usize] = Instruction::new(forward_gas_ext::delegate_call); - table[STATICCALL as usize] = Instruction::new(forward_gas_ext::static_call); + table[CALLCODE as usize] = Instruction::new(forward_gas_ext::call_code::); + table[DELEGATECALL as usize] = + Instruction::new(forward_gas_ext::delegate_call::); + table[STATICCALL as usize] = Instruction::new(forward_gas_ext::static_call::); table } @@ -423,10 +424,12 @@ mod rex4 { let mut table = rex3::instruction_table::(); // Rex4: CALL-like opcodes check for beneficiary volatile access disabled. - table[CALL as usize] = Instruction::new(volatile_data_ext::call); - table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); - table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); - table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); + table[CALL as usize] = Instruction::new(volatile_data_ext::call::); + table[STATICCALL as usize] = + Instruction::new(volatile_data_ext::static_call::); + table[DELEGATECALL as usize] = + Instruction::new(volatile_data_ext::delegate_call::); + table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code::); // Rex4: SELFDESTRUCT checks for beneficiary volatile access. table[SELFDESTRUCT as usize] = Instruction::new(volatile_data_ext::selfdestruct); @@ -480,7 +483,7 @@ mod rex5 { // REX5: SELFDESTRUCT charges storage gas for new beneficiary accounts, // gated behind the beneficiary-volatile guard. table[SELFDESTRUCT as usize] = - Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); + Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard::); table } @@ -660,20 +663,21 @@ mod rex7 { // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains unchanged. Under // Rex7 they open with a checkpoint prologue and close with an epilogue. - table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); - table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); - table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); - table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); - table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); - table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); - table[CREATE as usize] = Instruction::new(forward_gas_ext::create); - table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); - table[CALL as usize] = Instruction::new(volatile_data_ext::call); - table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); - table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); - table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); + table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore::); + table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, true, _, _>); + table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, true, _, _>); + table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, true, _, _>); + table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, true, _, _>); + table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, true, _, _>); + table[CREATE as usize] = Instruction::new(forward_gas_ext::create::); + table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2::); + table[CALL as usize] = Instruction::new(volatile_data_ext::call::); + table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code::); + table[DELEGATECALL as usize] = + Instruction::new(volatile_data_ext::delegate_call::); + table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call::); table[SELFDESTRUCT as usize] = - Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); + Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard::); table } @@ -839,10 +843,12 @@ macro_rules! run_inner_instruction_or_abort { /// /// Halts — returning from the enclosing handler — when the settlement surfaces a limit exceed, /// including one latched earlier by a non-compute mutation site. The restore has already happened -/// on that path, so the frame result carries true gas. No-op before REX7. +/// on that path, so the frame result carries true gas. No-op when `$cp` is false: frozen-spec +/// tables instantiate the shared handlers with `CHECKPOINT = false` so the compiler drops this +/// body entirely. macro_rules! checkpoint_prologue { - ($context:expr) => { - if $context.host.spec_id().is_enabled(MegaSpecId::REX7) { + ($context:expr, $cp:expr) => { + if $cp { let exceeding_result = { let mut additional_limit = $context.host.additional_limit().borrow_mut(); let remaining = $context.interpreter.gas.remaining(); @@ -871,12 +877,11 @@ macro_rules! checkpoint_prologue { /// Only applies when the frame keeps executing. A checkpoint that published an action has either /// suspended into a child frame (the resume clamps in `AdditionalLimit::before_frame_run`) or ended /// the frame (the frame's final result restores instead), and clamping either would strand hidden -/// gas across the boundary. No-op before REX7. +/// gas across the boundary. No-op when `$cp` is false (the `action().is_none()` check is also +/// dropped); frozen-spec tables instantiate the shared handlers with `CHECKPOINT = false`. macro_rules! checkpoint_epilogue { - ($context:expr) => { - if $context.host.spec_id().is_enabled(MegaSpecId::REX7) && - $context.interpreter.bytecode.action().is_none() - { + ($context:expr, $cp:expr) => { + if $cp && $context.interpreter.bytecode.action().is_none() { let mut additional_limit = $context.host.additional_limit().borrow_mut(); let hide = additional_limit.checkpoint_clamp_amount($context.interpreter.gas.remaining()); @@ -944,13 +949,15 @@ macro_rules! record_checkpoint_body_compute_gas { /// afterwards, so this is invisible there. /// /// Returns `Err(OutOfGas)` from the enclosing handler when the frame cannot afford the charge, -/// exactly as a bare `gas!` would — with nothing debited and so nothing to exclude. No-op before -/// REX7, where nothing measures against a segment. +/// exactly as a bare `gas!` would — with nothing debited and so nothing to exclude. When `$cp` is +/// false the exclude is dropped: nothing on a frozen spec measures against a segment. macro_rules! charge_storage_gas { - ($context:expr, $amount:expr) => {{ + ($context:expr, $amount:expr, $cp:expr) => {{ let amount: u64 = $amount; gas!($context.interpreter, amount); - $context.host.additional_limit().borrow_mut().exclude_storage_gas_from_segment(amount); + if $cp { + $context.host.additional_limit().borrow_mut().exclude_storage_gas_from_segment(amount); + } amount }}; } @@ -986,10 +993,9 @@ macro_rules! charge_storage_gas { /// reached on the non-halt path; without the return, a halt here would let a later `compute_gas!` /// add gas to the tracker after the OOG was already set. macro_rules! record_storage_compute_gas { - ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr) => {{ + ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr, $cp:expr) => {{ let spec = $context.host.spec_id(); let is_rex6 = spec.is_enabled(MegaSpecId::REX6); - let is_checkpoint_accounting = spec.is_enabled(MegaSpecId::REX7); let gas_after = $context.interpreter.gas.remaining(); // The per-opcode `$gas_before` window applies on every spec: under checkpoint accounting // the plain segment ahead of this opcode was already settled by @@ -1001,7 +1007,7 @@ macro_rules! record_storage_compute_gas { // before dispatch, or an outer volatile wrapper — does so ahead of the prologue, so under // checkpoint accounting it is already inside the settled segment and adding it back here // would bill it twice. - let mut gas_used = if is_checkpoint_accounting { + let mut gas_used = if $cp { $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged) } else { (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) @@ -1040,7 +1046,7 @@ macro_rules! record_storage_compute_gas { let mut additional_limit = $context.host.additional_limit().borrow_mut(); // Re-open the settlement window at this opcode's exit before recording, so neither a // halt here nor the frame-final settlement can bill this segment twice. - if is_checkpoint_accounting { + if $cp { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { @@ -1187,7 +1193,7 @@ mod mini_rex { table[MSTORE as usize] = Instruction::new(compute_gas_ext::mstore); table[MSTORE8 as usize] = Instruction::new(compute_gas_ext::mstore8); table[SLOAD as usize] = Instruction::new(compute_gas_ext::sload); - table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); + table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore::); table[JUMP as usize] = Instruction::new(compute_gas_ext::jump); table[JUMPI as usize] = Instruction::new(compute_gas_ext::jumpi); table[PC as usize] = Instruction::new(compute_gas_ext::pc); @@ -1266,15 +1272,15 @@ mod mini_rex { table[SWAP15 as usize] = Instruction::new(compute_gas_ext::swap15); table[SWAP16 as usize] = Instruction::new(compute_gas_ext::swap16); - table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); - table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); - table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); - table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); - table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); + table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, false, _, _>); + table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, false, _, _>); + table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, false, _, _>); + table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, false, _, _>); + table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, false, _, _>); - table[CREATE as usize] = Instruction::new(forward_gas_ext::create); - table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); - table[CALL as usize] = Instruction::new(forward_gas_ext::call); + table[CREATE as usize] = Instruction::new(forward_gas_ext::create::); + table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2::); + table[CALL as usize] = Instruction::new(forward_gas_ext::call::); table[CALLCODE as usize] = Instruction::new(compute_gas_ext::call_code); table[DELEGATECALL as usize] = Instruction::new(compute_gas_ext::delegate_call); table[STATICCALL as usize] = Instruction::new(compute_gas_ext::static_call); @@ -1359,6 +1365,9 @@ pub mod forward_gas_ext { /// is used by `CREATE` / `CREATE2`, whose table entries dispatch straight here; the CALL family /// is wrapped once more by `volatile_data_ext::wrap_call_volatile_check`, which owns the /// epilogue so that it lands after the detention cap that wrapper installs. + /// + /// Generated handlers are const-generic over `CHECKPOINT`. Frozen tables instantiate `false` + /// so the epilogue body is compiled out; the REX7 table instantiates `true`. macro_rules! wrap_gas_cap { ($fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr) => { wrap_gas_cap!(@inner $fn_name, $opcode_name, $wrapped_fn, $has_transfer_logic, false); @@ -1370,6 +1379,7 @@ pub mod forward_gas_ext { #[doc = concat!("`", $opcode_name, "` opcode with 98/100 gas forwarding rule.")] #[inline] pub fn $fn_name< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -1451,7 +1461,7 @@ pub mod forward_gas_ext { _ => {} } if $checkpoint_tail { - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, CHECKPOINT); } inner_outcome } @@ -1481,15 +1491,41 @@ pub mod forward_gas_ext { false } - wrap_gas_cap!(call, "CALL", storage_gas_ext::call, check_call_has_transfer); - wrap_gas_cap!(call_code, "CALLCODE", storage_gas_ext::call_code, check_call_has_transfer); - wrap_gas_cap!(delegate_call, "DELEGATECALL", storage_gas_ext::delegate_call, no_transfer); - wrap_gas_cap!(static_call, "STATICCALL", storage_gas_ext::static_call, no_transfer); wrap_gas_cap!( - @checkpoint_tail create, "CREATE", storage_gas_ext::create::, no_transfer + call, + "CALL", + storage_gas_ext::call::, + check_call_has_transfer + ); + wrap_gas_cap!( + call_code, + "CALLCODE", + storage_gas_ext::call_code::, + check_call_has_transfer + ); + wrap_gas_cap!( + delegate_call, + "DELEGATECALL", + storage_gas_ext::delegate_call::, + no_transfer + ); + wrap_gas_cap!( + static_call, + "STATICCALL", + storage_gas_ext::static_call::, + no_transfer + ); + wrap_gas_cap!( + @checkpoint_tail create, + "CREATE", + storage_gas_ext::create::, + no_transfer ); wrap_gas_cap!( - @checkpoint_tail create2, "CREATE2", storage_gas_ext::create::, no_transfer + @checkpoint_tail create2, + "CREATE2", + storage_gas_ext::create::, + no_transfer ); } @@ -1825,6 +1861,7 @@ pub mod volatile_data_ext { /// SELFDESTRUCT-specific hook. #[inline] pub fn selfdestruct_with_beneficiary_guard< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -1861,7 +1898,7 @@ pub mod volatile_data_ext { } run_inner_instruction_or_abort!( - super::storage_gas_ext::selfdestruct, + super::storage_gas_ext::selfdestruct::, context, inner_outcome ); @@ -1968,6 +2005,7 @@ pub mod volatile_data_ext { #[doc = concat!("`", stringify!($opcode), "` opcode with volatile data access disabled check for beneficiary.")] #[inline] pub fn $fn_name< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2057,7 +2095,7 @@ pub mod volatile_data_ext { // or depth rejection pushes 0 and lets the frame keep running). The epilogue is what // keeps the following plain segment bounded, and it sits after the cap above so a CALL // that just marked beneficiary access clamps against the detained headroom. - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, CHECKPOINT); inner_outcome } }; @@ -2065,10 +2103,22 @@ pub mod volatile_data_ext { // Conditionally volatile CALL-like opcodes — volatile only when targeting the block // beneficiary. These wrap forward_gas_ext handlers with a pre-execution beneficiary check. - wrap_call_volatile_check!(call, CALL, forward_gas_ext::call); - wrap_call_volatile_check!(static_call, STATICCALL, forward_gas_ext::static_call); - wrap_call_volatile_check!(delegate_call, DELEGATECALL, forward_gas_ext::delegate_call); - wrap_call_volatile_check!(call_code, CALLCODE, forward_gas_ext::call_code); + wrap_call_volatile_check!(call, CALL, forward_gas_ext::call::); + wrap_call_volatile_check!( + static_call, + STATICCALL, + forward_gas_ext::static_call:: + ); + wrap_call_volatile_check!( + delegate_call, + DELEGATECALL, + forward_gas_ext::delegate_call:: + ); + wrap_call_volatile_check!( + call_code, + CALLCODE, + forward_gas_ext::call_code:: + ); /* Checkpoint variants of the volatile handlers (REX7+). @@ -2099,14 +2149,14 @@ pub mod volatile_data_ext { if context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, $opcode, $access_type); } - checkpoint_prologue!(context); + checkpoint_prologue!(context, true); let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, true); inner_outcome } }; @@ -2134,14 +2184,14 @@ pub mod volatile_data_ext { ); } } - checkpoint_prologue!(context); + checkpoint_prologue!(context, true); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); charge_static_gas!(context, $opcode); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, true); inner_outcome } }; @@ -2233,14 +2283,14 @@ pub mod volatile_data_ext { if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, SLOAD, VolatileDataAccessType::Oracle); } - checkpoint_prologue!(context); + checkpoint_prologue!(context, true); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!(instructions::host::sload, context, inner_outcome); charge_static_gas!(context, SLOAD); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, true); inner_outcome } @@ -2259,14 +2309,14 @@ pub mod volatile_data_ext { VolatileDataAccessType::Beneficiary ); } - checkpoint_prologue!(context); + checkpoint_prologue!(context, true); let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, true); inner_outcome } } @@ -2294,6 +2344,7 @@ pub mod additional_limit_ext { /// /// Refunds data/KV when slot reset to original value. pub fn sstore< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2315,7 +2366,11 @@ pub mod additional_limit_ext { let loaded_data = SStoreResult { original_value, present_value, new_value }; // Execute the original SSTORE instruction - run_inner_instruction_or_abort!(storage_gas_ext::sstore, context, inner_outcome); + run_inner_instruction_or_abort!( + storage_gas_ext::sstore::, + context, + inner_outcome + ); // KV update bomb and data bomb (only when first writing non-zero value to originally zero // slot): check if the number of key-value updates or the total data size will exceed the @@ -2329,7 +2384,7 @@ pub mod additional_limit_ext { } drop(additional_limit); // REX7: re-clamp once every dimension this opcode touches has been recorded. - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, CHECKPOINT); inner_outcome } @@ -2344,6 +2399,7 @@ pub mod additional_limit_ext { /// MB). Halts when data limit exceeded. pub fn log< const N: usize, + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2356,7 +2412,11 @@ pub mod additional_limit_ext { let len = as_usize_or_fail!(context.interpreter, len); // Execute the original LOG instruction - run_inner_instruction_or_abort!(storage_gas_ext::log::, context, inner_outcome); + run_inner_instruction_or_abort!( + storage_gas_ext::log::, + context, + inner_outcome + ); // Record the size of the log topics and data. If the total data size exceeds the limit, we // halt. @@ -2369,7 +2429,7 @@ pub mod additional_limit_ext { } drop(additional_limit); // REX7: re-clamp once every dimension this opcode touches has been recorded. - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, CHECKPOINT); inner_outcome } } @@ -2455,6 +2515,7 @@ pub mod storage_gas_ext { ($fn_name:ident, $opcode:ident, $raw_fn:path, $has_transfer_logic:expr, $select_addr:path) => { #[doc = concat!("`", stringify!($opcode), "` opcode implementation modified from `revm` with compute gas tracking and dynamically-scaled storage gas costs.")] pub fn $fn_name< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2462,7 +2523,7 @@ pub mod storage_gas_ext { ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation, // so the storage charge and the body's 63/64 forwarding math see the true counter. - checkpoint_prologue!(context); + checkpoint_prologue!(context, CHECKPOINT); // Captured at the very top so the single compute window covers all of the // opcode's compute work. let gas_before = context.interpreter.gas.remaining(); @@ -2505,7 +2566,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(new_account_storage_gas); - charge_storage_gas!(context, new_account_storage_gas - drained) + charge_storage_gas!(context, new_account_storage_gas - drained, CHECKPOINT) } else { 0 }; @@ -2519,7 +2580,8 @@ pub mod storage_gas_ext { context, gas_before, storage_charged, - opcode::$opcode + opcode::$opcode, + CHECKPOINT ); inner_outcome } @@ -2702,6 +2764,7 @@ pub mod storage_gas_ext { /// circuits to [`create_rex6`] at the top; the body below is the pre-REX6 path, which can /// assume all features up to and including `MINI_REX` are enabled. pub fn create< + const CHECKPOINT: bool, WIRE: InterpreterTypes, const IS_CREATE2: bool, H: HostExt + ContextTr + JournalInspectTr + ?Sized, @@ -2714,7 +2777,7 @@ pub mod storage_gas_ext { // compute-gas recording taken after the body completes (see `create_rex6`), instead of // the pre-REX6 split `resize_gas` recording handled below. if spec.is_enabled(MegaSpecId::REX6) { - return create_rex6::(context); + return create_rex6::(context); } // Inspect the creator and compute the created address. REX5+ records the CREATE2 @@ -2764,7 +2827,7 @@ pub mod storage_gas_ext { context, inner_outcome ); - record_storage_compute_gas!(context, gas_before, 0, create_opcode(IS_CREATE2)); + record_storage_compute_gas!(context, gas_before, 0, create_opcode(IS_CREATE2), CHECKPOINT); // Pre-REX5 late-record path for the CREATE2 initcode memory-expansion gas. // Preserved verbatim for replay parity: pre-REX5 keeps the original "skip on inner @@ -2795,6 +2858,7 @@ pub mod storage_gas_ext { /// REX6 implies REX5 (and REX), so the REX5 operand validation and the contract-creation /// storage-gas path are taken unconditionally here. fn create_rex6< + const CHECKPOINT: bool, WIRE: InterpreterTypes, const IS_CREATE2: bool, H: HostExt + ContextTr + JournalInspectTr + ?Sized, @@ -2817,7 +2881,7 @@ pub mod storage_gas_ext { // REX7: settle the open segment and restore the clamp before any gas observation, so the // memory expansion, the storage charge and the body's forwarding math see the true counter. - checkpoint_prologue!(context); + checkpoint_prologue!(context, CHECKPOINT); // Captured before any gas movement so the single compute window covers the wrapper-side // CREATE2 memory expansion as well as the inner opcode. @@ -2851,7 +2915,8 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(create_contract_storage_gas); - let storage_charged = charge_storage_gas!(context, create_contract_storage_gas - drained); + let storage_charged = + charge_storage_gas!(context, create_contract_storage_gas - drained, CHECKPOINT); // Run the raw inner create opcode (no `compute_gas_ext` wrapper — REX6 records compute gas // once below). @@ -2875,7 +2940,8 @@ pub mod storage_gas_ext { context, gas_before, storage_charged, - create_opcode(IS_CREATE2) + create_opcode(IS_CREATE2), + CHECKPOINT ); inner_outcome } @@ -2894,13 +2960,14 @@ pub mod storage_gas_ext { /// This alternative implementation of `LOG` is only used when the `MINI_REX` spec is enabled. pub fn log< const N: usize, + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ?Sized, >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation. - checkpoint_prologue!(context); + checkpoint_prologue!(context, CHECKPOINT); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); let Some(len) = context.interpreter.stack.inspect::<1>() else { @@ -2932,12 +2999,15 @@ pub mod storage_gas_ext { // The `gas_or_fail!` above is the storage-gas charge, so it gets the same segment // exclusion `charge_storage_gas!` applies at every other charge site: the raw opcode below // can halt (a static frame rejects `LOG` outright) before the recording that would - // otherwise subtract it. - context - .host - .additional_limit() - .borrow_mut() - .exclude_storage_gas_from_segment(storage_charged); + // otherwise subtract it. Frozen specs skip the exclude — nothing measures against a + // segment. + if CHECKPOINT { + context + .host + .additional_limit() + .borrow_mut() + .exclude_storage_gas_from_segment(storage_charged); + } // Run the raw opcode and record compute gas once after the body completes (canonical // metering order). Byte-equivalent to the pre-REX6 per-`N` `compute_gas_ext::logK` @@ -2945,7 +3015,13 @@ pub mod storage_gas_ext { // consumes EVM gas. The wrapper is only ever instantiated for `N` in `0..=4`, so the // generic `instructions::host::log::` covers every valid call site. run_inner_instruction_or_abort!(instructions::host::log::, context, inner_outcome); - record_storage_compute_gas!(context, gas_before, storage_charged, opcode::LOG0 + N as u8); + record_storage_compute_gas!( + context, + gas_before, + storage_charged, + opcode::LOG0 + N as u8, + CHECKPOINT + ); inner_outcome } @@ -2965,13 +3041,14 @@ pub mod storage_gas_ext { /// enabled, so we can safely assume that all features before and including Mini-Rex are /// enabled. pub fn sstore< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation. - checkpoint_prologue!(context); + checkpoint_prologue!(context, CHECKPOINT); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); // The address to the underlying execution contract state @@ -3006,7 +3083,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(sstore_set_storage_gas); - charge_storage_gas!(context, sstore_set_storage_gas - drained) + charge_storage_gas!(context, sstore_set_storage_gas - drained, CHECKPOINT) } else { 0 }; @@ -3016,7 +3093,13 @@ pub mod storage_gas_ext { // every spec because nothing between `gas_before` and the storage charge above consumes // EVM gas. run_inner_instruction_or_abort!(instructions::host::sstore, context, inner_outcome); - record_storage_compute_gas!(context, gas_before, storage_charged, opcode::SSTORE); + record_storage_compute_gas!( + context, + gas_before, + storage_charged, + opcode::SSTORE, + CHECKPOINT + ); inner_outcome } @@ -3041,6 +3124,7 @@ pub mod storage_gas_ext { /// sees — via the REX6-gated arm below; pre-REX6 records nothing for an existing target. The /// rest of the body, and all ≤REX5 behavior, is unchanged. pub fn selfdestruct< + const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -3049,7 +3133,7 @@ pub mod storage_gas_ext { // REX7: settle the open segment and restore the clamp before any gas observation — the // beneficiary-creation storage charge below and the inner opcode both run on the true // counter, which is what keeps the storage charge outside every compute window. - checkpoint_prologue!(context); + checkpoint_prologue!(context, CHECKPOINT); // Inside a static frame, revm's inner SELFDESTRUCT halts on the // static-context check without changing state. Skip the mega host work below @@ -3099,7 +3183,7 @@ pub mod storage_gas_ext { }; let drained = context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); - charge_storage_gas!(context, cost - drained); + charge_storage_gas!(context, cost - drained, CHECKPOINT); // Record resource usage for new beneficiary account context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); @@ -3480,11 +3564,11 @@ pub mod compute_gas_ext { pub fn gas_checkpoint( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { - checkpoint_prologue!(context); + checkpoint_prologue!(context, true); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!(instructions::system::gas, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before); - checkpoint_epilogue!(context); + checkpoint_epilogue!(context, true); inner_outcome } } From 12380c85b5c4b6d65f1283ae98a5dfbf325c7325 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 13 Aug 2026 11:46:34 +0800 Subject: [PATCH 039/208] fix(evm): rebuild spec-latched limit state on cfg spec migration with_cfg / with_cfg_unpinned now reconstruct AdditionalLimit from the new spec and the already-configured runtime limits so tracker latch bits stay aligned with MegaContext.spec. --- crates/mega-evm/src/evm/context.rs | 218 ++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index a81e6f33..5957551b 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -367,6 +367,10 @@ impl MegaContext { /// specification, it automatically applies appropriate contract size limits /// if they are not already set in the configuration. /// + /// A spec change rebuilds the additional-limit trackers from the new spec and the + /// already-configured runtime limits so spec-latched state stays aligned. An unchanged + /// spec leaves the existing tracker in place. + /// /// # `tx_chain_id_check` is pinned off /// /// revm 40 flipped the `CfgEnv::tx_chain_id_check` default from `false` to `true` — a gate @@ -417,8 +421,16 @@ impl MegaContext { /// [`with_cfg_unpinned`](Self::with_cfg_unpinned): both adopt the caller's configuration the /// same way, and differ only in whether `tx_chain_id_check` is pinned to the revm-27 `false` /// or taken as the caller provided it. + /// + /// A spec change rebuilds [`AdditionalLimit`] from the new spec and the already-configured + /// runtime limits, so spec-latched tracker state stays aligned with [`Self::spec`]. Limits + /// already set by [`with_tx_runtime_limits`](Self::with_tx_runtime_limits) are kept; they are + /// not replaced by the new spec's defaults. An unchanged spec leaves the existing tracker in + /// place. fn apply_cfg(mut self, cfg: CfgEnv, intent: CfgIntent) -> Self { - self.spec = cfg.spec; + let new_spec = cfg.spec; + let spec_changed = new_spec != self.spec; + self.spec = new_spec; self.inner = self.inner.with_cfg(cfg.into_op_cfg()); if intent == CfgIntent::Pinned { self.inner.cfg.tx_chain_id_check = false; @@ -433,6 +445,10 @@ impl MegaContext { Some(constants::mini_rex::MAX_INITCODE_SIZE); } } + if spec_changed { + let limits = self.additional_limit.borrow().limits; + self.additional_limit = Rc::new(RefCell::new(AdditionalLimit::new(self.spec, limits))); + } self } @@ -923,15 +939,15 @@ impl IntoMegaethCfgEnv for CfgEnv { mod tests { use super::*; - use alloy_primitives::address; + use alloy_primitives::{address, Address, Bytes, U256}; use revm::{ - context::CfgEnv, + context::{tx::TxEnvBuilder, CfgEnv}, context_interface::cfg::{GasId, GasParams}, database::EmptyDB, primitives::hardfork::SpecId, }; - use crate::TestExternalEnvs; + use crate::{test_utils::MemoryDatabase, MegaTransactionNew as _, TestExternalEnvs}; /// A gas schedule an embedder could install: the spec table with one entry moved off its /// mainnet value. Distinct from every `GasParams::new_spec(..)` table, so a conversion that @@ -1269,6 +1285,200 @@ mod tests { } } + /// Compute limit tight enough that a leftover REX7 V0 clamp is visible in receipt gas. + const CFG_MIGRATION_COMPUTE_LIMIT: u64 = 50_000; + /// Transaction gas limit used by the `PUSH0 STOP` migration probe. + const CFG_MIGRATION_TX_GAS_LIMIT: u64 = 1_000_000; + const CFG_MIGRATION_CALLER: Address = address!("0000000000000000000000000000000000300000"); + const CFG_MIGRATION_CONTRACT: Address = address!("0000000000000000000000000000000000300001"); + /// `PUSH0 STOP` — a compute-only body, so a leftover V0 clamp shows up as receipt gas. + const CFG_MIGRATION_CODE: [u8; 2] = [0x5f, 0x00]; + + #[derive(Debug, PartialEq, Eq)] + struct CfgMigrationOutcome { + success: bool, + gas_used: u64, + compute_gas: u64, + } + + fn cfg_migration_limits() -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) + .with_tx_compute_gas_limit(CFG_MIGRATION_COMPUTE_LIMIT) + } + + fn cfg_migration_db() -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CFG_MIGRATION_CALLER, U256::from(10).pow(U256::from(18))) + .account_code(CFG_MIGRATION_CONTRACT, Bytes::from_static(&CFG_MIGRATION_CODE)) + } + + fn run_cfg_migration_tx( + mut context: MegaContext, + ) -> CfgMigrationOutcome { + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + let tx = TxEnvBuilder::default() + .caller(CFG_MIGRATION_CALLER) + .call(CFG_MIGRATION_CONTRACT) + .gas_limit(CFG_MIGRATION_TX_GAS_LIMIT) + .build_fill(); + let mut tx = crate::MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + + let mut evm = crate::MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("cfg-migration probe must execute"); + let compute_gas = evm.ctx.additional_limit.borrow().get_usage().compute_gas; + CfgMigrationOutcome { + success: result.result.is_success(), + gas_used: result.result.tx_gas_used(), + compute_gas, + } + } + + /// Spec-latched tracker bits must follow `with_cfg`, using the already-configured limits. + #[test] + fn test_with_cfg_rebuilds_latched_limit_state_when_spec_changes() { + let limits = cfg_migration_limits(); + + let rex7_to_rex6 = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7) + .with_tx_runtime_limits(limits) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX6)); + let rex6_direct = + MegaContext::new(EmptyDB::default(), MegaSpecId::REX6).with_tx_runtime_limits(limits); + + assert_eq!(rex7_to_rex6.mega_spec(), MegaSpecId::REX6); + assert_eq!( + rex7_to_rex6.additional_limit.borrow().checkpoint_accounting(), + rex6_direct.additional_limit.borrow().checkpoint_accounting(), + ); + assert!( + !rex7_to_rex6.additional_limit.borrow().checkpoint_accounting(), + "REX6 must not latch checkpoint accounting" + ); + assert_eq!(rex7_to_rex6.additional_limit.borrow().limits, limits); + + let rex6_to_rex7 = MegaContext::new(EmptyDB::default(), MegaSpecId::REX6) + .with_tx_runtime_limits(limits) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX7)); + let rex7_direct = + MegaContext::new(EmptyDB::default(), MegaSpecId::REX7).with_tx_runtime_limits(limits); + + assert_eq!(rex6_to_rex7.mega_spec(), MegaSpecId::REX7); + assert_eq!( + rex6_to_rex7.additional_limit.borrow().checkpoint_accounting(), + rex7_direct.additional_limit.borrow().checkpoint_accounting(), + ); + assert!( + rex6_to_rex7.additional_limit.borrow().checkpoint_accounting(), + "REX7 must latch checkpoint accounting" + ); + assert_eq!(rex6_to_rex7.additional_limit.borrow().limits, limits); + + let via_unpinned = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7) + .with_tx_runtime_limits(limits) + .with_cfg_unpinned(CfgEnv::new_with_spec(MegaSpecId::REX6)); + assert!( + !via_unpinned.additional_limit.borrow().checkpoint_accounting(), + "with_cfg_unpinned must rebuild latched limit state on a spec change" + ); + assert_eq!(via_unpinned.additional_limit.borrow().limits, limits); + } + + /// Same-spec `with_cfg` must not replace the additional-limit `Rc`. + #[test] + fn test_with_cfg_same_spec_keeps_additional_limit_identity() { + let limits = cfg_migration_limits(); + let context = + MegaContext::new(EmptyDB::default(), MegaSpecId::REX6).with_tx_runtime_limits(limits); + let before = Rc::clone(&context.additional_limit); + + let context = context.with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX6)); + + assert!(Rc::ptr_eq(&before, &context.additional_limit)); + assert_eq!(context.additional_limit.borrow().limits, limits); + assert!(!context.additional_limit.borrow().checkpoint_accounting()); + } + + #[test] + fn test_with_cfg_rex7_to_rex6_matches_direct_rex6_when_limits_applied_first() { + let limits = cfg_migration_limits(); + + let mut migrated_db = cfg_migration_db(); + let migrated = run_cfg_migration_tx( + MegaContext::new(&mut migrated_db, MegaSpecId::REX7) + .with_tx_runtime_limits(limits) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX6)), + ); + + let mut direct_db = cfg_migration_db(); + let direct = run_cfg_migration_tx( + MegaContext::new(&mut direct_db, MegaSpecId::REX6).with_tx_runtime_limits(limits), + ); + + assert_eq!(migrated, direct); + } + + #[test] + fn test_with_cfg_rex6_to_rex7_matches_direct_rex7_when_limits_applied_first() { + let limits = cfg_migration_limits(); + + let mut migrated_db = cfg_migration_db(); + let migrated = run_cfg_migration_tx( + MegaContext::new(&mut migrated_db, MegaSpecId::REX6) + .with_tx_runtime_limits(limits) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX7)), + ); + + let mut direct_db = cfg_migration_db(); + let direct = run_cfg_migration_tx( + MegaContext::new(&mut direct_db, MegaSpecId::REX7).with_tx_runtime_limits(limits), + ); + + assert_eq!(migrated, direct); + } + + #[test] + fn test_with_cfg_rex7_to_rex6_matches_direct_rex6_when_limits_applied_after() { + let limits = cfg_migration_limits(); + + let mut migrated_db = cfg_migration_db(); + let migrated = run_cfg_migration_tx( + MegaContext::new(&mut migrated_db, MegaSpecId::REX7) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX6)) + .with_tx_runtime_limits(limits), + ); + + let mut direct_db = cfg_migration_db(); + let direct = run_cfg_migration_tx( + MegaContext::new(&mut direct_db, MegaSpecId::REX6).with_tx_runtime_limits(limits), + ); + + assert_eq!(migrated, direct); + } + + #[test] + fn test_with_cfg_rex6_to_rex7_matches_direct_rex7_when_limits_applied_after() { + let limits = cfg_migration_limits(); + + let mut migrated_db = cfg_migration_db(); + let migrated = run_cfg_migration_tx( + MegaContext::new(&mut migrated_db, MegaSpecId::REX6) + .with_cfg(CfgEnv::new_with_spec(MegaSpecId::REX7)) + .with_tx_runtime_limits(limits), + ); + + let mut direct_db = cfg_migration_db(); + let direct = run_cfg_migration_tx( + MegaContext::new(&mut direct_db, MegaSpecId::REX7).with_tx_runtime_limits(limits), + ); + + assert_eq!(migrated, direct); + } + /// Sharing SALT env handles between parent and sandbox must not merge their bucket caches. #[test] fn test_shared_salt_env_keeps_dynamic_gas_cache_isolated() { From fc0fab97dae85f1f64f1d1d0c81cc337667d7f76 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 13 Aug 2026 15:07:34 +0800 Subject: [PATCH 040/208] docs(evm): replace the V0 design codename with the spec term gas clamp --- AGENTS.md | 2 +- crates/mega-evm/src/evm/context.rs | 4 ++-- crates/mega-evm/src/evm/instructions.rs | 10 ++++----- crates/mega-evm/src/limit/compute_gas.rs | 4 ++-- crates/mega-evm/src/limit/limit.rs | 21 ++++++++++--------- .../tests/rex7/checkpoint_settlement.rs | 4 ++-- .../tests/rex7/{v0_clamp.rs => gas_clamp.rs} | 2 +- crates/mega-evm/tests/rex7/gas_leakage.rs | 2 +- crates/mega-evm/tests/rex7/main.rs | 6 +++--- 9 files changed, 28 insertions(+), 27 deletions(-) rename crates/mega-evm/tests/rex7/{v0_clamp.rs => gas_clamp.rs} (99%) diff --git a/AGENTS.md b/AGENTS.md index 88a83c4c..f09299a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi - **Compute gas**: Measures pure computational cost. Through REX6 every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. - REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a V0 gas clamp. + REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a gas clamp. A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 5957551b..df428977 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -1285,13 +1285,13 @@ mod tests { } } - /// Compute limit tight enough that a leftover REX7 V0 clamp is visible in receipt gas. + /// Compute limit tight enough that a leftover REX7 gas clamp is visible in receipt gas. const CFG_MIGRATION_COMPUTE_LIMIT: u64 = 50_000; /// Transaction gas limit used by the `PUSH0 STOP` migration probe. const CFG_MIGRATION_TX_GAS_LIMIT: u64 = 1_000_000; const CFG_MIGRATION_CALLER: Address = address!("0000000000000000000000000000000000300000"); const CFG_MIGRATION_CONTRACT: Address = address!("0000000000000000000000000000000000300001"); - /// `PUSH0 STOP` — a compute-only body, so a leftover V0 clamp shows up as receipt gas. + /// `PUSH0 STOP` — a compute-only body, so a leftover gas clamp shows up as receipt gas. const CFG_MIGRATION_CODE: [u8; 2] = [0x5f, 0x00]; #[derive(Debug, PartialEq, Eq)] diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index c25d23f8..ceeed327 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -171,8 +171,8 @@ use revm::{ /// the volatile opcodes, and frame entry / resume / exit. Per-transaction totals are unchanged /// for a transaction that stays inside every limit and never halts exceptionally; a frame that /// does halt exceptionally additionally reports the budget it destroyed, which is enforced -/// against nothing. Enforcement inside a plain segment is the V0 gas clamp, which stops the -/// crossing opcode before it executes; an exceed detected by a settlement instead surfaces at the +/// against nothing. Enforcement inside a plain segment is the gas clamp, which stops the crossing +/// opcode before it executes; an exceed detected by a settlement instead surfaces at the /// checkpoint that settled it rather than at the opcode that crossed the limit. /// - Volatile opcodes: `volatile_data_ext::*_checkpoint` (raw instruction + segment settlement + /// detention cap) in place of the `compute_gas_ext` delegation @@ -657,7 +657,7 @@ mod rex7 { table[SELFBALANCE as usize] = Instruction::new(volatile_data_ext::selfbalance_checkpoint); table[SLOAD as usize] = Instruction::new(volatile_data_ext::sload_checkpoint); - // V0 gas-clamp enforcement: `GAS` has to be a checkpoint so the clamp is restored before + // Gas-clamp enforcement: `GAS` has to be a checkpoint so the clamp is restored before // the counter is observed. table[GAS as usize] = Instruction::new(compute_gas_ext::gas_checkpoint); @@ -870,7 +870,7 @@ macro_rules! checkpoint_prologue { }; } -/// REX7 checkpoint epilogue: re-applies the V0 gas clamp from the freshly settled usage — including +/// REX7 checkpoint epilogue: re-applies the gas clamp from the freshly settled usage — including /// any detention cap the checkpoint just installed — and re-opens the settlement window on the /// clamped counter. /// @@ -3556,7 +3556,7 @@ pub mod compute_gas_ext { /// `GAS` as a REX7 checkpoint. /// - /// `GAS` has to be a checkpoint under V0 clamp enforcement even though it charges nothing but + /// `GAS` has to be a checkpoint under gas-clamp enforcement even though it charges nothing but /// its static gas: the prologue hands the clamp-hidden gas back before the raw instruction /// reads the counter, so the value pushed on the stack is the true remaining and the clamp /// stays invisible to any transaction that never exceeds a limit. diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 4dffb7c9..8d55fbee 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -6,7 +6,7 @@ use super::{ }; use crate::{JournalInspectTr, MegaSpecId}; -/// The constraint that bounds the V0 gas clamp for one plain-opcode segment. +/// The constraint that bounds the gas clamp for one plain-opcode segment. /// /// Captured when the clamp is applied, so a clamp-induced out-of-gas can be classified against the /// constraint that was in force at the time rather than against whatever the tracker looks like @@ -151,7 +151,7 @@ impl ComputeGasTracker { self.frame_tracker.tx_limit() } - /// Returns the constraint the V0 gas clamp must bind to at this point in the transaction. + /// Returns the constraint the gas clamp must bind to at this point in the transaction. /// /// The headroom is the tighter of the current frame's remaining compute budget (Rex4+) and /// the TX-level remaining under the effective (possibly detained) limit — the same pair diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index ea6233a2..5c17cd26 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -123,7 +123,7 @@ pub struct AdditionalLimit { /// checkpoint prologue and body recording. checkpoint_baseline: u64, - /// V0 gas-clamp enforcement (REX7+): the clamp in force for the plain-opcode segment the + /// Gas-clamp enforcement (REX7+): the clamp in force for the plain-opcode segment the /// current frame is inside, so that revm's own per-opcode gas checks enforce the compute /// headroom at no per-opcode cost. /// @@ -144,7 +144,7 @@ pub struct AdditionalLimit { clamp_latched_detained: bool, } -/// A V0 gas clamp in force for one plain-opcode segment (REX7+). +/// A gas clamp in force for one plain-opcode segment (REX7+). /// /// The clamp is a lifecycle, not an amount. It is recorded exactly while it **binds** — while the /// interpreter's true remaining gas was at or above the compute headroom when the segment opened — @@ -288,7 +288,7 @@ impl AdditionalLimit { self.clamp.take().map_or(0, |clamp| clamp.hidden) } - /// Applies the V0 gas clamp for the segment that starts at `remaining`, and returns the amount + /// Applies the gas clamp for the segment that starts at `remaining`, and returns the amount /// the caller must debit from the interpreter's counter. /// /// The clamp is recorded — and the segment therefore enforces the compute limit — whenever the @@ -341,7 +341,7 @@ impl AdditionalLimit { self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); } - /// Finalises what the frame's own result decides about the clamp: restores any outstanding V0 + /// Finalises what the frame's own result decides about the clamp: restores any outstanding /// clamp into the result's gas and latches a clamp-induced out-of-gas as the compute exceed it /// stands for. /// @@ -355,9 +355,10 @@ impl AdditionalLimit { /// every checkpoint prologue takes it before its body. An out-of-gas exit from such a segment /// is a clamp artifact: the true counter held `hidden` more gas than the interpreter could see, /// and the crossing opcode was stopped at the clamp boundary *before executing* — exactly the - /// V0 enforcement point. When the crossing opcode would have exceeded the true remaining as - /// well, the compute classification still wins: the two are indistinguishable here, and - /// attributing the halt to the resource limit keeps the sender's remaining gas refundable. + /// gas-clamp enforcement point. When the crossing opcode would have exceeded the true + /// remaining as well, the compute classification still wins: the two are indistinguishable + /// here, and attributing the halt to the resource limit keeps the sender's remaining gas + /// refundable. pub(crate) fn settle_frame_final_result(&mut self, result: &mut InterpreterResult) { if !self.checkpoint_accounting { return; @@ -526,7 +527,7 @@ impl AdditionalLimit { access_type: VolatileDataAccess, ) -> Option { // `is_detained_exceed` covers per-opcode enforcement, where usage crossed the detained - // limit. `clamp_latched_detained` covers V0 clamp enforcement, where the crossing opcode + // limit. `clamp_latched_detained` covers gas-clamp enforcement, where the crossing opcode // was stopped before executing and usage therefore stays at or below the limit. (self.compute_gas.is_detained_exceed() || self.clamp_latched_detained).then(|| { MegaHaltReason::VolatileDataAccessOutOfGas { @@ -923,7 +924,7 @@ impl AdditionalLimit { )); } - // Checkpoint accounting: apply the V0 gas clamp and open the settlement window at the + // Checkpoint accounting: apply the gas clamp and open the settlement window at the // frame's clamped gas. This hook runs both at frame entry and at every resume after a child // frame's outcome — including the gas it returned — has been merged back into this frame's // interpreter, so the window always starts at an instruction boundary with the @@ -1137,7 +1138,7 @@ impl AdditionalLimit { /// the frame-exit delta cannot see that destroyed budget on any other classification. The /// result's own gas can: by the time this runs, /// [`settle_frame_final_result`](Self::settle_frame_final_result) has handed back whatever the - /// V0 clamp was hiding and the code-deposit storage charge has been taken, so + /// clamp was hiding and the code-deposit storage charge has been taken, so /// `result.gas().remaining()` is exactly what the frame still held and will not get to keep. /// /// Runs **after** action processing, which is the first point the classification is final: diff --git a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs index 2dde394f..e68714df 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs @@ -13,7 +13,7 @@ //! The two places where the models are *not* identical are pinned at the bottom of this file: //! a limit crossing inside a plain-opcode segment halts *before* the crossing opcode rather than //! after it, and a frame that halts out of gas settles its burned remainder as compute gas. The -//! enforcement mechanism behind the first — the V0 gas clamp — has its own suite in `v0_clamp`. +//! enforcement mechanism behind the first — the gas clamp — has its own suite in `gas_clamp`. use crate::common::{ transact, transact_default, transact_with_bucket_capacity, Outcome, CALLEE, CALLER, CONTRACT, @@ -470,7 +470,7 @@ fn plain_run_then_sstore_code(pairs: usize, include_sstore: bool) -> Bytes { /// The one enforcement difference this model has: a compute-gas crossing inside a plain-opcode /// segment is not caught *at* the crossing opcode — nothing is metered there — but *before* it, by -/// the V0 gas clamp, which leaves the interpreter only as much visible gas as the compute headroom +/// the gas clamp, which leaves the interpreter only as much visible gas as the compute headroom /// allows. Both specs halt, and both halt in the middle of the plain run without ever reaching the /// SSTORE checkpoint downstream; REX6 executes the crossing opcode and records it, so its usage /// ends up over the limit, while REX7 stops one opcode earlier and its usage stays at the limit. diff --git a/crates/mega-evm/tests/rex7/v0_clamp.rs b/crates/mega-evm/tests/rex7/gas_clamp.rs similarity index 99% rename from crates/mega-evm/tests/rex7/v0_clamp.rs rename to crates/mega-evm/tests/rex7/gas_clamp.rs index 2e8c2343..8b4a6112 100644 --- a/crates/mega-evm/tests/rex7/v0_clamp.rs +++ b/crates/mega-evm/tests/rex7/gas_clamp.rs @@ -1,4 +1,4 @@ -//! REX7 V0 gas-clamp enforcement. +//! REX7 gas-clamp enforcement. //! //! Plain opcodes under checkpoint accounting record nothing, so nothing checks a limit while a //! plain segment runs. Enforcement instead comes from the interpreter itself: at every checkpoint diff --git a/crates/mega-evm/tests/rex7/gas_leakage.rs b/crates/mega-evm/tests/rex7/gas_leakage.rs index 7cb35001..6a46d902 100644 --- a/crates/mega-evm/tests/rex7/gas_leakage.rs +++ b/crates/mega-evm/tests/rex7/gas_leakage.rs @@ -1,7 +1,7 @@ //! REX7: the three gas-leakage paths, exercised with a clamp outstanding. //! //! Any mechanism that hides, grants or adjusts gas per frame has to be unwound on every way out of -//! a frame, or system-held gas leaks back to the parent or the sender. The V0 clamp is such a +//! a frame, or system-held gas leaks back to the parent or the sender. The gas clamp is such a //! mechanism — it hides part of the interpreter's gas — and the three paths that have to handle it //! are the ones the leakage checklist names: //! diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index dee25dd0..4219947b 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -2,8 +2,8 @@ //! //! - `checkpoint_settlement` — checkpoint compute-gas settlement: per-transaction totals stay //! bit-identical to per-opcode recording, and the two places where the models diverge. -//! - `v0_clamp` — V0 gas-clamp enforcement: a crossing opcode is stopped before it executes, and -//! the resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. +//! - `gas_clamp` — gas-clamp enforcement: a crossing opcode is stopped before it executes, and the +//! resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. //! - `clamp_classification` — which constraint a clamp binds to, including the exact-value case, //! and the ABI payload / halt fields a clamp-induced exceed reports. //! - `checkpoint_families` — one parity case per checkpoint opcode the REX7 table wires, so the set @@ -32,9 +32,9 @@ mod clamp_classification; mod common; mod double_exceed_corner; mod exceptional_halt; +mod gas_clamp; mod gas_leakage; mod interceptor_resume; mod latch_surfacing; mod modexp_gas; mod parity_shapes; -mod v0_clamp; From f849383f0b135b27e577d47111adec58dc65de42 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 13 Aug 2026 15:27:13 +0800 Subject: [PATCH 041/208] refactor(evm): restore runtime checkpoint gating, matching upstream revm idiom Drop the T12 const CHECKPOINT monomorphization and return the REX7 checkpoint macros and handlers to a runtime spec.is_enabled(REX7) gate. Frame-exit settlement is unconditional again; AdditionalLimit's checkpoint_accounting flag is the single source of truth. Gas-clamp wording from the later rename is kept. --- crates/mega-evm/src/evm/execution.rs | 4 +- crates/mega-evm/src/evm/instructions.rs | 272 ++++++++---------------- 2 files changed, 95 insertions(+), 181 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index e6e4b5ee..53c4718f 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -459,9 +459,7 @@ impl MegaEvm { // REX7: hand any clamp-hidden gas back to the result and latch a clamp-induced // out-of-gas as the compute exceed it stands for, before the code-deposit charge below // observes the result's gas. - if ctx.spec.is_enabled(MegaSpecId::REX7) { - ctx.additional_limit.borrow_mut().settle_frame_final_result(interpreter_result); - } + ctx.additional_limit.borrow_mut().settle_frame_final_result(interpreter_result); // Charge storage gas cost for the number of bytes if frame.data.is_create() && interpreter_result.is_ok() { diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index ceeed327..3bdcc130 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -313,10 +313,9 @@ mod rex { let mut table = mini_rex::instruction_table::(); // Mini-Rex mistakenly not modifying these three call-like opcodes. They are fixed in Rex - table[CALLCODE as usize] = Instruction::new(forward_gas_ext::call_code::); - table[DELEGATECALL as usize] = - Instruction::new(forward_gas_ext::delegate_call::); - table[STATICCALL as usize] = Instruction::new(forward_gas_ext::static_call::); + table[CALLCODE as usize] = Instruction::new(forward_gas_ext::call_code); + table[DELEGATECALL as usize] = Instruction::new(forward_gas_ext::delegate_call); + table[STATICCALL as usize] = Instruction::new(forward_gas_ext::static_call); table } @@ -424,12 +423,10 @@ mod rex4 { let mut table = rex3::instruction_table::(); // Rex4: CALL-like opcodes check for beneficiary volatile access disabled. - table[CALL as usize] = Instruction::new(volatile_data_ext::call::); - table[STATICCALL as usize] = - Instruction::new(volatile_data_ext::static_call::); - table[DELEGATECALL as usize] = - Instruction::new(volatile_data_ext::delegate_call::); - table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code::); + table[CALL as usize] = Instruction::new(volatile_data_ext::call); + table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); + table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); + table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); // Rex4: SELFDESTRUCT checks for beneficiary volatile access. table[SELFDESTRUCT as usize] = Instruction::new(volatile_data_ext::selfdestruct); @@ -483,7 +480,7 @@ mod rex5 { // REX5: SELFDESTRUCT charges storage gas for new beneficiary accounts, // gated behind the beneficiary-volatile guard. table[SELFDESTRUCT as usize] = - Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard::); + Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); table } @@ -663,21 +660,20 @@ mod rex7 { // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains unchanged. Under // Rex7 they open with a checkpoint prologue and close with an epilogue. - table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore::); - table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, true, _, _>); - table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, true, _, _>); - table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, true, _, _>); - table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, true, _, _>); - table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, true, _, _>); - table[CREATE as usize] = Instruction::new(forward_gas_ext::create::); - table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2::); - table[CALL as usize] = Instruction::new(volatile_data_ext::call::); - table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code::); - table[DELEGATECALL as usize] = - Instruction::new(volatile_data_ext::delegate_call::); - table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call::); + table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); + table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); + table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); + table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); + table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); + table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); + table[CREATE as usize] = Instruction::new(forward_gas_ext::create); + table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); + table[CALL as usize] = Instruction::new(volatile_data_ext::call); + table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); + table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); + table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); table[SELFDESTRUCT as usize] = - Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard::); + Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); table } @@ -843,12 +839,10 @@ macro_rules! run_inner_instruction_or_abort { /// /// Halts — returning from the enclosing handler — when the settlement surfaces a limit exceed, /// including one latched earlier by a non-compute mutation site. The restore has already happened -/// on that path, so the frame result carries true gas. No-op when `$cp` is false: frozen-spec -/// tables instantiate the shared handlers with `CHECKPOINT = false` so the compiler drops this -/// body entirely. +/// on that path, so the frame result carries true gas. No-op before REX7. macro_rules! checkpoint_prologue { - ($context:expr, $cp:expr) => { - if $cp { + ($context:expr) => { + if $context.host.spec_id().is_enabled(MegaSpecId::REX7) { let exceeding_result = { let mut additional_limit = $context.host.additional_limit().borrow_mut(); let remaining = $context.interpreter.gas.remaining(); @@ -877,11 +871,12 @@ macro_rules! checkpoint_prologue { /// Only applies when the frame keeps executing. A checkpoint that published an action has either /// suspended into a child frame (the resume clamps in `AdditionalLimit::before_frame_run`) or ended /// the frame (the frame's final result restores instead), and clamping either would strand hidden -/// gas across the boundary. No-op when `$cp` is false (the `action().is_none()` check is also -/// dropped); frozen-spec tables instantiate the shared handlers with `CHECKPOINT = false`. +/// gas across the boundary. No-op before REX7. macro_rules! checkpoint_epilogue { - ($context:expr, $cp:expr) => { - if $cp && $context.interpreter.bytecode.action().is_none() { + ($context:expr) => { + if $context.host.spec_id().is_enabled(MegaSpecId::REX7) && + $context.interpreter.bytecode.action().is_none() + { let mut additional_limit = $context.host.additional_limit().borrow_mut(); let hide = additional_limit.checkpoint_clamp_amount($context.interpreter.gas.remaining()); @@ -949,15 +944,13 @@ macro_rules! record_checkpoint_body_compute_gas { /// afterwards, so this is invisible there. /// /// Returns `Err(OutOfGas)` from the enclosing handler when the frame cannot afford the charge, -/// exactly as a bare `gas!` would — with nothing debited and so nothing to exclude. When `$cp` is -/// false the exclude is dropped: nothing on a frozen spec measures against a segment. +/// exactly as a bare `gas!` would — with nothing debited and so nothing to exclude. No-op before +/// REX7, where nothing measures against a segment. macro_rules! charge_storage_gas { - ($context:expr, $amount:expr, $cp:expr) => {{ + ($context:expr, $amount:expr) => {{ let amount: u64 = $amount; gas!($context.interpreter, amount); - if $cp { - $context.host.additional_limit().borrow_mut().exclude_storage_gas_from_segment(amount); - } + $context.host.additional_limit().borrow_mut().exclude_storage_gas_from_segment(amount); amount }}; } @@ -993,9 +986,10 @@ macro_rules! charge_storage_gas { /// reached on the non-halt path; without the return, a halt here would let a later `compute_gas!` /// add gas to the tracker after the OOG was already set. macro_rules! record_storage_compute_gas { - ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr, $cp:expr) => {{ + ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr) => {{ let spec = $context.host.spec_id(); let is_rex6 = spec.is_enabled(MegaSpecId::REX6); + let is_checkpoint_accounting = spec.is_enabled(MegaSpecId::REX7); let gas_after = $context.interpreter.gas.remaining(); // The per-opcode `$gas_before` window applies on every spec: under checkpoint accounting // the plain segment ahead of this opcode was already settled by @@ -1007,7 +1001,7 @@ macro_rules! record_storage_compute_gas { // before dispatch, or an outer volatile wrapper — does so ahead of the prologue, so under // checkpoint accounting it is already inside the settled segment and adding it back here // would bill it twice. - let mut gas_used = if $cp { + let mut gas_used = if is_checkpoint_accounting { $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged) } else { (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) @@ -1046,7 +1040,7 @@ macro_rules! record_storage_compute_gas { let mut additional_limit = $context.host.additional_limit().borrow_mut(); // Re-open the settlement window at this opcode's exit before recording, so neither a // halt here nor the frame-final settlement can bill this segment twice. - if $cp { + if is_checkpoint_accounting { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { @@ -1193,7 +1187,7 @@ mod mini_rex { table[MSTORE as usize] = Instruction::new(compute_gas_ext::mstore); table[MSTORE8 as usize] = Instruction::new(compute_gas_ext::mstore8); table[SLOAD as usize] = Instruction::new(compute_gas_ext::sload); - table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore::); + table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); table[JUMP as usize] = Instruction::new(compute_gas_ext::jump); table[JUMPI as usize] = Instruction::new(compute_gas_ext::jumpi); table[PC as usize] = Instruction::new(compute_gas_ext::pc); @@ -1272,15 +1266,15 @@ mod mini_rex { table[SWAP15 as usize] = Instruction::new(compute_gas_ext::swap15); table[SWAP16 as usize] = Instruction::new(compute_gas_ext::swap16); - table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, false, _, _>); - table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, false, _, _>); - table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, false, _, _>); - table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, false, _, _>); - table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, false, _, _>); + table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); + table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); + table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); + table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); + table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); - table[CREATE as usize] = Instruction::new(forward_gas_ext::create::); - table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2::); - table[CALL as usize] = Instruction::new(forward_gas_ext::call::); + table[CREATE as usize] = Instruction::new(forward_gas_ext::create); + table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); + table[CALL as usize] = Instruction::new(forward_gas_ext::call); table[CALLCODE as usize] = Instruction::new(compute_gas_ext::call_code); table[DELEGATECALL as usize] = Instruction::new(compute_gas_ext::delegate_call); table[STATICCALL as usize] = Instruction::new(compute_gas_ext::static_call); @@ -1365,9 +1359,6 @@ pub mod forward_gas_ext { /// is used by `CREATE` / `CREATE2`, whose table entries dispatch straight here; the CALL family /// is wrapped once more by `volatile_data_ext::wrap_call_volatile_check`, which owns the /// epilogue so that it lands after the detention cap that wrapper installs. - /// - /// Generated handlers are const-generic over `CHECKPOINT`. Frozen tables instantiate `false` - /// so the epilogue body is compiled out; the REX7 table instantiates `true`. macro_rules! wrap_gas_cap { ($fn_name:ident, $opcode_name:expr, $wrapped_fn:path, $has_transfer_logic:expr) => { wrap_gas_cap!(@inner $fn_name, $opcode_name, $wrapped_fn, $has_transfer_logic, false); @@ -1379,7 +1370,6 @@ pub mod forward_gas_ext { #[doc = concat!("`", $opcode_name, "` opcode with 98/100 gas forwarding rule.")] #[inline] pub fn $fn_name< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -1461,7 +1451,7 @@ pub mod forward_gas_ext { _ => {} } if $checkpoint_tail { - checkpoint_epilogue!(context, CHECKPOINT); + checkpoint_epilogue!(context); } inner_outcome } @@ -1491,41 +1481,15 @@ pub mod forward_gas_ext { false } + wrap_gas_cap!(call, "CALL", storage_gas_ext::call, check_call_has_transfer); + wrap_gas_cap!(call_code, "CALLCODE", storage_gas_ext::call_code, check_call_has_transfer); + wrap_gas_cap!(delegate_call, "DELEGATECALL", storage_gas_ext::delegate_call, no_transfer); + wrap_gas_cap!(static_call, "STATICCALL", storage_gas_ext::static_call, no_transfer); wrap_gas_cap!( - call, - "CALL", - storage_gas_ext::call::, - check_call_has_transfer - ); - wrap_gas_cap!( - call_code, - "CALLCODE", - storage_gas_ext::call_code::, - check_call_has_transfer - ); - wrap_gas_cap!( - delegate_call, - "DELEGATECALL", - storage_gas_ext::delegate_call::, - no_transfer - ); - wrap_gas_cap!( - static_call, - "STATICCALL", - storage_gas_ext::static_call::, - no_transfer - ); - wrap_gas_cap!( - @checkpoint_tail create, - "CREATE", - storage_gas_ext::create::, - no_transfer + @checkpoint_tail create, "CREATE", storage_gas_ext::create::, no_transfer ); wrap_gas_cap!( - @checkpoint_tail create2, - "CREATE2", - storage_gas_ext::create::, - no_transfer + @checkpoint_tail create2, "CREATE2", storage_gas_ext::create::, no_transfer ); } @@ -1861,7 +1825,6 @@ pub mod volatile_data_ext { /// SELFDESTRUCT-specific hook. #[inline] pub fn selfdestruct_with_beneficiary_guard< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -1898,7 +1861,7 @@ pub mod volatile_data_ext { } run_inner_instruction_or_abort!( - super::storage_gas_ext::selfdestruct::, + super::storage_gas_ext::selfdestruct, context, inner_outcome ); @@ -2005,7 +1968,6 @@ pub mod volatile_data_ext { #[doc = concat!("`", stringify!($opcode), "` opcode with volatile data access disabled check for beneficiary.")] #[inline] pub fn $fn_name< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2095,7 +2057,7 @@ pub mod volatile_data_ext { // or depth rejection pushes 0 and lets the frame keep running). The epilogue is what // keeps the following plain segment bounded, and it sits after the cap above so a CALL // that just marked beneficiary access clamps against the detained headroom. - checkpoint_epilogue!(context, CHECKPOINT); + checkpoint_epilogue!(context); inner_outcome } }; @@ -2103,22 +2065,10 @@ pub mod volatile_data_ext { // Conditionally volatile CALL-like opcodes — volatile only when targeting the block // beneficiary. These wrap forward_gas_ext handlers with a pre-execution beneficiary check. - wrap_call_volatile_check!(call, CALL, forward_gas_ext::call::); - wrap_call_volatile_check!( - static_call, - STATICCALL, - forward_gas_ext::static_call:: - ); - wrap_call_volatile_check!( - delegate_call, - DELEGATECALL, - forward_gas_ext::delegate_call:: - ); - wrap_call_volatile_check!( - call_code, - CALLCODE, - forward_gas_ext::call_code:: - ); + wrap_call_volatile_check!(call, CALL, forward_gas_ext::call); + wrap_call_volatile_check!(static_call, STATICCALL, forward_gas_ext::static_call); + wrap_call_volatile_check!(delegate_call, DELEGATECALL, forward_gas_ext::delegate_call); + wrap_call_volatile_check!(call_code, CALLCODE, forward_gas_ext::call_code); /* Checkpoint variants of the volatile handlers (REX7+). @@ -2149,14 +2099,14 @@ pub mod volatile_data_ext { if context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, $opcode, $access_type); } - checkpoint_prologue!(context, true); + checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context, true); + checkpoint_epilogue!(context); inner_outcome } }; @@ -2184,14 +2134,14 @@ pub mod volatile_data_ext { ); } } - checkpoint_prologue!(context, true); + checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); charge_static_gas!(context, $opcode); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context, true); + checkpoint_epilogue!(context); inner_outcome } }; @@ -2283,14 +2233,14 @@ pub mod volatile_data_ext { if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, SLOAD, VolatileDataAccessType::Oracle); } - checkpoint_prologue!(context, true); + checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!(instructions::host::sload, context, inner_outcome); charge_static_gas!(context, SLOAD); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context, true); + checkpoint_epilogue!(context); inner_outcome } @@ -2309,14 +2259,14 @@ pub mod volatile_data_ext { VolatileDataAccessType::Beneficiary ); } - checkpoint_prologue!(context, true); + checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); apply_compute_gas_limit!(context); - checkpoint_epilogue!(context, true); + checkpoint_epilogue!(context); inner_outcome } } @@ -2344,7 +2294,6 @@ pub mod additional_limit_ext { /// /// Refunds data/KV when slot reset to original value. pub fn sstore< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2366,11 +2315,7 @@ pub mod additional_limit_ext { let loaded_data = SStoreResult { original_value, present_value, new_value }; // Execute the original SSTORE instruction - run_inner_instruction_or_abort!( - storage_gas_ext::sstore::, - context, - inner_outcome - ); + run_inner_instruction_or_abort!(storage_gas_ext::sstore, context, inner_outcome); // KV update bomb and data bomb (only when first writing non-zero value to originally zero // slot): check if the number of key-value updates or the total data size will exceed the @@ -2384,7 +2329,7 @@ pub mod additional_limit_ext { } drop(additional_limit); // REX7: re-clamp once every dimension this opcode touches has been recorded. - checkpoint_epilogue!(context, CHECKPOINT); + checkpoint_epilogue!(context); inner_outcome } @@ -2399,7 +2344,6 @@ pub mod additional_limit_ext { /// MB). Halts when data limit exceeded. pub fn log< const N: usize, - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2412,11 +2356,7 @@ pub mod additional_limit_ext { let len = as_usize_or_fail!(context.interpreter, len); // Execute the original LOG instruction - run_inner_instruction_or_abort!( - storage_gas_ext::log::, - context, - inner_outcome - ); + run_inner_instruction_or_abort!(storage_gas_ext::log::, context, inner_outcome); // Record the size of the log topics and data. If the total data size exceeds the limit, we // halt. @@ -2429,7 +2369,7 @@ pub mod additional_limit_ext { } drop(additional_limit); // REX7: re-clamp once every dimension this opcode touches has been recorded. - checkpoint_epilogue!(context, CHECKPOINT); + checkpoint_epilogue!(context); inner_outcome } } @@ -2515,7 +2455,6 @@ pub mod storage_gas_ext { ($fn_name:ident, $opcode:ident, $raw_fn:path, $has_transfer_logic:expr, $select_addr:path) => { #[doc = concat!("`", stringify!($opcode), "` opcode implementation modified from `revm` with compute gas tracking and dynamically-scaled storage gas costs.")] pub fn $fn_name< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -2523,7 +2462,7 @@ pub mod storage_gas_ext { ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation, // so the storage charge and the body's 63/64 forwarding math see the true counter. - checkpoint_prologue!(context, CHECKPOINT); + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers all of the // opcode's compute work. let gas_before = context.interpreter.gas.remaining(); @@ -2566,7 +2505,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(new_account_storage_gas); - charge_storage_gas!(context, new_account_storage_gas - drained, CHECKPOINT) + charge_storage_gas!(context, new_account_storage_gas - drained) } else { 0 }; @@ -2580,8 +2519,7 @@ pub mod storage_gas_ext { context, gas_before, storage_charged, - opcode::$opcode, - CHECKPOINT + opcode::$opcode ); inner_outcome } @@ -2764,7 +2702,6 @@ pub mod storage_gas_ext { /// circuits to [`create_rex6`] at the top; the body below is the pre-REX6 path, which can /// assume all features up to and including `MINI_REX` are enabled. pub fn create< - const CHECKPOINT: bool, WIRE: InterpreterTypes, const IS_CREATE2: bool, H: HostExt + ContextTr + JournalInspectTr + ?Sized, @@ -2777,7 +2714,7 @@ pub mod storage_gas_ext { // compute-gas recording taken after the body completes (see `create_rex6`), instead of // the pre-REX6 split `resize_gas` recording handled below. if spec.is_enabled(MegaSpecId::REX6) { - return create_rex6::(context); + return create_rex6::(context); } // Inspect the creator and compute the created address. REX5+ records the CREATE2 @@ -2827,7 +2764,7 @@ pub mod storage_gas_ext { context, inner_outcome ); - record_storage_compute_gas!(context, gas_before, 0, create_opcode(IS_CREATE2), CHECKPOINT); + record_storage_compute_gas!(context, gas_before, 0, create_opcode(IS_CREATE2)); // Pre-REX5 late-record path for the CREATE2 initcode memory-expansion gas. // Preserved verbatim for replay parity: pre-REX5 keeps the original "skip on inner @@ -2858,7 +2795,6 @@ pub mod storage_gas_ext { /// REX6 implies REX5 (and REX), so the REX5 operand validation and the contract-creation /// storage-gas path are taken unconditionally here. fn create_rex6< - const CHECKPOINT: bool, WIRE: InterpreterTypes, const IS_CREATE2: bool, H: HostExt + ContextTr + JournalInspectTr + ?Sized, @@ -2881,7 +2817,7 @@ pub mod storage_gas_ext { // REX7: settle the open segment and restore the clamp before any gas observation, so the // memory expansion, the storage charge and the body's forwarding math see the true counter. - checkpoint_prologue!(context, CHECKPOINT); + checkpoint_prologue!(context); // Captured before any gas movement so the single compute window covers the wrapper-side // CREATE2 memory expansion as well as the inner opcode. @@ -2915,8 +2851,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(create_contract_storage_gas); - let storage_charged = - charge_storage_gas!(context, create_contract_storage_gas - drained, CHECKPOINT); + let storage_charged = charge_storage_gas!(context, create_contract_storage_gas - drained); // Run the raw inner create opcode (no `compute_gas_ext` wrapper — REX6 records compute gas // once below). @@ -2940,8 +2875,7 @@ pub mod storage_gas_ext { context, gas_before, storage_charged, - create_opcode(IS_CREATE2), - CHECKPOINT + create_opcode(IS_CREATE2) ); inner_outcome } @@ -2960,14 +2894,13 @@ pub mod storage_gas_ext { /// This alternative implementation of `LOG` is only used when the `MINI_REX` spec is enabled. pub fn log< const N: usize, - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ?Sized, >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation. - checkpoint_prologue!(context, CHECKPOINT); + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); let Some(len) = context.interpreter.stack.inspect::<1>() else { @@ -2999,15 +2932,12 @@ pub mod storage_gas_ext { // The `gas_or_fail!` above is the storage-gas charge, so it gets the same segment // exclusion `charge_storage_gas!` applies at every other charge site: the raw opcode below // can halt (a static frame rejects `LOG` outright) before the recording that would - // otherwise subtract it. Frozen specs skip the exclude — nothing measures against a - // segment. - if CHECKPOINT { - context - .host - .additional_limit() - .borrow_mut() - .exclude_storage_gas_from_segment(storage_charged); - } + // otherwise subtract it. + context + .host + .additional_limit() + .borrow_mut() + .exclude_storage_gas_from_segment(storage_charged); // Run the raw opcode and record compute gas once after the body completes (canonical // metering order). Byte-equivalent to the pre-REX6 per-`N` `compute_gas_ext::logK` @@ -3015,13 +2945,7 @@ pub mod storage_gas_ext { // consumes EVM gas. The wrapper is only ever instantiated for `N` in `0..=4`, so the // generic `instructions::host::log::` covers every valid call site. run_inner_instruction_or_abort!(instructions::host::log::, context, inner_outcome); - record_storage_compute_gas!( - context, - gas_before, - storage_charged, - opcode::LOG0 + N as u8, - CHECKPOINT - ); + record_storage_compute_gas!(context, gas_before, storage_charged, opcode::LOG0 + N as u8); inner_outcome } @@ -3041,14 +2965,13 @@ pub mod storage_gas_ext { /// enabled, so we can safely assume that all features before and including Mini-Rex are /// enabled. pub fn sstore< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { // REX7: settle the open segment and restore the clamp before any gas observation. - checkpoint_prologue!(context, CHECKPOINT); + checkpoint_prologue!(context); // Captured at the very top so the single compute window covers the inner opcode. let gas_before = context.interpreter.gas.remaining(); // The address to the underlying execution contract state @@ -3083,7 +3006,7 @@ pub mod storage_gas_ext { .additional_limit() .borrow_mut() .try_consume_storage_stipend(sstore_set_storage_gas); - charge_storage_gas!(context, sstore_set_storage_gas - drained, CHECKPOINT) + charge_storage_gas!(context, sstore_set_storage_gas - drained) } else { 0 }; @@ -3093,13 +3016,7 @@ pub mod storage_gas_ext { // every spec because nothing between `gas_before` and the storage charge above consumes // EVM gas. run_inner_instruction_or_abort!(instructions::host::sstore, context, inner_outcome); - record_storage_compute_gas!( - context, - gas_before, - storage_charged, - opcode::SSTORE, - CHECKPOINT - ); + record_storage_compute_gas!(context, gas_before, storage_charged, opcode::SSTORE); inner_outcome } @@ -3124,7 +3041,6 @@ pub mod storage_gas_ext { /// sees — via the REX6-gated arm below; pre-REX6 records nothing for an existing target. The /// rest of the body, and all ≤REX5 behavior, is unchanged. pub fn selfdestruct< - const CHECKPOINT: bool, WIRE: InterpreterTypes, H: HostExt + ContextTr + JournalInspectTr + ?Sized, >( @@ -3133,7 +3049,7 @@ pub mod storage_gas_ext { // REX7: settle the open segment and restore the clamp before any gas observation — the // beneficiary-creation storage charge below and the inner opcode both run on the true // counter, which is what keeps the storage charge outside every compute window. - checkpoint_prologue!(context, CHECKPOINT); + checkpoint_prologue!(context); // Inside a static frame, revm's inner SELFDESTRUCT halts on the // static-context check without changing state. Skip the mega host work below @@ -3183,7 +3099,7 @@ pub mod storage_gas_ext { }; let drained = context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); - charge_storage_gas!(context, cost - drained, CHECKPOINT); + charge_storage_gas!(context, cost - drained); // Record resource usage for new beneficiary account context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); @@ -3564,11 +3480,11 @@ pub mod compute_gas_ext { pub fn gas_checkpoint( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { - checkpoint_prologue!(context, true); + checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); run_inner_instruction_or_abort!(instructions::system::gas, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before); - checkpoint_epilogue!(context, true); + checkpoint_epilogue!(context); inner_outcome } } From 62cd04222090d8334975fb247dfbfc5e21c3d78f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 01:34:16 +0800 Subject: [PATCH 042/208] refactor(limit): fold checkpoint/clamp state into a CheckpointTracker --- crates/mega-evm/src/evm/context.rs | 16 +-- crates/mega-evm/src/evm/instructions.rs | 2 +- crates/mega-evm/src/limit/checkpoint.rs | 162 ++++++++++++++++++++++++ crates/mega-evm/src/limit/limit.rs | 107 ++++------------ crates/mega-evm/src/limit/mod.rs | 1 + 5 files changed, 199 insertions(+), 89 deletions(-) create mode 100644 crates/mega-evm/src/limit/checkpoint.rs diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index df428977..52c31651 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -1352,11 +1352,11 @@ mod tests { assert_eq!(rex7_to_rex6.mega_spec(), MegaSpecId::REX6); assert_eq!( - rex7_to_rex6.additional_limit.borrow().checkpoint_accounting(), - rex6_direct.additional_limit.borrow().checkpoint_accounting(), + rex7_to_rex6.additional_limit.borrow().rex7_enabled(), + rex6_direct.additional_limit.borrow().rex7_enabled(), ); assert!( - !rex7_to_rex6.additional_limit.borrow().checkpoint_accounting(), + !rex7_to_rex6.additional_limit.borrow().rex7_enabled(), "REX6 must not latch checkpoint accounting" ); assert_eq!(rex7_to_rex6.additional_limit.borrow().limits, limits); @@ -1369,11 +1369,11 @@ mod tests { assert_eq!(rex6_to_rex7.mega_spec(), MegaSpecId::REX7); assert_eq!( - rex6_to_rex7.additional_limit.borrow().checkpoint_accounting(), - rex7_direct.additional_limit.borrow().checkpoint_accounting(), + rex6_to_rex7.additional_limit.borrow().rex7_enabled(), + rex7_direct.additional_limit.borrow().rex7_enabled(), ); assert!( - rex6_to_rex7.additional_limit.borrow().checkpoint_accounting(), + rex6_to_rex7.additional_limit.borrow().rex7_enabled(), "REX7 must latch checkpoint accounting" ); assert_eq!(rex6_to_rex7.additional_limit.borrow().limits, limits); @@ -1382,7 +1382,7 @@ mod tests { .with_tx_runtime_limits(limits) .with_cfg_unpinned(CfgEnv::new_with_spec(MegaSpecId::REX6)); assert!( - !via_unpinned.additional_limit.borrow().checkpoint_accounting(), + !via_unpinned.additional_limit.borrow().rex7_enabled(), "with_cfg_unpinned must rebuild latched limit state on a spec change" ); assert_eq!(via_unpinned.additional_limit.borrow().limits, limits); @@ -1400,7 +1400,7 @@ mod tests { assert!(Rc::ptr_eq(&before, &context.additional_limit)); assert_eq!(context.additional_limit.borrow().limits, limits); - assert!(!context.additional_limit.borrow().checkpoint_accounting()); + assert!(!context.additional_limit.borrow().rex7_enabled()); } #[test] diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 3bdcc130..7da79e15 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -3457,7 +3457,7 @@ pub mod compute_gas_ext { // `storage_gas_ext::selfdestruct`, which also restored the clamp; the window is re-opened // here so the frame's final settlement cannot bill this body a second time. let gas_used = pre_charged + gas_before.saturating_sub(gas_after); - if additional_limit.checkpoint_accounting() { + if additional_limit.rex7_enabled() { additional_limit.sync_checkpoint_baseline(gas_after); } if !additional_limit.record_compute_gas_all_dims(gas_used) { diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs new file mode 100644 index 00000000..8f43ee15 --- /dev/null +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -0,0 +1,162 @@ +//! REX7+ checkpoint settlement and gas-clamp state. +//! +//! Holds the spec latch, the open-segment interpreter-gas baseline, the clamp +//! in force for the current plain-opcode segment, and the detention-attribution +//! flag for a clamp-induced out-of-gas. Cross-tracker orchestration (reading +//! compute-gas headroom, latching `has_exceeded_limit`) stays on +//! [`AdditionalLimit`](super::AdditionalLimit). + +use super::compute_gas::ClampBinding; +use crate::MegaSpecId; + +/// Tracks REX7+ checkpoint-accounting and gas-clamp state for one transaction. +#[derive(Debug, Clone)] +pub(crate) struct CheckpointTracker { + /// REX7+: whether compute gas settles at checkpoints rather than per opcode. + /// + /// When set, plain opcodes run unwrapped and record nothing; the interpreter's own gas + /// counter is read at each checkpoint and the whole segment since the previous one is + /// recorded in a single call. + rex7_enabled: bool, + + /// Interpreter gas remaining at the start of the current unsettled segment — the previous + /// checkpoint, or the frame entry / resume that opened the window. Only meaningful while a + /// frame is running and only when [`rex7_enabled`](Self::rex7_enabled) is active. Re-synced + /// at every [`before_frame_run`](super::AdditionalLimit::before_frame_run) (which covers both + /// frame entry and every resume after a child frame's outcome is merged back) and at every + /// checkpoint prologue and body recording. + baseline: u64, + + /// Gas-clamp enforcement (REX7+): the clamp in force for the plain-opcode segment the + /// current frame is inside, so that revm's own per-opcode gas checks enforce the compute + /// headroom at no per-opcode cost. + /// + /// Present only while the current frame is inside a plain segment: every checkpoint takes it + /// before running its body — so CALL forwarding, `GAS` and storage charges observe the true + /// counter — and re-applies it on the way out, and the frame's final result takes it via + /// [`settle_frame_final_result`](super::AdditionalLimit::settle_frame_final_result). + clamp: Option, + + /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level + /// constraint. + /// + /// [`ComputeGasTracker::is_detained_exceed`] requires `used > detained_limit`, which a + /// clamp-stopped transaction never reaches — the crossing opcode is stopped before it + /// executes, so usage stays at or below the limit. The halt-reason attribution consults + /// this flag instead, keeping the reported reason `VolatileDataAccessOutOfGas` exactly as + /// per-opcode enforcement reports it. + latched_detained: bool, +} + +/// A gas clamp in force for one plain-opcode segment (REX7+). +/// +/// The clamp is a lifecycle, not an amount. It is recorded exactly while it **binds** — while the +/// interpreter's true remaining gas was at or above the compute headroom when the segment opened — +/// and a `hidden` of zero is a binding clamp whose two budgets happened to coincide, not the +/// absence of one. When the frame's own gas would run out ahead of the compute headroom no clamp +/// is recorded at all, and an out-of-gas inside that segment stays the EVM's own. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ClampState { + /// Interpreter gas hidden from the interpreter for this segment. + pub(crate) hidden: u64, + /// The constraint the clamp was bound to, captured at the moment it was applied. + pub(crate) binding: ClampBinding, +} + +impl CheckpointTracker { + pub(crate) fn new(spec: MegaSpecId) -> Self { + Self { + rex7_enabled: spec.is_enabled(MegaSpecId::REX7), + baseline: 0, + clamp: None, + latched_detained: false, + } + } + + pub(crate) fn reset(&mut self) { + self.baseline = 0; + self.clamp = None; + self.latched_detained = false; + } + + /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. + #[inline] + pub(crate) fn rex7_enabled(&self) -> bool { + self.rex7_enabled + } + + /// Interpreter gas remaining at the start of the current unsettled segment. + #[inline] + pub(crate) fn baseline(&self) -> u64 { + self.baseline + } + + /// Re-opens the settlement window at `remaining`, without recording anything. + #[inline] + pub(crate) fn sync_baseline(&mut self, remaining: u64) { + self.baseline = remaining; + } + + /// Moves the open segment's baseline down by `amount` of `MegaETH` storage gas just charged to + /// the interpreter, so the charge sits outside the segment rather than inside it. + /// + /// A checkpoint body normally subtracts its own storage charge when it closes its measurement + /// window. A body that aborts — a static-context `LOG`, a `SELFDESTRUCT` whose inner + /// instruction runs out of gas — never reaches that subtraction, and the frame-exit settlement + /// that follows would then bill the charge as compute. Excluding it from the baseline as it is + /// charged makes the exclusion hold on both paths; on the normal path the body's own window + /// re-syncs the baseline afterwards, so this is invisible there. + /// + /// No-op before REX7, where nothing measures against a baseline. + #[inline] + pub(crate) fn exclude_storage_gas_from_segment(&mut self, amount: u64) { + if self.rex7_enabled { + self.baseline = self.baseline.saturating_sub(amount); + } + } + + /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, + /// returning that amount. + #[inline] + pub(crate) fn restore_hidden(&mut self) -> u64 { + self.clamp.take().map_or(0, |clamp| clamp.hidden) + } + + /// Whether a clamp is currently outstanding. + #[inline] + pub(crate) fn has_clamp(&self) -> bool { + self.clamp.is_some() + } + + /// Records the clamp in force for the segment that starts now. + #[inline] + pub(crate) fn set_clamp(&mut self, hidden: u64, binding: ClampBinding) { + self.clamp = Some(ClampState { hidden, binding }); + } + + /// Takes the outstanding clamp, if any. + #[inline] + pub(crate) fn take_clamp(&mut self) -> Option { + self.clamp.take() + } + + /// Whether a clamp-induced out-of-gas was latched under a detained TX-level constraint. + #[inline] + pub(crate) fn latched_detained(&self) -> bool { + self.latched_detained + } + + /// Records whether the just-latched clamp exceed was under a detained TX-level constraint. + #[inline] + pub(crate) fn set_latched_detained(&mut self, latched: bool) { + self.latched_detained = latched; + } + + /// Returns the unsettled segment usage and re-opens the window at `remaining`. + #[inline] + pub(crate) fn take_segment(&mut self, remaining: u64) -> u64 { + let gas_used = self.baseline.saturating_sub(remaining); + self.baseline = remaining; + gas_used + } +} diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 5c17cd26..284cd154 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -13,7 +13,7 @@ use revm::{ }; use super::{ - compute_gas, data_size, frame_limit::TxRuntimeLimit, kv_update, state_growth, + checkpoint, compute_gas, data_size, frame_limit::TxRuntimeLimit, kv_update, state_growth, storage_call_stipend, }; use crate::{ @@ -108,55 +108,8 @@ pub struct AdditionalLimit { /// A tracker for the `STORAGE_CALL_STIPEND` granted to value-transferring calls (REX4+). pub(crate) storage_call_stipend: storage_call_stipend::StorageCallStipendTracker, - /// REX7+: whether compute gas settles at checkpoints rather than per opcode. - /// - /// When set, plain opcodes run unwrapped and record nothing; the interpreter's own gas - /// counter is read at each checkpoint and the whole segment since the previous one is - /// recorded in a single call. - checkpoint_accounting: bool, - - /// Interpreter gas remaining at the start of the current unsettled segment — the previous - /// checkpoint, or the frame entry / resume that opened the window. Only meaningful while a - /// frame is running and only when [`checkpoint_accounting`](Self::checkpoint_accounting) is - /// active. Re-synced at every [`before_frame_run`](Self::before_frame_run) (which covers both - /// frame entry and every resume after a child frame's outcome is merged back) and at every - /// checkpoint prologue and body recording. - checkpoint_baseline: u64, - - /// Gas-clamp enforcement (REX7+): the clamp in force for the plain-opcode segment the - /// current frame is inside, so that revm's own per-opcode gas checks enforce the compute - /// headroom at no per-opcode cost. - /// - /// Present only while the current frame is inside a plain segment: every checkpoint takes it - /// before running its body — so CALL forwarding, `GAS` and storage charges observe the true - /// counter — and re-applies it on the way out, and the frame's final result takes it via - /// [`settle_frame_final_result`](Self::settle_frame_final_result). - clamp: Option, - - /// Whether a clamp-induced out-of-gas was latched while gas detention was the binding TX-level - /// constraint. - /// - /// [`ComputeGasTracker::is_detained_exceed`] requires `used > detained_limit`, which a - /// clamp-stopped transaction never reaches — the crossing opcode is stopped before it - /// executes, so usage stays at or below the limit. The halt-reason attribution consults - /// this flag instead, keeping the reported reason `VolatileDataAccessOutOfGas` exactly as - /// per-opcode enforcement reports it. - clamp_latched_detained: bool, -} - -/// A gas clamp in force for one plain-opcode segment (REX7+). -/// -/// The clamp is a lifecycle, not an amount. It is recorded exactly while it **binds** — while the -/// interpreter's true remaining gas was at or above the compute headroom when the segment opened — -/// and a `hidden` of zero is a binding clamp whose two budgets happened to coincide, not the -/// absence of one. When the frame's own gas would run out ahead of the compute headroom no clamp -/// is recorded at all, and an out-of-gas inside that segment stays the EVM's own. -#[derive(Clone, Copy, Debug)] -struct ClampState { - /// Interpreter gas hidden from the interpreter for this segment. - hidden: u64, - /// The constraint the clamp was bound to, captured at the moment it was applied. - binding: compute_gas::ClampBinding, + /// A tracker for REX7+ checkpoint settlement and gas-clamp state. + pub(crate) checkpoint: checkpoint::CheckpointTracker, } /// The usage of the additional limits. @@ -184,10 +137,7 @@ impl AdditionalLimit { kv_update: kv_update::KVUpdateTracker::new(spec, limits.tx_kv_updates_limit), compute_gas: compute_gas::ComputeGasTracker::new(spec, limits.tx_compute_gas_limit), storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), - checkpoint_accounting: spec.is_enabled(MegaSpecId::REX7), - checkpoint_baseline: 0, - clamp: None, - clamp_latched_detained: false, + checkpoint: checkpoint::CheckpointTracker::new(spec), } } } @@ -229,15 +179,13 @@ impl AdditionalLimit { self.data_size.reset(); self.kv_update.reset(); self.storage_call_stipend.reset(); - self.checkpoint_baseline = 0; - self.clamp = None; - self.clamp_latched_detained = false; + self.checkpoint.reset(); } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. #[inline] - pub(crate) fn checkpoint_accounting(&self) -> bool { - self.checkpoint_accounting + pub(crate) fn rex7_enabled(&self) -> bool { + self.checkpoint.rex7_enabled() } /// Interpreter gas remaining at the start of the current unsettled segment. @@ -247,7 +195,7 @@ impl AdditionalLimit { /// unwrapped plain opcode executed since the previous checkpoint. #[inline] pub(crate) fn checkpoint_baseline(&self) -> u64 { - self.checkpoint_baseline + self.checkpoint.baseline() } /// Re-opens the settlement window at `remaining`, without recording anything. @@ -256,7 +204,7 @@ impl AdditionalLimit { /// call this once it has recorded, so a later settlement cannot bill the segment twice. #[inline] pub(crate) fn sync_checkpoint_baseline(&mut self, remaining: u64) { - self.checkpoint_baseline = remaining; + self.checkpoint.sync_baseline(remaining); } /// Moves the open segment's baseline down by `amount` of `MegaETH` storage gas just charged to @@ -272,9 +220,7 @@ impl AdditionalLimit { /// No-op before REX7, where nothing measures against a baseline. #[inline] pub(crate) fn exclude_storage_gas_from_segment(&mut self, amount: u64) { - if self.checkpoint_accounting { - self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); - } + self.checkpoint.exclude_storage_gas_from_segment(amount); } /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, @@ -285,7 +231,7 @@ impl AdditionalLimit { /// segment. #[inline] pub(crate) fn checkpoint_restore_hidden(&mut self) -> u64 { - self.clamp.take().map_or(0, |clamp| clamp.hidden) + self.checkpoint.restore_hidden() } /// Applies the gas clamp for the segment that starts at `remaining`, and returns the amount @@ -302,7 +248,7 @@ impl AdditionalLimit { /// instead). #[inline] pub(crate) fn checkpoint_clamp_amount(&mut self, remaining: u64) -> u64 { - debug_assert!(self.clamp.is_none(), "clamp applied while a clamp is outstanding"); + debug_assert!(!self.checkpoint.has_clamp(), "clamp applied while a clamp is outstanding"); if !self.has_exceeded_limit.within_limit() { return 0; } @@ -310,7 +256,7 @@ impl AdditionalLimit { let Some(hidden) = remaining.checked_sub(binding.headroom) else { return 0; }; - self.clamp = Some(ClampState { hidden, binding }); + self.checkpoint.set_clamp(hidden, binding); hidden } @@ -337,8 +283,10 @@ impl AdditionalLimit { // Preserve the volatile-detention attribution: when the binding TX-level constraint at // clamp time was the detained limit, the halt must classify as `VolatileDataAccessOutOfGas` // exactly as per-opcode enforcement classifies it. - self.clamp_latched_detained = !binding.frame_local && - self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); + self.checkpoint.set_latched_detained( + !binding.frame_local && + self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(), + ); } /// Finalises what the frame's own result decides about the clamp: restores any outstanding @@ -360,10 +308,10 @@ impl AdditionalLimit { /// here, and attributing the halt to the resource limit keeps the sender's remaining gas /// refundable. pub(crate) fn settle_frame_final_result(&mut self, result: &mut InterpreterResult) { - if !self.checkpoint_accounting { + if !self.checkpoint.rex7_enabled() { return; } - if let Some(clamp) = self.clamp.take() { + if let Some(clamp) = self.checkpoint.take_clamp() { result.gas.erase_cost(clamp.hidden); // `MemoryOOG` is the same gas shortage reported from the memory-expansion path; every // other result either is unrelated to gas or cannot arise from a plain opcode. @@ -527,9 +475,9 @@ impl AdditionalLimit { access_type: VolatileDataAccess, ) -> Option { // `is_detained_exceed` covers per-opcode enforcement, where usage crossed the detained - // limit. `clamp_latched_detained` covers gas-clamp enforcement, where the crossing opcode + // limit. `latched_detained` covers gas-clamp enforcement, where the crossing opcode // was stopped before executing and usage therefore stays at or below the limit. - (self.compute_gas.is_detained_exceed() || self.clamp_latched_detained).then(|| { + (self.compute_gas.is_detained_exceed() || self.checkpoint.latched_detained()).then(|| { MegaHaltReason::VolatileDataAccessOutOfGas { access_type, limit: self.compute_gas.detained_limit(), @@ -931,14 +879,14 @@ impl AdditionalLimit { // interpreter's counter in its real, post-merge state. No clamp can be outstanding // here: every suspension point (the CALL / CREATE checkpoint prologue) and every // frame end restores it first. - if self.checkpoint_accounting { - debug_assert!(self.clamp.is_none(), "frame resumed with a clamp outstanding"); + if self.checkpoint.rex7_enabled() { + debug_assert!(!self.checkpoint.has_clamp(), "frame resumed with a clamp outstanding"); let hide = self.checkpoint_clamp_amount(frame.interpreter.gas.remaining()); if hide > 0 { let clamped = frame.interpreter.gas.record_regular_cost(hide); debug_assert!(clamped, "clamp amount exceeds remaining gas"); } - self.checkpoint_baseline = frame.interpreter.gas.remaining(); + self.checkpoint.sync_baseline(frame.interpreter.gas.remaining()); } None } @@ -998,13 +946,12 @@ impl AdditionalLimit { // frame spend the same headroom a second time. What such a frame additionally destroys — // the budget it never gets to spend — is settled after action processing, outside // enforcement, by `settle_exceptional_halt_burn`. - if self.checkpoint_accounting { + if self.checkpoint.rex7_enabled() { if let InterpreterAction::Return(_) = action { let remaining = frame.interpreter.gas.remaining(); - let gas_used = self.checkpoint_baseline.saturating_sub(remaining); + let gas_used = self.checkpoint.take_segment(remaining); let _ = self.record_compute_gas_unguarded(gas_used); self.refresh_latched_compute_usage(); - self.checkpoint_baseline = remaining; } } @@ -1159,7 +1106,7 @@ impl AdditionalLimit { /// [`settle_frame_final_result`](Self::settle_frame_final_result) latches earlier in this /// frame exit. fn settle_exceptional_halt_burn(&mut self, result: &FrameResult) { - if !self.checkpoint_accounting || + if !self.checkpoint.rex7_enabled() || self.limit_exceeded() || result.instruction_result().is_ok_or_revert() { diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index a536565a..fe0a986e 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -1,6 +1,7 @@ use alloy_primitives::Bytes; use alloy_sol_types::SolError; +mod checkpoint; mod compute_gas; mod data_size; mod frame_limit; From b9323d51c31497a8b435f0e840de95ad45288a16 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 02:04:58 +0800 Subject: [PATCH 043/208] refactor(evm): align the leftover checkpoint_accounting local with the rex7 naming --- crates/mega-evm/src/evm/instructions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 7da79e15..b70ae4d3 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -989,7 +989,7 @@ macro_rules! record_storage_compute_gas { ($context:expr, $gas_before:expr, $storage_charged:expr, $opcode:expr) => {{ let spec = $context.host.spec_id(); let is_rex6 = spec.is_enabled(MegaSpecId::REX6); - let is_checkpoint_accounting = spec.is_enabled(MegaSpecId::REX7); + let is_rex7 = spec.is_enabled(MegaSpecId::REX7); let gas_after = $context.interpreter.gas.remaining(); // The per-opcode `$gas_before` window applies on every spec: under checkpoint accounting // the plain segment ahead of this opcode was already settled by @@ -1001,7 +1001,7 @@ macro_rules! record_storage_compute_gas { // before dispatch, or an outer volatile wrapper — does so ahead of the prologue, so under // checkpoint accounting it is already inside the settled segment and adding it back here // would bill it twice. - let mut gas_used = if is_checkpoint_accounting { + let mut gas_used = if is_rex7 { $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged) } else { (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) @@ -1040,7 +1040,7 @@ macro_rules! record_storage_compute_gas { let mut additional_limit = $context.host.additional_limit().borrow_mut(); // Re-open the settlement window at this opcode's exit before recording, so neither a // halt here nor the frame-final settlement can bill this segment twice. - if is_checkpoint_accounting { + if is_rex7 { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { From 2450928f4c33f21a74c5c320f69ba9646988ec45 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 10:00:07 +0800 Subject: [PATCH 044/208] refactor(evm): build the rex7 table by inheriting rex6 slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 13 storage-gas and frame-spawning slots the rex7 table shares with rex6 were declared twice, once per table, and kept in sync by convention. Copy them out of the rex6 table instead, together with the four opcodes revm wires ahead of the fork that activates them (DUPN, SWAPN, EXCHANGE, SLOTNUM) — rex6 holds control::unknown there, so inheriting the slots keeps the two opcode sets identical rather than restoring the handler by hand. The table's final contents are unchanged. --- crates/mega-evm/src/evm/instructions.rs | 67 ++++++++++++++++--------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index b70ae4d3..49a684a8 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -610,6 +610,15 @@ mod rex7 { /// where a limit-exceeding transaction halts: the exceed surfaces at the next checkpoint /// rather than at the opcode that crossed the limit. /// + /// Of those, only the volatile / detention handlers (plus `GAS`, a checkpoint so that the + /// clamp is restored before the counter is observed) are declared here. The storage-gas and + /// frame-spawning slots are copied out of the Rex6 table one opcode at a time, which is what + /// makes "the same handler chains as Rex6" a property of the construction rather than of two + /// declarations kept in sync. The same copy covers the four opcodes revm wires ahead of the + /// fork that activates them (`DUPN`, `SWAPN`, `EXCHANGE`, `SLOTNUM`): Rex6 leaves + /// `control::unknown` in those slots, so inheriting them keeps the two opcode sets identical + /// instead of letting revm's base table decide what Rex7 exposes. + /// /// The Rex6 behavior differences (canonical metering order, `create_rex6` dispatch, /// SELFDESTRUCT existing-target accounting, CALL-family EIP-7702 delegate resolution on the /// disabled path) live as internal `spec.is_enabled(MegaSpecId::REX6)` dispatch inside the @@ -627,15 +636,40 @@ mod rex7 { { use revm::bytecode::opcode::*; let mut table = instructions::instruction_table::(); + let rex6 = rex6::instruction_table::(); + + /// The opcodes Rex7 takes from the Rex6 table verbatim. + const INHERITED_FROM_REX6: &[u8] = &[ + // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains, which under + // Rex7 open with a checkpoint prologue and close with an epilogue. + SSTORE, + LOG0, + LOG1, + LOG2, + LOG3, + LOG4, + CREATE, + CREATE2, + CALL, + CALLCODE, + DELEGATECALL, + STATICCALL, + SELFDESTRUCT, + // Opcodes revm's table wires ahead of the fork that activates them: every + // `MegaSpecId` maps to a pre-activation Ethereum spec and no `MegaETH` table has ever + // dispatched them, so Rex6 holds `control::unknown` here. + DUPN, + SWAPN, + EXCHANGE, + SLOTNUM, + ]; - // revm's table wires these four ahead of the fork that activates them; every `MegaSpecId` - // maps to a pre-activation Ethereum spec, and no `MegaETH` table has ever dispatched them. - // Restore the unknown-opcode handler so the checkpoint table's opcode set is the same one - // Rex6 exposes. - table[DUPN as usize] = Instruction::new(control::unknown); - table[SWAPN as usize] = Instruction::new(control::unknown); - table[EXCHANGE as usize] = Instruction::new(control::unknown); - table[SLOTNUM as usize] = Instruction::new(control::unknown); + let mut i = 0; + while i < INHERITED_FROM_REX6.len() { + let opcode = INHERITED_FROM_REX6[i] as usize; + table[opcode] = rex6[opcode]; + i += 1; + } // Volatile / detention checkpoints: raw instruction, segment settlement, detention cap. table[BALANCE as usize] = Instruction::new(volatile_data_ext::balance_checkpoint); @@ -658,23 +692,6 @@ mod rex7 { // the counter is observed. table[GAS as usize] = Instruction::new(compute_gas_ext::gas_checkpoint); - // Storage-gas and frame-spawning checkpoints: the Rex6 handler chains unchanged. Under - // Rex7 they open with a checkpoint prologue and close with an epilogue. - table[SSTORE as usize] = Instruction::new(additional_limit_ext::sstore); - table[LOG0 as usize] = Instruction::new(additional_limit_ext::log::<0, _, _>); - table[LOG1 as usize] = Instruction::new(additional_limit_ext::log::<1, _, _>); - table[LOG2 as usize] = Instruction::new(additional_limit_ext::log::<2, _, _>); - table[LOG3 as usize] = Instruction::new(additional_limit_ext::log::<3, _, _>); - table[LOG4 as usize] = Instruction::new(additional_limit_ext::log::<4, _, _>); - table[CREATE as usize] = Instruction::new(forward_gas_ext::create); - table[CREATE2 as usize] = Instruction::new(forward_gas_ext::create2); - table[CALL as usize] = Instruction::new(volatile_data_ext::call); - table[CALLCODE as usize] = Instruction::new(volatile_data_ext::call_code); - table[DELEGATECALL as usize] = Instruction::new(volatile_data_ext::delegate_call); - table[STATICCALL as usize] = Instruction::new(volatile_data_ext::static_call); - table[SELFDESTRUCT as usize] = - Instruction::new(volatile_data_ext::selfdestruct_with_beneficiary_guard); - table } From dd17a15ed9c910a495c2c5949c2ea3eb9319bca8 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 10:00:12 +0800 Subject: [PATCH 045/208] test(rex7): guard the rex7 opcode set against the rex6 one Tables through rex6 are built from an all-unknown array, so an opcode revm gains stays unknown until someone wires it. The rex7 table starts from revm's table, where the same release activates it silently. Probe all 256 opcodes under both specs and require them to agree on whether the slot dispatches control::unknown, so a divergence fails here instead of waiting to be read out of revm's table by hand. --- crates/mega-evm/tests/rex7/main.rs | 3 + .../mega-evm/tests/rex7/opcode_set_parity.rs | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/opcode_set_parity.rs diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 4219947b..0c286b6b 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -14,6 +14,8 @@ //! stop. //! - `gas_leakage` — the three paths a per-frame gas mechanism can leak through (interception, //! TX-level rescue, frame return), each with a clamp outstanding. +//! - `opcode_set_parity` — all 256 opcodes probed under both specs, so the REX7 table cannot gain +//! an opcode REX6 does not have when revm's base table grows one. //! - `parity_shapes` — parity on the transaction shapes that enter through a different door: //! EIP-7702 authorizations, the `KeylessDeploy` sandbox, system-originated (exempt) transactions, //! the REX5 storage-call stipend, and oracle hints. @@ -37,4 +39,5 @@ mod gas_leakage; mod interceptor_resume; mod latch_surfacing; mod modexp_gas; +mod opcode_set_parity; mod parity_shapes; diff --git a/crates/mega-evm/tests/rex7/opcode_set_parity.rs b/crates/mega-evm/tests/rex7/opcode_set_parity.rs new file mode 100644 index 00000000..ad0fbfb1 --- /dev/null +++ b/crates/mega-evm/tests/rex7/opcode_set_parity.rs @@ -0,0 +1,98 @@ +//! REX7: the opcode set the table exposes must be exactly the one REX6 exposes. +//! +//! Every table through REX6 is built from scratch — `mini_rex` starts from an +//! all-`control::unknown` array and wires the opcodes it supports — so an opcode revm gains in a +//! future release stays unknown there until someone wires it. The REX7 table starts from revm's own +//! table instead, which makes the same release silently hand REX7 an opcode REX6 does not have. +//! That is a fail-open construction, and the four slots REX7 inherits back from REX6 (`DUPN`, +//! `SWAPN`, `EXCHANGE`, `SLOTNUM`) were found by reading revm's table by hand. +//! +//! This file replaces that reading with a check. It probes all 256 opcodes under both specs and +//! asserts they agree on one bit: whether the table dispatched `control::unknown`. +//! +//! ## What the probe reads, and what it does not +//! +//! The probe is a one-byte contract holding the opcode alone, run as a whole transaction. Its stack +//! is empty, so a supported opcode usually halts on a stack underflow — the probe deliberately does +//! not compare the two specs' full results, only whether each reports `OpcodeNotFound`. +//! `control::unknown` is the only handler that produces it, so the bit says exactly which slots +//! hold that handler, and nothing about how the surrounding wrappers differ. +//! +//! Two limits follow from reading a single bit. An opcode revm activates for the Ethereum spec +//! `MegaSpecId` maps to is caught, because REX7 would then execute it (or reject it some other way) +//! while REX6 still answers `OpcodeNotFound` — but an opcode revm wires and gates behind a later +//! fork is caught only because its `NotActivated` rejection is a different reason, not because the +//! opcode sets truly diverge. And an immediate-taking opcode whose immediate fails to decode maps +//! to `OpcodeNotFound` in revm, so an activated opcode of that shape could read as unknown here. +//! Both are conservative in the direction that matters: the check fails loudly and asks for an +//! explicit decision rather than passing quietly. + +use crate::common::{transact_default, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::MemoryDatabase, EthHaltReason, MegaHaltReason, MegaSpecId, OpHaltReason, +}; +use revm::{bytecode::opcode::OpCode, context::result::ExecutionResult}; + +/// Runs the one-byte probe for `opcode` under `spec` and reports whether the table dispatched +/// `control::unknown`. +fn dispatches_unknown(spec: MegaSpecId, opcode: u8) -> bool { + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, Bytes::from(vec![opcode])); + matches!( + transact_default(spec, db).result, + ExecutionResult::Halt { + reason: MegaHaltReason::Base(OpHaltReason::Base(EthHaltReason::OpcodeNotFound)), + .. + } + ) +} + +/// Names `opcode` for a failure message, falling back to its hex value when revm does not know it. +fn opcode_name(opcode: u8) -> String { + OpCode::new(opcode).map_or_else(|| format!("{opcode:#04x}"), |op| op.as_str().to_string()) +} + +/// The guard: no opcode may be unknown under one spec and dispatched under the other. +#[test] +fn test_rex7_opcode_set_matches_rex6() { + let divergent: Vec = (0..=u8::MAX) + .filter_map(|opcode| { + let rex6 = dispatches_unknown(MegaSpecId::REX6, opcode); + let rex7 = dispatches_unknown(MegaSpecId::REX7, opcode); + (rex6 != rex7).then(|| { + let known_to = if rex7 { "REX6" } else { "REX7" }; + format!("{} ({opcode:#04x}): dispatched only by {known_to}", opcode_name(opcode)) + }) + }) + .collect(); + + assert!( + divergent.is_empty(), + "REX7 must expose the same opcode set as REX6, but they disagree on:\n {}\n\ + Wire the opcode into both tables, or inherit the slot from REX6 in `mod rex7`.", + divergent.join("\n ") + ); +} + +/// The probe has to be able to answer both ways, or the guard above passes vacuously — a run where +/// every transaction failed before reaching the opcode would report no divergence at all. +#[test] +fn test_opcode_probe_discriminates_unknown_from_dispatched() { + // 0x0c is unassigned in the EVM: unknown under both specs, in revm's table and in MegaETH's. + const UNASSIGNED: u8 = 0x0c; + // `PUSH0` needs no operands, so it runs to completion under both. + let dispatched = revm::bytecode::opcode::PUSH0; + + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + assert!( + dispatches_unknown(spec, UNASSIGNED), + "{spec:?}: an unassigned opcode must report OpcodeNotFound" + ); + assert!( + !dispatches_unknown(spec, dispatched), + "{spec:?}: PUSH0 must not report OpcodeNotFound" + ); + } +} From 83b2a1bfbfbce0e748b84367c3624687e0ab740e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 09:55:59 +0800 Subject: [PATCH 046/208] feat(evm): charge static gas on REX7 volatile-guard reject REX7 now bills the opcode's static fee before a disableVolatileDataAccess revert; frozen specs still reject for free. Shared CALL / SELFDESTRUCT wrappers gate the new order with spec.is_enabled(REX7). The frozen detention-window tripwire is the same gate, so REX7 underfunded CALL / EXTCODECOPY shapes stay unmarked without a false "frozen divergence" panic. --- crates/mega-evm/src/evm/instructions.rs | 153 ++++--- .../mega-evm/tests/rex7/charge_on_reject.rs | 425 ++++++++++++++++++ .../mega-evm/tests/rex7/detention_window.rs | 105 +++++ crates/mega-evm/tests/rex7/main.rs | 8 + 4 files changed, 639 insertions(+), 52 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/charge_on_reject.rs create mode 100644 crates/mega-evm/tests/rex7/detention_window.rs diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 49a684a8..02810080 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1536,12 +1536,17 @@ opcode and revert immediately if the access would be volatile. This ensures that disabled volatile accesses do not pollute the tracker's `volatile_data_accessed` bitmap or lower the `compute_gas_limit`. -The check runs before *anything* is charged for the opcode, including its static gas: every spec's -static gas table zeroes the entries of the opcodes handled here, and the entry is charged via -[`charge_static_gas`] only after the check has declined to fire. +Through REX6 the check runs before *anything* is charged for the opcode, including its static gas: +every spec's static gas table zeroes the entries of the opcodes handled here, and the entry is +charged via [`charge_static_gas`] only after the check has declined to fire. That is what lets the rejection be a revert that keeps the frame's whole remaining gas even when that gas would not have covered the opcode's static cost. +REX7 charges the static entry first and then runs the same check: a rejection is still a revert +with the same payload, but the static fee stays charged and is settled into compute gas as part of +the open segment. A frame that cannot afford the static fee runs out of gas instead of reaching +the revert. Frozen specs are unchanged. + # Where the Static Gas Lands Once the Guard Declines `MegaETH`'s gas schedule is the one revm charged from inside each opcode's body, before a static gas @@ -1614,6 +1619,10 @@ pub mod volatile_data_ext { /// transaction is found, so the wrapper backfill archived for that case gets implemented /// instead of the divergence going unnoticed. /// + /// REX7 specifies that same charge-before-load order, so a window miss is not a replay + /// divergence there and this check must not fire. The gate is `!is_enabled(REX7)` rather + /// than a table split because the CALL-family wrapper is shared with the frozen specs. + /// /// The check over-approximates on purpose — it does not reconstruct how far revm 27 would /// have gotten — with one exception: a `MemoryOOG` halt is never routed here, because memory /// expansion was charged before the load under revm 27 as well, so that halt shape cannot @@ -1625,6 +1634,9 @@ pub mod volatile_data_ext { opcode: u8, raw_target: Option
, ) { + if host.spec_id().is_enabled(MegaSpecId::REX7) { + return; + } let Some(target) = raw_target else { return }; if host.volatile_data_tracker().borrow().has_accessed_beneficiary_balance() { return; @@ -1643,15 +1655,15 @@ pub mod volatile_data_ext { } /// Rejects the guarded opcode with the `disableVolatileDataAccess` revert data and returns from - /// the enclosing handler, leaving the frame's gas exactly as it was before the opcode. - /// - /// Every guard using this macro rejects its opcode *before* the opcode executes, so the opcode - /// must cost the frame nothing: the gas snapshot taken into the revert action is what the - /// parent frame gets back. Nothing has been debited at this point — the guarded opcodes are - /// excluded from the interpreter's static-gas pre-charge and are charged by - /// [`charge_static_gas`] only once the guard has declined to fire — so the snapshot is taken - /// as-is. Debiting and refunding around the guard instead would be observably wrong for a - /// frame holding less gas than the pre-charge: it would never reach the guard at all. + /// the enclosing handler, snapshotting the frame's gas as it stands. + /// + /// Through REX6 nothing has been debited at this point — the guarded opcodes are excluded from + /// the interpreter's static-gas pre-charge and are charged by [`charge_static_gas`] only once + /// the guard has declined to fire — so the snapshot is the gas the frame held on entry and the + /// parent gets all of it back. REX7 charges the static entry first, so the snapshot already + /// reflects that debit and the revert does not refund it. Debiting and refunding around the + /// guard instead would be observably wrong for a frozen-spec frame holding less gas than the + /// pre-charge: it would never reach the guard at all. macro_rules! revert_volatile_access_disabled { ($context:expr, $opcode:ident, $access_type:expr) => {{ $context.interpreter.bytecode.set_action(InterpreterAction::new_return( @@ -1854,20 +1866,20 @@ pub mod volatile_data_ext { // The guards apply only once the opcode actually acts on a target. if let Some(addr_word) = context.interpreter.stack.inspect::<0>() { let target: Address = addr_word.into_address(); + let spec = context.host.spec_id(); // REX6: the executing contract (source) reading and zeroing its own balance is // itself a beneficiary observation. Frozen off pre-REX6, where only the stack // target below was guarded. - if context.host.spec_id().is_enabled(MegaSpecId::REX6) && - context.interpreter.input.target_address() == beneficiary - { - revert_volatile_access_disabled!( - context, - SELFDESTRUCT, - VolatileDataAccessType::Beneficiary - ); - } + let hits_source = spec.is_enabled(MegaSpecId::REX6) && + context.interpreter.input.target_address() == beneficiary; // All specs: the stack target (the value-transfer destination). - if target == beneficiary { + let hits_target = target == beneficiary; + if hits_source || hits_target { + // REX7 charge-on-reject: the static entry is paid even though the body never + // runs. Frozen specs keep the historical zero-charge reject. + if spec.is_enabled(MegaSpecId::REX7) { + charge_static_gas!(context, SELFDESTRUCT); + } revert_volatile_access_disabled!( context, SELFDESTRUCT, @@ -1975,11 +1987,18 @@ pub mod volatile_data_ext { /// for a frame that can afford them. /// /// A frame that *cannot* afford the charge is the case where that order shows: the body never - /// runs, so it never loads the target and never marks beneficiary access. That divergence is - /// unreachable from a wrapper. What is reachable is the tail: the detention cap below is - /// applied on every path out of this handler, including the out-of-gas one, so an access marked - /// by an already-returned inner frame is still propagated into the transaction's compute - /// budget. + /// runs, so it never loads the target and never marks beneficiary access. Through REX6 that + /// window is a frozen replay hazard (wontfix #20, tripwire below). REX7 specifies it: the mark + /// is produced when the target account is loaded, so a frame that cannot pay the pre-load fees + /// produces none. + /// + /// REX7 also charges the static entry on a disable rejection (charge-on-reject); frozen specs + /// still reject for free. The charge sits in the open segment and the frame-exit settlement + /// records it as compute. + /// + /// What is reachable on every spec is the tail: the detention cap below is applied on every + /// path out of this handler, including the out-of-gas one, so an access marked by an + /// already-returned inner frame is still propagated into the transaction's compute budget. macro_rules! wrap_call_volatile_check { ($fn_name:ident, $opcode:ident, $inner_fn:path) => { #[doc = concat!("`", stringify!($opcode), "` opcode with volatile data access disabled check for beneficiary.")] @@ -1991,19 +2010,21 @@ pub mod volatile_data_ext { context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { let inner_outcome: InstructionExecResult; + let spec = context.host.spec_id(); + let is_rex7 = spec.is_enabled(MegaSpecId::REX7); // Rex4+: If targeting the beneficiary while volatile access is disabled, revert before // executing the opcode to avoid polluting the tracker. Only this disabled path can // revert and only it needs the EIP-7702 delegate resolved, so the resolve (a DB read) // is gated behind the disabled check to keep it off the common (enabled) hot path — // enabled-access detention is marked by the host as the CALL loads the resolved // delegate. + let mut reject_disabled = false; if context.host.volatile_access_disabled() { // Peek the target address from the stack (position 1 for CALL-like opcodes: // stack layout is [gas_limit, to, ...]). if let Some(addr_word) = context.interpreter.stack.inspect::<1>() { let target: Address = addr_word.into_address(); let beneficiary = context.host.beneficiary_address(); - let spec = context.host.spec_id(); // The raw target already being the beneficiary observes beneficiary state // regardless of where it itself delegates, so check it first — `||` short-circuits // so no EIP-7702 delegate is resolved (and no DB read happens) in that case. @@ -2020,11 +2041,7 @@ pub mod volatile_data_ext { context.host.best_effort_resolve_eip7702_delegate_address(target) == beneficiary) { - revert_volatile_access_disabled!( - context, - $opcode, - VolatileDataAccessType::Beneficiary - ); + reject_disabled = true; } } } @@ -2035,14 +2052,31 @@ pub mod volatile_data_ext { let tripwire_target: Option
= context.interpreter.stack.inspect::<1>().map(|w| w.into_address()); - // Charged here rather than after the body — see the macro's doc comment. The charge - // does not return early: the detention tail below has to run on this path too. - const STATIC_GAS: u64 = static_gas(opcode::$opcode); - if !context.interpreter.gas.record_regular_cost(STATIC_GAS) { - #[cfg(debug_assertions)] - debug_check_frozen_detention_window(context.host, opcode::$opcode, tripwire_target); - apply_compute_gas_limit!(context); - return Err(InstructionResult::OutOfGas); + // REX7 charges the static entry even when the guard will reject, so the fee lands in + // the open segment and the revert does not refund it. Frozen specs keep charging only + // after the guard declines, which is the zero-charge reject the historical tests pin. + // Charged here rather than after the body on the success path — see the macro's doc + // comment. The charge does not return early: the detention tail below has to run on + // this path too. + if is_rex7 || !reject_disabled { + const STATIC_GAS: u64 = static_gas(opcode::$opcode); + if !context.interpreter.gas.record_regular_cost(STATIC_GAS) { + #[cfg(debug_assertions)] + debug_check_frozen_detention_window( + context.host, + opcode::$opcode, + tripwire_target, + ); + apply_compute_gas_limit!(context); + return Err(InstructionResult::OutOfGas); + } + } + if reject_disabled { + revert_volatile_access_disabled!( + context, + $opcode, + VolatileDataAccessType::Beneficiary + ); } // Delegate to the existing forward_gas_ext handler via reborrow so that @@ -2095,17 +2129,23 @@ pub mod volatile_data_ext { from the fully settled usage exactly as the per-opcode order applies it, and the epilogue re-clamps against the possibly-lowered headroom. - Each handler keeps charging the opcode's static gas at the position its per-opcode counterpart - charges it, because that position decides what an underfunded frame has already done when it - halts. + Each handler still charges the opcode's static gas at the position its per-opcode counterpart + charges it on the success path, because that position decides what an underfunded frame has + already done when it halts. The one REX7 change is charge-on-reject: the static entry is + debited before the disable guard, so a rejection still pays and the fee lands in the open + segment for the prologue (success) or the frame-exit settlement (reject) to record as compute. The frozen detention-window tripwire the per-opcode conditional wrapper carries is not repeated here: it watches for historical transactions whose replay would diverge across a revm - bump, and no such transaction can exist for a spec with no activation history. */ + bump, and no such transaction can exist for a spec with no activation history. The shared + CALL-family wrapper still has the tripwire; that copy is spec-gated so REX7 cannot trip it. */ - /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: disabled guard, prologue, static - /// gas ahead of the raw instruction (the position these opcodes' revm bodies charge from), - /// body recording, detention cap, epilogue. + /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: static gas, disabled guard, + /// prologue, raw instruction, body recording, detention cap, epilogue. + /// + /// The static entry is charged before the guard so a rejection still pays (REX7 + /// charge-on-reject). The debit sits in the open segment: a passing guard lets the prologue + /// settle it, a rejecting guard leaves it for the frame-exit settlement. macro_rules! wrap_checkpoint_detain_gas_unconditional { ($fn_name:ident, $opcode:ident, $original_fn:path, $access_type:expr) => { #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: segment settlement, raw instruction, gas detention, re-clamp.")] @@ -2113,12 +2153,12 @@ pub mod volatile_data_ext { pub fn $fn_name( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { + charge_static_gas!(context, $opcode); if context.host.volatile_access_disabled() { revert_volatile_access_disabled!(context, $opcode, $access_type); } checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); - charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); @@ -2133,6 +2173,10 @@ pub mod volatile_data_ext { /// instruction, static gas after it (the position these opcodes' revm bodies charge from, so an /// underfunded frame has already popped its operands and marked its access), body recording, /// detention cap, epilogue. + /// + /// A disable rejection charges the static entry first (REX7 charge-on-reject) and then reverts + /// without running the body, so the success-path charge-after-load order — and the mark that + /// load produces — is unchanged. macro_rules! wrap_checkpoint_detain_gas_conditional { ($fn_name:ident, $opcode:ident, $original_fn:path) => { #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: segment settlement, raw instruction, gas detention, re-clamp.")] @@ -2144,6 +2188,7 @@ pub mod volatile_data_ext { let target: Address = addr_word.into_address(); let beneficiary = context.host.beneficiary_address(); if target == beneficiary && context.host.volatile_access_disabled() { + charge_static_gas!(context, $opcode); revert_volatile_access_disabled!( context, $opcode, @@ -2241,13 +2286,16 @@ pub mod volatile_data_ext { ); /// `SLOAD` as a checkpoint. Same oracle-volatile handling as [`sload`], but the raw revm - /// instruction runs unwrapped and the open segment settles in the prologue. + /// instruction runs unwrapped and the open segment settles in the prologue. A disable + /// rejection charges the static entry first (REX7 charge-on-reject) without running the + /// load, so the success-path charge-after-load order is unchanged. #[inline] pub fn sload_checkpoint( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { let target = context.interpreter.input.target_address(); if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { + charge_static_gas!(context, SLOAD); revert_volatile_access_disabled!(context, SLOAD, VolatileDataAccessType::Oracle); } checkpoint_prologue!(context); @@ -2262,13 +2310,15 @@ pub mod volatile_data_ext { } /// `SELFBALANCE` as a checkpoint. Same beneficiary-volatile handling as [`selfbalance`], but - /// the raw revm instruction runs unwrapped and the open segment settles in the prologue. + /// the raw revm instruction runs unwrapped and the open segment settles in the prologue. The + /// static entry is charged before the guard (REX7 charge-on-reject). #[inline] pub fn selfbalance_checkpoint( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { let target = context.interpreter.input.target_address(); let beneficiary = context.host.beneficiary_address(); + charge_static_gas!(context, SELFBALANCE); if target == beneficiary && context.host.volatile_access_disabled() { revert_volatile_access_disabled!( context, @@ -2278,7 +2328,6 @@ pub mod volatile_data_ext { } checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); - charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); diff --git a/crates/mega-evm/tests/rex7/charge_on_reject.rs b/crates/mega-evm/tests/rex7/charge_on_reject.rs new file mode 100644 index 00000000..6f318eea --- /dev/null +++ b/crates/mega-evm/tests/rex7/charge_on_reject.rs @@ -0,0 +1,425 @@ +//! REX7 charge-on-reject for `disableVolatileDataAccess` guards. +//! +//! Through REX6 a volatile guard that rejects its opcode charges nothing: the static gas table +//! zeroes the entry so the interpreter cannot pre-empt the guard, and the handler charges only +//! after the check declines. REX7 keeps the zeroed table — the guard must still be reachable — +//! but charges the static entry before rejecting, so the revert keeps the fee and segment +//! settlement records it as compute. +//! +//! Each test runs the same rejected shape on both specs. REX6 remaining and compute stay at the +//! historical zero-charge amounts; REX7 remaining drops by exactly the opcode's static entry and +//! compute / receipt `gas_used` rise by that same amount. + +use std::convert::Infallible; + +use alloy_primitives::{address, Address, Bytes, U256}; +use alloy_sol_types::{SolCall, SolError}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + IMegaAccessControl, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, MegaTransactionNew as _, + TestExternalEnvs, VolatileDataAccessType, ACCESS_CONTROL_ADDRESS, ORACLE_CONTRACT_ADDRESS, +}; +use revm::{ + bytecode::opcode::{ + BALANCE, BLOCKHASH, CALL, POP, SELFBALANCE, SELFDESTRUCT, SLOAD, STATICCALL, TIMESTAMP, + }, + context::{tx::TxEnvBuilder, BlockEnv, ContextTr, TxEnv}, + handler::EvmTr, + inspector::Inspector, + interpreter::{ + interpreter_types::{InputsTr, Jumps}, + CallInputs, CallOutcome, InstructionResult, Interpreter, InterpreterTypes, + }, +}; + +const CALLER: Address = address!("0000000000000000000000000000000000310000"); +const PARENT: Address = address!("0000000000000000000000000000000000310001"); +const CHILD: Address = address!("0000000000000000000000000000000000310002"); +const BENEFICIARY: Address = address!("0000000000000000000000000000000000310099"); + +const DISABLE_SELECTOR: [u8; 4] = IMegaAccessControl::disableVolatileDataAccessCall::SELECTOR; + +const TIMESTAMP_STATIC_GAS: u64 = 2; +const BLOCKHASH_STATIC_GAS: u64 = 20; +const WARM_ACCESS_STATIC_GAS: u64 = 100; +const SELFBALANCE_STATIC_GAS: u64 = 5; +const SELFDESTRUCT_STATIC_GAS: u64 = 5_000; + +struct GuardedFrameOutcome { + remaining: u64, + result: InstructionResult, + output: Bytes, +} + +/// Records the gas the guarded frame held on reaching `opcode`, and the outcome that frame +/// returned. `step` runs before the handler, so `remaining_before` is the pre-charge budget. +struct GuardedFrameGasInspector { + frame: Address, + opcode: u8, + remaining_before: Option, + outcome: Option, +} + +impl GuardedFrameGasInspector { + fn new(frame: Address, opcode: u8) -> Self { + Self { frame, opcode, remaining_before: None, outcome: None } + } +} + +impl Inspector for GuardedFrameGasInspector { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if interp.input.target_address() == self.frame && interp.bytecode.opcode() == self.opcode { + self.remaining_before = Some(interp.gas.remaining()); + } + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address == self.frame { + self.outcome = Some(GuardedFrameOutcome { + remaining: outcome.result.gas.remaining(), + result: outcome.result.result, + output: outcome.result.output.clone(), + }); + } + } +} + +struct RejectedGuard { + remaining_before: u64, + remaining_after: u64, + compute_gas: u64, + gas_used: u64, + output: Bytes, +} + +fn call_disable(builder: BytecodeBuilder) -> BytecodeBuilder { + builder + .mstore(0x0, DISABLE_SELECTOR) + .push_number(0_u64) + .push_number(0_u64) + .push_number(4_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_address(ACCESS_CONTROL_ADDRESS) + .push_number(100_000_u64) + .append(CALL) + .append(POP) +} + +fn append_call(builder: BytecodeBuilder, target: Address, gas: u64) -> BytecodeBuilder { + builder + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_address(target) + .push_number(gas) + .append(CALL) +} + +fn transact_rejected( + spec: MegaSpecId, + db: &mut MemoryDatabase, + tx: TxEnv, + inspector: &mut GuardedFrameGasInspector, +) -> (bool, u64, u64) { + let external_envs = TestExternalEnvs::::new() + .with_oracle_storage(U256::from(0), U256::from(0x1234)); + let mut context = MegaContext::new(db, spec) + .with_block(BlockEnv { beneficiary: BENEFICIARY, ..Default::default() }) + .with_external_envs((&external_envs).into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut evm = MegaEvm::new(context).with_inspector(inspector); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx must execute"); + let compute_gas = evm.ctx_ref().additional_limit.borrow().get_usage().compute_gas; + (result.result.is_success(), compute_gas, result.result.tx_gas_used()) +} + +fn run_child_reject( + spec: MegaSpecId, + opcode: u8, + child: Address, + child_code: Bytes, +) -> RejectedGuard { + let parent_code = append_call(call_disable(BytecodeBuilder::default()), child, 50_000_000) + .append(POP) + .stop() + .build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(PARENT, parent_code) + .account_code(child, child_code); + let mut inspector = GuardedFrameGasInspector::new(child, opcode); + let (success, compute_gas, gas_used) = transact_rejected( + spec, + &mut db, + TxEnvBuilder::default().caller(CALLER).call(PARENT).gas_limit(100_000_000).build_fill(), + &mut inspector, + ); + assert!(success, "{spec}: only the guarded child should revert"); + take_rejected(spec, inspector, compute_gas, gas_used) +} + +fn run_selfbalance_reject(spec: MegaSpecId) -> RejectedGuard { + let code = + call_disable(BytecodeBuilder::default()).append(SELFBALANCE).append(POP).stop().build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(BENEFICIARY, code); + let mut inspector = GuardedFrameGasInspector::new(BENEFICIARY, SELFBALANCE); + let (_success, compute_gas, gas_used) = transact_rejected( + spec, + &mut db, + TxEnvBuilder::default() + .caller(CALLER) + .call(BENEFICIARY) + .gas_limit(100_000_000) + .build_fill(), + &mut inspector, + ); + take_rejected(spec, inspector, compute_gas, gas_used) +} + +fn take_rejected( + spec: MegaSpecId, + inspector: GuardedFrameGasInspector, + compute_gas: u64, + gas_used: u64, +) -> RejectedGuard { + let remaining_before = inspector + .remaining_before + .unwrap_or_else(|| panic!("{spec}: guarded opcode was never reached")); + let outcome = inspector.outcome.unwrap_or_else(|| panic!("{spec}: guarded frame never ended")); + assert_eq!( + outcome.result, + InstructionResult::Revert, + "{spec}: guard must revert, got {:?}", + outcome.result + ); + RejectedGuard { + remaining_before, + remaining_after: outcome.remaining, + compute_gas, + gas_used, + output: outcome.output, + } +} + +fn assert_charge_on_reject( + label: &str, + static_gas: u64, + access_type: VolatileDataAccessType, + r6: RejectedGuard, + r7: RejectedGuard, +) { + assert_charge_on_reject_inner(label, static_gas, access_type, r6, r7, true) +} + +/// Same as [`assert_charge_on_reject`], but skips the in-frame remaining-before check. +/// +/// Needed when the guarded frame is the transaction recipient and that recipient is the +/// beneficiary: REX7 clamps interpreter-visible gas to the detained headroom, so +/// `remaining_before` is the clamped counter and is not the amount the revert hands back. +fn assert_charge_on_reject_after_restore( + label: &str, + static_gas: u64, + access_type: VolatileDataAccessType, + r6: RejectedGuard, + r7: RejectedGuard, +) { + assert_charge_on_reject_inner(label, static_gas, access_type, r6, r7, false) +} + +fn assert_charge_on_reject_inner( + label: &str, + static_gas: u64, + access_type: VolatileDataAccessType, + r6: RejectedGuard, + r7: RejectedGuard, + check_in_frame_debit: bool, +) { + assert_eq!( + r6.remaining_after, r6.remaining_before, + "{label}: REX6 must reject without charging; held {} returned {}", + r6.remaining_before, r6.remaining_after + ); + if check_in_frame_debit { + assert_eq!( + r7.remaining_after, + r7.remaining_before - static_gas, + "{label}: REX7 must charge exactly {static_gas}; held {} returned {}", + r7.remaining_before, + r7.remaining_after + ); + } + assert_eq!( + r7.remaining_after, + r6.remaining_after - static_gas, + "{label}: REX7 true remaining must be REX6's minus {static_gas}; REX6={} REX7={}", + r6.remaining_after, + r7.remaining_after + ); + assert_eq!( + r7.compute_gas, + r6.compute_gas + static_gas, + "{label}: REX7 compute must include the rejected static fee; REX6={} REX7={}", + r6.compute_gas, + r7.compute_gas + ); + assert_eq!( + r7.gas_used, + r6.gas_used + static_gas, + "{label}: REX7 gas_used must include the rejected static fee; REX6={} REX7={}", + r6.gas_used, + r7.gas_used + ); + + let expected = IMegaAccessControl::VolatileDataAccessDisabled { accessType: access_type }; + let encoded = expected.abi_encode(); + assert_eq!(r6.output.as_ref(), encoded.as_slice(), "{label}: REX6 revert data"); + assert_eq!(r7.output.as_ref(), encoded.as_slice(), "{label}: REX7 revert data"); +} + +/// Unconditional block-env family: `TIMESTAMP` (static 2). +#[test] +fn test_rejected_timestamp_charges_static_gas_only_on_rex7() { + let code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build(); + let r6 = run_child_reject(MegaSpecId::REX6, TIMESTAMP, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, TIMESTAMP, CHILD, code); + assert_charge_on_reject( + "TIMESTAMP", + TIMESTAMP_STATIC_GAS, + VolatileDataAccessType::Timestamp, + r6, + r7, + ); +} + +/// Same family, different static entry, so a shared constant would fail here. +#[test] +fn test_rejected_blockhash_charges_static_gas_only_on_rex7() { + let code = + BytecodeBuilder::default().push_number(0_u64).append(BLOCKHASH).append(POP).stop().build(); + let r6 = run_child_reject(MegaSpecId::REX6, BLOCKHASH, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, BLOCKHASH, CHILD, code); + assert_charge_on_reject( + "BLOCKHASH", + BLOCKHASH_STATIC_GAS, + VolatileDataAccessType::BlockHash, + r6, + r7, + ); +} + +/// Beneficiary-conditional family: `BALANCE(beneficiary)` (static 100). +#[test] +fn test_rejected_balance_charges_static_gas_only_on_rex7() { + let code = BytecodeBuilder::default() + .push_address(BENEFICIARY) + .append(BALANCE) + .append(POP) + .stop() + .build(); + let r6 = run_child_reject(MegaSpecId::REX6, BALANCE, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, BALANCE, CHILD, code); + assert_charge_on_reject( + "BALANCE", + WARM_ACCESS_STATIC_GAS, + VolatileDataAccessType::Beneficiary, + r6, + r7, + ); +} + +/// CALL family: `CALL(beneficiary)` (static 100, the zeroed-table entry). +#[test] +fn test_rejected_call_charges_static_gas_only_on_rex7() { + let code = + append_call(BytecodeBuilder::default(), BENEFICIARY, 100_000).append(POP).stop().build(); + let r6 = run_child_reject(MegaSpecId::REX6, CALL, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, CALL, CHILD, code); + assert_charge_on_reject( + "CALL", + WARM_ACCESS_STATIC_GAS, + VolatileDataAccessType::Beneficiary, + r6, + r7, + ); +} + +/// CALL family, different arity: `STATICCALL(beneficiary)`. +#[test] +fn test_rejected_staticcall_charges_static_gas_only_on_rex7() { + let code = BytecodeBuilder::default() + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_address(BENEFICIARY) + .push_number(100_000_u64) + .append(STATICCALL) + .append(POP) + .stop() + .build(); + let r6 = run_child_reject(MegaSpecId::REX6, STATICCALL, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, STATICCALL, CHILD, code); + assert_charge_on_reject( + "STATICCALL", + WARM_ACCESS_STATIC_GAS, + VolatileDataAccessType::Beneficiary, + r6, + r7, + ); +} + +/// Oracle-conditional `SLOAD` (static 100). +#[test] +fn test_rejected_oracle_sload_charges_static_gas_only_on_rex7() { + let code = + BytecodeBuilder::default().push_number(0_u64).append(SLOAD).append(POP).stop().build(); + let r6 = run_child_reject(MegaSpecId::REX6, SLOAD, ORACLE_CONTRACT_ADDRESS, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, SLOAD, ORACLE_CONTRACT_ADDRESS, code); + assert_charge_on_reject( + "oracle SLOAD", + WARM_ACCESS_STATIC_GAS, + VolatileDataAccessType::Oracle, + r6, + r7, + ); +} + +/// `SELFBALANCE` in the beneficiary's own frame (static 5). A CALL into the beneficiary would +/// itself be rejected, so the transaction targets the beneficiary directly. +#[test] +fn test_rejected_selfbalance_charges_static_gas_only_on_rex7() { + let r6 = run_selfbalance_reject(MegaSpecId::REX6); + let r7 = run_selfbalance_reject(MegaSpecId::REX7); + assert_charge_on_reject_after_restore( + "SELFBALANCE", + SELFBALANCE_STATIC_GAS, + VolatileDataAccessType::Beneficiary, + r6, + r7, + ); +} + +/// `SELFDESTRUCT(beneficiary)` (static 5,000) — shared REX5+ wrapper, runtime-gated on REX7. +#[test] +fn test_rejected_selfdestruct_charges_static_gas_only_on_rex7() { + let code = BytecodeBuilder::default().push_address(BENEFICIARY).append(SELFDESTRUCT).build(); + let r6 = run_child_reject(MegaSpecId::REX6, SELFDESTRUCT, CHILD, code.clone()); + let r7 = run_child_reject(MegaSpecId::REX7, SELFDESTRUCT, CHILD, code); + assert_charge_on_reject( + "SELFDESTRUCT", + SELFDESTRUCT_STATIC_GAS, + VolatileDataAccessType::Beneficiary, + r6, + r7, + ); +} diff --git a/crates/mega-evm/tests/rex7/detention_window.rs b/crates/mega-evm/tests/rex7/detention_window.rs new file mode 100644 index 00000000..3645bf54 --- /dev/null +++ b/crates/mega-evm/tests/rex7/detention_window.rs @@ -0,0 +1,105 @@ +//! REX7 detention-mark timing: a frame that cannot afford the pre-load fees does not mark. +//! +//! revm 40 charges CALL-family static / value fees (and EXTCODECOPY's copy cost) before the +//! target account is loaded. Through REX6 that order is a frozen replay window: historical +//! revm 27 executions marked first, and `debug_check_frozen_detention_window` panics when a +//! debug replay hits the window (`tests/rex4/frozen_window_tripwire.rs`). REX7 has no history +//! and specifies the new order: the mark is produced when the target account is loaded, so +//! these shapes must stay unmarked and the tripwire — now spec-gated — must stay silent. + +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + MegaContext, MegaEvm, MegaSpecId, MegaTransaction, MegaTransactionNew as _, +}; +use revm::{ + bytecode::opcode::{CALL, EXTCODECOPY}, + context::{tx::TxEnvBuilder, BlockEnv, TxEnv}, + handler::EvmTr, +}; + +const CALLER: Address = address!("0000000000000000000000000000000000320000"); +const OUTER: Address = address!("0000000000000000000000000000000000320001"); +const INNER: Address = address!("0000000000000000000000000000000000320002"); +const BENEFICIARY: Address = address!("0000000000000000000000000000000000320099"); + +fn append_call(builder: BytecodeBuilder, target: Address, gas: u64, value: u64) -> BytecodeBuilder { + builder + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_number(value) + .push_address(target) + .push_number(gas) + .append(CALL) +} + +fn inner_window_call(target: Address, value: u64) -> Bytes { + append_call(BytecodeBuilder::default(), target, 0, value).build() +} + +fn inner_window_extcodecopy(target: Address) -> Bytes { + BytecodeBuilder::default() + .push_number(0x8000_u64) + .push_number(0_u64) + .push_number(0_u64) + .push_address(target) + .append(EXTCODECOPY) + .build() +} + +fn window_db(inner_code: Bytes, inner_budget: u64) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000_000_u64)) + .account_code( + OUTER, + append_call(BytecodeBuilder::default(), INNER, inner_budget, 0).build(), + ) + .account_code(INNER, inner_code) +} + +fn transact_rex7(db: &mut MemoryDatabase, tx: TxEnv) -> bool { + let block = BlockEnv { beneficiary: BENEFICIARY, ..Default::default() }; + let mut context = MegaContext::new(db, MegaSpecId::REX7).with_block(block); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let mut evm = MegaEvm::new(context); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx must execute"); + let tracker = evm.ctx_ref().volatile_data_tracker.borrow(); + let marked = tracker.has_accessed_beneficiary_balance(); + drop(tracker); + marked +} + +fn default_tx() -> TxEnv { + TxEnvBuilder::default().caller(CALLER).call(OUTER).gas_limit(1_000_000).build_fill() +} + +/// Static-charge window: INNER reaches CALL holding fewer than 100 gas, so the load never runs. +#[test] +fn test_rex7_underfunded_call_to_beneficiary_does_not_mark() { + let mut db = window_db(inner_window_call(BENEFICIARY, 0), 80); + let marked = transact_rex7(&mut db, default_tx()); + assert!(!marked, "REX7 specifies charge-before-load: an underfunded CALL must not mark"); +} + +/// Value-transfer window: the frame affords the 100 static charge but not the 9,000 transfer cost. +#[test] +fn test_rex7_underfunded_value_call_to_beneficiary_does_not_mark() { + let mut db = window_db(inner_window_call(BENEFICIARY, 1), 800); + let marked = transact_rex7(&mut db, default_tx()); + assert!(!marked, "REX7 specifies charge-before-load: an underfunded value CALL must not mark"); +} + +/// EXTCODECOPY window: the copy cost sits before the load, so a poor frame never marks. +#[test] +fn test_rex7_underfunded_extcodecopy_of_beneficiary_does_not_mark() { + let mut db = window_db(inner_window_extcodecopy(BENEFICIARY), 100); + let marked = transact_rex7(&mut db, default_tx()); + assert!(!marked, "REX7 specifies charge-before-load: an underfunded EXTCODECOPY must not mark"); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 0c286b6b..a4562c11 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -26,12 +26,20 @@ //! - `burn_split` — which half of that budget enforces: the work the frame performed does, the //! remainder it destroyed does not, and both boundaries (a checkpoint's storage charge, revm's //! post-action create rejects) land on the right side. +//! - `charge_on_reject` — a `disableVolatileDataAccess` rejection still pays the opcode's static +//! fee, which segment settlement records as compute; REX6 keeps the historical zero-charge +//! revert. +//! - `detention_window` — an underfunded CALL / EXTCODECOPY that OOGs before the target load does +//! not mark beneficiary access; that order is specified, so the frozen-window tripwire stays +//! silent. mod burn_split; +mod charge_on_reject; mod checkpoint_families; mod checkpoint_settlement; mod clamp_classification; mod common; +mod detention_window; mod double_exceed_corner; mod exceptional_halt; mod gas_clamp; From df2d9c3d05769cab3a3b82d67abd327597aeb3c0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 09:56:04 +0800 Subject: [PATCH 047/208] docs(rex7): specify charge-on-reject and mark-at-load Record the two remaining REX7 deviations from the checkpoint plan: a disableVolatileDataAccess reject pays the opcode's static fee, and a detention mark is produced when the target account is loaded. --- AGENTS.md | 2 ++ docs/spec/evm/compute-gas.md | 2 +- docs/spec/evm/gas-detention.md | 24 +++++++++++++++- docs/spec/upgrades/overview.md | 3 +- docs/spec/upgrades/rex7.md | 52 +++++++++++++++++++++++++++++----- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f09299a8..3d95458d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -150,6 +150,8 @@ MegaETH's parallel EVM needs to minimize conflicts between concurrent transactio - Different volatile data categories (block env/beneficiary, oracle) have different cap levels defined in `constants.rs`. - The **most restrictive cap wins** when multiple volatile sources are accessed. - Caps are applied via host hooks (`evm/host.rs`) that mark access in a `VolatileDataAccessTracker` (`access/tracker.rs`), then enforced after each volatile opcode via `wrap_op_detain_gas!` in `evm/instructions.rs`. +- REX7 charges the opcode's static fee even when `disableVolatileDataAccess` rejects (charge-on-reject); frozen specs still reject for free. +- REX7 specifies that a detention mark is produced when the target account is loaded, so a frame that cannot afford the pre-load CALL / EXTCODECOPY fees produces no mark (the frozen-window tripwire is `!REX7`-gated). This forces transactions that touch volatile data to terminate quickly, reducing parallel execution conflicts without banning the access outright. Detained gas is effectively refunded — users only pay for actual computation performed. diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index ed6e7035..ee9b2210 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -473,7 +473,7 @@ At each checkpoint a node MUST: Non-opcode recording sites on this page (intrinsic gas, precompiles, code deposit, KeylessDeploy) are unchanged. -For every transaction that stays within every runtime resource limit and in which no frame ends in an exceptional halt, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. +For every transaction that stays within every runtime resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. #### Gas-clamp enforcement diff --git a/docs/spec/evm/gas-detention.md b/docs/spec/evm/gas-detention.md index d8c5bdac..e3ee277e 100644 --- a/docs/spec/evm/gas-detention.md +++ b/docs/spec/evm/gas-detention.md @@ -165,6 +165,28 @@ The CALL-family opcodes are excluded from this registration guarantee: their bas `EXTCODECOPY` is excluded for the same reason: its copy cost is charged before the target account is read, so only a frame that affords the copy cost registers the access. An access blocked by [`disableVolatileDataAccess()`](../system-contracts/mega-access-control.md) is the exception: the blocked opcode never runs, so it reads nothing and triggers nothing. +
+Rex7 (unstable): detention mark at account load + +Under Rex7 the CALL-family / `EXTCODECOPY` charge-before-load order is specified, not a frozen replay window. +A node MUST produce the beneficiary (or oracle) mark when the target account or slot is loaded, and MUST NOT produce that mark from a frame that cannot afford the fees charged before the load. +A CALL that exhausts the frame on its static fee or value-transfer fee, and an `EXTCODECOPY` that exhausts the frame on its copy fee, therefore halt without detaining the rest of the transaction. +See the [Rex7 Network Upgrade](../upgrades/rex7.md). + +
+ +
+Rex7 (unstable): charge-on-reject for disabled volatile access + +Under Rex7 a node MUST still revert a `disableVolatileDataAccess` rejection with `VolatileDataAccessDisabled` and MUST still leave the tracker unmarked. +A node MUST charge the rejected opcode's static fee before that revert. +The fee is ordinary EVM gas, is not refunded by the synthetic revert, and is recorded as compute gas when the open segment is settled. +A frame that cannot afford the static fee MUST halt out of gas instead of reaching the disable revert. +Through Rex6 the same reject charges nothing. +See the [Rex7 Network Upgrade](../upgrades/rex7.md). + +
+ ## Constants | Constant | Value | Description | @@ -213,4 +235,4 @@ Gas detention semantics evolved across specs: - [Rex3](../upgrades/rex3.md) — raised oracle cap to 20M and changed oracle detection from CALL-based to SLOAD-based - [Rex4](../upgrades/rex4.md) — changes absolute detention to relative detention and adds additional beneficiary-triggered behavior - [Rex6](../upgrades/rex6.md) — adds a beneficiary-detention trigger for an applied EIP-7702 authorization whose authority equals the block beneficiary; resolves a CALL-family target's EIP-7702 delegation one hop before the beneficiary comparison, so a call through a delegator whose delegate is the beneficiary triggers detention (through Rex5 only the raw target is compared); and stops enforcing the detention cap against system-originated transactions, whose volatile accesses are still tracked -- [Rex7](../upgrades/rex7.md) _(unstable)_ — enforces the detained limit inside plain-opcode segments by gas clamping, stopping a crossing opcode before it executes while preserving `VolatileDataAccessOutOfGas` and gas rescue +- [Rex7](../upgrades/rex7.md) _(unstable)_ — enforces the detained limit inside plain-opcode segments by gas clamping, stopping a crossing opcode before it executes while preserving `VolatileDataAccessOutOfGas` and gas rescue; specifies that a detention mark is produced when the target account is loaded, so a frame that cannot afford the pre-load fees produces no mark; and charges the static fee of an opcode rejected by `disableVolatileDataAccess` diff --git a/docs/spec/upgrades/overview.md b/docs/spec/upgrades/overview.md index d19dad89..7d2a67bf 100644 --- a/docs/spec/upgrades/overview.md +++ b/docs/spec/upgrades/overview.md @@ -153,7 +153,8 @@ Not yet scheduled {% endtabs %} Unstable; under active development. -Checkpoint-settled [compute gas](../glossary.md#compute-gas) accounting with gas-clamp enforcement: plain opcodes record nothing between checkpoints; within-limit transactions that never end a frame in an exceptional halt stay bit-identical to Rex6; a compute-gas or detention exceed inside a plain segment stops the crossing opcode before it executes. +Checkpoint-settled [compute gas](../glossary.md#compute-gas) accounting with gas-clamp enforcement: plain opcodes record nothing between checkpoints; within-limit transactions that never end a frame in an exceptional halt and never trip a `disableVolatileDataAccess` guard stay bit-identical to Rex6; a compute-gas or detention exceed inside a plain segment stops the crossing opcode before it executes. +A disabled-volatile rejection charges the opcode's static fee; a detention mark is produced when the target account is loaded. ## How to Read These Pages diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index a2132726..09f429e3 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -1,5 +1,5 @@ --- -description: Rex7 network upgrade — checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints, within-limit transactions that never end a frame in an exceptional halt stay bit-identical to Rex6, and limit-exceeding opcodes are stopped before they execute. +description: Rex7 network upgrade — checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints, within-limit transactions that never end a frame in an exceptional halt and never trip a disableVolatileDataAccess guard stay bit-identical to Rex6, a disabled-volatile rejection charges the opcode's static fee, and a detention mark is produced when the target account is loaded. --- # Rex7 Network Upgrade @@ -21,9 +21,14 @@ Rex7 replaces that per-opcode recording for ordinary opcodes with **checkpoint s Rex7 also introduces **gas-clamp enforcement**: between checkpoints the node restricts the interpreter-visible remaining gas to the remaining compute headroom, so the inherited EVM's own per-opcode gas check stops a limit-crossing opcode before that opcode executes. -For a transaction that never crosses a compute-gas, detention, or other resource limit and in which no frame ends in an exceptional halt, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. +For a transaction that never crosses a compute-gas, detention, or other resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, Rex7 is bit-identical to Rex6: the same gas, the same receipt, the same state, and the same `GAS` opcode readings. For a transaction that does cross a compute-gas or detention limit inside a plain-opcode segment, the halt lands before the crossing opcode rather than after it, the crossing opcode's cost is excluded from recorded compute usage, and remaining gas remains refundable under the same rescue rules as other transaction-level compute-limit halts. +Rex7 also makes two guard- and detention-related choices that Rex6 does not: + +- A `disableVolatileDataAccess` rejection still charges the rejected opcode's static fee. +- A detention mark is produced when the target account is loaded, so a frame that cannot afford the fees charged before that load produces no mark. + One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. @@ -67,8 +72,8 @@ At each checkpoint a node MUST: Non-opcode recording sites (transaction intrinsic gas, precompiles, contract-creation code deposit, KeylessDeploy overhead and sandbox merge) are unchanged. **Precision invariant.** -For every transaction that stays within every runtime resource limit and in which no frame ends in an exceptional halt, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. -The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed and no frame ends in an exceptional halt. +For every transaction that stays within every runtime resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. +The interpreter's gas counter already meters every opcode; settling by segment reproduces the per-opcode sum exactly when no limit is crossed, no frame ends in an exceptional halt, and no rejected guard charges a static fee. **Exceptional-halt frame carve-out.** A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. @@ -147,6 +152,37 @@ The two cases are indistinguishable once the frame has already reported out-of-g **Within-limit observability.** For a transaction that never crosses a compute or detention limit, the clamp MUST be unobservable: `GAS` returns the true remaining gas, call forwarding and storage-gas charges see the true counter, and gas, receipt, and state match Rex6. +### Charge-on-Reject for Disabled Volatile Access + +#### Previous behavior + +Through [Rex6](rex6.md), a node that has `disableVolatileDataAccess` active rejects a volatile-guarded opcode with a revert and the `VolatileDataAccessDisabled` payload, and charges the rejected opcode nothing. +The static gas table zeroes those opcodes so a frame holding less than the static fee still reaches the guard, and the handler charges the entry only after the check declines. +The reverting frame returns every unit of gas it held when it reached the opcode. + +#### New behavior + +Under Rex7, a node MUST still reject the same opcodes with the same revert payload, and MUST still leave the tracker unmarked. +A node MUST charge the rejected opcode's static fee before producing that revert. +The fee is ordinary EVM gas: it is debited from the frame, it is not refunded by the synthetic revert, and it MUST be recorded as compute gas when the open segment is settled — at the next checkpoint if the guard then passes, or at frame exit if it rejects. +A frame that cannot afford the static fee MUST halt out of gas instead of reaching the disable revert. + +The guarded set is unchanged from Rex6: the unconditional block-environment opcodes, the beneficiary-conditional account reads, `SELFBALANCE`, oracle-conditional `SLOAD`, the CALL family, and `SELFDESTRUCT`. + +### Detention Mark at Account Load + +#### Previous behavior + +Through Rex6, a node documents that `BALANCE`, `EXTCODESIZE`, `EXTCODEHASH`, `SLOAD`, and `SELFDESTRUCT` register a volatile access even when the frame then runs out of gas on that opcode's own cost. +The CALL family and `EXTCODECOPY` are excluded from that guarantee because their implementation charges the base access cost — and, for a value-transferring call, the transfer cost — or the copy cost before the target account is read. +That exclusion is a frozen replay window, not a Rex6 rule: historical executions under the previous interpreter loaded first and marked first. + +#### New behavior + +Under Rex7, a node MUST produce a beneficiary or oracle detention mark when the target account (or oracle slot) is loaded, and MUST NOT produce that mark from a frame that cannot afford the fees charged before the load. +A CALL-family opcode whose static fee or value-transfer fee exhausts the frame, and an `EXTCODECOPY` whose copy cost exhausts the frame, therefore halt without marking, and the rest of the transaction runs undetained unless some other access has already marked. +This is specified behavior, not a replay exception. + ## Developer Impact Rex7 is not scheduled on any network. @@ -154,8 +190,10 @@ Its semantics may still change before it is frozen. Contracts and tools that assume per-opcode compute-gas attribution for every instruction MUST treat that assumption as false under Rex7: only checkpoints settle compute gas during execution, and a plain-opcode segment has no intermediate recording. -Contracts that stay within every resource limit and never end a frame in an exceptional halt see no behavioral change relative to Rex6. +Contracts that stay within every resource limit, never end a frame in an exceptional halt, and never trip a `disableVolatileDataAccess` guard see no behavioral change relative to Rex6. Contracts that trip the compute-gas or detention limit inside a plain-opcode segment halt one opcode earlier than under Rex6, with the crossing opcode excluded from recorded compute usage and with remaining gas still refundable on a transaction-level halt. +A contract that disables volatile access and then hits a guarded opcode pays that opcode's static fee under Rex7 and gets the same revert payload; through Rex6 that reject cost nothing. +A CALL or `EXTCODECOPY` that cannot afford the fees charged before the target account is loaded does not detain the rest of the transaction. A transaction that halts exceptionally, or that calls into a child frame which does, may report a higher transaction-level compute-gas total under Rex7 than under Rex6 — for any exceptional halt, not just out-of-gas. The receipt `gas_used`, the halt or revert reported, and the execution success or failure of the outer transaction are unchanged by the destroyed half of that carve-out: it is reported, never enforced. @@ -171,8 +209,8 @@ Any node, tool, or test fixture pinned to Rex7 must expect its results to move. A deployment that needs stable semantics must select a frozen spec explicitly rather than relying on the latest one. The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and enforced usage does not pass the limit by that opcode's cost. -The exceptional-halt frame carve-out is the only path on which Rex7 can report more compute gas than Rex6 for the same inputs; it over-reports rather than under-reports. -Its enforcing half is never looser than Rex6's, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. +Rex7 can report more compute gas than Rex6 for the same inputs on two paths: the exceptional-halt frame carve-out, which over-reports rather than under-reports, and a `disableVolatileDataAccess` rejection, which now includes the rejected opcode's static fee. +The carve-out's enforcing half is never looser than Rex6's, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. ## References From 95a52f6df38d2520330d7938ca7b97a06ba2e7d6 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 10:46:20 +0800 Subject: [PATCH 048/208] fix(evm): charge REX7 reject static gas only on the reject arm Restore the guard-pass checkpoint order so the static fee is taken after the prologue restores true gas. Charge-on-reject stays on the disable arm only, matching the baseline REX7 body position. --- crates/mega-evm/src/evm/instructions.rs | 38 +- .../mega-evm/tests/rex7/charge_on_reject.rs | 195 ++++++++++ .../tests/rex7/guard_pass_static_gas.rs | 365 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + docs/spec/upgrades/rex7.md | 3 +- 5 files changed, 588 insertions(+), 17 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/guard_pass_static_gas.rs diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 02810080..6645564d 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1542,10 +1542,11 @@ charged via [`charge_static_gas`] only after the check has declined to fire. That is what lets the rejection be a revert that keeps the frame's whole remaining gas even when that gas would not have covered the opcode's static cost. -REX7 charges the static entry first and then runs the same check: a rejection is still a revert -with the same payload, but the static fee stays charged and is settled into compute gas as part of -the open segment. A frame that cannot afford the static fee runs out of gas instead of reaching -the revert. Frozen specs are unchanged. +REX7 charges the static entry on a rejection, before producing the revert: the payload is +unchanged, but the static fee stays charged and is settled into compute gas with the open +segment at frame exit. A passing guard still charges at the success-path position (after the +checkpoint prologue has restored the true counter). A frame that cannot afford the static fee +runs out of gas instead of reaching the revert. Frozen specs are unchanged. # Where the Static Gas Lands Once the Guard Declines @@ -2131,21 +2132,23 @@ pub mod volatile_data_ext { Each handler still charges the opcode's static gas at the position its per-opcode counterpart charges it on the success path, because that position decides what an underfunded frame has - already done when it halts. The one REX7 change is charge-on-reject: the static entry is - debited before the disable guard, so a rejection still pays and the fee lands in the open - segment for the prologue (success) or the frame-exit settlement (reject) to record as compute. + already done when it halts. The one REX7 change is charge-on-reject: a disable rejection + debits the static entry and then reverts, so the fee lands in the open segment for the + frame-exit settlement. A passing guard is unchanged — prologue, `gas_before`, then the + body charge — so the static fee is taken on the restored true counter. The frozen detention-window tripwire the per-opcode conditional wrapper carries is not repeated here: it watches for historical transactions whose replay would diverge across a revm bump, and no such transaction can exist for a spec with no activation history. The shared CALL-family wrapper still has the tripwire; that copy is spec-gated so REX7 cannot trip it. */ - /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: static gas, disabled guard, - /// prologue, raw instruction, body recording, detention cap, epilogue. + /// Checkpoint form of [`wrap_op_detain_gas_unconditional`]: disabled guard, prologue, static + /// gas ahead of the raw instruction (the position these opcodes' revm bodies charge from), + /// body recording, detention cap, epilogue. /// - /// The static entry is charged before the guard so a rejection still pays (REX7 - /// charge-on-reject). The debit sits in the open segment: a passing guard lets the prologue - /// settle it, a rejecting guard leaves it for the frame-exit settlement. + /// A disable rejection charges the static entry first (REX7 charge-on-reject) and then reverts + /// without running the prologue or the body. A passing guard is the baseline order: prologue, + /// `gas_before`, then the charge, so the static fee is taken on the restored true counter. macro_rules! wrap_checkpoint_detain_gas_unconditional { ($fn_name:ident, $opcode:ident, $original_fn:path, $access_type:expr) => { #[doc = concat!("`", stringify!($opcode), "` opcode as a checkpoint: segment settlement, raw instruction, gas detention, re-clamp.")] @@ -2153,12 +2156,13 @@ pub mod volatile_data_ext { pub fn $fn_name( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { - charge_static_gas!(context, $opcode); if context.host.volatile_access_disabled() { + charge_static_gas!(context, $opcode); revert_volatile_access_disabled!(context, $opcode, $access_type); } checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); + charge_static_gas!(context, $opcode); run_inner_instruction_or_abort!($original_fn, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); @@ -2310,16 +2314,17 @@ pub mod volatile_data_ext { } /// `SELFBALANCE` as a checkpoint. Same beneficiary-volatile handling as [`selfbalance`], but - /// the raw revm instruction runs unwrapped and the open segment settles in the prologue. The - /// static entry is charged before the guard (REX7 charge-on-reject). + /// the raw revm instruction runs unwrapped and the open segment settles in the prologue. A + /// disable rejection charges the static entry first (REX7 charge-on-reject); a passing guard + /// charges after the prologue, at the same position as the baseline handler. #[inline] pub fn selfbalance_checkpoint( context: InstructionContext<'_, H, WIRE>, ) -> InstructionExecResult { let target = context.interpreter.input.target_address(); let beneficiary = context.host.beneficiary_address(); - charge_static_gas!(context, SELFBALANCE); if target == beneficiary && context.host.volatile_access_disabled() { + charge_static_gas!(context, SELFBALANCE); revert_volatile_access_disabled!( context, SELFBALANCE, @@ -2328,6 +2333,7 @@ pub mod volatile_data_ext { } checkpoint_prologue!(context); let gas_before = context.interpreter.gas.remaining(); + charge_static_gas!(context, SELFBALANCE); run_inner_instruction_or_abort!(instructions::host::selfbalance, context, inner_outcome); record_checkpoint_body_compute_gas!(context, gas_before, detention_tail); diff --git a/crates/mega-evm/tests/rex7/charge_on_reject.rs b/crates/mega-evm/tests/rex7/charge_on_reject.rs index 6f318eea..6774daf7 100644 --- a/crates/mega-evm/tests/rex7/charge_on_reject.rs +++ b/crates/mega-evm/tests/rex7/charge_on_reject.rs @@ -9,6 +9,10 @@ //! Each test runs the same rejected shape on both specs. REX6 remaining and compute stay at the //! historical zero-charge amounts; REX7 remaining drops by exactly the opcode's static entry and //! compute / receipt `gas_used` rise by that same amount. +//! +//! A second set of tests pins the unaffordable-static-fee branch: the guarded frame is given +//! just enough gas to reach the opcode and not enough to pay the entry, so the result is +//! `OutOfGas` rather than a `VolatileDataAccessDisabled` revert. use std::convert::Infallible; @@ -409,6 +413,197 @@ fn test_rejected_selfbalance_charges_static_gas_only_on_rex7() { ); } +/// A child that reaches `opcode` holding less than its static fee must OOG rather than +/// produce the disable revert. `forward_gas` is the CALL stipend the parent hands the child +/// — enough to reach the opcode, not enough to pay the entry. +fn run_child_underfunded( + opcode: u8, + child: Address, + child_code: Bytes, + forward_gas: u64, +) -> (u64, GuardedFrameOutcome) { + let parent_code = append_call(call_disable(BytecodeBuilder::default()), child, forward_gas) + .append(POP) + .stop() + .build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(PARENT, parent_code) + .account_code(child, child_code); + let mut inspector = GuardedFrameGasInspector::new(child, opcode); + let (success, _compute_gas, _gas_used) = transact_rejected( + MegaSpecId::REX7, + &mut db, + TxEnvBuilder::default().caller(CALLER).call(PARENT).gas_limit(100_000_000).build_fill(), + &mut inspector, + ); + assert!(success, "only the underfunded child should fail"); + let remaining_before = inspector + .remaining_before + .unwrap_or_else(|| panic!("underfunded opcode {opcode} was never reached")); + let outcome = inspector.outcome.unwrap_or_else(|| panic!("underfunded frame never ended")); + (remaining_before, outcome) +} + +fn assert_oog_not_disable_revert( + label: &str, + static_gas: u64, + remaining_before: u64, + outcome: &GuardedFrameOutcome, +) { + assert!( + remaining_before < static_gas, + "{label}: remaining_before={remaining_before} must be below static {static_gas}" + ); + assert_eq!( + outcome.result, + InstructionResult::OutOfGas, + "{label}: unaffordable static fee must OOG, got {:?}", + outcome.result + ); + assert!( + outcome.output.is_empty(), + "{label}: OOG must not carry a disable-revert payload, got {:?}", + outcome.output + ); +} + +/// Unconditional family: child holds 1 gas, `TIMESTAMP` costs 2. +#[test] +fn test_rejected_timestamp_oog_when_static_gas_unaffordable() { + let code = BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build(); + let (remaining_before, outcome) = run_child_underfunded(TIMESTAMP, CHILD, code, 1); + assert_oog_not_disable_revert("TIMESTAMP", TIMESTAMP_STATIC_GAS, remaining_before, &outcome); +} + +/// Same family, larger static entry: `PUSH1 0` (3) then `BLOCKHASH` (20), forwarded 4. +#[test] +fn test_rejected_blockhash_oog_when_static_gas_unaffordable() { + let code = + BytecodeBuilder::default().push_number(0_u64).append(BLOCKHASH).append(POP).stop().build(); + let (remaining_before, outcome) = run_child_underfunded(BLOCKHASH, CHILD, code, 4); + assert_oog_not_disable_revert("BLOCKHASH", BLOCKHASH_STATIC_GAS, remaining_before, &outcome); +} + +/// Conditional account-read family: `PUSH20 beneficiary` (3) then `BALANCE` (100), forwarded 4. +#[test] +fn test_rejected_balance_oog_when_static_gas_unaffordable() { + let code = BytecodeBuilder::default() + .push_address(BENEFICIARY) + .append(BALANCE) + .append(POP) + .stop() + .build(); + let (remaining_before, outcome) = run_child_underfunded(BALANCE, CHILD, code, 4); + assert_oog_not_disable_revert("BALANCE", WARM_ACCESS_STATIC_GAS, remaining_before, &outcome); +} + +/// CALL family: setup is five `PUSH1 0` + `PUSH20` + `PUSH3` (21), then `CALL` (100). +#[test] +fn test_rejected_call_oog_when_static_gas_unaffordable() { + let code = + append_call(BytecodeBuilder::default(), BENEFICIARY, 100_000).append(POP).stop().build(); + let (remaining_before, outcome) = + run_child_underfunded(CALL, CHILD, code, 21 + WARM_ACCESS_STATIC_GAS - 1); + assert_oog_not_disable_revert("CALL", WARM_ACCESS_STATIC_GAS, remaining_before, &outcome); +} + +/// Oracle `SLOAD`: `PUSH1 0` (3) then `SLOAD` (100), forwarded 4. +#[test] +fn test_rejected_oracle_sload_oog_when_static_gas_unaffordable() { + let code = + BytecodeBuilder::default().push_number(0_u64).append(SLOAD).append(POP).stop().build(); + let (remaining_before, outcome) = + run_child_underfunded(SLOAD, ORACLE_CONTRACT_ADDRESS, code, 4); + assert_oog_not_disable_revert( + "oracle SLOAD", + WARM_ACCESS_STATIC_GAS, + remaining_before, + &outcome, + ); +} + +/// `SELFDESTRUCT`: `PUSH20` (3) then static `5000`, forwarded 4. +#[test] +fn test_rejected_selfdestruct_oog_when_static_gas_unaffordable() { + let code = BytecodeBuilder::default().push_address(BENEFICIARY).append(SELFDESTRUCT).build(); + let (remaining_before, outcome) = run_child_underfunded(SELFDESTRUCT, CHILD, code, 4); + assert_oog_not_disable_revert( + "SELFDESTRUCT", + SELFDESTRUCT_STATIC_GAS, + remaining_before, + &outcome, + ); +} + +/// `SELFBALANCE` is only rejected in the beneficiary's own frame. `GAS` after `disable` +/// stores the true remaining; the transaction gas limit is then cut so the frame holds +/// `static − 1` at `SELFBALANCE`. +#[test] +fn test_rejected_selfbalance_oog_when_static_gas_unaffordable() { + use revm::bytecode::opcode::{GAS, SSTORE}; + const HIGH_LIMIT: u64 = 100_000_000; + const GAS_SLOT: u64 = 0x67; + + let calibrate_code = call_disable(BytecodeBuilder::default()) + .append(GAS) + .push_u256(U256::from(GAS_SLOT)) + .append(SSTORE) + .stop() + .build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(BENEFICIARY, calibrate_code); + let external_envs = TestExternalEnvs::::new(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_block(BlockEnv { beneficiary: BENEFICIARY, ..Default::default() }) + .with_external_envs((&external_envs).into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = + TxEnvBuilder::default().caller(CALLER).call(BENEFICIARY).gas_limit(HIGH_LIMIT).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).expect("calibration must execute"); + assert!(result.result.is_success(), "calibration run must succeed: {:?}", result.result); + let true_after_gas: u64 = result + .state + .get(&BENEFICIARY) + .and_then(|account| account.storage.get(&U256::from(GAS_SLOT))) + .map(|slot| slot.present_value()) + .expect("calibration SSTORE must land") + .try_into() + .expect("remaining fits in u64"); + // `GAS` charges 2 and then pushes the post-charge remaining. `SELFBALANCE` sits where + // `GAS` sat, so the true remaining there is that reading plus 2. + let true_at_selfbalance = true_after_gas + 2; + let gas_limit = HIGH_LIMIT - true_at_selfbalance + (SELFBALANCE_STATIC_GAS - 1); + + let code = + call_disable(BytecodeBuilder::default()).append(SELFBALANCE).append(POP).stop().build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(BENEFICIARY, code); + let mut inspector = GuardedFrameGasInspector::new(BENEFICIARY, SELFBALANCE); + let _ = transact_rejected( + MegaSpecId::REX7, + &mut db, + TxEnvBuilder::default().caller(CALLER).call(BENEFICIARY).gas_limit(gas_limit).build_fill(), + &mut inspector, + ); + let remaining_before = inspector.remaining_before.expect("SELFBALANCE must be reached"); + let outcome = inspector.outcome.expect("beneficiary frame must end"); + assert_oog_not_disable_revert( + "SELFBALANCE", + SELFBALANCE_STATIC_GAS, + remaining_before, + &outcome, + ); +} + /// `SELFDESTRUCT(beneficiary)` (static 5,000) — shared REX5+ wrapper, runtime-gated on REX7. #[test] fn test_rejected_selfdestruct_charges_static_gas_only_on_rex7() { diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs new file mode 100644 index 00000000..525c6e91 --- /dev/null +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -0,0 +1,365 @@ +//! Guard-pass static-gas position under REX7 checkpoint accounting. +//! +//! Charge-on-reject debits a disabled opcode's static entry on the reject arm only. A passing +//! guard must charge after [`checkpoint_prologue!`] restores the true counter — the same +//! position the baseline REX7 handler used — so a compute headroom of `static_gas − 1` lets +//! the body run and reports a frame-local `MegaLimitExceeded` rather than a clamp halt that +//! never reads the host. +//! +//! `headroom = static_gas − 1` and `headroom = static_gas` are the two edges: the first +//! overshoots after the body, the second can afford the charge. Each is exercised in the +//! top-level frame and in a nested child. `remainingComputeGas` is the value +//! `MegaLimitControl.remainingComputeGas` would return after the transaction: the TX-level +//! remaining, which is what the interceptor reads once the frames have been popped. + +use crate::common::{transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use alloy_sol_types::{SolCall as _, SolError}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IMegaLimitControl, LimitKind, MegaContext, MegaEvm, MegaLimitExceeded, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, VolatileDataAccess, + LIMIT_CONTROL_ADDRESS, +}; +use revm::{ + bytecode::opcode::{CALL, MLOAD, POP, SSTORE, STATICCALL, STOP, TIMESTAMP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + handler::EvmTr, +}; + +/// `TIMESTAMP` static gas — the unconditional-family representative. +const TIMESTAMP_STATIC_GAS: u64 = 2; + +/// Slot the nested caller stores its `remainingComputeGas` reading into. +const REMAINING_SLOT: u64 = 0xc0; + +fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) +} + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// Codex / knife-edge program: `TIMESTAMP; POP; STOP`. +fn timestamp_pop_stop() -> Bytes { + BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build() +} + +/// The same checkpoint with nothing after it, so `headroom = static_gas` can finish. +fn timestamp_stop() -> Bytes { + BytecodeBuilder::default().append(TIMESTAMP).stop().build() +} + +fn stop_only() -> Bytes { + BytecodeBuilder::default().append(STOP).build() +} + +struct GuardPassRun { + outcome: Outcome, + accessed: VolatileDataAccess, + /// Post-tx TX-level remaining — what `remainingComputeGas` returns with no frame on the stack. + remaining_compute_gas: u64, +} + +fn run(code: Bytes, limits: EvmTxRuntimeLimits) -> GuardPassRun { + run_db(base_db(code), limits) +} + +fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let result = + alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); + let (usage, detained_compute_gas_limit, remaining_compute_gas) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + ( + additional_limit.get_usage(), + additional_limit.detained_compute_gas_limit(), + additional_limit.current_call_remaining_compute_gas(), + ) + }; + let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); + let gas_used = result.result.tx_gas_used(); + let outcome = Outcome { + result: result.result, + compute_gas: usage.compute_gas, + data_size: usage.data_size, + kv_updates: usage.kv_updates, + state_growth: usage.state_growth, + gas_used, + detained_compute_gas_limit, + state: result.state, + }; + GuardPassRun { outcome, accessed, remaining_compute_gas } +} + +fn intrinsic_stop() -> Outcome { + transact( + MegaSpecId::REX7, + base_db(stop_only()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + ) +} + +fn storage_overhead(intrinsic: &Outcome) -> u64 { + intrinsic.gas_used - intrinsic.compute_gas +} + +fn assert_timestamp_marked(label: &str, run: &GuardPassRun) { + assert!( + run.accessed.contains(VolatileDataAccess::TIMESTAMP), + "{label}: TIMESTAMP body must run and mark detention; accessed={:?}", + run.accessed + ); +} + +fn assert_timestamp_unmarked(label: &str, run: &GuardPassRun) { + assert!( + !run.accessed.contains(VolatileDataAccess::TIMESTAMP), + "{label}: TIMESTAMP must not have marked; accessed={:?}", + run.accessed + ); +} + +fn decode_top_revert(label: &str, outcome: &Outcome) -> MegaLimitExceeded { + match &outcome.result { + ExecutionResult::Revert { output, .. } => MegaLimitExceeded::abi_decode(output) + .unwrap_or_else(|e| panic!("{label}: revert is not MegaLimitExceeded: {e}")), + other => panic!("{label}: expected Revert(MegaLimitExceeded), got {other:?}"), + } +} + +fn call_callee(builder: BytecodeBuilder, gas: u64) -> BytecodeBuilder { + builder + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(gas) + .append(CALL) +} + +fn store_remaining_compute_gas(builder: BytecodeBuilder, slot: u64) -> BytecodeBuilder { + builder + .mstore(0, IMegaLimitControl::remainingComputeGasCall::SELECTOR) + .push_number(32u64) + .push_number(0u64) + .push_number(4u64) + .push_number(0u64) + .push_address(LIMIT_CONTROL_ADDRESS) + .push_number(1_000_000u64) + .append(STATICCALL) + .append(POP) + .push_number(0u64) + .append(MLOAD) + .push_u256(U256::from(slot)) + .append(SSTORE) +} + +/// Parent that only calls the child and stops — used to place the compute knife edge on +/// the child's first opcode rather than on a later `remainingComputeGas` CALL. +fn nested_db(child: Bytes) -> MemoryDatabase { + let parent = call_callee(BytecodeBuilder::default(), 50_000_000).append(STOP).build(); + base_db(parent).account_code(CALLEE, child) +} + +/// Same parent, then a `remainingComputeGas` read. Only used when the TX compute limit is +/// unconstrained, so the CALL itself is not the binding opcode. +fn nested_db_with_remaining_read(child: Bytes) -> MemoryDatabase { + let parent = store_remaining_compute_gas( + call_callee(BytecodeBuilder::default(), 50_000_000), + REMAINING_SLOT, + ) + .append(STOP) + .build(); + base_db(parent).account_code(CALLEE, child) +} + +/// Codex reproduction: `TIMESTAMP; POP; STOP` with compute limit `intrinsic + 1`. +/// +/// Headroom at `TIMESTAMP` is `static_gas − 1`. Charging after the prologue records the body +/// and reverts `MegaLimitExceeded` with compute `intrinsic + 2`. Charging before the prologue +/// lets the clamp stop the opcode: a TX-level `Halt(ComputeGasLimitExceeded)` with compute +/// `intrinsic + 1` and no detention mark. +#[test] +fn test_top_frame_timestamp_one_below_static_gas_reverts_after_the_body() { + let intrinsic = intrinsic_stop(); + assert_eq!(intrinsic.compute_gas, 21_000, "intrinsic compute is 21_000"); + let limit = intrinsic.compute_gas + TIMESTAMP_STATIC_GAS - 1; + assert_eq!(limit, 21_001); + + let run = run(timestamp_pop_stop(), compute_limit(limit)); + + let decoded = decode_top_revert("headroom=static-1", &run.outcome); + assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); + // Top-frame remaining after intrinsic equals the headroom (`static_gas − 1`). The + // per-opcode record path classifies that as frame-local, so the payload names the + // frame budget (1), not the TX compute limit (21001). + assert_eq!( + decoded.limit, + TIMESTAMP_STATIC_GAS - 1, + "the payload names the top-frame budget that the body overshot" + ); + assert_eq!( + run.outcome.compute_gas, + intrinsic.compute_gas + TIMESTAMP_STATIC_GAS, + "the body charge is recorded; compute must be 21002, not the clamp's 21001" + ); + assert_eq!(run.outcome.compute_gas, 21_002); + assert_eq!( + run.outcome.gas_used, + run.outcome.compute_gas + storage_overhead(&intrinsic), + "receipt gas is compute plus the intrinsic storage component" + ); + assert_eq!(run.outcome.gas_used, 60_002); + assert_timestamp_marked("headroom=static-1", &run); + assert_eq!( + run.remaining_compute_gas, 0, + "usage is past the limit, so remainingComputeGas is zero" + ); +} + +/// Neighbouring edge: headroom equals the static fee, so `TIMESTAMP` itself can finish. +/// +/// Nothing follows the checkpoint (`TIMESTAMP; STOP`) so the transaction continues rather +/// than walking into a zero-headroom clamp on `POP`. +#[test] +fn test_top_frame_timestamp_at_static_gas_continues() { + let intrinsic = intrinsic_stop(); + let limit = intrinsic.compute_gas + TIMESTAMP_STATIC_GAS; + assert_eq!(limit, 21_002); + + let run = run(timestamp_stop(), compute_limit(limit)); + + assert!( + run.outcome.is_success(), + "headroom=static_gas must let TIMESTAMP finish; got {:?}", + run.outcome.result + ); + assert_eq!(run.outcome.compute_gas, intrinsic.compute_gas + TIMESTAMP_STATIC_GAS); + assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + storage_overhead(&intrinsic),); + assert_timestamp_marked("headroom=static_gas", &run); + assert_eq!(run.remaining_compute_gas, 0); +} + +/// Nested sibling of the Codex edge: the child's first opcode is `TIMESTAMP`, and the TX +/// compute limit leaves `static_gas − 1` of headroom when that opcode is reached. +#[test] +fn test_nested_timestamp_one_below_static_gas_reverts_after_the_body() { + let empty = run_db(nested_db(stop_only()), EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + assert!( + empty.outcome.is_success(), + "calibration child must succeed: {:?}", + empty.outcome.result + ); + let before = empty.outcome.compute_gas; + let limit = before + TIMESTAMP_STATIC_GAS - 1; + + let run = run_db(nested_db(timestamp_stop()), compute_limit(limit)); + + assert_timestamp_marked("nested headroom=static-1", &run); + assert_eq!( + run.outcome.compute_gas, + before + TIMESTAMP_STATIC_GAS, + "the child body must be recorded; clamp-before-prologue would stop at {limit}" + ); + match &run.outcome.result { + ExecutionResult::Revert { output, .. } => { + let decoded = MegaLimitExceeded::abi_decode(output) + .unwrap_or_else(|e| panic!("nested revert is not MegaLimitExceeded: {e}")); + assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); + } + ExecutionResult::Success { .. } => { + // Frame-local child revert absorbed by the parent. The TX remaining is what + // `remainingComputeGas` would report; the body overshot, so it is zero. + } + ExecutionResult::Halt { reason, .. } => { + panic!( + "nested headroom=static-1 must not be a clamp Halt (the e4dfbca shape); \ + got {reason:?} compute={} marked={}", + run.outcome.compute_gas, + run.accessed.contains(VolatileDataAccess::TIMESTAMP), + ); + } + } + assert_eq!( + run.remaining_compute_gas, 0, + "remainingComputeGas after the body overshoot is zero" + ); +} + +/// Nested sibling of the exact-fee edge: the child can afford `TIMESTAMP` and returns, so +/// the parent continues. +#[test] +fn test_nested_timestamp_at_static_gas_continues() { + let empty = run_db(nested_db(stop_only()), EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + let before = empty.outcome.compute_gas; + let limit = before + TIMESTAMP_STATIC_GAS; + + let run = run_db(nested_db(timestamp_stop()), compute_limit(limit)); + + assert_timestamp_marked("nested headroom=static_gas", &run); + assert_eq!(run.outcome.compute_gas, before + TIMESTAMP_STATIC_GAS); + assert!( + run.outcome.is_success(), + "headroom=static_gas must let the child TIMESTAMP finish and the parent continue; got {:?}", + run.outcome.result + ); + assert_eq!(run.remaining_compute_gas, 0); +} + +/// Unconstrained nested read: after a child `TIMESTAMP` the parent's `remainingComputeGas` +/// is the detained remaining, which is how the mark is observed on chain. +#[test] +fn test_nested_timestamp_remaining_compute_gas_is_detained() { + let empty = run_db( + nested_db_with_remaining_read(stop_only()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + ); + let with_ts = run_db( + nested_db_with_remaining_read(timestamp_stop()), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + ); + assert!(empty.outcome.is_success(), "empty child must succeed: {:?}", empty.outcome.result); + assert!( + with_ts.outcome.is_success(), + "TIMESTAMP child must succeed: {:?}", + with_ts.outcome.result + ); + assert_timestamp_unmarked("empty child", &empty); + assert_timestamp_marked("TIMESTAMP child", &with_ts); + + let empty_reading: u64 = empty + .outcome + .storage_value(CONTRACT, U256::from(REMAINING_SLOT)) + .try_into() + .expect("remainingComputeGas fits in u64"); + let ts_reading: u64 = with_ts + .outcome + .storage_value(CONTRACT, U256::from(REMAINING_SLOT)) + .try_into() + .expect("remainingComputeGas fits in u64"); + assert!(empty_reading > 0, "undetained remainingComputeGas must be non-zero"); + assert!( + ts_reading < empty_reading, + "TIMESTAMP detention must shrink remainingComputeGas; empty={empty_reading} ts={ts_reading}" + ); + assert_eq!(with_ts.outcome.compute_gas, empty.outcome.compute_gas + TIMESTAMP_STATIC_GAS,); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index a4562c11..f305d5c0 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -29,6 +29,9 @@ //! - `charge_on_reject` — a `disableVolatileDataAccess` rejection still pays the opcode's static //! fee, which segment settlement records as compute; REX6 keeps the historical zero-charge //! revert. +//! - `guard_pass_static_gas` — a passing guard charges the checkpoint static fee after the prologue +//! restores true gas, so a compute headroom of `static_gas − 1` records the body rather than +//! stopping at the clamp. //! - `detention_window` — an underfunded CALL / EXTCODECOPY that OOGs before the target load does //! not mark beneficiary access; that order is specified, so the frozen-window tripwire stays //! silent. @@ -44,6 +47,7 @@ mod double_exceed_corner; mod exceptional_halt; mod gas_clamp; mod gas_leakage; +mod guard_pass_static_gas; mod interceptor_resume; mod latch_surfacing; mod modexp_gas; diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 09f429e3..fcf1f419 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -164,7 +164,8 @@ The reverting frame returns every unit of gas it held when it reached the opcode Under Rex7, a node MUST still reject the same opcodes with the same revert payload, and MUST still leave the tracker unmarked. A node MUST charge the rejected opcode's static fee before producing that revert. -The fee is ordinary EVM gas: it is debited from the frame, it is not refunded by the synthetic revert, and it MUST be recorded as compute gas when the open segment is settled — at the next checkpoint if the guard then passes, or at frame exit if it rejects. +The fee is ordinary EVM gas: it is debited from the frame, it is not refunded by the synthetic revert, and it MUST be recorded as compute gas when the open segment is settled at frame exit. +A guard that does not reject MUST charge the opcode's static fee in the checkpoint body, after the clamp is restored, at the same position the body charged it before this rule. A frame that cannot afford the static fee MUST halt out of gas instead of reaching the disable revert. The guarded set is unchanged from Rex6: the unconditional block-environment opcodes, the beneficiary-conditional account reads, `SELFBALANCE`, oracle-conditional `SLOAD`, the CALL family, and `SELFDESTRUCT`. From 33a8ce91035a01e979b13d909b5ca0fc6932aa8e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 11:10:58 +0800 Subject: [PATCH 049/208] fix(rex7): correct guard-pass charge site and pin SELFBALANCE Charge-on-reject does not move a passing guard's static fee: keep that historical site as the only spec anchor, and name the CALL family's pre-load position so it stays consistent with mark-at-load. Add the SELFBALANCE headroom=static-1 / static edges so a pre-prologue charge cannot regress silently. --- .../tests/rex7/guard_pass_static_gas.rs | 94 +++++++++++++++++-- docs/spec/upgrades/rex7.md | 3 +- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index 525c6e91..23ffcf06 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -1,14 +1,15 @@ //! Guard-pass static-gas position under REX7 checkpoint accounting. //! //! Charge-on-reject debits a disabled opcode's static entry on the reject arm only. A passing -//! guard must charge after [`checkpoint_prologue!`] restores the true counter — the same -//! position the baseline REX7 handler used — so a compute headroom of `static_gas − 1` lets -//! the body run and reports a frame-local `MegaLimitExceeded` rather than a clamp halt that -//! never reads the host. +//! guard must charge at the same position the baseline REX7 handler used. For the +//! checkpoint-wrapped families that is after [`checkpoint_prologue!`] restores the true +//! counter, so a compute headroom of `static_gas − 1` lets the body run and reports a +//! frame-local `MegaLimitExceeded` rather than a clamp halt that never reads the host. //! //! `headroom = static_gas − 1` and `headroom = static_gas` are the two edges: the first -//! overshoots after the body, the second can afford the charge. Each is exercised in the -//! top-level frame and in a nested child. `remainingComputeGas` is the value +//! overshoots after the body, the second can afford the charge. `TIMESTAMP` is exercised in +//! the top-level frame and in a nested child; `SELFBALANCE` covers the dedicated checkpoint +//! handler at the top-level edges. `remainingComputeGas` is the value //! `MegaLimitControl.remainingComputeGas` would return after the transaction: the TX-level //! remaining, which is what the interceptor reads once the frames have been popped. @@ -22,7 +23,7 @@ use mega_evm::{ LIMIT_CONTROL_ADDRESS, }; use revm::{ - bytecode::opcode::{CALL, MLOAD, POP, SSTORE, STATICCALL, STOP, TIMESTAMP}, + bytecode::opcode::{CALL, MLOAD, POP, SELFBALANCE, SSTORE, STATICCALL, STOP, TIMESTAMP}, context::{result::ExecutionResult, tx::TxEnvBuilder}, handler::EvmTr, }; @@ -30,6 +31,9 @@ use revm::{ /// `TIMESTAMP` static gas — the unconditional-family representative. const TIMESTAMP_STATIC_GAS: u64 = 2; +/// `SELFBALANCE` static gas (`LOW`) — the dedicated-checkpoint representative. +const SELFBALANCE_STATIC_GAS: u64 = 5; + /// Slot the nested caller stores its `remainingComputeGas` reading into. const REMAINING_SLOT: u64 = 0xc0; @@ -54,6 +58,16 @@ fn timestamp_stop() -> Bytes { BytecodeBuilder::default().append(TIMESTAMP).stop().build() } +/// Dedicated-checkpoint sibling of [`timestamp_pop_stop`]: `SELFBALANCE; POP; STOP`. +fn selfbalance_pop_stop() -> Bytes { + BytecodeBuilder::default().append(SELFBALANCE).append(POP).stop().build() +} + +/// Dedicated-checkpoint sibling of [`timestamp_stop`]: `SELFBALANCE; STOP`. +fn selfbalance_stop() -> Bytes { + BytecodeBuilder::default().append(SELFBALANCE).stop().build() +} + fn stop_only() -> Bytes { BytecodeBuilder::default().append(STOP).build() } @@ -363,3 +377,69 @@ fn test_nested_timestamp_remaining_compute_gas_is_detained() { ); assert_eq!(with_ts.outcome.compute_gas, empty.outcome.compute_gas + TIMESTAMP_STATIC_GAS,); } + +/// Dedicated-checkpoint sibling of the Codex edge: `SELFBALANCE; POP; STOP` with compute +/// limit `intrinsic + 4`. +/// +/// Headroom at `SELFBALANCE` is `static_gas − 1`. Charging after the prologue records the +/// body and reverts `MegaLimitExceeded` with compute `intrinsic + 5`. Charging before the +/// prologue (the e4dfbca shape) lets the clamp stop the opcode: a TX-level +/// `Halt(ComputeGasLimitExceeded)` with compute `intrinsic + 4`. +#[test] +fn test_top_frame_selfbalance_one_below_static_gas_reverts_after_the_body() { + let intrinsic = intrinsic_stop(); + assert_eq!(intrinsic.compute_gas, 21_000, "intrinsic compute is 21_000"); + let limit = intrinsic.compute_gas + SELFBALANCE_STATIC_GAS - 1; + assert_eq!(limit, 21_004); + + let run = run(selfbalance_pop_stop(), compute_limit(limit)); + + let decoded = decode_top_revert("SELFBALANCE headroom=static-1", &run.outcome); + assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); + // Top-frame remaining after intrinsic equals the headroom (`static_gas − 1`). The + // per-opcode record path classifies that as frame-local, so the payload names the + // frame budget (4), not the TX compute limit (21004). + assert_eq!( + decoded.limit, + SELFBALANCE_STATIC_GAS - 1, + "the payload names the top-frame budget that the body overshot" + ); + assert_eq!( + run.outcome.compute_gas, + intrinsic.compute_gas + SELFBALANCE_STATIC_GAS, + "the body charge is recorded; compute must be 21005, not the clamp's 21004" + ); + assert_eq!(run.outcome.compute_gas, 21_005); + assert_eq!( + run.outcome.gas_used, + run.outcome.compute_gas + storage_overhead(&intrinsic), + "receipt gas is compute plus the intrinsic storage component" + ); + assert_eq!(run.outcome.gas_used, 60_005); + assert_eq!( + run.remaining_compute_gas, 0, + "usage is past the limit, so remainingComputeGas is zero" + ); +} + +/// Neighbouring edge: headroom equals the static fee, so `SELFBALANCE` itself can finish. +/// +/// Nothing follows the checkpoint (`SELFBALANCE; STOP`) so the transaction continues rather +/// than walking into a zero-headroom clamp on `POP`. +#[test] +fn test_top_frame_selfbalance_at_static_gas_continues() { + let intrinsic = intrinsic_stop(); + let limit = intrinsic.compute_gas + SELFBALANCE_STATIC_GAS; + assert_eq!(limit, 21_005); + + let run = run(selfbalance_stop(), compute_limit(limit)); + + assert!( + run.outcome.is_success(), + "headroom=static_gas must let SELFBALANCE finish; got {:?}", + run.outcome.result + ); + assert_eq!(run.outcome.compute_gas, intrinsic.compute_gas + SELFBALANCE_STATIC_GAS); + assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + storage_overhead(&intrinsic),); + assert_eq!(run.remaining_compute_gas, 0); +} diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index fcf1f419..bf7a78e5 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -165,7 +165,8 @@ The reverting frame returns every unit of gas it held when it reached the opcode Under Rex7, a node MUST still reject the same opcodes with the same revert payload, and MUST still leave the tracker unmarked. A node MUST charge the rejected opcode's static fee before producing that revert. The fee is ordinary EVM gas: it is debited from the frame, it is not refunded by the synthetic revert, and it MUST be recorded as compute gas when the open segment is settled at frame exit. -A guard that does not reject MUST charge the opcode's static fee in the checkpoint body, after the clamp is restored, at the same position the body charged it before this rule. +A guard that does not reject MUST charge the opcode's static fee at the same position the body charged it before this rule. +For the CALL family that position remains before the target account is read. A frame that cannot afford the static fee MUST halt out of gas instead of reaching the disable revert. The guarded set is unchanged from Rex6: the unconditional block-environment opcodes, the beneficiary-conditional account reads, `SELFBALANCE`, oracle-conditional `SLOAD`, the CALL family, and `SELFDESTRUCT`. From 49403cadc33d4991cd026188734b984d89b308f2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 12:45:13 +0800 Subject: [PATCH 050/208] docs(rex7): correct checkpoint docs and specify prepaid-crossing semantics --- crates/mega-evm/src/evm/AGENTS.md | 2 +- docs/spec/evm/compute-gas.md | 11 +++++++++-- docs/spec/evm/dual-gas-model.md | 4 +++- docs/spec/upgrades/rex7.md | 6 ++++++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 0245c104..f356ba84 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -23,7 +23,7 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - Keep inspector and non-inspector paths behaviorally aligned. ## 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). +- New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` still aliases `rex5` and expresses its deltas as `is_enabled` dispatch inside the shared handlers; `rex7` is a standalone checkpoint table built from revm's base table, with the 17 storage / CALL / CREATE / SELFDESTRUCT / not-yet-activated slots inherited from `rex6`, and with 15 volatile `*_checkpoint` handlers plus `gas_checkpoint` registered as rex7-only). - Volatile access detention trigger changes: `host.rs` and volatile wrappers in `instructions.rs`. - Call forwarding and stipend interplay: `instructions.rs` + `../limit/storage_call_stipend.rs`. - New external gas pricing path: `host.rs` gas helper methods. diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index ee9b2210..6d19fea1 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -494,6 +494,11 @@ The `limit` reported by either shape MUST be the constraint that bound the clamp Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. The `actual` a transaction-level clamp halt reports MUST be the transaction's final compute usage, after the frame-exit settlement has closed the partial segment the crossing opcode stopped inside. +A checkpoint that still carries a non-zero static fee — `GAS` and `LOG0` through `LOG4` — MAY itself be the crossing opcode of the preceding plain-opcode segment. +When the clamped visible remainder is less than that fee, the inherited per-opcode check stops the opcode before the body runs, and a node MUST treat that stop as a plain-segment crossing. +The CALL family is the same stop: its static fee is charged before the body, so a clamped remainder below that fee stops the opcode before the target account is read. +`CREATE` and `CREATE2` charge their inherited creation fee inside the body, after the true remaining gas has been restored, so a compute headroom below that fee MUST NOT stop them before the body. + When the current frame's remaining per-frame compute budget equals the transaction-level remaining budget, a node MUST bind the clamp to the transaction-level constraint (including detention when detention is the effective transaction-level bound). A clamp-induced exceed under that binding MUST halt the transaction with gas rescue; a node MUST NOT classify the equality as frame-local. Through Rex6, the same equality is classified by the per-opcode check as a frame-local exceed; at the top-level frame that surfaces as a revert rather than a halt. @@ -505,8 +510,10 @@ When the crossing opcode would exhaust both the true remaining EVM gas and the c A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. A node MUST settle that whole budget as compute gas, split into two parts that are accounted differently: -- **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. -- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). +- **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. + This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. +- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. + A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. diff --git a/docs/spec/evm/dual-gas-model.md b/docs/spec/evm/dual-gas-model.md index 60043b5e..b92c615c 100644 --- a/docs/spec/evm/dual-gas-model.md +++ b/docs/spec/evm/dual-gas-model.md @@ -102,7 +102,9 @@ The no-record rule when the body does not run to completion is specified in [Sin
Rex7 (unstable): checkpoint settlement of compute gas -Under Rex7, the metering order above continues to govern every **checkpoint** opcode — the storage-affecting set listed in this section, the volatile / detention-guarded set, and `GAS` — and those checkpoints still charge storage gas before the body and record compute gas after it. +Under Rex7, the metering order above continues to govern every **checkpoint** opcode that has a storage-gas component — the storage-affecting set listed in this section. +Those checkpoints still charge storage gas before the body and record compute gas after it. +Volatile / detention-guarded checkpoints and `GAS` record compute gas after the body and charge no storage gas. Plain opcodes between checkpoints MUST NOT record compute gas when they finish; their compute gas settles as an interpreter-gas segment delta at the next checkpoint or at frame entry, resume, or exit. Limit enforcement inside a plain-opcode segment uses gas clamping rather than a post-opcode record step: a crossing opcode is stopped before it executes, and its cost is excluded from recorded usage. See [Compute Gas Accounting](compute-gas.md) and the [Rex7 Network Upgrade](../upgrades/rex7.md) for the full checkpoint set, clamp rules, and the exceptional-halt frame carve-out. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index bf7a78e5..7ead53e5 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -134,6 +134,12 @@ The reported `limit` MUST be the constraint that bound the clamp, not whichever The revert payload is visible to the calling contract, so a frame-local exceed that reported the transaction-level limit would be a different observable return value for the same execution, not merely a different diagnostic. Because the crossing opcode never executes, a node MUST NOT include its cost in recorded compute-gas usage. + +A checkpoint that still carries a non-zero static fee — `GAS` and `LOG0` through `LOG4` — MAY itself be the crossing opcode of the preceding plain-opcode segment. +When the clamped visible remainder is less than that fee, the inherited per-opcode check stops the opcode before the body runs, and a node MUST treat that stop as a plain-segment crossing. +The CALL family is the same stop: its static fee is charged before the body, so a clamped remainder below that fee stops the opcode before the target account is read. +`CREATE` and `CREATE2` charge their inherited creation fee inside the body, after the true remaining gas has been restored, so a compute headroom below that fee MUST NOT stop them before the body. + The usage the clamp **enforces** therefore ends at or below the limit, not strictly above it. The `actual` a transaction-level clamp halt reports MUST be the transaction's final reported compute usage — the frame-exit settlement closes the partial segment after the exceed is identified, and a node MUST NOT report the usage as it stood before that settlement. Reported usage is not the same quantity as enforced usage: it also carries the destroyed remainders of any frame that halted exceptionally earlier in the transaction, which are reported and never enforced. From a73fef79dcfd9712f077045fae2be6b3da0b92b4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 12:45:17 +0800 Subject: [PATCH 051/208] test(rex7): pin static-fee edges for GAS, LOG1, and CREATE --- .../tests/rex7/checkpoint_static_fee_edges.rs | 306 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + 2 files changed, 310 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs diff --git a/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs new file mode 100644 index 00000000..1c3a142a --- /dev/null +++ b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs @@ -0,0 +1,306 @@ +//! Table-prepaid checkpoint static-fee edges under REX7 gas-clamp enforcement. +//! +//! revm's `step()` pre-charges an opcode's gas-table entry before the handler runs. Under REX7 the +//! zeroed set is only the volatile-guarded family, so `GAS` (2) and `LOG0`–`LOG4` (375) keep a +//! non-zero entry. When the clamp's visible remainder is below that entry and true EVM gas is +//! still sufficient, the inherited per-opcode check stops the opcode before the body — a +//! plain-segment crossing: Halt(`ComputeGasLimitExceeded`), the fee never enters compute usage, +//! and the body has no observable effect. +//! +//! `CREATE` / `CREATE2` are the contrast: their table entry is 0 (revm charges 32,000 inside the +//! body, after the checkpoint restores the true counter). A compute headroom of `32_000 − 1` does +//! not stop them before the body; the body runs, the fee is recorded, and a top-frame exceed +//! surfaces as `Revert(MegaLimitExceeded)` with the created account discarded. +//! +//! Each family has two top-frame edges, calibrated so the named headroom is the remaining compute +//! at the opcode itself (prefix `PUSH` opcodes are measured out first). + +use crate::common::{transact, Outcome, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Address, Bytes, U256}; +use alloy_sol_types::SolError; +use mega_evm::{ + constants::mini_rex::LOG_TOPIC_STORAGE_GAS, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CREATE, GAS, LOG1, STOP}, + context::result::ExecutionResult, +}; + +/// `GAS` static gas — prepaid by the inherited table. +const GAS_STATIC_GAS: u64 = 2; + +/// `LOG0`–`LOG4` table entry (base LOG cost). Per-topic and per-byte costs are charged in the body. +const LOG_STATIC_GAS: u64 = 375; + +/// `LOG1` with empty data: table 375 + one topic 375. +const LOG1_BODY_COMPUTE: u64 = 750; + +/// `CREATE` body fee. The REX7 table entry is 0; revm charges this inside the body. +const CREATE_BODY_GAS: u64 = 32_000; + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) +} + +fn run(code: Bytes, limit: u64) -> Outcome { + transact(MegaSpecId::REX7, base_db(code), compute_limit(limit)) +} + +fn unconstrained(code: Bytes) -> Outcome { + transact(MegaSpecId::REX7, base_db(code), EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)) +} + +fn stop_only() -> Bytes { + BytecodeBuilder::default().append(STOP).build() +} + +fn storage_overhead(intrinsic: &Outcome) -> u64 { + intrinsic.gas_used - intrinsic.compute_gas +} + +fn account_nonce(outcome: &Outcome, address: Address) -> u64 { + outcome.state.get(&address).map(|account| account.info.nonce).unwrap_or(0) +} + +fn created_addresses(outcome: &Outcome) -> Vec
{ + outcome + .state + .iter() + .filter(|(_, account)| account.is_created()) + .map(|(address, _)| *address) + .collect() +} + +/// Transaction-level clamp crossing: Halt, usage stops at the limit, the opcode's fee is not in +/// compute, receipt gas is compute plus the intrinsic storage component. +fn assert_tx_level_crossing(label: &str, outcome: &Outcome, limit: u64, storage_overhead: u64) { + match outcome.halt_reason(label) { + MegaHaltReason::ComputeGasLimitExceeded { limit: reported, actual } => { + assert_eq!(*reported, limit, "{label}: reported limit is the TX compute limit"); + assert_eq!( + *actual, outcome.compute_gas, + "{label}: reported actual is the transaction's final compute usage" + ); + } + other => panic!("{label}: expected ComputeGasLimitExceeded, got {other:?}"), + } + assert_eq!( + outcome.compute_gas, limit, + "{label}: crossing usage stops at the limit; the opcode's fee must not be recorded" + ); + assert_eq!( + outcome.gas_used, + outcome.compute_gas + storage_overhead, + "{label}: receipt gas is compute plus the intrinsic storage component" + ); +} + +// --------------------------------------------------------------------------------------------- +// GAS (table static = 2) +// --------------------------------------------------------------------------------------------- + +/// `GAS; STOP` with compute headroom `static − 1`. +/// +/// The table pre-charges 2, so the clamp stops `GAS` before the handler. A charge after the +/// prologue would let the body run and record 2. +#[test] +fn test_gas_one_below_static_is_a_plain_segment_crossing() { + let intrinsic = unconstrained(stop_only()); + assert_eq!(intrinsic.compute_gas, 21_000, "intrinsic compute is 21_000"); + let limit = intrinsic.compute_gas + GAS_STATIC_GAS - 1; + assert_eq!(limit, 21_001); + + let outcome = run(BytecodeBuilder::default().append(GAS).append(STOP).build(), limit); + + assert_tx_level_crossing( + "GAS headroom=static-1", + &outcome, + limit, + storage_overhead(&intrinsic), + ); +} + +/// Neighbouring edge: headroom equals the static fee, so `GAS` itself can finish. +#[test] +fn test_gas_at_static_executes_the_body() { + let intrinsic = unconstrained(stop_only()); + let limit = intrinsic.compute_gas + GAS_STATIC_GAS; + assert_eq!(limit, 21_002); + + let outcome = run(BytecodeBuilder::default().append(GAS).append(STOP).build(), limit); + + assert!( + outcome.is_success(), + "headroom=static_gas must let GAS finish; got {:?}", + outcome.result + ); + assert_eq!(outcome.compute_gas, intrinsic.compute_gas + GAS_STATIC_GAS); + assert_eq!(outcome.gas_used, outcome.compute_gas + storage_overhead(&intrinsic)); +} + +// --------------------------------------------------------------------------------------------- +// LOG1 (table static = 375; empty-data body adds 375 for the topic) +// --------------------------------------------------------------------------------------------- + +fn log1_operands(builder: BytecodeBuilder) -> BytecodeBuilder { + builder.push_number(0xabu64).push_number(0u64).push_number(0u64) +} + +/// `LOG1; STOP` with compute headroom `table_static − 1` at the opcode. +/// +/// The three operand `PUSH` opcodes are measured out first so the named headroom is the remainder +/// the clamp shows `LOG1`. The table pre-charges 375, so 374 stops the opcode before the body: no +/// log, no topic storage gas. +#[test] +fn test_log1_one_below_static_is_a_plain_segment_crossing() { + let intrinsic = unconstrained(stop_only()); + let before = unconstrained(log1_operands(BytecodeBuilder::default()).append(STOP).build()); + let limit = before.compute_gas + LOG_STATIC_GAS - 1; + + let outcome = + run(log1_operands(BytecodeBuilder::default()).append(LOG1).append(STOP).build(), limit); + + assert_tx_level_crossing( + "LOG1 headroom=static-1", + &outcome, + limit, + storage_overhead(&intrinsic), + ); + assert!( + outcome.result.logs().is_empty(), + "LOG1 body must not run; logs={:?}", + outcome.result.logs() + ); +} + +/// Neighbouring edge: headroom equals the full `LOG1` body cost, so the log is emitted. +/// +/// Table static is 375; the topic adds another 375. Headroom of 375 would enter the handler and +/// then overshoot on the topic, reverting the log. The executing edge is the full body cost. +#[test] +fn test_log1_at_full_body_cost_emits_the_log() { + let intrinsic = unconstrained(stop_only()); + let before = unconstrained(log1_operands(BytecodeBuilder::default()).append(STOP).build()); + let full = + unconstrained(log1_operands(BytecodeBuilder::default()).append(LOG1).append(STOP).build()); + assert_eq!( + full.compute_gas, + before.compute_gas + LOG1_BODY_COMPUTE, + "empty-data LOG1 compute is table 375 plus one topic 375" + ); + let limit = before.compute_gas + LOG1_BODY_COMPUTE; + + let outcome = + run(log1_operands(BytecodeBuilder::default()).append(LOG1).append(STOP).build(), limit); + + assert!( + outcome.is_success(), + "headroom=full LOG1 body must emit the log; got {:?}", + outcome.result + ); + assert_eq!(outcome.compute_gas, full.compute_gas); + assert_eq!(outcome.result.logs().len(), 1, "LOG1 body must emit exactly one log"); + assert_eq!( + outcome.gas_used, + outcome.compute_gas + storage_overhead(&intrinsic) + LOG_TOPIC_STORAGE_GAS, + "receipt gas includes the LOG topic storage component" + ); +} + +// --------------------------------------------------------------------------------------------- +// CREATE (table static = 0; body charges 32,000 after the true counter is restored) +// --------------------------------------------------------------------------------------------- + +fn create_operands(builder: BytecodeBuilder) -> BytecodeBuilder { + builder.push_number(0u64).push_number(0u64).push_number(0u64) +} + +fn decode_top_revert(label: &str, outcome: &Outcome) -> MegaLimitExceeded { + match &outcome.result { + ExecutionResult::Revert { output, .. } => MegaLimitExceeded::abi_decode(output) + .unwrap_or_else(|e| panic!("{label}: revert is not MegaLimitExceeded: {e}")), + ExecutionResult::Halt { reason, .. } => panic!( + "{label}: CREATE table entry is 0, so headroom below 32_000 must not be a clamp \ + Halt (that would mean the 32_000 was prepaid); got {reason:?} compute={}", + outcome.compute_gas + ), + other => panic!("{label}: expected Revert(MegaLimitExceeded), got {other:?}"), + } +} + +/// `CREATE; STOP` with compute headroom `32_000 − 1` at the opcode. +/// +/// The table does not pre-charge CREATE, so the body runs on the restored counter, records +/// 32,000, and the top-frame per-opcode exceed reverts. The created account is discarded; the +/// fee stays in compute. A table entry of 32,000 would have made this a clamp Halt instead. +#[test] +fn test_create_one_below_body_fee_runs_then_reverts() { + let intrinsic = unconstrained(stop_only()); + let before = unconstrained(create_operands(BytecodeBuilder::default()).append(STOP).build()); + let full = unconstrained( + create_operands(BytecodeBuilder::default()).append(CREATE).append(STOP).build(), + ); + assert_eq!( + full.compute_gas, + before.compute_gas + CREATE_BODY_GAS, + "empty-initcode CREATE compute is the 32_000 body fee" + ); + let limit = before.compute_gas + CREATE_BODY_GAS - 1; + + let outcome = + run(create_operands(BytecodeBuilder::default()).append(CREATE).append(STOP).build(), limit); + + let decoded = decode_top_revert("CREATE headroom=32000-1", &outcome); + assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); + assert_eq!( + outcome.compute_gas, full.compute_gas, + "the 32_000 body fee is recorded; a table-prepaid crossing would stop at {limit}" + ); + assert!( + created_addresses(&outcome).is_empty(), + "the reverted CREATE must not leave a created account; created={:?}", + created_addresses(&outcome) + ); + assert_eq!(account_nonce(&outcome, CONTRACT), 0, "the creator nonce must not advance"); + assert_eq!( + outcome.gas_used, + outcome.compute_gas + storage_overhead(&intrinsic), + "empty-initcode CREATE adds no storage gas at minimum bucket capacity" + ); +} + +/// Neighbouring edge: headroom equals the 32,000 body fee, so CREATE finishes and the account +/// remains. +#[test] +fn test_create_at_body_fee_creates_the_account() { + let intrinsic = unconstrained(stop_only()); + let before = unconstrained(create_operands(BytecodeBuilder::default()).append(STOP).build()); + let limit = before.compute_gas + CREATE_BODY_GAS; + + let outcome = + run(create_operands(BytecodeBuilder::default()).append(CREATE).append(STOP).build(), limit); + + assert!( + outcome.is_success(), + "headroom=32000 must let CREATE finish; got {:?}", + outcome.result + ); + assert_eq!(outcome.compute_gas, before.compute_gas + CREATE_BODY_GAS); + assert_eq!(account_nonce(&outcome, CONTRACT), 1, "CREATE must advance the creator nonce"); + assert_eq!( + created_addresses(&outcome).len(), + 1, + "CREATE must leave one created account; created={:?}", + created_addresses(&outcome) + ); + assert_eq!(outcome.gas_used, outcome.compute_gas + storage_overhead(&intrinsic)); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index f305d5c0..d18c81f4 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -35,11 +35,15 @@ //! - `detention_window` — an underfunded CALL / EXTCODECOPY that OOGs before the target load does //! not mark beneficiary access; that order is specified, so the frozen-window tripwire stays //! silent. +//! - `checkpoint_static_fee_edges` — a table-prepaid checkpoint (`GAS`, `LOG1`) whose static fee +//! exceeds the clamp headroom is a plain-segment crossing; `CREATE`'s 32,000 is charged inside +//! the body, so the same headroom runs the body and then reverts. mod burn_split; mod charge_on_reject; mod checkpoint_families; mod checkpoint_settlement; +mod checkpoint_static_fee_edges; mod clamp_classification; mod common; mod detention_window; From 8c98a34d1b110b10071276864d4944046cf43d91 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 13:41:24 +0800 Subject: [PATCH 052/208] feat(evm): split REX7 precompile-halt compute into executed and destroyed lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failing precompile never reaches interpreter-frame halt settlement. REX7 now books executed work as enforcing usage and the unused caller-supplied envelope — including the REX5 forwarded-gas cap gap — as destroyed. Success and revert paths, and every spec through REX6, are unchanged. --- crates/mega-evm/src/evm/precompiles.rs | 193 +++++++++++- crates/mega-evm/src/limit/limit.rs | 13 + crates/mega-evm/tests/compute_gas/claims.rs | 39 ++- crates/mega-evm/tests/compute_gas/main.rs | 35 ++- .../mega-evm/tests/compute_gas/snapshot.txt | 2 +- crates/mega-evm/tests/rex7/burn_split.rs | 24 +- crates/mega-evm/tests/rex7/common.rs | 80 ++--- .../tests/rex7/guard_pass_static_gas.rs | 21 +- crates/mega-evm/tests/rex7/main.rs | 4 + crates/mega-evm/tests/rex7/precompile_halt.rs | 284 ++++++++++++++++++ 10 files changed, 607 insertions(+), 88 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/precompile_halt.rs diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 6246922c..8ecefe2c 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -325,20 +325,32 @@ impl PrecompileProvider= GAS_COST`): record the declared fixed cost. revm's `PrecompileError` - // halt still consumes the parent's forwarded `gas_limit` from the EVM-gas meter, so - // compute-gas here is intentionally a separate number from the EVM-gas burn. Address + // `limit() >= GAS_COST`): the declared fixed cost is the work performed. Address // match uses `bytecode_address` (see above) so DELEGATECALL/CALLCODE to KZG still hit - // this arm. + // this arm. Through REX6 that amount is the whole recorded charge; REX7 keeps it on + // the enforcing lane and books the rest of the parent-frame loss — the + // caller-supplied `gas_limit`, not the REX5-capped effective limit — as destroyed. + // The REX5 cap still prevents the precompile from *doing* more work than the + // remaining compute budget; the cap gap is parent-frame loss, not work, so it belongs + // with the destroyed remainder. // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the // wrapper's pre-check itself OOG'd before verification could run): after the - // spend_all undo above, `total_gas_spent() == 0` again. The parent still permanently - // loses the forwarded amount, so record `limit()` to match the EVM-gas burn (do not - // use `total_gas_spent()` here — the structural `limit()` is the intentional charge, - // independent of whether upstream spend_all'd). + // spend_all undo above, `total_gas_spent() == 0` again. Through REX6 the parent still + // permanently loses the forwarded amount, so those specs record `limit()` as + // enforcing usage to match the EVM-gas burn. REX7 treats the same path as + // performed-zero / destroyed-all: no work ran, so nothing enforces, and the + // parent-frame loss (`gas_limit`, again the uncapped forwarded envelope) is reported + // only. + // + // The split is computed here rather than by the interpreter-frame halt settlement. + // A halt `Gas` is `Gas::new(limit)` after the undo above (`remaining() == limit()` + // == the effective, capped budget), so copying that formula would double-count the + // generic arm's REX6 charge or drop the cap gap. if is_rex5_enabled { - let compute_gas = if output.result.is_ok_or_revert() { - output.gas.total_gas_spent() + let is_rex7 = context.spec.is_enabled(MegaSpecId::REX7); + let mut additional_limit = context.additional_limit.borrow_mut(); + if output.result.is_ok_or_revert() { + additional_limit.record_compute_gas(output.gas.total_gas_spent()); } else if address == kzg_point_evaluation::ADDRESS && output.gas.limit() >= kzg_point_evaluation::GAS_COST { @@ -349,11 +361,16 @@ impl PrecompileProvider= GAS_COST`) instead of an error-variant match keeps this arm // robust against upstream KZG adding new non-OOG variants. - kzg_point_evaluation::GAS_COST + let executed = kzg_point_evaluation::GAS_COST; + additional_limit.record_compute_gas(executed); + if is_rex7 { + additional_limit.record_burned_gas(gas_limit.saturating_sub(executed)); + } + } else if is_rex7 { + additional_limit.record_burned_gas(gas_limit); } else { - output.gas.limit() - }; - context.additional_limit.borrow_mut().record_compute_gas(compute_gas); + additional_limit.record_compute_gas(output.gas.limit()); + } } else if context.spec.is_enabled(MegaSpecId::MINI_REX) { context .additional_limit @@ -975,6 +992,154 @@ mod tests { ); } + /// REX7 KZG verification failure: the fixed fee is the work performed (enforcing) and + /// the rest of the caller-supplied envelope is destroyed. The reported total therefore + /// equals the parent-frame loss, not just the fixed fee. + #[test] + fn test_kzg_precompile_rex7_verification_failure_splits_the_parent_loss() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), + ); + let inputs = generate_invalid_proof_kzg_test_input(); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded_gas = 1_000_000u64; + + let result = + precompiles_map.run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)); + let output = result.expect("run ok").expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileError), + "expected PrecompileError on invalid-proof; got {:?}", + output.result + ); + + let additional = context.additional_limit.borrow(); + assert_eq!( + additional.get_usage().compute_gas, + forwarded_gas, + "reported compute is the whole parent-frame loss", + ); + assert_eq!( + additional.burned_compute_gas(), + forwarded_gas - GAS_COST, + "the unused envelope is destroyed, not enforced", + ); + } + + /// REX7 generic error (blake2f malformed input): no work ran, so nothing enforces and the + /// whole caller-supplied envelope is destroyed. + #[test] + fn test_blake2f_precompile_rex7_malformed_input_destroys_the_envelope() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), + ); + let address = Address::with_last_byte(9); + let inputs = InputsImpl { + target_address: address, + bytecode_address: Some(address), + caller_address: address, + input: revm::interpreter::CallInput::Bytes(Bytes::from(vec![0xAAu8; 32])), + call_value: Default::default(), + }; + let forwarded_gas = 1_000_000u64; + + let result = + precompiles_map.run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)); + let output = result.expect("run ok").expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileError), + "expected PrecompileError on wrong-length blake2f; got {:?}", + output.result + ); + + let additional = context.additional_limit.borrow(); + assert_eq!( + additional.get_usage().compute_gas, + forwarded_gas, + "reported compute is the whole parent-frame loss", + ); + assert_eq!( + additional.burned_compute_gas(), + forwarded_gas, + "generic error performed no work, so the whole envelope is destroyed", + ); + } + + /// REX7 + REX5 cap: `effective < gas_limit`, verification still runs. Destroyed is + /// `gas_limit − GAS_COST`, which includes the cap gap. Recording `effective − GAS_COST` + /// instead would drop that third piece of the parent-frame loss. + #[test] + fn test_kzg_precompile_rex7_cap_gap_is_destroyed_not_dropped() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7); + set_tx_compute_gas_limit(&mut context, MegaSpecId::REX7, GAS_COST + 1_000); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), + ); + let inputs = generate_invalid_proof_kzg_test_input(); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded_gas = 500_000u64; + + let result = + precompiles_map.run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)); + let output = result.expect("run ok").expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileError), + "expected PrecompileError on invalid-proof under the cap; got {:?}", + output.result + ); + // Halt Gas stays on the capped effective limit. + assert_eq!(output.gas.limit(), GAS_COST + 1_000); + + let additional = context.additional_limit.borrow(); + assert_eq!( + additional.get_usage().compute_gas - additional.burned_compute_gas(), + GAS_COST, + "only the fixed fee enforces", + ); + assert_eq!( + additional.burned_compute_gas(), + forwarded_gas - GAS_COST, + "destroyed includes the cap gap (forwarded − effective) plus the unused effective \ + remainder", + ); + } + + /// REX6 KZG verification failure stays on the historical single-lane recording: the + /// fixed fee is enforcing and nothing is destroyed. + #[test] + fn test_kzg_precompile_rex6_verification_failure_stays_single_lane() { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX6); + let mut precompiles_map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX6).precompiles(), + ); + let inputs = generate_invalid_proof_kzg_test_input(); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded_gas = 1_000_000u64; + + let result = + precompiles_map.run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)); + let output = result.expect("run ok").expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileError), + "expected PrecompileError on invalid-proof; got {:?}", + output.result + ); + + let additional = context.additional_limit.borrow(); + assert_eq!( + additional.get_usage().compute_gas, + GAS_COST, + "REX6 still records only the fixed fee", + ); + assert_eq!(additional.burned_compute_gas(), 0, "REX6 has no destroyed lane"); + } + /// Direct unit coverage for `PrecompileProvider::contains` on the Mega /// `PrecompilesMap` wrapper. /// diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 284cd154..78332840 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -366,6 +366,19 @@ impl AdditionalLimit { self.compute_gas.burned_usage() } + /// Records a destroyed remainder into the non-enforcing compute-gas lane (REX7+). + /// + /// Raises the reported total and leaves every limit comparison unchanged — the same + /// [`ComputeGasTracker::record_burned_gas`](compute_gas::ComputeGasTracker::record_burned_gas) + /// the interpreter-frame halt path uses. Precompile halt accounting calls this at the + /// recording site rather than through frame-exit settlement: a halt `Gas` is reset to + /// `Gas::new(limit)`, so the frame formula `remaining()` would double-count or miss the + /// forwarded-cap gap. + #[inline] + pub(crate) fn record_burned_gas(&mut self, amount: u64) { + self.compute_gas.record_burned_gas(amount); + } + /// Gets the usage of the additional limits. #[inline] pub fn get_usage(&self) -> LimitUsage { diff --git a/crates/mega-evm/tests/compute_gas/claims.rs b/crates/mega-evm/tests/compute_gas/claims.rs index b70aff44..5bfc2811 100644 --- a/crates/mega-evm/tests/compute_gas/claims.rs +++ b/crates/mega-evm/tests/compute_gas/claims.rs @@ -27,8 +27,9 @@ use revm::{ use crate::{ base_db, push_call_operands, push_valueless_call_operands, transact, transact_output, - transact_with_access_list, transact_with_envs, transact_with_limits, Outcome, CALLEE, CALLER, - CONTRACT, EMPTY_TARGET, EXISTING_TARGET, ONE_ETH, PRECOMPILE_IDENTITY, PRECOMPILE_KZG, + transact_with_access_list, transact_with_envs, transact_with_limits, + transact_with_limits_outcome, Outcome, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, EXISTING_TARGET, + ONE_ETH, PRECOMPILE_IDENTITY, PRECOMPILE_KZG, }; /// `remainingComputeGas()` — the `MegaLimitControl` selector the interceptor recognizes. @@ -256,12 +257,18 @@ fn test_refunds_do_not_reduce_compute_gas() { /// A direct transaction with empty calldata to the KZG precompile under a 30,000 tx compute /// limit: the intrinsic records 21,000, leaving 9,000 of transaction-level remainder. The /// forwarded cap is that remainder, which is below the precompile's 100,000 minimum cost, so the -/// invocation fails and records exactly the 9,000 cap — total recorded compute gas is exactly -/// the 30,000 limit. A cap that ignored the recorded intrinsic (forwarding 30,000) or one derived -/// from an undefined frame budget would shift the total away from the limit. +/// invocation fails without running verification. +/// +/// Through Rex6 that failure records the 9,000 cap as enforcing usage, so the reported total is +/// exactly the 30,000 limit. Rex7 still caps the work (verification does not run) but books the +/// parent-frame loss — the caller-supplied envelope, not the capped remainder — as destroyed, +/// so the reported total is the intrinsic plus that envelope and the enforced half stays at the +/// intrinsic alone. A cap that ignored the recorded intrinsic (forwarding 30,000) or one derived +/// from an undefined frame budget would still shift the enforced total away from these numbers. #[test] fn test_direct_precompile_transaction_cap_is_the_tx_level_remainder() { const TX_COMPUTE_LIMIT: u64 = 30_000; + const INTRINSIC_COMPUTE: u64 = 21_000; for (spec, spec_name) in crate::ALL_SPECS { if !spec.is_enabled(MegaSpecId::REX5) { @@ -269,6 +276,28 @@ fn test_direct_precompile_transaction_cap_is_the_tx_level_remainder() { } let limits = EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(TX_COMPUTE_LIMIT); + if spec.is_enabled(MegaSpecId::REX7) { + let outcome = + transact_with_limits_outcome(spec, base_db(Bytes::new()), PRECOMPILE_KZG, limits); + assert!( + matches!(outcome.result, ExecutionResult::Halt { .. }), + "{spec_name}: the underfunded direct precompile transaction should halt, got \ + {:?}", + outcome.result + ); + assert_eq!( + outcome.compute_gas_used - outcome.compute_gas_destroyed, + INTRINSIC_COMPUTE, + "{spec_name}: a wrapper OOG performed no work, so only the intrinsic enforces", + ); + assert!( + outcome.compute_gas_destroyed > TX_COMPUTE_LIMIT - INTRINSIC_COMPUTE, + "{spec_name}: destroyed is the caller-supplied envelope, not the capped \ + remainder ({})", + outcome.compute_gas_destroyed, + ); + continue; + } let (result, usage) = transact_with_limits(spec, base_db(Bytes::new()), PRECOMPILE_KZG, limits); assert!( diff --git a/crates/mega-evm/tests/compute_gas/main.rs b/crates/mega-evm/tests/compute_gas/main.rs index b2e0c0ab..87e7845b 100644 --- a/crates/mega-evm/tests/compute_gas/main.rs +++ b/crates/mega-evm/tests/compute_gas/main.rs @@ -227,6 +227,25 @@ fn transact_with_limits( (result.result, usage) } +/// [`transact_with_limits`] through [`MegaEvm::execute_transaction`], so a claim can read the +/// executed / destroyed split. +pub(crate) fn transact_with_limits_outcome( + spec: MegaSpecId, + mut db: MemoryDatabase, + to: Address, + limits: EvmTxRuntimeLimits, +) -> mega_evm::MegaTransactionOutcome { + let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default().caller(CALLER).call(to).gas_limit(100_000_000).build_fill(); + let mut tx = MegaTransaction(op_revm::OpTransaction::new(tx)); + tx.enveloped_tx = Some(Bytes::new()); + MegaEvm::new(context).execute_transaction(tx).expect("tx should not surface EVMError") +} + /// Runs the same transaction as [`transact`] and returns the transaction's output bytes. /// Used by the claim tests that read a value the contract computed (e.g. forwarded gas). fn transact_output(spec: MegaSpecId, mut db: MemoryDatabase) -> Bytes { @@ -858,11 +877,13 @@ fn test_compute_gas_snapshot_matches() { /// blessed by a regeneration. Comparing the readings directly makes the first accidental Rex7 /// divergence a failure. /// -/// The one sanctioned divergence is the exceptional-halt carve-out: a frame that halts +/// The sanctioned divergences are the exceptional-halt carve-out — a frame that halts /// exceptionally returns none of its remaining budget, and Rex7 settles that burned remainder as -/// compute gas where per-opcode recording attributes nothing to it. That moves compute gas upward -/// only — the receipt and the outcome still have to match exactly. `tests/rex7/exceptional_halt.rs` -/// pins the settled amount itself. +/// compute gas where per-opcode recording attributes nothing to it — and the same split applied +/// at the precompile recording site, which a precompile halt never reaches through frame-exit +/// settlement. Both move compute gas upward only; the receipt and the outcome still have to match +/// exactly. `tests/rex7/exceptional_halt.rs` and `tests/rex7/precompile_halt.rs` pin the settled +/// amounts themselves. #[test] fn test_rex7_matches_rex6_on_every_program() { for program in corpus() { @@ -879,7 +900,11 @@ fn test_rex7_matches_rex6_on_every_program() { rex7.gas_used, rex7.outcome, ); - if rex7.outcome.starts_with("halt ") { + // A top-level halt, or a precompile halt the caller absorbs, can raise the reported + // total without changing the receipt. The KZG invalid-input program is the corpus + // case that takes the latter path: the outer transaction succeeds, so the halt + // prefix alone would miss it. + if rex7.outcome.starts_with("halt ") || program.name == "precompile_kzg_invalid_input" { assert!( rex7.compute_gas >= rex6.compute_gas, "{}: the exceptional-halt carve-out only ever moves compute gas up \ diff --git a/crates/mega-evm/tests/compute_gas/snapshot.txt b/crates/mega-evm/tests/compute_gas/snapshot.txt index c417e4b2..ec62f2f8 100644 --- a/crates/mega-evm/tests/compute_gas/snapshot.txt +++ b/crates/mega-evm/tests/compute_gas/snapshot.txt @@ -340,7 +340,7 @@ precompile_kzg_invalid_input Rex3 23633 262633 success precompile_kzg_invalid_input Rex4 23633 262633 success precompile_kzg_invalid_input Rex5 123633 262633 success precompile_kzg_invalid_input Rex6 123633 262633 success -precompile_kzg_invalid_input Rex7 123633 262633 success +precompile_kzg_invalid_input Rex7 223633 262633 success precompile_underfunded Equivalence 0 21140 success precompile_underfunded MiniRex 23639 23640 success diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 6fc2b5e8..42356aeb 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -425,22 +425,20 @@ fn transact_create_reject( let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let result = - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let (usage, detained_compute_gas_limit) = { - let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); - (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) - }; - let gas_used = result.result.tx_gas_used(); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let detained_compute_gas_limit = + EvmTr::ctx_ref(&evm).additional_limit.borrow().detained_compute_gas_limit(); + let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { - result: result.result, - compute_gas: usage.compute_gas, - data_size: usage.data_size, - kv_updates: usage.kv_updates, - state_growth: usage.state_growth, + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, gas_used, + destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, - state: result.state, + state: outcome.result_and_state.state, } } diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 4f52d7f9..24e90230 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -37,6 +37,9 @@ pub(crate) struct Outcome { pub(crate) state_growth: u64, /// Receipt `gas_used` (combined compute + storage EVM gas). pub(crate) gas_used: u64, + /// The part of [`compute_gas`](Self::compute_gas) an exceptionally halted frame destroyed + /// rather than performed (REX7+, else 0). + pub(crate) destroyed: u64, /// Post-tx detained compute gas limit — equal to the configured TX limit unless volatile /// access lowered it. pub(crate) detained_compute_gas_limit: u64, @@ -57,6 +60,11 @@ impl Outcome { } } + /// The part of the reported compute total that a resource limit is evaluated against. + pub(crate) fn enforced(&self) -> u64 { + self.compute_gas - self.destroyed + } + /// Reads a storage slot out of the produced state, defaulting to zero when the transaction /// never touched it. pub(crate) fn storage_value(&self, address: Address, slot: U256) -> U256 { @@ -100,22 +108,20 @@ pub(crate) fn transact_with_gas_limit( let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let result = - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let (usage, detained_compute_gas_limit) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) - }; - let gas_used = result.result.tx_gas_used(); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let detained_compute_gas_limit = + evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { - result: result.result, - compute_gas: usage.compute_gas, - data_size: usage.data_size, - kv_updates: usage.kv_updates, - state_growth: usage.state_growth, + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, gas_used, + destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, - state: result.state, + state: outcome.result_and_state.state, } } @@ -154,22 +160,20 @@ pub(crate) fn transact_tx( let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let result = - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let (usage, detained_compute_gas_limit) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) - }; - let gas_used = result.result.tx_gas_used(); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let detained_compute_gas_limit = + evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { - result: result.result, - compute_gas: usage.compute_gas, - data_size: usage.data_size, - kv_updates: usage.kv_updates, - state_growth: usage.state_growth, + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, gas_used, + destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, - state: result.state, + state: outcome.result_and_state.state, } } @@ -298,21 +302,19 @@ pub(crate) fn transact_with_bucket_capacity( let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let result = - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let (usage, detained_compute_gas_limit) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.get_usage(), additional_limit.detained_compute_gas_limit()) - }; - let gas_used = result.result.tx_gas_used(); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let detained_compute_gas_limit = + evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { - result: result.result, - compute_gas: usage.compute_gas, - data_size: usage.data_size, - kv_updates: usage.kv_updates, - state_growth: usage.state_growth, + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, gas_used, + destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, - state: result.state, + state: outcome.result_and_state.state, } } diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index 23ffcf06..929ba23a 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -97,27 +97,26 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let result = - alloy_evm::Evm::transact_raw(&mut evm, tx).expect("tx should not surface EVMError"); - let (usage, detained_compute_gas_limit, remaining_compute_gas) = { + let executed = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let (detained_compute_gas_limit, remaining_compute_gas) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); ( - additional_limit.get_usage(), additional_limit.detained_compute_gas_limit(), additional_limit.current_call_remaining_compute_gas(), ) }; let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); - let gas_used = result.result.tx_gas_used(); + let gas_used = executed.result_and_state.result.tx_gas_used(); let outcome = Outcome { - result: result.result, - compute_gas: usage.compute_gas, - data_size: usage.data_size, - kv_updates: usage.kv_updates, - state_growth: usage.state_growth, + result: executed.result_and_state.result, + compute_gas: executed.compute_gas_used, + data_size: executed.data_size, + kv_updates: executed.kv_updates, + state_growth: executed.state_growth_used, gas_used, + destroyed: executed.compute_gas_destroyed, detained_compute_gas_limit, - state: result.state, + state: executed.result_and_state.state, }; GuardPassRun { outcome, accessed, remaining_compute_gas } } diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index d18c81f4..2d997b09 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -38,6 +38,9 @@ //! - `checkpoint_static_fee_edges` — a table-prepaid checkpoint (`GAS`, `LOG1`) whose static fee //! exceeds the clamp headroom is a plain-segment crossing; `CREATE`'s 32,000 is charged inside //! the body, so the same headroom runs the body and then reverts. +//! - `precompile_halt` — a precompile that halts exceptionally is split at the recording site the +//! same way an interpreter frame is: executed work enforces, the unused forwarded envelope does +//! not. mod burn_split; mod charge_on_reject; @@ -57,3 +60,4 @@ mod latch_surfacing; mod modexp_gas; mod opcode_set_parity; mod parity_shapes; +mod precompile_halt; diff --git a/crates/mega-evm/tests/rex7/precompile_halt.rs b/crates/mega-evm/tests/rex7/precompile_halt.rs new file mode 100644 index 00000000..291eebc7 --- /dev/null +++ b/crates/mega-evm/tests/rex7/precompile_halt.rs @@ -0,0 +1,284 @@ +//! A precompile that halts exceptionally is split the same way as an interpreter frame. +//! +//! A precompile runs inside `frame_init` and comes back as a result, so it never reaches the +//! interpreter-frame halt settlement. REX7 therefore splits it at the precompile recording site: +//! +//! - **Executed** — the work the precompile actually performed (the KZG fixed fee when verification +//! ran; zero when the input was rejected before any work). This is enforcing. +//! - **Destroyed** — the rest of the parent-frame loss, which is the caller-supplied forwarded +//! envelope, not the REX5-capped effective limit. This is reported and never enforced. +//! +//! Through REX6 the same recording site stays single-lane: success / revert still charge spent, +//! a KZG verification failure still charges the fixed fee, and every other error still charges +//! the (capped) limit as enforcing usage. + +use crate::common::{transact, transact_default, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{address, Address, Bytes}; +use mega_evm::{ + kzg_point_evaluation, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{CALL, INVALID, POP, STOP}; + +/// KZG point evaluation. +const KZG: Address = address!("000000000000000000000000000000000000000a"); +/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. +const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); + +/// Gas every probed CALL forwards. Far above both precompiles' real costs, so the destroyed +/// remainder dominates every other term and the REX5 forwarded-gas cap is a no-op unless a +/// test tightens the compute limit. +const FORWARDED: u64 = 1_000_000; + +/// A CALL forwarding [`FORWARDED`] gas to `target`, with 32 bytes of calldata from `mem[0..]` — +/// enough for both precompiles to reject it as malformed. The success flag is popped so the +/// caller survives, and `tail_pairs` plain pairs run afterwards. +fn call_then_work(target: Address, tail_pairs: usize) -> Bytes { + let mut builder = BytecodeBuilder::default() + .mstore(0, [0xAAu8; 32]) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(32u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(FORWARDED) + .append(CALL) + .append(POP); + for _ in 0..tail_pairs { + builder = builder.push_number(1u64).append(POP); + } + builder.append(STOP).build() +} + +/// Runs `code`, optionally deploying `callee` at [`CALLEE`]. +fn run( + spec: MegaSpecId, + code: Bytes, + callee: Option, + limits: EvmTxRuntimeLimits, +) -> Outcome { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, alloy_primitives::U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, alloy_primitives::U256::from(ONE_ETH)); + if let Some(callee) = callee { + db = db.account_code(CALLEE, callee); + } + transact(spec, db, limits) +} + +fn default_limits(spec: MegaSpecId) -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(spec) +} + +fn stop_code() -> Bytes { + BytecodeBuilder::default().append(STOP).build() +} + +fn invalid_code() -> Bytes { + BytecodeBuilder::default().append(INVALID).build() +} + +/// REX7 splits a precompile halt by actual work: KZG verification failure enforces the +/// fixed fee and destroys the rest of the forwarded envelope; a generic error (blake2f +/// malformed input) enforces nothing and destroys the whole envelope. An interpreter +/// frame that `INVALID`s in the same position is the control — same destroyed amount, +/// same unenforced remainder. +#[test] +fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { + let spec = MegaSpecId::REX7; + let limits = default_limits(spec); + let base = run(spec, call_then_work(CALLEE, 0), Some(stop_code()), limits); + let kzg = run(spec, call_then_work(KZG, 0), None, limits); + let blake = run(spec, call_then_work(BLAKE2F, 0), None, limits); + let interpreter = run(spec, call_then_work(CALLEE, 0), Some(invalid_code()), limits); + + for (label, r) in + [("baseline", &base), ("kzg", &kzg), ("blake2f", &blake), ("interpreter", &interpreter)] + { + assert!(r.is_success(), "{label}: the caller must absorb the failure: {:?}", r.result); + } + + let delta = |r: &Outcome| { + ( + r.compute_gas as i64 - base.compute_gas as i64, + r.enforced() as i64 - base.enforced() as i64, + r.destroyed as i64 - base.destroyed as i64, + r.gas_used as i64 - base.gas_used as i64, + ) + }; + + let (kzg_dc, kzg_de, kzg_dd, kzg_dg) = delta(&kzg); + let (blake_dc, blake_de, blake_dd, blake_dg) = delta(&blake); + let (interp_dc, interp_de, interp_dd, interp_dg) = delta(&interpreter); + + for (label, dg) in [("kzg", kzg_dg), ("blake2f", blake_dg), ("interpreter", interp_dg)] { + assert!( + dg >= FORWARDED as i64, + "{label}: the forwarded envelope must actually be lost; Δgas_used={dg}", + ); + } + + assert_eq!( + interp_dd, FORWARDED as i64, + "the control frame destroys exactly the forwarded envelope", + ); + assert_eq!(interp_de, 0, "and none of the control frame's envelope is enforced"); + assert_eq!( + interp_dc, FORWARDED as i64, + "the control's reported total is the destroyed envelope on top of the caller", + ); + + assert_eq!( + kzg_de, + kzg_point_evaluation::GAS_COST as i64, + "a KZG verification failure enforces the fixed fee, not the forwarded envelope", + ); + assert_eq!( + kzg_dd, + (FORWARDED - kzg_point_evaluation::GAS_COST) as i64, + "the rest of the forwarded envelope is destroyed, not enforced", + ); + assert_eq!(kzg_dc, FORWARDED as i64, "the reported total covers the whole parent-frame loss",); + + assert_eq!(blake_de, 0, "a generic precompile error performed no work, so nothing enforces"); + assert_eq!( + blake_dd, FORWARDED as i64, + "the whole forwarded envelope is destroyed on the generic error arm", + ); + assert_eq!(blake_dc, FORWARDED as i64, "the reported total still covers the parent-frame loss"); +} + +/// Through REX6 the same three shapes stay on the historical single-lane recording: KZG +/// charges the fixed fee as enforcing usage, the generic error charges the (capped) limit +/// as enforcing usage, and nothing is booked as destroyed. +#[test] +fn test_rex6_precompile_halt_accounting_is_unchanged() { + let spec = MegaSpecId::REX6; + let limits = default_limits(spec); + let base = run(spec, call_then_work(CALLEE, 0), Some(stop_code()), limits); + let kzg = run(spec, call_then_work(KZG, 0), None, limits); + let blake = run(spec, call_then_work(BLAKE2F, 0), None, limits); + let interpreter = run(spec, call_then_work(CALLEE, 0), Some(invalid_code()), limits); + + for (label, r) in + [("baseline", &base), ("kzg", &kzg), ("blake2f", &blake), ("interpreter", &interpreter)] + { + assert!(r.is_success(), "{label}: the caller must absorb the failure: {:?}", r.result); + assert_eq!(r.destroyed, 0, "{label}: REX6 has no destroyed lane"); + } + + let d = |r: &Outcome| r.compute_gas as i64 - base.compute_gas as i64; + assert_eq!( + d(&kzg), + kzg_point_evaluation::GAS_COST as i64, + "REX6 KZG still records only the fixed fee", + ); + assert_eq!(d(&blake), FORWARDED as i64, "REX6 generic error still records the whole envelope"); + assert_eq!( + d(&interpreter), + 0, + "REX6 attributes neither the failing opcode nor the destroyed remainder", + ); + assert_eq!(kzg.enforced(), kzg.compute_gas, "REX6 KZG charge is entirely enforcing"); + assert_eq!(blake.enforced(), blake.compute_gas, "REX6 generic charge is entirely enforcing"); +} + +/// The generic-arm destroyed remainder is not enforcing, so work after the failing CALL can +/// still run under the same compute limit that REX6 spends entirely on the envelope. REX6 +/// keeps starving the tail — that single-lane charge is frozen. +#[test] +fn test_generic_precompile_halt_does_not_starve_the_tail() { + const TAIL_PAIRS: usize = 2_000; + + let base = run( + MegaSpecId::REX7, + call_then_work(CALLEE, TAIL_PAIRS), + Some(stop_code()), + default_limits(MegaSpecId::REX7), + ); + assert!(base.is_success(), "the baseline shape must fit: {:?}", base.result); + + let limit = base.compute_gas + 5_000; + let limits7 = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit); + let limits6 = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6).with_tx_compute_gas_limit(limit); + + let blake7 = run(MegaSpecId::REX7, call_then_work(BLAKE2F, TAIL_PAIRS), None, limits7); + let interp7 = + run(MegaSpecId::REX7, call_then_work(CALLEE, TAIL_PAIRS), Some(invalid_code()), limits7); + let blake6 = run(MegaSpecId::REX6, call_then_work(BLAKE2F, TAIL_PAIRS), None, limits6); + let interp6 = + run(MegaSpecId::REX6, call_then_work(CALLEE, TAIL_PAIRS), Some(invalid_code()), limits6); + + assert!( + blake7.is_success(), + "REX7 does not enforce the generic-arm envelope, so the tail must run: {:?}", + blake7.result, + ); + assert!( + interp7.is_success(), + "the interpreter control's destroyed remainder is not enforcing either: {:?}", + interp7.result, + ); + assert!( + !blake6.is_success(), + "REX6 still enforces the generic-arm envelope, so the same tail must starve: {:?}", + blake6.result, + ); + assert!( + interp6.is_success(), + "REX6 still attributes nothing to an interpreter halt, so that tail survives: {:?}", + interp6.result, + ); +} + +/// When the REX5 forwarded-gas cap binds (`effective < gas_limit`), the parent still burns +/// the caller-supplied envelope. The gap is parent-frame loss, not work, so it lands in +/// the destroyed remainder rather than disappearing from both lanes. +#[test] +fn test_destroyed_remainder_includes_the_forwarded_cap_gap() { + let unconstrained = run( + MegaSpecId::REX7, + call_then_work(CALLEE, 0), + Some(stop_code()), + default_limits(MegaSpecId::REX7), + ); + assert!(unconstrained.is_success(), "calibration run must succeed: {:?}", unconstrained.result); + + // Comfortable room for the caller and the CALL body, nowhere near the forwarded envelope. + // The cap therefore binds: effective remaining is a few tens of thousands, forwarded is 1M. + let limit = unconstrained.compute_gas + 50_000; + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit); + + let blake = run(MegaSpecId::REX7, call_then_work(BLAKE2F, 0), None, limits); + assert!(blake.is_success(), "the caller must absorb the generic error: {:?}", blake.result); + assert_eq!( + blake.enforced() - unconstrained.enforced(), + 0, + "the generic arm still performed no work under the cap", + ); + assert_eq!( + blake.destroyed, FORWARDED, + "destroyed is the caller-supplied envelope, including the cap gap; \ + recording the effective limit instead would report only the leftover headroom", + ); +} + +/// Unconstrained default-limit reading, so a regression in the caller's own cost is visible +/// next to the split numbers rather than only inside a delta. +#[test] +fn test_kzg_halt_default_limits_report_the_full_parent_loss() { + let db = MemoryDatabase::default() + .account_balance(CALLER, alloy_primitives::U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, call_then_work(KZG, 0)) + .account_balance(CONTRACT, alloy_primitives::U256::from(ONE_ETH)); + let r = transact_default(MegaSpecId::REX7, db); + assert!(r.is_success(), "the caller must absorb the KZG failure: {:?}", r.result); + assert_eq!( + r.destroyed, + FORWARDED - kzg_point_evaluation::GAS_COST, + "default limits still leave the unused envelope in the destroyed lane", + ); +} From 95c4bdc77dc0aad8778f18f6b9844066df2551de Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 13:41:31 +0800 Subject: [PATCH 053/208] test(rex7): pin equal-value spend-all OutOfGas receipt against REX6 An equal-value clamp on EXP (the OutOfGas variant that spends all) charges the same gas_used as REX6 at the same point. The halt reason moves to a compute exceed; the receipt does not. --- .../tests/rex7/clamp_classification.rs | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index 2ef235d3..8167a71d 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -25,8 +25,8 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{ - CALL, DUP1, JUMPDEST, JUMPI, MSTORE, POP, RETURN, RETURNDATACOPY, RETURNDATASIZE, STOP, - SUB, SWAP1, + CALL, DUP1, EXP, JUMPDEST, JUMPI, MSTORE, POP, RETURN, RETURNDATACOPY, RETURNDATASIZE, + STOP, SUB, SWAP1, }, context::result::ExecutionResult, }; @@ -186,6 +186,95 @@ fn test_knife_edge_neighbours_classify_by_which_budget_binds() { ); } +/// The two shapes the spend-all knife edge needs: everything up to the `EXP`'s operands, and +/// the same thing with the `EXP` itself. A full-width exponent is charged through the plain +/// `gas!` macro, so a shortage is `InstructionResult::OutOfGas` — the variant +/// `Interpreter::halt` spends all remaining gas for. +fn spend_all_knife_edge_shapes() -> (Bytes, Bytes) { + let operands = |mut code: Vec| { + let mut builder = BytecodeBuilder::default(); + builder = builder.push_u256(U256::MAX).push_number(2u64); + code.extend_from_slice(&builder.build_vec()); + code + }; + let mut before = operands(plain_filler(20)); + before.push(STOP); + let mut full = operands(plain_filler(20)); + full.push(EXP); + full.push(STOP); + (Bytes::from(before), Bytes::from(full)) +} + +fn calibrate_spend_all_knife_edge() -> KnifeEdge { + let (before_code, full_code) = spend_all_knife_edge_shapes(); + let before = transact_default(MegaSpecId::REX7, base_db(before_code)); + let full = transact_default(MegaSpecId::REX7, base_db(full_code.clone())); + assert!(before.is_success(), "calibration run must succeed: {:?}", before.result); + assert!(full.is_success(), "calibration run must succeed: {:?}", full.result); + // The receipt carries compute gas plus MegaETH storage gas; only the compute half is what + // the compute limit bounds. Both calibration shapes must carry the same storage gas, or the + // two budgets cannot be lined up from these readings. + assert_eq!( + before.gas_used - before.compute_gas, + full.gas_used - full.compute_gas, + "the two calibration shapes must carry the same storage gas", + ); + + let crossing_cost = full.compute_gas - before.compute_gas; + assert!(crossing_cost > 1, "the EXP must have a real cost, got {crossing_cost}"); + KnifeEdge { + code: full_code, + // One gas short of the EXP on the EVM's own counter... + gas_limit: before.gas_used + crossing_cost - 1, + // ...and one gas short of it on the compute headroom, so the two coincide exactly. + compute_limit: before.compute_gas + crossing_cost - 1, + compute_before: before.compute_gas, + } +} + +/// An equal-value clamp on a spend-all out-of-gas charges the sender the same as REX6 at the +/// same point. The halt reason moves to a compute exceed; the receipt `gas_used` does not. +/// +/// The existing equal-value case above is a `MemoryOOG` (`MSTORE`), which does not spend all +/// and is already pinned for classification. This is the `OutOfGas` neighbour, where revm +/// zeroes the counter before the clamp restore, so rescue has nothing to hand back. +#[test] +fn test_equal_value_clamp_on_a_spend_all_out_of_gas_matches_rex6_gas_used() { + let edge = calibrate_spend_all_knife_edge(); + + let r7 = transact_with_gas_limit( + MegaSpecId::REX7, + base_db(edge.code.clone()), + compute_limit(edge.compute_limit)(MegaSpecId::REX7), + edge.gas_limit, + ); + let r6 = transact_with_gas_limit( + MegaSpecId::REX6, + base_db(edge.code.clone()), + compute_limit(edge.compute_limit)(MegaSpecId::REX6), + edge.gas_limit, + ); + + assert_eq!( + r7.gas_used, edge.gas_limit, + "an equal-value clamp on a spend-all out-of-gas returns nothing to the sender", + ); + assert_eq!( + r7.gas_used, r6.gas_used, + "REX6 and REX7 charge the sender identically at the equal-value spend-all edge", + ); + assert!( + matches!(r7.halt_reason("REX7"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "the equal-value clamp still classifies as a compute exceed; got {:?}", + r7.result, + ); + assert!( + !matches!(r6.halt_reason("REX6"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "REX6 reports the EVM's own out-of-gas, not a compute exceed; got {:?}", + r6.result, + ); +} + // --------------------------------------------------------------------------------------------- // The payload a clamp-induced exceed reports. // --------------------------------------------------------------------------------------------- From b1a06b1bb3d3fb016c9ffb7fe6de66f9b4eb4549 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 13:41:31 +0800 Subject: [PATCH 054/208] docs(rex7): specify precompile-halt split and inspector accounting contract Rex7 now documents the executed/destroyed split for a failing precompile, including the intentional generic-arm enforcement difference from Rex6. Inspector alignment is scoped to inspectors that do not rewrite frame results or edit interpreter gas. --- AGENTS.md | 1 + crates/mega-evm/src/evm/AGENTS.md | 3 +++ crates/mega-evm/src/evm/result.rs | 8 ++++++++ docs/spec/evm/compute-gas.md | 18 ++++++++++++++++-- docs/spec/hardfork-spec.md | 1 + docs/spec/upgrades/rex7.md | 24 ++++++++++++++++++++---- 6 files changed, 49 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3d95458d..09c2ded1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -118,6 +118,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a gas clamp. A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. + A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index f356ba84..9b5d17a0 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -21,6 +21,9 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - Oracle `sload` handling forces cold semantics for deterministic replay. - `MegaEvm` methods read aggregate resource usage from `additional_limit` after execution. - Keep inspector and non-inspector paths behaviorally aligned. + That alignment assumes the inspector does not rewrite a frame result and does not edit the interpreter gas counter. + A rewriting inspector — one that changes `CallOutcome` / `CreateOutcome` in `call_end` / `create_end`, or that spends or refunds interpreter gas in `step` / `step_end` — will make REX7 compute accounting diverge: the exceptional-halt burn split is settled before `frame_end`, and the plain-segment delta is measured from the interpreter counter, so either edit moves the reported `compute_gas_used` / `compute_gas_destroyed` off the uninspected path. + Observational inspectors (`NoOpInspector`, tracers that only read) stay aligned. ## WHERE TO LOOK - New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` still aliases `rex5` and expresses its deltas as `is_enabled` dispatch inside the shared handlers; `rex7` is a standalone checkpoint table built from revm's base table, with the 17 storage / CALL / CREATE / SELFDESTRUCT / not-yet-activated slots inherited from `rex6`, and with 15 volatile `*_checkpoint` handlers plus `gas_checkpoint` registered as rex7-only). diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index a14d4358..d4fed47f 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -34,6 +34,11 @@ pub struct MegaTransactionOutcome { /// exceptionally halted frame destroyed rather than performed. It is the number to report and /// to accumulate into block-level compute accounting; it is not the number to compare against /// a limit — see [`compute_gas_destroyed`](Self::compute_gas_destroyed). + /// + /// These two fields are the uninspected execution's split. + /// An inspector that rewrites a frame result or edits the interpreter gas counter will make + /// them diverge from that path: the burn split is settled before `frame_end`, and the + /// plain-segment delta is read from the interpreter counter. pub compute_gas_used: u64, /// The part of [`compute_gas_used`](Self::compute_gas_used) that exceptionally halted frames /// destroyed rather than performed (Rex7+, always 0 before). @@ -42,6 +47,9 @@ pub struct MegaTransactionOutcome { /// The transaction's own limits already excluded it while executing; a consumer that /// accumulates this outcome into a further limit — today the block compute-gas counter — must /// subtract it too, and compare `compute_gas_used - compute_gas_destroyed` instead. + /// + /// Same inspector caveat as [`compute_gas_used`](Self::compute_gas_used): the field is the + /// uninspected split, and a rewriting inspector will move it. pub compute_gas_destroyed: u64, /// The state growth used. pub state_growth_used: u64, diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 6d19fea1..43edd972 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -471,7 +471,8 @@ At each checkpoint a node MUST: 2. Record that segment amount as compute gas and evaluate the compute-gas limit (and any latched non-compute resource-limit exceed) at that checkpoint — the latch-surface point is the next checkpoint rather than the next per-opcode recording site. 3. Record the checkpoint opcode's own body under the measurement-window rules for its metering class, then re-open the settlement window. -Non-opcode recording sites on this page (intrinsic gas, precompiles, code deposit, KeylessDeploy) are unchanged. +Non-opcode recording sites on this page (intrinsic gas, successful or reverting precompiles, code deposit, KeylessDeploy) are unchanged. +A precompile that fails is split under the exceptional-halt carve-out below. For every transaction that stays within every runtime resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. @@ -515,6 +516,19 @@ A node MUST settle that whole budget as compute gas, split into two parts that a - **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). +A precompile invocation that fails is the same split, taken at the precompile recording site. +A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. + +- **Executed** — the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work. + A node MUST record that part through the ordinary enforcing path. +- **Destroyed** — the rest of the parent-frame loss: the caller-supplied call gas limit minus the executed part. + That loss is the uncapped forwarded envelope, not the Rex5-capped effective gas limit; when the cap binds, the gap belongs to the destroyed part. + A node MUST record it in the reported total and MUST NOT evaluate any resource limit against it. + +Through Rex6 the generic error arm recorded the effective gas limit as enforcing usage. +Under Rex7 that arm enforces nothing, which is a deliberate enforcement difference. +The Rex5 forwarded-gas cap is unchanged: a precompile still MUST NOT perform more work than the remaining compute budget. + The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. @@ -663,4 +677,4 @@ System-granted gas leaks to the sender, who recovers gas that was never theirs t - [Rex4](../upgrades/rex4.md) — introduced the per-call-frame compute gas budget; made gas detention caps relative to usage at the access point; added beneficiary volatile-access guards to the `CALL` family, `SELFDESTRUCT`, and `SELFBALANCE`. - [Rex5](../upgrades/rex5.md) — excluded the `CALL_STIPEND` from the forwarded-gas deduction; moved `CREATE2` memory-expansion recording ahead of the storage-gas charge; made contract-creation code-deposit compute gas atomic with the deployment commit; refined precompile compute-gas recording and bounded it by the remaining compute budget; added the `SELFDESTRUCT` empty-beneficiary storage-gas charge; removed `CALLCODE` from the cold first-touch charge and added `SELFDESTRUCT`'s beneficiary to it; stopped following EIP-7702 delegation in the pre-execution inspection, restoring inherited warmth for delegates. - [Rex6](../upgrades/rex6.md) — unified the measurement window across all storage-affecting opcodes and folded `CREATE2` memory expansion into it, ending the two-window exception; returned forwarded gas to the failing frame on a compute-gas exceed; rescued the unused envelope on a keyless-deploy dispatch exceed; made beneficiary detection delegation-aware, returning `CALLCODE` call targets to the cold first-touch charge; exempted system-originated transactions from the compute gas limit and gas detention. -- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit; splits a failing precompile the same way at the recording site. diff --git a/docs/spec/hardfork-spec.md b/docs/spec/hardfork-spec.md index b717ea36..84015532 100644 --- a/docs/spec/hardfork-spec.md +++ b/docs/spec/hardfork-spec.md @@ -155,5 +155,6 @@ REX7 is the current **unstable** spec under active development; its semantics ma - **Checkpoint-settled compute gas** — Plain opcodes record no compute gas between checkpoints; settlement runs at storage-gas opcodes, the CALL / CREATE family, volatile opcodes, `GAS`, and frame entry / resume / exit. - **Gas-clamp enforcement** — Between checkpoints the interpreter-visible remaining gas is clamped to the remaining compute headroom, so a compute-gas or detention exceed stops the crossing opcode before it executes (zero overshoot; crossing cost excluded from recorded usage). - **Exceptional-halt frame carve-out** — A frame that ends in an exceptional halt (including out-of-gas) settles its burned remainder as compute gas, so nested out-of-gas calls may report higher compute usage than REX6 while EVM gas and the receipt stay the same. + A failing precompile is split the same way: executed work enforces, the unused forwarded envelope is reported only. _See [Rex7 Network Upgrade](upgrades/rex7.md) for the full previous/new pairing._ diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 7ead53e5..93fc76c0 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -29,8 +29,11 @@ Rex7 also makes two guard- and detention-related choices that Rex6 does not: - A `disableVolatileDataAccess` rejection still charges the rejected opcode's static fee. - A detention mark is produced when the target account is loaded, so a frame that cannot afford the fees charged before that load produces no mark. -One deliberate accounting carve-out remains: a frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. +Two deliberate accounting carve-outs remain. +A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. +A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope is destroyed. +The generic error arm therefore stops enforcing the whole forwarded amount, which is an intentional enforcement difference from Rex6; the Rex5 forwarded-gas cap still prevents the precompile from performing more work than the remaining compute budget. ## What Changed @@ -69,7 +72,8 @@ At each checkpoint a node MUST: 2. Record that segment amount as compute gas and evaluate the compute-gas limit (and any latched non-compute resource-limit exceed) at that checkpoint. 3. Record the checkpoint opcode's own body compute gas under the same measurement-window rules that apply through Rex6 for that opcode class, then re-open the settlement window for the next segment. -Non-opcode recording sites (transaction intrinsic gas, precompiles, contract-creation code deposit, KeylessDeploy overhead and sandbox merge) are unchanged. +Non-opcode recording sites (transaction intrinsic gas, successful or reverting precompiles, contract-creation code deposit, KeylessDeploy overhead and sandbox merge) are unchanged. +A precompile that **fails** is the exception below. **Precision invariant.** For every transaction that stays within every runtime resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, a node MUST produce the same recorded compute-gas total, the same four-dimension resource usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 as under Rex6. @@ -95,6 +99,15 @@ This is the one shape where Rex7 enforcement is stricter than Rex6's, which attr A node MUST take the split from the frame's **final** result, after the create-return processing that can still turn a successful constructor into a canonical code-deposit out-of-gas, an EIP-3541 reject or a runtime code-size reject — each of which destroys the frame's remainder just as a halt from the interpreter loop does. When a nested execution merges its usage into an outer one, which today is only the `KeylessDeploy` sandbox boundary, a node MUST carry the split across that boundary: the outer transaction reports the inner total in full and enforces only its executed part. +A precompile invocation that fails is the same split, taken at the precompile recording site rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. +The **executed** part is the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work (malformed input, or a wrapper out-of-gas that never reached verification). +A node MUST record that part through the ordinary enforcing path. +The **destroyed** part is the rest of the parent-frame loss: the caller-supplied call gas limit, not the Rex5-capped effective gas limit. +When the cap binds, the gap between those two amounts is parent-frame loss rather than work, and a node MUST include it in the destroyed part. +Through Rex6 the generic error arm recorded the effective gas limit as enforcing usage; under Rex7 that arm enforces nothing. +That is a deliberate enforcement difference. +The Rex5 forwarded-gas cap is unchanged: a precompile still MUST NOT perform more work than the remaining compute budget. + Under per-opcode recording through Rex6, neither the failing opcode nor the destroyed remainder is attributed to compute gas. Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. @@ -206,6 +219,8 @@ A CALL or `EXTCODECOPY` that cannot afford the fees charged before the target ac A transaction that halts exceptionally, or that calls into a child frame which does, may report a higher transaction-level compute-gas total under Rex7 than under Rex6 — for any exceptional halt, not just out-of-gas. The receipt `gas_used`, the halt or revert reported, and the execution success or failure of the outer transaction are unchanged by the destroyed half of that carve-out: it is reported, never enforced. The executed half does enforce, so a contract that calls into a failing child and keeps working can trip a resource limit at the same point it would under Rex6 — and, for a child that ran out of gas with no clamp in force, marginally earlier. +A contract that calls a precompile which then fails is on the same split: work the precompile performed still binds the remaining compute budget; the unused forwarded envelope does not. +Under Rex6 that unused envelope was enforcing, so the same tail work can survive under Rex7 and starve under Rex6. ## Safety and Compatibility @@ -217,8 +232,9 @@ Any node, tool, or test fixture pinned to Rex7 must expect its results to move. A deployment that needs stable semantics must select a frozen spec explicitly rather than relying on the latest one. The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and enforced usage does not pass the limit by that opcode's cost. -Rex7 can report more compute gas than Rex6 for the same inputs on two paths: the exceptional-halt frame carve-out, which over-reports rather than under-reports, and a `disableVolatileDataAccess` rejection, which now includes the rejected opcode's static fee. -The carve-out's enforcing half is never looser than Rex6's, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. +Rex7 can report more compute gas than Rex6 for the same inputs on three paths: the exceptional-halt frame carve-out, which over-reports rather than under-reports; a failing precompile whose unused forwarded envelope is now reported as destroyed; and a `disableVolatileDataAccess` rejection, which now includes the rejected opcode's static fee. +The carve-out's enforcing half is never looser than Rex6's on interpreter frames, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. +On a precompile that fails before performing work, Rex7 enforcement is deliberately looser than Rex6's: the unused envelope does not bind the compute limit. ## References From 38068a3e02583d3dd7b164a79bbd871b8e4e1345 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 14:06:23 +0800 Subject: [PATCH 055/208] docs(rex7): anchor the destroyed part to the forwarded envelope and pin the claims value --- crates/mega-evm/src/evm/precompiles.rs | 19 +++++++++---------- crates/mega-evm/tests/compute_gas/claims.rs | 11 +++++------ crates/mega-evm/tests/rex7/precompile_halt.rs | 13 ++++++++----- docs/spec/evm/compute-gas.md | 3 ++- docs/spec/upgrades/rex7.md | 5 +++-- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 8ecefe2c..646133cf 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -328,19 +328,18 @@ impl PrecompileProvider= GAS_COST`): the declared fixed cost is the work performed. Address // match uses `bytecode_address` (see above) so DELEGATECALL/CALLCODE to KZG still hit // this arm. Through REX6 that amount is the whole recorded charge; REX7 keeps it on - // the enforcing lane and books the rest of the parent-frame loss — the + // the enforcing lane and books the rest of the forwarded envelope — the // caller-supplied `gas_limit`, not the REX5-capped effective limit — as destroyed. // The REX5 cap still prevents the precompile from *doing* more work than the - // remaining compute budget; the cap gap is parent-frame loss, not work, so it belongs - // with the destroyed remainder. + // remaining compute budget; the cap gap is part of the forwarded envelope, not work, + // so it belongs with the destroyed remainder. // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the // wrapper's pre-check itself OOG'd before verification could run): after the // spend_all undo above, `total_gas_spent() == 0` again. Through REX6 the parent still // permanently loses the forwarded amount, so those specs record `limit()` as // enforcing usage to match the EVM-gas burn. REX7 treats the same path as - // performed-zero / destroyed-all: no work ran, so nothing enforces, and the - // parent-frame loss (`gas_limit`, again the uncapped forwarded envelope) is reported - // only. + // performed-zero / destroyed-all: no work ran, so nothing enforces, and the forwarded + // envelope (`gas_limit`, again uncapped) is reported only. // // The split is computed here rather than by the interpreter-frame halt settlement. // A halt `Gas` is `Gas::new(limit)` after the undo above (`remaining() == limit()` @@ -994,7 +993,7 @@ mod tests { /// REX7 KZG verification failure: the fixed fee is the work performed (enforcing) and /// the rest of the caller-supplied envelope is destroyed. The reported total therefore - /// equals the parent-frame loss, not just the fixed fee. + /// equals the forwarded envelope, not just the fixed fee. #[test] fn test_kzg_precompile_rex7_verification_failure_splits_the_parent_loss() { let mut db = MemoryDatabase::default(); @@ -1019,7 +1018,7 @@ mod tests { assert_eq!( additional.get_usage().compute_gas, forwarded_gas, - "reported compute is the whole parent-frame loss", + "reported compute is the whole forwarded envelope", ); assert_eq!( additional.burned_compute_gas(), @@ -1060,7 +1059,7 @@ mod tests { assert_eq!( additional.get_usage().compute_gas, forwarded_gas, - "reported compute is the whole parent-frame loss", + "reported compute is the whole forwarded envelope", ); assert_eq!( additional.burned_compute_gas(), @@ -1071,7 +1070,7 @@ mod tests { /// REX7 + REX5 cap: `effective < gas_limit`, verification still runs. Destroyed is /// `gas_limit − GAS_COST`, which includes the cap gap. Recording `effective − GAS_COST` - /// instead would drop that third piece of the parent-frame loss. + /// instead would drop that third piece of the forwarded envelope. #[test] fn test_kzg_precompile_rex7_cap_gap_is_destroyed_not_dropped() { let mut db = MemoryDatabase::default(); diff --git a/crates/mega-evm/tests/compute_gas/claims.rs b/crates/mega-evm/tests/compute_gas/claims.rs index 5bfc2811..21e6ec94 100644 --- a/crates/mega-evm/tests/compute_gas/claims.rs +++ b/crates/mega-evm/tests/compute_gas/claims.rs @@ -261,7 +261,7 @@ fn test_refunds_do_not_reduce_compute_gas() { /// /// Through Rex6 that failure records the 9,000 cap as enforcing usage, so the reported total is /// exactly the 30,000 limit. Rex7 still caps the work (verification does not run) but books the -/// parent-frame loss — the caller-supplied envelope, not the capped remainder — as destroyed, +/// forwarded envelope — the caller-supplied envelope, not the capped remainder — as destroyed, /// so the reported total is the intrinsic plus that envelope and the enforced half stays at the /// intrinsic alone. A cap that ignored the recorded intrinsic (forwarding 30,000) or one derived /// from an undefined frame budget would still shift the enforced total away from these numbers. @@ -290,11 +290,10 @@ fn test_direct_precompile_transaction_cap_is_the_tx_level_remainder() { INTRINSIC_COMPUTE, "{spec_name}: a wrapper OOG performed no work, so only the intrinsic enforces", ); - assert!( - outcome.compute_gas_destroyed > TX_COMPUTE_LIMIT - INTRINSIC_COMPUTE, - "{spec_name}: destroyed is the caller-supplied envelope, not the capped \ - remainder ({})", - outcome.compute_gas_destroyed, + assert_eq!( + outcome.compute_gas_destroyed, 99_940_000, + "{spec_name}: destroyed is the caller-supplied envelope (the 98/100 forward of \ + the frame's remaining gas at the CALL), not the capped remainder", ); continue; } diff --git a/crates/mega-evm/tests/rex7/precompile_halt.rs b/crates/mega-evm/tests/rex7/precompile_halt.rs index 291eebc7..67b364fa 100644 --- a/crates/mega-evm/tests/rex7/precompile_halt.rs +++ b/crates/mega-evm/tests/rex7/precompile_halt.rs @@ -5,7 +5,7 @@ //! //! - **Executed** — the work the precompile actually performed (the KZG fixed fee when verification //! ran; zero when the input was rejected before any work). This is enforcing. -//! - **Destroyed** — the rest of the parent-frame loss, which is the caller-supplied forwarded +//! - **Destroyed** — the rest of the forwarded envelope, which is the caller-supplied forwarded //! envelope, not the REX5-capped effective limit. This is reported and never enforced. //! //! Through REX6 the same recording site stays single-lane: success / revert still charge spent, @@ -141,14 +141,17 @@ fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { (FORWARDED - kzg_point_evaluation::GAS_COST) as i64, "the rest of the forwarded envelope is destroyed, not enforced", ); - assert_eq!(kzg_dc, FORWARDED as i64, "the reported total covers the whole parent-frame loss",); + assert_eq!(kzg_dc, FORWARDED as i64, "the reported total covers the whole forwarded envelope",); assert_eq!(blake_de, 0, "a generic precompile error performed no work, so nothing enforces"); assert_eq!( blake_dd, FORWARDED as i64, "the whole forwarded envelope is destroyed on the generic error arm", ); - assert_eq!(blake_dc, FORWARDED as i64, "the reported total still covers the parent-frame loss"); + assert_eq!( + blake_dc, FORWARDED as i64, + "the reported total still covers the forwarded envelope" + ); } /// Through REX6 the same three shapes stay on the historical single-lane recording: KZG @@ -235,8 +238,8 @@ fn test_generic_precompile_halt_does_not_starve_the_tail() { } /// When the REX5 forwarded-gas cap binds (`effective < gas_limit`), the parent still burns -/// the caller-supplied envelope. The gap is parent-frame loss, not work, so it lands in -/// the destroyed remainder rather than disappearing from both lanes. +/// the caller-supplied envelope. The gap is part of the forwarded envelope, not work, so it lands +/// in the destroyed remainder rather than disappearing from both lanes. #[test] fn test_destroyed_remainder_includes_the_forwarded_cap_gap() { let unconstrained = run( diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 43edd972..b148fc8a 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -521,7 +521,8 @@ A precompile never becomes a child EVM frame, so the frame-exit settlement canno - **Executed** — the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work. A node MUST record that part through the ordinary enforcing path. -- **Destroyed** — the rest of the parent-frame loss: the caller-supplied call gas limit minus the executed part. +- **Destroyed** — the rest of the call's gas limit: the caller-supplied envelope minus the executed part. + On a value-transferring call the envelope includes the protocol-granted call stipend, so it can exceed what the parent itself funded. That loss is the uncapped forwarded envelope, not the Rex5-capped effective gas limit; when the cap binds, the gap belongs to the destroyed part. A node MUST record it in the reported total and MUST NOT evaluate any resource limit against it. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 93fc76c0..ee3e09e1 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -102,8 +102,9 @@ When a nested execution merges its usage into an outer one, which today is only A precompile invocation that fails is the same split, taken at the precompile recording site rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. The **executed** part is the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work (malformed input, or a wrapper out-of-gas that never reached verification). A node MUST record that part through the ordinary enforcing path. -The **destroyed** part is the rest of the parent-frame loss: the caller-supplied call gas limit, not the Rex5-capped effective gas limit. -When the cap binds, the gap between those two amounts is parent-frame loss rather than work, and a node MUST include it in the destroyed part. +The **destroyed** part is the rest of the call's gas limit — the caller-supplied envelope, not the Rex5-capped effective gas limit. +On a value-transferring call that envelope includes the protocol-granted call stipend, so it can exceed what the parent itself funded. +When the cap binds, the gap between the envelope and the effective limit is destroyed budget rather than work, and a node MUST include it in the destroyed part. Through Rex6 the generic error arm recorded the effective gas limit as enforcing usage; under Rex7 that arm enforces nothing. That is a deliberate enforcement difference. The Rex5 forwarded-gas cap is unchanged: a precompile still MUST NOT perform more work than the remaining compute budget. From 26be4e7a7c97dc1ead395452521b31ac2b7722d4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 14:47:08 +0800 Subject: [PATCH 056/208] test(rex7): kill T20 mutation survivors and record equivalent suppressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pin the REX7 disable-guard XOR corners, latch_clamp_exceed classification edges, and CheckpointTracker reset across reused contexts. Suppress the debug-only has_clamp→false equivalent and the i*=1 instruction_table timeout. Document the unguarded record_compute_gas bypass in AGENTS.md. --- AGENTS.md | 4 +- crates/mega-evm/src/limit/checkpoint.rs | 33 +++++ crates/mega-evm/src/limit/limit.rs | 116 ++++++++++++++++++ .../mega-evm/tests/rex7/charge_on_reject.rs | 75 +++++++++++ crates/mega-evm/tests/rex7/gas_clamp.rs | 48 ++++++++ mutants/suppressions.toml | 33 +++++ 6 files changed, 308 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 09c2ded1..c162ebab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -239,8 +239,10 @@ Correctness of the other three dimensions (data size, KV updates, state growth) The protocol governs mutation sites that run during execution; the REX6+ post-execution fee-reward accounting is deliberately outside it. That accounting merges usage into the transaction's reported totals and the block-level cumulative counters after the execution result is final, without latching, and never retroactively fails the transaction. -Rule 1 is backed by a `debug_assert!` in `record_compute_gas`: if a non-compute dimension is over its limit but not yet latched, the assert trips at the exact opcode whose mutation site forgot to call `check_limit()`. +Rule 1 is backed by a `debug_assert!` inside `record_compute_gas_impl`, reached through the guarded entry `record_compute_gas` (`GUARD_LATCH_PROTOCOL = true`): if a non-compute dimension is over its limit but not yet latched, the assert trips at the exact opcode whose mutation site forgot to call `check_limit()`. The sub-tracker checks are non-mutating, so the guard compiles out of release builds. +The same impl is also reached through `record_compute_gas_unguarded` (`GUARD_LATCH_PROTOCOL = false`), which skips the assert. +REX7 frame-exit tail settlement (`after_frame_run_instructions`) uses the unguarded entry: that settlement can observe SELFDESTRUCT pre-inner recorder usage that rule 2 deliberately left unlatched, because the frame is about to pop and discard it, and the guarded entry would trip the assert on that path. When adding an opcode or mutation site that touches a non-compute dimension, decide whether it records after or before its inner instruction, follow the matching case above, and add a test asserting the exceed halts at that opcode. diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index 8f43ee15..8fd95785 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -160,3 +160,36 @@ impl CheckpointTracker { gas_used } } + +#[cfg(test)] +mod tests { + use super::*; + + fn dirty_tracker() -> CheckpointTracker { + let mut tracker = CheckpointTracker::new(MegaSpecId::REX7); + tracker.sync_baseline(99_999); + tracker.set_clamp(7, ClampBinding { headroom: 1, frame_local: false, limit: 1 }); + tracker.set_latched_detained(true); + tracker + } + + /// `AdditionalLimit::reset` (called from `MegaContext::on_new_tx`) must wipe leftover + /// checkpoint state so a reused context cannot leak the previous transaction's baseline, + /// outstanding clamp, or detention-attribution flag into the next one. + #[test] + fn test_reset_clears_baseline_clamp_and_latched_detained() { + let mut tracker = dirty_tracker(); + + tracker.reset(); + + assert_eq!(tracker.baseline(), 0, "reset must drop the previous transaction's baseline"); + assert!( + tracker.take_clamp().is_none(), + "reset must drop an outstanding clamp left by the previous transaction" + ); + assert!( + !tracker.latched_detained(), + "reset must drop a leftover detention-attribution flag" + ); + } +} diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 78332840..27a6a0d5 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -1380,6 +1380,7 @@ mod tests { use revm::context::tx::TxEnvBuilder; use super::{super::LimitKind, *}; + use crate::VolatileDataAccess; fn test_limits() -> EvmTxRuntimeLimits { EvmTxRuntimeLimits { @@ -1549,6 +1550,121 @@ mod tests { ); } + fn oog_result() -> InterpreterResult { + InterpreterResult::new(InstructionResult::OutOfGas, Bytes::new(), Gas::new(100_000)) + } + + fn rex7_limit() -> AdditionalLimit { + AdditionalLimit::new(MegaSpecId::REX7, EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)) + } + + /// A frame-local clamp exceed while detention is active must not be attributed to + /// detention: the child reverts with `MegaLimitExceeded`, and `latched_detained` stays + /// clear so a later halt cannot be rewritten as `VolatileDataAccessOutOfGas`. + /// + /// Kills `&&` → `||` in `latch_clamp_exceed`: the `||` would fire on the detained-limit + /// arm alone and stamp the frame-local exceed as detained. + #[test] + fn test_latch_clamp_exceed_frame_local_with_detention_is_not_detained() { + let mut limit = rex7_limit(); + limit.set_compute_gas_limit(1); + assert!( + limit.compute_gas.detained_limit() < limit.compute_gas.base_tx_limit(), + "the fixture must actually tighten detention" + ); + limit.checkpoint.set_clamp( + 50, + compute_gas::ClampBinding { headroom: 100, frame_local: true, limit: 1_000 }, + ); + + limit.settle_frame_final_result(&mut oog_result()); + + assert!( + limit.has_exceeded_limit.is_frame_local(), + "the exceed must stay frame-local; got {:?}", + limit.has_exceeded_limit + ); + assert!( + !limit.checkpoint.latched_detained(), + "a frame-local clamp must not inherit detention attribution" + ); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::TIMESTAMP).is_none(), + "frame-local + detention must not classify as VolatileDataAccessOutOfGas" + ); + } + + /// A TX-level clamp exceed when detention did not tighten (`detained_limit == base`) + /// must stay a compute-gas halt, not `VolatileDataAccessOutOfGas`. + /// + /// Kills `<` → `<=` in `latch_clamp_exceed`: at equality the `<=` mutant stamps + /// `latched_detained` and the halt-reason remap would blame detention. + #[test] + fn test_latch_clamp_exceed_tx_level_without_tightened_detention_is_not_detained() { + let mut limit = rex7_limit(); + assert_eq!( + limit.compute_gas.detained_limit(), + limit.compute_gas.base_tx_limit(), + "the fixture is the detained == base knife edge" + ); + let tx_limit = limit.compute_gas.base_tx_limit(); + limit.checkpoint.set_clamp( + 50, + compute_gas::ClampBinding { headroom: 100, frame_local: false, limit: tx_limit }, + ); + + limit.settle_frame_final_result(&mut oog_result()); + + assert!( + !limit.checkpoint.latched_detained(), + "detained_limit == base_tx_limit must not count as a detained clamp" + ); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::TIMESTAMP).is_none(), + "a TX-level clamp with no tightened detention must not classify as \ + VolatileDataAccessOutOfGas" + ); + } + + /// The same `AdditionalLimit` is reused across transactions (`on_new_tx` calls `reset`). + /// Leftover checkpoint state from a detained clamp halt must not pollute the next + /// transaction's halt classification. + #[test] + fn test_reset_clears_checkpoint_so_the_next_tx_is_not_classified_as_detained() { + let mut limit = rex7_limit(); + limit.set_compute_gas_limit(1); + limit.checkpoint.sync_baseline(99_999); + limit.checkpoint.set_clamp( + 50, + compute_gas::ClampBinding { headroom: 100, frame_local: false, limit: 1 }, + ); + limit.settle_frame_final_result(&mut oog_result()); + assert!( + limit.checkpoint.latched_detained(), + "TX1's detained clamp must stamp the attribution flag" + ); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::TIMESTAMP).is_some(), + "TX1 must classify as a detained halt" + ); + + limit.reset(); + + assert_eq!(limit.checkpoint.baseline(), 0, "reset must drop TX1's baseline"); + assert!( + limit.checkpoint.take_clamp().is_none(), + "reset must drop any clamp TX1 left behind" + ); + assert!( + !limit.checkpoint.latched_detained(), + "reset must drop TX1's detention-attribution flag" + ); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::TIMESTAMP).is_none(), + "TX2 must not inherit TX1's VolatileDataAccessOutOfGas attribution" + ); + } + /// `mark_frame_result_as_exceeding_limit` rewrites both frame-result variants in place. #[test] fn test_mark_frame_result_as_exceeding_limit_rewrites_both_variants() { diff --git a/crates/mega-evm/tests/rex7/charge_on_reject.rs b/crates/mega-evm/tests/rex7/charge_on_reject.rs index 6774daf7..d5e638b0 100644 --- a/crates/mega-evm/tests/rex7/charge_on_reject.rs +++ b/crates/mega-evm/tests/rex7/charge_on_reject.rs @@ -13,6 +13,9 @@ //! A second set of tests pins the unaffordable-static-fee branch: the guarded frame is given //! just enough gas to reach the opcode and not enough to pay the entry, so the result is //! `OutOfGas` rather than a `VolatileDataAccessDisabled` revert. +//! +//! A third set pins the conjunction XOR corners: `disableVolatileDataAccess` is on, but the +//! target is not the oracle / beneficiary, so `SLOAD` / `SELFBALANCE` must still execute. use std::convert::Infallible; @@ -618,3 +621,75 @@ fn test_rejected_selfdestruct_charges_static_gas_only_on_rex7() { r7, ); } + +/// Runs `child_code` after the parent disables volatile access, and returns the child's +/// frame outcome. The child is a non-oracle, non-beneficiary address, so a conjunction +/// guard (`target == oracle/beneficiary && disabled`) must let the opcode execute. +fn run_child_executes( + spec: MegaSpecId, + opcode: u8, + child: Address, + child_code: Bytes, +) -> GuardedFrameOutcome { + let parent_code = append_call(call_disable(BytecodeBuilder::default()), child, 50_000_000) + .append(POP) + .stop() + .build(); + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(1_000_000)) + .account_code(PARENT, parent_code) + .account_code(child, child_code); + let mut inspector = GuardedFrameGasInspector::new(child, opcode); + let (success, _compute_gas, _gas_used) = transact_rejected( + spec, + &mut db, + TxEnvBuilder::default().caller(CALLER).call(PARENT).gas_limit(100_000_000).build_fill(), + &mut inspector, + ); + assert!(success, "{spec}: the parent must succeed when only the child is under test"); + inspector.outcome.unwrap_or_else(|| panic!("{spec}: child frame never ended")) +} + +fn assert_not_disable_revert(label: &str, spec: MegaSpecId, outcome: &GuardedFrameOutcome) { + assert_ne!( + outcome.result, + InstructionResult::Revert, + "{label}/{spec}: disabled but off-target must execute, not revert; output={:?}", + outcome.output + ); + let selector = IMegaAccessControl::VolatileDataAccessDisabled::SELECTOR; + assert!( + outcome.output.len() < 4 || outcome.output[..4] != selector, + "{label}/{spec}: must not revert with VolatileDataAccessDisabled; output={:?}", + outcome.output + ); +} + +/// XOR corner: `disableVolatileDataAccess` is on, but the `SLOAD` target is not the +/// oracle. The conjunction must stay false, so the load executes. +/// +/// Kills `&&` → `||` in `sload_checkpoint`: the `||` would reject on the disabled arm +/// alone. +#[test] +fn test_sload_at_non_oracle_with_access_disabled_executes() { + let code = + BytecodeBuilder::default().push_number(0_u64).append(SLOAD).append(POP).stop().build(); + let r6 = run_child_executes(MegaSpecId::REX6, SLOAD, CHILD, code.clone()); + let r7 = run_child_executes(MegaSpecId::REX7, SLOAD, CHILD, code); + assert_not_disable_revert("non-oracle SLOAD", MegaSpecId::REX6, &r6); + assert_not_disable_revert("non-oracle SLOAD", MegaSpecId::REX7, &r7); +} + +/// XOR corner: `disableVolatileDataAccess` is on, but the frame is not the beneficiary. +/// The conjunction must stay false, so `SELFBALANCE` executes. +/// +/// Kills `&&` → `||` in `selfbalance_checkpoint`: the `||` would reject on the disabled +/// arm alone. +#[test] +fn test_selfbalance_at_non_beneficiary_with_access_disabled_executes() { + let code = BytecodeBuilder::default().append(SELFBALANCE).append(POP).stop().build(); + let r6 = run_child_executes(MegaSpecId::REX6, SELFBALANCE, CHILD, code.clone()); + let r7 = run_child_executes(MegaSpecId::REX7, SELFBALANCE, CHILD, code); + assert_not_disable_revert("non-beneficiary SELFBALANCE", MegaSpecId::REX6, &r6); + assert_not_disable_revert("non-beneficiary SELFBALANCE", MegaSpecId::REX7, &r7); +} diff --git a/crates/mega-evm/tests/rex7/gas_clamp.rs b/crates/mega-evm/tests/rex7/gas_clamp.rs index 8b4a6112..8b9eb68b 100644 --- a/crates/mega-evm/tests/rex7/gas_clamp.rs +++ b/crates/mega-evm/tests/rex7/gas_clamp.rs @@ -92,6 +92,16 @@ fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { } } +/// Per-spec runtime limits with both the TX compute gas limit and the block-environment +/// detention cap replaced. +fn compute_and_detention(compute: u64, cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| { + EvmTxRuntimeLimits::from_spec(spec) + .with_tx_compute_gas_limit(compute) + .with_block_env_access_compute_gas_limit(cap) + } +} + /// The compute gas a transaction running `code` uses when nothing constrains it. fn unconstrained_compute_gas(code: Bytes) -> u64 { transact_default(MegaSpecId::REX7, base_db(code)).compute_gas @@ -262,6 +272,44 @@ fn test_frame_local_clamp_exceed_reverts_to_the_parent() { ); } +/// A TX-level clamp exceed after a volatile access that did not tighten the detained +/// limit (`detained_limit == base_tx_limit`) must stay a compute-gas halt. +/// +/// `TIMESTAMP` marks volatile access so the halt-reason remap consults +/// `latched_detained`, but the detention cap is far above the TX compute budget, so +/// `set_detained_limit` leaves the effective limit at the base. The `<` comparison in +/// `latch_clamp_exceed` is then false; `<=` would stamp the flag and rewrite the halt +/// as `VolatileDataAccessOutOfGas`. +#[test] +fn test_tx_level_clamp_exceed_without_tightened_detention_is_compute() { + let code = countdown_loop_code(&[TIMESTAMP, POP], 10_000); + let free = transact_default(MegaSpecId::REX7, base_db(code.clone())); + assert!(free.is_success(), "the unconstrained run must succeed: {:?}", free.result); + let with_timestamp = unconstrained_compute_gas( + BytecodeBuilder::default().append(TIMESTAMP).append(POP).append(STOP).build(), + ); + let midpoint = (with_timestamp + free.compute_gas) / 2; + // Far above the TX compute budget, so usage_at_access + cap cannot undercut it. + let limits = compute_and_detention(midpoint, 50_000_000); + + let r7 = transact(MegaSpecId::REX7, base_db(code), limits(MegaSpecId::REX7)); + + assert_eq!( + r7.detained_compute_gas_limit, midpoint, + "detention must not tighten: detained={} base={midpoint}", + r7.detained_compute_gas_limit + ); + match r7.halt_reason("REX7") { + MegaHaltReason::ComputeGasLimitExceeded { limit, .. } => { + assert_eq!(*limit, midpoint, "the reported limit is the TX compute limit"); + } + other => panic!( + "a TX-level clamp with detained_limit == base must be ComputeGasLimitExceeded, \ + not {other:?}" + ), + } +} + /// A detention crossing keeps its `VolatileDataAccessOutOfGas` attribution. /// /// Detention lowers the TX-level limit to `usage_at_access + cap`, so it is the TX-level constraint diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index cd8d4000..14472b91 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -359,3 +359,36 @@ file = "crates/mega-evm/src/evm/host.rs" mutant = "spec-gate self.spec.is_enabled(MegaSpecId::MINI_REX) && address == ORACLE_CONTRACT_ADDRESS, -> self.spec.is_enabled(MegaSpecId::EQUIVALENCE) && address == ORACLE_CONTRACT_ADDRESS," justification = "Equivalent: the mutated conjunct lives in the debug_assert! at the top of oracle_sload, which restates the sole caller's guard (sload_skip_cold_load, host.rs:181-182). is_enabled(EQUIVALENCE) is vacuously true, so the mutant only weakens an assertion that never fires on guarded code, and debug_assert! is compiled out of release builds. The guard's own spec-gate mutants are killed." reviewer = "RealiCZ (cz)" + +# --- checkpoint.rs:128 has_clamp -> false (equivalent: debug_assert only) --------- +# +# `CheckpointTracker::has_clamp` has exactly two call sites, both `debug_assert!(!has_clamp())` +# (limit.rs `checkpoint_clamp_amount` and `before_frame_run`). Those asserts fire only if a +# clamp is already outstanding when a new one is applied or a frame is resumed — an invariant +# violation that no green test reaches. Replacing the predicate with `false` makes +# `debug_assert!(!false)` always pass, so every currently-passing test stays green. Release +# builds compile the asserts out, leaving `has_clamp` with zero call sites. The sibling +# `has_clamp -> true` mutant *does* trip those asserts on every REX7 clamp path and is killed. +# Do not add a `#[should_panic]` test of the debug_assert. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/limit/checkpoint.rs" +mutant = "replace CheckpointTracker::has_clamp -> bool with false" +justification = "Equivalent: has_clamp is consumed only by two debug_assert!(!has_clamp()) sites (checkpoint_clamp_amount, before_frame_run). Those fire only on an outstanding-clamp invariant violation that no green test reaches, so forcing the predicate to false cannot change any passing test. Release builds compile the asserts out (zero call sites). The has_clamp -> true sibling is not equivalent and is killed by the same asserts." +reviewer = "RealiCZ (cz)" + +# --- instructions.rs:671 += → *= in rex7::instruction_table (dead: non-terminating) - +# +# The loop copies INHERITED_FROM_REX6 into the Rex7 table: `while i < len { ...; i += 1 }`. +# `i *= 1` is a no-op increment, so the loop never advances. The function is `const fn`, so +# the mutant hangs const evaluation / the test build rather than producing a distinguishable +# wrong table — structurally unkillable. The neighbouring `+=` → `-=` and the loop-bound +# `<` / `==` / `>` mutants do terminate and are killed by opcode-set tests. +[[suppress]] +kind = "line" +category = "dead" +file = "crates/mega-evm/src/evm/instructions.rs" +mutant = "replace += with *= in rex7::instruction_table" +justification = "Dead/non-terminating: i *= 1 does not advance the INHERITED_FROM_REX6 copy loop, so instruction_table (a const fn) never finishes evaluating. The mutant times out at build rather than yielding a wrong table that a test could reject. Structurally unkillable; the += → -= and loop-bound mutants on the same loop are caught." +reviewer = "RealiCZ (cz)" From 63444a4ddc346f6074e5f2ab793f0525e79cde29 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 15:54:10 +0800 Subject: [PATCH 057/208] feat(evm): take over the precompile seam and split KZG halts by reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MegaPrecompiles::run` no longer delegates to alloy-evm's `PrecompilesMap::run`. It mirrors that function so the accounting arms can read `PrecompileOutput.status` before `precompile_output_to_interpreter_result` folds every non-OOG halt into one opaque `PrecompileError` code. The `InterpreterResult` still comes out of that same public conversion, and a parity probe drives both paths over a dispatch-miss / success / revert / KZG-variant / gas-gate / generic-halt / OOG / DELEGATECALL matrix and compares the whole struct, so an alloy-evm bump that changes the upstream body turns red instead of silently shifting consensus. With the real halt reason in hand, REX7 splits KZG failures where they actually happen: an input whose length is not 192 bytes is rejected at the doorway before the commitment is read, so nothing was performed and the whole forwarded envelope is destroyed; every other non-OOG halt is raised once verification is under way and keeps the fixed fee on the enforcing lane. The non-doorway side is a wildcard on purpose — an unfamiliar upstream halt can only over-charge. REX6 and earlier are untouched: they still charge the fixed fee for every KZG halt past the wrapper's gas gate, doorway rejects included. The REX7 precompile-halt tests fed 32 bytes to KZG while asserting a verification failure. They now feed a real 192-byte invalid-proof vector for the priced arm and keep the 32-byte input as its own doorway case, with each delta measured against a baseline running byte-identical caller code. --- crates/mega-evm/src/evm/precompiles.rs | 508 ++++++++++++++++-- crates/mega-evm/tests/rex7/precompile_halt.rs | 265 ++++++--- docs/spec/evm/compute-gas.md | 4 +- docs/spec/upgrades/rex7.md | 6 +- 4 files changed, 669 insertions(+), 114 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 646133cf..f1ea9931 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -5,12 +5,16 @@ #[cfg(not(feature = "std"))] use alloc as std; -use std::{boxed::Box, string::String, sync::Arc}; +use std::{ + boxed::Box, + string::{String, ToString}, + sync::Arc, +}; -use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; +use crate::{ExternalEnvTypes, MegaContext, MegaInnerContext, MegaSpecId}; use alloy_evm::{ - precompiles::{DynPrecompile, PrecompilesMap}, - Database, + precompiles::{DynPrecompile, Precompile, PrecompileInput, PrecompilesMap}, + Database, EvmInternals, }; use delegate::delegate; use once_cell::race::OnceBox; @@ -18,9 +22,9 @@ use op_revm::OpSpecId; use revm::{ context::Cfg, context_interface::ContextTr, - handler::{EthPrecompiles, PrecompileProvider}, + handler::{precompile_output_to_interpreter_result, EthPrecompiles, PrecompileProvider}, interpreter::{CallInputs, Gas, InterpreterResult}, - precompile::Precompiles, + precompile::{PrecompileHalt, Precompiles}, primitives::{Address, AddressSet, HashMap}, }; @@ -224,6 +228,53 @@ impl Default for MegaPrecompiles { } } +/// Runs the precompile addressed by `inputs`, reporting the halt reason next to the +/// `InterpreterResult`. +/// +/// This is a mirror of `>>::run` +/// (`alloy_evm::precompiles`, alloy-evm 0.37.1) rather than a call into it. Delegating would +/// hand back only the converted `InterpreterResult`, and the conversion folds every non-out-of-gas +/// halt into one opaque `PrecompileError` code — so the caller could no longer tell a doorway +/// reject apart from a failure raised mid-computation, which is exactly the distinction the +/// compute-gas split needs. Mirroring keeps the raw `PrecompileStatus` in reach while the +/// `InterpreterResult` still comes out of the same public conversion, so the two paths agree +/// bit-for-bit (pinned by `test_precompile_mirror_matches_upstream_delegation`). +/// +/// Upgrading alloy-evm obliges a re-read of that upstream function: a new `PrecompileInput` +/// field, a different dispatch address, a result cache, or any other added step must be mirrored +/// here, or this silently stops being the same call. +fn run_precompile_capturing_halt( + precompiles: &PrecompilesMap, + context: &mut MegaInnerContext, + inputs: &CallInputs, +) -> Result)>, String> { + let Some(precompile) = precompiles.get(&inputs.bytecode_address) else { + return Ok(None); + }; + + let (block, tx, cfg, journaled_state, _, local) = context.all_mut(); + + let output = { + let _span = + tracing::trace_span!("precompile", name = precompile.precompile_id().name()).entered(); + precompile.call(PrecompileInput { + data: inputs.input.as_bytes_local(local).as_ref(), + gas: inputs.gas_limit, + reservoir: inputs.reservoir, + caller: inputs.caller, + value: inputs.call_value(), + is_static: inputs.is_static, + internals: EvmInternals::new(journaled_state, block, cfg, tx), + target_address: inputs.target_address, + bytecode_address: inputs.bytecode_address, + }) + } + .map_err(|e| e.to_string())?; + + let halt = output.status.halt_reason().cloned(); + Ok(Some((precompile_output_to_interpreter_result(output, inputs.gas_limit), halt))) +} + impl PrecompileProvider> for PrecompilesMap { @@ -271,15 +322,12 @@ impl PrecompileProvider>::run( - self, - &mut context.inner, - inputs, - )?; + // Run the precompile against the context `MegaContext` wraps. `halt` is the precompile's + // own halt reason, which the `InterpreterResult` no longer carries — the accounting arms + // below read it instead of trying to rebuild it from the collapsed instruction result. + let maybe_output = run_precompile_capturing_halt(self, &mut context.inner, inputs)?; - Ok(maybe_output.map(|mut output| { + Ok(maybe_output.map(|(mut output, halt)| { // Upstream revm-handler (`precompile_output_to_interpreter_result`) calls // `gas.spend_all()` for every non-success/non-revert precompile status, so // error-path Gas now reports `total_gas_spent() == limit`. Frozen REX5 @@ -325,17 +373,19 @@ impl PrecompileProvider= GAS_COST`): the declared fixed cost is the work performed. Address - // match uses `bytecode_address` (see above) so DELEGATECALL/CALLCODE to KZG still hit - // this arm. Through REX6 that amount is the whole recorded charge; REX7 keeps it on - // the enforcing lane and books the rest of the forwarded envelope — the + // `limit() >= GAS_COST`): the call got through the gas gate and into the upstream + // body, so how far it got decides what was performed. Address match uses + // `bytecode_address` (see above) so DELEGATECALL/CALLCODE to KZG still hit this arm. + // Through REX6 the fixed cost is charged for every halt this arm sees and is the + // whole recorded charge; REX7 splits it by halt reason (below), keeping the performed + // part on the enforcing lane and booking the rest of the forwarded envelope — the // caller-supplied `gas_limit`, not the REX5-capped effective limit — as destroyed. // The REX5 cap still prevents the precompile from *doing* more work than the // remaining compute budget; the cap gap is part of the forwarded envelope, not work, // so it belongs with the destroyed remainder. // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the - // wrapper's pre-check itself OOG'd before verification could run): after the - // spend_all undo above, `total_gas_spent() == 0` again. Through REX6 the parent still + // wrapper's pre-check itself OOG'd before the body could run): after the spend_all + // undo above, `total_gas_spent() == 0` again. Through REX6 the parent still // permanently loses the forwarded amount, so those specs record `limit()` as // enforcing usage to match the EVM-gas burn. REX7 treats the same path as // performed-zero / destroyed-all: no work ran, so nothing enforces, and the forwarded @@ -353,17 +403,30 @@ impl PrecompileProvider= kzg_point_evaluation::GAS_COST { - // KZG with the wrapper's `gas_limit < GAS_COST` pre-check passed: upstream - // verification ran and returned a non-OOG error - // (`BlobInvalidInputLength` / `BlobMismatchedVersion` / - // `BlobVerifyKzgProofFailed`). Charge the fixed cost regardless of which - // error variant fired. Using the structural predicate - // (`limit() >= GAS_COST`) instead of an error-variant match keeps this arm - // robust against upstream KZG adding new non-OOG variants. - let executed = kzg_point_evaluation::GAS_COST; - additional_limit.record_compute_gas(executed); - if is_rex7 { - additional_limit.record_burned_gas(gas_limit.saturating_sub(executed)); + // Inside the KZG body the halt reason marks how much of the fixed-price work + // the call bought before failing. `BlobInvalidInputLength` is the doorway: + // the input length is checked first, so a rejected length means the + // commitment was never read and nothing was computed. Every other halt is + // raised at or after the versioned-hash comparison, i.e. once verification is + // under way, and MegaETH prices verification as the whole fixed fee however + // far it got. + // + // The non-doorway side is a wildcard on purpose: an upstream KZG release that + // adds a halt reason lands there, which can only over-charge, never + // under-charge. So does the unreachable `halt == None` shape (a success or + // revert whose `gas_used` outran the gas limit and was converted to an + // out-of-gas), which no MegaETH-wired precompile can produce. + // + // REX6 and earlier take neither branch's split: they charge the fixed cost for + // every halt this arm sees, doorway rejects included. + if is_rex7 && matches!(halt, Some(PrecompileHalt::BlobInvalidInputLength)) { + additional_limit.record_burned_gas(gas_limit); + } else { + let executed = kzg_point_evaluation::GAS_COST; + additional_limit.record_compute_gas(executed); + if is_rex7 { + additional_limit.record_burned_gas(gas_limit.saturating_sub(executed)); + } } } else if is_rex7 { additional_limit.record_burned_gas(gas_limit); @@ -401,17 +464,20 @@ mod tests { use alloc as std; use std::{rc::Rc, vec::Vec}; - use super::{kzg_point_evaluation::GAS_COST, mini_rex, modexp, rex, MegaPrecompiles}; + use super::{ + kzg_point_evaluation::GAS_COST, mini_rex, modexp, rex, run_precompile_capturing_halt, + MegaPrecompiles, + }; use crate::{ test_utils::MemoryDatabase, AdditionalLimit, EvmTxRuntimeLimits, MegaContext, MegaSpecId, }; - use alloy_evm::precompiles::PrecompilesMap; + use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap}; use alloy_primitives::{Address, Bytes, U256}; use core::cell::RefCell; use revm::{ handler::PrecompileProvider, interpreter::{CallInputs, CallScheme, CallValue, InputsImpl, InstructionResult}, - precompile::{PrecompileHalt, PrecompileOutput, PrecompileStatus}, + precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileStatus}, primitives::eip7823, }; use sha2::{Digest, Sha256}; @@ -499,6 +565,37 @@ mod tests { inputs } + /// Mirror of `generate_kzg_test_input()` truncated to 191 bytes — one short of the + /// required 192. Upstream rejects it at the doorway with + /// `PrecompileHalt::BlobInvalidInputLength`, before the commitment is looked at. + fn generate_wrong_length_kzg_test_input() -> InputsImpl { + let mut inputs = generate_kzg_test_input(); + let bytes = match inputs.input { + revm::interpreter::CallInput::Bytes(b) => b, + _ => panic!("expected Bytes"), + }; + let mut buf = bytes.to_vec(); + buf.pop(); + assert_eq!(buf.len(), 191, "the doorway probe must be one byte short of 192"); + inputs.input = revm::interpreter::CallInput::Bytes(Bytes::from(buf)); + inputs + } + + /// Mirror of `generate_kzg_test_input()` with the versioned hash's trailing byte flipped — + /// still 192 bytes, so upstream clears the length doorway and fails the following + /// commitment/versioned-hash comparison with `PrecompileHalt::BlobMismatchedVersion`. + fn generate_mismatched_version_kzg_test_input() -> InputsImpl { + let mut inputs = generate_kzg_test_input(); + let bytes = match inputs.input { + revm::interpreter::CallInput::Bytes(b) => b, + _ => panic!("expected Bytes"), + }; + let mut buf = bytes.to_vec(); + buf[31] ^= 0x01; + inputs.input = revm::interpreter::CallInput::Bytes(Bytes::from(buf)); + inputs + } + fn set_spec_for_context( precompiles_map: &mut PrecompilesMap, _context: &MegaContext, @@ -1139,6 +1236,349 @@ mod tests { assert_eq!(additional.burned_compute_gas(), 0, "REX6 has no destroyed lane"); } + // ── KZG failure split: doorway reject vs. verification under way ──────────────── + // + // A KZG call that clears the wrapper's gas gate can still fail in two very different + // places, and REX7 prices them differently. The probes below pin each variant on its own + // side of the split, pin the frozen REX6 amounts for the same inputs, and pin the upstream + // check order the split is derived from. + + /// Drives a KZG call that fails through the wired precompile table on `spec`. + /// + /// Returns the collapsed instruction result together with the tracker's reported and + /// destroyed compute gas, so each probe can state the split as + /// `reported - destroyed == enforced`. + fn record_kzg_failure( + spec: MegaSpecId, + inputs: &InputsImpl, + forwarded_gas: u64, + ) -> (InstructionResult, u64, u64) { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, spec); + let mut precompiles_map = + PrecompilesMap::from_static(MegaPrecompiles::new_with_spec(spec).precompiles()); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + + let result = + precompiles_map.run(&mut context, &call_inputs(inputs, address, true, forwarded_gas)); + let output = result.expect("run ok").expect("Some output"); + assert!(!output.result.is_ok_or_revert(), "the probe must fail; got {:?}", output.result,); + + let additional = context.additional_limit.borrow(); + (output.result, additional.get_usage().compute_gas, additional.burned_compute_gas()) + } + + /// The premise the REX7 split rests on: upstream checks the input length before it reads + /// the commitment, so a wrong length is a doorway reject and a mismatched versioned hash + /// is not. If upstream ever reorders those checks, the split's "nothing was computed" + /// claim stops holding and this turns red before the accounting probes do. + #[test] + fn test_kzg_halt_reasons_distinguish_the_doorway_from_verification() { + let map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), + ); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + + for (label, inputs, expected) in [ + ( + "wrong length", + generate_wrong_length_kzg_test_input(), + PrecompileHalt::BlobInvalidInputLength, + ), + ( + "mismatched versioned hash", + generate_mismatched_version_kzg_test_input(), + PrecompileHalt::BlobMismatchedVersion, + ), + ( + "invalid proof", + generate_invalid_proof_kzg_test_input(), + PrecompileHalt::BlobVerifyKzgProofFailed, + ), + ] { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7); + let (_, halt) = run_precompile_capturing_halt( + &map, + &mut context.inner, + &call_inputs(&inputs, address, true, 1_000_000), + ) + .expect("run ok") + .expect("Some output"); + assert_eq!(halt, Some(expected), "{label}: unexpected KZG halt reason"); + } + } + + /// REX7 splits KZG failures by where they were raised: a doorway reject performed no work, + /// everything past the doorway is priced at the whole fixed fee. + #[test] + fn test_kzg_precompile_rex7_splits_failures_by_halt_reason() { + let forwarded = 1_000_000u64; + + let (result, reported, destroyed) = record_kzg_failure( + MegaSpecId::REX7, + &generate_wrong_length_kzg_test_input(), + forwarded, + ); + assert!( + matches!(result, InstructionResult::PrecompileError), + "a wrong-length reject is not an out-of-gas; got {result:?}", + ); + assert_eq!(reported, forwarded, "reported compute is the whole forwarded envelope"); + assert_eq!(destroyed, forwarded, "a doorway reject performed no work"); + assert_eq!(reported - destroyed, 0, "so nothing enforces"); + + for (label, inputs) in [ + ("mismatched versioned hash", generate_mismatched_version_kzg_test_input()), + ("invalid proof", generate_invalid_proof_kzg_test_input()), + ] { + let (_, reported, destroyed) = record_kzg_failure(MegaSpecId::REX7, &inputs, forwarded); + assert_eq!(reported, forwarded, "{label}: reported compute is the whole envelope"); + assert_eq!( + reported - destroyed, + GAS_COST, + "{label}: verification was under way, so the fixed fee is the work performed", + ); + } + } + + /// REX6 pin for the same three inputs: the frozen spec charges the fixed fee for every KZG + /// failure it sees, doorway rejects included, and has no destroyed lane. Any drift in the + /// REX7 arm that leaked back into the shared code path shows up here. + #[test] + fn test_kzg_precompile_rex6_charges_the_fixed_cost_for_every_failure() { + let forwarded = 1_000_000u64; + for (label, inputs) in [ + ("wrong length", generate_wrong_length_kzg_test_input()), + ("mismatched versioned hash", generate_mismatched_version_kzg_test_input()), + ("invalid proof", generate_invalid_proof_kzg_test_input()), + ] { + let (_, reported, destroyed) = record_kzg_failure(MegaSpecId::REX6, &inputs, forwarded); + assert_eq!(reported, GAS_COST, "{label}: REX6 records only the fixed fee"); + assert_eq!(destroyed, 0, "{label}: REX6 has no destroyed lane"); + } + } + + /// The knife edge of the fixed-cost arm's `limit() >= GAS_COST` predicate, taken on a + /// doorway-rejected input. + /// + /// At exactly `GAS_COST` the wrapper's `gas_limit < GAS_COST` pre-check passes by one, the + /// call reaches the length doorway, and the split books the envelope as destroyed. One gas + /// lower the wrapper halts out of gas before the doorway, which is the generic error arm. + /// Both arms destroy the same envelope under REX7, so the edge is invisible there — but + /// REX6 charges the fixed fee on one side and the (equal) forwarded limit on the other, and + /// pinning both keeps the predicate's boundary from drifting unnoticed. + #[test] + fn test_kzg_precompile_wrong_length_at_the_fixed_cost_boundary() { + let inputs = generate_wrong_length_kzg_test_input(); + + let (result, reported, destroyed) = record_kzg_failure(MegaSpecId::REX7, &inputs, GAS_COST); + assert!( + matches!(result, InstructionResult::PrecompileError), + "at exactly GAS_COST the wrapper's gas gate passes; got {result:?}", + ); + assert_eq!(reported, GAS_COST); + assert_eq!(destroyed, GAS_COST, "the doorway reject destroys the whole envelope"); + + let (result, reported, destroyed) = + record_kzg_failure(MegaSpecId::REX7, &inputs, GAS_COST - 1); + assert!( + matches!(result, InstructionResult::PrecompileOOG), + "one gas below GAS_COST the wrapper's gate halts first; got {result:?}", + ); + assert_eq!(reported, GAS_COST - 1); + assert_eq!(destroyed, GAS_COST - 1, "the generic arm destroys the envelope too"); + + // Frozen side of the same edge. + let (_, reported, destroyed) = record_kzg_failure(MegaSpecId::REX6, &inputs, GAS_COST); + assert_eq!(reported, GAS_COST, "REX6 charges the fixed fee at the boundary"); + assert_eq!(destroyed, 0); + let (_, reported, destroyed) = record_kzg_failure(MegaSpecId::REX6, &inputs, GAS_COST - 1); + assert_eq!(reported, GAS_COST - 1, "REX6 charges the forwarded limit below it"); + assert_eq!(destroyed, 0); + } + + // ── seam parity: the mirror must still be the upstream call ────────────────────── + // + // `run_precompile_capturing_halt` reimplements alloy-evm's `PrecompilesMap::run` so the + // precompile's halt reason survives the conversion. That is only safe while the + // reimplementation produces exactly what delegating would have produced, and nothing in + // the type system holds it there — an alloy-evm bump can change the upstream body + // underneath it. The probe below drives both paths over the same inputs and compares the + // whole `InterpreterResult`, so an upstream change the mirror has not picked up turns red + // here rather than silently shifting consensus. + + /// Address the parity matrix installs a dynamic precompile at. Outside the builtin table, + /// so with the dynamic precompile absent the same case doubles as a dispatch-miss probe. + const PARITY_DYN_ADDRESS: Address = Address::with_last_byte(0x7e); + + /// Builds the table both paths run against. + /// + /// `with_dynamic` installs a reverting precompile at [`PARITY_DYN_ADDRESS`], which also + /// flips the map into its dynamic representation — the `PrecompilesMap::get` branch the + /// builtin table never reaches, and the only way to produce a reverting precompile at all + /// (no builtin returns `PrecompileStatus::Revert`). + fn parity_precompiles(with_dynamic: bool) -> PrecompilesMap { + let mut map = PrecompilesMap::from_static( + MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), + ); + if with_dynamic { + map.apply_precompile(&PARITY_DYN_ADDRESS, |_| { + Some(DynPrecompile::new(PrecompileId::Custom("parity-revert".into()), |input| { + Ok(PrecompileOutput::revert( + input.gas / 4, + Bytes::from_static(b"reverted"), + input.reservoir, + )) + })) + }); + } + map + } + + /// One case per distinguishable path through the upstream body: dispatch miss, revert, + /// success, each KZG failure variant, the wrapper's own gas gate, a generic + /// malformed-input halt, an out-of-gas halt, and the DELEGATECALL bytecode-vs-target split. + fn parity_cases() -> Vec<(&'static str, CallInputs)> { + let kzg = revm::precompile::kzg_point_evaluation::ADDRESS; + let ecrecover = Address::with_last_byte(1); + let identity = Address::with_last_byte(4); + let blake2f = Address::with_last_byte(9); + let not_a_precompile = Address::with_last_byte(0xff); + + let plain = |address: Address, data: Vec| InputsImpl { + target_address: address, + bytecode_address: Some(address), + caller_address: address, + input: revm::interpreter::CallInput::Bytes(Bytes::from(data)), + call_value: Default::default(), + }; + + vec![ + ( + "dispatch miss", + call_inputs( + &plain(not_a_precompile, vec![1, 2, 3]), + not_a_precompile, + true, + 1_000_000, + ), + ), + ( + "dynamic revert", + call_inputs( + &plain(PARITY_DYN_ADDRESS, vec![7; 8]), + PARITY_DYN_ADDRESS, + true, + 1_000_000, + ), + ), + ("success", call_inputs(&plain(identity, vec![0xAB; 64]), identity, true, 1_000_000)), + ("kzg success", call_inputs(&generate_kzg_test_input(), kzg, true, 1_000_000)), + ( + "kzg wrong length", + call_inputs(&generate_wrong_length_kzg_test_input(), kzg, true, 1_000_000), + ), + ( + "kzg mismatched versioned hash", + call_inputs(&generate_mismatched_version_kzg_test_input(), kzg, true, 1_000_000), + ), + ( + "kzg invalid proof", + call_inputs(&generate_invalid_proof_kzg_test_input(), kzg, true, 1_000_000), + ), + ( + "kzg below the fixed cost", + call_inputs(&generate_kzg_test_input(), kzg, true, GAS_COST - 1), + ), + ( + "blake2f malformed input", + call_inputs(&plain(blake2f, vec![0xAA; 32]), blake2f, true, 1_000_000), + ), + ( + "ecrecover out of gas", + call_inputs(&plain(ecrecover, vec![0u8; 128]), ecrecover, true, 100), + ), + ( + "delegatecall to kzg", + call_inputs_with_scheme( + &generate_invalid_proof_kzg_test_input(), + Address::repeat_byte(0xAB), + kzg, + true, + 200_000, + CallScheme::DelegateCall, + ), + ), + ] + } + + #[test] + fn test_precompile_mirror_matches_upstream_delegation() { + for with_dynamic in [false, true] { + for (label, inputs) in parity_cases() { + // Mirror path. + let mut mirror_db = MemoryDatabase::default(); + let mut mirror_ctx = MegaContext::new(&mut mirror_db, MegaSpecId::REX7); + let mirror_map = parity_precompiles(with_dynamic); + let mirrored = + run_precompile_capturing_halt(&mirror_map, &mut mirror_ctx.inner, &inputs) + .expect("the mirror must not fail fatally"); + + // Delegated path: alloy-evm's own provider impl, reached exactly as the + // pre-mirror code reached it. + let mut delegate_db = MemoryDatabase::default(); + let mut delegate_ctx = MegaContext::new(&mut delegate_db, MegaSpecId::REX7); + let mut delegate_map = parity_precompiles(with_dynamic); + let delegated = PrecompileProvider::>::run( + &mut delegate_map, + &mut delegate_ctx.inner, + &inputs, + ) + .expect("delegation must not fail fatally"); + + match (mirrored, delegated) { + (None, None) => {} + (Some((mirror_result, halt)), Some(delegate_result)) => { + assert_eq!( + mirror_result.result, delegate_result.result, + "{label} (dynamic table: {with_dynamic}): instruction result", + ); + assert_eq!( + mirror_result.output, delegate_result.output, + "{label} (dynamic table: {with_dynamic}): output bytes", + ); + assert_eq!( + mirror_result.gas, delegate_result.gas, + "{label} (dynamic table: {with_dynamic}): gas", + ); + // Whole-struct equality also covers `Gas`'s private tracker and memory + // fields, which the accessors above do not reach. + assert_eq!( + mirror_result, delegate_result, + "{label} (dynamic table: {with_dynamic})", + ); + // The captured signal must agree with the code the conversion collapsed + // it into; a halt reason next to a success would mean the mirror read + // the wrong output. + assert_eq!( + halt.is_some(), + !mirror_result.result.is_ok_or_revert(), + "{label} (dynamic table: {with_dynamic}): captured halt reason must \ + match the collapsed result", + ); + } + (mirror, delegate) => panic!( + "{label} (dynamic table: {with_dynamic}): dispatch disagreed — mirror \ + returned {:?}, delegation returned {:?}", + mirror.is_some(), + delegate.is_some(), + ), + } + } + } + } + /// Direct unit coverage for `PrecompileProvider::contains` on the Mega /// `PrecompilesMap` wrapper. /// diff --git a/crates/mega-evm/tests/rex7/precompile_halt.rs b/crates/mega-evm/tests/rex7/precompile_halt.rs index 67b364fa..ee0d12a5 100644 --- a/crates/mega-evm/tests/rex7/precompile_halt.rs +++ b/crates/mega-evm/tests/rex7/precompile_halt.rs @@ -3,14 +3,19 @@ //! A precompile runs inside `frame_init` and comes back as a result, so it never reaches the //! interpreter-frame halt settlement. REX7 therefore splits it at the precompile recording site: //! -//! - **Executed** — the work the precompile actually performed (the KZG fixed fee when verification -//! ran; zero when the input was rejected before any work). This is enforcing. +//! - **Executed** — the work the precompile actually performed (the KZG fixed fee when the call +//! reached verification; zero when the input was rejected before any work). This is enforcing. //! - **Destroyed** — the rest of the forwarded envelope, which is the caller-supplied forwarded //! envelope, not the REX5-capped effective limit. This is reported and never enforced. //! +//! KZG therefore lands on both sides of the split depending on where it failed: an input whose +//! length is not 192 bytes is turned away at the doorway, before the commitment is read, while +//! any failure past that point means verification was under way and is priced at the whole fixed +//! fee. +//! //! Through REX6 the same recording site stays single-lane: success / revert still charge spent, -//! a KZG verification failure still charges the fixed fee, and every other error still charges -//! the (capped) limit as enforcing usage. +//! every KZG failure past the wrapper's gas gate charges the fixed fee — doorway rejects +//! included — and every other error still charges the (capped) limit as enforcing usage. use crate::common::{transact, transact_default, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH}; use alloy_primitives::{address, Address, Bytes}; @@ -20,6 +25,7 @@ use mega_evm::{ EvmTxRuntimeLimits, MegaSpecId, }; use revm::bytecode::opcode::{CALL, INVALID, POP, STOP}; +use sha2::{Digest, Sha256}; /// KZG point evaluation. const KZG: Address = address!("000000000000000000000000000000000000000a"); @@ -31,15 +37,54 @@ const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); /// test tightens the compute limit. const FORWARDED: u64 = 1_000_000; -/// A CALL forwarding [`FORWARDED`] gas to `target`, with 32 bytes of calldata from `mem[0..]` — -/// enough for both precompiles to reject it as malformed. The success flag is popped so the -/// caller survives, and `tail_pairs` plain pairs run afterwards. -fn call_then_work(target: Address, tail_pairs: usize) -> Bytes { +/// Calldata short enough that every precompile probed here rejects it on length alone — for KZG +/// that is the doorway reject, for blake2f the generic malformed-input error. +fn malformed_calldata() -> Vec { + vec![0xAAu8; 32] +} + +/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped. +/// +/// Still 192 bytes with a matching versioned hash, so KZG clears the length doorway and the +/// commitment comparison and fails inside proof verification — the priced side of the split. +fn verification_failure_calldata() -> Vec { + let commitment = hex::decode( + "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ + 5f26936857bc3a7c2539ea8ec3a952b7", + ) + .unwrap(); + let mut versioned_hash = Sha256::digest(&commitment).to_vec(); + versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG + let z = + hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); + let y = + hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); + let proof = hex::decode( + "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ + 4faf0070725e00b60ad9a026a15b1a8c", + ) + .unwrap(); + + let mut input = Vec::new(); + input.extend_from_slice(&versioned_hash); + input.extend_from_slice(&z); + input.extend_from_slice(&y); + input.extend_from_slice(&commitment); + input.extend_from_slice(&proof); + assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); + let last = input.len() - 1; + input[last] ^= 0x01; + input +} + +/// A CALL forwarding [`FORWARDED`] gas to `target`, with `calldata` laid out at `mem[0..]`. The +/// success flag is popped so the caller survives, and `tail_pairs` plain pairs run afterwards. +fn call_then_work(target: Address, calldata: &[u8], tail_pairs: usize) -> Bytes { let mut builder = BytecodeBuilder::default() - .mstore(0, [0xAAu8; 32]) + .mstore(0, calldata) .push_number(0u64) // retSize .push_number(0u64) // retOffset - .push_number(32u64) // argsSize + .push_number(calldata.len() as u64) // argsSize .push_number(0u64) // argsOffset .push_number(0u64) // value .push_address(target) @@ -81,40 +126,66 @@ fn invalid_code() -> Bytes { BytecodeBuilder::default().append(INVALID).build() } -/// REX7 splits a precompile halt by actual work: KZG verification failure enforces the -/// fixed fee and destroys the rest of the forwarded envelope; a generic error (blake2f -/// malformed input) enforces nothing and destroys the whole envelope. An interpreter -/// frame that `INVALID`s in the same position is the control — same destroyed amount, -/// same unenforced remainder. +/// The `(reported, enforced, destroyed, gas_used)` deltas of a failing call over a baseline +/// that runs byte-identical caller code against a STOP callee. +/// +/// Sharing the calldata between case and baseline is what makes the deltas exact: the caller's +/// own MSTORE / memory-expansion / CALL cost cancels, leaving only what the failed call +/// contributed. +fn deltas( + spec: MegaSpecId, + target: Address, + callee: Option, + calldata: &[u8], + limits: EvmTxRuntimeLimits, + label: &str, +) -> (i64, i64, i64, i64) { + let base = run(spec, call_then_work(CALLEE, calldata, 0), Some(stop_code()), limits); + let case = run(spec, call_then_work(target, calldata, 0), callee, limits); + assert!(base.is_success(), "{label}: the baseline must succeed: {:?}", base.result); + assert!(case.is_success(), "{label}: the caller must absorb the failure: {:?}", case.result); + ( + case.compute_gas as i64 - base.compute_gas as i64, + case.enforced() as i64 - base.enforced() as i64, + case.destroyed as i64 - base.destroyed as i64, + case.gas_used as i64 - base.gas_used as i64, + ) +} + +/// REX7 splits a precompile halt by actual work. +/// +/// Four shapes, each measured against a baseline running the identical caller code against a +/// STOP callee: +/// +/// - KZG fed a 192-byte input that fails proof verification — the call got past the length doorway, +/// so the fixed fee is the work performed and enforces; the rest of the forwarded envelope is +/// destroyed. +/// - KZG fed a 32-byte input — turned away at the length doorway before the commitment is read, so +/// nothing was performed and the whole envelope is destroyed. +/// - blake2f fed the same 32-byte input — the generic error arm, which behaves the same way. +/// - an interpreter frame that `INVALID`s in the same position — the control, which destroys the +/// whole envelope too. #[test] fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { let spec = MegaSpecId::REX7; let limits = default_limits(spec); - let base = run(spec, call_then_work(CALLEE, 0), Some(stop_code()), limits); - let kzg = run(spec, call_then_work(KZG, 0), None, limits); - let blake = run(spec, call_then_work(BLAKE2F, 0), None, limits); - let interpreter = run(spec, call_then_work(CALLEE, 0), Some(invalid_code()), limits); - - for (label, r) in - [("baseline", &base), ("kzg", &kzg), ("blake2f", &blake), ("interpreter", &interpreter)] - { - assert!(r.is_success(), "{label}: the caller must absorb the failure: {:?}", r.result); - } + let malformed = malformed_calldata(); - let delta = |r: &Outcome| { - ( - r.compute_gas as i64 - base.compute_gas as i64, - r.enforced() as i64 - base.enforced() as i64, - r.destroyed as i64 - base.destroyed as i64, - r.gas_used as i64 - base.gas_used as i64, - ) - }; - - let (kzg_dc, kzg_de, kzg_dd, kzg_dg) = delta(&kzg); - let (blake_dc, blake_de, blake_dd, blake_dg) = delta(&blake); - let (interp_dc, interp_de, interp_dd, interp_dg) = delta(&interpreter); + let (kzg_dc, kzg_de, kzg_dd, kzg_dg) = + deltas(spec, KZG, None, &verification_failure_calldata(), limits, "kzg verification"); + let (door_dc, door_de, door_dd, door_dg) = + deltas(spec, KZG, None, &malformed, limits, "kzg doorway"); + let (blake_dc, blake_de, blake_dd, blake_dg) = + deltas(spec, BLAKE2F, None, &malformed, limits, "blake2f"); + let (interp_dc, interp_de, interp_dd, interp_dg) = + deltas(spec, CALLEE, Some(invalid_code()), &malformed, limits, "interpreter"); - for (label, dg) in [("kzg", kzg_dg), ("blake2f", blake_dg), ("interpreter", interp_dg)] { + for (label, dg) in [ + ("kzg verification", kzg_dg), + ("kzg doorway", door_dg), + ("blake2f", blake_dg), + ("interpreter", interp_dg), + ] { assert!( dg >= FORWARDED as i64, "{label}: the forwarded envelope must actually be lost; Δgas_used={dg}", @@ -134,7 +205,8 @@ fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { assert_eq!( kzg_de, kzg_point_evaluation::GAS_COST as i64, - "a KZG verification failure enforces the fixed fee, not the forwarded envelope", + "a KZG failure raised inside verification enforces the fixed fee, not the forwarded \ + envelope", ); assert_eq!( kzg_dd, @@ -143,6 +215,14 @@ fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { ); assert_eq!(kzg_dc, FORWARDED as i64, "the reported total covers the whole forwarded envelope",); + assert_eq!( + door_de, 0, + "a KZG input rejected on length performed no work, so nothing enforces — the fixed fee \ + must not be charged for a call that never read the commitment", + ); + assert_eq!(door_dd, FORWARDED as i64, "the whole envelope is destroyed on a doorway reject"); + assert_eq!(door_dc, FORWARDED as i64, "the reported total still covers the envelope"); + assert_eq!(blake_de, 0, "a generic precompile error performed no work, so nothing enforces"); assert_eq!( blake_dd, FORWARDED as i64, @@ -154,39 +234,47 @@ fn test_precompile_halt_splits_executed_work_from_the_destroyed_envelope() { ); } -/// Through REX6 the same three shapes stay on the historical single-lane recording: KZG -/// charges the fixed fee as enforcing usage, the generic error charges the (capped) limit -/// as enforcing usage, and nothing is booked as destroyed. +/// Through REX6 the same shapes stay on the historical single-lane recording: every KZG failure +/// past the wrapper's gas gate charges the fixed fee as enforcing usage — doorway rejects +/// included, which is where REX7 now differs — the generic error charges the (capped) limit as +/// enforcing usage, and nothing is booked as destroyed. #[test] fn test_rex6_precompile_halt_accounting_is_unchanged() { let spec = MegaSpecId::REX6; let limits = default_limits(spec); - let base = run(spec, call_then_work(CALLEE, 0), Some(stop_code()), limits); - let kzg = run(spec, call_then_work(KZG, 0), None, limits); - let blake = run(spec, call_then_work(BLAKE2F, 0), None, limits); - let interpreter = run(spec, call_then_work(CALLEE, 0), Some(invalid_code()), limits); - - for (label, r) in - [("baseline", &base), ("kzg", &kzg), ("blake2f", &blake), ("interpreter", &interpreter)] - { - assert!(r.is_success(), "{label}: the caller must absorb the failure: {:?}", r.result); - assert_eq!(r.destroyed, 0, "{label}: REX6 has no destroyed lane"); + let malformed = malformed_calldata(); + + let kzg_verification = + deltas(spec, KZG, None, &verification_failure_calldata(), limits, "kzg verification"); + let kzg_doorway = deltas(spec, KZG, None, &malformed, limits, "kzg doorway"); + let blake = deltas(spec, BLAKE2F, None, &malformed, limits, "blake2f"); + let interpreter = deltas(spec, CALLEE, Some(invalid_code()), &malformed, limits, "interpreter"); + + for (label, (dc, de, dd, _)) in [ + ("kzg verification", kzg_verification), + ("kzg doorway", kzg_doorway), + ("blake2f", blake), + ("interpreter", interpreter), + ] { + assert_eq!(dd, 0, "{label}: REX6 has no destroyed lane"); + assert_eq!(de, dc, "{label}: REX6 charges are entirely enforcing"); } - let d = |r: &Outcome| r.compute_gas as i64 - base.compute_gas as i64; assert_eq!( - d(&kzg), + kzg_verification.0, kzg_point_evaluation::GAS_COST as i64, "REX6 KZG still records only the fixed fee", ); - assert_eq!(d(&blake), FORWARDED as i64, "REX6 generic error still records the whole envelope"); assert_eq!( - d(&interpreter), - 0, + kzg_doorway.0, + kzg_point_evaluation::GAS_COST as i64, + "REX6 charges the fixed fee for a doorway reject too", + ); + assert_eq!(blake.0, FORWARDED as i64, "REX6 generic error still records the whole envelope"); + assert_eq!( + interpreter.0, 0, "REX6 attributes neither the failing opcode nor the destroyed remainder", ); - assert_eq!(kzg.enforced(), kzg.compute_gas, "REX6 KZG charge is entirely enforcing"); - assert_eq!(blake.enforced(), blake.compute_gas, "REX6 generic charge is entirely enforcing"); } /// The generic-arm destroyed remainder is not enforcing, so work after the failing CALL can @@ -196,9 +284,10 @@ fn test_rex6_precompile_halt_accounting_is_unchanged() { fn test_generic_precompile_halt_does_not_starve_the_tail() { const TAIL_PAIRS: usize = 2_000; + let malformed = malformed_calldata(); let base = run( MegaSpecId::REX7, - call_then_work(CALLEE, TAIL_PAIRS), + call_then_work(CALLEE, &malformed, TAIL_PAIRS), Some(stop_code()), default_limits(MegaSpecId::REX7), ); @@ -208,12 +297,22 @@ fn test_generic_precompile_halt_does_not_starve_the_tail() { let limits7 = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit); let limits6 = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6).with_tx_compute_gas_limit(limit); - let blake7 = run(MegaSpecId::REX7, call_then_work(BLAKE2F, TAIL_PAIRS), None, limits7); - let interp7 = - run(MegaSpecId::REX7, call_then_work(CALLEE, TAIL_PAIRS), Some(invalid_code()), limits7); - let blake6 = run(MegaSpecId::REX6, call_then_work(BLAKE2F, TAIL_PAIRS), None, limits6); - let interp6 = - run(MegaSpecId::REX6, call_then_work(CALLEE, TAIL_PAIRS), Some(invalid_code()), limits6); + let blake7 = + run(MegaSpecId::REX7, call_then_work(BLAKE2F, &malformed, TAIL_PAIRS), None, limits7); + let interp7 = run( + MegaSpecId::REX7, + call_then_work(CALLEE, &malformed, TAIL_PAIRS), + Some(invalid_code()), + limits7, + ); + let blake6 = + run(MegaSpecId::REX6, call_then_work(BLAKE2F, &malformed, TAIL_PAIRS), None, limits6); + let interp6 = run( + MegaSpecId::REX6, + call_then_work(CALLEE, &malformed, TAIL_PAIRS), + Some(invalid_code()), + limits6, + ); assert!( blake7.is_success(), @@ -242,9 +341,10 @@ fn test_generic_precompile_halt_does_not_starve_the_tail() { /// in the destroyed remainder rather than disappearing from both lanes. #[test] fn test_destroyed_remainder_includes_the_forwarded_cap_gap() { + let malformed = malformed_calldata(); let unconstrained = run( MegaSpecId::REX7, - call_then_work(CALLEE, 0), + call_then_work(CALLEE, &malformed, 0), Some(stop_code()), default_limits(MegaSpecId::REX7), ); @@ -255,7 +355,7 @@ fn test_destroyed_remainder_includes_the_forwarded_cap_gap() { let limit = unconstrained.compute_gas + 50_000; let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit); - let blake = run(MegaSpecId::REX7, call_then_work(BLAKE2F, 0), None, limits); + let blake = run(MegaSpecId::REX7, call_then_work(BLAKE2F, &malformed, 0), None, limits); assert!(blake.is_success(), "the caller must absorb the generic error: {:?}", blake.result); assert_eq!( blake.enforced() - unconstrained.enforced(), @@ -269,19 +369,30 @@ fn test_destroyed_remainder_includes_the_forwarded_cap_gap() { ); } -/// Unconstrained default-limit reading, so a regression in the caller's own cost is visible -/// next to the split numbers rather than only inside a delta. +/// Unconstrained default-limit readings, so a regression in the caller's own cost is visible +/// next to the split numbers rather than only inside a delta. Both KZG sides are read here: +/// a failure inside verification leaves the fixed fee out of the destroyed lane, a doorway +/// reject leaves nothing out of it. #[test] fn test_kzg_halt_default_limits_report_the_full_parent_loss() { - let db = MemoryDatabase::default() - .account_balance(CALLER, alloy_primitives::U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, call_then_work(KZG, 0)) - .account_balance(CONTRACT, alloy_primitives::U256::from(ONE_ETH)); - let r = transact_default(MegaSpecId::REX7, db); - assert!(r.is_success(), "the caller must absorb the KZG failure: {:?}", r.result); + let run_default = |calldata: &[u8]| { + let db = MemoryDatabase::default() + .account_balance(CALLER, alloy_primitives::U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, call_then_work(KZG, calldata, 0)) + .account_balance(CONTRACT, alloy_primitives::U256::from(ONE_ETH)); + let r = transact_default(MegaSpecId::REX7, db); + assert!(r.is_success(), "the caller must absorb the KZG failure: {:?}", r.result); + r + }; + assert_eq!( - r.destroyed, + run_default(&verification_failure_calldata()).destroyed, FORWARDED - kzg_point_evaluation::GAS_COST, "default limits still leave the unused envelope in the destroyed lane", ); + assert_eq!( + run_default(&malformed_calldata()).destroyed, + FORWARDED, + "a doorway reject leaves the whole envelope in the destroyed lane", + ); } diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index b148fc8a..37d857dc 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -520,7 +520,9 @@ A precompile invocation that fails is the same split, taken at the precompile re A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. - **Executed** — the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work. - A node MUST record that part through the ordinary enforcing path. + For KZG the dividing line is its own input-length check, which runs before the commitment is read: an input whose length is not 192 bytes is turned away before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost regardless of how far it got. + A node MUST price an unrecognised non-out-of-gas KZG failure as verification under way, so an unfamiliar failure can only over-charge. + A node MUST record the executed part through the ordinary enforcing path. - **Destroyed** — the rest of the call's gas limit: the caller-supplied envelope minus the executed part. On a value-transferring call the envelope includes the protocol-granted call stipend, so it can exceed what the parent itself funded. That loss is the uncapped forwarded envelope, not the Rex5-capped effective gas limit; when the cap binds, the gap belongs to the destroyed part. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index ee3e09e1..a1c2101a 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -32,7 +32,7 @@ Rex7 also makes two guard- and detention-related choices that Rex6 does not: Two deliberate accounting carve-outs remain. A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. -A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope is destroyed. +A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own 192-byte length check included) enforces, and the unused caller-supplied envelope is destroyed. The generic error arm therefore stops enforcing the whole forwarded amount, which is an intentional enforcement difference from Rex6; the Rex5 forwarded-gas cap still prevents the precompile from performing more work than the remaining compute budget. ## What Changed @@ -101,7 +101,9 @@ When a nested execution merges its usage into an outer one, which today is only A precompile invocation that fails is the same split, taken at the precompile recording site rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. The **executed** part is the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work (malformed input, or a wrapper out-of-gas that never reached verification). -A node MUST record that part through the ordinary enforcing path. +For KZG the boundary is its own input-length check, which runs before the commitment is read: an input whose length is not 192 bytes is a rejection before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost, however far it got. +A node MUST price an unrecognised non-out-of-gas KZG failure as verification under way, so that an unfamiliar failure can only over-charge. +A node MUST record the executed part through the ordinary enforcing path. The **destroyed** part is the rest of the call's gas limit — the caller-supplied envelope, not the Rex5-capped effective gas limit. On a value-transferring call that envelope includes the protocol-granted call stipend, so it can exceed what the parent itself funded. When the cap binds, the gap between the envelope and the effective limit is destroyed budget rather than work, and a node MUST include it in the destroyed part. From fe062fd7399a74727ee5746b4b6f100b461231ed Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 15:54:15 +0800 Subject: [PATCH 058/208] docs(rex7): move the two block compute-gas readings into a details block The Rex7 reported / enforced split was sitting in main prose on the resource-limits page. Unstable-spec behavior belongs in a labeled details block until the spec is sealed. --- docs/spec/evm/resource-limits.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/spec/evm/resource-limits.md b/docs/spec/evm/resource-limits.md index 131d18a3..68a83733 100644 --- a/docs/spec/evm/resource-limits.md +++ b/docs/spec/evm/resource-limits.md @@ -131,12 +131,17 @@ Subsequent candidate transactions MUST be skipped before execution once the bloc Although block compute gas usage MAY be tracked, the protocol does not impose a separate block-level compute gas cap. +
+Rex7 (unstable): two readings of cumulative block compute gas + From [Rex7](../upgrades/rex7.md) onward, a node that tracks cumulative block compute gas MUST track it as two readings, because the [exceptional-halt frame carve-out](compute-gas.md#exceptional-halt-frame-carve-out) makes them differ. The **reported** reading accumulates each transaction's full compute-gas total, destroyed remainders included; it is the block's compute-gas statistic. The **enforced** reading accumulates only the part each transaction performed, and is the only one a node MAY compare against a configured block compute-gas ceiling, and the only one such a ceiling's rejection MUST report as the block's usage. Comparing the reported reading instead would let a transaction that destroyed a large gas envelope while performing almost no work close the block's compute capacity for every transaction behind it. Before Rex7 nothing is destroyed, so the two readings coincide. +
+ ### Two-Phase Block Building Workflow When constructing a block, a node or sequencer MUST process candidate transactions in the following order: From ab513f8da816e901f89809cd4dfbbe055dc861a5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 16:09:59 +0800 Subject: [PATCH 059/208] feat(sandbox): book the KeylessDeploy interceptor's destroyed envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interceptor answers from `frame_init`, before a child EVM frame exists, so the frame-exit settlement that splits an ordinary exceptional halt never runs for it. Two of its halts keep the whole call envelope and were leaving the unperformed part out of REX7's destroyed lane: - the call cannot pay the fixed dispatch overhead — nothing was performed, so the whole envelope is destroyed; - the call paid the overhead but cannot pay the deploy signer's materialization storage gas — the overhead stays enforcing, the rest is destroyed. Both go through one helper that reads the gas before building the halt result, using the same formula frame-exit settlement uses: whatever the call still held is destroyed, whatever it recorded as compute stays enforcing. Every other synthetic result on this path is left alone, and the reasoning is now written down where each sits: the system-contract interceptors return or revert with the envelope intact, `CallTooDeep` is a revert code, and the resource-limit and TX-level-exceed halts rescue their remainder for the sender. A rescue is a refund — recording it as destroyed too would report the same gas twice and inflate the block's compute statistic by that amount. The new test suite pins the rescued shape at zero destroyed next to the two destroying ones, and predicts each expected amount from the frame's gas envelope measured through the `GasLimitTooLow` revert rather than from the lane under test. REX6 and earlier record nothing on any of these paths, unchanged. --- crates/mega-evm/src/sandbox/execution.rs | 40 ++- .../tests/rex7/keyless_synthetic_halt.rs | 272 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 3 + docs/spec/evm/compute-gas.md | 4 + docs/spec/upgrades/rex7.md | 4 + 5 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 53eff283..d59f41a9 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -174,7 +174,9 @@ pub fn execute_keyless_deploy_call let cost = constants::rex2::KEYLESS_DEPLOY_OVERHEAD_GAS; let has_sufficient_gas = gas.record_regular_cost(cost); if !has_sufficient_gas { - return make_halt!(); + // The call cannot even pay the dispatch overhead, so nothing has been recorded as + // compute gas and nothing is rescued — the whole envelope is destroyed. + return destroying_oog_frame_result(ctx, &gas, &return_memory_offset); } if ctx.spec.is_enabled(MegaSpecId::REX3) { let mut additional_limit = ctx.additional_limit.borrow_mut(); @@ -197,6 +199,11 @@ pub fn execute_keyless_deploy_call // `last_frame_result` then erases `rescued_gas` from the final spend, so // the receipt's `gas_used` excludes the rescued amount. Pre-REX6 specs // leave the un-rescued full-spend in place for replay parity. + // + // Nothing is destroyed on this branch under REX7: the rescue hands the whole + // post-overhead remainder back to the sender, so the only gas actually lost is + // the overhead already recorded as compute above. Booking the rescued remainder + // as destroyed as well would report gas that was refunded. if ctx.spec.is_enabled(MegaSpecId::REX6) { additional_limit.try_rescue_gas(&gas); } @@ -1015,7 +1022,9 @@ fn charge_caller_materialization_pre_sandbox( @@ -1086,6 +1098,30 @@ fn reject_if_tx_limit_overflow( Some(result) } +/// Builds the [`oog_frame_result`] shape for a synthetic halt that keeps the whole envelope, +/// recording the part it destroys (REX7+). +/// +/// The `KeylessDeploy` interceptor produces its results inside `frame_init`, before a child EVM +/// frame exists, so the frame-exit settlement that splits an ordinary exceptional halt never +/// sees them. The split is taken here instead, by the same formula that settlement uses: the +/// gas the frame still held when it gave up is destroyed, and whatever it had already recorded +/// as compute gas stays on the enforcing lane. `gas` must therefore be read *before* the halt +/// result is built — the result itself reports zero remaining, because the envelope is gone. +/// +/// Only for halts that keep the envelope. A halt whose remaining gas is rescued for the sender +/// destroys nothing: the rescue is a refund, and booking the same gas here as well would report +/// gas that was handed back. +fn destroying_oog_frame_result( + ctx: &MegaContext, + gas: &Gas, + return_memory_offset: &core::ops::Range, +) -> FrameResult { + if ctx.spec.is_enabled(MegaSpecId::REX7) { + ctx.additional_limit.borrow_mut().record_burned_gas(gas.remaining()); + } + oog_frame_result(gas.limit(), return_memory_offset) +} + /// Single source of truth for the `OutOfGas` halt `FrameResult` shape (empty return /// data, all gas consumed). Callers that need the exceeding-limit marker apply /// `mark_frame_result_as_exceeding_limit` on the returned value. diff --git a/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs b/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs new file mode 100644 index 00000000..5b22e208 --- /dev/null +++ b/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs @@ -0,0 +1,272 @@ +//! The `KeylessDeploy` interceptor's synthetic halts destroy the envelope they burn. +//! +//! An interceptor returns its result out of `frame_init`, before a child EVM frame exists, so the +//! frame-exit settlement that splits an ordinary exceptional halt never runs for it. Two of the +//! interceptor's halts keep the whole call envelope, and under REX7 the part they did not perform +//! belongs in the destroyed lane: +//! +//! - the call cannot pay the fixed dispatch overhead, so nothing at all was performed; +//! - the call paid the overhead but cannot pay the deploy signer's materialization storage gas, so +//! the overhead is the only work performed. +//! +//! Every other synthetic result on this path is a `Return`, a `Revert`, or an out-of-gas whose +//! remaining gas is rescued for the sender. None of those destroys anything: the first two hand +//! the envelope back to the parent, and a rescue is a refund — booking it as destroyed as well +//! would report gas the sender got back, and would inflate the block's compute statistic by the +//! same amount. The rescue shape is pinned here next to the two destroying ones so the difference +//! stays visible. +//! +//! Every probe's expected destroyed amount is predicted from the frame's gas envelope, and that +//! envelope is measured through the `GasLimitTooLow` revert rather than read back out of the lane +//! under test. + +use crate::common::{transact_tx, Outcome, ONE_ETH}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::{SolCall as _, SolError as _}; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + constants::{rex::NEW_ACCOUNT_STORAGE_GAS_BASE, rex2::KEYLESS_DEPLOY_OVERHEAD_GAS}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, MegaSpecId, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, + MIN_BUCKET_SIZE, +}; +use revm::{ + bytecode::opcode::STOP, + context::{result::ExecutionResult, tx::TxEnvBuilder}, +}; +use std::vec::Vec; + +/// Relayer that sends the keyless-deploy transactions. +const RELAYER: Address = address!("0000000000000000000000000000000000340009"); + +/// The minimum bucket capacity, at which deploy-signer materialization is free and the +/// materialization arm is unreachable. +const MIN_BUCKET: u64 = MIN_BUCKET_SIZE as u64; + +/// Twice the minimum capacity, which prices deploy-signer materialization at one base unit +/// (`NEW_ACCOUNT_STORAGE_GAS_BASE × (multiplier − 1)`). +const DOUBLE_BUCKET: u64 = 2 * MIN_BUCKET; + +/// The materialization charge [`DOUBLE_BUCKET`] produces. +const MATERIALIZATION_GAS: u64 = NEW_ACCOUNT_STORAGE_GAS_BASE; + +/// The inner keyless transaction's own gas limit. `gasLimitOverride` is passed well above it so +/// the pre-cap check clears; the post-cap re-check at step 4b is what the calibration below reads. +const INNER_TX_GAS_LIMIT: u64 = 200_000; + +/// Builds a deterministic pre-EIP-155 keyless deployment transaction. Its init code never runs on +/// any path exercised here — every probe fails before the sandbox is built. +fn keyless_tx_bytes() -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: INNER_TX_GAS_LIMIT, + to: TxKind::Create, + value: U256::ZERO, + input: BytecodeBuilder::default().append(STOP).build(), + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// Runs a top-level keyless-deploy call. Depth zero is the only depth the interceptor fires at, so +/// every probe here has to be a direct transaction. +fn run( + spec: MegaSpecId, + tx_gas_limit: u64, + bucket_capacity: u64, + tx_compute_gas_limit: Option, +) -> Outcome { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(), + gasLimitOverride: U256::from(1_000_000u64), + } + .abi_encode(); + + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + if let Some(limit) = tx_compute_gas_limit { + limits = limits.with_tx_compute_gas_limit(limit); + } + + let db = MemoryDatabase::default().account_balance(RELAYER, U256::from(10 * ONE_ETH)); + let tx = TxEnvBuilder::default() + .caller(RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(tx_gas_limit) + .chain_id(Some(1)) + .data(Bytes::from(call_data)) + .build_fill(); + + transact_tx( + spec, + db, + limits, + tx, + &TestExternalEnvs::default().with_default_bucket_capacity(bucket_capacity), + ) +} + +/// The transaction gas limit the envelope calibration runs at. +/// +/// It has to sit in the window where the interceptor clears both the dispatch overhead and the +/// materialization charge — so the same value works at either bucket capacity — while leaving the +/// capped override below the inner transaction's own gas limit, which is what makes the post-cap +/// re-check revert instead of letting the sandbox start. +const CALIBRATION_GAS: u64 = 300_000; + +/// The gas envelope the top-level frame opens with, measured through a channel the destroyed lane +/// cannot influence. +/// +/// A keyless-deploy call that clears the dispatch overhead but cannot cover the inner +/// transaction's own gas limit reverts with `GasLimitTooLow`, and the `providedGasLimit` it +/// carries is what the outer frame had left at that point. Adding back the charges taken before it +/// recovers the envelope the frame started with — so the probes below predict their expected +/// destroyed amount instead of reading it back out of the lane under test. +fn frame_envelope(bucket_capacity: u64, charges_before: u64) -> u64 { + let probe = run(MegaSpecId::REX7, CALIBRATION_GAS, bucket_capacity, None); + let ExecutionResult::Revert { output, .. } = &probe.result else { + panic!("the calibration probe must revert, got {:?}", probe.result); + }; + let decoded = IKeylessDeploy::GasLimitTooLow::abi_decode(output) + .expect("the calibration probe must revert with GasLimitTooLow"); + assert_eq!( + decoded.txGasLimit, INNER_TX_GAS_LIMIT, + "the revert must be the post-cap re-check against the inner transaction's gas limit", + ); + decoded.providedGasLimit + charges_before +} + +/// The gas the transaction pays before the top-level frame opens. +fn withheld_intrinsic() -> u64 { + let envelope_at_min = frame_envelope(MIN_BUCKET, KEYLESS_DEPLOY_OVERHEAD_GAS); + // Cross-check: raising the bucket capacity must change nothing except the materialization + // charge. This validates the charge's size and, at the same time, that the pre-frame intrinsic + // is not itself bucket-scaled — which is what lets one calibration serve both probes. + let envelope_at_double = + frame_envelope(DOUBLE_BUCKET, KEYLESS_DEPLOY_OVERHEAD_GAS + MATERIALIZATION_GAS); + assert_eq!( + envelope_at_min, envelope_at_double, + "the materialization charge must be the only thing the bucket capacity changes", + ); + CALIBRATION_GAS - envelope_at_min +} + +/// Asserts that REX7 changed nothing a consumer of the transaction can see, which is what lets the +/// destroyed lane be a pure addition to the reported total. +fn assert_receipt_parity(rex6: &Outcome, rex7: &Outcome, label: &str) { + assert_eq!( + std::format!("{:?}", rex6.result), + std::format!("{:?}", rex7.result), + "{label}: the execution result must be identical across the two specs", + ); + assert_eq!(rex6.gas_used, rex7.gas_used, "{label}: receipt gas_used must be identical"); + assert_eq!(rex6.destroyed, 0, "{label}: REX6 has no destroyed lane"); + assert_eq!( + rex7.enforced(), + rex6.compute_gas, + "{label}: REX7 must enforce exactly what REX6 recorded — the destroyed lane is an \ + addition to the reported total, never a change to the enforced one", + ); +} + +/// The call cannot pay the fixed dispatch overhead. Nothing was recorded as compute and nothing is +/// rescued, so the whole envelope the frame opened with is destroyed. +#[test] +fn test_underfunded_dispatch_destroys_the_whole_envelope() { + // An envelope below the dispatch overhead, and not a round number, so an amount that happened + // to coincide with some other quantity would not pass. + const ENVELOPE: u64 = 33_333; + const { assert!(ENVELOPE < KEYLESS_DEPLOY_OVERHEAD_GAS) }; + let tx_gas_limit = withheld_intrinsic() + ENVELOPE; + + let rex6 = run(MegaSpecId::REX6, tx_gas_limit, MIN_BUCKET, None); + let rex7 = run(MegaSpecId::REX7, tx_gas_limit, MIN_BUCKET, None); + + assert!(!rex7.is_success(), "the underfunded dispatch must halt: {:?}", rex7.result); + assert_eq!( + rex7.gas_used, tx_gas_limit, + "the whole transaction envelope is burnt — nothing is rescued on this path", + ); + assert_receipt_parity(&rex6, &rex7, "underfunded dispatch"); + + assert_eq!( + rex7.destroyed, ENVELOPE, + "the destroyed part is the whole envelope the frame opened with, because the call failed \ + before performing anything", + ); +} + +/// The overhead is paid, the deploy signer's materialization storage gas is not. The overhead is +/// the only work performed, so what the call still held is what gets destroyed. +#[test] +fn test_underfunded_signer_materialization_destroys_what_the_overhead_left() { + // Enough envelope for the dispatch overhead, then a remainder too small for the + // materialization charge — so the charge is the thing that cannot fit. + const REMAINDER: u64 = 9_999; + const { assert!(REMAINDER < MATERIALIZATION_GAS) }; + let tx_gas_limit = withheld_intrinsic() + KEYLESS_DEPLOY_OVERHEAD_GAS + REMAINDER; + + let rex6 = run(MegaSpecId::REX6, tx_gas_limit, DOUBLE_BUCKET, None); + let rex7 = run(MegaSpecId::REX7, tx_gas_limit, DOUBLE_BUCKET, None); + + assert!(!rex7.is_success(), "the underfunded materialization must halt: {:?}", rex7.result); + assert_eq!( + rex7.gas_used, tx_gas_limit, + "the whole transaction envelope is burnt — this path does not rescue either", + ); + assert_receipt_parity(&rex6, &rex7, "underfunded materialization"); + + assert_eq!( + rex7.destroyed, REMAINDER, + "the destroyed part is what the call still held after paying the dispatch overhead, not \ + the whole envelope — the overhead was performed and stays enforcing", + ); + assert_eq!( + rex7.enforced() - withheld_compute(), + KEYLESS_DEPLOY_OVERHEAD_GAS, + "and the enforcing lane carries that overhead on top of the intrinsic, which is what \ + separates this probe from the underfunded-dispatch one", + ); +} + +/// The compute gas an underfunded-dispatch run records: the transaction intrinsic and nothing +/// else, because the call fails before the overhead is charged. +fn withheld_compute() -> u64 { + run(MegaSpecId::REX7, withheld_intrinsic() + 1_000, MIN_BUCKET, None).enforced() +} + +/// The transaction-level compute exceed crossed while recording the dispatch overhead rescues the +/// call's remaining gas for the sender. A rescue is a refund, so nothing on that path is +/// destroyed — booking the rescued remainder as destroyed as well would report gas that was +/// handed back. +#[test] +fn test_rescued_overhead_exceed_is_not_also_destroyed() { + // A compute limit under the overhead makes the overhead's own recording cross it. + const TX_COMPUTE_LIMIT: u64 = 50_000; + const TX_GAS_LIMIT: u64 = 30_000_000; + + let rex6 = run(MegaSpecId::REX6, TX_GAS_LIMIT, MIN_BUCKET, Some(TX_COMPUTE_LIMIT)); + let rex7 = run(MegaSpecId::REX7, TX_GAS_LIMIT, MIN_BUCKET, Some(TX_COMPUTE_LIMIT)); + + assert!(!rex7.is_success(), "the compute exceed must halt: {:?}", rex7.result); + assert_eq!( + rex7.destroyed, 0, + "a rescued halt destroys nothing: the remaining gas goes back to the sender, so counting \ + it as destroyed would report the same gas twice", + ); + assert_receipt_parity(&rex6, &rex7, "rescued overhead exceed"); + + // The rescue is real, so the receipt lands far below the envelope — the contrast that makes + // the zero above meaningful rather than vacuous. + assert!( + rex7.gas_used < TX_GAS_LIMIT / 2, + "the rescue must actually refund most of the envelope; gas_used={}", + rex7.gas_used, + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 2d997b09..64eb3cad 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -10,6 +10,8 @@ //! is covered exhaustively rather than through representatives. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. +//! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that +//! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. //! - `gas_leakage` — the three paths a per-frame gas mechanism can leak through (interception, @@ -56,6 +58,7 @@ mod gas_clamp; mod gas_leakage; mod guard_pass_static_gas; mod interceptor_resume; +mod keyless_synthetic_halt; mod latch_surfacing; mod modexp_gas; mod opcode_set_parity; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 37d857dc..fb78f064 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -532,6 +532,10 @@ Through Rex6 the generic error arm recorded the effective gas limit as enforcing Under Rex7 that arm enforces nothing, which is a deliberate enforcement difference. The Rex5 forwarded-gas cap is unchanged: a precompile still MUST NOT perform more work than the remaining compute budget. +A [system contract](../system-contracts/overview.md) invocation a node answers without opening an EVM frame takes the same split, at the site that produces the answer. +It applies only when the answer is a halt that keeps the call's gas: the part the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. +An answer that returns or reverts hands the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund; a node MUST NOT record either as destroyed, because that gas was not lost. + The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index a1c2101a..ad507145 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -111,6 +111,10 @@ Through Rex6 the generic error arm recorded the effective gas limit as enforcing That is a deliberate enforcement difference. The Rex5 forwarded-gas cap is unchanged: a precompile still MUST NOT perform more work than the remaining compute budget. +A system contract invocation a node answers without opening an EVM frame — the `KeylessDeploy` dispatch is the only one today — takes the same split at the site that produces the answer, and only when that answer is a halt which keeps the call's gas: whatever the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. +An answer that returns or reverts gives the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund. +A node MUST NOT record either as destroyed; that gas was not lost, and counting it would report it twice. + Under per-opcode recording through Rex6, neither the failing opcode nor the destroyed remainder is attributed to compute gas. Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. From be5432e530da3c9a4c3e0415be923919f023c300 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 17:58:20 +0800 Subject: [PATCH 060/208] feat(evm): book the pre-execution intrinsic halt's destroyed envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MegaHandler::before_execution` answers a transaction whose initial gas outgrew its gas limit with a synthetic top-level out-of-gas that burns the whole envelope having executed nothing, produced before any frame exists — so the frame-exit settlement that splits an ordinary exceptional halt never runs for it. REX7 now takes the same split at the site: the intrinsic compute gas `validate` recorded is the only work performed and stays enforcing, and the rest of the envelope is destroyed, which makes the reported total cover what the receipt burns. The branch is reachable on MINI_REX..REX4 only. Those specs run their initial-gas check midway through the MegaETH storage-gas additions, so a contribution added after it can still push the total past the gas limit; REX5 moved the check to the end of the additions, which turns the same transaction into a `CallGasCostMoreThanGasLimit` validation error before `pre_execution` debits the sender. No transaction on a spec that has the destroyed lane therefore reaches the halt today — the recording is what keeps the lane correct if a later spec grows an intrinsic component resolved after validation. Both sides of that boundary are pinned. The new REX7 suite asserts that REX6 and REX7 reject the overrun in validation with the sender untouched, and that REX4 still answers it with a halt that burns the whole envelope while reporting only the intrinsic and destroying nothing. The recording itself is driven at the hook, where the REX7 split and the REX6 zero are asserted to the gas. The synthetic result never traverses `last_frame_result`, frame-exit settlement or any rescue hook, so nothing downstream can double-count what is booked here. REX6 and earlier are unchanged. The spec pages state the boundary rather than the site: the carve-out's enumeration of envelope-keeping halts is complete for REX7, and a validation reject — which produces no receipt — must not be recorded as a destroyed remainder. --- crates/mega-evm/src/evm/execution.rs | 104 +++++++++++++- crates/mega-evm/tests/rex7/main.rs | 4 + .../rex7/pre_execution_intrinsic_reject.rs | 130 ++++++++++++++++++ docs/spec/evm/compute-gas.md | 4 + docs/spec/upgrades/rex7.md | 4 + 5 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 53c4718f..f316f4b2 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -167,9 +167,31 @@ where // Check if the initial gas exceeds the tx gas limit, if so, we halt with out of gas let ctx = evm.ctx(); let tx = ctx.tx(); - if tx.gas_limit() < init_and_floor_gas.initial_regular_gas { + let gas_limit = tx.gas_limit(); + let tx_kind = tx.kind(); + if gas_limit < init_and_floor_gas.initial_regular_gas { + // REX7+: this halt burns the whole transaction envelope without running a single + // opcode, and it produces its result before any frame exists — so the frame-exit + // settlement that splits an ordinary exceptional halt can never see it. Take the same + // split here: the intrinsic compute gas `validate` already recorded is the only work + // performed and stays on the enforcing lane, and the rest of the envelope is + // destroyed, which makes the reported total cover what the receipt burns. + // + // REX5+ rejects this transaction in `validate` instead — the final initial-gas check + // there compares the same pair after every MegaETH storage-gas contribution has been + // folded in, and returns `CallGasCostMoreThanGasLimit` before `pre_execution` debits + // the sender. So no transaction on a spec that has the destroyed lane reaches this + // branch today; the recording is what keeps the lane correct if a later spec grows an + // intrinsic component that is only resolved after validation. + if ctx.spec.is_enabled(MegaSpecId::REX7) { + let mut additional_limit = ctx.additional_limit.borrow_mut(); + // Nothing can have been destroyed before the first frame, so the recorded total + // is entirely enforcing usage. + let performed = additional_limit.get_usage().compute_gas; + additional_limit.record_burned_gas(gas_limit.saturating_sub(performed)); + } // If not sufficient gas, we halt with out of gas - let oog_frame_result = gen_oog_frame_result(tx.kind(), tx.gas_limit()); + let oog_frame_result = gen_oog_frame_result(tx_kind, gas_limit); return Ok(Some(oog_frame_result)); } Ok(None) @@ -1711,6 +1733,11 @@ fn gen_call_too_deep_result(call_inputs: &revm::interpreter::CallInputs) -> Fram /// which can happen after `MegaHandler::validate` has added any MegaETH-specific /// intrinsic gas (calldata storage gas, REX intrinsic storage gas, callee-side /// new-account storage gas, or the REX5+ deposit-caller storage gas). +/// +/// Reachable on `MINI_REX`..REX4 only: those specs run their initial-gas check midway through +/// the `MegaETH` additions, so a contribution added after it can still push the total past the +/// gas limit and land here. REX5 moved that check to the end of the additions, which turns the +/// same transaction into a `CallGasCostMoreThanGasLimit` validation error instead. fn gen_oog_frame_result(tx_kind: TxKind, gas_limit: u64) -> FrameResult { match tx_kind { TxKind::Call(_address) => FrameResult::Call(CallOutcome::new( @@ -1857,4 +1884,77 @@ mod mutation_tests { }; consume_synthetic_limit_frame(evm.ctx_ref(), result); } + + /// Drives [`MegaHandler::before_execution`] straight at its short-circuit: a transaction whose + /// gas limit is one below the initial gas it is handed, with `recorded_intrinsic` already on + /// the compute-gas tracker the way `validate` leaves it. + /// + /// Returns `(halt gas spent, reported compute total, destroyed part)`. + /// + /// Driving the hook directly is the only way to reach the branch from REX5 on: those specs + /// run their initial-gas check after every `MegaETH` storage-gas contribution, so a transaction + /// with this shape is rejected in `validate` instead (pinned by + /// `tests/rex7/pre_execution_intrinsic_reject.rs`). + fn run_before_execution_short_circuit( + spec: MegaSpecId, + recorded_intrinsic: u64, + ) -> (u64, u64, u64) { + let mut context = MegaContext::new(MemoryDatabase::default(), spec); + context.inner.tx.base.gas_limit = TEST_GAS_LIMIT; + context.additional_limit.borrow_mut().record_compute_gas(recorded_intrinsic); + + let mut evm = MegaEvm::new(context); + let handler = MegaHandler::< + _, + revm::context::result::EVMError, + (), + >::new(); + let result = handler + .before_execution(&mut evm, &InitialAndFloorGas::new(TEST_GAS_LIMIT + 1, 0)) + .expect("the short circuit cannot fail") + .expect("initial gas above the gas limit must short-circuit"); + + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + ( + result.gas().total_gas_spent(), + additional_limit.get_usage().compute_gas, + additional_limit.burned_compute_gas(), + ) + } + + /// REX7: the pre-execution intrinsic overrun burns the whole envelope having performed only + /// the intrinsic compute gas `validate` recorded, so that intrinsic stays enforcing and the + /// rest of the envelope is destroyed. The reported total covers the envelope the halt burns. + #[test] + fn test_before_execution_short_circuit_destroys_the_unperformed_envelope() { + const INTRINSIC: u64 = 21_000; + let (spent, reported, destroyed) = + run_before_execution_short_circuit(MegaSpecId::REX7, INTRINSIC); + + assert_eq!(spent, TEST_GAS_LIMIT, "the halt burns the whole transaction envelope"); + assert_eq!(reported, TEST_GAS_LIMIT, "the reported total covers the burnt envelope"); + assert_eq!( + destroyed, + TEST_GAS_LIMIT - INTRINSIC, + "everything the transaction did not perform is destroyed", + ); + assert_eq!( + reported - destroyed, + INTRINSIC, + "only the intrinsic compute gas already recorded enforces", + ); + } + + /// REX6 has no destroyed lane: the same short circuit records nothing beyond the intrinsic + /// `validate` already put on the tracker, and burns the same envelope. + #[test] + fn test_before_execution_short_circuit_records_nothing_before_rex7() { + const INTRINSIC: u64 = 21_000; + let (spent, reported, destroyed) = + run_before_execution_short_circuit(MegaSpecId::REX6, INTRINSIC); + + assert_eq!(spent, TEST_GAS_LIMIT, "the burnt envelope is spec-independent"); + assert_eq!(reported, INTRINSIC, "REX6 reports only what validate recorded"); + assert_eq!(destroyed, 0, "REX6 has no destroyed lane"); + } } diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 64eb3cad..57f010a9 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -43,6 +43,9 @@ //! - `precompile_halt` — a precompile that halts exceptionally is split at the recording site the //! same way an interpreter frame is: executed work enforces, the unused forwarded envelope does //! not. +//! - `pre_execution_intrinsic_reject` — the one envelope-keeping synthetic halt REX7 cannot reach: +//! an intrinsic overrun is a validation error from REX5 on, so the destroyed lane never has to +//! account for it. mod burn_split; mod charge_on_reject; @@ -63,4 +66,5 @@ mod latch_surfacing; mod modexp_gas; mod opcode_set_parity; mod parity_shapes; +mod pre_execution_intrinsic_reject; mod precompile_halt; diff --git a/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs b/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs new file mode 100644 index 00000000..ff2672f9 --- /dev/null +++ b/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs @@ -0,0 +1,130 @@ +//! The pre-execution intrinsic overrun: a transaction whose MegaETH-side intrinsic gas outgrows +//! the gas limit its sender supplied. +//! +//! `MINI_REX`..REX4 answer that with a synthetic top-level out-of-gas that burns the whole +//! envelope having executed nothing — the one halt shape that keeps a transaction's entire gas +//! envelope without ever creating a frame. REX5 moved the initial-gas check to the end of the +//! `MegaETH` storage-gas additions, which turns the same transaction into a validation error +//! before `pre_execution` debits the sender. +//! +//! That ordering is what keeps the REX7 destroyed lane complete without the synthetic halt having +//! to participate: no transaction on a spec that has the lane can reach the halt. These probes pin +//! both sides of the boundary, so a future change that re-opens the halt for REX7 turns red here +//! rather than silently reporting a transaction whose burnt envelope no lane accounts for. + +use std::convert::Infallible; + +use alloy_primitives::{address, Address, Bytes, TxKind, U256}; +use mega_evm::{ + test_utils::MemoryDatabase, EVMError, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + MegaTransactionError, MegaTransactionNew as _, MegaTransactionOutcome, SaltEnv, + TestExternalEnvs, MIN_BUCKET_SIZE, +}; +use revm::{context::TxEnv, Database as _}; + +const CALLER: Address = address!("2000000000000000000000000000000000000002"); +/// Value-transfer recipient that does not exist yet, so the transaction owes new-account storage +/// gas for materialising it. +const NEW_ACCOUNT: Address = address!("9000000000000000000000000000000000000009"); + +/// Covers the standard EVM intrinsic (21,000) and the REX flat intrinsic storage gas (39,000), +/// but not the dynamic new-account storage gas the hot bucket below scales up. +const INSUFFICIENT_GAS_LIMIT: u64 = 80_000; + +/// Standard EVM intrinsic gas for this transaction — no calldata, no access list. This is the +/// whole of what `validate` records as compute gas before the first frame. +const INTRINSIC_COMPUTE_GAS: u64 = 21_000; + +const CALLER_BALANCE: u64 = 10_000_000; + +/// Runs the overrun transaction under `spec`: a value-transferring call to an empty account whose +/// SALT bucket is ten times the minimum, so the dynamic new-account storage gas alone is far +/// larger than the gas limit. +fn run_intrinsic_overrun( + db: &mut MemoryDatabase, + spec: MegaSpecId, +) -> Result> { + let bucket_id = TestExternalEnvs::::bucket_id_for_account(NEW_ACCOUNT); + let external_envs = TestExternalEnvs::::new() + .with_bucket_capacity(bucket_id, MIN_BUCKET_SIZE as u64 * 10); + + let mut context = MegaContext::new(db, spec).with_external_envs(external_envs.into()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + let mut tx = MegaTransaction::new(TxEnv { + caller: CALLER, + kind: TxKind::Call(NEW_ACCOUNT), + data: Bytes::new(), + value: U256::from(1), + gas_limit: INSUFFICIENT_GAS_LIMIT, + ..Default::default() + }); + tx.enveloped_tx = Some(Bytes::new()); + + MegaEvm::new(context).execute_transaction(tx) +} + +fn funded_db() -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_balance(CALLER, U256::from(CALLER_BALANCE)); + db +} + +fn assert_sender_untouched(db: &mut MemoryDatabase) { + let info = db.basic(CALLER).expect("db read should succeed").unwrap_or_default(); + assert_eq!( + info.balance, + U256::from(CALLER_BALANCE), + "a validation reject must not debit the sender", + ); + assert_eq!(info.nonce, 0, "a validation reject must not bump the sender's nonce"); +} + +/// REX7 (and REX6, its immediate predecessor) reject the overrun in validation. There is no +/// receipt, no burnt envelope, and therefore nothing for the destroyed lane to account for — the +/// synthetic halt that would keep the envelope is unreachable on both specs. +#[test] +fn test_intrinsic_overrun_is_a_validation_reject_from_rex6_on() { + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + let mut db = funded_db(); + let err = match run_intrinsic_overrun(&mut db, spec) { + Err(err) => err, + Ok(outcome) => panic!( + "{spec:?} must reject the intrinsic overrun before execution, got {:?}", + outcome.result_and_state.result, + ), + }; + let rendered = format!("{err:?}"); + assert!( + rendered.contains("CallGasCostMoreThanGasLimit"), + "{spec:?}: expected CallGasCostMoreThanGasLimit, got {rendered}", + ); + assert_sender_untouched(&mut db); + } +} + +/// REX4 keeps the frozen shape: validation accepts the transaction, the sender pays, and execution +/// answers with a synthetic out-of-gas that burns the whole envelope. The compute-gas total is +/// exactly the intrinsic `validate` recorded — the burnt remainder is attributed to nothing, which +/// is the frozen accounting the destroyed lane must not retroactively change. +#[test] +fn test_rex4_intrinsic_overrun_burns_the_envelope_with_no_destroyed_lane() { + let mut db = funded_db(); + let outcome = run_intrinsic_overrun(&mut db, MegaSpecId::REX4) + .expect("pre-REX5 specs must not reject the overrun as a validation error"); + + assert!(outcome.result_and_state.result.is_halt(), "REX4 answers the overrun with a halt"); + assert_eq!( + outcome.result_and_state.result.tx_gas_used(), + INSUFFICIENT_GAS_LIMIT, + "the halt burns the whole envelope", + ); + assert_eq!( + outcome.compute_gas_used, INTRINSIC_COMPUTE_GAS, + "REX4 reports only the intrinsic compute gas validate recorded", + ); + assert_eq!(outcome.compute_gas_destroyed, 0, "pre-REX7 specs have no destroyed lane"); +} diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index fb78f064..dd574ba1 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -536,6 +536,10 @@ A [system contract](../system-contracts/overview.md) invocation a node answers w It applies only when the answer is a halt that keeps the call's gas: the part the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. An answer that returns or reverts hands the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund; a node MUST NOT record either as destroyed, because that gas was not lost. +The one remaining way to burn a whole envelope without executing anything — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — is not part of this carve-out. +Since [Rex5](../upgrades/rex5.md) a node rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited, so it is never included and there is no burnt envelope to split. +A node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. + The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index ad507145..d8ea0d16 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -115,6 +115,10 @@ A system contract invocation a node answers without opening an EVM frame — the An answer that returns or reverts gives the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund. A node MUST NOT record either as destroyed; that gas was not lost, and counting it would report it twice. +Those sites are the complete set of ways a Rex7 transaction can lose an envelope without executing it. +One further shape burns a whole envelope having executed nothing — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — but [Rex5](rex5.md) already rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited. +It therefore produces no receipt on Rex7 and there is no envelope to split; a node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. + Under per-opcode recording through Rex6, neither the failing opcode nor the destroyed remainder is attributed to compute gas. Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. From 926b1a855206626ef678ef19f131d92ac81af539 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 14 Aug 2026 17:58:35 +0800 Subject: [PATCH 061/208] test(rex7): widen the precompile mirror matrix and pin the KZG cap coupling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in the precompile seam's own coverage. The parity matrix that holds `run_precompile_capturing_halt` to alloy-evm's `PrecompilesMap::run` left the state-gas reservoir and the call value at zero on every case. Both are forwarded fields of `PrecompileInput`, and no builtin precompile reads either, so an upstream step keyed on one of them would have passed parity unnoticed. Each now appears on a returning and on a failing case, and the matrix's dynamic precompile echoes the call value into its output the way it already echoed the reservoir into its gas — so both are observed in the compared `InterpreterResult` rather than merely handed over. The KZG fixed-cost arm's `record_compute_gas(GAS_COST)` is safe only because it can never latch a limit exceed: a latch would make `frame_init` return a halt whose remaining gas is rescued for the sender while the same envelope was already booked as destroyed, reporting that gas twice. What rules it out is the REX5 forwarded-gas cap — the effective limit the arm tests against the fixed fee is `min(gas_limit, remaining)` for the very remaining the limit check consults — and nothing said so. A `debug_assert` now states it at the recording site, and a boundary probe pins both halves on REX5, REX6 and REX7: with the budget exactly at the fixed fee the arm fires and lands on the limit rather than over it, and one unit lower the cap sends the call into the wrapper's gas gate so the arm is not reached at all. No behavior change on any spec; the assert compiles out of release builds. --- crates/mega-evm/src/evm/precompiles.rs | 178 ++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index f1ea9931..204c5b69 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -423,6 +423,24 @@ impl PrecompileProvider= executed, + "the forwarded-gas cap must keep the KZG fixed fee inside the \ + remaining compute budget", + ); additional_limit.record_compute_gas(executed); if is_rex7 { additional_limit.record_burned_gas(gas_limit.saturating_sub(executed)); @@ -1205,6 +1223,80 @@ mod tests { ); } + /// The fixed-cost arm's `record_compute_gas(GAS_COST)` must never latch a limit exceed. + /// A latch there would make `frame_init` return a halt whose remaining gas is rescued for the + /// sender while the same envelope has already been booked as destroyed, reporting the gas + /// twice. + /// + /// The forwarded-gas cap is what rules it out, and the two halves of that coupling are pinned + /// here at the one gas unit where they meet. With the remaining budget exactly at the fixed + /// fee, the arm fires and lands exactly on the limit — never over it. One unit lower, the cap + /// pushes the effective limit below the fixed fee, so the wrapper's own gas gate fires and the + /// arm is not taken at all: there is no shape in which the recording is reached with less + /// budget than it records. + #[test] + fn test_kzg_fixed_cost_arm_cannot_latch_at_the_cap_boundary() { + for spec in [MegaSpecId::REX5, MegaSpecId::REX6, MegaSpecId::REX7] { + let inputs = generate_invalid_proof_kzg_test_input(); + let address = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded_gas = 500_000u64; + + // Remaining budget exactly at the fixed fee: the tightest cap that still admits the + // arm. + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, spec); + set_tx_compute_gas_limit(&mut context, spec, GAS_COST); + let mut precompiles_map = + PrecompilesMap::from_static(MegaPrecompiles::new_with_spec(spec).precompiles()); + let output = precompiles_map + .run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)) + .expect("run ok") + .expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileError), + "{spec:?}: verification must fail past the gas gate; got {:?}", + output.result, + ); + let additional = context.additional_limit.borrow(); + assert_eq!( + additional.get_usage().compute_gas - additional.burned_compute_gas(), + GAS_COST, + "{spec:?}: the fixed-cost arm fired and only the fixed fee enforces", + ); + assert!( + !additional.limit_exceeded(), + "{spec:?}: the fixed fee exactly exhausts the budget, which is not an exceed", + ); + drop(additional); + + // One unit lower: the cap forces the wrapper's gas gate, so the fixed-cost arm is + // never reached. + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, spec); + set_tx_compute_gas_limit(&mut context, spec, GAS_COST - 1); + let mut precompiles_map = + PrecompilesMap::from_static(MegaPrecompiles::new_with_spec(spec).precompiles()); + let output = precompiles_map + .run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)) + .expect("run ok") + .expect("Some output"); + assert!( + matches!(output.result, InstructionResult::PrecompileOOG), + "{spec:?}: one unit below the fixed fee must stop at the gas gate; got {:?}", + output.result, + ); + assert!( + output.gas.limit() < GAS_COST, + "{spec:?}: the capped effective limit is what excludes the fixed-cost arm", + ); + let additional = context.additional_limit.borrow(); + assert!( + !additional.limit_exceeded(), + "{spec:?}: the arm the cap excluded cannot latch either", + ); + } + } + /// REX6 KZG verification failure stays on the historical single-lane recording: the /// fixed fee is enforcing and nothing is destroyed. #[test] @@ -1418,6 +1510,11 @@ mod tests { /// flips the map into its dynamic representation — the `PrecompilesMap::get` branch the /// builtin table never reaches, and the only way to produce a reverting precompile at all /// (no builtin returns `PrecompileStatus::Revert`). + /// + /// It echoes the forwarded call value into its output and the forwarded reservoir into its + /// gas, so both become observable in the compared `InterpreterResult`. No builtin precompile + /// reads the call value, so without this the value cases in the matrix would only prove the + /// two paths agree on an input neither of them can act on. fn parity_precompiles(with_dynamic: bool) -> PrecompilesMap { let mut map = PrecompilesMap::from_static( MegaPrecompiles::new_with_spec(MegaSpecId::REX7).precompiles(), @@ -1425,9 +1522,11 @@ mod tests { if with_dynamic { map.apply_precompile(&PARITY_DYN_ADDRESS, |_| { Some(DynPrecompile::new(PrecompileId::Custom("parity-revert".into()), |input| { + let mut echoed = Vec::from(b"reverted".as_slice()); + echoed.extend_from_slice(&input.value.to_be_bytes::<32>()); Ok(PrecompileOutput::revert( input.gas / 4, - Bytes::from_static(b"reverted"), + Bytes::from(echoed), input.reservoir, )) })) @@ -1436,9 +1535,29 @@ mod tests { map } + /// Sets the EIP-8037 state-gas reservoir on an already-built [`CallInputs`]. + /// + /// `reservoir` and `value` are two of the nine `PrecompileInput` fields the mirror forwards, + /// and no builtin precompile reads either — so an upstream step keyed on one of them would + /// pass a matrix that leaves both at zero. The cases built with these helpers vary them + /// instead, on both an `is_ok_or_revert` arm and a failing arm. + fn with_reservoir(mut inputs: CallInputs, reservoir: u64) -> CallInputs { + inputs.reservoir = reservoir; + inputs + } + + /// Sets the call value on an already-built [`CallInputs`], as a non-static CALL. + fn with_value(mut inputs: CallInputs, value: U256) -> CallInputs { + inputs.value = CallValue::Transfer(value); + inputs.is_static = false; + inputs + } + /// One case per distinguishable path through the upstream body: dispatch miss, revert, /// success, each KZG failure variant, the wrapper's own gas gate, a generic /// malformed-input halt, an out-of-gas halt, and the DELEGATECALL bytecode-vs-target split. + /// Each of the two forwarded inputs no builtin consults — the state-gas reservoir and the + /// call value — additionally appears on a succeeding and on a failing case. fn parity_cases() -> Vec<(&'static str, CallInputs)> { let kzg = revm::precompile::kzg_point_evaluation::ADDRESS; let ecrecover = Address::with_last_byte(1); @@ -1510,6 +1629,63 @@ mod tests { CallScheme::DelegateCall, ), ), + ( + "success with reservoir", + with_reservoir( + call_inputs(&plain(identity, vec![0xCD; 96]), identity, true, 1_000_000), + 250_000, + ), + ), + ( + // The dynamic precompile echoes `input.reservoir` straight into its output, so + // this is the one case where a mirrored reservoir is directly observable in the + // returned `InterpreterResult` rather than only forwarded. + "dynamic revert with reservoir", + with_reservoir( + call_inputs( + &plain(PARITY_DYN_ADDRESS, vec![7; 8]), + PARITY_DYN_ADDRESS, + true, + 1_000_000, + ), + 250_000, + ), + ), + ( + "kzg invalid proof with reservoir", + with_reservoir( + call_inputs(&generate_invalid_proof_kzg_test_input(), kzg, true, 1_000_000), + 250_000, + ), + ), + ( + "success with value", + with_value( + call_inputs(&plain(identity, vec![0xEF; 96]), identity, false, 1_000_000), + U256::from(7u64), + ), + ), + ( + // The dynamic precompile echoes `input.value` into its output bytes, so this is + // the case that observes a mirrored call value rather than only forwarding it. + "dynamic revert with value", + with_value( + call_inputs( + &plain(PARITY_DYN_ADDRESS, vec![7; 8]), + PARITY_DYN_ADDRESS, + false, + 1_000_000, + ), + U256::from(7u64), + ), + ), + ( + "blake2f malformed input with value", + with_value( + call_inputs(&plain(blake2f, vec![0xAA; 32]), blake2f, false, 1_000_000), + U256::from(7u64), + ), + ), ] } From 4bc36d3b26671948123da5e960e49375c786766e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 15 Aug 2026 00:20:49 +0800 Subject: [PATCH 062/208] feat(rex7): derive the destroyed remainder from a conservation law Add a settlement-point derivation of a REX7 transaction's destroyed compute gas from what its envelope actually spent, and cross-check it in debug builds against the total the per-site recordings booked: destroyed = spent + double_counted_call_stipend - non_compute_gas - enforced_compute_gas Two lanes feed it. The non-compute lane collects the EVM gas a transaction spends that is neither compute work nor a destroyed remainder: in-frame MegaETH storage gas (through the existing exclude_storage_gas_from_segment funnel), the MegaETH share of intrinsic gas, the code-deposit storage charge, the KeylessDeploy caller materialization charge, and the sandbox boundary's residue. The stipend lane measures the one place recorded compute gas is deliberately not a partition of the envelope: a value-transferring CALL/CALLCODE has its CALL_STIPEND recorded by the caller and again by the callee, so the two frames book one stipend more than the envelope ever debited. Both lanes are REX7-gated at the tracker and the derivation lives inside a debug_assert, so frozen specs and release builds are unaffected. EIP-3529 refunds and the EIP-7623 floor need no term: they are applied after this read point and only move the number the receipt reports. Reading the envelope with total_gas_spent instead of the deprecated spent, which is the same subtraction while EIP-8037 stays pinned off. --- crates/mega-evm/src/evm/execution.rs | 50 +++++++++- crates/mega-evm/src/evm/instructions.rs | 8 ++ crates/mega-evm/src/limit/checkpoint.rs | 122 +++++++++++++++++++++++ crates/mega-evm/src/limit/compute_gas.rs | 9 +- crates/mega-evm/src/limit/limit.rs | 106 +++++++++++++++++++- crates/mega-evm/src/sandbox/execution.rs | 18 +++- 6 files changed, 306 insertions(+), 7 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index f316f4b2..062b9be2 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -487,7 +487,15 @@ impl MegaEvm { if frame.data.is_create() && interpreter_result.is_ok() { let code_deposit_storage_gas = constants::mini_rex::CODEDEPOSIT_STORAGE_GAS * interpreter_result.output.len() as u64; - if !interpreter_result.gas.record_regular_cost(code_deposit_storage_gas) { + if interpreter_result.gas.record_regular_cost(code_deposit_storage_gas) { + // Storage gas, not compute work — and it is charged after the frame's tail + // segment has been measured, so nothing else can classify it. + ctx.additional_limit + .borrow_mut() + .record_non_compute_gas(i128::from(code_deposit_storage_gas)); + } else { + // Nothing was debited, so there is nothing to classify; the frame now halts + // out of gas and the remainder it keeps is settled as destroyed below. interpreter_result.result = InstructionResult::OutOfGas; } } @@ -744,6 +752,11 @@ where ctx.additional_limit() .borrow_mut() .record_compute_gas(initial_and_floor_gas.initial_regular_gas); + // Everything this block adds to `initial_regular_gas` from here on is MegaETH storage + // gas: it is charged to the transaction's envelope but is not compute work, and only + // the base intrinsic just recorded is. Snapshot the base so the difference can be + // booked as non-compute gas once every addition is in. + let base_intrinsic_gas = initial_and_floor_gas.initial_regular_gas; // MegaETH MiniRex modification: calldata storage gas costs (10x the standard EVM rates) // - Standard tokens: 40 gas per token (vs 4) @@ -934,6 +947,15 @@ where .into()); } } + + // Book the MegaETH share of intrinsic gas — calldata storage gas, the flat REX + // intrinsic storage gas, and the callee-side / deposit-caller account-creation gas — + // as non-compute gas. Deliberately last: every contribution above is inside it, and + // the paths that return early from here are validation rejects, which never reach a + // settlement that would read the lane. + ctx.additional_limit().borrow_mut().record_non_compute_gas(i128::from( + initial_and_floor_gas.initial_regular_gas.saturating_sub(base_intrinsic_gas), + )); } Ok(initial_and_floor_gas) @@ -1031,6 +1053,32 @@ where let additional_limit = ctx.additional_limit.borrow(); let gas = frame_result.gas_mut(); gas.erase_cost(additional_limit.rescued_gas); + + // REX7: the transaction's envelope is now final — op-revm has normalised the gas + // object to `tx.gas_limit()`, the rescue has been handed back, and every frame's + // burn settlement and every precompile recording already ran. Nothing after this + // point burns gas; `post_execution`'s EIP-3529 refund and EIP-7623 floor only move + // the number the receipt reports. So this is where the destroyed remainder can be + // re-derived from what the transaction spent and checked against what the per-site + // recordings booked. A mismatch means either a site that destroys an envelope + // without booking it, or a spend the non-compute lane does not know about. + // + // `total_gas_spent` rather than the deprecated `spent`: the two are the same + // subtraction today, and EIP-8037's state-gas split — which is what deprecated the + // latter — is pinned off for every `MegaEVM` transaction, so the reservoir is + // structurally zero here. + debug_assert!( + !additional_limit.rex7_enabled() || + additional_limit.derived_burned_compute_gas(gas.total_gas_spent()) == + i128::from(additional_limit.burned_compute_gas()), + "destroyed compute gas disagrees with the conservation law: \ + derived {} vs booked {} (spent {}, non-compute {}, enforced compute {})", + additional_limit.derived_burned_compute_gas(gas.total_gas_spent()), + additional_limit.burned_compute_gas(), + gas.total_gas_spent(), + additional_limit.non_compute_gas(), + additional_limit.enforced_compute_gas(), + ); } Ok(()) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 6645564d..0736fc78 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1030,6 +1030,9 @@ macro_rules! record_storage_compute_gas { // for replay parity. `forwarded_child_gas` records that deducted amount so the abort path // below can return it to the parent. let mut forwarded_child_gas: u64 = 0; + // The stipend the caller keeps in its own window above and the callee will record again, + // booked only once the child is certain to run — see the abort path below. + let mut kept_call_stipend: u64 = 0; match $context.interpreter.bytecode.action() { Some(InterpreterAction::NewFrame(FrameInput::Call(call_inputs))) => { let stipend_from_revm = if spec.is_enabled(MegaSpecId::REX5) && @@ -1040,6 +1043,7 @@ macro_rules! record_storage_compute_gas { } else { 0 }; + kept_call_stipend = stipend_from_revm; let parent_contributed = call_inputs.gas_limit.saturating_sub(stipend_from_revm); forwarded_child_gas = parent_contributed; gas_used = gas_used.saturating_sub(parent_contributed); @@ -1061,6 +1065,10 @@ macro_rules! record_storage_compute_gas { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { + // The child will run, so the stipend this window kept is about to be recorded a + // second time by the callee. Book it where the destroyed-remainder derivation can + // reconcile the two sides. + additional_limit.record_double_counted_call_stipend(kept_call_stipend); None } else { Some(additional_limit.exceeding_instruction_result()) diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index 8fd95785..0a5ed875 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -46,6 +46,36 @@ pub(crate) struct CheckpointTracker { /// this flag instead, keeping the reported reason `VolatileDataAccessOutOfGas` exactly as /// per-opcode enforcement reports it. latched_detained: bool, + + /// EVM gas the transaction spends that is neither compute work nor a destroyed remainder. + /// + /// Every gas unit the transaction burns is exactly one of three things: work the trackers + /// record as compute gas, `MegaETH` storage gas, or budget an exceptional halt threw away. + /// This field is the running total of the second kind, which makes the third derivable at the + /// transaction's settlement point instead of having to be booked at each site that destroys + /// one — see [`AdditionalLimit::derived_burned_compute_gas`]( + /// super::AdditionalLimit::derived_burned_compute_gas). + /// + /// Signed because one contributor is a difference rather than a charge: the `KeylessDeploy` + /// sandbox boundary hands the parent one number for what the sandbox cost and another for what + /// it performed, and an inner EIP-3529 refund can make the second exceed the first. + non_compute_gas: i128, + + /// Compute gas the transaction records twice: revm's `CALL_STIPEND`, once in the caller and + /// again in the callee. + /// + /// A value-transferring `CALL` / `CALLCODE` charges the caller the full `CALLVALUE` fee and + /// then hands the callee a `CALL_STIPEND` funded out of that fee. From REX5 the caller's + /// compute window deliberately keeps the stipend — it subtracts only the gas it contributed + /// itself, `gas_limit - CALL_STIPEND` — while the callee separately records whatever of the + /// stipend it spends, and returns the rest to the caller. The caller's envelope is therefore + /// short by exactly one `CALL_STIPEND` per such call against what the two frames recorded + /// between them, whether or not the callee spent any of it. + /// + /// That makes the recorded compute total *not* a partition of the gas the transaction spent, + /// so the destroyed-remainder derivation has to add this back before the two sides can agree. + /// The behaviour is REX5 semantics and is frozen; this field only measures it. + double_counted_call_stipend: u64, } /// A gas clamp in force for one plain-opcode segment (REX7+). @@ -70,6 +100,8 @@ impl CheckpointTracker { baseline: 0, clamp: None, latched_detained: false, + non_compute_gas: 0, + double_counted_call_stipend: 0, } } @@ -77,6 +109,8 @@ impl CheckpointTracker { self.baseline = 0; self.clamp = None; self.latched_detained = false; + self.non_compute_gas = 0; + self.double_counted_call_stipend = 0; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -152,6 +186,39 @@ impl CheckpointTracker { self.latched_detained = latched; } + /// Adds `amount` to the transaction's non-compute EVM gas — see + /// [`non_compute_gas`](Self::non_compute_gas). No-op before REX7, where nothing derives the + /// destroyed remainder. + #[inline] + pub(crate) fn record_non_compute_gas(&mut self, amount: i128) { + if self.rex7_enabled { + self.non_compute_gas += amount; + } + } + + /// The transaction's non-compute EVM gas so far — see + /// [`non_compute_gas`](Self::non_compute_gas). + #[inline] + pub(crate) fn non_compute_gas(&self) -> i128 { + self.non_compute_gas + } + + /// Books one `CALL_STIPEND` the caller and callee both record — see + /// [`double_counted_call_stipend`](Self::double_counted_call_stipend). No-op before REX7. + #[inline] + pub(crate) fn record_double_counted_call_stipend(&mut self, amount: u64) { + if self.rex7_enabled { + self.double_counted_call_stipend += amount; + } + } + + /// The transaction's twice-recorded `CALL_STIPEND` total — see + /// [`double_counted_call_stipend`](Self::double_counted_call_stipend). + #[inline] + pub(crate) fn double_counted_call_stipend(&self) -> u64 { + self.double_counted_call_stipend + } + /// Returns the unsettled segment usage and re-opens the window at `remaining`. #[inline] pub(crate) fn take_segment(&mut self, remaining: u64) -> u64 { @@ -170,6 +237,8 @@ mod tests { tracker.sync_baseline(99_999); tracker.set_clamp(7, ClampBinding { headroom: 1, frame_local: false, limit: 1 }); tracker.set_latched_detained(true); + tracker.record_non_compute_gas(4_242); + tracker.record_double_counted_call_stipend(2_300); tracker } @@ -191,5 +260,58 @@ mod tests { !tracker.latched_detained(), "reset must drop a leftover detention-attribution flag" ); + assert_eq!( + tracker.non_compute_gas(), + 0, + "reset must drop the previous transaction's non-compute gas" + ); + assert_eq!( + tracker.double_counted_call_stipend(), + 0, + "reset must drop the previous transaction's twice-recorded call stipend" + ); + } + + /// The non-compute lane is REX7-only state: a frozen spec must keep it at zero so the + /// derivation it feeds can never be read as meaningful there. + #[test] + fn test_non_compute_gas_is_inert_before_rex7() { + let mut tracker = CheckpointTracker::new(MegaSpecId::REX6); + tracker.record_non_compute_gas(1_000); + assert_eq!(tracker.non_compute_gas(), 0, "pre-REX7 must not accumulate non-compute gas"); + } + + /// The sandbox boundary can hand the lane a negative contribution, so the accumulator must be + /// signed rather than saturating at zero. + #[test] + fn test_non_compute_gas_accumulates_signed() { + let mut tracker = CheckpointTracker::new(MegaSpecId::REX7); + tracker.record_non_compute_gas(100); + tracker.record_non_compute_gas(-250); + assert_eq!(tracker.non_compute_gas(), -150); + } + + /// Like the non-compute lane, the twice-recorded stipend is REX7-only state. + #[test] + fn test_double_counted_call_stipend_is_inert_before_rex7() { + let mut tracker = CheckpointTracker::new(MegaSpecId::REX6); + tracker.record_double_counted_call_stipend(2_300); + assert_eq!( + tracker.double_counted_call_stipend(), + 0, + "pre-REX7 must not accumulate the twice-recorded call stipend" + ); + } + + /// One transaction can make several value-transferring calls, and each contributes its own + /// stipend to the reconciliation — so the term accumulates rather than latching a single one. + #[test] + fn test_double_counted_call_stipend_accumulates_per_call() { + let mut tracker = CheckpointTracker::new(MegaSpecId::REX7); + tracker.record_double_counted_call_stipend(2_300); + tracker.record_double_counted_call_stipend(2_300); + // A call that forwards no stipend must leave the running total alone. + tracker.record_double_counted_call_stipend(0); + assert_eq!(tracker.double_counted_call_stipend(), 4_600); } } diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 8d55fbee..f228bb81 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -239,8 +239,15 @@ impl ComputeGasTracker { } /// Total recorded usage minus the destroyed remainders that must not enforce. + /// + /// This is the transaction's claim about how much compute work it actually performed. It is + /// built only from [`record_gas_used`](Self::record_gas_used) and + /// [`merge_persistent_usage`](Self::merge_persistent_usage) — a + /// [`record_burned_gas`](Self::record_burned_gas) raises `net_usage` and `burned` by the same + /// amount and so cancels here — which is what lets the destroyed remainder be re-derived from + /// it independently of how it was booked. #[inline] - fn enforced_tx_usage(&self) -> u64 { + pub(crate) fn enforced_tx_usage(&self) -> u64 { self.frame_tracker.net_usage().saturating_sub(self.burned) } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 27a6a0d5..cebb9c09 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -218,9 +218,98 @@ impl AdditionalLimit { /// re-syncs the baseline afterwards, so this is invisible there. /// /// No-op before REX7, where nothing measures against a baseline. + /// + /// The same charge is also the canonical funnel for the transaction's in-frame `MegaETH` + /// storage gas, so it feeds the non-compute lane the destroyed-remainder derivation reads. #[inline] pub(crate) fn exclude_storage_gas_from_segment(&mut self, amount: u64) { self.checkpoint.exclude_storage_gas_from_segment(amount); + self.checkpoint.record_non_compute_gas(i128::from(amount)); + } + + /// Records EVM gas the transaction spends that is neither compute work nor a destroyed + /// remainder (REX7+). + /// + /// The in-frame storage-gas charges arrive through + /// [`exclude_storage_gas_from_segment`](Self::exclude_storage_gas_from_segment); this is the + /// entry point for the contributions that are charged outside an open settlement segment — + /// the `MegaETH` share of intrinsic gas, the code-deposit storage charge, the `KeylessDeploy` + /// interceptor's caller-materialisation charge, and the sandbox boundary's residue. + #[inline] + pub(crate) fn record_non_compute_gas(&mut self, amount: i128) { + self.checkpoint.record_non_compute_gas(amount); + } + + /// Re-derives the transaction's destroyed compute gas from what it spent, rather than from the + /// sites that booked it (REX7+). + /// + /// Every unit of EVM gas a transaction burns is *almost* exactly one of three things: compute + /// work the trackers enforce, `MegaETH` storage gas, or budget an exceptional halt threw away + /// without executing anything for it. Two of those three are counted as they happen — + /// [`ComputeGasTracker::enforced_tx_usage`](compute_gas::ComputeGasTracker::enforced_tx_usage) + /// and the non-compute lane — so the third is whatever is left of `tx_gas_spent`: + /// + /// ```text + /// destroyed = tx_gas_spent + double_counted_call_stipend + /// − non_compute_gas − enforced_compute_gas + /// ``` + /// + /// The stipend term is what makes "almost" necessary: recorded compute gas is deliberately not + /// a partition of the gas the transaction spent, because a value-transferring call's + /// `CALL_STIPEND` is recorded by the caller and the callee both — see + /// [`CheckpointTracker::double_counted_call_stipend`]( + /// checkpoint::CheckpointTracker::double_counted_call_stipend). Adding it back is measurement, + /// not correction: the amount is booked from the single site that already computes it, and + /// without it the two sides disagree by one stipend per such call. + /// + /// `tx_gas_spent` must be read once the transaction's envelope is final and before any + /// post-execution adjustment that moves gas without anyone having burnt it — see the caller in + /// `MegaHandler::last_frame_result`. Gas that is rescued for the sender or hidden by the gas + /// clamp is erased from the envelope before that point, so neither can reach this subtraction. + /// + /// Signed on purpose: a mismatch against the booked total is a defect to report, and clamping + /// it at zero would hide the half of the mismatch space where the booking over-counts. + #[inline] + pub(crate) fn derived_burned_compute_gas(&self, tx_gas_spent: u64) -> i128 { + i128::from(tx_gas_spent) + i128::from(self.double_counted_call_stipend()) - + self.non_compute_gas() - + i128::from(self.enforced_compute_gas()) + } + + /// The `CALL_STIPEND` total this transaction recorded twice, in the caller and again in the + /// callee — the term that keeps recorded compute gas from being a partition of what the + /// transaction spent. Always 0 before REX7. + #[inline] + pub(crate) fn double_counted_call_stipend(&self) -> u64 { + self.checkpoint.double_counted_call_stipend() + } + + /// Books one `CALL_STIPEND` that the caller's compute window kept and the callee will record + /// again (REX7+). + /// + /// Called from the CALL-family settlement once the child frame is certain to run: on the + /// compute-limit abort path the pending child is discarded and its forwarded gas returned, so + /// no stipend is ever handed over and nothing is recorded twice. + #[inline] + pub(crate) fn record_double_counted_call_stipend(&mut self, amount: u64) { + self.checkpoint.record_double_counted_call_stipend(amount); + } + + /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, + /// always 0 before) — the second term of + /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas). + #[inline] + pub(crate) fn non_compute_gas(&self) -> i128 { + self.checkpoint.non_compute_gas() + } + + /// The compute gas the transaction claims to have performed: the reported total less the + /// destroyed remainders — the third term of + /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas), and the number every + /// compute-gas limit comparison runs against. + #[inline] + pub(crate) fn enforced_compute_gas(&self) -> u64 { + self.compute_gas.enforced_tx_usage() } /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, @@ -1138,7 +1227,22 @@ impl AdditionalLimit { /// reclassified here — the parent reports it and never enforces it, exactly as the sandbox /// did. Merging it as ordinary usage instead would let a sandbox frame's ordinary EVM halt /// fail the outer transaction on a resource limit. - pub(crate) fn merge_usage(&mut self, usage: LimitUsage, burned_compute_gas: u64) { + /// + /// `sandbox_gas_used` is the EVM gas the sandbox cost the parent's own counter: the parent + /// pre-debits a reservation and gets the unused part back, so this is what the parent's + /// envelope is short by. Whatever of it the sandbox did not spend on compute work is + /// non-compute gas from the parent's point of view — the sandbox's own storage gas, less any + /// refund the sandbox's receipt handed back — and joins the lane the destroyed-remainder + /// derivation reads. The difference is taken against the merged total rather than the enforced + /// part so the sandbox's destroyed remainder stays destroyed on the parent's books instead of + /// being reclassified as storage gas. + pub(crate) fn merge_usage( + &mut self, + usage: LimitUsage, + burned_compute_gas: u64, + sandbox_gas_used: u64, + ) { + self.record_non_compute_gas(i128::from(sandbox_gas_used) - i128::from(usage.compute_gas)); self.compute_gas.merge_persistent_usage(usage.compute_gas); self.compute_gas.merge_burned_usage(burned_compute_gas); self.data_size.merge_persistent_usage(usage.data_size); diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index d59f41a9..9d0ddb98 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -970,7 +970,7 @@ fn apply_sandbox_post_accounting( sandbox_gas_used: u64, return_memory_offset: &core::ops::Range, ) -> Option { - merge_sandbox_limit_usage(ctx, limit_usage); + merge_sandbox_limit_usage(ctx, limit_usage, sandbox_gas_used); ctx.volatile_data_tracker.borrow_mut().merge_accesses_from_bitmap(volatile_accesses); refund_unused_sandbox_gas(gas, reservation, sandbox_gas_used); reject_if_tx_limit_overflow(ctx, gas, return_memory_offset) @@ -1026,6 +1026,9 @@ fn charge_caller_materialization_pre_sandbox( ctx: &MegaContext, limit_usage: SandboxUsage, + sandbox_gas_used: u64, ) { - ctx.additional_limit - .borrow_mut() - .merge_usage(limit_usage.usage, limit_usage.burned_compute_gas); + ctx.additional_limit.borrow_mut().merge_usage( + limit_usage.usage, + limit_usage.burned_compute_gas, + sandbox_gas_used, + ); } /// Returns the unused portion of the sandbox's pre-debited gas reservation to the From 7cc03e963cf8bc6bf385eb703f75e8d5e60aae8f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 15 Aug 2026 23:10:38 +0800 Subject: [PATCH 063/208] refactor(rex7): name the call-stipend term after the mint that creates it The CALL_STIPEND term in the destroyed-remainder derivation is not a double count: revm mints the stipend into the child frame's budget without debiting the caller, and the caller's compute window already subtracts exactly what it contributed. Rename the field and its accessors to minted_call_stipend and restate every comment in terms of the mint. --- crates/mega-evm/src/evm/instructions.rs | 28 +++++---- crates/mega-evm/src/limit/checkpoint.rs | 83 +++++++++++++------------ crates/mega-evm/src/limit/limit.rs | 31 +++++---- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 0736fc78..4ab62b47 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1024,15 +1024,16 @@ macro_rules! record_storage_compute_gas { (const { static_gas($opcode) } + $gas_before.saturating_sub(gas_after)) .saturating_sub($storage_charged) }; - // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` - // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the - // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit - // for replay parity. `forwarded_child_gas` records that deducted amount so the abort path - // below can return it to the parent. + // Exclude gas forwarded to a child frame. REX5+ keeps the revm-side `CALL_STIPEND` out of + // the subtraction — a value-transferring CALL/CALLCODE mints it into the child's budget + // instead of deducting it from the parent — so the parent's compute gas is not + // under-counted; pre-REX5 subtracts the full child gas limit for replay parity. + // `forwarded_child_gas` records that deducted amount so the abort path below can return it + // to the parent. let mut forwarded_child_gas: u64 = 0; - // The stipend the caller keeps in its own window above and the callee will record again, - // booked only once the child is certain to run — see the abort path below. - let mut kept_call_stipend: u64 = 0; + // The stipend revm mints into the child's budget without debiting the caller, booked only + // once the child is certain to run — see the abort path below. + let mut minted_call_stipend: u64 = 0; match $context.interpreter.bytecode.action() { Some(InterpreterAction::NewFrame(FrameInput::Call(call_inputs))) => { let stipend_from_revm = if spec.is_enabled(MegaSpecId::REX5) && @@ -1043,7 +1044,7 @@ macro_rules! record_storage_compute_gas { } else { 0 }; - kept_call_stipend = stipend_from_revm; + minted_call_stipend = stipend_from_revm; let parent_contributed = call_inputs.gas_limit.saturating_sub(stipend_from_revm); forwarded_child_gas = parent_contributed; gas_used = gas_used.saturating_sub(parent_contributed); @@ -1065,10 +1066,11 @@ macro_rules! record_storage_compute_gas { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { - // The child will run, so the stipend this window kept is about to be recorded a - // second time by the callee. Book it where the destroyed-remainder derivation can - // reconcile the two sides. - additional_limit.record_double_counted_call_stipend(kept_call_stipend); + // The child will run, so the stipend revm minted into its budget is now live: the + // callee either spends it as work no envelope funded, or hands it back and shrinks + // the envelope. Book it where the destroyed-remainder derivation can reconcile the + // recorded work against what the transaction spent. + additional_limit.record_minted_call_stipend(minted_call_stipend); None } else { Some(additional_limit.exceeding_instruction_result()) diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index 0a5ed875..8e128cf0 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -61,21 +61,26 @@ pub(crate) struct CheckpointTracker { /// it performed, and an inner EIP-3529 refund can make the second exceed the first. non_compute_gas: i128, - /// Compute gas the transaction records twice: revm's `CALL_STIPEND`, once in the caller and - /// again in the callee. + /// `CALL_STIPEND` gas revm mints into child frames, which the transaction's envelope never + /// funded. /// - /// A value-transferring `CALL` / `CALLCODE` charges the caller the full `CALLVALUE` fee and - /// then hands the callee a `CALL_STIPEND` funded out of that fee. From REX5 the caller's - /// compute window deliberately keeps the stipend — it subtracts only the gas it contributed - /// itself, `gas_limit - CALL_STIPEND` — while the callee separately records whatever of the - /// stipend it spends, and returns the rest to the caller. The caller's envelope is therefore - /// short by exactly one `CALL_STIPEND` per such call against what the two frames recorded - /// between them, whether or not the callee spent any of it. + /// A value-transferring `CALL` / `CALLCODE` debits the caller only the gas it forwards, and + /// then hands the child a frame budget of `forwarded + CALL_STIPEND`. The extra stipend is + /// conjured at the frame boundary — it is the protocol's EIP-150 subsidy, notionally paid for + /// by the `CALLVALUE` fee, but mechanically it is never taken out of anyone's gas counter. + /// No frame records the same gas twice: from REX5 the caller's compute window subtracts + /// exactly what the caller contributed, `gas_limit - CALL_STIPEND`. /// - /// That makes the recorded compute total *not* a partition of the gas the transaction spent, - /// so the destroyed-remainder derivation has to add this back before the two sides can agree. - /// The behaviour is REX5 semantics and is frozen; this field only measures it. - double_counted_call_stipend: u64, + /// The minted gas leaves the transaction one stipend richer per such call, however it is used. + /// Spent by the callee, it is recorded as work no envelope paid for; returned when the child + /// exits, it shrinks the envelope by the same amount. Either way the frames' recorded work + /// exceeds what the transaction spent by exactly one `CALL_STIPEND` per call, whatever the + /// callee did with it. + /// + /// So the recorded compute total is *not* a partition of the gas the transaction spent, and + /// the destroyed-remainder derivation has to account for the minted gas before the two sides + /// can agree. The behaviour is REX5 semantics and is frozen; this field only measures it. + minted_call_stipend: u64, } /// A gas clamp in force for one plain-opcode segment (REX7+). @@ -101,7 +106,7 @@ impl CheckpointTracker { clamp: None, latched_detained: false, non_compute_gas: 0, - double_counted_call_stipend: 0, + minted_call_stipend: 0, } } @@ -110,7 +115,7 @@ impl CheckpointTracker { self.clamp = None; self.latched_detained = false; self.non_compute_gas = 0; - self.double_counted_call_stipend = 0; + self.minted_call_stipend = 0; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -203,20 +208,20 @@ impl CheckpointTracker { self.non_compute_gas } - /// Books one `CALL_STIPEND` the caller and callee both record — see - /// [`double_counted_call_stipend`](Self::double_counted_call_stipend). No-op before REX7. + /// Books one `CALL_STIPEND` minted into a child frame — see + /// [`minted_call_stipend`](Self::minted_call_stipend). No-op before REX7. #[inline] - pub(crate) fn record_double_counted_call_stipend(&mut self, amount: u64) { + pub(crate) fn record_minted_call_stipend(&mut self, amount: u64) { if self.rex7_enabled { - self.double_counted_call_stipend += amount; + self.minted_call_stipend += amount; } } - /// The transaction's twice-recorded `CALL_STIPEND` total — see - /// [`double_counted_call_stipend`](Self::double_counted_call_stipend). + /// The transaction's minted `CALL_STIPEND` total — see + /// [`minted_call_stipend`](Self::minted_call_stipend). #[inline] - pub(crate) fn double_counted_call_stipend(&self) -> u64 { - self.double_counted_call_stipend + pub(crate) fn minted_call_stipend(&self) -> u64 { + self.minted_call_stipend } /// Returns the unsettled segment usage and re-opens the window at `remaining`. @@ -238,7 +243,7 @@ mod tests { tracker.set_clamp(7, ClampBinding { headroom: 1, frame_local: false, limit: 1 }); tracker.set_latched_detained(true); tracker.record_non_compute_gas(4_242); - tracker.record_double_counted_call_stipend(2_300); + tracker.record_minted_call_stipend(2_300); tracker } @@ -266,9 +271,9 @@ mod tests { "reset must drop the previous transaction's non-compute gas" ); assert_eq!( - tracker.double_counted_call_stipend(), + tracker.minted_call_stipend(), 0, - "reset must drop the previous transaction's twice-recorded call stipend" + "reset must drop the previous transaction's minted call stipend" ); } @@ -291,27 +296,27 @@ mod tests { assert_eq!(tracker.non_compute_gas(), -150); } - /// Like the non-compute lane, the twice-recorded stipend is REX7-only state. + /// Like the non-compute lane, the minted stipend is REX7-only state. #[test] - fn test_double_counted_call_stipend_is_inert_before_rex7() { + fn test_minted_call_stipend_is_inert_before_rex7() { let mut tracker = CheckpointTracker::new(MegaSpecId::REX6); - tracker.record_double_counted_call_stipend(2_300); + tracker.record_minted_call_stipend(2_300); assert_eq!( - tracker.double_counted_call_stipend(), + tracker.minted_call_stipend(), 0, - "pre-REX7 must not accumulate the twice-recorded call stipend" + "pre-REX7 must not accumulate the minted call stipend" ); } - /// One transaction can make several value-transferring calls, and each contributes its own - /// stipend to the reconciliation — so the term accumulates rather than latching a single one. + /// One transaction can make several value-transferring calls, and each mints its own stipend + /// into its child — so the term accumulates rather than latching a single one. #[test] - fn test_double_counted_call_stipend_accumulates_per_call() { + fn test_minted_call_stipend_accumulates_per_call() { let mut tracker = CheckpointTracker::new(MegaSpecId::REX7); - tracker.record_double_counted_call_stipend(2_300); - tracker.record_double_counted_call_stipend(2_300); - // A call that forwards no stipend must leave the running total alone. - tracker.record_double_counted_call_stipend(0); - assert_eq!(tracker.double_counted_call_stipend(), 4_600); + tracker.record_minted_call_stipend(2_300); + tracker.record_minted_call_stipend(2_300); + // A call that mints no stipend must leave the running total alone. + tracker.record_minted_call_stipend(0); + assert_eq!(tracker.minted_call_stipend(), 4_600); } } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index cebb9c09..1b742e55 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -250,17 +250,17 @@ impl AdditionalLimit { /// and the non-compute lane — so the third is whatever is left of `tx_gas_spent`: /// /// ```text - /// destroyed = tx_gas_spent + double_counted_call_stipend + /// destroyed = tx_gas_spent + minted_call_stipend /// − non_compute_gas − enforced_compute_gas /// ``` /// /// The stipend term is what makes "almost" necessary: recorded compute gas is deliberately not /// a partition of the gas the transaction spent, because a value-transferring call's - /// `CALL_STIPEND` is recorded by the caller and the callee both — see - /// [`CheckpointTracker::double_counted_call_stipend`]( - /// checkpoint::CheckpointTracker::double_counted_call_stipend). Adding it back is measurement, - /// not correction: the amount is booked from the single site that already computes it, and - /// without it the two sides disagree by one stipend per such call. + /// `CALL_STIPEND` is minted into the child frame rather than debited from the caller — see + /// [`CheckpointTracker::minted_call_stipend`]( + /// checkpoint::CheckpointTracker::minted_call_stipend). Adding it back is measurement, not + /// correction: the amount is booked from the single site that already computes it, and without + /// it the two sides disagree by one stipend per such call. /// /// `tx_gas_spent` must be read once the transaction's envelope is final and before any /// post-execution adjustment that moves gas without anyone having burnt it — see the caller in @@ -271,28 +271,27 @@ impl AdditionalLimit { /// it at zero would hide the half of the mismatch space where the booking over-counts. #[inline] pub(crate) fn derived_burned_compute_gas(&self, tx_gas_spent: u64) -> i128 { - i128::from(tx_gas_spent) + i128::from(self.double_counted_call_stipend()) - + i128::from(tx_gas_spent) + i128::from(self.minted_call_stipend()) - self.non_compute_gas() - i128::from(self.enforced_compute_gas()) } - /// The `CALL_STIPEND` total this transaction recorded twice, in the caller and again in the - /// callee — the term that keeps recorded compute gas from being a partition of what the + /// The `CALL_STIPEND` total this transaction's value-transferring calls minted into their + /// child frames — the term that keeps recorded compute gas from being a partition of what the /// transaction spent. Always 0 before REX7. #[inline] - pub(crate) fn double_counted_call_stipend(&self) -> u64 { - self.checkpoint.double_counted_call_stipend() + pub(crate) fn minted_call_stipend(&self) -> u64 { + self.checkpoint.minted_call_stipend() } - /// Books one `CALL_STIPEND` that the caller's compute window kept and the callee will record - /// again (REX7+). + /// Books one `CALL_STIPEND` minted into a child frame that the caller never funded (REX7+). /// /// Called from the CALL-family settlement once the child frame is certain to run: on the /// compute-limit abort path the pending child is discarded and its forwarded gas returned, so - /// no stipend is ever handed over and nothing is recorded twice. + /// the mint never reaches a frame and there is nothing to book. #[inline] - pub(crate) fn record_double_counted_call_stipend(&mut self, amount: u64) { - self.checkpoint.record_double_counted_call_stipend(amount); + pub(crate) fn record_minted_call_stipend(&mut self, amount: u64) { + self.checkpoint.record_minted_call_stipend(amount); } /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, From 2731ddd14e61f84ee5a4ed9c1d0a1ed7b6ab1e5a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 15 Aug 2026 23:16:05 +0800 Subject: [PATCH 064/208] feat(rex7): report the destroyed remainder derived at settlement MegaTransactionOutcome.compute_gas_destroyed now comes from the conservation law settled once the transaction's envelope is final, not from the sum of the sites that destroyed it. The per-site bookings stay as the enforcement split and as the independent second opinion the settlement point's debug assert holds the derivation to. A negative derivation clamps to zero in release and trips in debug. --- crates/mega-evm/src/block/limit.rs | 9 +-- crates/mega-evm/src/evm/execution.rs | 32 ++++------ crates/mega-evm/src/evm/mod.rs | 4 +- crates/mega-evm/src/evm/result.rs | 10 ++- crates/mega-evm/src/limit/checkpoint.rs | 25 ++++++++ crates/mega-evm/src/limit/limit.rs | 81 ++++++++++++++++++++++-- crates/mega-evm/src/sandbox/execution.rs | 7 ++ 7 files changed, 133 insertions(+), 35 deletions(-) diff --git a/crates/mega-evm/src/block/limit.rs b/crates/mega-evm/src/block/limit.rs index 05026140..6cb69b66 100644 --- a/crates/mega-evm/src/block/limit.rs +++ b/crates/mega-evm/src/block/limit.rs @@ -926,10 +926,11 @@ impl BlockLimiter { /// accounting. /// /// Compute gas arrives as two numbers, not one: `compute_gas_used` is the transaction's full - /// reported total and `compute_gas_destroyed` is the part of it that Rex7+ exceptionally - /// halted frames destroyed rather than performed (0 before Rex7). The reported total lands in - /// the public statistic and the difference in the counter the block compute-gas limit is - /// evaluated against. + /// reported total and `compute_gas_destroyed` is the part of it that Rex7+ destroyed rather + /// than performed (0 before Rex7). The reported total lands in the public statistic and the + /// difference in the counter the block compute-gas limit is evaluated against. Both come from + /// the transaction's own report, so the block inherits whatever the transaction settled — it + /// does not re-derive the split, and there is no second definition of it here. #[allow(clippy::too_many_arguments)] pub fn post_execution_update_raw( &mut self, diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 062b9be2..3462a07a 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1050,35 +1050,25 @@ where if is_mini_rex { let ctx = evm.ctx_mut(); - let additional_limit = ctx.additional_limit.borrow(); + let mut additional_limit = ctx.additional_limit.borrow_mut(); let gas = frame_result.gas_mut(); gas.erase_cost(additional_limit.rescued_gas); - // REX7: the transaction's envelope is now final — op-revm has normalised the gas - // object to `tx.gas_limit()`, the rescue has been handed back, and every frame's - // burn settlement and every precompile recording already ran. Nothing after this - // point burns gas; `post_execution`'s EIP-3529 refund and EIP-7623 floor only move - // the number the receipt reports. So this is where the destroyed remainder can be - // re-derived from what the transaction spent and checked against what the per-site - // recordings booked. A mismatch means either a site that destroys an envelope - // without booking it, or a spend the non-compute lane does not know about. + // REX7 settlement point. The transaction's envelope is final exactly here: op-revm + // has normalised the gas object to `tx.gas_limit()`, the rescue has been handed back, + // and every frame's burn settlement and every precompile recording already ran. + // Nothing after this point burns gas — `post_execution`'s EIP-3529 refund and EIP-7623 + // floor only move the number the receipt reports — so this is the one place the + // envelope can be read to derive what the transaction destroyed. Reading it any later + // would fold a refund into the destroyed total; reading it any earlier would miss the + // rescue. The derived value is what the transaction reports, so this read point is + // load-bearing for the reported number, not just for the cross-check it also runs. // // `total_gas_spent` rather than the deprecated `spent`: the two are the same // subtraction today, and EIP-8037's state-gas split — which is what deprecated the // latter — is pinned off for every `MegaEVM` transaction, so the reservoir is // structurally zero here. - debug_assert!( - !additional_limit.rex7_enabled() || - additional_limit.derived_burned_compute_gas(gas.total_gas_spent()) == - i128::from(additional_limit.burned_compute_gas()), - "destroyed compute gas disagrees with the conservation law: \ - derived {} vs booked {} (spent {}, non-compute {}, enforced compute {})", - additional_limit.derived_burned_compute_gas(gas.total_gas_spent()), - additional_limit.burned_compute_gas(), - gas.total_gas_spent(), - additional_limit.non_compute_gas(), - additional_limit.enforced_compute_gas(), - ); + additional_limit.settle_destroyed_compute_gas(gas.total_gas_spent()); } Ok(()) diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index b8173420..a4fb8aea 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -367,7 +367,7 @@ where data_size, kv_updates, compute_gas_used: compute_gas, - compute_gas_destroyed: additional_limit.burned_compute_gas(), + compute_gas_destroyed: additional_limit.destroyed_compute_gas(), state_growth_used: state_growth, }) } @@ -399,7 +399,7 @@ where data_size, kv_updates, compute_gas_used: compute_gas, - compute_gas_destroyed: additional_limit.burned_compute_gas(), + compute_gas_destroyed: additional_limit.destroyed_compute_gas(), state_growth_used: state_growth, }) } diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index d4fed47f..97249beb 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -40,8 +40,14 @@ pub struct MegaTransactionOutcome { /// them diverge from that path: the burn split is settled before `frame_end`, and the /// plain-segment delta is read from the interpreter counter. pub compute_gas_used: u64, - /// The part of [`compute_gas_used`](Self::compute_gas_used) that exceptionally halted frames - /// destroyed rather than performed (Rex7+, always 0 before). + /// The part of [`compute_gas_used`](Self::compute_gas_used) the transaction destroyed rather + /// than performed (Rex7+, always 0 before). + /// + /// Derived from what the transaction spent, not summed from the sites that destroyed it: gas + /// the transaction burnt is either work the trackers recorded, MegaETH storage gas, or a + /// budget something threw away without executing anything for it, and this field is the last + /// of the three read off as the remainder. A transaction that produces no receipt has no + /// envelope to split and reports zero. /// /// Destroyed gas is not work the network did, so no resource limit is evaluated against it. /// The transaction's own limits already excluded it while executing; a consumer that diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index 8e128cf0..55ee1646 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -81,6 +81,15 @@ pub(crate) struct CheckpointTracker { /// the destroyed-remainder derivation has to account for the minted gas before the two sides /// can agree. The behaviour is REX5 semantics and is frozen; this field only measures it. minted_call_stipend: u64, + + /// The transaction's destroyed compute gas, as derived from the conservation law once its + /// envelope was final — the number the transaction reports. + /// + /// Zero until the settlement point writes it, so a transaction that never reaches settlement + /// (a validation reject, which produces no receipt) reports nothing destroyed. See + /// [`AdditionalLimit::settle_destroyed_compute_gas`]( + /// super::AdditionalLimit::settle_destroyed_compute_gas). + settled_destroyed: u64, } /// A gas clamp in force for one plain-opcode segment (REX7+). @@ -107,6 +116,7 @@ impl CheckpointTracker { latched_detained: false, non_compute_gas: 0, minted_call_stipend: 0, + settled_destroyed: 0, } } @@ -116,6 +126,7 @@ impl CheckpointTracker { self.latched_detained = false; self.non_compute_gas = 0; self.minted_call_stipend = 0; + self.settled_destroyed = 0; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -224,6 +235,20 @@ impl CheckpointTracker { self.minted_call_stipend } + /// Stores the destroyed compute gas the settlement point derived — see + /// [`settled_destroyed`](Self::settled_destroyed). + #[inline] + pub(crate) fn set_settled_destroyed(&mut self, amount: u64) { + self.settled_destroyed = amount; + } + + /// The destroyed compute gas the settlement point derived — see + /// [`settled_destroyed`](Self::settled_destroyed). + #[inline] + pub(crate) fn settled_destroyed(&self) -> u64 { + self.settled_destroyed + } + /// Returns the unsettled segment usage and re-opens the window at `remaining`. #[inline] pub(crate) fn take_segment(&mut self, remaining: u64) -> u64 { diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 1b742e55..d500d063 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -269,6 +269,8 @@ impl AdditionalLimit { /// /// Signed on purpose: a mismatch against the booked total is a defect to report, and clamping /// it at zero would hide the half of the mismatch space where the booking over-counts. + /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) is what turns the + /// signed result into the number the transaction reports. #[inline] pub(crate) fn derived_burned_compute_gas(&self, tx_gas_spent: u64) -> i128 { i128::from(tx_gas_spent) + i128::from(self.minted_call_stipend()) - @@ -284,6 +286,68 @@ impl AdditionalLimit { self.checkpoint.minted_call_stipend() } + /// Settles the transaction's destroyed compute gas from the conservation law and stores it as + /// the number the transaction reports (REX7+; a no-op before, where nothing is destroyed). + /// + /// `tx_gas_spent` must be the envelope the transaction actually burnt, read at the one moment + /// it is final: after op-revm has normalised the gas object and the resource-limit rescue has + /// been handed back, and before `post_execution` applies the EIP-3529 refund and the EIP-7623 + /// floor. Those two move the number the receipt reports without anybody having burnt the + /// difference, so reading after them would fold a refund into the destroyed total. Gas that is + /// rescued for the sender and gas the clamp was hiding are both erased from the envelope + /// before this point, so neither can reach the subtraction either. + /// + /// The per-site destroyed bookings do not feed this number. They stay as the independent + /// second opinion the `debug_assert` below cross-checks the derivation against, so a site that + /// destroys an envelope without booking it — or a spend the non-compute lane does not know + /// about — still fails loudly in debug builds and in the test corpus. + /// + /// A negative derivation is defended against rather than expected: it would mean the recorded + /// compute and non-compute lanes together claim more gas than the transaction spent, which no + /// spec produces today. Debug builds trip on it; release builds clamp to zero so a reporting + /// defect cannot wrap into an enormous destroyed total. + #[inline] + pub(crate) fn settle_destroyed_compute_gas(&mut self, tx_gas_spent: u64) { + if !self.rex7_enabled() { + return; + } + let derived = self.derived_burned_compute_gas(tx_gas_spent); + debug_assert!( + derived >= 0, + "derived destroyed compute gas is negative: {derived} \ + (spent {tx_gas_spent}, minted stipend {}, non-compute {}, enforced compute {})", + self.minted_call_stipend(), + self.non_compute_gas(), + self.enforced_compute_gas(), + ); + debug_assert!( + derived == i128::from(self.burned_compute_gas()), + "destroyed compute gas disagrees with the conservation law: \ + derived {derived} vs booked {} \ + (spent {tx_gas_spent}, minted stipend {}, non-compute {}, enforced compute {})", + self.burned_compute_gas(), + self.minted_call_stipend(), + self.non_compute_gas(), + self.enforced_compute_gas(), + ); + let settled = u64::try_from(derived.max(0)).unwrap_or(u64::MAX); + self.checkpoint.set_settled_destroyed(settled); + } + + /// The transaction's destroyed compute gas, as settled by + /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) — the part of + /// [`get_usage`](Self::get_usage)'s `compute_gas` that is reported and accounted but never + /// enforced. + /// + /// This is the reporting answer, and the only one a caller outside this module should use. + /// [`burned_compute_gas`](Self::burned_compute_gas) is the per-site booking that backs the + /// transaction's own enforcement and cross-checks this derivation; the two agree, and reading + /// the wrong one would silently pick the wrong side of that check. + #[inline] + pub(crate) fn destroyed_compute_gas(&self) -> u64 { + self.checkpoint.settled_destroyed() + } + /// Books one `CALL_STIPEND` minted into a child frame that the caller never funded (REX7+). /// /// Called from the CALL-family settlement once the child frame is certain to run: on the @@ -442,13 +506,18 @@ impl AdditionalLimit { self.has_exceeded_limit = LimitCheck::Exempt; } - /// The part of [`get_usage`](Self::get_usage)'s `compute_gas` that exceptionally halted frames - /// destroyed rather than performed (REX7+, always 0 before). + /// The destroyed remainders the per-site bookings recorded, summed as they happened (REX7+, + /// always 0 before) — **not** the number the transaction reports. + /// + /// This is the sum that separates the recorded compute total into the work every limit is + /// evaluated against and the remainder none of them sees, so it is what the transaction's own + /// enforcement runs on, and what a tracker merging this transaction's usage — today the + /// `KeylessDeploy` sandbox boundary — must carry alongside the total, or the receiving tracker + /// re-enforces gas the EVM already destroyed. /// - /// Reported and accounted like the rest of the total, never enforced. A caller that merges - /// this transaction's usage into another tracker — today the `KeylessDeploy` sandbox boundary — - /// has to carry it alongside the total, or the receiving tracker re-enforces gas the EVM - /// already destroyed. + /// It is also the second opinion the settlement point's `debug_assert` holds the derivation + /// to. For the reported destroyed total use + /// [`destroyed_compute_gas`](Self::destroyed_compute_gas). #[inline] pub(crate) fn burned_compute_gas(&self) -> u64 { self.compute_gas.burned_usage() diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 9d0ddb98..4ca9950b 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -659,6 +659,13 @@ fn run_sandbox_ctx( let additional_limit = sandbox_evm.ctx.additional_limit.borrow(); SandboxUsage { usage: additional_limit.get_usage(), + // The per-site booking, deliberately, not the sandbox's own derived report. What + // crosses this boundary is the split the parent must *enforce* on: the parent adds + // the sandbox's whole total to its own and then declares this much of it + // non-enforcing, which is how it inherits the sandbox's executed compute. The + // parent's reported destroyed total is settled once, from the conservation law, at + // the outer transaction's settlement point — this number is an input to the term + // that derivation reads, not a second place destroyed gas gets reported. burned_compute_gas: additional_limit.burned_compute_gas(), } }; From 6d9eca88ca49da2480e280a672bbcaf767cf2c4d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 15 Aug 2026 23:35:24 +0800 Subject: [PATCH 065/208] test(rex7): cover the conservation law's terms in combination New rex7/conservation_terms module: a minted call stipend and a destroyed envelope in one transaction, several mints in one transaction, and a KeylessDeploy sandbox whose EIP-3529 refund drives the non-compute lane negative (on its own and alongside a destroyed remainder). Seam tests in limit.rs drive the negative-derivation guard and the signed lane directly. The EIP-8037 reservoir is documented as pinned off rather than constructed. --- crates/mega-evm/src/evm/result.rs | 2 +- crates/mega-evm/src/limit/limit.rs | 66 ++++ crates/mega-evm/tests/rex7/burn_split.rs | 16 +- crates/mega-evm/tests/rex7/common.rs | 56 ++- .../mega-evm/tests/rex7/conservation_terms.rs | 343 ++++++++++++++++++ .../tests/rex7/guard_pass_static_gas.rs | 16 +- crates/mega-evm/tests/rex7/main.rs | 1 + 7 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/conservation_terms.rs diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 97249beb..6331fb88 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -44,7 +44,7 @@ pub struct MegaTransactionOutcome { /// than performed (Rex7+, always 0 before). /// /// Derived from what the transaction spent, not summed from the sites that destroyed it: gas - /// the transaction burnt is either work the trackers recorded, MegaETH storage gas, or a + /// the transaction burnt is either work the trackers recorded, `MegaETH` storage gas, or a /// budget something threw away without executing anything for it, and this field is the last /// of the three read off as the remainder. A transaction that produces no receipt has no /// envelope to split and reports zero. diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index d500d063..8035bc78 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -375,6 +375,16 @@ impl AdditionalLimit { self.compute_gas.enforced_tx_usage() } + /// Test-only reads of the conservation law's terms, so a test can assert which term a fixture + /// actually moved instead of inferring it from the destroyed total the terms combine into. + /// + /// Returns `(non-compute gas, minted call stipend, per-site destroyed bookings)`. + #[cfg(any(test, feature = "test-utils"))] + #[doc(hidden)] + pub fn conservation_terms_for_test(&self) -> (i128, u64, u64) { + (self.non_compute_gas(), self.minted_call_stipend(), self.burned_compute_gas()) + } + /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, /// returning that amount. /// @@ -1865,4 +1875,60 @@ mod tests { assert_eq!(create_outcome.result.result, InstructionResult::OutOfGas); assert_eq!(create_outcome.result.output, output); } + + /// The settlement point turns the signed derivation into the number the transaction reports, + /// clamping the one direction that must never reach a consumer. + /// + /// A negative derivation means the recorded compute and non-compute lanes together claim more + /// gas than the transaction spent, which no spec produces today — the guard is defence, not an + /// expected shape. Driving it at the seam is the only way to reach it: every end-to-end fixture + /// that could produce it would have to break the conservation law first. + /// + /// Debug builds trip the assert instead of clamping, so the test asserts the panic there and + /// the clamp in release. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "derived destroyed compute gas is negative") + )] + fn test_negative_derivation_is_clamped_to_zero() { + let mut limit = AdditionalLimit::new(MegaSpecId::REX7, test_limits()); + // Claim more non-compute gas than the envelope the settlement is handed. + limit.record_non_compute_gas(1_000); + + limit.settle_destroyed_compute_gas(100); + + assert_eq!( + limit.destroyed_compute_gas(), + 0, + "a negative derivation must clamp to zero rather than wrap into an enormous total", + ); + } + + /// The sandbox boundary hands the lane a difference, not a charge, so a sandbox whose own + /// EIP-3529 refund outgrew its storage gas drives the non-compute lane negative. The + /// derivation has to stay correct across that sign change — a lane that saturated at zero + /// would silently under-report the destroyed remainder by the whole overshoot. + /// + /// The end-to-end shape that produces a negative lane is in the REX7 suite; this pins the + /// arithmetic at the seam, where the sign can be set directly. + #[test] + fn test_derivation_survives_a_negative_non_compute_lane() { + let mut limit = AdditionalLimit::new(MegaSpecId::REX7, test_limits()); + // A sandbox that cost the parent 1,000 gas while recording 3,000 of compute work: the + // 2,000 difference is refund the sandbox's own receipt handed back. + limit.merge_usage(LimitUsage { compute_gas: 3_000, ..Default::default() }, 0, 1_000); + assert_eq!(limit.non_compute_gas(), -2_000, "the lane must carry the difference signed"); + // On top of that, a frame destroyed 4,000 of its budget. + limit.record_burned_gas(4_000); + + // 5,000 spent = 3,000 enforced + (−2,000) non-compute + 4,000 destroyed. + limit.settle_destroyed_compute_gas(5_000); + + assert_eq!( + limit.destroyed_compute_gas(), + 4_000, + "the negative lane must add to the destroyed remainder, not saturate away", + ); + } } diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 42356aeb..490f5021 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -426,8 +426,17 @@ fn transact_create_reject( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let detained_compute_gas_limit = - EvmTr::ctx_ref(&evm).additional_limit.borrow().detained_compute_gas_limit(); + let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); + ( + additional_limit.detained_compute_gas_limit(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) + }; let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { result: outcome.result_and_state.result, @@ -438,6 +447,9 @@ fn transact_create_reject( gas_used, destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, state: outcome.result_and_state.state, } } diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 24e90230..773234e7 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -43,6 +43,14 @@ pub(crate) struct Outcome { /// Post-tx detained compute gas limit — equal to the configured TX limit unless volatile /// access lowered it. pub(crate) detained_compute_gas_limit: u64, + /// Post-tx non-compute EVM gas — the `MegaETH` storage gas and sandbox residue the destroyed + /// derivation subtracts. Signed: the sandbox boundary contributes a difference, not a charge. + pub(crate) non_compute_gas: i128, + /// Post-tx `CALL_STIPEND` total minted into child frames by value-transferring calls. + pub(crate) minted_call_stipend: u64, + /// Post-tx sum of the per-site destroyed bookings — the second opinion the derived + /// [`destroyed`](Self::destroyed) is cross-checked against, never the reported number. + pub(crate) booked_destroyed: u64, /// The state the transaction produced. pub(crate) state: EvmState, } @@ -109,8 +117,17 @@ pub(crate) fn transact_with_gas_limit( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let detained_compute_gas_limit = - evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); + ( + additional_limit.detained_compute_gas_limit(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) + }; let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { result: outcome.result_and_state.result, @@ -121,6 +138,9 @@ pub(crate) fn transact_with_gas_limit( gas_used, destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, state: outcome.result_and_state.state, } } @@ -161,8 +181,17 @@ pub(crate) fn transact_tx( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let detained_compute_gas_limit = - evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); + ( + additional_limit.detained_compute_gas_limit(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) + }; let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { result: outcome.result_and_state.result, @@ -173,6 +202,9 @@ pub(crate) fn transact_tx( gas_used, destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, state: outcome.result_and_state.state, } } @@ -303,8 +335,17 @@ pub(crate) fn transact_with_bucket_capacity( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let detained_compute_gas_limit = - evm.ctx_ref().additional_limit.borrow().detained_compute_gas_limit(); + let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); + ( + additional_limit.detained_compute_gas_limit(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) + }; let gas_used = outcome.result_and_state.result.tx_gas_used(); Outcome { result: outcome.result_and_state.result, @@ -315,6 +356,9 @@ pub(crate) fn transact_with_bucket_capacity( gas_used, destroyed: outcome.compute_gas_destroyed, detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, state: outcome.result_and_state.state, } } diff --git a/crates/mega-evm/tests/rex7/conservation_terms.rs b/crates/mega-evm/tests/rex7/conservation_terms.rs new file mode 100644 index 00000000..9ce14918 --- /dev/null +++ b/crates/mega-evm/tests/rex7/conservation_terms.rs @@ -0,0 +1,343 @@ +//! REX7: the terms of the destroyed-remainder conservation law, in combination. +//! +//! A REX7 transaction's destroyed compute gas is not summed from the sites that destroyed it. It +//! is derived once, when the envelope is final, from what the transaction spent: +//! +//! ```text +//! destroyed = spent + minted call stipends − non-compute gas − enforced compute gas +//! ``` +//! +//! The rest of the suite exercises the derivation one term at a time — a halted frame, a failing +//! precompile, a sandbox merge. This file exercises the terms **together**, because a term that is +//! individually right can still be wrong in company: a minted stipend that leaked into the +//! destroyed lane, or a second stipend that overwrote the first, would be invisible to any fixture +//! that produces only one of them. +//! +//! Two terms of the law are deliberately not covered here: +//! +//! - **The EIP-8037 reservoir.** The derivation reads `total_gas_spent`, which nets the state-gas +//! reservoir out of the envelope. Every `MegaEVM` transaction pins the reservoir at zero — the +//! flag is forced off at configuration time and re-forced inside the transaction, pinned by +//! `evm::factory`'s `test_embedder_cannot_enable_amsterdam_eip8037` — so a non-zero reservoir is +//! not a state this spec can reach, and it is not constructed here. If that pin is ever lifted, +//! the derivation needs revisiting before the reservoir can carry gas. +//! - **A negative derivation.** Reaching it end-to-end would require breaking the conservation law +//! first, so the guard is driven directly at the seam, in `limit::limit`'s +//! `test_negative_derivation_is_clamped_to_zero`. + +use crate::common::{ + default_envs, transact_default, transact_tx, Outcome, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, + ONE_ETH, +}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, MegaSpecId, KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{ADD, CALL, MSTORE8, POP, STOP}, + context::tx::TxEnvBuilder, +}; +use std::vec::Vec; + +/// Empty accounts the value-transferring calls pay into. Each one is fresh, so every call really +/// transfers value and really mints a stipend. +const VALUE_TARGETS: [Address; 3] = [ + EMPTY_TARGET, + address!("0000000000000000000000000000000000300010"), + address!("0000000000000000000000000000000000300011"), +]; + +/// Gas operand the envelope-destroying call forwards. Far below 63/64 of what the caller holds, so +/// the child's budget is exactly this number and the destroyed remainder is exactly computable. +const DESTROYING_CHILD_GAS: u64 = 1_000_000; + +/// `VERYLOW`, the gas `ADD` charges before it discovers the stack is empty — the only work the +/// destroying child performs. +const ADD_GAS: u64 = 3; + +/// What the destroying child leaves behind: its whole budget less the one opcode it paid for. +const EXPECTED_DESTROYED: u64 = DESTROYING_CHILD_GAS - ADD_GAS; + +/// `CALL_STIPEND`: what revm mints into a value-transferring call's child frame without debiting +/// the caller. +const CALL_STIPEND: u64 = 2_300; + +/// Relayer that sends the keyless-deploy transactions. +const KEYLESS_RELAYER: Address = address!("0000000000000000000000000000000000340004"); + +fn call_code(builder: BytecodeBuilder, target: Address, value: u64, gas: u64) -> BytecodeBuilder { + builder + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(value) + .push_address(target) + .push_number(gas) + .append(CALL) + .append(POP) +} + +/// A caller that makes `value_transfers` one-wei calls into fresh empty accounts and then, +/// optionally, one call into a callee that destroys its whole forwarded envelope. +/// +/// The caller pops every success flag, so it survives the failing child and returns normally — +/// which keeps the destroyed remainder attributable to the child alone. +fn run_fixture(value_transfers: usize, destroy: bool) -> Outcome { + let mut builder = BytecodeBuilder::default(); + for target in VALUE_TARGETS.iter().take(value_transfers) { + builder = call_code(builder, *target, 1, 100_000); + } + if destroy { + builder = call_code(builder, CALLEE, 0, DESTROYING_CHILD_GAS); + } + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, builder.append(STOP).build()) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + // A bare `ADD`: the child charges `VERYLOW`, finds the stack empty, and halts with its + // budget intact. A stack underflow is not a gas shortage, so the interpreter keeps its + // counter and the whole remainder is destroyed rather than already zero. + .account_code(CALLEE, Bytes::from_static(&[ADD])); + transact_default(MegaSpecId::REX7, db) +} + +/// A transaction that both mints a stipend and destroys an envelope must report exactly the +/// envelope it destroyed. +/// +/// The two terms pull the derivation in opposite directions — the mint raises recorded work above +/// what the envelope funded, the halt leaves envelope unspent — and they meet for the first time +/// here. If the mint leaked into the destroyed lane the reported remainder would be one stipend +/// too high; if it were dropped, one stipend too low. +#[test] +fn test_minted_stipend_and_destroyed_envelope_in_one_transaction() { + let alone = run_fixture(0, true); + let together = run_fixture(1, true); + + assert!(alone.is_success(), "the caller must survive its failing child: {:?}", alone.result); + assert!( + together.is_success(), + "the caller must survive its failing child: {:?}", + together.result, + ); + + // Both terms are live, and each is live only where it should be. + assert_eq!(alone.minted_call_stipend, 0, "no value transfer, no mint"); + assert_eq!(together.minted_call_stipend, CALL_STIPEND, "one value transfer mints one stipend"); + assert!(together.gas_used > alone.gas_used, "the value transfer must really have happened"); + + assert_eq!( + together.destroyed, EXPECTED_DESTROYED, + "the destroyed remainder is the child's forwarded budget less the one opcode it paid for", + ); + assert_eq!( + together.destroyed, alone.destroyed, + "a minted stipend must not move the destroyed remainder in either direction", + ); + assert_eq!( + together.destroyed, together.booked_destroyed, + "the derived remainder and the per-site bookings must agree", + ); +} + +/// A minted stipend on its own destroys nothing. +/// +/// This is the other half of the composition: the mint makes recorded compute exceed the envelope, +/// and a derivation that took that overshoot for unspent budget would report it as destroyed. +#[test] +fn test_minted_stipend_alone_destroys_nothing() { + for transfers in 1..=VALUE_TARGETS.len() { + let outcome = run_fixture(transfers, false); + assert!(outcome.is_success(), "{transfers} transfers: {:?}", outcome.result); + assert_eq!( + outcome.destroyed, 0, + "{transfers} transfers: a transaction that never halts destroys nothing", + ); + } +} + +/// Several value transfers in one transaction each mint their own stipend, and the derivation has +/// to account for all of them. +/// +/// A term that latched the first mint instead of accumulating would leave the derived remainder +/// short by one stipend per extra call — which the invariance below is exactly sensitive to. +#[test] +fn test_several_minted_stipends_in_one_transaction() { + for transfers in 1..=VALUE_TARGETS.len() { + let outcome = run_fixture(transfers, true); + + assert!(outcome.is_success(), "{transfers} transfers: {:?}", outcome.result); + assert_eq!( + outcome.minted_call_stipend, + CALL_STIPEND * transfers as u64, + "{transfers} transfers: every value-transferring call mints its own stipend", + ); + assert_eq!( + outcome.destroyed, EXPECTED_DESTROYED, + "{transfers} transfers: the destroyed remainder must not drift with the mint count", + ); + assert_eq!( + outcome.destroyed, outcome.booked_destroyed, + "{transfers} transfers: the derived remainder and the per-site bookings must agree", + ); + } +} + +/// Builds a deterministic pre-EIP-155 keyless deployment transaction. +fn keyless_tx_bytes(init_code: Bytes, gas_limit: u64) -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// Runs one `KeylessDeploy` whose sandbox executes `init_code` with `gas_limit`. +/// +/// [`CALLEE`] is preloaded with a bare `ADD` so a constructor can open a sub-frame that halts +/// exceptionally without failing the constructor itself. +fn keyless_deploy(init_code: Bytes, gas_limit: u64) -> Outcome { + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(init_code, gas_limit), + gasLimitOverride: U256::from(gas_limit), + } + .abi_encode(); + let tx = TxEnvBuilder::default() + .caller(KEYLESS_RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(30_000_000) + .chain_id(Some(1)) + .data(Bytes::from(call_data)) + .build_fill(); + let db = MemoryDatabase::default() + .account_balance(KEYLESS_RELAYER, U256::from(10 * ONE_ETH)) + .account_code(CALLEE, Bytes::from_static(&[ADD])); + transact_tx( + MegaSpecId::REX7, + db, + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + tx, + &default_envs(), + ) +} + +/// Set/clear `SSTORE` pairs a constructor runs to earn the EIP-3529 refund. +/// +/// Twenty pairs is what it takes to outgrow the sandbox's own storage gas by a clear margin: each +/// pair returns 19,900, and the sandbox pays a flat transaction storage-gas intrinsic plus the +/// per-token storage gas on the initcode it carries. +const REFUND_PAIRS: u64 = 20; + +/// Memory offset the refunding constructors touch. The expansion cost is quadratic, so one +/// `MSTORE8` this far out buys millions of gas from a handful of bytecode bytes — enough for +/// EIP-3529's one-fifth-of-gas-spent cap to stop binding on the refunds above. +const REFUND_MEM_OFFSET: u64 = 1_500_000; + +/// Sandbox gas limit the refunding deployments run with: above what the memory expansion costs, so +/// the constructor reaches its own end rather than running out. +const REFUND_SANDBOX_GAS: u64 = 8_000_000; + +/// The refund-earning prologue: `pairs` set/clear `SSTORE` pairs, then the memory expansion that +/// lifts the refund cap. +fn refunding_prologue(pairs: u64) -> BytecodeBuilder { + let mut builder = BytecodeBuilder::default(); + for slot in 1..=pairs { + builder = builder.sstore(U256::from(slot), U256::from(1)); + builder = builder.sstore(U256::from(slot), U256::ZERO); + } + builder.push_number(1u64).push_number(REFUND_MEM_OFFSET).append(MSTORE8) +} + +/// A constructor that earns `pairs` refunds and then returns, deploying empty code — so no +/// per-byte code-deposit storage gas offsets the refund. +fn refunding_initcode(pairs: u64) -> Bytes { + refunding_prologue(pairs).return_empty().build() +} + +/// The non-compute lane is signed, and this is the shape that needs it. +/// +/// The lane is the transaction's `MegaETH` storage gas, except at the `KeylessDeploy` sandbox +/// boundary, where the parent books a **difference**: what the sandbox cost its gas counter, less +/// what the sandbox recorded as compute work. A sandbox whose own EIP-3529 refund outgrows its own +/// storage gas hands that difference over negative, and a large enough refund drives the whole +/// lane below zero. A lane that saturated at zero would then over-report the destroyed remainder +/// by the entire overshoot. +/// +/// The control run is the same deployment without the refunds, which leaves the lane positive. +#[test] +fn test_sandbox_refund_drives_the_non_compute_lane_negative() { + let control = keyless_deploy(refunding_initcode(0), REFUND_SANDBOX_GAS); + let refunding = keyless_deploy(refunding_initcode(REFUND_PAIRS), REFUND_SANDBOX_GAS); + + assert!(control.is_success(), "the control deployment must run: {:?}", control.result); + assert!(refunding.is_success(), "the refunding deployment must run: {:?}", refunding.result); + assert!( + control.non_compute_gas > 0, + "the control must leave the lane positive, or it proves nothing; got {}", + control.non_compute_gas, + ); + + assert!( + refunding.non_compute_gas < 0, + "the sandbox's refund must drive the lane negative; got {}", + refunding.non_compute_gas, + ); + // The signature of a negative lane: the transaction records more compute work than its + // envelope ever paid for. + assert!( + refunding.compute_gas > refunding.gas_used, + "recorded compute {} must exceed the envelope {}", + refunding.compute_gas, + refunding.gas_used, + ); + assert_eq!( + refunding.destroyed, refunding.booked_destroyed, + "the derivation must stay exact across the sign change", + ); +} + +/// The negative lane must compose with a destroyed envelope too. +/// +/// The constructor earns its refunds, opens a sub-frame that halts exceptionally, absorbs the +/// failure and returns — so one transaction carries a negative non-compute lane *and* a destroyed +/// remainder that the lane's sign is part of deriving. The constructor has to survive: EIP-3529 +/// refunds only reach the receipt of a transaction that succeeds, so a constructor that halted +/// would take the refund — and the negative lane — down with it. +#[test] +fn test_negative_non_compute_lane_composes_with_a_destroyed_envelope() { + let init_code = call_code(refunding_prologue(REFUND_PAIRS), CALLEE, 0, DESTROYING_CHILD_GAS) + .return_empty() + .build(); + + let outcome = keyless_deploy(init_code, REFUND_SANDBOX_GAS); + + assert!(outcome.is_success(), "the deployment must succeed: {:?}", outcome.result); + assert_eq!( + outcome.destroyed, EXPECTED_DESTROYED, + "the halted sub-frame's whole forwarded budget, less the one opcode it paid for, must \ + cross the sandbox boundary as destroyed", + ); + assert!( + outcome.non_compute_gas < 0, + "the refunds must still drive the lane negative; got {}", + outcome.non_compute_gas, + ); + assert_eq!( + outcome.destroyed, outcome.booked_destroyed, + "the derivation must stay exact with both terms live", + ); +} diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index 929ba23a..f85fa6df 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -98,11 +98,22 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let executed = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, remaining_compute_gas) = { + let ( + detained_compute_gas_limit, + remaining_compute_gas, + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); ( additional_limit.detained_compute_gas_limit(), additional_limit.current_call_remaining_compute_gas(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, ) }; let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); @@ -116,6 +127,9 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { gas_used, destroyed: executed.compute_gas_destroyed, detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, state: executed.result_and_state.state, }; GuardPassRun { outcome, accessed, remaining_compute_gas } diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 57f010a9..22b76bf5 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -54,6 +54,7 @@ mod checkpoint_settlement; mod checkpoint_static_fee_edges; mod clamp_classification; mod common; +mod conservation_terms; mod detention_window; mod double_exceed_corner; mod exceptional_halt; From c5ebe32297c45b2060c7f749a1f27144cd0e2299 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sat, 15 Aug 2026 23:39:18 +0800 Subject: [PATCH 066/208] docs(rex7): define destroyed compute gas by the conservation law MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec now defines a transaction's destroyed compute gas as the remainder of what it spent — envelope plus minted call stipends, less MegaETH storage gas and enforced compute — and demotes the site list to what fixes the executed side at each site. The completeness claim becomes a corollary of the law rather than an assertion about the enumeration. AGENTS.md gains the obligation to re-run the conservation scan after a revm / alloy-evm upgrade. --- AGENTS.md | 5 +++++ docs/spec/evm/compute-gas.md | 42 ++++++++++++++++++++++++++++++------ docs/spec/upgrades/rex7.md | 21 ++++++++++++++++-- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c162ebab..1f6b107a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,6 +120,8 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). + The destroyed part a transaction *reports* is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. + `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). @@ -311,6 +313,9 @@ When the agent is requested to implement a new feature or bug fix, it should con New or modified benchmarks must be executed locally (`cargo bench -p mega-evm --bench `) to verify they pass before committing. Benchmarks may compile but panic at runtime due to missing setup (e.g., required block fields), so compilation alone is not sufficient. For instruction-count deltas across a PR, use the CodSpeed report posted on the PR rather than local wall-clock numbers. +- **Re-run the destroyed-gas conservation scan after a revm / alloy-evm upgrade.** + The REX7 destroyed total is derived from the envelope, so any upstream change that moves gas without a MegaETH site recording it — a new minted subsidy like `CALL_STIPEND`, a changed refund or floor ordering, a new component of `total_gas_spent` — becomes a missing term in the law rather than a compile error. + After bumping revm or alloy-evm, run `cargo test -p mega-evm` and `cargo test -p mega-state-test -p state-test` (the `debug_assert` cross-check is live in debug builds) plus the replay fixtures under the latest spec (`cargo run -p state-test -- --bench --bench-spec bench/replay/fixtures`), whose own `post` expectations pin an older spec and would otherwise give the derivation no coverage. - **Use `test_` prefix for Rust test function names.** New `#[test]` functions should be named with a `test_` prefix for consistency with this repository and upstream revm style. If editing nearby tests in the same module, align names to the same `test_` style when reasonable. diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index dd574ba1..cfaf580e 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -513,9 +513,31 @@ A node MUST settle that whole budget as compute gas, split into two parts that a - **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. -- **Destroyed** — whatever the frame still held when its result became final, including any gas the clamp was hiding. +- **Destroyed** — the budget the frame never spent and never handed back. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). +The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. +The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. + +#### Destroyed compute gas + +The destroyed part of a transaction is defined by a conservation law over the gas the transaction spent, not by an enumeration of the places that can destroy an envelope. + +Every unit of EVM gas a transaction spends is exactly one of three things: compute work its frames performed, MegaETH storage gas, or budget that was lost without anything being executed for it. +Two of those three are recorded as they happen, so a node MUST derive the third: + +`destroyed = spent + minted_stipends − storage_gas − executed_compute` + +- `spent` — the EVM gas the transaction's envelope burnt, read once, at the moment the envelope is final: after the transaction's gas accounting has settled and any resource-limit gas rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied. Those two move the number the receipt reports without anything having been burnt, so a node MUST NOT read `spent` after them. Gas rescued for the sender, and gas the clamp was hiding, are both out of the envelope by this point and MUST NOT be added back. +- `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations whose child frame ran. The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whether the child spends it or returns it. A node MUST add it back; without it the two sides of the law disagree by exactly that amount. +- `storage_gas` — the MegaETH storage gas the transaction was charged: the storage-gas share of intrinsic gas, the in-frame storage-gas surcharges, the code-deposit charge, and the charges a system contract invocation takes outside an EVM frame. At a nested-execution boundary this term takes the **difference** between what the nested execution cost the outer gas counter and what it recorded as compute, which can be negative when the nested execution's own EIP-3529 refund outgrew its storage gas; a node MUST NOT clamp that contribution at zero. +- `executed_compute` — the transaction's recorded compute total less its destroyed part: the work every resource limit is evaluated against. + +The result is the number a node MUST report as the transaction's destroyed compute gas, and the number block-level compute accounting MUST subtract from the reported total to obtain the block's enforced compute counter. +A node MUST NOT report a negative result: the law cannot produce one on this spec, and a node that computes one MUST report zero rather than a wrapped value. + +The rules that follow fix `executed_compute` at each site that can leave budget unspent, which is what makes the law's remainder well defined; they are not themselves the definition of the destroyed total. + A precompile invocation that fails is the same split, taken at the precompile recording site. A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. @@ -536,13 +558,10 @@ A [system contract](../system-contracts/overview.md) invocation a node answers w It applies only when the answer is a halt that keeps the call's gas: the part the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. An answer that returns or reverts hands the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund; a node MUST NOT record either as destroyed, because that gas was not lost. -The one remaining way to burn a whole envelope without executing anything — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — is not part of this carve-out. -Since [Rex5](../upgrades/rex5.md) a node rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited, so it is never included and there is no burnt envelope to split. +A transaction a node rejects during validation has no envelope to split. +Since [Rex5](../upgrades/rex5.md) a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied is rejected during validation — after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited — so it produces no receipt. A node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. -The destroyed part is bounded by the sender's gas envelope rather than by the compute limit, and halting on it would rescue gas the EVM already destroyed and change the receipt this carve-out requires to stay identical. -The executed part carries no such problem: it is work, and leaving it out of enforcement would let a frame that keeps executing after absorbing a failed child spend the same compute headroom a second time. - The split MUST be driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. That zeroing has one consequence a node MUST accept: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero when the frame exits, so the whole segment measures as executed and is enforced in full. A node MUST NOT try to recover the split in that case. @@ -557,6 +576,7 @@ A clamp-induced out-of-gas is not an exceptional halt for this rule — the cros A frame whose exit latches a resource-limit exceed destroys nothing either: it reverts to its parent (frame-local) or halts the transaction with its gas rescued (transaction-level). When a nested execution merges its usage into an outer one — the [KeylessDeploy](../system-contracts/keyless-deploy.md) sandbox is the only such boundary — a node MUST carry the split across it, reporting the inner total in full while enforcing only the executed part. +The outer transaction's destroyed total is still derived once, from its own envelope, after the merge.
@@ -619,6 +639,16 @@ Permitting both lets an implementation choose whichever is cheaper at a given si For a value-transferring `CALL` or `CALLCODE`, the inherited EVM adds `CALL_STIPEND` to the child's gas limit without deducting it from the parent's remaining gas. Treating the child's full gas limit as forwarded would therefore subtract gas the parent never contributed, under-counting the parent's compute gas by the stipend. +
+Rex7 (unstable): why destroyed compute gas is defined by a conservation law + +A definition that enumerates the sites which can destroy an envelope is only as complete as the enumeration, and its completeness is not checkable — a site added later, or one an implementation reaches by a path the list did not anticipate, silently under-reports with nothing to notice it. +The conservation law has no such failure mode: it is stated over quantities a node already tracks for other reasons, so any envelope lost anywhere shows up in the remainder whether or not the loss was foreseen. +It also gives the site rules something to be checked against, since the two are computed independently and must agree. +The cost is one correction term — the inherited EVM's minted `CALL_STIPEND`, which makes recorded work exceed the envelope — and one ordering obligation on where the envelope is read. + +
+ **Why is the first `CALL`-family touch of a preload-warm address charged cold?** MegaETH's storage-gas pricing inspects the callee account before the opcode's own access, and that inspection materializes the account without inheriting its preloaded warmth, so the opcode's subsequent access observes a cold account. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index d8ea0d16..8e29df0b 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -32,6 +32,7 @@ Rex7 also makes two guard- and detention-related choices that Rex6 does not: Two deliberate accounting carve-outs remain. A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. +The reported destroyed total is derived from a conservation law over what the transaction spent rather than summed from the sites that destroyed it, so an envelope lost anywhere lands in it whether or not a site was written to book it. A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own 192-byte length check included) enforces, and the unused caller-supplied envelope is destroyed. The generic error arm therefore stops enforcing the whole forwarded amount, which is an intentional enforcement difference from Rex6; the Rex5 forwarded-gas cap still prevents the precompile from performing more work than the remaining compute budget. @@ -88,10 +89,23 @@ The **executed** part is the open plain-opcode segment, measured as the interpre A node MUST record it through the ordinary path, so it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against — exactly as the same opcodes would if the frame had returned normally. A parent frame keeps executing after it absorbs a failed child; excluding the child's work from enforcement would let the code that follows spend the same compute headroom a second time. -The **destroyed** part is whatever the frame still held when its result became final, including any gas the clamp was hiding from the interpreter. +The **destroyed** part is the budget the frame never spent and never handed back. A node MUST record it in the transaction's reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it — at transaction level or at block level, where a destroyed remainder that counted toward admission would close the block's compute capacity for the transactions behind it (see [Resource Limits](../evm/resource-limits.md)). It is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the reported total past that limit; halting on it would rescue gas the EVM has already destroyed and change a receipt this carve-out requires to stay identical. +**The destroyed total is derived, not summed.** +Rex7 does not define a transaction's destroyed compute gas as the sum of what its halted frames, precompiles and system-contract invocations booked. It defines it as a conservation law over the gas the transaction spent: + +`destroyed = spent + minted_stipends − storage_gas − executed_compute` + +`spent` is the EVM gas the envelope burnt, read once at the moment the envelope is final — after the transaction's gas accounting has settled and any resource-limit rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied, since those move the receipt's number without anything having been burnt. +`minted_stipends` is the sum of `CALL_STIPEND` over the value-transferring `CALL` and `CALLCODE` invocations whose child frame ran: the inherited EVM grants that stipend to the child's frame budget without debiting the caller, so the frames record one stipend more work than the envelope funded per such call, and a node MUST add it back. +`storage_gas` is the MegaETH storage gas the transaction was charged, taken as a signed difference at a nested-execution boundary — negative when the nested execution's own EIP-3529 refund outgrew its storage gas — which a node MUST NOT clamp at zero. +`executed_compute` is the recorded compute total less the destroyed part: the work every resource limit is evaluated against. +A node MUST NOT report a negative result; the law cannot produce one on this spec, and a node that computes one MUST report zero rather than a wrapped value. + +Enforcement is unaffected: the work a limit is evaluated against still comes from the checkpoint and per-opcode recordings, and only the reported destroyed total — and the block's enforced counter, which subtracts it — comes from the law. + The split MUST be driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. That zeroing has one consequence a node MUST accept rather than work around: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero at frame exit, so the whole segment measures as executed and is enforced in full. This is the one shape where Rex7 enforcement is stricter than Rex6's, which attributes the failing opcode to neither part. @@ -115,7 +129,10 @@ A system contract invocation a node answers without opening an EVM frame — the An answer that returns or reverts gives the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund. A node MUST NOT record either as destroyed; that gas was not lost, and counting it would report it twice. -Those sites are the complete set of ways a Rex7 transaction can lose an envelope without executing it. +Those sites are where a Rex7 transaction is known to lose an envelope without executing it, and they are what fixes `executed_compute` at each one — but they are not what makes the enumeration complete. +Completeness is a consequence of the law: a lost envelope is gas the transaction spent that neither the compute lanes nor the storage-gas lane accounts for, so it lands in the remainder whether or not a site above anticipated it. +Reading the two independently and requiring them to agree is what turns the list from an assumption into a checkable claim. + One further shape burns a whole envelope having executed nothing — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — but [Rex5](rex5.md) already rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited. It therefore produces no receipt on Rex7 and there is no envelope to split; a node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. From b68a838f119fb90829a961db02ec81a41ae7fbc1 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Sun, 16 Aug 2026 01:12:11 +0800 Subject: [PATCH 067/208] fix(rex7): keep block admission on the per-site compute lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The block's enforced compute counter was reconstructed as the reported total less the transaction's derived destroyed remainder, which put a reporting derivation on an enforcement face: in release builds a missing term in the law would have repacked blocks rather than misreported a statistic. MegaTransactionOutcome now carries compute_gas_enforced, read from the lane the transaction enforced its own compute limit on, and the block accumulates that. The call-stipend term's stated condition is corrected the other way: the spec text and the comments said the stipend is counted once its child frame runs, while the implementation books it per mint at the CALL-family settlement. A value call turned away at frame entry runs no child, yet its refund returns the mint into the caller's envelope, so the law needs it — the implementation was right and is now pinned, the wording was wrong by 2,300 gas per such call. Also aligns the settlement-point rationale with the before_execution short-circuit, which produces a receipt without reaching settlement, and rewrites rex7.md's circular executed_compute phrasing. --- AGENTS.md | 5 +- crates/mega-evm/src/block/executor.rs | 15 ++-- crates/mega-evm/src/block/limit.rs | 50 +++++++----- crates/mega-evm/src/block/result.rs | 3 +- crates/mega-evm/src/evm/execution.rs | 6 ++ crates/mega-evm/src/evm/instructions.rs | 17 ++-- crates/mega-evm/src/evm/mod.rs | 2 + crates/mega-evm/src/evm/result.rs | 31 ++++++-- crates/mega-evm/src/limit/checkpoint.rs | 13 +-- crates/mega-evm/src/limit/limit.rs | 12 ++- crates/mega-evm/src/sandbox/execution.rs | 4 + .../tests/block_executor/compute_gas_lanes.rs | 79 ++++++++++++++++++- .../mega-evm/tests/rex7/conservation_terms.rs | 73 +++++++++++++++++ docs/spec/evm/compute-gas.md | 10 ++- docs/spec/evm/resource-limits.md | 2 +- docs/spec/upgrades/rex7.md | 9 ++- 16 files changed, 274 insertions(+), 57 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1f6b107a..b5e11d03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,9 +119,10 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. - The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). + The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). The destroyed part a transaction *reports* is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. - `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call. + The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. + `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call; it is booked per mint event — at the CALL-family settlement, before frame init — so a value call turned away at frame entry (insufficient balance, call depth) books one too, because its refund returns the mint into the caller's envelope. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 7e32f103..423d5666 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -598,7 +598,12 @@ where data_size, kv_updates, compute_gas_used, - compute_gas_destroyed, + // The transaction's derived destroyed total is a reported number; the block + // reports it through `compute_gas_used`, which already carries it, and reaches + // its own enforced counter through `compute_gas_enforced` instead of + // subtracting this one back out. + compute_gas_destroyed: _, + compute_gas_enforced, state_growth_used, }, } = result; @@ -619,9 +624,9 @@ where // next transaction. The deposit-nonce record doubles as the deposit signal here. // // Compute gas crosses this boundary as the pair execution produced it — the full reported - // total and the destroyed part of it — so the limiter can report one and enforce the - // other. Collapsing them here would hand the block a single number that is right for - // reporting and wrong for admission. + // total and the part of it the transaction enforced its own limits against — so the + // limiter can report one and enforce the other. Collapsing them here would hand the block + // a single number that is right for reporting and wrong for admission. self.block_limiter.post_execution_update_raw( result.tx_gas_used(), tx_size, @@ -629,7 +634,7 @@ where data_size, kv_updates, compute_gas_used, - compute_gas_destroyed, + compute_gas_enforced, state_growth_used, depositor.is_some(), ); diff --git a/crates/mega-evm/src/block/limit.rs b/crates/mega-evm/src/block/limit.rs index 6cb69b66..95ad9ea2 100644 --- a/crates/mega-evm/src/block/limit.rs +++ b/crates/mega-evm/src/block/limit.rs @@ -657,7 +657,7 @@ impl BlockLimits { /// /// // Post-execution update (the executor commit path drives this internally) /// limiter.post_execution_update_raw( -/// gas, tx_size, da_size, data, kv, compute, destroyed_compute, growth, is_deposit, +/// gas, tx_size, da_size, data, kv, compute, enforced_compute, growth, is_deposit, /// ); /// } /// ``` @@ -696,13 +696,17 @@ pub struct BlockLimiter { pub block_compute_gas_used: u64, /// The part of [`block_compute_gas_used`](Self::block_compute_gas_used) the block enforces: - /// the same total with each transaction's destroyed remainder subtracted. + /// the sum of what each transaction performed and evaluated its own compute limit against. /// /// Destroyed gas is not work the network performed, and no resource limit is evaluated against /// it at any level. A transaction whose reported total dwarfs its executed work would /// otherwise close the block's compute capacity for everyone behind it while having computed /// almost nothing. Before Rex7 nothing is ever destroyed, so this counter and the reported one /// advance in lockstep. + /// + /// Each transaction contributes the number its own enforcement ran on, not that transaction's + /// reported total less its reported destroyed remainder — the two are equal, but only one of + /// them is a measurement of work performed. pub block_compute_gas_enforced: u64, /// Cumulative state growth consumed by all transactions in the block. @@ -926,11 +930,17 @@ impl BlockLimiter { /// accounting. /// /// Compute gas arrives as two numbers, not one: `compute_gas_used` is the transaction's full - /// reported total and `compute_gas_destroyed` is the part of it that Rex7+ destroyed rather - /// than performed (0 before Rex7). The reported total lands in the public statistic and the - /// difference in the counter the block compute-gas limit is evaluated against. Both come from - /// the transaction's own report, so the block inherits whatever the transaction settled — it - /// does not re-derive the split, and there is no second definition of it here. + /// reported total, and `compute_gas_enforced` is the part of it the transaction performed and + /// evaluated its own compute limit against (equal to the total before Rex7, which destroys + /// nothing). The reported total lands in the public statistic and the enforced part in the + /// counter the block compute-gas limit is evaluated against. + /// + /// The enforced number is taken, not computed here from the transaction's reported destroyed + /// total. Block admission is enforcement, and it therefore reads the same per-opcode and + /// checkpoint recordings the transaction enforced on, rather than a quantity derived for + /// reporting: subtracting the reported destroyed total would put a reporting derivation on the + /// admission path, where an error in it would repack blocks instead of misreporting a + /// statistic. #[allow(clippy::too_many_arguments)] pub fn post_execution_update_raw( &mut self, @@ -940,7 +950,7 @@ impl BlockLimiter { tx_data: u64, kv_updates: u64, compute_gas_used: u64, - compute_gas_destroyed: u64, + compute_gas_enforced: u64, state_growth_used: u64, is_deposit: bool, ) { @@ -968,9 +978,8 @@ impl BlockLimiter { // Block compute gas limit, no need to check here since we allow the last transaction to // exceed the limit. Only the executed part advances the enforced counter. self.block_compute_gas_used = self.block_compute_gas_used.saturating_add(compute_gas_used); - self.block_compute_gas_enforced = self - .block_compute_gas_enforced - .saturating_add(compute_gas_used.saturating_sub(compute_gas_destroyed)); + self.block_compute_gas_enforced = + self.block_compute_gas_enforced.saturating_add(compute_gas_enforced); // Block state growth limit, no need to check here since we allow the last transaction to // exceed the limit. @@ -1062,7 +1071,7 @@ mod tests { u64::MAX, u64::MAX, u64::MAX, - 0, + u64::MAX, u64::MAX, false, ); @@ -1097,25 +1106,26 @@ mod tests { fn test_post_execution_update_raw_splits_the_compute_gas_lanes() { let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); - limiter.post_execution_update_raw(0, 0, 0, 0, 0, 1_000_000, 900_000, 0, false); + // A transaction reporting 1,000,000 having performed 100,000 of it. + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 1_000_000, 100_000, 0, false); assert_eq!(limiter.block_compute_gas_used, 1_000_000, "the report takes the whole total"); assert_eq!(limiter.block_compute_gas_enforced, 100_000, "enforcement takes only the work"); // A second transaction that destroyed nothing advances both counters by the same amount. - limiter.post_execution_update_raw(0, 0, 0, 0, 0, 50_000, 0, 0, false); + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 50_000, 50_000, 0, false); assert_eq!(limiter.block_compute_gas_used, 1_050_000); assert_eq!(limiter.block_compute_gas_enforced, 150_000); } - /// Nothing is ever destroyed before Rex7, so a block of transactions that report a zero - /// destroyed part leaves the two counters equal at every step — the pre-Rex7 behaviour, which - /// the split must reproduce byte for byte. + /// Nothing is ever destroyed before Rex7, so a block of transactions whose enforced part is + /// their whole total leaves the two counters equal at every step — the pre-Rex7 behaviour, + /// which the split must reproduce byte for byte. #[test] fn test_compute_gas_lanes_coincide_without_a_destroyed_part() { let mut limiter = BlockLimiter::new(BlockLimits::no_limits()); for compute in [21_000, 500, 1_234_567, 0] { - limiter.post_execution_update_raw(0, 0, 0, 0, 0, compute, 0, 0, false); + limiter.post_execution_update_raw(0, 0, 0, 0, 0, compute, compute, 0, false); assert_eq!( limiter.block_compute_gas_used, limiter.block_compute_gas_enforced, "with nothing destroyed the reported and enforced counters must not diverge" @@ -1133,7 +1143,7 @@ mod tests { let mut limiter = BlockLimiter::new(limits); // One transaction reporting far past the block limit, having performed almost none of it. - limiter.post_execution_update_raw(0, 0, 0, 0, 0, 5_000_000, 4_950_000, 0, false); + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 5_000_000, 50_000, 0, false); assert!( limiter.block_compute_gas_used > limits.block_compute_gas_limit, "the reported total must carry the destroyed remainder past the limit" @@ -1148,7 +1158,7 @@ mod tests { ); // Executed work fills it, and the error names the enforced counter. - limiter.post_execution_update_raw(0, 0, 0, 0, 0, 950_000, 0, 0, false); + limiter.post_execution_update_raw(0, 0, 0, 0, 0, 950_000, 950_000, 0, false); assert!(limiter.is_block_limit_reached(), "executed work does fill the block"); let error = limiter .pre_execution_check(B256::ZERO, 0, 0, 0, false) diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 3aa3c6f7..5e0113cb 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -285,6 +285,7 @@ mod tests { kv_updates: 2, compute_gas_used: 3, compute_gas_destroyed: 1, + compute_gas_enforced: 2, state_growth_used: 4, }; @@ -306,7 +307,7 @@ mod tests { // One hop for the resource dimensions (`Copy` scalars may leave through a deref). let kv: u64 = outcome.kv_updates; assert_eq!((kv, outcome.compute_gas_used, outcome.state_growth_used), (2, 3, 4)); - assert_eq!(outcome.compute_gas_destroyed, 1); + assert_eq!((outcome.compute_gas_destroyed, outcome.compute_gas_enforced), (1, 2)); } #[test] diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 3462a07a..df17a8a1 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -183,6 +183,12 @@ where // the sender. So no transaction on a spec that has the destroyed lane reaches this // branch today; the recording is what keeps the lane correct if a later spec grows an // intrinsic component that is only resolved after validation. + // + // That reachability is what the booking here relies on: this path returns its result + // without running the frame loop, so it never reaches the settlement in + // `last_frame_result` that derives the reported destroyed total. A spec that made the + // branch reachable would produce a receipt reporting nothing destroyed while this + // booking says otherwise, and would have to settle the derivation here as well. if ctx.spec.is_enabled(MegaSpecId::REX7) { let mut additional_limit = ctx.additional_limit.borrow_mut(); // Nothing can have been destroyed before the first frame, so the recorded total diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index 4ab62b47..eef74ee4 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1031,8 +1031,10 @@ macro_rules! record_storage_compute_gas { // `forwarded_child_gas` records that deducted amount so the abort path below can return it // to the parent. let mut forwarded_child_gas: u64 = 0; - // The stipend revm mints into the child's budget without debiting the caller, booked only - // once the child is certain to run — see the abort path below. + // The stipend revm mints into the child's budget without debiting the caller, booked once + // this opcode hands the child invocation on — whether or not a child frame then runs. The + // one path that mints nothing is the compute-limit abort below, which discards the pending + // child before the EVM ever sees it. let mut minted_call_stipend: u64 = 0; match $context.interpreter.bytecode.action() { Some(InterpreterAction::NewFrame(FrameInput::Call(call_inputs))) => { @@ -1066,10 +1068,13 @@ macro_rules! record_storage_compute_gas { additional_limit.sync_checkpoint_baseline(gas_after); } if additional_limit.record_compute_gas(gas_used) { - // The child will run, so the stipend revm minted into its budget is now live: the - // callee either spends it as work no envelope funded, or hands it back and shrinks - // the envelope. Book it where the destroyed-remainder derivation can reconcile the - // recorded work against what the transaction spent. + // The invocation survives this opcode, so the stipend revm minted into its budget + // is now live, whatever becomes of the child: the callee spends it as work no + // envelope funded, or hands it back and shrinks the envelope, or never runs at all + // — a frame init that fails on balance or depth refunds the whole child budget, + // mint included, into the caller's envelope, which shrinks it by the same amount. + // Book it where the destroyed-remainder derivation can reconcile the recorded work + // against what the transaction spent. additional_limit.record_minted_call_stipend(minted_call_stipend); None } else { diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index a4fb8aea..2d683602 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -368,6 +368,7 @@ where kv_updates, compute_gas_used: compute_gas, compute_gas_destroyed: additional_limit.destroyed_compute_gas(), + compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, }) } @@ -400,6 +401,7 @@ where kv_updates, compute_gas_used: compute_gas, compute_gas_destroyed: additional_limit.destroyed_compute_gas(), + compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, }) } diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 6331fb88..7a948cb9 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -46,17 +46,36 @@ pub struct MegaTransactionOutcome { /// Derived from what the transaction spent, not summed from the sites that destroyed it: gas /// the transaction burnt is either work the trackers recorded, `MegaETH` storage gas, or a /// budget something threw away without executing anything for it, and this field is the last - /// of the three read off as the remainder. A transaction that produces no receipt has no - /// envelope to split and reports zero. + /// of the three read off as the remainder. /// - /// Destroyed gas is not work the network did, so no resource limit is evaluated against it. - /// The transaction's own limits already excluded it while executing; a consumer that - /// accumulates this outcome into a further limit — today the block compute-gas counter — must - /// subtract it too, and compare `compute_gas_used - compute_gas_destroyed` instead. + /// The derivation runs once, where the transaction's gas envelope is final. A transaction that + /// never reaches that point reports zero: a validation reject, which produces no receipt and + /// has no envelope to split, and the pre-execution intrinsic overrun, which is a validation + /// reject on every spec that has this lane — see `MegaHandler::before_execution`, whose + /// short-circuit books the split for a future spec that could reach it but is not itself a + /// settlement point. + /// + /// This is a reported number and nothing else. Destroyed gas is not work the network did, so + /// no resource limit is evaluated against it at any level — a consumer that accumulates this + /// outcome into a further limit reads + /// [`compute_gas_enforced`](Self::compute_gas_enforced), which comes off the enforcement lane + /// itself rather than out of this subtraction. /// /// Same inspector caveat as [`compute_gas_used`](Self::compute_gas_used): the field is the /// uninspected split, and a rewriting inspector will move it. pub compute_gas_destroyed: u64, + /// The part of [`compute_gas_used`](Self::compute_gas_used) every compute-gas limit is + /// evaluated against: the work the transaction performed, with Rex7+ destroyed remainders left + /// out (equal to `compute_gas_used` before Rex7, which destroys nothing). + /// + /// Read straight off the lane the transaction enforced its own compute limit on, built from + /// the per-opcode and checkpoint recordings — deliberately *not* reconstructed as + /// `compute_gas_used - compute_gas_destroyed`. The two are equal, and a cross-check in debug + /// builds fails loudly if they ever stop being, but they are equal by agreement of two + /// independent measurements rather than by construction. Every further limit this outcome + /// feeds — today the block compute-gas counter — is enforcement, and enforcement stays on the + /// measurement it was always on. + pub compute_gas_enforced: u64, /// The state growth used. pub state_growth_used: u64, } diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index 55ee1646..a520df6a 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -73,9 +73,11 @@ pub(crate) struct CheckpointTracker { /// /// The minted gas leaves the transaction one stipend richer per such call, however it is used. /// Spent by the callee, it is recorded as work no envelope paid for; returned when the child - /// exits, it shrinks the envelope by the same amount. Either way the frames' recorded work - /// exceeds what the transaction spent by exactly one `CALL_STIPEND` per call, whatever the - /// callee did with it. + /// exits, it shrinks the envelope by the same amount. A child that never runs at all — a frame + /// init that fails on balance or call depth — refunds the whole budget, mint included, and so + /// shrinks the envelope exactly as a child that returned it would. Every outcome leaves the + /// frames' recorded work exceeding what the transaction spent by one `CALL_STIPEND` per call, + /// which is why the mint, not the child frame, is what this field counts. /// /// So the recorded compute total is *not* a partition of the gas the transaction spent, and /// the destroyed-remainder derivation has to account for the minted gas before the two sides @@ -86,8 +88,9 @@ pub(crate) struct CheckpointTracker { /// envelope was final — the number the transaction reports. /// /// Zero until the settlement point writes it, so a transaction that never reaches settlement - /// (a validation reject, which produces no receipt) reports nothing destroyed. See - /// [`AdditionalLimit::settle_destroyed_compute_gas`]( + /// reports nothing destroyed: a validation reject, which produces no receipt to report into, + /// and the pre-execution intrinsic overrun, which is itself a validation reject on every spec + /// that has this lane. See [`AdditionalLimit::settle_destroyed_compute_gas`]( /// super::AdditionalLimit::settle_destroyed_compute_gas). settled_destroyed: u64, } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 8035bc78..0675d41f 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -348,11 +348,15 @@ impl AdditionalLimit { self.checkpoint.settled_destroyed() } - /// Books one `CALL_STIPEND` minted into a child frame that the caller never funded (REX7+). + /// Books one `CALL_STIPEND` minted into a child invocation that the caller never funded + /// (REX7+). /// - /// Called from the CALL-family settlement once the child frame is certain to run: on the - /// compute-limit abort path the pending child is discarded and its forwarded gas returned, so - /// the mint never reaches a frame and there is nothing to book. + /// Called from the CALL-family settlement once the opcode has handed the invocation on, which + /// is where the mint is created — not once a child frame runs. A frame init that then fails on + /// balance or call depth refunds the whole child budget, mint included, to the caller, so the + /// envelope shrinks against recorded work by exactly one stipend just as a child that ran and + /// returned it would. The one path that mints nothing is the compute-limit abort, which + /// discards the pending child and returns its forwarded gas before the EVM sees it. #[inline] pub(crate) fn record_minted_call_stipend(&mut self, amount: u64) { self.checkpoint.record_minted_call_stipend(amount); diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index 4ca9950b..f439dff6 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -666,6 +666,10 @@ fn run_sandbox_ctx( // parent's reported destroyed total is settled once, from the conservation law, at // the outer transaction's settlement point — this number is an input to the term // that derivation reads, not a second place destroyed gas gets reported. + // + // The sandbox never settles a derivation of its own: the law is stated over a + // transaction's final envelope, and the sandbox's gas is a charge inside the outer + // transaction's envelope rather than one of its own. burned_compute_gas: additional_limit.burned_compute_gas(), } }; diff --git a/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs b/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs index 9a3895ca..4ca666bc 100644 --- a/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs +++ b/crates/mega-evm/tests/block_executor/compute_gas_lanes.rs @@ -128,6 +128,22 @@ fn run_block( spec: MegaSpecId, block_compute_gas_limit: u64, txs: &[MegaTxEnvelope], +) -> Vec { + run_block_reporting(spec, block_compute_gas_limit, txs, None) +} + +/// [`run_block`], with a test channel that rewrites each transaction's **reported** destroyed +/// total between execution and commit. +/// +/// `reported_destroyed` is the value handed to the block in place of the one the transaction +/// derived, which is how a test forces the derived report and the per-site enforcement lane apart +/// — a divergence execution itself cannot produce, since the two are cross-checked against each +/// other at settlement. The `Contribution` still carries what the transaction really derived. +fn run_block_reporting( + spec: MegaSpecId, + block_compute_gas_limit: u64, + txs: &[MegaTxEnvelope], + reported_destroyed: Option, ) -> Vec { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); @@ -156,12 +172,15 @@ fn run_block( let mut contributions = Vec::new(); for tx in txs { - let Ok(outcome) = executor.run_transaction(Recovered::new_unchecked(tx, CALLER)) else { + let Ok(mut outcome) = executor.run_transaction(Recovered::new_unchecked(tx, CALLER)) else { break; }; let succeeded = outcome.result.is_success(); let reported = outcome.compute_gas_used; let destroyed = outcome.compute_gas_destroyed; + if let Some(rewritten) = reported_destroyed { + outcome.compute_gas_destroyed = rewritten; + } executor.commit_transaction_outcome(outcome).expect("the commit must be admitted too"); let limiter = &executor.block_limiter; @@ -283,6 +302,64 @@ fn test_rex7_sandbox_destroyed_remainder_does_not_close_the_block_either() { assert!(cheap.succeeded, "the second transaction must execute normally"); } +/// Block admission must not move when the transaction's *reported* destroyed total does. +/// +/// The reported number is derived from a conservation law over the transaction's gas envelope; the +/// number the block admits on is the compute-gas recordings' own enforcement lane. The two agree — +/// a settlement cross-check fails loudly in debug builds if they ever stop — but only one of them +/// is a measurement of work performed, and admission runs on that one. Here the two are forced +/// apart at the seam the block reads, in both directions, which is a divergence execution cannot +/// produce on its own. +/// +/// The block must be indifferent. A block that reached its enforced counter by subtracting the +/// reported total would not be: the fixture reports five times the block's compute ceiling, so +/// under-reporting the destroyed part would close the block on the spot and refuse the transaction +/// behind it, and over-reporting it would credit the transaction with no work at all. That is the +/// difference between a lost term in the law misreporting a statistic and repacking blocks. +#[test] +fn test_rex7_block_admission_ignores_the_reported_destroyed_total() { + let txs = [ + envelope(0, HALTING_TX_GAS_LIMIT, HALTING, Bytes::new()), + envelope(1, 100_000, CHEAP, Bytes::new()), + ]; + let honest = run_block(MegaSpecId::REX7, BLOCK_COMPUTE_GAS_LIMIT, &txs); + + assert_eq!(honest.len(), 2, "the honest run must admit both transactions"); + assert!(honest[0].destroyed > 0, "the fixture must destroy a remainder to misreport one"); + assert!( + honest[0].reported > BLOCK_COMPUTE_GAS_LIMIT, + "the reported total must clear the block ceiling, or rewriting the destroyed part could \ + not change admission however it were read; got {}", + honest[0].reported, + ); + + // Under-reporting the destroyed part, then over-reporting it past the reported total itself. + for rewritten in [0, u64::MAX] { + let poisoned = + run_block_reporting(MegaSpecId::REX7, BLOCK_COMPUTE_GAS_LIMIT, &txs, Some(rewritten)); + + assert_eq!( + poisoned.len(), + honest.len(), + "reported destroyed {rewritten}: the block admitted a different set of transactions", + ); + for (index, (seen, expected)) in poisoned.iter().zip(&honest).enumerate() { + assert_eq!( + seen.block_enforced, expected.block_enforced, + "reported destroyed {rewritten}, tx {index}: the enforced counter moved", + ); + assert_eq!( + seen.block_reported, expected.block_reported, + "reported destroyed {rewritten}, tx {index}: the reported counter moved", + ); + assert_eq!( + seen.block_full, expected.block_full, + "reported destroyed {rewritten}, tx {index}: block admission changed", + ); + } + } +} + /// Executed work still fills the block: the split is a classification, not a way out of the block /// compute-gas limit. #[test] diff --git a/crates/mega-evm/tests/rex7/conservation_terms.rs b/crates/mega-evm/tests/rex7/conservation_terms.rs index 9ce14918..4dd55e4b 100644 --- a/crates/mega-evm/tests/rex7/conservation_terms.rs +++ b/crates/mega-evm/tests/rex7/conservation_terms.rs @@ -186,6 +186,79 @@ fn test_several_minted_stipends_in_one_transaction() { } } +/// A caller with no balance at all, so its value-transferring call cannot be funded. +/// +/// Everything else about the fixture matches [`run_fixture`]: the caller pops the call's success +/// flag and returns normally, so the transaction succeeds and the only destroyed remainder is the +/// one the destroying child leaves behind. +fn run_unfunded_fixture(destroy: bool) -> Outcome { + let mut builder = call_code(BytecodeBuilder::default(), EMPTY_TARGET, 1, 100_000); + if destroy { + builder = call_code(builder, CALLEE, 0, DESTROYING_CHILD_GAS); + } + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, builder.append(STOP).build()) + // No balance for CONTRACT: revm turns the value call away at frame init. + .account_code(CALLEE, Bytes::from_static(&[ADD])); + transact_default(MegaSpecId::REX7, db) +} + +/// A value-transferring call whose child frame never runs still mints a stipend, and the law needs +/// it booked. +/// +/// The stipend is created by the CALL opcode, before the callee is entered: the inherited EVM adds +/// it to the child's budget without debiting the caller. A call that is then turned away at frame +/// init — here for want of balance, equally for exceeding the call depth — hands that whole budget +/// back to the caller, mint included, so the envelope shrinks against recorded work by exactly one +/// stipend, the same way a child that ran and returned it would. +/// +/// Booking the mint on "the child frame ran" instead would leave the term short by 2,300 on this +/// shape, which any contract can produce, and the derived remainder short by the same amount: +/// [`EXPECTED_DESTROYED`] − [`CALL_STIPEND`] rather than [`EXPECTED_DESTROYED`]. +#[test] +fn test_stipend_is_minted_by_a_value_call_whose_child_frame_never_runs() { + let alone = run_unfunded_fixture(false); + let with_destroyed_envelope = run_unfunded_fixture(true); + + assert!(alone.is_success(), "the caller must survive its failed call: {:?}", alone.result); + assert!( + with_destroyed_envelope.is_success(), + "the caller must survive both children: {:?}", + with_destroyed_envelope.result, + ); + + // The child frame really never ran: an unfunded value call materialises nothing at the target. + assert!( + alone.state.get(&EMPTY_TARGET).is_none_or(|account| account.is_empty()), + "the value call must have been turned away before the target was touched", + ); + + assert_eq!( + alone.minted_call_stipend, CALL_STIPEND, + "the mint is created by the CALL opcode, not by the child frame", + ); + assert_eq!( + alone.destroyed, 0, + "a refunded child budget is not a destroyed one: nothing here was thrown away", + ); + + assert_eq!( + with_destroyed_envelope.minted_call_stipend, CALL_STIPEND, + "the failed value call still mints, with a destroying sibling alongside it", + ); + assert_eq!( + with_destroyed_envelope.destroyed, + EXPECTED_DESTROYED, + "only the destroying child's remainder is destroyed; dropping the mint would report {}", + EXPECTED_DESTROYED - CALL_STIPEND, + ); + assert_eq!( + with_destroyed_envelope.destroyed, with_destroyed_envelope.booked_destroyed, + "the derived remainder and the per-site bookings must agree", + ); +} + /// Builds a deterministic pre-EIP-155 keyless deployment transaction. fn keyless_tx_bytes(init_code: Bytes, gas_limit: u64) -> Bytes { let tx = TxLegacy { diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index cfaf580e..aae786dc 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -529,13 +529,17 @@ Two of those three are recorded as they happen, so a node MUST derive the third: `destroyed = spent + minted_stipends − storage_gas − executed_compute` - `spent` — the EVM gas the transaction's envelope burnt, read once, at the moment the envelope is final: after the transaction's gas accounting has settled and any resource-limit gas rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied. Those two move the number the receipt reports without anything having been burnt, so a node MUST NOT read `spent` after them. Gas rescued for the sender, and gas the clamp was hiding, are both out of the envelope by this point and MUST NOT be added back. -- `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations whose child frame ran. The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whether the child spends it or returns it. A node MUST add it back; without it the two sides of the law disagree by exactly that amount. +- `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints. The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whatever becomes of the stipend afterwards. The mint is created when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from that point rather than from the child frame running: an invocation turned away at frame entry — for want of balance, or at the call-depth limit — hands the whole child budget back to the caller with the stipend inside it, which shrinks the envelope against recorded work by exactly as much as a child that ran and returned it would. An invocation a node halts before handing it to the EVM, which is what a compute-gas limit reached at the call site does, mints nothing and a node MUST NOT count it. A node MUST add the total back; without it the two sides of the law disagree by exactly that amount. - `storage_gas` — the MegaETH storage gas the transaction was charged: the storage-gas share of intrinsic gas, the in-frame storage-gas surcharges, the code-deposit charge, and the charges a system contract invocation takes outside an EVM frame. At a nested-execution boundary this term takes the **difference** between what the nested execution cost the outer gas counter and what it recorded as compute, which can be negative when the nested execution's own EIP-3529 refund outgrew its storage gas; a node MUST NOT clamp that contribution at zero. -- `executed_compute` — the transaction's recorded compute total less its destroyed part: the work every resource limit is evaluated against. +- `executed_compute` — the compute gas the transaction is recorded as having performed, fixed site by site by the rules below, and the quantity every resource limit is evaluated against. It equals the reported total less the destroyed part, but that identity is a consequence of the law rather than a definition of either side. -The result is the number a node MUST report as the transaction's destroyed compute gas, and the number block-level compute accounting MUST subtract from the reported total to obtain the block's enforced compute counter. +The result is the number a node MUST report as the transaction's destroyed compute gas. A node MUST NOT report a negative result: the law cannot produce one on this spec, and a node that computes one MUST report zero rather than a wrapped value. +The law defines a reported quantity, and nothing else. +Enforcement — the transaction's own compute-gas limit, and the block's enforced compute counter, which accumulates each transaction's `executed_compute` — runs on the recorded work at every level, never on this remainder or on a total with it subtracted back out. +The two readings agree by construction of the law; keeping enforcement on the recorded side is what confines an error in the derivation to the number it reports. + The rules that follow fix `executed_compute` at each site that can leave budget unspent, which is what makes the law's remainder well defined; they are not themselves the definition of the destroyed total. A precompile invocation that fails is the same split, taken at the precompile recording site. diff --git a/docs/spec/evm/resource-limits.md b/docs/spec/evm/resource-limits.md index 68a83733..87bc0a4f 100644 --- a/docs/spec/evm/resource-limits.md +++ b/docs/spec/evm/resource-limits.md @@ -136,7 +136,7 @@ Although block compute gas usage MAY be tracked, the protocol does not impose a From [Rex7](../upgrades/rex7.md) onward, a node that tracks cumulative block compute gas MUST track it as two readings, because the [exceptional-halt frame carve-out](compute-gas.md#exceptional-halt-frame-carve-out) makes them differ. The **reported** reading accumulates each transaction's full compute-gas total, destroyed remainders included; it is the block's compute-gas statistic. -The **enforced** reading accumulates only the part each transaction performed, and is the only one a node MAY compare against a configured block compute-gas ceiling, and the only one such a ceiling's rejection MUST report as the block's usage. +The **enforced** reading accumulates only the part each transaction performed — its [`executed_compute`](compute-gas.md#destroyed-compute-gas), taken from the recordings the transaction enforced its own compute limit against rather than by subtracting the transaction's reported destroyed total — and is the only one a node MAY compare against a configured block compute-gas ceiling, and the only one such a ceiling's rejection MUST report as the block's usage. Comparing the reported reading instead would let a transaction that destroyed a large gas envelope while performing almost no work close the block's compute capacity for every transaction behind it. Before Rex7 nothing is destroyed, so the two readings coincide. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 8e29df0b..3cc80811 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -99,12 +99,15 @@ Rex7 does not define a transaction's destroyed compute gas as the sum of what it `destroyed = spent + minted_stipends − storage_gas − executed_compute` `spent` is the EVM gas the envelope burnt, read once at the moment the envelope is final — after the transaction's gas accounting has settled and any resource-limit rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied, since those move the receipt's number without anything having been burnt. -`minted_stipends` is the sum of `CALL_STIPEND` over the value-transferring `CALL` and `CALLCODE` invocations whose child frame ran: the inherited EVM grants that stipend to the child's frame budget without debiting the caller, so the frames record one stipend more work than the envelope funded per such call, and a node MUST add it back. +`minted_stipends` is the sum of `CALL_STIPEND` over the value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints: the inherited EVM grants that stipend to the child's frame budget without debiting the caller, so the frames record one stipend more work than the envelope funded per such call, and a node MUST add it back. +The mint happens when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from there rather than from the child frame running — an invocation turned away at frame entry, for want of balance or at the call-depth limit, returns the whole child budget with the stipend inside it and shrinks the envelope by exactly as much as a child that ran and returned it would. +An invocation a node halts before handing it to the EVM, which is what a compute-gas limit reached at the call site does, mints nothing and a node MUST NOT count it. `storage_gas` is the MegaETH storage gas the transaction was charged, taken as a signed difference at a nested-execution boundary — negative when the nested execution's own EIP-3529 refund outgrew its storage gas — which a node MUST NOT clamp at zero. -`executed_compute` is the recorded compute total less the destroyed part: the work every resource limit is evaluated against. +`executed_compute` is the compute gas the transaction is recorded as having performed, fixed site by site by the rules below and by [Compute Gas](../evm/compute-gas.md); it is the quantity every resource limit is evaluated against, and it equals the reported total less the destroyed part as a consequence of the law rather than by definition. A node MUST NOT report a negative result; the law cannot produce one on this spec, and a node that computes one MUST report zero rather than a wrapped value. -Enforcement is unaffected: the work a limit is evaluated against still comes from the checkpoint and per-opcode recordings, and only the reported destroyed total — and the block's enforced counter, which subtracts it — comes from the law. +Enforcement is unaffected, at every level: the work a limit is evaluated against — the transaction's own compute-gas limit and the block's enforced compute counter alike — still comes from the checkpoint and per-opcode recordings, and only the reported destroyed total comes from the law. +The block's enforced counter accumulates each transaction's `executed_compute`; it does not subtract the reported destroyed total from the reported total, which would put the derivation on the admission path. The split MUST be driven by the halt classification, not by the interpreter's own counter — an inherited EVM zeroes that counter for ordinary out-of-gas only. That zeroing has one consequence a node MUST accept rather than work around: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero at frame exit, so the whole segment measures as executed and is enforced in full. From c702acb2f14227cbd8203ebf2a60f7a0e1f97e59 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 17 Aug 2026 09:02:17 +0800 Subject: [PATCH 068/208] chore: fix prettier formatting and refresh a drifted suppression line --- AGENTS.md | 2 +- ...heckpoint-compute-gas-accounting-design.md | 268 +++++ .../checkpoint-fig1-per-opcode-tax-en.png | Bin 0 -> 92676 bytes .../assets/checkpoint-fig1-per-opcode-tax.png | Bin 0 -> 86398 bytes .../assets/checkpoint-fig2-granularity-en.png | Bin 0 -> 100690 bytes design/assets/checkpoint-fig2-granularity.png | Bin 0 -> 98387 bytes .../assets/checkpoint-fig3-enforcement-en.png | Bin 0 -> 74192 bytes design/assets/checkpoint-fig3-enforcement.png | Bin 0 -> 72021 bytes .../checkpoint-fig4-final-effect-en.png | Bin 0 -> 91660 bytes .../assets/checkpoint-fig4-final-effect.png | Bin 0 -> 98653 bytes design/assets/make_charts.py | 268 +++++ .../reference-checkpoint_accounting_tests.rs | 238 ++++ ...opcode-interpreter-hotloop-workload-.patch | 74 ++ ...-clamp-enforcement-for-checkpoint-ac.patch | 1021 +++++++++++++++++ ...ype-checkpoint-based-compute-gas-acc.patch | 732 ++++++++++++ mutants/suppressions.toml | 4 +- 16 files changed, 2604 insertions(+), 3 deletions(-) create mode 100644 design/2026-07-24-checkpoint-compute-gas-accounting-design.md create mode 100644 design/assets/checkpoint-fig1-per-opcode-tax-en.png create mode 100644 design/assets/checkpoint-fig1-per-opcode-tax.png create mode 100644 design/assets/checkpoint-fig2-granularity-en.png create mode 100644 design/assets/checkpoint-fig2-granularity.png create mode 100644 design/assets/checkpoint-fig3-enforcement-en.png create mode 100644 design/assets/checkpoint-fig3-enforcement.png create mode 100644 design/assets/checkpoint-fig4-final-effect-en.png create mode 100644 design/assets/checkpoint-fig4-final-effect.png create mode 100644 design/assets/make_charts.py create mode 100644 design/reference-checkpoint_accounting_tests.rs create mode 100644 design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch create mode 100644 design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch create mode 100644 design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch diff --git a/AGENTS.md b/AGENTS.md index b5e11d03..b71669f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,7 +120,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). - The destroyed part a transaction *reports* is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. + The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call; it is booked per mint event — at the CALL-family settlement, before frame init — so a value call turned away at frame entry (insufficient balance, call depth) books one too, because its refund returns the mint into the caller's envelope. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). diff --git a/design/2026-07-24-checkpoint-compute-gas-accounting-design.md b/design/2026-07-24-checkpoint-compute-gas-accounting-design.md new file mode 100644 index 00000000..5a6be803 --- /dev/null +++ b/design/2026-07-24-checkpoint-compute-gas-accounting-design.md @@ -0,0 +1,268 @@ +--- +description: 检查点式 compute gas 记账 + V0 gas 钳制执法 —— 普通操作码零计量税、限额执法零 overshoot,热循环实测 -50%,以 REX7 发布。 +--- + +# 检查点式 Compute Gas 记账 —— 设计与验证报告(REX7) + +> 🕵️ **TL;DR** +> +> mega-evm 给每条操作码都包了一层 compute gas 记账,对廉价高频操作码这是一笔与本体相当甚至更高的固定税;PR #313 已在"逐位不变"约束下把能省的都省完,剩余 5–9% 全程序周期是"检查逐操作码存在"本身的成本——删掉它必须动不变式,因此以新 spec **REX7** 发布。 +> +> **目的**:普通操作码零计量税,同时不越限交易逐位等价、限额与 detention 执法不弱化。 +> +> **做了什么**: +> +> - **检查点结算**:约 140 个普通操作码直接接 revm 原装指令,compute gas 在检查点按解释器 gas 差值一次结算; +> - **粒度探索三轮收敛**:V1(无跳转检查点)→ 发现纯循环段无界、撤回 → V2 被 V1.5 支配淘汰 → **V0(gas 钳制执法)当选**; +> - **V0 可行性原型验证通过**:钳制解释器可见余量,revm 自身的逐操作码 gas 检查成为执法工具——零 overshoot,halt 提前到越限操作码执行前。 +> +> **结果如何**: +> +> - 解释器热循环(70 万廉价操作码)**1.87 ms → 0.93 ms(-50%)**,与原装 revm 地板持平;真实 ERC20 转账同 run 快于逐操作码记账; +> - **1,182 项既有测试零改动全绿**;激活钳制下 `GAS` 返回值、compute 总量、回执与逐操作码逐位一致; +> - 执法比现行更紧:越限操作码在执行前被拦下,detention 在无检查点纯循环内零 overshoot。 + +## 一、方法与方案 + +### 1.1 税在哪里:问题定位 + +MegaEVM 给每个操作码都包了一层 compute gas 记录(`evm/instructions.rs` 里的 `compute_gas_ext::*`):每执行一条指令,就把该操作码的 gas 记入 `AdditionalLimit` 并检查一次每交易 compute gas 限额。 +对昂贵操作码这笔记账开销可以忽略,但对真实执行流中占绝对多数的廉价高频操作码,它是一笔与操作码本体相当甚至更高的固定税。 + +PR #313(已合并,"trim per-opcode hot path")是对同一笔税的上一刀:把 `record_compute_gas` 从每操作码扫四维收窄为只查 compute 一维(其余三维挪到各自变更位点 latch,即现行资源限额检查协议的由来),并拆出精简包装宏让约 95 个简单操作码甩掉用不上的子帧 gas 分支。 +它的边界是**逐位不变**:halt 仍落在恰好越限的那个操作码上,行为零变化,因此无需新 spec。 + +| | PR #313 | 本设计(检查点记账 + V0 钳制) | +| -------- | ---------------------------------------- | --------------------------------------------------------------------------------- | +| 思路 | 逐操作码检查**做便宜**(少查维、精简宏) | 普通操作码的检查**整个移除**,结算挪到检查点、执法交给 revm 自身的 gas 检查 | +| 不变式 | halt 落点逐位不变 | 不越限交易逐位相同;越限交易 halt 提前到越限操作码执行前(V0 钳制,零 overshoot) | +| spec | 不需要 | 必须 REX7 | +| 收益量级 | 常数因子级 | 5–9% 全程序周期 | + +两者是接力关系:本轮全部测量的基线已包含 #313 的成果,剩余的税(如 `jb` 检查跳转占 `push1` 采样 25%)是"检查存在本身"的成本;自动补丁搜索在"逐位不变"约束下确证已无可削(见 2.1),所以需要动不变式本身。 +本设计的 latch 协议调整也直接搭在 #313 建立的 latch 机制上——只是把浮出点从"下一次 `record_compute_gas`"挪到"下一个检查点"。 + +![逐操作码计量税解剖](assets/checkpoint-fig1-per-opcode-tax.png) + +| 指标 | 值 | +| ---------------------------------------------- | -----------------------: | +| `push1` 占全程序 cycles / instructions / Ir | 18.27% / 23.72% / 20.08% | +| `add` 占 cycles / instructions | 4.79% / 6.49% | +| `pop` 占 cycles / instructions | 4.09% / 5.59% | +| `push1` 内部记账包装占比(Ir 归因) | 约 27% 的 push1 | +| `push1` 内部记账包装占比(`perf annotate` 带) | 35–50% 的采样 | +| 单条限额检查跳转 `jb` | 25.05% 的 push1 采样 | +| **包装成本归零时的全程序天花板** | **5–9% 的 cycles** | + +这不是微架构病理:全局分支 miss 率仅 0.093%,`push1` 只占分支 miss 的 0.79%、icache miss 的 0.78%,稳态 page fault 为零。 +成本就是逐操作码检查的比较/记录/跳转指令本身被执行了——这也是任何原位改写都删不掉它的原因:按现行语义,检查必须逐操作码存在。 + +注意范围:`push1` 只是测量中最大的单项代表——它是真实字节码中出现频率最高的操作码(每个常量入栈、每个跳转目标偏移都是一条 PUSH)。 +同样的固定税落在**每一个**被包装的普通操作码上(表中 `add`、`pop` 呈现完全同构的占比结构),**5–9% 的天花板是整个包装族的合计,不是 `push1` 单项**。 +昂贵操作码(`SSTORE` 等)不在此列:它们的包装成本相对操作码本体可忽略(检查点处约 99% 的 gas 质量是 `SSTORE` 本体),且它们在新方案中本来就作为检查点继续保留包装。 + +### 1.2 方案:检查点结算 + +REX7 下,指令表把**全部约 140 个普通操作码直接接到 revm 原装指令**——无包装、无逐操作码记录。 +Compute gas 在每个检查点结算: + +```text +段内 compute gas = 解释器 gas(上一检查点) - 解释器 gas(当前) + - 段内已单独记录的非 compute gas +``` + +检查点 = 本来就必须包装的位置(storage gas 操作码 `SSTORE`/`LOG0`–`LOG4`/`SELFDESTRUCT`、`CALL` 族、`CREATE`/`CREATE2`、volatile/detention 操作码)+ `GAS`(V0 需要,见 1.3)+ 帧进出。 +由于所有记录非 compute gas 的位置(storage gas 附加费)本身就是检查点,减项是精确的。 +结算值记入 `ComputeGasTracker`,随后执行完整的四维限额检查,浮出任何已 latch 的越限。 + +**精确性不变式:不越限交易的记账总量与逐操作码记账逐位相同。** +解释器的 gas 计数器本来就计量每个操作码(含内存扩展等动态部分);按差值结算得到完全相同的总和。 +从不越限的交易——绝对多数——逐字节不受影响:gas 相同、回执相同、状态相同。 +越限交易的 halt 语义由执法机制决定,见 1.3。 + +### 1.3 执法:V0 gas 钳制 + +结算解决"记多少",执法解决"何时停"。 +朴素的"检查点才检查"会让越限交易多跑一段(overshoot),而段长上界正是整个探索的主战场(见 2.2);最终当选的执法机制是 **V0 gas 钳制**,它让 overshoot 概念整个消失: + +- 在每个检查点和帧进出/续跑处,把解释器**可见**剩余 gas 钳到 compute 余量(帧级预算与 TX 级含 detention 余量的较小值,并记住绑定方),差额隐藏; +- 普通段内只有纯 compute 操作码消耗 gas,revm 自身的逐操作码 gas 检查就成了执法工具——越限操作码在钳制边界"假 OOG"、**执行前**被拦下,零附加税、零 overshoot; +- 下一检查点先恢复隐藏余量再跑本体,`CALL` 转发、`GAS` 读数、storage 扣费全部看到真实计数器;帧末恢复隐藏量入帧结果,假 OOG 按绑定方重分类(帧级 → revert、TX 级 → halt),detention 归因保持 `VolatileDataAccessOutOfGas` 与现行一致。 + +代价是设计复杂度,三处必须处理:`GAS` 操作码必须入检查点集(否则钳制值可被合约观测,破坏精确性不变式);假 OOG 需在帧结果层识别、还原并重分类;`CALL` 族的 63/64 转发必须在检查点先恢复真实余量再计算。 +**可行性验证已通过**(实现侧原型,检查点原型分支),验证细节与证据见 2.4。 + +### 1.4 协议调整与不改什么 + +- **资源限额检查协议**:变更位点仍然立即 latch 越限(规则 1 不变——这些位点全是检查点);latch 的浮出点从下一次 `record_compute_gas` 改为下一个检查点;规则 1 的 `debug_assert` 相应移位。 +- **Detention**:volatile 操作码保持包装并照旧设 cap;V0 下钳制余量含 detention 余量,执法精确到操作码、无 overshoot。 +- **Halt 落点规则(V0,已定)**:越限操作码在执行前于钳制边界假 OOG,帧末还原隐藏余量并按绑定方重分类(帧级预算绑定为帧 revert,TX 级/detention 绑定为 TX halt,detention 归因保持 `VolatileDataAccessOutOfGas`),剩余 gas 照旧保留用于退款。 + **双超界角点已裁定:compute 判定优先**——越限操作码同时超出真实 EVM 余量与 compute 余量时,判为 compute/detention halt(带 rescue 退款)而非 EVM OOG 燃尽。 + 理由:帧末无法区分二者(OOG 时无操作码成本信息);该角点仅在两类余量于同一操作码同时耗尽的刀尖出现;方向优待发送者且不引入新的可套利面——想避免燃尽的调用方本就可以主动 REVERT。 +- **Gas 泄漏路径**(系统合约拦截、限额越限时的 gas rescue、帧返回):`CALL` 族检查点在 `frame_init` _之前_ 结算调用方的段并恢复钳制,因此拦截短路、rescue 路径和 63/64 转发看到的都是已结算、真实域的状态;帧返回检查点在子帧 gas 合并回父帧前结算子帧并恢复钳制。 +- **检查点集合修正**:按长度计费的动态成本操作码 {`KECCAK256`、`CALLDATACOPY`、`CODECOPY`、`RETURNDATACOPY`、`MCOPY`} 单个即可烧掉远超码长界的 gas;V2/V1.5 类方案必须把它们入检查点集(主网执行占比 <0.72%,包装税可忽略),V0 钳制对此天然免疫。 + `EXTCODECOPY` 已在 volatile 检查点集内,`EXP` 动态部分封顶 1,600 gas 可忽略;剩余的 `MLOAD`/`MSTORE` 一次性内存扩张是现行逐操作码执法同样存在的单操作码敞口,V0 下同样被钳制精确拦截。 +- **不动 revm**:MegaEVM 本来就注入自己的指令表;本设计只改变哪些表项带包装,解释器循环、revm 原生 gas 计量、全部 revm 指令实现保持原装,revm 的 gas 计数器就是结算数据源与执法工具。 +- **所有既有 spec 逐字节冻结**:逐操作码表继续为 ≤ REX6 接线;检查点表只为 REX7 接线,沿用现有的按 spec 指令表模式。 +- **Storage gas、data size、KV 更新、state growth 四路记账**保持现有记录位点和语义不变;这些位点按构造就是检查点。 + +--- + +## 二、观测与探索记录 + +### 2.1 测量方法 + +五条互相独立的手段交叉验证: + +1. **硬件计数器背离研究**(AMD EPYC 9754,钉核,每计数器组 5 轮取 median/MAD):逐函数的 cycles、instructions、IPC、分支 miss、L1 icache miss、page fault,并与 Callgrind Ir 份额交叉对照。 +2. **`push1` 指令级解剖**(Callgrind 文件/行归因 + `perf annotate`):最热操作码里,操作码本体和记账包装各占多少。 +3. **检查点 overshoot 模拟**:回放逐操作码 gas 轨迹,按候选检查点粒度切段(V1 = 仅现有必包装位置;V2 = V1 + `JUMP`/`JUMPI`;V3 = V2 + `JUMPDEST`;后补 V1.5 = 仅回跳结算),测量段长的 gas 加权分布——即越限 halt 最晚会落到多远——覆盖合成/probe 语料和 **1,000 笔真实主网交易**(chain id 4326,区块 22,085,597–22,086,184,`debug_traceTransaction`,共 9,737,035 个执行操作码;n=100 与 n=1000 之间分布已收敛)。 +4. **自动补丁搜索**(可编辑面在通过变异敏感度门、并把差分 oracle 强化到可观测四维用量与 halt 标签之后,扩展到 `limit/limit.rs`、`limit/compute_gas.rs`、`limit/frame_limit.rs`):没有找到任何安全的原位优化——证明这笔税是结构性的,不是实现失误。 +5. **实现侧原型差分**(检查点原型分支):把每个候选真实接进 REX6 指令表,用同一套 criterion 基准组(vanilla revm / op-revm / equivalence / rex4 / rex5 / rex6 同 run 对照)测 wall-clock,用全量测试套件(1,182 项,含 REX5↔REX6 逐位平价断言)做行为差分——模拟给方向,原型给实证。 + +三类硬件指标在探索中的分工与读数: + +- **IPC(周期对指令的背离)是主力**,两个核心发现都由它给出。 + 主探针全局 IPC 3.42(SSTORE/LOG 负载 2.823,CREATE 负载 3.366);逐函数背离比给出 `push1` = 0.77(高 IPC 纯计算,税是真周期,Ir 看得见——支撑本设计的收益估计),以及反方向的 `HashMap::get_mut` = 9.02×、`hashbrown::rustc_entry` = 4.9×、`FoldHasher::hash` = 4.15×(低 IPC 访存瓶颈,合计约 12% 周期——Ir 失明的状态访问局部性矿脉,归属大头在 revm 上游,另列一线,不在本设计范围内)。 +- **Cache miss 用于证伪**:`push1` 的 L1 icache miss 份额仅 0.78%(对比其 18.27% 周期),驳掉"包装宏代码膨胀撑爆 I-cache"假设;同族的分支 miss 全局仅 0.093%,`push1` 占 miss 的 0.79%,派发表不进榜,驳掉"解释器派发打爆分支预测器"假设。 +- **Page fault 用于排除**:major fault 为 0,稳态(第 2–5 秒)0 faults/s,与全部发现无关。 + +### 2.2 探索主线:粒度的三次收敛 + +#### 第一轮:overshoot 模拟初判 V1 + +检查点方案下,普通操作码不再包装;越限交易的 halt 落到下一个检查点,最多多执行一段。 +段长的 gas 加权分布: + +| 来源 | 变体 | p50 / p90 / p99 / max(gas) | 残余包装操作占比 | 可收回收益 | +| ----------- | ---- | ----------------------------: | ---------------: | -------------: | +| 合成+probe | V1 | 45,512 / — / 50,002 / 50,002 | 3.02% | 4.85–8.73% | +| 合成+probe | V2 | 184 / — / 50,002 / 50,002 | 4.04% | 4.80–8.64% | +| 合成+probe | V3 | 184 / — / 50,002 / 50,002 | 5.26% | 4.74–8.53% | +| 主网 n=1000 | V1 | 665 / 4,325 / 26,935 / 27,151 | **0.71%** | **4.96–8.94%** | +| 主网 n=1000 | V2 | 57 / 168 / 6,706 / 6,772 | 7.80% | 4.61–8.30% | +| 主网 n=1000 | V3 | 60 / 168 / 6,705 / 6,771 | 14.39% | 4.28–7.71% | +| 主网 n=1000 | V1.5 | 194 / 317 / 6,753 / 6,861 | 3.87%(等效)\* | 4.81–8.65% | + +\* V1.5 等效残余税 = V1 位点完整结算 0.66% + 回跳完整结算 1.91% + 非回跳跳转的轻量方向判断 5.19%×0.25(按轻量成本 ≈ 完整结算 25% 的点估计折算)。 +回跳占执行流 1.91%,全部跳转占 7.10%;码长界经验验证:250,080 个段检查,0 反例。 + +![检查点粒度两难](assets/checkpoint-fig2-granularity.png) + +真实合约代码是跳转密集的:把 `JUMP`/`JUMPI`(V2)乃至 `JUMPDEST`(V3)设为检查点,等于给主网 7.8% / 14.4% 的执行操作码重新上税——恰恰是要消除的廉价操作码税;V1 让 99.3% 的执行操作码完全免税。 +初版据此裁定 V1,并推导"24 KB 合约按 1 字节 3 gas 直线执行约 73k gas、直线无法成环"的码长上界。 +原型第一轮(V1 接线)实测:热循环 1.87 ms → 0.93 ms(-50%),落在原装 revm 地板上——收益侧完全兑现。 + +#### 第二轮:循环洞——V1 裁定撤回 + +**猜想**:73k 码长上界对 V1 成立("直线无法成环")。 +**验证**:该推理只在 `JUMP`/`JUMPI` 是检查点时成立,而 V1 恰恰不含跳转——一个十几字节的纯算术循环(`JUMPDEST; …; JUMPI` 回跳)内部没有任何 V1 检查点,段会跨越任意多次迭代。 +实现侧测试钉住:detention cap 1000 下,逐操作码记账(Rex5)在 cap 附近停住;V1 原型把 26 万 gas 的循环整个跑完,直到帧末结算才 halt。 +**结论**:V1 的段长上界实际是**剩余 EVM gas**,不是码长;compute 限额对循环密集代码退化为建议值,detention 有效界变成 `cap + 剩余 gas`,破坏"访问易变数据后快速终止"的并行目标。 +测量数据中的 `synth_jump_loop` V1 段长 45,512 也是同一现象——它受负载形状约束,不是结构界。 +**V1 裁定据此撤回**,粒度重开为待定问题。 + +#### 第三轮:候选重开与逐一定夺 + +- **V2(`JUMP`/`JUMPI` 全设检查点)**:段无法跨越跳转,码长上界恢复;原型实测热循环 1.09 ms(对 V1 回吐约三分之一收益)。 + **被 V1.5 支配淘汰**:两者结构上界相同(码长界),主网实测尾部几乎相同(max 6,861 vs 6,772),而 V1.5 的完整结算税(2.57%)远低于 V2(7.80%)——V2 相对 V1.5 没有任何优势维度。 +- **仅回跳结算(V1.5)**:`JUMP`/`JUMPI` 仍包装但只在回跳(目标 < 当前 PC)时完整结算;只经前向跳转的段内 PC 单调前进,段操作数受码长约束;每次循环迭代付一次结算——这是任何要给循环设界的方案的内禀成本。 + 主网重切:回跳占执行流 1.91%,段长 p50/p99/max = 194/6,753/6,861,码长界 250,080 段零反例,等效残余税 3.87%,可收回 4.81–8.65%;最长段即算术热循环体(POP/MULMOD/SWAP/DUP),正是靠回跳边界切开——机制按设计工作。 + 原型实测:最坏形状(回跳占跳转 100% 的热循环)1.11 ms,与 V2 同价;功能面上钉住 V1 循环洞的测试按预期翻转——halt 从帧末(compute 281k)收回到回跳处(compute 22k ≈ cap + 一次迭代)。 + µs 级单笔 ERC20 负载对各粒度变体无区分度(代码布局噪声 ±5% 盖过差异),残余税定值以主网轨迹重切为准。 +- **码长界的第二个洞(检查点集合修正)**:即便有了跳转检查点,"1 字节 3 gas"的推导也只对常数费用操作码成立——`KECCAK256`(6 gas/字)等长度计费操作码单个即可烧掉远超码长界的 gas(24 KB 直线塞满 1 MiB 输入的 `KECCAK256`,单段约 6 亿 gas)。 + V2/V1.5 须把五个长度计费操作码并入检查点集(主网占比 <0.72%,代价可忽略);`MLOAD`/`MSTORE` 的一次性内存扩张无法经济地检查点化,只能表述为"与现行逐操作码执法等同、不因本设计引入的单操作码敞口"。 + 这把 V2/V1.5 的上界钉成了**相对式**:相对逐操作码执法的额外 overshoot ≤ 约 73k,绝对界仍然做不到。 +- **V0(gas 钳制执法)**:对上述全部问题天然免疫——无 overshoot、无段长界问题、无动态计费敞口,普通操作码零附加税,且比现行执法更紧(越限操作码执行前拦下 vs 现行执行完才检查)。 + 按既定规则(可行性验证通过则选 V0)完成原型验证后,**粒度封定为 V0**:零 overshoot、比现行执法更紧、热循环贴 V1/原装 revm 地板、残余税最低(V1 位点 0.71% + `GAS` ≲0.1%)。 + V1.5 作为已测完备的回退方案归档(回跳结算,4.81–8.65% 收回,变体补丁留存)。 + +![执法精确性对比](assets/checkpoint-fig3-enforcement.png) + +### 2.3 主网数据佐证(热度与限额逼近度) + +REX7 归类下的主网执行流(1,000 笔):**普通操作码占执行次数 92.20%**(gas 质量仅 13.26%——gas 被 LOG/SLOAD/SSTORE 主导),检查点类占 7.34% 次数 / 57.63% gas,volatile 类 0.47% / 29.12%。 +5–9% 天花板的前提(绝大多数执行次数免税)在真链按执行次数口径成立;gas 质量口径与包装税无关,不作前提。 + +长度计费五操作码合计执行占比 < 0.72%(`KECCAK256` 0.626% 为主,其余 ≤0.073%),本窗单次最大 gas 仅 716——入检查点集的包装税实测可忽略。 +本窗未出现"单次烧穿码长界"的极端大拷贝(样本偏小输入),理论敞口仍在,检查点集修正保留。 + +限额逼近度:单笔 compute 总量 p50/p99/max = 56,495 / 485,145 / 2,279,067,对 200M per-tx 限额仅 0.028% / 0.24% / 1.14%;**99.9% 的交易触碰 volatile 数据**,post-access 用量对 20M cap 为 2.4% / 11.4%(p99/max)。 +含义:真实流量远离一切 binding cap——**overshoot 是正确性上界的设计问题,不是高频用户可见行为**(触限案例 hard 0、near 4)。 +这不弱化上界要求(对抗形状仍需容纳),但说明执法机制选择不会造成现网可感知的行为面变化。 + +### 2.4 原型实测效果(V0) + +![最终效果](assets/checkpoint-fig4-final-effect.png) + +各方案在解释器热循环基准(70 万廉价操作码,本地 wall-clock,criterion 同 run 内含 vanilla revm / equivalence / rex5 对照行): + +| 方案 | 热循环用时 | vs 逐操作码 | 执法语义 | +| -------------------- | ----------: | ----------: | ---------------------------- | +| 逐操作码(改造前) | 1.87 ms | — | halt 在越限操作码(执行后) | +| V2(跳转全结算) | 1.09 ms | -42% | overshoot ≤ 码长界(相对式) | +| V1.5(仅回跳结算) | 1.11 ms | -40% | overshoot ≤ 码长界(相对式) | +| V1(无跳转检查点) | 0.93 ms | -50% | overshoot 无界(已否决) | +| **V0(钳制,最终)** | **0.93 ms** | **-50%** | **零 overshoot,halt 提前** | +| 原装 revm(下界) | 0.93 ms | — | — | + +V0 与 V1 同速——钳制只在检查点付费,普通段零附加税;同 run 的 rex5(逐操作码代表)与 equivalence 行波动 <2%,是测量有效性的对照。 +真实 ERC20 转账(weth9 transfer,同 run):V0 9.26 µs,快于逐操作码 rex5 的 9.40 µs,对 equivalence(8.86 µs)残余差距约 4.5%——来自帧钩子、storage gas 检查点、拦截器分发等非指令税。 + +V0 等价与执法证据清单: + +- **不越限逐位等价**:全量 1,182 项测试零改动全绿(含 REX5↔REX6 平价套件的 compute_gas / gas_used 逐位断言)、workspace 1,325 全绿;该主张经监督者独立复跑核实(passed 1,182 / failed 0 / exit 0);专项测试证明激活钳制下 `GAS` 返回值、compute 总量、回执与 Rex5 逐位一致。 +- **执法精确**:专项测试证明越限交易 halt 在越限操作码执行前(Rex5 记入越限操作码、usage 超限;V0 的 usage 停在限额内),detention 在无检查点纯循环内零 overshoot,halt reason 保持 `VolatileDataAccessOutOfGas` 归因。 +- **泄漏路径**:拦截、rescue、帧返回三条路径由既有测试覆盖,钳制在帧边界处恒为已恢复状态(`CALL` 检查点在 `frame_init` 前恢复,帧末结算恢复入帧结果)。 +- **门禁**:clippy 零新增警告、fmt、riscv no_std、cargo bench 本地全部通过。 + +### 2.5 相关工作对照:为什么不采用 evmone 的 basic block 校验 + +evmone(advanced 解释器)的高效 gas 计算算法:装载期把字节码切成 basic block(`JUMPDEST` 开块,`JUMP`/`JUMPI`/终结指令收块),预计算每块的静态 gas 总和与栈需求,运行时**块入口一次校验**代替逐操作码校验;`GAS`/`CALL` 需要精确 gas-left,用每指令附带的修正常数还原,不为它们切块。 +评估结论:思想已殊途同归,机制不适用于我们的任何一个维度,不采用。 + +先记三个同源点: + +- 他们的 basic block ≈ 我们的段(块界恰好是我们淘汰的 V2/V3 粒度); +- 他们的 `GAS`/`CALL` 修正常数(预计算)≈ 我们的钳制恢复恒等式"真实 = 可见 + 隐藏"(运行时维护); +- 他们"块入口预扣整块 gas、halt 提前到块入口,因 OOG 燃尽帧 gas 且回滚全部效果而无共识影响"的论证,与 V0"halt 提前到越限操作码执行前"的裁定同构——主流实现的现成先例。 + +不采用的原因,按维度: + +1. **mega compute 维——V0 已经比它更粗、且更强。** + 块预计算的前提是费用静态可知(每操作码基础费是编译期常数)。 + V0 钳制下普通段内零检查零记账,结算位点(V1 位点 0.71% + `GAS`)比块界(主网 7.8–14.4%)稀疏一个量级;块界当结算点我们实测过(热循环 +17%)。 + 预计算省的是"段总额的计算",而我们检查点成本的大头是记账机器(RefCell + 记录 + 四维浮出),不在计算上;且钳制执法根本不需要段界,"块入口一次校验"没有需求对象。 +2. **revm 原生 gas/栈检查(地板本身)——检查长在够不着的地方。** + 我们贴着的 equivalence 地板内部,是 revm 每条指令函数体自带的 `gas!` 扣费与栈检查——不在解释器循环里,换指令表消不掉。 + 仿照 evmone 必须给约 140 个普通操作码提供"无检查体"的自制实现,外加按 code hash 缓存的装载期块分析和 EVM gas 的 `GAS`/`CALL` 修正值。 + 技术可行、共识论证成立(同上第三个同源点),但等于用约 140 份手抄的共识关键指令体替换 revm 原装——掏空本设计"revm 原装指令即正确性来源"的信任论证,且每次 revm 升级都要逐一重审。 + 归入 revm 上游 / 自研解释器长线(见后续方向),不属于 REX7。 +3. **storage gas 维——无静态成分可预计算,且成本结构不是"检查"。** + SSTORE 收不收、收多少取决于槽的原值/现值/新值与 SALT 桶容量,CALL 的新账户费取决于目标账户空否与转账值,LOG 的费取决于栈上操作数——全部是运行时信息,分析期连"要不要收费"都判定不了,块预计算没有对象。 + τ_state(≈68 ns/SSTORE,`sstore_100` 仍 1.51× op-revm)的构成是状态访问(SALT 桶查找、账户/槽 inspect、journal HashMap——即硬件计数器研究的低 IPC 访存矿脉)加四维记账机器;"限额检查"经 #313 降维 + latch 后只占零头,消检查省不出可见收益。 + 这些维度也不是 gas 计价的,钳制式执法同样用不上;data size 等限额是 DoS 防线,执法不能推迟到帧末。 + 评估为收益不大,不立项;存量杠杆是消 CALL 族 wrapper 的重复 host 查询与 revm 上游存储路径升级(pin 之后上游已把存储路径优化约 1.5×)。 + +--- + +## 三、后续方向 + +- **REX7 转正**:本轮验证在 REX6 原型分支上完成;按既定流程引入 `MegaSpecId::REX7`(spec 枚举、hardfork 映射、constants 段),REX6 回滚为逐操作码并冻结,检查点 + V0 机制整体平移到 REX7 门控,补 `docs/spec/upgrades/rex7.md` 升级页。 +- **Tracing 兼容**:是否保留可选的逐操作码记账模式,供依赖逐操作码 compute gas 归因的调试/追踪工具使用。 +- **测试计划(REX7 化)**:spec 门控的钳制执法测试(每类检查点、双超界角点、假 OOG 重分类)、四维 latch 浮出测试、三条 gas 泄漏路径测试、断言旧 spec 逐字节相同的差分回放。 +- **CodSpeed 量化**:PR 化后由 CodSpeed 指令数报告量化实际收益对 5–9% 天花板的兑现程度(本地 wall-clock 已验证方向与量级)。 +- **上界常数**:随 V0 当选作废(无 overshoot 即无上界常数);若未来回退 V1.5,按相对式表述(相对现行执法额外 ≤ 约 73k)。 +- **升级 revm 基座**:硬件计数器研究另行发现的低 IPC 状态访问矿脉(约 12% 周期,`HashMap`/hash 访存)归属 revm 上游,另列一线跟进;上游在我们 pin 的版本之后已把存储路径优化约 1.5×。 +- **压 revm 地板本身(长线)**:消 revm 原生逐操作码 gas/栈检查的 evmone 式块级校验是已验证的路线,但只能走 revm 上游或自研解释器(不适用原因与代价见 2.5);立项前先做"无检查指令体"消融实验测天花板。 + +## 附录 + +### 原型分支 + +检查点原型分支 `cz/feat/rex7-checkpoint-accounting`(基于 REX6 预览线):热循环基准、V1 原型、V1.5/V2 变体实测、V0 钳制实现与全部专项测试;图表由 `assets/` 下的 Python 脚本产物提供。 + +### 数据出处 + +以上全部数字来自 2026 年 7 月的测量:硬件计数器背离研究、`push1` 解剖、自动补丁搜索穷尽运行、检查点 overshoot 模拟(合成语料 + 1,000 笔主网交易)、主网轨迹重切(回跳/热度/限额逼近度)、以及检查点原型分支的实现侧差分与基准。 +含 SHA-256 清单的原始证据树已归档,可按需提供。 diff --git a/design/assets/checkpoint-fig1-per-opcode-tax-en.png b/design/assets/checkpoint-fig1-per-opcode-tax-en.png new file mode 100644 index 0000000000000000000000000000000000000000..b26993647d5f4e1b0aff3f3039fd3d0aa087cb3f GIT binary patch literal 92676 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%0AxkJ(tIPu#V&k~1>XUAcn4j$@fus&W+C#Lv@`AvU z#mQeb9}g3N5;0OLvaZLZ^4;9$axd-j2Eben3g37mUdRBIsMNitFP41qU;4y|d#1Dh zF;a-t(@~34#YUEySb>`4^&8t*h{V~>_|H9zjVK3br^h52a5GK+Qy})D;Gl z2HaoOXO4s{w|GH4LI5g$|Blub&FJ5926TghZfD{vl}Fb0?@YbL@`mkvK)KbP4Hn-j zUR>8xzZNvillJLB$f)(6RvNT?62&zaGbh-a`Ji2;ThqO8l}ICI>V@N4;dN{{({3Zu z-?%vNbhpo?h2F{#x0WE>3G6No*;umr1$+Ch0Q$PqsOx{6N%DVQmm|5E{&_|0{MTRZXab0q+5@0N>~m=_VxP~j(fj3^ ztw!W+zPq-g<(7dXNDi%Vh#6GIz(y}7&_phgMxxja&@Xlgv#oO!5cr=fFMxwqVwc}0 zGR8AG3#jXr2^%lk`!~?7l`}rrWV9~6ZhIkS42bxX7cV|__0ZycA0tWEC{mmcNKD5M ziBX6yAnb+ntv2*T;e%6=?sjs6vl#7 z-)Y6(dn}HEuRNAUj#zvkWb!K@uCi=_QCy61WyetYI$tS(wV)PsEL(q51QwBAvl#P+ z4RJkDG2g(AS9`_iB^}Z%#VF21zE1yJkJe`AnVZXxXBx}diK1xhEu*RMVQbKBSGrzw>lgaUEi2C^079gF#=YSm+d?2g$jLT0Gnd>nrG&+|~A{TDwKu?e>{LNa^d ziNDwt3qk>_r^jFXT#UwPrKT^D7K6MW%fPa*7Hyk?HyOxNxN z_z*UW5lC3gi5~)x4YVQM+5aLM@$RViW+quI7CW3PfZF#$Rrf@|KBMNTbnz$OqaI~v zyt|T>q~mVQ;$)LdSYX&)$mE1}pv?fdK$b|J0w1N7jrc7e%CGrr_y%a$`Uk`H{*Hi4 z6k-40!(IwNL#gKs7BZv-v%t?VQWT(gRD#BH@a*4KOFWVoa)D^5cdy-hHr9fF%nP;E2`MB%ECbld9r&fBAm0O;>N81uOWV7q6;Y&Z!?cK{}~=7+&LHO6?yE8JIMIUWGbF-K;7y0y_ijKWDWUcv?p#94TG~& z8_eQ9;nxQ9g#d$kko9q+c$SS=9`SF!fA><8X{+AB$H1aG+=vLONXa|vr(U4ZBqHH9 zI5);H+`_(;`2e1ZfvQ}z1EbtxrTxa1eupEi5pLyi^hm4a?@xB7xbhb>WcKl1kvYjY7XV2#8XU!X5PKZN-ZTXjp3d2`P}}N=Phn@qWkeZo zY-&wzYo;lU!Nnq@IfvIvbAKWnG%Tm8jEo{^JIUnjIj~t_DyC(vguc*>gsxJ(8q=n` zce|*3|KR~?4r99?dZYEhomW4FJ@ztaxS;T?1$a0|KKyh-^nLLA(V{pdJNN^OD=+<(v@aiD`gQWV@Hwj$a5Od2tjnADeTska)R1pWa7+wk zCp2ZQ#@Yf@1G~npQO`^==~omH1i_-n2`(#P1-Zs$)nB2OuH?`Tz|+fz|M=dwbhI>I zb%=^)|5KWslcD;MMzH^>mtQ9Mu-eijvHFqVw6+Zwe(-#|qrv$1H{66#rF1}{BMTEw z!mj(?0U9PMJ!;je*6Y)&EpN}~W@R@U^qj5ZveNqK_1u1b(*fF&i<7RlNeNiu4=)5| ziv(5Otq!MM)q>5ayH!$IPMh+Mkec3DPZ;+QQ0!h~HlZvJ@tY{CZzWSS9~k+cMtD%> zGkDAWa@)m85(r_eHp>**AchN}*iU-GBN_m7ZmZt~_X$^@U^i$UEv@@jZ_q07AWp~K zF8CMhLVeNF@pML67T5Vz$@%d6bNf$3{LK02rr-ie_ao%gFrj0X+5pK!3Isn{h^(td zg9iUp%cX}lIDR~n3?slTCKtotMm}Zvz(05tyzYUK2KdixLY6a@@fM9c3o(S4;}H(i zbDXi&$}gxH1Oc_+Pkx_{=h^$P!+PO|L;=jW2W-p>twE*HM1mKSWTIRNfTHpH#0Rih z3&p>9b?q9^y@}wXChF+QsCV1pWYsRuSQDBE;huFN8xY}cE-e@{B}>-0whFK7)%I9i z;-{CTJu6zt=CycziKXZGWZa_FaQ5-==yq@|Wgqc1dJ=gw zM||VRoMYmNFr1Exbm7e=N4G%6B|e88fpkxF@Et^&jE;(AJ8Xh+v^wouLX8Is_>5M+ z-G6AIpT32#60|`q9Zk1{*}d>^Ho~=+1(1VMXX#4U({;4zpM>)I`x%&|VM(T#Zo%B# zmBG~-m@0WP@isz^;BM*Z3aEULc9+_~Jvj zsw@p<+;7a^A^I4Y)Zshw#dP5o;fMWt_9jl__hH!EdByVK%4VX(c8T+6lh^2Pghk`( zGW0XWsBnV4ebi|PT505T`6hfuQ$-y>eK7k-?MMP0^jtympy(zNU2b|wUJgv|io)+9 zJK@gqKjo>|dtbPggX_HGqZdo!Pb@uP8)3pTQop~~rXoK!WM~39E*v z-HC|#EL6g*Li7Fs5LnUqMIZD6EuQk8(b7d5k)W_L5pyaE=ycAunoWVr<8)EF_G97I z6a=o;_&MCk6+D09SHGNQbE45*#R*bB)H{yZ##yT{R5=wUB#Fr$BNE8c^0 z1GJU#ndMP|mU6!&!LQv#1m8W2HQroaLq+Zo`4=Q}dbe@`?m(p0nqy*mLz3H_o ze~Kd7=EJp7Hu&zSNj#x+;19X#y}lRTRWM2<3uHx!J04T+W~CC1W8$khQ%V79Eb!77 zp{_|acT>o(K0xBn$oYvtI|>Qhx;b(%D@6MW=(QO%e3Y5)O>}>=PluRg83m=DrRKR+ zrA?yNKiBTxe7BUGmmPIxSyf+{2qYw4nAUxIaS_BovC3vlARCCe%Mwp10PoidL;f70 z$O4ZhkBy0R7!`Zv>S_s+j{9hJY8>AWgAFtCT}ZQ4I{hd{hD0F6y#WsV#|?ZUJE<7? z1BK;T!U2+7q2cr@7!j_oslA(rzx2KAsG$MIm}C>-4oQmW6fur(v=LS|0kKrC37#UN zw~h`ezRZ{^A|bLJ<2F(^bT`QBNKkDz(xsoBrq)p&g7MpwVhpE^eF8&54O>5LhVmDe{^)iH1OzTl-)8VJXhFxrht8Ihf3}8Wg$23py3cV=$gX z;>C=VRbCIe$&0GTe>sjSsc+##0J+`TJMSjI@+qT!mlO>!`Hmv#h|jhOn76^=vDbMP zQj%xdT&OMBI9$6?%xXCrF2Rr6v0TW?-qa9->6$9R6erMl2avaEd1v^)r669nhEeIO zh64teFggJ5k<86a$Bo{b2y&MvK!3!sfV)6oo~yU+;S|z1%Prk48KKXakR`G+%alZm z6hr)bLY326B;=pjf+Z+h0CpY1gND8aW0IIL#`!l{5VhCyzR{7ND@4DCiv0Woggf3e zmGX4it70k?d{=q&*Vi!a`bbsZCQf!@h=7eEw%_JYp%&JIy5)si$E%v5w2$-`ssn;Y zATu#CnG-9L7FwP)6VyIayt5NY#NI?+SZyqhD^tdVwP4;Rh!7XWtLHPoZ>s{H<|2S)?LUO z1I16%LFBkG=~r7RAmNiACg;{_Oy$P1wnIC=eL;~XrlyD=^_9;@@ zu?lW?+4#|6rjYWTnc?rO=hwFBgQt@h!6D{z)wEjt9*eIu7VeuAh_737Lk!zw)vtpm z%1VX76;)m46ml9~dqn+OhyP;IRCJs#a6CxGCY{lf(~!Ks`qlcotoM4_6wTrsUOMm3 zsMsN#>jNN{V4U5tyU0MJ_cx(wiprzTY8BQ!#LU$Wi`lh3Es_NYEM5dUDhU|Kxp0c$ zh*w%Z_O-arg5m>>;eJxrNLd>h?!AC_bddsxBeRZuLfqHRZ)4K<)FsAabD0wU;~#5?@- zbpOs`Dtru!!Tj!mVyaSC6nqrZhqh|}P>Y6DWTGBPjTy^lO$&imLo710NEH47E^--8 z#mQIC`f+dDm1C@nN_xl%3QFq*AXyi=`yEV8oEr;bb^e9Sdg!x*KR=vDR)(LNuXW4v znhzQm?bs&zvWwDvpR2une#OlQ8 zu5C}>vW9Q#N8l!si6%^xwc%bVlw)&m zGeCZjQK_}J;JOemnBWazO&4%!#fa0RGwlt&|ItFzV+qJvP#~LCx(7BU9PzE6E#`1amyv>1eScb3_Mr-BDg0j-h4Qoga|%P~6|s4pP{M4CIHcc<2== zT37_ju~0)`9)j53#C~9>Aui>!wYAut2%Ly7$Y*3{vd1&h5NU5$>6LC!e8ppu0Jl0u z-(L?vVfh0DNG#rjGjqi8Cb@7Q8RNV&dMEJ)TZWXx;4SP}|2E+LWR!o27Yanz@9Lmv zy9rP_HGMJ3*Z>SwzA(Ak6{fih8ORpOLC!NF&8TFoUbAZUjG+}Fw z$!^2|zIioY^}>+@${SHG6Ms%13TwaAsNk^Gze$<%X&3DM6Db}rjo;eOao3*&&mma6 zstns+vBPkM=Fsuch!=D7e*J57&CoO*p={k*ojdFtuhPfw>O?leN(FE{d`4v_@YUafxW(WO{Wl z@gK4i>c#jLGj^t%3>caYj;0fV{$Vc{UVt8>m*>Hal0#ZM5J8>3r|8@#cR)ke8?@mF zA`oYJGXnLdXCOh3^Tiy<+rnyXHi694sEE-!@d!va@*QOKspfp+vwDq!V#;ZT1ae~; zY-w=%?eV>HuT+knJu7dK%5y66O~7EfZvzJ;h47A$9&>x#2qs+u__b2gL4aXCg@>dT z1`m_<{e{vSVN|=RX0a=i@H(7-5=;^Ddv={O3>{Y!U6K8VAei_%@ZmavxfET_TL)1E zXS?aPgpp^sM|UM*_ai-Y-B77V7%4cJHj^LRw) zy9{ngvlH}_QrEu5+pTn?IUf~2Y-%tV3x8D9FSK?TO%`()3!aTZQcWZERKp0u6_Cf9 z-9Yxm?yU>)0!e@ALuKf7c;nce;aG5<9Ial0(;_{2;*!;^MWeyk)mIre+ zuV-bAC(cC%&x7MmnsIksgowcNRKZ+n9nbK}lqPO>c0KjsCKnfFJgAwb6Q)36@|+{(gd z?=zQePqNNY4S1$f(`hG$zscUxQ=;S3`|8gTIyfOh~Xh$BT_mtehVwDf-$< zFWe&(WbQphFi-GE@qcp~FDPkL+!@as5y3tHS>E&A+A4~J2xdwwaRki%b` zR<}1mj+WVB$HHH)rKFCkjmv)FtwYR|kxp$`21HXd!QMwq_oqxb_EDArO}5jedv1VT z^Ey17FsWf~C1EI-IZig`UBiC?@9zneNoCO{yR0j1ew4*7L^S@sALgS2AMZ+B&|2&X z&LOePCog3>c30Xm-})F>qlpx)s-U5a1JQ+Pue8_mBd@)8yI3M(I7IW-(vb08)rd5k z%+cfYCM=lLa>~j2;v^~it}I%|VS?DGp7w4S6)CvX!o4MVTeC(%S^+r6WBj+;w`a{x zPv&>fIGeG%@Uq>rZxNND;)mr_?S+b@5)^R`&6Y#+bYV3fq>p{tV;c?J{j2k%o3u;@ z%gr8I8<~1@C`Ls3hlS2-FBSRC?%B!Mxmi8qJb+OL(#sgZCjbjuQ|pfVIQ<*1L=J<* zm0@OsxUAxs6R^&IAdEL`5ycShNS(qjT=qnk7f?!|{N01Vxy87D^ z%KX0s7Po+aEH?YFaOSSKj^ixE;Ti6ms%PDfMcm(#n%OEWd4^ zjoo z0liG*3;q*8Ye(00)bV+)TycsG=Pfon`BqQ-Y;h(s?t(@{^O!->qlEN1WrNJ<7h&&2 zw=D(P3xWn_ZaPW-bWQ_JeF=FK8d2<;2%P_f_Da<-4e`g(9B+J3e>&i^l%4@G)PcQ! zMN*GuZ(r~%NIgHH;M7^l26V&=I0F2EPhW8#$XYwP?NnOTvuR2PZ%d7u-w@@?bXs-zwtW|4LOU7 zkbITydhuEoI`E3ptYGE)3*Gg;^ORU}PDrB3!bDU|RYj{*X3v~nyT4FbRCs~EZi{F9 z=r=y-i(R@~$eB=5w$ap67wxliCQ&?XfVq2(gNfa2%x z|B<_v(Ww{NbBcVTt$-c9<#ZNe zN6!j2XN%JYEaEzml^-h5BBf55D!|Wul3t~I1KJeEv5+&2s~l7vXpa&%-;W4KbU}g( z?K4OgXF{H@|9y5mILHe2P-ieckeWmJMLC7R)C4z1Mi;`bl7ze`PUD{drTTTegx zL;pxR(s;IetJa|h?nVN~oGR`kF*@zQ&Utv{9t-UkccR$qgjlN<&%@e1f6NpMJ7;{( z02Vrc@CaC|8wE(i&BTu|Ju?BRQ00*lt8r;s2nE>_v}-pQySj@Mv%mTcNJ^~#!u=;y zUQvlYzia=Yhb(I`Ulz>hN1wYoNlW0Z197U42MNa=@)cCpMDu%}7*B6=BQTX|!%T=` zyS^9 zM(pL21wy3sm>c9oBD05U9!(4*&ZNXw4Fth;k!T9+e;xsaU6I)f)lDm0xi>Vy;g9Sn zSIs?90%$CS%Fk-$`19|Pw5RCyU^`<~>{}ZW25OOK0w#H`bqWIv`YTuz5LHvGR}eGw z)?oHM4-zW5%z*7FPVDEA8!H3bq)kHp!_=vo#A6}OOvc~cB(^+$EPC`>AVs0_*rG3V z4bL?_>HSKmd_o|Pl@{G!8c(j4li_zG=Cdx~6U7~N3iWf%JLS-=mh=eCmzDTnh=NZm zt}cy79OzEA1U3w}JVmY%TLoqb_JAH8@I#wx@&lm+8ryjz0FcoGSh956LWz(VH-^*7 zhBGm6(XYF)Qww;c(jO-o&K)Z5GV0uDBm&Q#)!e$`CTM}F!PkHurQVM}fRl5?>wn{1 zuMsVPD?$efpta9_nzkK^@hDW+X$7;)y)SVy1^%l}VHwVL{Ym!cTtyL~Z z^NYBZm@g3vLSQfP)*FF2gU3iPj$?rshkTJ*#?NtTywWkM*NnXG427tuOXtOP6(B)b zPubOJE<>>v*vXJYwZz3>e(Pe^NtmdY5C0{LdMQu3<$bsXrL6On)fItkLLs~ff)rst zRXjNiMRvf+;rYgEp&d%KNO7vl17d(yn`sEx_w~0C(tw7!Q^$dUJ7D~bXX|8*!F8kG zqEOwS|?Q1g#9WkA_xLc`wF`HXzzc6}s9`h@DC zUuUV3=HBnwU77p)n<(zCzL;U-YbC0n#HTnc4xGTbg@_og)HYNsqSAIGG&E={H&cE* zk~an#`V=Dxv+~`lSj08H1x_BG@h{2f(&@6<;yyE$9CbyWF_Wd9y0zz%x&FdA&#dtE zd~(gn73z4@jM2_T(iOb-KmGtQI;@J$#-%zd{Gyayj&(UXTL(}mb0c9hwP=7X0%1TM z7Wy*X{{Bs>2DGn3r)uSFwM*u$-hUaqg_n4Yln!W64IXjHrpSRnbx~Xt5qJ}yc-I^d z2YvaDQ2>{TQZz5C79`jErCNQWl#{>;*b|94!k&>)dM7R&jx?&=LEq&y`$O zJB|5aFY+w7`z?3dCvNFCcxJl5_`-BdLK7n}wgu#QeycHko2lZwPdA}n)cxCT|sqm{L{a= zAgX=jV__fHdovb+JBAg?6~;~Rr~hohE!iJIF=Yh8rk@{|i?qFz+Fc6Toc+=x2Yvhij187KEVY1L&lnK9y zA1C4jR?oof@r*jx1XkYc9FhKA65Ea9apb`3O#)GhHIfKK*9O`69Z*U>#Y=H7P*9Mg z`~k}C8FuWqiMmw>un%71$5bfSUj9_H-k5vz(W95Km<7*pG5Lt9aB0GiI7qXTtO!~^T}{VPtg1HaW_jKfTOvUbKS!U{eNK4D4ynkN+nqxeQY2-G`H$!|L zSm%?{0;}5iRE=d~LU2ZBH^V**Dccd4(4$MqK7>dkdX}&re*%-PhLbTgnS*PvT}6o} zLFK*Ks-N3Im|AF*$QNWw8-rhZYzG*~a!w!GnFfKlhu>sl@V7t4=r6>DQJCcBno*E~ zgZ4=Yb`-Dt7%`;P*&c3sv4z5^;wE$hBAw;wJndZujDNN%jV^8Rbj`$Ya#!c^7#nxp z`fg#BRRb(g+i)x+*c22j8CKmB9O`ERWkHSBo6lA$UtwEH3>A$DmPSE&gPsFxUMk=m zh`*NwraZWSxxYNq_WYa)3fM1Gv^v??s;tThG|w|LRHo6nyLc@LoD=m=Fx0)#Ie6J1 zFg!zSvpf^GKqbso8!rPh@gPDb{Y-xp20A{muQr&*#sM}lk#;K?$EV_>klG$WG&G_} zk-Ed75RTkBD?IjMFK^lP_BnXG6mCBYrj2*3B$nj8GrSK21+|s*KTnv%mD-F{5j*bH z9D3}{wq?H%PAiZ_O9isLB}hobWH>L2navv`OszbFUJ-Ql;v2$P1cs}EfccvM3rYUi z9lZIl(K>GoFgd3>FA`QG;GxEJx7EC^YG7R$=2i(oMzgrT>d;*B>~#02gsc(AH3u|tt)00ftKqXSW| zu}$<0(Xu|6=)8AhQllO@+-riTV9c7wJp1KwCgC6xzfl&>%i2Aj6vHkZ)B2A-IM$s6j=hce3}-@EX21p=d73$khRVN6Ro5JCSfsPl~zM0gLa)<8t}3w<72K zi!kn-xfig~a=S++J3-Q(VxVTP^C^<okfIvT7CpzIKkl0%Ff{OUpMsYu=m~jv z5vN7}7oH^;$2{5&2SxP4${U_7B(qCf^WPInqw+DPgw-)q+Nr;3trP4_JUU^GYe|d~B`lsOi6XK=uujk8hhu}?j zhlF&uAf3|iF4XVyoH5S%2hJGpFAk}F@4eUBYtJ?3b$zCn-qH+jQGK`@B&jq+8~TV-1xV1hF3*lI<~!z;0mAMx?6XBest zkXmcSW_*XawFAHb>&hkNCE?r;FOB3K-^|tAidl*NH2UBcmd}dVK}Hv_%n%~TcBl+Rf~ItG zlQHE~)Q-rGC{Z>T51)i7opu(r2%M&ug;wHRp>%Ui{{&Lv((Ol4D~`?~C6WQ0Z!O8@ zL#slo6(~CMMMQ&5&R4r!cpaCGBCK531yvL%OMoPM>n4Fyria5U4zGjs%)xL=+n_ z1(wMLYRd&;tbFN0v6q7O)Tf=dqMg=m5-Zcaf^ftHumq~U<8r_%9OGyr*5XX+Z~p@_Qm95TkQyeoJEphcJhxrBI^!3F3y}y z&>^TKgn>FG#7nE-)r8V&6Vfn;V2jEGNAUz8OE0I#W6bqdVqj%du&Y{4medXR%k_rS z4C6?ZioH3uE@p$W@Y$n>w&C>IR45V|#GPv2uo?4HWhL72)#aPQDguN`Ta=x%Lg<*N zuj6`Qm13BJm^QiCFAIM>Z40fUu?P)C6dRicGLwAi`;sei+@L)Gu>hj3LU%^WI^#-{u1~&R zEgubn6w&3xgl*|N)$5D6MTC7J#~ajCp`jXV{!764MSIoJk`PVSZAo)Oy%-{kjPgIQ z8i1|wMCc9#lLFk_fB&pLw)(%{_`mn#{|D3IiTCE$f0LH;fp4=pl=Xl{NdZ=yKCoWW ziiINMa#}D)lDsH08@#CeD4%W69v-*6lZX3fw&T+LH_>Sz723?sTOi}QkKgO+G?Gu5=7U5euFf$b1M{bWEE&Z9Xu)+;+((F`Vj{b)^3vRj0^A*sluLJBb^@%oRLf= zPjUVI&0LiKe)He3dm_+L(fs-7PHDZ`!|eG+Cw&88A3GQHl`v@bW*p8EN-3JKzAW+m zcLyH-cK{R%ppKx>f6qBJ;T3ylXAG&B=GmdJ8?Swl+uJk z%A-X8ENBr^xiIhkj7z)`ooH~V^Tsi)Az{ehkt!F5`*;0qgY=S71F7Fh))$)^npsi* z9?akkqSfDZF*u`Nqv!#sWHvo==ltsjf+1n}zhmGGnN(^wdU*Z)#R%%P?pT#<={V9v zkhqquKP95|zqy!P8yNxSJKAT4jq&77h!+(f|6N&3gy@anzcW|-y|=(Jn%@%-#2JyH zX!Ka*^I8F#%miL{H+5i?2h+O#4hT)}2;6^{g!t66)^SC^o&4`HsQzaZ{(cP>_poKk zKcCeZ{z3R2IQD&I0XK^!5|=hHxlm319oH{kedfQXE&%)AWV;9ig5v(@*GcQsmDI>k zV;7na>;0M1@3=1IaOvcBJ8=I!KdzAAzjrd;=qC^+{8$TDIzH|~D>SHzH5*7($EA^> z1XYL>yq>r|;17)78qFKXp`r%lpWbFx*1u;M6!yQj_zl|6{?9QxW``pQiIk#Z0X-n? z!6F#2#cV#Hk^C^=AEW;bc<>>4-M=yFG*`+LU^Va8WaAPF0VpWh8Ry6Aok#g?2HgKH z3+2Ddg69N7k~&^E#wSRxQN*<^@b?$QIRB%u;SK`0kvh4c_txnLAgeAq`TI^p`9AVU z@y~x{*pfKRt$BSf0>@lpNre~~)jp#_IdJ|QXBFeWX-<>Qs8duzDMSz^E%%iDHz zKT(`I5CnJq(e>A{5EFPaKg^vh$`4(tYqb9^DU)c7z7Je|@(b70k{Mcqn+xarMSWhc zspG>H@yN$asS>fII^elv0ThG8boSdjWpJLMv|BK@ zFvr8f_Yah@Pp-ez9(XW`?52o}T4g2G)@PZ(1f&ZV%$2cY7QI>(fLEhq(e>XLbB?cw zvA2A{Hw?7QH2KtADFRz1O*LDMG4=$ho`82@Z@jo{p<5zvkoQ&!`hW5@-i#HPzuNcW|4rpRBgY|z2xLM8d&SJCMVOX zwSiKQRjr0jgaS+rOsaVjIEKJ}K@5g7)c4Lywhxd@D*Bh=e;7m@AQQ5F0Mkw!cnLpW zVH}ZP9uvL{h!;Kd?}UV0(Hlnr$wA@p#Sz}c?rr-PP>K_S^OCRFQZ&xY+2&$5kw^n3 zLz(&7qjBF|fVxNmGCSGRE&nUJBUbPlOak%*Sx`Lq1iX~Snh*Cb&^9DG@Q8>nlp2kI zIUej|p7K7(Q@w7lVn_u%RdB(Yy!;KMT3Ph#UBBI3i<XkUpAC)l;Gw^;@LR^M9dysW_Y zF=0I!d1i%!w3_NY5{NmNm7=YX53XQdiu>|jBCSnwhco_P(yL%wLe+u?E zE3uwg04=EeE9{%~1UI`#I7TKzZMJxgA@u_A8zJ1^>wR=}_As|Ha(4(tz@!lRIQukf zi4bvXMDu`2vm~BH7b=r0_U3gUrt!$5+n@^IoHq9zN?}SH%x-B_sch@#Z@haV6-@&q zhw)&E=>_$+kfsuV@>0php}z9NUBv)D-E!w2Yl+0BT8~iLi!_6pilp(XeT-MN(D$g< zqxzI(JB&sl)Tn`ZiQ6=S^C8NqUf76~5`K@0=JZWP`^U%@xh}a08ZYOTuG>!mtS_&$ zL9QR2e)}-O|7f*N!0+m1(Nw`&2riy7?vL)ZKM&jDkJna#nB>(;RzJ0IHC^+T{V}&N zEb2Re&^@JWuk7ejTzVLxR(!tdLzaUF8rmu%@Q6zjv7W*vW>>Dh5%9m!#%O%N6=IQTZTCT&vEA3czpvSz zf3PZ>UMM=+or!Zi36&2{3&(^BzFy@q&X*_=-IvW8xn0JZ;P z9rV>7#3GNir+5TnnIzyrmIL&_n>Pz=uyFgJI79`0*O>Mtb_FOj16akt z1N+kb2EYUcfYQYr3?T?=Xl37VWvmZnf~erNwRRlaE0$N)DnEy_pMhXZ3r=HuyzrAs zt{5nKAO(amH(Kg>>`dB$w~ADUB^#gR>6R*oNhdN9msR|1cNmLU!0k7ER)fZZ_S$4X zJ$rDB;R_0I#+K{WXztI~<@YBE2l)S*sY)w62GVns$7y;~&sUY(-nZyC{NBDL2|ABG zud<$yo7g{GJZb>k%z+n6o^(M&fXSuNz~_A$Q8X7Pe5Va`vQ&nC6JlLSV-JEP@87#r z;JuLy=wI&)99jeKtiOmw5g7Ib!oYo4b-$MmkV$w_cXkh!rG(=Rri<%^X1{AQ zu*-Me9+!O%0YE)jkj{SgVB^|ukSclgO%B-PpMufXIEyG%c(AcFU+*_iGDVYc+o~M= z-ksr;!+8^Zb+zO0Q4HbU97%;7;LRvbsjAtlgP3)`0>!rmcdD-N*gFCHD;#|JFW*Ht~OL{FAJoqE<{%K%osuh(RW zL6e{uT%=k06p!xa^Xv#%s@6fV)Y>GHBd`3M?d7+kwN`A1PN_LH^fWIEXTSw8nw$YN z;;|GhFoCOmvwqHK*xdM9CaN_$rNG7Z^&17LmTv7B|4?s{C03Q3oL???0C@J;9q|Bl z57$WsWU^_%Y7p;xxqBU(kSA<0sQrxv-wpl~x!dW47R!XT6+AG)BV^(E5#}QQ_$bQ+ z=B4ASXU1<*4Qrsp*`ibXO&UCKr}vH~%P=hdQRc&0f*3y&6PL|;(VE3Pi6lopt=oGu--9IUD~ z_37Wv6TK>z$OL{rkqIQ+G|{*o&v<|cEw*(%bsTH4nb+fGv|1NhPzb-QS z^=>xe9n7DrReg4p#&g`G-ed>3obFEtvG9>^**izs{+AT27vW>7t z=IYmzKA1ix8W2bhQy1qR;RP1_sR`f5Umu+)X#G5~fbVhPQ7f6Yd|Dlyk+s*~i1e`k zAXWm1oB?;cehtC7iO^y)zG^M!a+@i8jt4kngL`Sc{c5Lda_m@@rShkIwOZIID5&}?knT;3;(h%4m5qpTK3@+Y%F$k;AS;;U&5 z1X@JN@L(AFRZwxTpy1VDeJHsqa{~&FbNjqGMmc3K>}Ctl_f`B?Rw9E?5LFo{9xC%v zI6Zq$hZ@)2NX_P4PCXc?;RS`*c?g*_^`H(*pAmIv>PZE?t2tVY0>j$TL#W_?17z4s z+`(@)QW=PC;cu@dpS-0m}sL9K4$*%*+65>7zlD_Z>dILK*fwN{H|x4kFh$ zS+tl;AWRokg-E2RE-Alz^hf1v1G(f2Jw`)_z!ACe;q_k4Qi&4*U_TXeeCiNEq{R$* zuiEemXzZ^%*1BV0nZ-N8VoI`_YPsn=Vu+vLwOE!fJ=#XbjW=nB3BlUN0h$T!k~C1; zE^=pg-MZ!peN?IsKyp? zhoK-7EU+2>J_+>y_|?_s@UHpxw=S>eVe37{TaAf^5SVXZw#yN9gpRn2p^K9xyWVgE zh3Jo*Ho|^5_#{2%*_>y9>;}!Imd5-PL9Mj3zu+@&hLgm^5Cfv*%I^2B2n=gLRg}_l zzxXLx_Y?%^)#?h?IaXqQP<`f6k<TnvCL0Kv znl`+(faLuAvvF`3_p3@1Nw@ENbN6bd@B}`THKZ7_K}1ZWP$TS~P~rmuDeLhi0H#Wg zmLM|%!tK^0f6w!D9;1>#0T4jcs{PJ2%l`l<&FaqN2kki33!0_P>S12d(roW?{aV=` zd4*q*a1T0crXWt08n$G;!fzA9aYca$P8^e@}V7QVYjIRyS6makV{PQ)h4s*qTPJT_%jtx&k^BV8| zq@{}MyJe}k%z^ilgKi3D+v|`jp+>VAo99QmIV%*{X@(PaB z*tQJmJfaCwV~7TXY2PeFVbGJzVYW^U%tC08O^OVSII}{@lW3$fyCC z7;@Vl4T3qw1qP)LwcvG(F(G8^7|e>5e~>RdeM!LB%ZKwbeM_C~!`iImTi+*;MWfSm zmrfmeuz9JL_l)s?h2wGsSS>awu{dByYb?U!!Ox9rpC*DV@YiN_;4nXFql|`H6kCcD z`X8^q4n$idz9Fxt_*n(*w)8l))f(Bu-vFZLcdyXL_q-1l{p7GDboHRNbw6xW2(@>? zmi)t4_SV-(SJ?n2YlROhhZ)kNZH3n>j zfaN! zTHiiTa29niQa<+C@4z*}=Of@Phai;u2s%( zYFYpH&Xm@#@^qWA3(`jfxIbS{m1WWbighZ;g&|!L1Ibx6AjXd-QcJrJ0BtY&@WWQA zei%S)N|wlk{pS;(NIDPDXX96X5b*l3e*-pF+7Avv|0CQ~e( zZyklSaNK~Mb!whu)GMJoo~jKH_D>!FAyBEmdeKY90g&Oxac)UAG?ac$IV zxA-?*>AykUU;$e#D1VsG=0Mt1BA?sS0l?uZU0S<(;G@teXAOdzq&)!A61)B$ARZq8 zAfTdH3L)R=eeDO}%@L;TrxeJS7n(AM+w1H+1pge|9~ z{v2dacALXVx27vY_`-g#{AmJU8qXU9V@J{gvRF+UGo07{k1hkb(d$f^C{6{B{0~0> zr3Z-L0a*6|H67qg%jTB9p#|)BH%$)XSXXedhK-)1N{eg!r|i+stg>To!L~If6@>RX zK>EF=jL1XH@Rj&0&PGslvHF+TL{;c~qu1Eeht>+F9?Wc}w1Lqb=S@E6(1#@7KBW5# zcv}~*fvFAU{|%_PMduMm2h)|N?mZxvWkJN_80an4%h7Z9s4nb0&bz|z*;Ng|L1~@e zfYp0Fm+!_@JH5z@Ea4a%M@5RKf6Ba!GTi)q@$;PNr z9xk8Xw#8ZD!+_Fxy)zJRIpRSnZF`M*1;8C%EC7#KbNwBdvyugd046JwmBVRO!VPQ7 zF6+m8*!*my&`Bn$O{x&eF*5VBhtwZ0SY)dBxjptt&jhoc=j_a7;iX@pzg05*9J1^U z;{=dqXPZ1we(^ZMkqMIL)*nw@zJrkEC5gDs(T(R4)SJMpfjk@(BdoR4!1sS^IWQQ4 zMA@CN(%DyjAT7~6Ys#X?DH}wN_FqA@a2d+kmVn&5`8bi9wgG(hxEnh~6~YPa7A$~i zX`J+dxN??k>UL_u5@UbU@v}VAM@uAlGnCl^`cqp@a5RfOCWhj@#>ZbbcgkC!P~CB? zp&D~=%fjr)zmjf>8#cOZ#tQ4AnGyMKUijOZe+PR2^a0XoUd~gq8&*K|QL>Z#B!B(u zHQ+h^o(eykMpzhiZ}vP`6qE;^!tu&{>9}cq2XHFeK3j$H!qDaWku>(Kpx-b=|=0}Era5;g;}6wkqy5UdV4h~vfp5%ei5Gwa|M@U-0=*9zqtX} zO{Mi82;+CLqOw8D?V;mPJaM8n2jC61^i)K*!0u#Y=2b_`p)tlKnyyc;dytDvbcAhKN%<)(<~ic`|vU)Y07yBq2`GuG2-^sLNW`3(F_+b@)}1lU==jPiP` zND#%)LFCi!UTbjF0G#60u1c(3n^7c6+{|^G1bPS~KFpjb0V*6msqV%s0Yq_RAJ!%i zbIW7aksk!+J03uAr?v;|U?z8ne2+TV{6p%G!(fO5Zm)>aKrT{t`vN>vrAwmh?Plv> z>&4deEc(?2*;PXE7Sx2RZ}N0+Zpu(Ms15yKcGSok4WE@kZ7Tqp6 zRwH3FHS~ZMJmBbc7m7d~1E{)z?2lSH@`%upi4{~1bsJw$I|`oqeOQ~O0NLcc<{G;f zv8a6b*We{SBS=k9St;@wot?58u}Y;#BZq zH`SaI&RQi7kfWd0q{G)m9)QG<)x~?jYtsR_jZXxXTxJQZeLBh zP{&T1FY$F$XTG;yz0PZt^_ zy<+RFQ~Mwh%n3R(M~9W^&Q{Ku`c`*rR0b4Yv*qw3%&dMUvgtEdYskz8-b^0i$!c;K z>f-#QByvOcXTkpam~FSDi(DdWHMLC_cS#RJVPG?SYbxDYRFg-?A&`*GM z_fpuAt9EwCG>2cb#g&G#jv$@4s&ud|IE$0bp0}TF&O7)6r`Q~|ZDt54@)n|xRgh({ z2pBI$U+NiQGz_7SI8JMW#s*K{C)`CJ7~O=8E+jmlBxBOj*fg}7JO~nNI+``#!ozBs=-$$UyMM0^e6lK^PFh+j@B|N2r z7;WomvEb5Y5m=#i5wQW&_Q89>(;T^N?(TssEV1kcFi;~902t;;y0GYwKqriBq(BBp zLk5<*KbesUWW64W3)(H>TeyK2uPi9bNLO8{X%I|?c!OEKN7z_PT%GzKRaDr?=MQ(e zv)E=-A{-!B##DS;V%~Q!66r)bupEn29WCUNk>~n z4)4ur;@Askuz6OQ<7xv{_lZ=EM%}7CqiM_y2(*K&B4%)=L863doN2ZNaZU|~9M%rm z374#w3zx^qrLSDUycpYQ+ z4a$Dt`W3rZRNoMbVzvhpe#;a9ow-ZlLVH_z`Os6CJjFCk-+$ZVHr1_AB%PCBk2NZ` zQt6hn^_eh#j;4DwDow$t6J9zp{kFgj*0x2GLL!|8Jb67A6~v)#V$uAzOblMDG`D|~v=4DX~(CVtJc z?FHpPT1mpDgSlkDe$tB*T5L64k=q$}An;2~<fx>(e;QZ& zwmqq~7|(cD(h6iS(ex><7~8F_GWH>aIw8|jP-AnhkvbgQM|_F-LPWUP)U}7HSus0L zPJ^lWnBjhsA)BruU%|%C^xJG1SPew8HiN)j!HM+bg1$s~3p7jmM%oezFW^_C)x;r~T*w!g z=7=b2TwCd_zXhoEewAFklDP9F&qgT%Bs>6ItIi?9#snW zpa%|&5FUI+z6nx3wFwb_*8zy*Ei_QMB0j-)V4C1i7b2j; zdb&eM9VwKFv!2(+Re7LOzfH)Muq=`!HR$n(oesZ1L(V)z3?nnbpPF=1!_&qloYWy@ zXCRBHi-K8l$_wRrtKC_t5&Gb7r-g{B30ty{2g9yf=jshD#@Stup2cZLT)2dlxp;^~ z3v+f9u*TW>h~(kNKQF#tcYtm+M+&XKcFp9WD@Q3%Iy9)<3hY#lXo;1CG-91f=A(qs zUu;see6<*`SRBgy_Pn#4t3$`fsm8wgM@_)J(8LSuVH|d8eI1@Rz8)6ZqI&!aS)|vw zoB~wE>`)%KekPIV*9!G;{F+Z>(b?o8tPCe@Cr{WhDTnBiZF^-bk5X|$CXaX)dAISB z(uRf5JUPHd=l)d^F_jP{ZmsXFoBZ=i5mGD2F}pD;Ly)R=p50FQ%em;A`@0vWvl3&H zj-4A_*FO&8L+#>=I}~1wd1vj_MjSlw%r5~~RpSnWCNU)I{Bo04LqS99EHYx2_fPT(E#&`A@TYOLM!=s*|vnggBAp(bo!v01< zjoS{v;~o+(MO$_oe(mH@6hatW4aDS})W&ecEv!d^71VgvDx&T_kQ}Oc_Ka~ctYUjm z^iX3mB5n#s71vW4%`)kw*^|qI)m@W3`N^_gHT<@a@?6ICh4Z@184@)dzEp``-P7e~ zhM4o5ZP&!ziGhhszjh4joM&E}$Y`}Td7tm+Cqv9=ds<6kB0ZJCOCc3xvJtn4fLkXd zZjEw%h+mrbyXKDFC?Yy~(W6D~A!NA*F5S{^29-xW@KiMT`KiS1ZKQPy5wVQ|lt{z1B1+XMMkdpgUDTS9` z2czKP#xCE>=yBLgDs=47$p4&|K{q0G-wj_(oq!=KPY@+*?RdgqO56W7Qdy*1*H7P* zadx0r0;b=F*=R2I%OQGOCtgF;^61aE(qH!47YqaTRAenz5JE$n0&b4Ni)=hsY8;kC z6(Jpbs`--YQDvZe2BVspdWUB9)+p^sj}9odziQKyTSc3nIH?}>-W<~BFzt5TKtP8> zPaG8EZe=(GT@{MEXe{9+9Z=u;@1w#DjCHyna}~ftg%&1W`$p2coiS+KHO zBM1nUOPVOt$f6_f@$P=-by4~01f^81;kV+&j$eGwkZ{7z$Z!MB!+MI@-O-4uN-m|S zzlv;+SP53KHqsZm>pq*nR3L;@ZerC?;_jmiQw!xp+eBz(mXse7P@Wg(@zX2*`&x4JKK5DhYP3~%N~wH+EiB_U7gt|T|bQtd07}Oz7GFQiIUKm#zp!U{GoNR z+NZZyAycTDvy~W_sLnu8vJR>dlahkUIoB}jnKg7ASq1MwKS4>NY~kx&PNEn4gaPD` z_%sVdtJ|{7pv@QC;fu)=V8`sX?Udhxwa^30ueo}DNY^nbZ%I_+rgKKHdCu{pjyV9} z!8v&53`nBV*g7SmA&e@f?8)Atv#k{9GY>w|-ytAf(DzUTd`K2o3@TIKL^H(0DUwX3 zTp4nulJb7=`HqJh&kVx(FIVE(k`ZKEUfNqT&3^AW`gu<5G2brYzq}Qogymr7?^o9X zX%gtv_KEltP}Z1qLd=C5taWEOJb9n9WzY<3N`hVm@d&uy^9bP2uQ(*dMXxdb#T%?U z>`us%zihYrT8C(wx2jwwvD2Y#dw+pnX}AglYu~Z+!5=84j32R%h zm5B_FVl}A)J0#^YKD5{m^XMM73S&fnB)k=L6VcliC*rg?ew7KT`;(`Dy&cMcDkIx1 zQ7WHS2AbP>Kc2$YB_T8YhJ>KC{;V+=E-`JfjD*=JPpCUC+o<+-cqW*TPH1Qq^XjaF zWD>Gjwv3`vYRd9HXxanXkObzBHJpq{%h+1VKHec}I1bSY`+Q2mVWOztHXEtMmUh_C z;RypXBha#F+&&6J=p4CL896wq>+_VCF*=-E(Ud2OqEom~?5lb`cC%?;Nz34!M1ST& zt(W%Y*NxXn#;>pGo8V>e!{+RgD%Y(FusIHT2BU?>nu75J%R2?e7?`yjc~5)`hH*%} zp$$egi0+0+_!j$MkFT_up7nwA8kMCS4mHhly;rq-o+2s|xh5#HN2=R$(B}IrYHZtb zvi!NmfUZJ!_p-|D!sx#9O7&=n4X>a z^!K4!4?%a1{f4-Hdq;`YsImeQ@$D|#2#>u!CkKb=>PUl!0}AW1TX?-lW;$c0)qspj zAWIV4=O7^Ac-qlT5it1td2~C^R}lM(q+z(Rd!Jg3tpTBkTDBBQ^9ThF0U{g;od{d& ziXU~ew~P#Tk*J`Us+fW_q(4R^n4KnmYjME~S5;XBq(L^iP~&u}WSQyS167LJhLqa- z*2$z9Mm~X7hZaS)TNb*{fY+J|wj3sBspUFVVTn#s%NZcwyvJ&cS=(o+rVhC@XtA|G(7BcC2*2XG&RM|=;fzrH!aWfcymH+v4X zc~2ySjcNmmEY?5D#4q99OC-?E^);)wgAp`4Ys9s2$U^Y;@_^{gI zW0Faikh;NJsjZ7cn6u*lLa`H^-FZ&aRS97~v^ssgQFE9qys0xyKYtl~1{IMvgY6gg z7F^=*iC@^4{0LYH#nHQZ!|$p6CY9gg=1OenLmhfg?@X1YtQ94fKsf#B?(!pd+-h@3 z&UaE+9Qw=m4mUp!VwD1ksr{Z+xL6nlEu4*9R^X8}C`1R|VA=*?7Bp9He$yEuh<3A3 zA^^$|)!*eH8&TN%2tX+kL5gGGA!8`UgaQE*7rVkc4)Q(S5hzilQG}%_Pvc2Tfn2UX zFr@E`Q+qAL(~#pgXqx~H;jbdcWP(Y=4miPX7@fpG>Rr3Qjy(BLy@JK5bUAHujlTkM z#LPmK!;-(Xnjt_w2f!gmLs(vMX|;68C70H4(zSwz`e;)SkvbKV{6KD=FVJ#ts=S4x z*ErCTORQrJZ8+3Mt3YM+O0YlLXYlG=zXn`pZs#3=AV z)0rVl9R)C^J#oUAC+E83)M6q$LOuYzH@*ovU#Lf#a1}HFOJgrk29;9j*p^@3-F2gA zp4Lf4vV0(;U^B|OdSL3_lqTja_+Ib5=*JG`F0Pq2WqJ)ze8oYk$8%w6CDoRv>)98NC{1*e4q5lE=cW~j9I`_&{K^90)%I~NRu zw`op&x*M8tX)eD%9}cd-0BTINw`7SdJWs`}bRQbF`Q^FoaI>a8&w-lw#M*f+??rxm zcQ|6b)STIJED|KTO4*BOX2Q*Z&R0y2F^e#ZZ}wIyI}U{vJvrjp3(rH!fCF--ZIJ3z z^OK;D4@$r>@2GstqSOn7j?M?xdSUFE$gP2`4>_b?I^q`z{2m=8Y zHWoRam}hQtOLJ<=&-2mEX}Tw<3JY73f}O?P(`IP>`k*>d4AaKog=a>JFS<_S*ti6m zV7wHTxHWC-(FlH0{LP6lqkFFU_1-Wia<`Fh9C!G|4YaQtmQ(YIEeb(sE^-HdpHnWf z6a}+{n7?@HEM0%wq;sQ>rxVXMTzrOhqa+{=&p7W=1Hq$)!FWVmcfC1$T_N;n7f^dW<(y>o2(eP74Svz^SNcd&9#0Czg%J(*knbxXD zrwfzPL3Y(Kpf8E7S@FX~!xdI{F^AIJ*ILU4WpDYIApIt<>ATw-9LErUj{^ z&Eq{FailRLpx0;;sFE|r$taIo8o3d3p1Q#j?CXVE{|5{_>*V0&Vtj zw|P_G4#V8HkT+H5_sxrXFPN%<-4CRO5q%mj{PR>)CnUE^9SN8 zl7M3ZZ$>dQ_%5npoux^BSe(4BNs$=uzz`zmv8{XlRGY04s{TSaXRI7n>9{)`Kd8;r z&^XMS#&CA960jMc$u|-bV}n-X1(nD6 zfjIR_nf0G#<13zUxo*W-=b@Uyeq9h3ZIvL+aLp~C9O5d=^|~4#7QgPp^MS+D+J&Iy3vj|w}NsIMxY6_SpK|<*>;Uz9LgZw^)_YxZCYsO z8|=lyb+Odlhg{&EFkt`Zr5TEB1y6EOsZ7bcWM2Pr-|Y`@$9eu`TBevH-(7`ny?Wpvuz7Z1cf9 zAjrC8;K#i*Znd6+X1*+a^?l#N^Y=o-eJTi2i`?}KRE#%pLRbO;2QmSOsSDjBCE+d4 zwj=D{%TU6=ojmkt@mtn!*Y-T!!ZBCr)^tWUvZ(x)D`E8bm5*G&QCAu+{zx1`4-U^`4t6r_6>O-4PeB^4BP<*EpAfBFE z7(-sjA{}m1!nEy?oOkwQ$Q1yc@zkWud+``@w5@FBYa{2*PVYXD76DC|ilG;d>2s1j zw693$21v5ScC3zGA-?$2xWJS@1z(t~hrawItCW%ec5JcFV zU^%gwo%Yha$36*Qqn=&{`bV&YSz*a&M~;y1cSfVXEkT5YAF>*k1ksZT9O1l0@l*}?_58rQptb2>6mi5w z!tKWgy2?31`0u!i9;EV`k;q*P>$Q^W8^vkIKhJD3*ayC;7}KohZ9rTh*9R1}0<6|V zrDv*J)FU%&Qq;2KCPAwoTJEmoi3}=|DnTD~7|>%4EMxf(%NVVD6i9HN-=CzC9hQSeq*QRUS3q~6ia zz-Cg*m6xo^fl67$FnxF&^y2=dG?EJ*DLPyd2bhElN{#tuxf1$j2PzHh--?| zcdpWrvK^M}2lG+{8pIr}0+{;K6~=Y=b$VByL3-8Abc!|Z``-HzYG6X7u+P!Mm?4iVHz03;JWnU2j~kzuX1v{u1d$6J^?{Vr-3vD4x;5uB z&VUle|E2@kPeIw_l(NSE7I9+(BZL$E2>TryyvP>!>mTEW&b$zOod6pBqQu0&FvoA7MObOZox-Ea|S_$ z^JPn$1gssITa1P?`cn{#LcPn8@DsZWvMQgTt|@|(nt+NaOdi^5>x8$VJNT)yx+#(%7dg6Apl0zEoDAi^um ze;M*cZ(ymlI<0jaBl;ewjkVCcWx7!iAiAqY>0wAULnixH6{G!>0s%++#(`nsXaZOq zmUr4-uSDBkpzh&d2c=99E5IH$SBk?fJ_aHeOz(odZN74;JtC%yk_(%fGA(GKn zqxnn@R&!Tk!PT5!l#33)Xs@arTj3s$I-}=4wWYtFy&gz$QdPcy!6pBm&aTeF0mNu8 zdC5dr#aUb56DlFbpFy3q40}|UP(h{#ayzf#XqSu@<`inyPSI^fBT)|Y8{SgX`M9Te z%qrsNT_-t%3>}9n!m@xcn@qkE`Ji=JLpCc-Na#_wBAaCP2qwReadm20BH6hbu>@e7 zQdhQ629P32en*(Q*J>1ppb~0RHSy`DY}3~s`{ix>V>S-lvEe$J8KD}~%1BX9Li5|gL-=^}G8%_LUzWs>-fqan*aH0ol$I19kMOQh#y-| zcbT~~5(36gzAej$=jfNA-f!@Twheqx8~81uDZCek!%DV#0foxuE6HFCDwN3q$Xo>+W5I*&#PDvQq@- zKiM^63)|sEu%(YjLpF`-BIIEuely((OW;1;>V$9>bFL7LVtM4cta2X$Cj(bF*?TP+6o!IQ?O;E2}NM|0XajSYM#Pi@r zZX}FUE53&|vpzjy16Iy7;D`+dJz-?cabhThgE)k!^989opT{R7>5L}~QUd+#Qycy( zT&hmNMa#e!N_VSFwn>$A%KJM%;w#!)?mwi&CDNR^u_L2s!phkL-zSYLz40Q{uHiTk z(7H?IBw{}X$kfKhlS~w8X!Xfw+&|fEqY3@%;)Lh?qC}onFq&7WqOh{=Cxj8D{Zi`6 zR2plb|FQfhy-T?t%Te7zEKGk5prS$YsQ>+)c=&0^vxp>4W*~=iFH}E|GGc!G&n6m9 z{?7vr&j~tyCV+&mJlJZ`>hbu9yMB~O*vS7VORMr|LF&Rb|tP^s>j-Ng`Nyjt{43N+v6xi+^&J+B^p{q zn#CJQ9DiNMt0<8Fu|NNu9;H?*^OY1|vFbj zuD(`9XZm>%*oiibnDGAIt;N9q?pAO{Z$f_W8hu=5*2&E!XG(nh-wPc<|6c>K&R2^x zQ)x_o!Og7|@kAlN?*Gr@08jfs0(H3APHIe8&kJDAN5a}q0|sU!pP7I6n`VWXAgU{0 z>#w6bbh^^(#5)p)`m;nNsU*#ZC6D_%7p2r^x+RUdpx+|(5gtu)gU5dFN(a*VX;Eb27ivF@1aqW^QLG$a3AYDC)W`3mlr4meI)XPESymd{Sr3jcko5KhPn`QKab zJK{T4A0o%FwFz?@Kxk#D&3t@3ArGwPUXJ*2f36$5TolXd*~t{tZTqBZb>rc||8p2I z{PC1OP=R)#)4<;xK-hcehV%CWP!d$M{-8uquK)LJevPm2UqUr}`jUT{p55n)U&%&q zvC~*rTj35-(jaj&HhbRbfq3=lg3sxRXyiX!0s4QI>{W9hgJuEq3wn1y-nWL!>x}7jElV*PM%%CwI=q?d#c>nzw*Z=;RR1Enuz+U)v7cd)riAyCG`Y7Q1_nz*vwCKNA zcYtdU9QmNZFa7uI`dZN_Bx{4DiS2Dman|=TNa5duUM>sd94s_3g~yS`kI^glUY^|d z1F7u>$TtQby0e>fC)YZy%5_Im$WPkN&{_@&;IeqTI^h2^#$(H#m+gI0+t^lv>PZ?B zzWFz$OhLanugkAp&Rfg=4-IlVqlx5yd>Lk-u#Eh2hw*F+*ur{&tifz^Xb+T85P@C) zE&gqFE!Zu~uhPpb_pScD2;3(?*P07N-#uWR{5ua^Avp<|K5aL3U(LakA{PuxX3>%r zV)|VNmWm&2#Wk^(9{anmXP|afwo6b+_x)+Q37g3j)W=0K7l&;GM1N;Tyb---$#g&a zzl*4dl|~p;;PKlfy#Fxsm{fsw+$HF)jnBgG2s4v zw5U>V5K#MKTcC1BxSwcDBz_|NovX%ajs#fdh!Rg8&via+u9uQAUAr>hu5#5Op8Rvm zBPfQm1(R^7!(>1mb7LS(aU(x2ktpkYdqO^vl=p3@Mgh~q%Put3PvEEh%G;HT?id^A zy;+UNCe%7#@e_HRaBIr{(jR23zPM~o9{>JM@&HjF<@J@xlZttYLp@XQlOO@~DDMSA zU9;|zH_gmX|DLkw6YbyEg1{-;eb4tQmP~N0OFt4VUj%KPp_oob`eN^Av zPh>pl3C^LQS0+#TrA}Y8{zmcrw^IrBe-F{)o00tN@+gr%U&>TqFH#etP>2F^>(kq- zJ4#EX>{E}M-h>dfa)0W+O#19&_gHYNK$=|Jda?+tG`nXK|2>Y60V_n0+g_V#s$AdA zd$H0~`emNj-=}z8{Dnve0C>oq5G?f%=ZE)AvXJaSv6}tQyeGL^2d+ZgTN%Y^8rJ(ybqP$$)qHvPZ7m z-kN84!0U_=GS_~uJtsy%(FCKRL?V$dHAk~hF@eubk9QA5a79J~Hm#rNifR74LqBTu z5)*pn50e>Oo70Wq<|@o68kPS&3$dFDU&I+tkdJBgn*&0o>XF4p?I$H>zUZ|6@pMbm z8qKFt>z&oZ`lwm$tTf%=Pcd0$wy;EgxjUv+A9)0!3FAS~*g1o_(k%c;pm)2S9>=VX zRch%q`xAAfUqs-BV(;PyBEEQz|KXiC-RYfetPy`bGkYDGk010&dLy_>;B)c zhfX$hn)?cf5aeyhk*D*8Pa-;ZekJAN14(-;bS=ueg70z_mAo2ON5i9|*Tr3KJAZ(> ziGJGERtM0?X(=l+GIgZ5yS^>ipP-E2o)KKU80 z%w*U_xWSZx1G#@8Tg6?hB30xgJt_{>uJz@ifJ%kDi15qtn|cSzFb^v6h_7uL*Tqn9 zsKg)Uj@pLto%psahCYWQQ=&KLU^X*V}A z%|5Ze$pQxeRb=%0OyOTTqEPE(qC-=gB~lv1z+QnR?6E29(&*WXN{@{^bb@{rgdYCM z#e(M(g%F&xmBOiiQZ=yEJVx+Cnfmg!ORgTy`s>T58fl4zb9~<6cI1Czei{hQG|vI$ z%PeyDm@Lfzj~~Iq?PYq{+<*JmE+RN8u8<$5Ah@eg$RzMtJCjTclXAH}0#^CO4)fy> zcz+A8tCYw$_`z)=_UL8y<(8fwZibT+b-YS9Ekc?2ICd6L;mpiM{}aum9kTKy4{_c`cpm)=Gfb16 zl8IO-WfTpm&^t)9`SV@7(q$p2A&6K5q~(l}*@Gca?mMPQUhH@iruZ|6^TAAVr?C%q#(Z_^KAEc{45AM2KTENG6n(M4FuSo4COABHESk%+tur=j%eYQQySc6rk266= zsFfnLsZdnYnEdX@@Hn5JAsE|Hxn5!RpxD@E^|$VD@sTB+Y(YrL>%P`yL$`%pMuNu$ zUi)NT={63}STqrvCdm@S>5gmcE{;TvPUIz1kbirT8RX}MBn-;m z($Zw=3_*GT-RI^Cn9&;@7aM4@@csU_0 zb6u=>mKhl5J@|;|GE&<8bP@Mo|5XzX@^MKgzG{Ss#jt5xjMc0`w%o&IzsDU{I#r~z z%D&0CGb0gKC2^rEUO5pcBcT8Vh<~oq86il#8ZbFv-mg5x8TBw`!(Qz&9Nyg5=(p@q z4WpL&U(CUN%98~48gycOhoT6V6%I>vW{^#VrzGAB=d_-FdNRIat^$7aokeb=Mlh2; zGlEe=j-Y@-s33S~HQ=#nUwicJII+Q;CheiUjd6uUNN5xjJvK&?`EK2#*K_346UL)e zRgFZAX@(WLYV+Tf_#rU3MsX!KE%eKE9DMbu{VHc~vi=h;oBgDR8dak6CoBr^Tf)ii zL4YtEMM|#okZ^%nqs%O1Vm6S5RC=7%Ze3}O%ZDsqO-BBU_}Xp_Rvm$d$agm9!xDc; z+k{%9>yi%Ffv=OLNn1<+aCbx?(IDtfqA9hi(62@%YsE!c-w)UR?xR)0)N;CEBMNf9v2(Ybnm?HaL`A@3@G%$#}tyZ;>l)7L+_d zf)w%&Ww(8)kH+AvmY05F%jKuoDrX^;ep3g%pzi6h52d@;jcLpd#Zv>6l0E0S0aH!mLJzs`hJRU~MxO`iVsk zaTAezpQ$aLiiM8;{^fNMtMBw)$p!gR;2}`MoA$Q#kiHluNE8Ih+kjJ-<7r3t-}^Lq|L_M%>0v;-{AS@* zzUx^sI)8!9Sn05l_s#uee<0>AOB0V22a|aUFvp2R zSZv!)y6mmD!t^|V|DJ>Hk_+~m%CXB`ZrQZVImvh$4LfOez z;E^9JE_!}**RIKtk_-7I-A5C}e^T(m4&@HLJ~K}cuO6CJ*)wl*g8IV zvouxp?FhLXb_2%OYrv_X?Zcde ztExC6n1qD114ag=#0Q(*RkcdT&9LN?1<=!gDU;CLeW$rLT>pEFu{K6IikV@L&H7b^ z%?svw)1iKnu|#;T2nCZ!iAOV0fKW|YIwa^j$6)G#`%2aCNz@xNyd_K zlwhzHJ>iBT0fP-cC&W>_c&%3CSi3r0BheO>y|}?y#z*qBLI(}^P>B46eJ9)A^Hj+g zR{OJHta=Zc8ygcf3L^8*e9&5f*@kd58`Os~C=9qS67{}1Z7`c5UGN>V1)yi@%oJ)7@-Rm zVkm(p9%;r-Wj#%~9DfV%H2gO;2G04GBYg_bM!v}yKOr!n8{s|zPf<3EN`e|;djhT* zg2N|Z%0P}F>mc2_*8)U2$a2%4g0HImR-W~?fSHdN_=rSI5hU}xf}t&?LLUq) zX@`;pF*C4URwJWNVG3~Bd~#7eFR?kVqgJ`>lF*{;ZpgvPr}$#KG$KJQb*uzXU6#E& zF1@CrS-(L7Dq;V(9x$Hp$Hnk*eqWfWt?aJW5<84NF{fHr{>o1xRl-zhN zI3biVd_;x>P9Obt4_K83Z_H3>kIEHD~fi9HDkE!uGA*1=%VWq>R}fXP^e?%~E0@ z;I_{(=ny@cLeYIIlEd@JqHROETLK0iunN4RB&CW>u6K{ZZO3Z|MWX@e-`kY(9gJlFWh zHHm(E1VT5=_hA+Vrt2Cu+S7U3KJG&-5@l{d4vP^X_)7}N3DKOX+#Yy4U$=cPJsx2b zgseB~|50Rjd4OSV-0ar$?qKFoIt*vHKE;ArUMUqfh_I~kAnAQ{6MC|YvO~T-f~yNq zM2mx?k{BdgpA&*D?#?uy(F2Pr0*aACZtY?t#+b*ZVdw;`s_JFO>OSN6EYmayvCD_E z<#y5hus6C{HI;7Dwq0V z5NBauBY%bTEe6Ye24Fa_9?KPImIXm_vF-YpSgtB$7+Se$NSaZZ5Y%y~Yy;ks0gNyu zeq|Y70JcXaI^knck)Zp@)3VH3Rg&Q1!@OiJ8dA6IQ1K+cNamY-7iVG<9FPV8 z8A^A@us5IXXR9z9b}r*0^2X&orIhKYb~&I*^@#Hsc9>jl1}m5E3E*?vMacdb;zD znSC>6lU++lvV4`T>579N)@>qXSZir6d!)0l^4cW0K~kIbN>tJ>gcO*QO5H-u4VZdw zMWR}sqwGn<6yf%tP5JP+4B8G?>c_#!tVOSqiE!)OHp23|GhAVvim?14F#ct2frO!4 zoTptw4~9C5#3b6Sd2jK%7XPvtb;xSa8Fj@k$v-rD75VAO#vO2bES~MQ0RBQbPZLnscsUKm5MJD2WnIy;rHn+%XWoD5lg@JxQPJz+6y78& zW9je5_wL=|@z*!oU7=T$VPY{mP zN6_VSGaqey0bGOWkCCzOj(BZ-FRqe^-iS2j0DHLi2xgXAg_S3K{WIl!Z3?JSqhSJH zE5h1-?W@fkJv}wMeRudv4uSgWQki@v@u>miMY|Q{cEIhFqBg)gq9rG$k5cfbP(bB< z5y2PDQ9jTxCPc%c#5-Qj&R28{f~!OR6aY@S)$|-anF=&>pe2|4Jo>ONlL8E?6v?>3 zCxu2tAbkfuCD(TH>Dn*lEsLC(!{SZuq&$41~ zJw&NW+tXDwg}IRdhTe=xOtEODcRMbN+oRPV@?;qF@MtEWoY%63b& zR`@~{=<)neY;w!YK`)14!-!KjMYBN46hx~OM|-5QLH@r|gd+iZ0Miv0Lm*wz^PmJQ z!Cnl7Wg?m(tuPC<7pv_eChit`^_)M8_CFjQNc}#r4P|$~TyYpJdlg|b#~@ZbKiRUU zFSq<$CP*5B-PH$5ZB+Q^fP2Oan%?cv%0T$D)y{^^9Ckxp-nv#b!-O(`Z&Kc~E6@%t1jXOIP@ zVIv!LY!~iuACnT$h%E{c=@H*9iiI4z9c)V!k^8zn*X=!Nt#myo65oA~&!u{s?K&|& z*`orBpZ2$Wv*Y>P)KLS$1*X!LFChb1P6+eXrGf0|@j}R^+9o|Viatt}m=8(g%81^2 zadCr4$bt<1GNrf<3_Corox)TeaZWb!oJ(^19N0YgU z>>Nzion7|xGCmE9!&#sA%<6mf0f2>VE$E{m`Zhm+ZrTFFks{o$oizR`+lB?yn*98| z;wzKi_@SDZloC;rFn@v>k)-Tz&T!j(2*79Ys?%33upG+Eh0;I-r~$7|NDSW=4wHP` z!ACW^ZVR>M9H1YAXJA4j!0Hj-*d?(&m`fB!+3G^Uj;giT_-+Bi-~2Hk`YKJsa1ylx zAUj}&HO=?@Ydohl0C_RFW%?kN8-8t=4Ym{r=SoOu?(Y!0e+&wa%!HtGR2|&7B^@~h z0v`Eldkc@|y^YCyJ~m_by8Wqu%^8nK02(sW-UUrPuRWM2ihP7+?G3)SS=YKLFx{d0 z*Q(+;KMt(CEHeGB)>y06PwPeQ(-1)>s;AA6pj!j zKlm5xg4^s*tUxYw6X3_RVt14pautj^As=4%MS8cR?Z4&bC#PUXyxN?+d}M1HcGM^A zPJC7khCy|#c3U?p-Tw6+6}d-j3JEg=YnL{RssUD)}r3JQKX(3}s=>&J$H(FI^(CM~`pLTonn}r~A%IK#+9j z{$Dl$n28t<%+!GD)7m6a!nxHWLNl1x|2xk@Yj0rqmvK-^`C~Vxxz~)aTAq?~xy=KzpePtd zCG(~0Y@_a9kz=bo^jQmSPij*EI_h072jkNa{%EG?7&dAN=uxf(4df|3pYPP6CFZt^ z1l#CV7?U?!K1tJ#cYC=iGJEwoMSSB!_h*=u;QGJ4J|LXHvwMp$f$zD<(tt~nI@Wjl z8Z7?X%Dzo}fJRhppU&iXGmTvw0?Bgs{6-Q^@t)j$tXb|4efSI+W(G|a&z3Mi%WhfTG=|467G^qTrEM~S- zMRufQjS|sTdt)rqUZF$7J@Aja; zgb=cui2@ffviK@Xleuyk2zLQP^oVjhI*l>`Ecx`BPi~>CUPAy~rhIx7{eYQ|=?vQ@ z`qf5d-@k7|tnWVd^l36i#4yL$!a7O$st>uw9TBZK0Mm}k7c0BL(#dF$36Z7RAf0LZ49WgjB?A)uul0aWSW_sM$?fZN!ho)Bz0_ zd7r)?ULbz&{u|s?WsPE^T!m5D)8m7=ZvN;ht-(74`RXG{-@y=eVsg*#XsqT1zvaYb zD-82`QNife&@N-q(FQT=Z1b-$S~&#c8_W+)xpHn_dL~H}^kUNo` zaf1>|taFeffTs6lX760Tli<{R;7XC}F!r39#eAph%Ak~3C~N7VN7<_TsI0@rh^`~w zv&6OW8VOc|j)!7=;)H2M=ebA7o$L98=N$xC1uzu1HVk8}7W&l8SsN<(>eM(*!EyxW zC+D-92R#)wb4B2^!E=}RTVlRe4Pn|5kK3+d)WZatH%3y$Pv8A+;*96BOWE3@)A|yB z2=0CRVRZszKoLs?+b_PEYNNiU(k)Q-iymx6dMRFX&$gy8f5kN|2wrz-ALn*RvkFZq z(mf$uVQF~FXTMs2PSXDZ8RKy2Z20>{6YSuTKv*m!-%oiLhWj#3S@WxJ{WR$}?-4!y z#PmJrD_>Y2H3#7gbzKSMv_~LoS5KV3 z-e4#-8`@L;uW4@$#lG_H`iX|P>uZw9gYTX-VXTolCILxs?%+bI6bFu`r5Q!P7Twk2kCNcdu~ z0i^+9l76#4#T`-}E9b6Ka}^keaRV~xi<=^gLX6d@Jw`H|@nyF;8BwZqn(^%ojftXmM^zC?ke>#Bk|pB_^}P5v%x-Af%`Hc^hmjkKBWN_hikFAZ^z@1z;z?1W7up=~AU-jqP3gG`A1^GG zP^qKQjgia-`Z$h!mC2LCc3P){t#ml*MWK1mI9Y1O4BKuQ65#W2gRO_4?nuTRRs$^s zffz8j9o-3q@~~~oPy!_9I9p2X%5Bdt!%I&9+;*8Su$8UdX=DAW?PWoQlx@`TjlEmEjXnC*0-f0DAyFv$0kP8GmsyNdZK%z$DklY~0H(I%VX8-KuErCu+{?zax5;(e zev@C|t^W1wi2zD~uns1~-x3$J`yv|r`TsYi13Tq*4}T#3 zLZ-y!g8#B9@wh_5XX?>3INBhxVqn&(6hxmVV&0fCKk@%ZzO-9*C4(kyi#O7tb+d>Z(NpG_MhBJB-j5#hL$s-aM>J`bgZ$)el70;s?2&rBxI|B#K175usAw;@T&xGlKkV1HeRs0w#wyu*GXXdSg3)*( z$mqo;Px;&ac-9y;ZtPJUL=~?ZrRB$4&?-(-Tq?rm`*wDN9x*5c zs6P@}il&h9p6}Ub&u(Z{?`{5%?x?|%l&zRccXqOuf6U>Y!{cZ*xsHE&HtNk{GxgIc zQ$088zYDpzQ$RkP1)xw9jD36}Zq%JX4R)5H2Cedr~0xBkDU z75fpdtKIjuISisRx7k=H_D_6}oFuKJ-bEe^u7V9Se=`>T?8iOHfA_!OLx0^Cs@Jr|jK+aUIE&`;*E9@3|Hx`!#SarmV-WBm zF_`eVx078wAYEcg;^V!b(Jx6d1`Qt7Qw84r!_w?!M*xbvYF=Fi5wtrd zY?$sdK*F{;8kFL4*)4fBUL?k0Ir4yj%`gfyyb=lD4&GIZUVQlt2BcfKQHZ|a)e{%d zIeKBedbHCaB`I3R=GR#a|aa$R@JzU|Q`)agI zErH*I8q(_MdB)nv4pQUN1cee4mV?>wZiB6~P4Qn}E@ysu8zKrBKnJ2}J8RPn z`zOSb>;|vfL{FjtzEQ2?x8E-4h-Xo&J*AjnHVPZL4*?no3y+aX5o**Aw#9*6=K1r6kY5YKlW zVQ>I%{a>bxsB^NE@)@ZlB6t2LK%k$hv73S^Sz<8B$nZ~6BIR>mm>Y!XXlPaKLs3LG0+e{7mv&}p1MS=1WjMUOKsnz=TWFq21^BacgJkF^IwdkN;ob4TJ2o9cFSFY$VjV{g3}{ zjJ^mUPiy)|JW7w#XI~aHs~oifE13FUW=9TuHowOfr$8b;EPgd!B?m2H>R+$0vD9Ih zTV@z0U*uD}x@}Xn%gi(sc@bVk3!J*?s+(b9k95gBn&I%FS{cfX!cL6-fAVh_qa$j6 zTq*@Vi44ub#Bv@@-l_ksbTe32d1~3U_kY)^9B!|JU-Mu@j@ld1<9#?E|#>ksCb~?g+5oaN%GO4I~2ynSkM9P@bCW$1z7%)St|LAxb({Zd&Q`qkxfE?3yH{O zDxG&eKtoDweXN!WaXJCZQ5mm5Wh(k`~d zW_n+aPH_<@ag20|_i$(F%YET5-I@@BZnWv}FA z=b5jLjOP-S1%l=nmxZpY^Ig1qH5*Pc zIV#= zTn`>xq42%a&~)2e_Nx`k@n8Krk6-sbLrTt&iF;dz*To_l=MO)C)=4}9eX-$@{q`eB zK4gwKbxytTc?=@D?1D+SynSvNHCNV{vKdi4_YFXukS2#;Db)_IQ*FWXhu`vEnZ38$N!^d|81wBRMIJ8nq;txgjjJ*93T#k5%Y zCOUdUxXA{?aJRgDs`R+8oL4ttWVYh__stkNS~Xr*O%S&vu61>BdYvkjhojt&_sR+M zv+)TCQWY~Ji0Gs*2I(yc35ic}8s9~mw{Bh`Bl~d^4NbgY&vj}ecrrug z8Q@PnC2A7KHCB0ORxj-5w^6-)e3D;Pq7}Z*kQ_!+J?NI_@mRn9q8S}^knj9sW8@`a z0=9xepK5fC!1h{;U`?CJ#?d)ir~$GaB&2eRJLL;9uC zyQ_JHuNk5>UfX#r?B+TmCAsV_Vy_fISL%V*QzD*!hxrjC@KCWDJ2BldB&7@Y?kIFp zdMEyz>}D1T3Q8{YUV@|$=@#W&rS534b$n_wTo`Mj;jr)CnSrTZ%7R~qj-{IT*J-a(mDh%AU+?J+t+n4_b#?uQMMy}f zuV!N6w_H(XcEXO`_lA_zk=MyPAn|r z$~@HBeL~1#mSy(DBOpBz7tRpOg{Pi^p38i0&ZEp`M2JV8j^TVu0Nlk0^_$Tf!@(?P z)~cb2Rt~N@56)UL{U%9#b|coD+}zUkG=&6;3?fLAyrE7=(LQPNA7naze7wN&^hLwX z{?nYtpWT3tDln8kiI0SKPr04S#C5lWu4JRVF*9F;wpY-zdq#mML;D(Ww$vn&f2z)X>d2J?TYSk#^5 z>{Yn0pKN6YuX-@Lmi*UW{gWe zo^Nx!8l?zVd*F1pH0aOpA;E7G!8ICq zltz{kre~0~i$~LCh}XO2xgOj~0k5K;4GoH&mgJu6Ee1??>BMMCld1MCK65Y@8CWZ~ z2a)pTuw7yN_a&ak{Vs5Ng}-!2CysMmyq4{IZ==)-*$gBT)Uf39pITJ?Hb$XA{L>+J zP!Pe?998?$gRas_Le^-+L#pYAn?xDBCAU36Y+>#i*72a;xKM&n(?8de(<#Gl*W zy?bYUHbHFAd1#1J3wr}LIkku9v%i0TXLdTnaI8nt2|Q?)8^$AcX?13)&Hks)yxw79 zW80-z{mOi!f?Gkt<@x|P=)o|FW%_UOiCJ$0#LpC0pEg>hs+@@tVZ>+CeT1dK+P6y* z{F&2f8Xr4cG7TFqM%HjGR!OzUgzwt*>w4&Pnw5HV06V4Tw!T1oPec#l6Vsr#Pb?q3 z?y%aa5Fc31Qf%5UtS>a1vtdV_it1cZk}`5b@Wtqnuq$^G^KQ zQT<(=IYH+?{(W7`V>z>J5OHT%1SL0K06N`MsBp!oGv4#$h&P1aLr_&D)=&0YWAE^7 zG&F(iK^>akN50-SJSu`mLI%kabx4-Omom5(0Q4R zr6HlYfAWk+EGpXM;gJ#DzdgS=-yC{7zq0szuWpCP3z&R*D^a(zjNU-O+mqvDUQ?>u z6Czj06i(QGoA9yR6T}-&p(J=4HRTP12EK#C+mzFiSJjeHMAFs8?{B}~*|XHsdhzsM z9@kg_)HL_Gop18-O%xOtzb}?hMknUD=d?91QLuad@|MDQtFx<2E9P7L3uiLOAN;x9 z(b=ga@)&XVayA*|SGLch7=Mmem;Wl$a#8B*8X0-ow&izcCn`1i$G-e728QZxT`8#+ zv%(h`P`%QPdN_f28A3YZMahT*-cKG4_6@M6y}w9lAVSQc^R4sCH(;QBgsGC}m?G5L zJF<{=+yv^1m!CO4(|b&5r$h*jiXwhWgaHj1`kgy3nj~X(O3x#EiopuNRQRm>Di1&L zOB}^_#F67*KIJqJu#&}ePh>%b5XWx9eg4T|udY0;HpkH1{q{A#6LG|2QE9|+dSJ8ZjW5#8I(kCUk_9fW03sL@WsSNeETmC zCM}-e!`rb*s)g9gn{YgB{`tWh@lZ*GS2~>fHoZ!o@6p~mVMRLP(uKrIkhjOV%jK$U z)jATN+!`%=Ex)s}y1H80PC-tNNqFzWpCo}2lVN3@&IgDGB5vdAmRL_^uDqt-ErF-| z`gIU!56u#b3&n!ro(R7BT>Ma;D>b&Q*VfL!J?HD|Dgu@(Qi#9L{BR>$XaC5YG?Cvy zJ#y?33(ElC!otEwAhHqu-*@ub?yIV*tN$DuyC(89>ctg^k0P?NA+@mrh@TNbSqW=E zi@Q84jOfRYAD}^h6_FP3_wS?C(P|>K__Ovf>htRif*=H~>ik-c^N)_mDK$C3^}Vwe zoWNW4wbvGWB%eiQvG=oPBJrBfqw_c&SI$=!*3$fR*zMFN$)+FI zU35)NeE_M+Bt2`_1+dBg*A+VvFa1N#}+x9oPFcP zO96-?TItK+x@k7k<423A3K{qLQk!j5)gj`uKE7$xeMmet zHD&fJa-rKTq;VJtD)4uak?gLCN8f+3TyInPnwC#}@$Z~u0L^U0$^H+)ZP)Ke*U0s{oxzYGBTJbCz9YcYQ z&D-1CB-&%3tftL-qQE`y^0_0PJw!Z~E5!Hiq2Yv|VZ+g!1OBcIYx-)+RgzqqHPe}w zSA}>OSaRa%0^CoH5P8_z%&TE7i1jt}GR zR0??h(s98Jb=Aw*Jsrkilg5wBP{Tp=u=!= zcEEXpoD$LX`!{~Ek@Sm^k|kLXk1OUX<48-Zp^ra2f7G)?Z);bbuwGZ%)c^d++Qi!G zd$Cj*kj(etsBU)4J38^Q*y3CS85v|-E2~FE zoYjlw|IY8GHO${16$toAuYo=A17I2JMa<%q^J2%aSL4c4a71)mo=g8PB)I^rEjVgV z+>>R;4K}dPkW!U$7j5A#<=GXpwH0!opT8T;CB`dSrOu42zsm?d-QV72=pE8S@{63c+CiqVRF@Ef&*`dyR zu4`CXl8Dz$tKOmYUldY~k1t9ZA0qCll7h=|(nLx%bdR)MdiChd?xgSQ^PFC;_ol!d z!XA{jp=j*Phc4^y&tBjqC+JkUme$4!o?oqNaZ0<2d^yO2bwPyW`}%zA%k`oPJhA+R zJUBS`=t=q8P`QpjZ-j(yJbUJag^Bs~a)qbgR4c0Znsf2iXGJWpE`I4$Uo!7If3oL) zM4;mnRbBmm{-1S;@w)Sr*Kk3Z^er~FH;Z4tZ=QBaNxrn)xB|6K%V!$WfIxyf=ULLl zuAqYe2Ktp?G}<~B8l2m0j8%I$cg;>WR#&|&M;xAkR(fZE^88CLz8>}JVtV_~OT<3k zUL=FDrD$ME=#`sG3|nama{J$fZfxN1;7_k){=IpW2olXE5c)rI?x5tXHW*MTM!bfL zU$m7dNqg(VKEN#1U)|5lYVZ|-1s%d>%oIVZMwlvsE&`-c^x{Wg`%ipe+8D3C1ko)%o~n2ia(u1 z&!4g==9u2JnKB1yZ3J>C={QOpdd{F%kj|c;^l_DeEpgvo%FnXjF5-b z`kAqsA&8XwzOleTzyFLGFU;r6BUhlNabrKfiH~rREmHbfWb-;l*tzI%C^6CHGAe=6N*(yx>eNA@vubyWO zbg}-m9INwDM3W^O-neHPUhoXaB<>mi9v;npxeX-|770m^a_(r#%Vcu16Ob&D!=C)A zP@Y^Hd$GhQbF_-FcXu&Vi=B~n*M~DjkYZzF&7xljT_OANv2J(mVW-3>e1&fBVD8D zRcUhE#hLxhT;RptSThl{@s#&2Hbt6;yL;{5y*(8wOgsPm5Q~k4g+1X7oO}0nh4Wz- zq(n&M6Z8N)*9fIl|15oP=C6X15}`gLM!AgF*YSxmei`GOuL5)?PR^V6?nU|UKGia~ z_E(F$>}UH*A)5FX8OvM7(K%n$vY9^IvwI&JDi^Vne!4v`wPHQrA-2!wfk8w>WY8H) z3=r*epibhXNo_l&eVc;Ge^%hw1#Yx2P8i?Jws>0B9uUG1OOe>eXP@=qKM1o`&WO6# z_S%Qq=-o~A=xB2+^{(iG1(HFTb*sg5xE6uc>*$mGu0X7I2wyujN^6`S~ z>^&&0rh%CbOPJsN^uYMlWbJimNB9!vFnY~cz(9D4a$H<|67 zqDl`ZB`3b+wF%cv7f2^Xb}B*s6BzW_-EZr(mpPHg8htQF+aJtevO_=Ub;a{5{dMZ7 z$ZWwhO^9;dA<{jm@+h?Lq=z}+H=uewnBJ8;7K=egy~oYXJu~T{Qvd6hT(#u2&c=_` zfYeR_LV^n8s`C?q8E79S7H`z9e5J0B=XOZgJ%BC_IZ*RIu^9)U89XuRxq{Fk2B)GV zWymD@0$u#&Oq0||(i22RlyW0UhJ^ov*??KoO+NFMe@fW{-$9i`@D`B183E`kj1&wKA#V_6{+O5`8yphCSax=F`~yuSShIQ&e~Lvw zZlZMM5R^Y7S8O$vbM{{tDl)5=;Dg^?FP{hKdX&T|wp?Jq9m&UgjL)DPfsAk9%~tV4 zjWTpl_4q_a4$z!QBLc5o&nVS0%AE}gnc52Tda*T5wczs@Kou^j@Z6%lw+-#!AD&Q^ zEyvSxr?nA7CBlZ`VPRf})kR%9ovMKK4z-y2bm#}uixLwO9;#W0TNv=&ZeQW9*ssyU{{{d#y=nhQTvlb7UgEn-D%f7*v*;P**hMAsxn-?eSXl6nd;vsV zx4{o9i{}Nn`K3GreCB;-rfI~&<6!pi+uBvOa!B2%+bUJH{^b`_fMFx-)a^glJe|r3QkVezB;`-97QQ8@n;{IMZR2AR#9Lzj zrEx{wsHN6v<|)16*ZV6^Sxeff%sJfHJrOUf<>Pt$F(+Vq zX{Jkl$9W4Msv4!8xa-%i%U0XFvC~auAQw<*)V;<9k#v@uW}1n$(gK$aa&}!J^DB$B z>9L^CpKGc-c9!Q}OcomlGZivrYgT%-b-2&l-J#Rk(5cE2u-r~81%D1&xo?13B!K44 zcWSuL*vhUmNayI(D~d68s&cPB`1R35&C%jO1}W%U{c7FZJ#zz{;_LcG%Qs%aAU%_z zGWK4zxCYg7`&|0{^dZYn9CqZ(1GzpB3-keWF&QbrA{na=BCXmL*fEQg%N6I^8Cgt! z2KOskmq3uT5z*SpzJ!!CJ}#l#$S)yRD33eahWo}QOyh~=%gG6?eL&^@_k1YBD^xa4 z>H8dkXQB$Aj&h#rm!beeY<|>vaVag-=^Ceh$ber|IL#0qpPT?D>f$eNGV$#;%ZoVz+&o=?O)6N!?>WYa>YR$Usod}ld`*Acv zUG%S7n~Lb)y}?{=(P$<{w7V_up;G=j`d4?Ocr^X)3s>r_{+w#N6%5IE9$Yvo6-G^Z zE2duPl$&g_+D(>On160(P|A7xn*Zc2|7pBWW}mxCH<+mbaI_e}qEAB(v3x(>*x}Xh z3M3%4H^UJS+Q@Zl#A6hrGnOOB;RrGy*xDLbs+8B9&Afg9*(61MXM|R@;L+;p+7#67 z4AIQGlM~}som0Lec6@wX$ma=x4Ndq->)MZZE}Sm&C0RADhyAEGkc2U&34c)6+_qWh z`Uo_aWP?`L0*4-6!SMw@l7>9(RE{VnwWQbJ|0sLaI+Fkl@>e*^aqjNdb}r4xnePYJ z%{Nzd=npk9cYPIOGSK@DK=KS^Wi7Gn46d%O{lf1p>nxzMcWL^h*fTVQ2ewW^aM;+s z?(Xis13Hanu$+{GL7?T9n!kS+JMe>8I7qLty{T&A4;e*Ih9!Tb79-AG z@q<7xothfiiTnM{=@!O)&bM-@&B?$G&7$6c9yg2q7^Q)AR}hSBb8oVQrL?_LkxV22 z=6O4x5y&T*>In))ee3{3q0115*`s} zQ0Rjh4tsAJi(E{PzgnBX;WTu2ly>4};yF{U^(<(D8dn zVUS-V*re)OZX~s3zWQBo734wn(AM}n6U?9WJ@;4|in>79QrUI}z0X=XgI1xHX);{o zgQf>OVSKIX)4LnpAfq%_bOFx&N^qXJDIJb#ZKCQ#ef@P1LN_!8#)ZH$Rtihitrx+6 zY*L`Eoi45fNf921bPcW`YBp?|D~tvlYnHVq(nzt1JWk#Su;K>`MG+@=w&8gU&HOBR zx>p$6erRCvSs95l(b*}pnQI&Jy;Z7JxPI$m8@{NBjXE=nucRNx`sP77%sg|5yNVP_j~X6ryW&xI+Zl$ElcwTH!S5 z07mQEpnQ;3%tY%r>+P-%TfKRs&^=!3b_=8rw;zM+1Yuvs*&Z7r>MyZhaU1?eK4vqg z>}doYD|UslF}N#O^43uE>w*9t+|# z9u8_OV;NAZZ#`mT8#?L^kr%o{`J#(83j4lRm1{7}$w8d9^a>+g(tE=GFpKZUDAZjX zuY|yUi9hpp7z^VMgh;TyzyGmzeyBX@q}vdd+u^q0T$@r!HxW^W9Q#*_@aFh{5ZbJy zqzCN1jcsl8ZInetbuB)W!waC;!{2a|$IE0sqk@dN_esqThS4cIakc&HzM=W^>Sd7v zvPY%BC0-J4$T`WHY%&FpyU(J&p5ZZQ%a2Q7iv`_({kzDC7s6Fa4)fLUQgh{>Lwj7y z{YU!-4tG7U{g{+<@UxV23C;f=fc*A%ar5)YmHqxd#hh>EORAU^sCP%Yf4x)ZKmPkJ zYjX^!K(y_!zkMK(2sufUEOd3IQHT#4+%M(3<7twmZat zmS6sayAc4)aaW1&-w$}$#dJ8rg9t!ajqmy$Yn>Z$6sQA*hl_Bbu`8#lrdHp)Kmh1U zFmSpu+kbA12yt${b$)GQeQYIq10H6+P47ZDYj$>tZVlV77`2Hjxdz6 zsJBGl!W@arBk=Ie8aknQ=9XXUenQva+)E z{`f|@WMiLqYu2AIqyqq@8Je!|^HS#4OVdcJ+8T*{%>LKAeL@cY1$QUlo+e9UUEd52IO0V6tN~I8GOs4ba2uz1^454W1#T zd{b~`d};Tk^0n)}4-XTZomLYD1qF>5A_|Mm>267R znFsBg2r8esC;9qoW4h~i)=r`<9o zLX_;)BmX?sk5*~$Et6&iEu;%Up%lo7gSEn84}*l?`D+cY^Y#@)yWZ!Y@y;c5nsT=u zYAY@L9$eRZ*bl?~yY#ZjPBN#__;9H3Y~)0kBEZ~*5H!9&!7AfdzK#bB0#k|%Rx33# z3_xXvpY;K(Jt!VL5IE8VLzPE*4WddoT6j{##u^%j=F2lObLB1QZiAJf}5fe)IUKVsHimu=`3j2g3MCB^SbQnjg(|&1xU>n z-IM3$EjwHaBjC0Aa|M?+4=Rob!?3HLsU+@!soeE9^Bq4x71Q6~3E;SrS|8}lg2PN1 zEylroF9LlPE->>5tX#++J(3;JIRkU{{%!%Gh^*^9of;*crKRj0opw6O(b?|6PcRER z$D9x-nj4fx&jKpEZ#Zz77@Wy76GGZrIQ6xkK?O(d3paIU+*2yWgq^3BawQ z+)Zee(4mc%s@!uiMU_X9V@dt?E(JMxASgYj7W$3F6UVSXH7o-9yM9xU5Dp7h4M~m& z-&xF8DZzp|ks{ewcmajt*fm<-R?0wNWze?0%#EDs%f+xmq=MTlDW5YbF}<=oe6%dD zMxmx4h%+Vh0GH)e^ePB(rP#E;s>qK>~?%Mbi?(2CDUF& zudbLxlLks@(=Hyax}7wG@s7pOzK!+L5IXoqG=tF)QqBQ^BH7Dtq;N7N60SWI6OOuw z(PMd~M`s5QRL{8GE;To8j(#8TfN3pM@bIBb8mlQJ0{z>*pdNyeXiYn1LsI&@<2bhH z(+l&Li#ZL%H1w_l*M<*8I6B25t!AtsN`Ab1O8GL42{8$$;n6~rk;<wh1 z-Q4)MMgaIU-|w?H9B?zN@vHo#2YI*s+C;*(1qecz?LTKK4_<;|A!wbZzQjhZxg5Xv zN*fRE$PY|LF2c<|nLO>l;^T*FN!+l5os90P3ZYY2gBg?sV73tNm&AHBR)gboNkc>V zOd0$V{jY)e&aQ{M_|MCXvS={C%;U_+=xBYV3$*L#Iyu={0+MwlA4`OQ-G}kw6R3UN zl%!9$p8xii+l~>uBGM#%XMIJ-qWd}1r49_alOp|I&;JrQv89yd<dd!n*C&hseV%!(wVib!Ex{%=ds`K zd)%KI-Hz|HeRvjZQ-X?WRZ8?H!7t0`n;>CiX7bf66*ZWOWxcpf2y)4bP^*OcEhgZ- zeLuUOLW}uK4^0M*QKk5cv}e~;law{Up*DBrF_iPIs){VgK{7=QPHrNGhMC*msN%_- zm3VX<&E;@_E`%QX`TO&ENj;&}WofAVeB0w4TB+ekB{QgIJ;8=br`dN4UOsdd1KUSX zpy(6!e^=cVE^#*uzGik(v95sFo!>mfjEAavN+*X_mvp<{lfiX9I@rEOFl|76OOhRZ zuEycd)VG$6Ot~`A+(j5;bA9g*iGcQ>KaLo8KF}8y1?*Ah|N7smPD&}|sCt}S^u)vs z8_LTKCJ;7HFMyGPB90MDR&qYqv1O5L#MgGqB)0`4hgUDw&`rSdZegfEDK7eAnVyql!Fyr}#BJ-`S0kDxg- zRrWQ3S9`$t8#EO#zNMEmvKBZc4#}mQtZhKQFwHIY{fjL{RqL)AY6=zDlY>O^$8%px z9UJKBrN6?tnLoh_Xh&*=St(>ceOaoMQ1fo>P4$y0HMHR1&~4b5<*S5tDr9vsk;i*` zd)aYc+HO%ZVR&K4lo?68usuHeCcc=Xt?EhjCG6-1cY)e6R3IXU-5ym^T+D9GirUNN z6ww?fcf*Hc>k3}frM>Dk9WR#{9sB;}{HwG!mP^oM^-MQZ+2NA-kKa)) zvV^K#-FhdRdi9Q?#7lx5UrX~pF`Qh zT^8%m^Vq?|!*evffC|i!KV&5{D$uM!WDrL5+qx4b{B`~M-AN*zsBY)Gv9+iqe?;Tt zgq4KkbmH}dL>;$(K!Zm%A;!W(OD=)&{$;QfxUYPGx|}-s6os-F_th`;8B;ms-zdLSTCuN?~rct>zCH7Eb%Lr!afs`(=@iUU`by#UZ7JuL$R%HI%> z<$4#aTJ>cO$`mXk&B>HLV=!l58>+qbhE7Qc0DLXeYo-xR=W3Xq?r6OtKw_?xX}VsF zo{Wsuak}6D*KwBHx0U1CDSpyVgHd^__c|4`Zcjm`OmgvgNo;KFDKLBGa67(8&h`rL zVa14}=^fNpR|nML(bwlTy>-nyzOT+B|4z2!oIvNAAkfA%w+}Hq&{s=@G0u#As(u2X z8Y#a87Wj9*2B=eWb!-$k6c6AR=SJ<&Mn)gcmjxqFGOC()XG_q-(+Br&OEQYkeITmU z-=4%SAc#qCb;4cmL=}d8)6>)Upb`qyUwX^Jq2EyQLEZNi7S#KTbC^4Nq?0gb>J8o^ zt*~}?3+>u)eSI^C7*UtrCpmMA6S!ngPtS{^M5uoih3{ToT;AHyNTm>aiXin#%}|8n zJABUwX}jR&hT30X`*etzICt(`!<5*iaNdvia&vP5^2A_e-S}j0jQpNkUUB?pG%ySR z+sf*KVTSNs<;-(DwyQAWa$BHik{a61r1Mws&t16ij8N|csAnNI6eUdi5*yJd2Z*?J zw?MLifBl%aSuDgWPW6?^a3vnJ?^<`%svFzyzW%RA<%hTO+G-C9B04JCpH8V6YGa!H zFOJvwOwgpHr4V2sch?}xR^>^>3s`~YXhJ9hEC@5Q{&vr!j9MwJ6cUv>UVVzu<;TXt znmgqah1<)qHi2_mTglVqeJb$v`BnsOFsdb^1nb{uKc|dvY_Qsoc%+>bZ7zoD`lscgOMc| z-GWs-w&~9@rBI=U%L{V9Z5WEx?zXLxc$4R2r=Yumnkc2nG5zV|kNm8W6t=w%-nLLQ^SCAQ+BmWEeLD{H zZn2DRBDYAEpa9(z-Ucvu)a)I%Vm23`+!`5T7Q!bc&PTxt;I(ke#ZmQd8J$)lf~)&i zIL!i|+cEtMHLIp!uDtCkclU>dMG|I7#Hdyu^*UN96h`=4L*fE2%yv(%Px&D*zmX94 zM_rNLloccU(@!>v1U3p1F9Fy%`@Fs2UJCmJDx~C6rD6eH#jbo6RRoK0Mtd$E_5;{i z8z9=?CrPlG<}JG(4;Jo{mz~-81@P8=|fY8n|~sj6~sF7gvW>~Hkrb<8o+@dwi_Ur5fbR%ao1TUdhXO!c$2 z^80M8URAMGU)Xmo@CC#T{j*&vbbl;(8=aQRi)E@LZERjMf?3R4b{857R^12UA5T9` z7p+<*`Bq_Rv1e>7XHXE|b&qJ3Iw2vU%S1#`GN%xiBt9V#%P9_jSH$yd(g7}Mv`if8 zIwgDa)z`g!($mkgU!5oABlS4v?dyGumi98}(U`^m-kg@(-l(ri)W2&%wYFYugKrd{ z-l>uwf_#GrrdsTz+S){|w(YO>n``LK2?sa4bg*6aQYBGa!X{;{GSp#Px zdtMg|<9o)sT}$2&^e0RG_>*mRT?J?|ld;n4;Hu`mx-nZ=R3tc%`=htef_3_99KYZ& z>Op0B0drhe*R8cW0Z=_2UdY;pQ~p03P!%fUf)FX!+jE0BxivSI6}G=%7(dYTgZiE) z#u!rar?(-;4tp9vH66Dhi~us`=5h2 zJ#%6Wh9vc7ZG>uU^0Dl6jBvU&HTL44!44MQ-#mHKn)4!^Yh7H_3xx8e(O`j6J^nz!0SUww06mF>-Lp&hUnK-!SbO1MXIXTaYfFBXs~RF) zup_++xzWJC2atwK8H?TW?Hd70&Gn<|hfYF#^c{2;(=*i}*Cg`?2uVC>^}&o3DpY6#>X2_!?Ky2&W!1aNo{9>sVh{ zP4_W=blPE$W>E2e2x7r4)GI6(sny(;b)zJAc{Nbol)Pb>iPm16{_ih>wVqRBl9*k*2Mz#%F^n6S4A5h z=)&Va`9^YK=4(@7@{t1e&#tlS$F6no7=5An` zmtViPl%*#bBCQQ9Y(1bOtUmsIv0XqPBqI<^`MDT*_uStFKbE1m6tBPk@w@(bR0)kCm zy@+Nw$2Hq0&)So$7>P7FBbkYcGn+)a;A6%3!(igP7osCQnX1DvApiLd(t`rb4) zCV1c+P0vUR5gnaEzAu8{4VT?p|5_1Ww|fLV&_D|vUnG5?E2(z%YQ4|S9#W+4cvV$_ zi;400_;_Kg@mIa=DQE78wh!Yb-rjd5g7`7)I|B(29;wjfKm!y2&Y1rwV+ojRA`;Jq zn}V50)fsk^3W=n?e|Q4qO*;?$R2!ZcpryVSuX=&hArVb>f0=~w{WFh+&3PLG4!9vA zXA5+UxlvCL2$;bVrZduK z>p&EwEu~4DE(D-^>XTYujlREPqs?e^(^s!N{OwjZ|3UsahRk+|Yyl|8?<%kNfTIFv z)hSnMvezAF=tRkSf!a}dy*_%_kupqEo>bW~Q2EDL<<0JKerFvHkQxu6z$MpSg6t}~RkaCfx|pr7;5q)+GSC&%{} zP*x`O^zsa=7fAjWJVjK%{IunhGoX_?tYT#z;IuYR3{fI?kwe?p*(egGeH5r_0L+NFK~gdqVx1n>|!n!LQgC#O7Ly1HnUuuDFoV$ln--Xcs&g&e70h z{5_sezeM^cv{Jyniw*1)D&Daz-lC_!s8rxM*|b*ztAYm@Qd}!5*6B`-%Pwx$(*8zH zsJ|&Ic|kzTz{n^N)ZSEHI-)~EX>DL|vbLE;@@ljuLHU1E6cD?+?oRN*bj1+JGk$?{ z>F2l1RFZEP{or_SViW87WC#&MOJiFbw!XeTNK_fVrAZL#H~?ks>4j*pFrWt|Y#JAo z6#n`x>R!UaJcNbGpvTS#3LRF1CzC&(F~Jyl95^Y(1`{2d`rin`i|%sa;zeOEtS#ua z;f~aw2)q#u{Q8vNrlm-yjjvjogoFg-(kvZT(8O;5k7M(reKsEFT^Ti9dZl~{up9S< z22a}%kU4(LT1}WEl@`~)FS`-^iC;qjxB2sD@!9Sk@W+Ipgy;&Ly#G%36XB>A?|Gdh zgD=*&tui_ujwn$Zyyvw#558l<;C_xf%7fZ`(yqEE@VvZ;M#aI&nXHs}Y;MPtUQhx5 z=^FSNr+9V~*oWNPr3aJZs{nubLm|9zXo#cr56m~vL;xAZAWOC^BZ0@(-1im*T$&^!1Of_!&i^R$65qSK@o91n$&T%x(1 zaFj6HE|Ef4ADfUys*lfYZ9?#hbF{!8=%J@#=GULpvDO`Jt6Hy);X*3s0kSsy0Noyh zciDnJQOWNn^`@;T(^fpU>&_Y16RYc3m#+EnIBYaR{IhwmMRM`_5DB8bgtq$T);6>t z2B2xitl5AD&dFDy)#j6x^@GIJY=3he+}X|_A8s%ytEw6&YJ3FrEScGSB8e1T`|;y@ zS8`pq7OsGI9XWO6bIF&&>h)mB2Ja~|1_lPC`C~O?fL%5Nrm3Z;w3&? zH#sTF>jYenFJG}^3V@v8*F>WW)=Ns{?UQ}f>m(U#oeA@JL(S@zz31z@8`r|8WYP?o z;Uq=#!rBxmNZ?=i2r8MZRv>ldw=H03L2r(d875|pM@Xe;jYL2Y4DNo`|B?|pG|)y> zi3g%EK9czS!-o%thK43!*5&YQQ&W?U6m6yheI#xm^iNaVPFxWu*7xyheD&*)%nnVp zT~e5_dI7U}(`YtM&+RS(8>x+2C%e9BZ-^0mLMtA+pM$;O=@-}*)0J=DygB#nw-fYP zCJO5Hvm4CqcMlv>5@A2a&}j`)6r6@>7!0kJ*w{|?4h}8*j&5#lZ_r(4yRI!;0s8=g z%LLhhjm;WNCnu+&;o)rZ%yE~U`0R&j+46mkptZOgfFC5EcTo9R*n4JXCSU;dl~QyS z&OG1U+nbyTHU@DR1*1C02MqiS{R~?9%yV3}3!X5Y9955C_kfmrnC3dnI#I|9f^fc} zt83+T1&qpZrQotY2dzRy^vSu7Xoegk0g$o1IO4Cq`-~>%02)??iID;4xvdQgisRzg zjA8d>(gvep!=L30uz~efSs6Sq89E0rv|=7pBiVo(O-j^f&_BF-@i7nr=eFuD;0qwr z+7|wMx)kFG?92_QoPAH-sp9!%g<3Mp-Kz1CkCuL`6yB{A?^4}%g2Rn&{YiG*Q2+ z+3iaz2pQO6=FI7QkvRU23kscj5 zrlFz9)Trm<;epu$!}1*5++q%+O!bGJl&*80@C&G*Z%+5o5r&pPE%<%M7)*`53eMFz zuGeD=WnB5PaN^nX*xo&O`R)ckC4PAF8+sKin?<&!pm{=Y3{Y2y(t(<^QD0k`am@O{ z@5c{J`shW?PlKx}U}PPKgMRD}L-Ec9l*GIWJ5}btZtbpoe3I>JsSA--HcuO%N$cR5 zdu1q$>kDOx^PZ*6c>d_1MgHvETqZ^jpm+#Nd6}3v5b6hIgcqRmb_x2oTwa6kq)c(- zyWevX!YCLyvOajBpXqU6;W{R6-HGkDTN!EbJbM9f@D!qS(~sMFkt3lsV{pSpb_kFW5XN7HX z&xYR4YhV52Rm0HGUg}8*3(CiHW5aiQR+bF-u$J+`dVmHTRJoqRfcg1b)dEDJ4M>C$ zhTz?F{88Z9`)_-~NTB{bM}B^OnA$D|jt|&x?kV;1nVGy7)iV{N3FRcaNqwidp3P83 z<4p;?u^x2N8_X~{sr5^&n-f z81FLwCHoLmqw>MC5FLrBL_^1$g4Yofj`&`_JE7q8dTlk)<8#k{alzBIJvb>0e{SDH z7y(WNrn;=t>(y{i@z4wX4Au=OVjK1(d7PKyw_ig8s_Hzv_D%3T zQl)Ou2`+;-dWv$<+aNqiu*)WB)pRj%hVAZuPQ#g9hWbg%E#%6op0x$fN@ zqD-ukz13%@2bmotv0}GMPyIdkNkOb2`Ag-k95z=mtSN06)?x^D=h@>F0GE3{w+Iiw zqP|hC-EyAC;SVO0m;~m!ACSXkq0nFPX-?tRQqx4^Y@8gqx{{G`aP8upW z?{D%PG9c?6BGTOSVYJeW5;J}M>w3x+n4*xGT)w=96#`o3Qs^LQbN~}~&rp_N6OLA_J6Ba*-CAXeL-7=7WN59ng0uL2i%H=(d3Pc&xNh|;%|SZ9w0zAv zdWFOFkQAl?<#_h>_JdTw)DE^hx^pySWB+KngB^h=<*o$YoZ^V|+!mnTVQ>E$S~mmO z3!Rn2{5L)3j%jg9y`jQi$}n{RRzdnUE6hNqDZ3Ox$$t~t^Wv>#JoS2=#IVVzp3U?r zO&1Oz7+%GYt#ft&9dPE(;0tTOpir3Fc6|`p4-!dQw?j6KqZO)?T4Khx!QVfGq&mDV zkOA;xh?`?_&x-N|hZ`~I*F_LN0+0oIm7`MGxyNw35cQRO8y{omG0BeQt(3G^(6eVw zF#etqT-^d76YKg|9cIIA!soBn-$0OWZ@&~!Lf_anEFMJ!^wGs}w{exTHcnd$!Juim z`P{Sq2}G)5Fd#-Iy~C}nhbl6uv`>QHdM;_3vpp9^%;z}9GybX3<)h?vls|Z3UnxaP zx|I@|v2CS1bZ|iqnV$CBjONLEZ|IJGTE+fgUsP81`p+&*q~vg2y5B1OHsoS>#C3>% z`tt182}rrpC@rghKkCnB!|l;NJ?Dts)$2^~p@fzU6UP@Y!TE%fxdTu2V`ltv+9R6Q zhzYUraV!xB^_kl9I5>(bux!#O*Ruj1N4*y(wg99g$MTBX5!4fu>wM1sur9MrmH{Wz zf(dc3Q6*=e9CkACD$K|8x$I|eQOij0Q$-5Qd=9_YWpn#r50b`z@f?H`(UDiVRh_on ztS)4klAvGcOyhnT49z=}<*^DxY&d7d0QAPm;YMh-8Y5c|{~KsQgV{Yh{H7_D8s|N^ z(F(&}43LYo0of7=%6~ApxC+IRXKa};e8mf zLcR}}wNyOUS@6_v4kD}90dg%9;wa0RFCNJM2qvRSgw%3P5lL8T6ZS!VuMqPZ+5>nQvsr!M9hf( zbzyV?r;Mv;g}6##*&x{G!i@8@0={LocY3E1RZ;EQj+h6Ak2!QI6;xNDzL~9_3ZuI- z)jsgS)Hsq=P3!LKYT9awRhDPd9vgTWsP7OIZbc0vXh_s1kMrg^81Dr_V~5^MLDLs? zL^c@RBuvih+ol{mObMVyOFB;u^OD$FZI?{GPyO;buy7q5uX9isy-Ibv!!e$#T&QhZ z<5z6OLyR*Rza}RyT3L|=V1ZF5PAsdQ_s;hN=5hmLoe=*DkD+?A4;?ATI;})+GjQ4x z-K4}CG4^^Z?Ckn4b;Pt?0b{PlLx+u?Y>|$F3-O>9>K)6m9mRUfWAOlShXHwR)9`*h z{VtrRCd&2XBuwwy%#N_`JV$MM{t?z_)ledoO89eUW$dF@QiA~4fn3;HcnC7cFN6%z zwZgH5Vq8N33>+Nd{fOGT;GIgH;_|*xN{kYd#>8$B)$U?wRwndFstE%Fss*>H-39yYoX7nKG*1tBxs+sgMLGV*Km9WyDakTf+f&TP$fZ!DhqS8gyzq? zRx`BSK0ba^4IV;}4Qb{s|8Y2`aM|A$OB~~s%^f=EHQZU@7icF~k!5V^gw?Gyu@(1F zd?#G)CXpMiw>wVYQ`veF&D08}JuW@_WJ5au^z zd{wphJ*)=WI__3WGcz+N(Oi$`vKgp5hA?)h^d%vv2-8YdHX$rmFt3q_oZ%X01xa)K zM{BW2ny+g*gV%>=zkX%VeEEj4!}#0Jx>Y4zTgxNEJfr;wV3jHY{gYosPTVyKJgp@k zo`M3$9dh?%(XU+QwzmJ-EhwUK{gDU`i_A;=+<}wRo^g;pwlD_re0ns&X z+djUNr5~(86r30^0JHI1dS@R61$l%Zx8o*lWo2a&ux?yp$w%8i%@9Se??o~KJryXa zSS$_6mW9wHxo*zkLn(@RH@D4ozG9jCYV~QCq5U2js3(KLMBqkNabck_u!WN^81HyK zy}pi~c+&g(0&z_URl1E$ZDUlKv z^mt>f?^{khKir)PMbf}(n*$uc#eJcg1u_87a4NBd<-aQOyK(9mJ>h4uu#g6_{pK7x z^fPk^uKg&kN|Ed*U+}({6(L1Mh9M!mdAHo;`O7&P84RPL-Fre0JtCN)+Q3Ff5B+dp zJQu3g*37TGjdm5)g&45`f=LAdhwkf_=#}qNDk>_{subTID9{LnTm5Osb-b=V#pH)2 zUD6?JzS(;We`db=0qdF!G_O3CwKa?sZGc!`K}LloDfOOw=6%5V5YoG~n}0_D!W?G9 zo*SiR<2MD}U}}A$I)Acpg+|z&r+i!Xhn4t+<;A|xvhNBgpuWJ%K`XwMj(w3_%boZ% z+_N$$)NG`$aoi)A|Nawv%J36n7&8MD!DP^X)osrAmIy(;=q4aYFqD`StYR?1 zU4l9K;NYO~=K)zDk1@k_C8(tdHO zjLzKVnD5n!jbeDgWuP;fzTpf12dzVIvO+uY5veJNIdWA$64ne{zxd zg!gIu+x}dIJEuoWnwiDfLBO^fo8`l8ucLG2B9&Z&q*hS;Zq$3SMPrzWeslpF z8+&ulMh4W(VPv%jhg_G-%eRecKShIAMd`}}29VhXK#vmgeOwNgeav#R@gOaseGs6P zfbeoPjQWDXb(9>B>5ksqtvkG*?(81kJ#C#*D>y#(r~>^3ARYMWsn=pJaz=6WKt|wx zvoB}x98_FCfnh09eLOsL{x$a&G`j|3JBd!sE!5j8%czPefoGI!NlD2dU!B7SR|o|k zQrb;l+HI-K!g8CkE zgrTF{i1$y_-GlMl5pnFfB5*F>HyD%qvg^VK4G&~sf>${ThM;Z7N-BZwz;Cvu=4Knl znAuB|V+8}J4HRiS%HdFQw#6imp2Vj8c?(-K1FJ`2k7$rn_ zUnV6@>To?*b?N*?sjy`fLqe8|Rr-wB!1}oE9&-Bf)qX?_GuRpfty5}iu2X?6ll8*L z6|mg-YcP~@TVjF7&#(55-*&yKLb5LoIkEQVwal1Te>t9@iGB( zpqA>onw|eYiwIQa$A$DFKpdUI!VNvLKoG<!&=W+EcIXOpmh5lLBH^;@ygAa)9+{hHHxi`R0&)&0Qht=E1SJZ(I z1H$ifP?eXddz3D099o}PK!-y>0<@0J3HbKgoBpR;|3A@XU5=3xf4T0unD+Lq5{=GN zN{{WVZSWrud!(^j8REBGdziiRW(Iet;?H5pZ?7th5;Euui=Y`<}5`UcNp&L+SQ4 z)F!qqKUm|v?8baP&7_m8w)jZ*C47#r%*Xy9U{i|~|&YX=#HWsjE0SCd1U}IU(iaPgit15S<(S;*5H}IGO z8%l0=Q{|k0kd*{*M55%858krqQGno5;~6KjH)5-Jskdqb96bSB2CtZx_W@hpHQ3XB z&*6#*Q+*pt?@krD{G7l27TyP_Y|4+)>Gse87FJDnCElS7`O_Gw*!cUd zlNSLaBaUHIZy2ln2;Fy#K?=^^kE>{vA7dm>{S;+8z%tPq<>`^C&)4?G=hPAFxj2o5 zE;wF?=t-wy;tKra=Ft3c!pDcv+th@S_uMV-wB!q3Lh(RpC}3CXX0a6ITLEthMmlPL zZRI2*;Mh)&U;`iq+#MH$us?-oo%$_h8w-5=7=r*+Yar!`?|hobl=|gUZ z@ZE47-yjR1G3aAX`KbujT0Mcmobhwu=+O$&9@#2O8#d=PpjNWkOJ~zBfDoDJ&u=F8 zVVi}2)@`U?gt)}Dw!XZtIbm)xUQPznG35!fa*uZ?%f1W`ljr>`6&iKK?U(v_`l%`h zX3vuWNC9huLdTH`yN3#%LPiWiN^Tst9!U$3UM6+V`mRso>UG)(bwqpq_T9S_i^!RU z?MWYg#1FBt5x>0LcHdd`bY0Wj>PUqL1YXZgLqp02kANEUetw;p{GGoks)sKriarM% z#ZB-d1s~3&>yLY4Jy4*EWNz;>h2tpA1-HEz7_NS-|ME#M)hNf2WN@!`Q?yNe5^#410 z@Z`~3b~Uc)u1FGa!v@&e=GVJs+>DHjlf295$U2j5Fz(A$QTl7YhWeb8C}Q^ncD?NRUH7uV`-U=cCQgp9dhW#U=4ZRf3TJsiL3cI*lKeKSHt z0u;A-^}f`9ZxG`11JksUEZ-~?+1u2w(ak5E6hgyi=WsIQc%*U-$bo9UMSF?g%I-x z33A+|5Ir?RH>`N!Q0O-P3Y-+O+CW3t)*ji`H|<5kk7RtWKKX`0ld;gH@rNf#v`12Quli@JSpo*wkvK={R7z!(=}6n^=Z0tq+B+3aape9#mD+yeEKBkZtBTj9_L9EM`6-5=0xAJ8&ihAx{#&|jh%Cz2+} zx6I}_5dZ^VtB@-5E9E})^Q}uxf5B5U5JO;amz%Xlf4wS?#>MzJreHwn?{9w(UYAqg zAzf*|P61*4CBUNp2!c)`nA&@OH!Q&an#V89WY3O{SY|C5wg*AOJH|uxG3tH5JkxXiI=4*u z3lZzZvBw%_rw31N`+M4aPRnf7@A`RtpBuqeLkny?5o0@W@oMk&ME%uVcG&imaX#U4v1Ba(6?D>exq8ek<8;El|&VITpgbi`w#2cEDeH4Q6n| z(b3fOjQ}jfK^DOF_hAvd@Jy#MgZkmi?-|cjC|F_xGZjAY4HO)a9Jwt9z>H*&&dPqQ z`?3q>tG*Kxnh(qxpZB8fiDOXQ{cF%oZHWDTM*$~f+&~J^3V_j&@H|W_G&dZucB6uE${H9iz(Rym&S3y1%h0Xx8qmjMjRAI=-=UF@T(MZs8pvv>$!IvO?GH ztJc4VT8m~aBXfBmPc6Xj(uz()625HoGI-|ZC^8BuD_1%$O;3Bj8g(kMLhapIU7#-r zr;#Ryp~{$$jr|o)Y0l{*Z$3UXtPn#(TACor02uag^L5GND7_e7CT(!w{52VI1-8{k z&^-mq5kE-iH2?I~YZ`Z&oK8~Ug`O%lV4|LH-hfBqBh-`R!RT??S(0m@OvTK_rfE~_uz)eDdB-P?Kr=Q6XQ0Hw*#YMkY|m9YQ7MLNZ7^4~Bhz-*UiM-Z#+ zqWPDwUd?4>wce5}MxTSa4X7j>z^CN$};qzov@{FY!RyfMtqO3d%;{2=Id=1yn;{uOI4lKlb-W`$IxY zq@z;4ls{D8O_luDs@X&;o`)FlSA+v0qLnAb+Xl75Mhkdg6vBQ|Y1u*uoT2WV{^?E= zjJEkYhAC`pfW37C=D#5!10ayZ8{m?98K6L+^(~hWM5GSY8~eQ`4e3Le(TDfIc z(`mh>3FalNZPs{e9N>zE-)c#vkHq&O$&qi4l9+4puO<8IRe=R#HTIrYRROEn!R?1U61L3pp&B6lIZXm&UW|++{Zoz*b{~0CZzo_!f z6zHwNUR^I}tGT(^0_+;6tviE10g|hFxOYGVPBmJaL+xM_d!QvjrXZ>dp0O~N0sYTR zXDB!_+;KcW-WOUur?oXcI^j6~`_*9AIJNZSnYhW)J=9SW@x}dBPvKn{1no!mohG~5 zh9D^6;SLjY#E4r0KZ>b5eDDAhxId)uA=`o`bUu9`AvE;iu-ze_oZmigd*7=a8|*7L z#;YB4QYwOx50k5d6VJu^+I);?&1(T|ozoi_p^H`8=MkCg0F{+bz&xP|H*|E6^Y1J{ zeem(JBdq17KYDICPmIfq%4ABF`zEZlIe?<4XULPOG-r2mZ041+6UlhY~8R03Uf`(76hPr?i;E7%&h-G=62McxQN@1shw8 z=$<;k?@pyh?Q(jW=ODVJQd;2=2bw5%vGR9EFnqxYP|<>>;l52avQ%jGEI~~zo?U5mz;!#}5H;=rB?=}V+3}YfJic=@B&o1)yThqJTkWaz zeYg)Y@HQeU72o*U+Jz75Rn9mE+bfDzxt|Z|0)k!iBwyLr(6CUfxC#3fvBqw_A>=`e zr-0FzbdoBU)l9ynEA60rplfWZua1e_kS0&%U=@e1egCT)=}ur)HTrY7j(<;Lk6?8} zNCSK*D(h9{Jo_q6WGx$yLL&%qs<$aQE*dKqd|FZa3EtP-XtAHUA4$(Kc+3(r)PH-j z690Bc>B+I&xZT-4Ow39e!L^;3?-6_eg_&Iw*<%j-z>u19-!quUAlM2?J)nw>2)?AG0jW9 zK{t%sod0$5EBkX>{=1je;f0yLPKea2;e(t*d>o)S8@!Ye!ugtd8pSK%Y%u7G7URD~Z( z#k1hSmLv$19-$5Lqz3eEewUVk_8UZsJDO(Q?`)ATw)q?EU$6gO2akrrxmAbVnV(NG zVVDp*EyMGQJ#z5)Zvq{fOp#^3u-U!;EKcL{!4;Gk@H@AyUw?CW~+FQ3C1$Ls2 zOW9lVy?LyNrVIju*6J=OO%&>H0c}`ID|PZw_G$&Ge_F+D|eq$e6%xd&Wn3! z%~%h`d>&gfvGA0YfHBmdk`(Vxe=C{+^gKKisiJsIM^1LK8Bz|A2wwuAA6)9rz8bF% z0EtU82AQzO5b{fjylLu&U8)-IREH-$FE{ z?Db?h9MMDNHdI`0Cxl>#lVugJC3NsBD~4p|i)!x1kp0oVLJ%37@X?Xe4q4c_Jet6l z&HHT1AN75@^jHX*@4jXQA*^>w%gD3k%&0^s=TH{Cdr58GE`3&rfgUJrQ&ctv498NW zJ?j$T_}Smw#{sFz{i7XDaVgijy502IONba736w4_uliUOLWNW+o{tdl4R&j*h_bfO z>)Jaj%azW11ki!eGSP+9PmGA}#YH4g^L^?cb6ao!@QYzNuf?BL&~Kmh{iEi`Zf?~X ze6zXrMwK?&J2{Gb_l#{4+109_I;tVo3rre!rGMtH^= z4S<%eFy+d0$JA%#q3m9t5!~`|WrFVejhS@_wMO|J8JAE!ymTxx4)Ki9lCd)c_694g*Q0s}O45WTuAW%Za$Ye-e z&xz4Aw$#+xbDa(-+d8m1H70G3y95_6xwZon&3X4PQDA|;c_>yB;k$mn?nlF=iMGeN zOqTgKIQC~ZB$BwByyyrL7#npAxcq zoio`iNr*PdY7b-tVU1So^QPc3=MaxR2lif>5oRrcG2-6Dg!37kN*!;wnUL^Lr%HFX zJMke9v$=v*mRA%+t+RPPJaBSjx?LitL00vRf51jiPU*%Ajl{EGyG;!Le7xN2wNB)Q zM(4OsOC=_feJy9(@zvJX&)COnCwtaI z+tM5LW%t`RG8&p1J|QCh#PWjny7q7E!coJxl$`3gF7aFr6p(17COE;~fQ0@Kk*3Pe zv%KY)w~L}zY3-3tHw?UDr$pBCXD4t4{FLajlEuR(L2;xT^xM+o*SGWyB#)-2=VMUu zq-B=S$ODbu0;|(hD_a`-a9y^?el9vXx)GlHXAko|RZJ|bOg)M9L(X46b>^aPKLy<{ z%n3RHsoq1-k6nY9v)y)e;E3eSm0I0(9yUrv%Z%=BDY-o!@^;S6I|+7o*n$dWJrSkY z_UcAM3Uvxrh$*x8ju0l_`W23{awrlxAj7 z_5pD?6*8z~!9%33>EO@r#|l(x+js6t>lhp7EGV+S9Htvs=6BjA0#)xP=xt6cE?y85 z6SFy6r7op?mgHVdo5& zg8xl}uT9P`hrJX!oC(l;v*f&EH-5Qx5zq0d(S1BJE^@-w;7@4*slEF-1q$VI;^yZ7 zr%~&lOiRmx5jn8Q5%Msp34nJ%X?MNL0r6~iydb1n;dh_uvwiw8K7I+ZB-swL8rLmJ zQz7wj#gLe()~C-77VDspeOMK`uRb@BdI`Mnvak8pzx>VxG9sLA7iW3mC}5EL^1t7Z zoEe^VzC^)md9K9Zt4#ArdQ|+ypLG^T*qQJe9HulGeD?64EUi-4NYAifZWmW z1&9q0hiFPr3|ExC5BY;hZ z*7?XeoHLRT!iN0)dT5ii*58Yn8manD*E72m0T$S7^0B|k0b^aD0ARzD^nq^Ff|JwI zSh8B3%YnWPAs|hn9}BphOKY<)S!`a$qZ}z7qM?|BqNsbo|R<`r5 zX3vzBSSbR}dbP0M{Vi9b$fY@g|g2|_ZEKI z_Xh{7kBj1Yv#4}}W~C2ZiR-fKaotd@)m0dbRDjW+?cIu9ya57ROxBZk9)LAv<2@1= zzmLQ=y{W#L?ap$NjV>uquMm;?_oiYsgPSdz(SYdDzO<&cQ3{?tOHm+ZEVtdBMBc$# zql?u0`;ye0odG@I%62* z?s!PYm3szDJ0j^ix1EXXwb|t6w%f5>p;_^V*WjEC!I=`BSUqc{pqT^%A{TB4MF83K zCrc;xwumquDE}>_g`B$+QiL=B+pi`a_cqzTr-DR09bI(d@EMRVBkYX;t_9BmYKtPlf%~N zZS>gz(>leCl+WFz`{O5B4L=$Ln-v;v{qYWhwv_0b?0yA-$NO6ZHxk*bxUyg5dxVT1 zCGp;jDSzxP&2rp4UMDg|!vG`47}RT>8z-y?Xh>v)r#CV_9ex~-nb?{xqAOH@`5g}o zdoaOL_0x~gW(TSCa_q%NnItX{V%<`<$?x+rOGGV)f7E=j#g5_hYl)_y(|QX6f}lQGi!=A_Wo z%Tzl7aea11<>C83yfh>X&NYY=2?Id!=X`M~b6DpxZ<6fX-ep4Xa6RK&Oi76SwEHzA z`7|A?{Os=pilyW-aRhSq9KWT>lF%@{=l^i5{%7Y+ebr%}?)X8u%bh;R#XrV_^$aP$ zEhZRzriJKOdWdXubDmt~GeJW{g^1nFAiw3Vd$nd1XG!Gw3H^>~Z-efdF*jGN%~qh7 z8b@p8qgGSE89n<`CRm~*NkkGac(wxU$_%bfHiFb#IC7mL_kWoECcUfYJ&mz4#>=hH z;IjMKVm(1#rtbT62_4CcG#*)?%Ev+tVZQOQ0YEg9FwW9=@T8BgCQM)pWX)jkloROi zB^s2I(#Zr8}j&hVGIQ>27J1 z?#^N0JiPDso^{Ur;Tl-WS)09Q@8^E*`?`Kt7Y$BCUIri}S1X;iMWSU|0f`EKFH!|?!YoF=Ba=U;=cDzk z0;uxGNkkh-%_3EmY6X*K|A4SC2L>kneEtV%A-0PG!i{4q3VZo%t4(ZwYC^0PdfzC6 z&ZIt41i)*liy$fF*UEQa$(8%iXz#SKvarU;Duxv(rDgU7ws!8HeShN{%oH_+9`g`Gwg(b4NYKH9Nu{7~0)Me@ZI zHNsN*gEwGlS6Xi`1Du+0N$iPiUBM*%A#7LJpynpXOsOS`GM!_7{ge)erX@ol{3Izs z1%nl824bdEn1nUTRpb<+7;qF!gl67WL#jG~vBCVs};0W%Xa#d(r*fG6dzbk zU{t3r+~gT4sM3l<;U-|SGa*z=2Gt5I(4$V)0Q-(_?DmZz_C-ai^8Qkbz)6hTOnv3;yA%s^Y3aCgNa1h^ zPw3R^#oo0KG+OKAC>h@%uS^Ex@PMoBCM+X{#w2R^XPgCF4q^reie?g}Hmiog6|5-B zW2VeNva@?AJN3v?vDIW6()j+>EYOHfbyS)Z*RlA~jQ`pCy)7kh2N9>;d`}AlMmsB& zhg{W&;(#9piZmwN75OhpO<^&Fv|@BQ7)Wtp&+y1-L?~uQXzfdNn`Pr|qGGXl`Xasf zeD;E#N%MQ}GFnQ=gcwZA%YO*C3U?p#DEjg#hu(brcBiLwNCqQU3ddFy@E?KO(4-hFF%SMa72mVw;I^9HG>4Gxe zLVdX{Xo9|a%E}~rd-wG4GZu$C>e{ZH*ywj-QsHu(rToNi@S(EVg_qCKei20yg2Ko& zjDmdjkk5NrBmT`qTYpOO3u6NrmL%F_dVboa|36k~?}@tYNEil1f`mc9c0^DLoBuZ2mgl90g(= zYQd(o_UEMVEGQru(F~hgKmnY6gQRLn!+MREcNU1)OxZ_DYlO^BNi*B$6#c{-RIXF+ zf<{OQ3Aab(hNz3wsI}tOpH-mE2JH*$m(IaRDaN#QwTokj#Eak%G07LwrqlXGEaxXk zqN1W2(_l~p=ut2@hi&>TZ#^jr|774OX4ExA?mLg z)G4O-!SL<9Nw&-eWDPE>_{um%S}S060M&oeIczDI8XgliaTV1RDwoI*bQXr0Tp!fZ zN{SbNxRQ%Ohd1QFN}FVe^)%rQDhYZZ>Y9^Xp_}TezozKZ>2liM2DJ<*w&*d9_ zUnc_-o-?YlO5!xWB;!J#tp4Vg1s`xF+f{*BE7>9DB%9i$f?>Z~&DrMg^#R?Ssv1Io-r_KtslVS;oM9rU88 zVVQ3s@naQ6)wtEo{qg){a}@u_cZmueJCsh#)4&q`u@ zfr=*wF>_s9l!|YZWB$aq+}@4&50z{l_<~K!UqOg1PO5O{D_tjfcqV)5m>~|z$eGwP z6@OOMkHjp!x~Y2@|918=_a*1$N(C?s@IQ?)qQu@y7jRS!KkLi5Z$k)$;PeOA=Vl2MF>7PE~@u*$CT#4u*6OQtoJsPF60D_U&L9lQYv^y@IGdr!)<?&g4L2z@_8Dy(W}~U66S0-lxHI~65tF9Tf!^@zSl^WH)Haiv zqP|h=aD#jh8Ok!yPX4XG6T{luB$3_Lb^{*zpSJLTUkx$zu!?}iK)6N#a@X{u;2*6~ z-g;H!>Md;;d5%slynkmqC}m4~WTkp8#fadyORi1MV+^6`a9jZrLT^|T?@q37qk~lG zrc9q9pruyYcUs0*x?k*B01pd)x829SpoBbnD*aU>vFXOPWg5X^PSx7M;O=+FHS zO~~J%v}pydHOcVI)pb#OWNB#&=a|^d#;qQ;uPP^bE8b^RxNG7m7wTyK^~+3e80TBk ztV1bizr8Y3lk{J4>o0P?LWa(RVXaEub37I&6wdXh8}ylvzo|B{KE~X+q=Y#)Iay4( z2I##TM^VU5gcNmeR+F^ZTBJZsx?iJ(l_{nno5P$@H0nL4`b_{y(>D(PJU0bGX4_az zq{UTZdd{xJ3R zK4&!zlO{K14aljBWH@D_Kx{n1nwK{4^)=l0Ip*FI_{50wEIanFn@+=W zrQHXELr1YczIR!l=>?@k$s+|+St{c(i2zhtZ^EJ1s%&a(@nhuJSmM;xkEMtsD6}fT z^e)?*fsnW{o1Ehq0DkiWI4yTJU#cvuWm23sI-!%tcxdA%_tAeGz>0oiwsub5wsBFK z!fQy5qKOwH9+%v;`QF0V*x!kh6Y67aU8dKV`&g7|>@mtrSu62I`2d&zPsvyWpdAKo zV~%aoYcA*`B`_Os2LkOzmRs%#0Y2+E%|lv5U;_Sf(~eM&grBV{p9X7T4oU_kxwcw9 zNfRhn8r}}L#+sh+;4Kv=!8*El=*#q5u&hwO041Qff}bR}GCXQkX$hW>(x}^m5w5fU&y)YZ)Mxs+c%Z*wgd! zplF%6Wlv>BFmE5q-HS0nx5+jSPB-q5Y!5GUC^*1%j(ljxYHgm5wN)qATjwkl`1nhL z;bSJ9QXZ|NaAh$Fa}P;;fbI^r`L20OOD4!|2LcgM_j{sw7LmI$EbHmG&?wJqKK;f$ zyxmQajV2HlYG|e-i45GI;wiIl(0!5le$02AOI|MFAEuavx^|*#-XsX^1Bzrf|*p5 z3DKHko1$`bxS#V8YuzVwY|wxArWq#*OI3;GN``O9iNAQ4nBy%Y6Xm`hVghxf*Rm&9 zT44LMcSI9gV%~cs(zwH@HNtUtd1v5CKu%VQO`v^O=^PY8&qD)Dja^q^#5@i!J^5XV z?gv=4W>TRE)eu7Qw!d^JI@^f--fLrM5~1t~ZHHK87=KFq}NoV;)Ut zUrch%L%fRp$*)S>`3B#6B4F(Z*x}gu)vFjC0Rs|!<2^Ih6A>!P=wKy5jwi8atG~y@ z06LH2=Hehgu*E0DVRvzt+6lJJ=VZOyJvx;89x{^t1ayvnSKJxaj_j1|r*x7kJ}>uz@hieu>u6_>eK z98iX8Z|Kg|kC02QC_PRGAhJq20g768j@qx3(zF>VTbr+9UblQ$Vko(G{Z{uw^6bU1Quf&C{!J0q36MCP7r`>d>{6oBsOP-|`75em+FVo=OjktLQ`HM8D+Bwl z!o3^l)RL{?O(RfOsbUjKMxt2M-Jf)Y(q!PF8pzq$^SqF}XNr1m?r{6V?k?i(l%P&BBpL|ZZ<4zX!L?5Gpo-clnv zz*>u~_fN%Gw>TxNWFi_vASF8r=y+!j?bRn-_54p?psI4DKsIBO8aT8H*Pn}eo?Gp`2FRkV`b(!>dy@oGAZHBrXn26OnwGQ~hVFeYBh@OnOIwOxJ!LTF{>w zg@!mAxwdj`6FB^x+h;Nm-c;7_7WL#F^b^!269>!aJ0m#O{;KpS3ax+!GRw#JzEdpG zX!J=X5RhsJ^(U*&9q`1;lgw}^0d$G<{`9EWz>s(-bdMP1zb~|SKL-*`_35vJw}ff( zxRivS8Vhx+)U`SIl@y=4>@VcAiYBw$7=6x2&5rGJSo7poTDpZ&RUjf!awkpmmL|JN zJq~4^Z<4FLLJu$aMcpHl^h%dCg2GX4X4FBu!0kBZce5)FfGI?%jLSM!a$a?=8H~x~ zS3y|!fh~4=TC?{cSxrHQg1QzJidOpZ`?99qqk72Y+T zVNet+iZLn6?OVBHB#kP_h6DB#Xk=IU6sel1)LIVF;8~tj_xJHyHBg}^qpq)|(zRlp z9x{pa4C^<6d|4J;?o2M=SuKe0#0nGiyvgl%NFdQY!+M$FkQxrVJiSX*Rl^d8^Ar1+ zMaMD#1*frqZCr+7HKG1|B5He13A4$b(IE)Rht|Uo&4}&4J+v|@qT70p>)0o&51_|Fd8@=*86jO zsA!)%ZC)tc{ysOe88S%1Rk=%;$PeqxeB?cO84HmRI`x6hk#(GBG{Lje-l?4WM2~Fd z<%)uqs=B^8kITgYoA4%L=$Qd%8&4kEgZU2BsVJ;FdW~$hoJjCT0aa~$*xDiQec|wC zcI^1eD%#eo_LzI;`>Xvcc%js9emqO*Dkee_$;B$d{P9lbY{cmYZKKq>4dHBs+48q%# zrq6MV&Bb)+09B;I@8M=hm~pYcSPCEPy;OijEF1r9h=wXmL1#3bqfICdh$7RVOpRUv zf3XYWmvg+z!3+%na9vfM69H8shY3wiv&O5w7beAId{X4?2Aq6A-*Q-C*I|7XK@@Vb zP7hr!5BnVzD$Tn2b#Uet{9}zI99iMr$KQZ;vBP0SUId2!Sn?o7?%idvjhcj`oexd@#MhGzC7y3tevB=oTEB+)iT#q-xCzn)E*GdBo z(f|bmr4Oc`0tS;(N7n-nc0!^9HlAg?J#wC6h}jQlLytT)Lw$;-tYthalOHEtraUk; z0fwCq5?iQPBA76R5pqdPJ&HxB(R+J|>MOZ55oXmEJ%8 z4-+BiDs9^Io6@=LPm2M(Y)R{)W518pDt#6$AJSMvzI@ri)h+iCW4D--<@=hBCinQu z_-1sh)3&Yja)A%Ld(I;qi#@6eeV@;X(@Bl?#kc2e7{YS-8YR2081o;GHcgM6={{Nd z5qy50#(V!1D6P*006fQ(+st01LsSBU)zO3OqL%=jp`jfZ?az{NDnLTri#0jdU=-d@ z6N~Mh2=LbV`&@wIV-yxVNkKwVJ_|Y>9^r`H6cyB8g24nzVq0uL!irXl>=E<|k{W%7 zg!n^fCdM&c33Pof0bYy6X#9|FUi)!{0}~Bg*}bhpOse>V&=B0dp;RwRXbCcX(m@v4 zEnjR^%n4sqhT7xO=)Mu=9vf{U?~La!Vob8;Oinzu)pl<@dk;0MBoZ0Y%Vm$_n2B9M zoWOF{u9ARCO<{9?NZj74E9T)!vH*(D&a~WA)ZO9cv&nR!-RvYZtyU>^&~h#q-dz9q zH4Y_extgC^q;<@=z|PmBVhaFl(o?xf=LKvB(h>U z9D}4vHK3fy8xB1>1JoeEHwey=SEeR^1xCT85!em~)NOL8g|c-uOb%sK8Q46neaBWk z5sbhqE(wenK$;bH<0*M@clu7oO3$UxA<{kw2ZQ*EcDa+He{3w_x4RN ztmX$AFD->!JZ~Rti|V%%Yp3Kwp<$!7RaK%{q7v#tJAhowBFLs1VY=|u!g9^uOLCoW zpkwLLr^b`a!*YLh5`Mw8Q<`OZ)jJKQpEk2qBImP^pHtS_)j7J6OEg+cmZ-5{@2^#+ zs;^J7RAW?na}8k+zV(K%`-chRvtDVX;S_$o^;!3t@1nGdNrx=Z_^oErKI+B!f|$L* ztVgu_%NVG?ijrC*<71F$^9U$D(01<9TI^1r^Eurfpl~!+Z7mcL0_IZ=dW7^l(JE~S zR(koIkJZYws>3nQ@)Mx^TU#saKB?-7z0-g0Sg$yJ|3SF{% z3B>bys&=tFk0TM(bOm}~n%arW%9x(NYZ9y=Gi5-=RAp`MdzHN31lC)M@y*N67aS#H z@_lb3{l!tg@u@ge8TY)2r!*+JDo}7r{rynSW|cngYU%^Wj?ANZ@;>zkH?Qf}#2Mp# z<03PfQzLOIO6+W8hV91y+68Cy&PS~6 zRZn5I!;r~hO4(tzw-h%Hsu_A}GpR9Uz>i`ChAj_y5f3@>08}id+Vh|4Z4abAoxy4g z@L73${yK3S$MGlh74|0-7s@*IikAT)6$JfXz54Y3d|j-cL3-vN@c074t#A*o!vn%W zAk6_DaV@r~L|WZPz>)Q>v9a-g;R$E6|2a@aDF}4@h5?QCUv7T%638@{=nhMYj+1e( z`Nu2M+dHZtGyVQQLvLlB;=l@99yf?t>Z&)aZwDqR{3cp)+JH4|Sp?>+7WY&9M~_PK z6mBTV+kAFmY+%g)HFxJ=aULo@2dM1mi*bQTxF4+$Y2H@9!tRdzl_P~Q=?F-b z9Uy!gZIL~D>nr^7sVy+Q)OI=H^e4(YO44wHTE~>#!u+ z+^XEh_<{ENg+xdbK@*SLpTqKt*tWaWH|y8n@f8igtGBk-Wc%U;XYT*^Br~RPedx_x zw%b|})N9-e22pe8w|~S?-T`qKSe1eSjp?k9fsxV3#)fd!9SB|@T)9k4vuNW?vU5Pv zv=tfG-@gMFj0V~^#QA*0kVH_4C@K!y4a7?cB!UP)=d1$2oDhUvTA7+g016O@x?Tj* zWlg`@L>g8wL~ZbfQR&oL6r6sK{>@MWlbNi;B`5$STo=_?DPsPL9^q0sy#FpO1PDA- zXHE9`T)J!Uswr`#n@c7x^^a{}hZR6Q3SeN!M0aKd#%hF6T?!tzHMs z*I3v*9B36twVnKy1lD`J8bVb|DsJwrHt)$nJ;DJ%gQ+nwKUo_>0oy>3JtRgrzjgZK zKH(n$X|{N^4H$tksS$BlO-lfEYn%ydp8_xx+g~0Kf@I0p5}|lL=9aRgof9yiN%TjK z6bE8qK`R2_T{Bwku6?M79@J6v^z>k4AmJ}L1mx^vxVy`Zj?YuL$~xgNS4>>optn2= z>O3L9mfBT#&Y1CyF=b};Z`bKVhz6i$|G(mr-*o_r@VmhX{a~@J*ryI4?27lftfrqm zC^~7vG2h^a;Q0^pr=$%x*3<$nt5BO zU;hXM$>mJPKd1D)W1;NlocEmoNxzBsUo{*I$D6sP;lY4^$Y-HT&hlKYwx;olfs(F?mt*`i*10S#H)%XUM zMW6i|h%Vdtp40fQ$_Q==V(hShmdbx~J@j|IS>nx}%!dZuNWr<)V#pn^Lm>k@&ENlD z6vWMA5_}>Em>8Bqfb}huE^z8sl!RBs&68sQ@3v!Bk=-W%D%hOI1NcBNqRB_)2aEeB)>D``;^1-tn6!-KQcz* zsO7mWJ%#+iQZ+L`y9AwV$iREGwyz zIo(B%>(mM&Ch9{sqto;)PRuXT5%;4pF<;>}gaZsL-Llm=^QZMATUVe>eD?IWtWg#z z#KJ=6;x%1Atl1vhRl|*MYB}+zsO|V(W2gnZX?bh1Z#6C|N*F+yvn<#)EOZQx$jo%> za$g`U21myFRNoqGR6reGOAXx(;@ORLPn__uubw8OeCdy`rZzLv!i)Orf*W9`1ST;v z%I5E%=JES7^-NsnDCPbfSGTvXJW1wtzI%V}DWj;EZy?YVm7>dm&$py>2x~w&&sW%` zxLvh+QGaOXVxRMZP-xsrp#2L0iame(uNl72gFl~CgZx1ibVT~iy`3p~8h!@#1AmkL zg!tWNsX&S!b+EKLjq5x9ojGt!%b2S0pku)2Dgs!>q=p2R z3+ndy`UTDbN0i<6z{#Tc58Q_ywWC<0Sj6@f4T*oQ^Rk675mEa1p|v0Lh z(e1AY_z2)dl5&i~T=vTy?=BOuaR4ldCL-DPhOnaiYJk)Zl0+jGN5MFl@VZ+690aR& zFz4gi#&b!yBR=ebG@#V|4dtg($5KBgUNCE&~|QuCsCo5JIn z_4U`Wy?vD%Hp=)D6!ZeG>v6iGYcak<&)IR?J`<3JD4kG;JA$r6sF_c+IWreI&XrI+ z&V9SPlv9uB7bcYH%VYV}X0b}<@m37w{u&0%c!0_ybu-$4gWI=ZX32JExpZw8q+U=W zSe=XTDSX%5-REWZ+Co5Wk3+cmp)6;Qwhh2FT-Wgn5G5*6zeA_(w8XH?|wL2ot3OB=wpQKx4dHA!969`wtzdxs# zI!>UsW&Q#Ou^yPhV)@TdTS*NG4bPa&kPH^c$;f~9&DbWVmiM3QclaxK=^v$Whg)0Q zaLlK0oMv~0ZjAm!s@d<>eaQ&#!th_aO(_v+AlP>{q1!N7D7&Us@xt%t& zWt*yk{ABfddOTsbXd(LVARQ ze{?Soyw(4(ttwwk9AwCF;G)>PW$(k(d5@cRS3X;I-cVHvAX}3yJ!Z*ANyql;r8iWB zfQrA#>74J5OlFE04>2GR1^sC))oT%Q`km;p*#RW| zTWi6_C%2g7JnOnb;0;ruuFu!es5(r)P*1wwe`-lzVpv%mbnT6-Y>s1 z5d`#yOBpo;;6U>93CuaJJ6sefC~yQLGX$+s!Ym*71l?V`R}-;vtl`=G#*yY_GZXujBkE@#9la~6^0;AlHk?!QB`5n?Xx-?5EF?`IKNM1^8?8FkHSrmp*09y@ zc5IK2U!IPu(QoYcY3ST)2@`8NKR29KeayQ0%jgk<{F2Fp2R zA?uBegx1!jC_b;zrZhS)O*i-2xJPOFU-;B*%zyV}oJ~H@z-u#;3d6TL+FTpSP||T0 zw?fBF;#>LYJ=gj3)A$N0xyGMrRS_P$BfFM_H(n83&bImB`*z7tPp_c^Emz2^AsE{y zFNf&)(tFIL_fdO78C zSHD5j&Flzsd9^>^EOo;qB@r+IkmVdtsNb6Axwcc1@jKfUSaNjw69v4XFd-s3#9J5^ z7{C54?<~Y30u>%oY4JdBnZQ&)rU(_K>v$l*XZMG`vWpqr*+JCk=o?2^zy?0Ce|2KC0r z4WxhiaPEX(0Esywp@2gaAfSr^1Y-R5j9z7D=sX%Oci0Y>vR2=hYcH>Y9Y$PE-zBu2 zvFdW|=RPdy-OX#w5>;Evh0Hwz>UuU!jHKKREuNbZ+K(8tv{L*!%?yj`;Nn|F0J0kzQ+xURA|1KZGePCPH4 zsZzB_eu0)ezx0tQ@LV-DZ>Qws)G<}ClgQ->Nql#QKh&|$<@Ts^@CH8js&-0`tHz#y z^o~$h$MkOy_U-0z$)j|hUzZyD=da{>T-{tESwDPyX2ACOzG7=@-sxh{Vy=bnbH4md za=v_;*th2?J};=3l++pJpKknj|B5xBgC;)|KRu?O_^VTal-SL-kF*Az-#|RQ?KH~! zH?Q$%;em~2C7-Sa_xV39MyP6-9Q#R#G&9QK#FuL~?zKjzV?PGf z!cZLcA1>!13}L?}ZW_C~!Hgk~xh9MAEq{DQr;BGP(O>=sOdJB^quTvhZ0YhRM5(jB zz8H%kA(IS51^eaIJD;%(nDzb?dVZokSO{jL|FM30P(Yk0t(!n9CROyN4T9G)?4Uff zDR7ym@#fv2;`F!YYIaA?)veSQ5=cd)$F8dWW{b3R8C z=eX_Gc4+I=(o$)zrB4_001c1!Vqj9iaryf@fA!!-PxcvWX8op;nu?$h)yt%HiIj%= zvLBZ<5r*!Jw2%f%9e3y_4QC=bIghKOFJqf+PG-n;R)=l&vH7gt9#hbDk2bY~&E`-0 z(8Sx9z59UiZNh%l2uvgK-V)8|Hh|O+?>N;gLEm!3L;8mZ09&}kdZ1`v$M4j!vbu2M zuY1Aoc@b9ixE^mM#JBbC`_9knwzD#vX9pJLdely4Qb)CjBy2o91x)RpQ_8;fd)8)z=q_CBoqpG!Bww-qL!XlNq%hvryThub9boyiFZ$5 z-_&FxDzEaR|1B?|#$m_`@)zq$6@2whB^b+PZEsH&ShwOWwiiOk+1bAXqO!Q{+6Z$C zMTYsg-MVb?C*sSvDDG}%$s2da=cot9&1@f17zAhQyrqUt`JC^*166=%EL8;g$mJ5X zl$4pQtZX(wr>c&w%S|?q;6RL1coou%(;LHkBL`Z^mr;!iRbunit8xk9rz1AJ6}A{% zJv|fc9i6@Xbfr=yfq~S7ybY12!{0>#1FFCe>6xsX%ZK}n$?BO+RmPhunYm;7j)cl@ z$nHBeSZ-FA`b^qYuk?W@WN8&x(=v5cRL+b7htIAgtkCw`<*zes_sout&6(fywO9!4 zFX{fg>hoF6L*cSr^$pbj@<_ellK?O1*?3H~n#PF)?9clY)LYHOYd@NcJoXHxPllZn zXT#DYFN=O$IxmB2XfdbjxtW@5NR#(Bo+nKF6=#}5^(GrXK^5FLRSUHw6)g+7GwS0S z-^poO*4=><5Lmg(guWHyMwFA8We{nu^qFWX4==~iBkiBT-|tW)ikoYp7apeGcR_qx zxA?SG5xw?}JW?+je`wF#oDN--0D!?uXw6Xzcv$}kSM3>Iq^L<3oQh2L^yu@BITS&8 zv6r_(FjKgU;}1A(=YrQD3+=2YA>1M3hdbN%)KtA(X9ekA=VuFzV$e)|y#;mHCX5DU z#>;TcQ%QuLen{u7SIJUh(iY_WqS^ z@}}FT=Z=ajZ$mT%6wExJE>=Yflx6q&g^N0ICD!D^JxGf03CaDb>VDv#OQC_Vn6Hm= zLw&<(v(S)W9Wtl5B#kh~H`Ra!dly3?5Kh!-cPvd;k_1pd5Ewp~~>OtiYcZo3~}pr+R-6OGLuj1eJ4x-`F^Gkaz;f5WHWj8E}g;575uGJ&r5 zsOB(=Peh$T#(AuWaqFizI{g5APA@GrqoFKz3{e8hBBcCEX%@5Z|2#JkSO-U38F-o- z#-#LxhPK5XUCjVbe@>+#dd>(8J)7##W9{tE=U}%Z#1$hISP-J1J`Kl6qI6V@Qe#}au86gOfH~je*vlCgm zcJ=qM&xziP!~`a7dC-!{%s;RwwR*MeTd(|c_xgE4Au>ke%uP4CRW4Q%eVF^pAQXL2 zr$cwlhBROxZ8U4v5ociUslHg?S&rBB>BOM+bA`K>q3;_~Mt`!{ZSO9j(0h?59&&AN zWyl(HUycL{Tr=*}LE$u4VT6vP-ztq7j55vJk#oAkyo{4QfKd1Ysp-@Ssw z%p(g4<>7u7<_rxErZU6PD*C%B_zlE&9A?_)tvM|F7wh0H79{%dlrr5v$t8A-YRCgHXx{HWuJc4`!k z^+1pYePZmlJjz;&dD4jp9ATXD*mi+5TM3DcIfUdx<`Sv$)~6Y4PhbD9<}QIzTZE3Z zq$0sjcAFRk@juTd!rB{!#8wlyBLxkENE;F zXhRR{E04$CKSzQ0h3MaFrM(B(9c$~0ewNcE3IKya!BuZLxgVSU>SaYZz(nLc%g|0@ zHR&5@rqoAmN;U35d-(5BcrS&)$pr;nWkbVz2fM<@X1S7B{O>xo}ixwF^&-0nm08tNMGs~kJf${KRB*i43kssdt^ZLy2<=-aS zzdu$X{(G(e{izqrz1p#Mh)7Be-71J6T~b4XfON;uje&rSl+qnacL_+y&^3Ze zmo!7%=Y8+G-}f)vXDt>BCf3Yze&?Kh_SrjDOH+-4jFAik0#Q79to#B5BK`yd5z0Uz zz<-*0U9^B-lAbDto;t3!o<5eZZ9vZ~J>8sLJ)Iq_*u8CDdpNkh65*HRf5gXb@9F90 zffNvM`JX57yS}y)U`<)F2i}C#?Xi&u2*mL5KR*OdywLAJ1R&58Wd&W|tnEc&U)_E> zl6`bDozL`7!roRb)A(jO9%JUX=KowuVRVJP_qB2)dNJ}TEqii!+s5VqpV}v45fL|g zL4Nm*nwtY`{tjn|uW;T>p3=pZ@p3fqVwvyZ`eT zXR;M8<9}Z$9ryp=3xyB<{ZW_n_?xiM;J*5{^>15Hz@PDaHOi3XTDxRvzkkZIK5Vjq zDJg^f{c1~XL0boFt-c3&%o1+$Ev>EGRjt8S#@>5NJC*%bcsGp}zr()OdD@?5O`cDJ z&;QUHHG4nHX&X7-n*MWneyDl+gN+LlYi0yc583`+w-fbHKOM`(=(@WuL|Wlyy4vgoe4yypdrLCO?P6`KCp+`s<7gj9`(O8;pY+7u z`d9pmx!t;-k~GQ9S<0!>*Uztbo=w^pS9wLA{k>SH@Uqoq=0-F7ZnlmenK@h=0@0_Dc@R6Nd&=fAqV2jVgK^UKg@ zi_Y*Bdi6BE)ZN99_em^LPopVWu8w8|77LzzMEx10eZ!&TpDZiT7O*NCcDPz$CiUlN zLxe`k+c_E!#6t_C9(;9d;IP<~=sO3*@XPb;rJvti<<2(>vp;)n={xSf+Q)Ic$dL~N z$%u#B+6=x+>xv{FTIp4n{`zBUy7Db>h}6gPdhfhiXkhglDSVziSm!Wt$N$KlhWn{5 z>8(CjNmf}|=!xI=TD#HDz6Uh_j;GC=TW+E#m~{8)m*qw}Z3~`f=Z;v$(>+WB;v7@R zXYy4yO~Ab3>o5FX`v(d474Z5wt6Xqdc$WGH{?zVhs-bit>o0-l|2CPUJ#uCxUv&E+ zh;BD{ue4o2=%g4&f@hVFt#3TDEllep7+m()Zt@b6BE7+Z_CNJxe?Dexfz~3K;TjSMY zZ|_M!sOKriIv*NJvk4b9dv0)=e0!xAMj!a9Pg92Qu#ZwEIF4R`-(zjW$`r-u0Xiu0!06<({x3Nw^9Uj1pT}1;3W9LXttbAzc&!kMSl(8rcAPb zK?E)I#8?J5Ke%8_wC-95^6y0riu6Nrua}5*U&7BCn+NBAKf2yAszK4*c9YKivC5}F zuzdgI7m$ntKtbg8`}h0bm#)V9iV;wSR)n(h?JmK64dqTkq%O2Zw?y}e%})+^Ztc3` zWC{1LuE_rs%sH^w_2uF7*%rUrJC9SeG9NiOPt3}X9iAW3^NRHA$#=EY+MJs_n#@H8 zp2bY;Jj;?uB@;X6Hi3681BpF*X=W#&SNYQHk`UvFlFRLm9ifvYML5>KnJ5Z*WJWhSR@qYyk zWE{X|I5&ImcuRd;DDv4`+H6g4^gk}S&8s_xHFaO zQfwmxPm%N>OHJ@C0lw0mU6bVTj~dlu7<}RHGOmg;)akxXfi8Y4hMG&4oo;PdHIWg& z)p+dACRuYMY;@pP=eqoIXM!gnZ-c$~S|bIM6vpMFSGmiO-s5+=J?pXA;BjKKauHhQ z0si{~Cmn7*^NpSV>KmnD(6ShTZceD*VSUqsDetb22SOxJ>LpC5@ndaE;Fi?4B{WA?1a&vGCmqr zTQ$O1x0mDvkG7@_t{Q&kDv@l=jKAKE0@``$Ps4^JcjQyWy6YA+MnN?5XwecL*1^jQ0>iJ21)IAQim zB>k?M#GE@*X6cnHp>=ytOB?8wDR(jt;C>Tp9 zmvWU7u|XC<)hQX-?TU;ywO!_#0iJ_84tka@sC`9I?>;W6nuejQLx`MK$#JE}l=-sA zH+k=5USJ+M(0p7|i$ZShaKbG}rK7+4W%v1G215I<-y*#nKk|j3DjY8Y;uumhw067f zkQN>C$IBr%SKnX^f*qryVS9so`w4H9JpK;9H?DUq`H8UZHx_ml9ro_?y+Fy|9A zv{GMaKGFUqYCrR%xa(_)q~pU4kWi9CRy1`>k0f?>i(~z!pDjcJ1ne5Gbv%g+&M!PjPjYF@cS z(Y2`VQ}>5dm^PZdskKKE@8H{~-~qW3`bEx0m~ z-Mc^o!6H16>PHz#Ml1G1UTn9}H^`2_oJGQAs;u$zvyZBmU86^VhlE_30(abaL&AbnoK~68vnSMvSo~})sFM7$U^|n+Iq1*FsNu@46Ty8!Qp#E3Ht9YkvxT3R#{Dz8S%yvkW!A&a0;Rc zxb*}>|4bixxJ7gZ;`sPE^PZ#slM?!ZdLBJ7sl2sx1h>Iw2>=Ma3skueSNI#P8&$EA zpfLo6^P}y6lWiN3XZm)1ITL69{yf-vXW)4PADB3PkZKbb(oDt}+8qBdkx}%im+(h1 z7u&;AAcu|UTT4u)TOBUeb!UC}#)@k+;vd>vq%Q~Kp8=qY55S({oV-i~&CwW6`q`A7`oq=|6s5kxQ3}?f;yNTQ}VU*G}KmM%=;G9_W zz`w`GMIp3m$*kqbH#>9n;uJ4-W~$AC+WRbs3%rOR76~ci*FdYEl#M9^`qV0LsCt9P zn|%b{Z#XZ!Y`wuXUk=pP_qKzfidU!0EFsgns{8Pa#1S0cd7_}4&rr~u_E zNk1a|YemgywK=EMq^y!CFoL|kIC9wPh@pO(WZ7<7Yg^uZ2;7p8e~Uh|dQ)-`;$gZZ zQOIRAl?kL`pD`CJx$^giiKF1H)BXez{MU&!4XQW}>)_vyr~4JOx^UD_V=ZxhzXwuZ zlpA|}^ngJOfI5^%Lm+q-HjG+52wP=USOezGK_DcPvQb;ogHDz?0T)Lbn-bzu6+#c- zXlbCgAI=QNjL!D|v&`yMQ9io=U)V#rDd@d*QtD{BrFvLl)~tJF2i8vINFXMNh9Sy! zqxnoRhzONS_-&PbG9&#HjNrTHg0={bd|J6a3sSl<1vN@hH_Rkxa{dn!xUzZ?>dSyu zSm$~6N&3mNdKFc^0xZ|QOznO2XZJ|lhharEsNA+4%A6+2S9)S9?nqmf@v=iA6eP$Ew$>Q8G%7lYq@YFuDdI& zZ?10L)GE76F({UXyJnZ9RKNV4CI(sR?1;ZAHg6B+C2G?vof7-T(Exk$W@aj5YM+ID zAd&rD5DqcnD1@OWAhE_7k^CZD#p7SFcl!a+;??@?+$p}yPA+OBe7L8l#7;`2dZNIN zHT)JGc@6hfeQ42+;^7p5)q)_D%)kEyGvp{~foFaD7%wlFBM&)}NKh<{r!HBw1Z(zo zU+#P25VfQ!oojp_Z=>Z#AeZ|LwR07pB0Sh?EK-dUoG3=9FJv}clse#F#OA(UsZd1x z3WlDoJH2};czf+WI998#>+uJr`({mbM|*M|940j6(wtIeR4-Rr!g6j6@@6R9PoF9` z!6PZyjJA0yE$`2dD_rYw9>lY99+IA zF{mu~M9%WebJ&wYSHlFGsE?A}@=sjH=2jG|J3EjW~MhJ2Wc@7C}2^l z{VW-+zAwR?mv8^XgSaGy{wW5TcdXg{-JA^3i`KUbZ04#8Pa7Cms@ zMFlaj^m=am%y>m4%f_b>3&R2vZ%HJ1Jk__vp?49+)8XWEtwy4^3Q&HE=0 zrAmd8JDK0*jFS)Ky-Vh|Tycg|W*18>VBW9rTW=7Vit~LDm+d<54%Pw@6{n|KJs0qI z(yt*|mI@yN5dQ@d?LiiP#DMDpIrPT8&w5q+a;-z}Y8xZvQ!?xmNHT!yubAH2o(A?y+~Y`( zowWgmPXp2N17rIeoL{Hjllx{;tkatKAW8G5a>_7;il&|)veb}b(ksExwqBzmI0;k7ja?(c1by2cT|wz8;s1XGZcIxP!=_c z=ydjULpK%JJPz+*9;8@k^iX?WP;7dx_Kh_fCJtub88WwU>x6Ayu*&!=14BXyM8j=~ zyi7xM`?Mllo$*8g^=ENjuJc#jRBTpLAIJIUg76od8uaNb0 z=qu0z;X3!E{(6OM*8`b+ZcSM5&%RMsk}GvM+JLjKQcY+x8*qW=KYVDXKt=XzDYTz0dM7w-IUpiSK4 zy2WPyJ4WT|_1|HKD^0hB^_mAW$(SE29oajhOgvIi#Nw${zhb`@L|vTna7BcP-_gR& zMJRaGO$b447m}X7=Pkj%zptZeJKVwEei-(wF7=bNA76KLveuVi**0;u*&3U8pq6WJ zr{%ar7}++cVk^x0%j-3^Evvw!>_w%Cp)y|)v7pA+e{@}f?L?^Du3t8Lmm8xPle_V1 z^Nb>V+U83F{T$TVkGncVsEM7{VOzbk2^3n73Q}dl6Ko!ZKF}}A2`?-nO^h_YB*IkC zJv4s&BU7|I#A&XsxIRA)q|_ZnF+S!TB~*oaTUK?qrHLe&2%11|q4kR56ISoo4CN(i$E1rUxBOORNF>A|MfJMJM@ zLo>&KgKaA^hE`xe{oEqHo9NcujF&mt186QuPh2WN(iNQC^xOtSt^m$b(k^W9-kIA> z@8R?VO7n~OAxIrv@rZBV+HX>ZszsrDrhAL;ZMR_(iaU03*q?;6Ui!cY$oC zir93suuyX;zPrt6!b4+0vlY8zZ* z_=M^xnTmdfJBl5&vqwdhlmKn_Vz&L}(nI^bf?~&aqYQfRy3jr|m+J5hRZ3=vn8e;{ z`u1{rcKqn*>jjyh3uTwL{GB-G6oO@1x_zwZH_@P%Li6p_dt5O|od)1DjEs2AqWiO+ z;ro1>2`bMH5SwA)!Zw2?y>{B>3}K>p0_Qr{zoXf9Kgy^E6YRi0s5_K1X!)k1_gHE} zg;9?MfF%@}7J~n|d*S8tbip(a;uGrOv<|C*&_i*@(D~6_#ahGRrDVhE5|aIV98}Y>Q1X{#et_4hh-J2<`yG64>GLGXop`uO<~VPe)#3v zi38cvKQyj;Orbnm`7zvsyTS|DU^O$s2HrpT;89A-|%YD|KlhXhHlkv z?^Pjml)|>YDiP!1secqSJ}&P--$Ej-CjZMr5kB*6*aaJ{Vn@1sOE^+1ni`WnTWeQ5 zY-|v=))s^>38Qa$-lEy!d+=v-(rJp%1@)8wsR)H|K49roYsX%5s}=Pot@;n7lNbr5R|H+8R=j3I3aQ=t@0QkfnI;9F(-@UJ2mVUt$&cZ`$9T7@6!ZAXa zJ%SVIaF5oML+*#3Mtg7x&!pJI-cUlbT-{iOD4RIslXJqPL>_HhTIgi&X>Xlji*>}l zaTQ%$HgXFB<00eskK)Nlea!p0j+N{p;ynWfxQZYJ0XM39r73)m8Etanwv85A7jwgb zXn(6f1fSZlPt3G?9aFJN-&q^M=n( zmu~Jt>@jkSr=%3Pg;F8c(Oz`JD>pphkl*ao6h z;J2mCqIwv7`>M?fG#n-n_Vz>UI&6FQc*4WaU?8FoZIZ8l#~inhFZ~4IT3M1wpqe3- zvH-t?BlfYE*-uh1k9maH`nP3Uz0+eXzE}Q1_{bSws!Os(Thz4j3Z#NW!Zp8~QsRU4 z!&~4kpPjj#I*nVx$;YDlB!ve{NUzPJ@DT|EjvSXPy)chw`KlC?lJNP@Bz2~p)nPdz zCkWVrWkH5);Z_eFEYK>&@9Kg=Y>gQ87|8uSnrT97KSW(3^}D_`^d13XNm<#P=D)a8 zzT6t4lFLCg_ucaLPCFSDP+REqS-(Fx(K0TCe$HHPZGskWTz1Q!3i3=_yp~Dy7E_QGFJ=}`ic0gmxeS7fh@X` ze*YpiJ2=yK`Ale+xsXpngWws((0_hp$cQisqh9e>rc_7Mot(jaFkw5vn19_=y=G~$ z?j(@FjZqIn0l^^1f4r@Zj( z5v%Bv1R~hLm?%HL?>drvrgy#TCNpTjQu0Gl>dDDsY1 z0l@}NIV*;O*K#FReMFG-*N|dpnCMniHAf+lnB@{!A&NEC%N;`CNV$D@ev01~7<^EN*lb z(L$nL%)8d<32VdoCn1DvxwVe$Y%jM385YY;#P!5rwGM(hT}BR>iA)b`zvx#eBa&~I zP=sA97A=-fKkaCX+L8!&<_WNcNm~n&hiSpm8cPHc%bYI3bg(8u+YHNJ=3uBhv&CJq zIV<0gfx%aENB7=`#gF!z`SP8Zmr;b((}UJ5udcjy7kU|Wy@_j=$0)mE9G-s)9KHkW7vI3fAW~kNb*S(E>P?$Gjp{>E*+ad#A%4KRt}ICb7K{r-Mlh4wGIWB$wK9nZRKb{372OKj+%J?H=#-3$8*58IZAj?J0~}}PpAeV_=RdBK5aovSY`8F!s(RJ+GGYdWx`WH*SBe=e{7q9J?)QE&<%$lhOK zsPmix(Qt_E(=@*O)LfY)#E-y6g1v0n^n|19WgOkaQwwF^MTK9L}c!p5K^1I5M8zQBS*nbX5h2aa5FiP+Cb7Io+_LaO19EsBS*X}upv_&ci4lIGnUAI)u$jJ^m z!c73{`#25W05X!vWT`bP4f2~`(QN+}in!}r@9Wt~GmUCDq_GQ@1YLR29a~eOU1$2W zqK&58eISS-!{5^|$Yb|ss&sb<)fvhS<9>^caoTz2!WCqXOa5}qqqM& z5p1wxwpTHG1*19Lm{1ERJ0X=}M;~&9hxa+~~)d`5$kV)7kgVQSt;uSWiJoi42_?)*hnBxB||u@X18DnGfShA0IiqF>CQGx{niMg>89+XByRfI(1`q;=SNrs`$Aj@HvI+4Zttz6yRq z@A8Fg?|yh-@N!27+GO3a5bRB0OJiX{%iS$#QL$kZ2sM^6Sp`k+09(sqm)CY->aedk z59O3OMBRDy6+?sCzo=jrSOk%6aYm<5elip1|HHuy^uyyB1<)!BB!9sgf(f&JmZK1- zNwGpTk`BwiD#ybuZQNp?dg)3*opk| zf*&;np4XIkEPQn_`JQ#|^wIeq*kzmP7!9HDX3Xl^IwStdhOxt|l#V$l+tL&qS^AfW z%+`k7ySWa~^D+%0*Uck}&1tNyA;=~pyuV&{@5xF7h}zNEDp#;}UUn+OoAq|TPh1!= zYH|Gq!$7c78p9^k%8|QZ_&X$wm9(I)dMSDc{=T6k%jh1>dS}opDY5V+RXg-=o2BZZnM(6mxM= z7;Vq6?GP;yM7|(Ek(T80S7&&v@lBNM44JZ9klS|3Zu{^&h}dJ2I|&oN(f4@SbqY-nlVrT zM>-9LK@sS|CVf|}$!{uYe2~X#Nz7l`e3%~qypSG&(6E@pLPIqfjWymg=muEamFAcL zf`@XVih-KGM%XcG;*0v;KC;M+cn0HSL-s`#`1{9-rToX=z90WGN2_VN91s z4TF`s*QV+mM1ZpQjh8!WT1>IruS!l|D}cqV!jqUtX160a=@4oA&_ezPZD9LZiy%WN zA~MuS#<0$BeT-w1F=p7^6rC9FMME7_{9nnKx(|DTg59IMRO;~izUf`kj#&Hm55qb{ zkTdpf`LB3{BgYhW=BHnT!1+xZiiAIp7b}i|F(zOlFTok39Z8K!r`eiFX4$}=yG%+- z-l^L1^gXXh!r%I~^Qf%w?r(qC)-dztDk2&U-O~~HY<{OFu z|DNCo4F*)shAlcGC2>zibutU{u^1bGJfljz* zy+4ov%z7o6GbiLyjOIiI8X1f^Z-{?0pHMH;DJy9WLMPhY`GtmjTL3kVS5sSaRZO}R z{0yLutFx`#e(>#=)uycWoHr!SyhKs0WW~$h4rhjzA%{Y1zjn?Vzn0ie5t$?xKV^ zEn*4yF%G3$K~h646dKSa_U>CmL+NpqmX6XDX6aL}h7+*+Mx6Tz78(2sznNe&Ipv{O zw9`Sb7gnJ0V7Bhyuq{d3=avQN*8gz0VT7oQtw^awA=jlq+L^8#LgA%@R<5cf8gASE z54B}BsBo^(L@z$Lmg)wfh=iiHwKJg8DKD{blxTORE$Ab1&ti<|OR8H%$0@qZahPws zYM*5=fMX`!i+f>FT)_M*>YUSXmra7dO8y?>$pvZFjfh~D~8fAAT;dJe6xB?w2jiJ#u(G#aH-o;NDWuF_TUL z`T`F~G-@0$wkX5Azh5fMoT=j9NtX5|YWf2^bls`DH9wE)?B(T$WQ!t=%TK=OE_|U6 zS9(H7!NPaR6sFR;kD&Xc{PlB)`iGo}L|ja584~uDUBX`K{ep0Q$E=mdZ*hi{*WS#= z!c(ofpxbhQxKNwC*fO(@|Ccq@M2bPVaruboS@6y&1-Ni5OwFRganjMT1B@^?lob2WqSyJvT2z91kt*bI2hlv3hi50j*fSD~JlU;3zM_zetV)esl ztWyZ~NJV$t&0)O1nDc9UsI$7AW65X-hXAFdt`;=D*^;`evhB4%!*8-rY{w1M>na;O zsF1EJeA*$yW*QPbs|bB$eDbY|(ra4?=ahXv>F`6_U(TvsXL;~iGaxOJ%5ptA@gp0l zl%PzdE*a9y7#ArKbm|MPLZDL_7&@H80fBd;j9dJyqQu>U3;$FZu$T7pz_FY#-uZc! z^hC51)6*JUJM~OmB>~VC?!Qk=5Q|@01{AZ7$bc>wD;g)3)cyAdW>mKW$3RCnnN4sa z0M5|P19#2?%vDiNU9Ay$HWy3BQF(%z+8d;UocN!WYOmb&QudQ1BMc#EarQ$Olc>Cs@f3H?AQ4Ocig_XRU4){FhI4*$t@R z`B!$PV{-yy$sH$L?1}Y~RSmp!9(|$|L6q3~K4+HNJG7j6DGC44y-aEOoP98#E7639 zR}9>qKyaLt25;r?A)gNHD_h_Ib;FeW9YRm!$4S|ac-TFS;%@82Fji}`Cn)22JNaD8 zgb_(c8j?r*2~FK21w%xP%}usk;kkwk#N6Vh3bypJ4>W9&;2bSK@IO0w9;HOz{38`vfIl*-mrbZ=$ju7M?oZdC#5 zJIz!(hsm%+>#g`;4xR{kK|*c+j}_+a1y8wh^f_@?xBx}1A=z4TgaZ_YENzdAdu&ysfo*Kr$>%D%k@8g`So@5*Bi15VI3*-`x7Lh08(p?e7 z61J%vQJRtkC;4fQ27Pdbk4igBIkaMfZvRn$KUOi*&6?2eB2;iSvqlJZKpDVT!N;VZ z46!f*d2omOOGQEEiQlP~WFwG7H~y|;bpcT6^4k$!mA}jtYBS!?Cx_MaIUIjZm7coi zs`0&!yMo6{Xez7R;<<;m2H|A~B8`?6I7+~NHLj;3|?)w~?PC<9e_ zI70WRMeID82fkgOq9@Z4t=!f$y)neJ z`l?VtyEUJtgLG7aTdM*@@+s-ep84B*&tDKYF}E{7$x3MWuX8Im`u-_ob1^%eb+UhU z%@%+RFo`+m(CPGC+-+hB3uEY*yLWP=SlB={rSDFXL|*3rXn^9{2U!W8c39{`Y8p-I z>~l}l`IwWe=Nd+xB%WK{^mz?ph?y4=z8y{%Q?I;9WR>2zb|$;aGoXDXWC39-Gj5ul zlT7o!;Dv(1ly6g~-ce64D&fq$V58N^ikFPB>m#DX+Y5B?+Gnb-Mtx_$o4Cgwt5|5AQ( zttT%sv`h_5TeVmukTh2gy?mZ6`)o1kwD?qt#hyVhr~M-FG=7iV$oC}7?v$S4(zpGC zz(WE%$R{}#fZt{7(SUi@08Q3Qn;Hk~$Uh9~d!&iRZS)x~kYT($UsV?w1Dd(=H7P8c z%Ay`@jKeUhEJPzZTi7Pf7&zHy&R(yhjE9QLy0ZFUO z#X|B`r|f=0@gdat-qYWw#-+Y&hQkb>fP9-1oNT`3MVG31tY7j(J__7*g4rp;F zK}oLY3Hz;dV%(!ZI-Z^I6Ht;mil0kxZ4lUnW39TcE{+Z60mMXLZQ$kLb zk;R6o{w-vcCVx6}qvcQ0shnkJ*0}~Us)Yp7MTA($^!yX2ZR--vTRB1 zcb_qqUJ<(JSZ`37<#ju|zr^Cjm-<-@Q)mV8aKyxjMHKtXTOtG7w?wN#@?r2nQ;WZh z6|@Yvj?k#tl`yEc^-Z5s6Pfg?tV9UAd-o(e>xiqpn)L~Fg)5KOZ`fuS7b(#+Ll9*b zUa_Fo2*n>ZB_g5JQeZ~;=MhB> z%c2P9#a}0e*)PvL7gz45pAB5UZQbjJ+TxJf0XX2IP!h(`P*A*-PTyjf!&%!TI609ACs$D)P39BHiDt*4Ea^?wK9yujOy9 zCIIJ!PZ4!wYf*HuLwT8hnFxsayIz!six7M7&G4O?WM$t^<2z5%f zm`mp1oRXHR-31rgh(rgzzStHNZ45iYb)sLN?)J}pJb|xwcZD--yN9^-1)UtH6MUh9 zpH)>FB^>fWDBLI%{D|IR#?V1w6nL7G?4u19`c;vCqksrpg=Ksr?+Y|D_jar+Q9EFW z(qGfG9t!~MYnvDGPGPJ(Jpq1Kb3;urIjQ^)Qtm!;3~5aUq#7k*#P9CA`54QM^fNH` zoW_C5ufiQdt?j1DW;bVga!8YW49IbfN&?7D{&(qJd7(vzrFH^mVrOyAUM*2U8IX6U z8Pa|=^({n6v)LinM&UG$cc#lt)QFkguierT;}JIYQXojMtY}^RzrR27D|XVoA|p44 zae=XAMuFEA5Dqu#995Avl6Q%MQ(7ch{RC^)ScG_&-_mQg(=AbJEt}s1)FsanI-kFT zia2%W9{mLPR@A@W_bDsMe|)?tBOGPW?aC{{Ig81k|D)Aat$T@`E#rgEsXm@*@IN-M zm#A|UG2PGNISo4vSGZeiO`U-QCJ^nlUAgO-%8Fvc+cczL34`3^73Z&^n#>j$JI;%r z|2_H;#&RexP*E2%i_w&rAUbNs%ykBivjm(9pbj>(JwB%m7lT#+v{5*?DT%WHVpSp` zOkO>3o6cQm=qHq{NT|@sH?N}d6XyZ*kJ8WAN%Y_grwPKJ)Gzs9i?kib^})}CV$h&Dpq6pljhIe>x`A4% zJ7$_qUX{ZBi9|&IXxVL~ZuFDNK1=^|;Bb5fJUp7;5C{}Nn;6U%7ah1ljCzaix!gCx z&4x+ar8LSq{8Xm>-P-}$#B+iYhn=yis%aA8W(*P@b_e2F?bZ9~{NM6167HtU-8(-C zCFG<`B*X=Owfq?g@LH3-IEQZwk)P%0i_HjV{^cKbuC5H(C2M>BEqcd#x904Y0veP%&YJdqVS1|Hz2KlsL7J|jyR$nmVgHDoQR)!D)Cr9Eh<^d zF-p-+_oY*HBhjo;PZ|hhNie0zE^Gb7B${$(@|a|HWQ0zn@6f`w6VOTMf|$ds?7mpR z*B=_yd`2!CR9xmeCNnKRfY1vWM&yXtrX$cJ$lR4hsd<6`*gFdlAyK6JN{?P=z-o28 zysTr-s+7EgoXjf86C$2$v^8t}QT5~egM~<^yHAaOops8;(i&yH!+C-IzXra;DfA9RT{V_mZmg~H&j!BzUG$x3Q;w_L!Vh~cax#Z{wi@F=W;Lo9e zvSr^Q>|A^HQLH$Lp~ebB9N`I?nGN^(6y9+!I04T0TAU$a`=Q20rBQ9#5PrkCmWDg7 z6m(0L9IQWFBen0h+1D-wH&hX1I;J20B|4<6Kq2Fq`PBvAAea**&zzxRcsAHRhENpK z{POgDpxF$Gjv4h#EdAapVMO{m?CBR`(E zTpB8X{BE1!V%P&yxSwT0Q2TJ$*3?2l1N%)anyFJKD41FD_2+j2=9)I_%S~Q7HVvFb zVeHYe-tO1#c68C^NfgqBMq`MzX27IeJR%2y0v#0PaoxR^h?A))dl=X{BEn+ zE~;?lGTC{Xf@x|90L7Db+0uSwwD3+oa@3tBlYdI1ZXKR3m+1%pE*fI;9vH&u<7n{; zioAg@i)@nFF+pD&3oS3CYpR8C;a3#XA=sxU4}m@?Im=BSWbB3UVvZ4T z3957#qKYk^k71*|7??(28d5$!Lp(Hy8m%}0SIK$R8%N7KPrJrAeA#6&D0gG}W_Ph! z>Y;Xcry<`3%+O+Fg6O2EJaN_kU`54opCa1_;Jt{W`5{=ZR%_Tg!p=U>k>`D+N|xPl zdd1Xo$I3?rc4gclQ1`1T=Paq6?GWRk1WLokU4#WAgD-O^B8`ZieZ0Q&XYl=dz;RQ; z&NnhHQJBkGXI}d{)j8~hcCKe&Ma{1Pa0i#n3NTbDjKYk`GnnuRTPO% zSgU5hvbWIeW8gSlk&lcSvFPN)Qei}$3N~|cC zj;&kzjpAF%D^k4y)A7OQC#--DBfqB7x+kW%?YZloxaKbY`len56Y{C(VVp{bAh9@L zNq)mUBwlbjQJ+)2ysjyQe9mJK+VQ@Yvc&yBQNz0LU`U%o-5>?bw2 zM8}E~R5hO&Hn@nBg!}ew`K*s=^Xe8A5%vsR_EOa>S1}l62!GKNoPY7OsGaP!#E^#4 z2|rypU<+Fl)yVC8GB~6}$AJyy5$nW}n0b8KOb? zA~XHwExz~vJA z3Q6*p)*){CY9^!U3i7mQ^|M`o^vHSoCMQiUi6ra)u=kcF!QJ z6i~X6Zcs`(1VlhWP*4d0>27JHLqa+P6qGK7Nlap$^I2;>d+dK;zu4n9Uw991h6DJ{ zYhKrN9_Mjy$qrsxWzru&q_kuJ_z^XkV@Y z+gd&@GpVd{JayG?%?=tYilP2pJG|QE#`cPBBQZ9sSHmAf%)WJ_1C`eaEHke;&%x$f z$m#u&WBU!K8+WMfFwHHYF6j^hvU`Hc)(ZYP{PGIs%Xm=6}~8Y)$|*M51)KIus~YB8_+8Estu-}OT3FV{lpw)|6)zC z`xW_PPpiG>%%Z;u#B1@onV;d(-a;w$#E>vR6Ta@KBUci?(pPg&#GfBlq zeFCbMVv&D8|8A6P!$y+>)UKI_KuF-0xCfob-5XX4mR--cnDJZ`DxN4!xMGv<&#ncp z57P@dV;JzQ5oBjaEq$!&V%kq@xKUo`gu*9ZQkHEasC=rqBg?~h^o3Y_7@nSI#&-a&u46sDIWl53t1pI4H@1C|+WYXtGG5;GC>5a#-x`&yL z)voU00ja=MlId!zvCbbJYM1NzBfI?8K0#!QgE)#hcIAQo|fmYLaJ09E8}>OG`{lV zKa&hg-uRcp*JieMaMOh(MRNb$;?&pt79-b2kwM7+=|`VSmu#p&itlUlj2U^O-fD&0 z=@HxA!C3rHx37NTWqX;*?CWP@z4hx^M{x0ZR-7_k_r^0-KmwSU79BjlwJL|5cxBF( zUa)&JOSpFCIc>WKt&j4^jU%PIoC3X6J$xi0E|N8`F>!M+!V-5Xv3bV|Eyj4cqRi?K65aC9H#vkO^16nrMWfT>ymJ}3k5KIvGFY;3`Ct za_dZ*d`9m;;87X^jUEjTd`u)S>lOo-m-WEhi#)kNF-e0dd~a-d?>*S^|Mh`oI%B2p z%?YHxI&25teru85fr`UL>Jn76`9KYTkQ{ySJ1QN9MwkEj#eDc`XRWmFLGOCNd!ec= z{C7NNNHKICb`tK-q5%ZbRiN7)L!iF>j%s43H1sb2p^cHlkdJWQi9BB!FL-mY;N_pO zvx5&h{olJrwZ&s|aIw#HS^V-WZ|rF0_wV1k&aBU-;})BqFXugb&vE0lq5*^O%qLEMKW^KPw5(7|?#Gbck-2kxvHydd z>$F*X7#k@ac@(#~1lSOhK#Ka|?YX)D(-!~rglDBKbnvZJnrjgsZ|u|7InU{yQ?JhS z#L=Tp8ZOUHo_RKpxGXfS3j~~cw*cvXU(b_1XOjK*Z=?y4=vw^0FS;(s8vOV5{3f*9 z{`*?dB9OfC->)U8ptENGef`fr{GW&OKP%_|`%i~fFq+y0SuiF5MS1vUKq95T!m{V= z@C;6ZcuXHI0*;r@{@3BSF9E39gSAl;@FH6O^LBN0eSWx+ z+!S=VaNrIZ;m2i1$ZexK(^crUeb0U1c=B&p#`!`TeBG)1)|F#ZCm&6keRV*)We1J( z;GZS@$pk{xvy-%+wO!G-`ceh*!oIaUgdM_a7-(X%b*^`yErY-J?EC_?`@Pxe`jhUaEfC*o0S{3x9FeT5i7dHC>)_e68csKPy3+eI&$&mb3DWDp zce3w$$CI({M=fZV36Us$cHQC?B+kYDau1o1gZ5$S3;q*Xk{poPLT_^DYD1drx0?j_ zj;X1s0}8C&w-cC@yr^D&1j`=SmJPn=s5(BIgSwj4tfdKvSY`z}MYZ+#i`Ouhp#lEp zN!Kfb%a)+azyhPX*JW{wkeRiDV*lSk_aHW@SdZ(5g zc=OV*HLw{SXhiP(;Ha<+`n}pSlrCk24j(IKyJ6{?di5qB;RZ({5Ax8&y?cmOn}aD8!k5qAn=LL2g31R zU{KY4Imd3{G%J0tFdz6%9-EU7*$pPzbGT6Ex!)zV4FK~MR9gU9+u zX%Xo>U=69kdOUA30UtmD^o4tYRj1=O&jT0&?iz=Q#Epp}H1Dgcm1Ill13k)jx}5$9 zXK}XBb)m`qOWDwSq5G%EZDv{tuOzcT`#YZ$BwTMSf&g(?urXP32YTVp92@uN0USnt zIotF++8?!D*Rmr5eu+f5`Sfp2nHFk9TIL#5* z)*(_;fBq3KX?19uQeAj?Xtma=@6lcD{P=ZeNQTG5l2iq6JuT+2-4({Y2D{NG1zOp; z1Gw=stF&D2w^tED4mS^}hu=v)e;V0CE9I+gU8)%MX)0hfGDMenFUG` z8jAz@-N$aAi9O%{_2t{*Vrx*II8pbIC0d-^cNw;I|#(1ab}jh%nq>0$%fu z6#|gy5tL-9k~2Y5%Wx+WKXd;1Rp~GAA?};Ym(S@m_8^RRNB_uuxZ#X}Jw9>)FrreS zy%=n4?C&I2it+!Ts(jSq|NP{1v#ew|OkI8RNlf z$y^fBxdA{jg}W_41j!w4h3jM#ImcUtCB$N+L1pYHuUL%){y zrANITr^^#EYNObA;Zk?=Yd9gbu8Dr5CrSrAo*Q{ROtOw%jAy&8m)h~faxB;so(k97 zh13PUcG1VGVyn8#DQfaMjK_sMn!gqMXnt6F>DAmqvjVeV=+;8>{a@X&MO1ornZIiG z0zJa)etyN-@QPVsva`(~4|bNdq$n#}S5{sfGSR*x+tGCfSz!p6=|mnyPo;^trc?E) zwB3po!5-YB`=0Ab*QwYo4-7h*&?p}oszn@p3hmG2n)06l&rg35oiFT*QHB#aU;7JN z^0c6WiAKP)J8iK}PF9+h8GJ27n039ScgPZ3!|x^Puo1DVm7aNz_Cn02{3U=Z2X5`BL9!L=m;U|xZh0Vb{@ z^9`OxXW5#cLWzc>hG{ZbTO|B-MgPQxAba96^pC^{4&Yc(u{K%0kU86PXTXZ54V9H5 zg*0!@T&qopqBm^kW>R#{IiK&p%ik*XcEm=FL2NL|3729K?(_)SkobRC6-#G#2!Wvv$*Pv@Xc`b4*D6g(T_5_Z+2we#P~ zw-0){rN+PW%!O=jH-yd#)rR7Eld%MVfdRV7xvkrBSQG7XXhNVwMl zMp+uMA+}jOSGZrKV7{Qgl$7yclDr#LW}`W83G zYyX!F(Jg;$Nj`SxB+2Len?cOkx7x+9Tc?T*vZ=aY_kG%M4*RoSYT0`ilFm5#;oGv& zqledS+|JJ|3@`|RsA3-^!nGN0fCm8_=Ss;muwYshn6YQB+x2RE5H1Xj7T=Q;XNG+4g_hhS?D_5cT1)4Q^>I_xJUF4Un{YvM5@&u-^y?MLOF1DRdNH6`hfr}~~KjUZK z{8YP>8E>Ihc7yK)9IRr^#0y2F{2z<-E=5y9oveR|2~MU$Szmf6+H-O7O=MB6=XIJ> zI(|>ujul)|!v%dK-UsnV3N1G<2c7LVUYAsPy|q9fYzu_U+YqtcI}D^e?vCTI%Us6pGfU=Uf^A6pl0cQ0LFP&4J^QJhTd*=? z0JzcRR&-)JT4rW8@mi63LWd0dl%|^>Vb{UL_KGYN-VwLd^H-e-FRnRX#hw4$Du0Px zvVFsTWQzB;;4y9h-ZH0zZMn1aT&+&P?IbS6#a=M{E{2&c;-5Bjd-=nv(B)U8_yrzr{pCcByjLX zJ4nftm*94pgBeR1(qkuAvI zTg|x7USC4}n6j&@`ZjjcpaZ}bc6G?jt9evE-7;wF{YxKo_Q|EkE0Z(@942s|jD8<+ zxPL*fEh`>$fk`IfLNh@kZ%l~5w##J1nkdG2IeG*r(rIkhL(Hdu%F;Xx9p#lx2!dGyI9RSw4VKq-kPl$J9VX%rRBHE?|-YzOqg-R(^{RZ`H6;_H9sz9>YwxeD>T)F zK>{7Ie^{47b~>Xo-H zQSNayVxVZ!=K_})%8xt;Y)NKsKf@%{+OmX_qb5+W##| zA=3KhAsY0d(uxXGzjsinanNAtR#gTp)}4`bmA*RokyN4AH~=xzq*a+zf9 z4ui4^zwp)g%%w;L)~0~kXBbM=KVb3TMyp%xP<$#BOGWJoKKs9-@-j1iWjvy0iwrSH zLslV#^aOPF-}gRMek*p$CaPOOLCkP+?fgbH3QWe?xFPTna+*E5eR>l*#HW}Hni1;w zNdbHK*k1B%0<5cMjW+VDs#uHCSK&*r%;0;}vHx%FCi- zDhYf)l$YaT*4c4>;Oev)Jg9a}*~FK?bOCDFV^%^pt*N;1Qz7Q;WU+pzOkBv+j&`c~LgxL!Zn9jM>Sl+YZ) z#&}Q_u>khM3D>IA@qDqXcOQu^{Gs;OK>PL##`C>b-)RL2w(xNghdlQ8hO~)pDwbW7 zg+u~Z3~sjjWe6yZ_g07K1^f=BUtrv_%=I?k-8^j^+eH{0C$sBR-$i7_Edl{pzmB{I z&mmr7GiB-gJyZ=pD$?s{;>oVulr@^&cWKxh5x) zzK2Ln8>}$uwT@qn+p|I|oFl63#Gt}4ejKVja4m?{D{DGCY^d*Lv?Lolt|08jXR72R z+3Q-|<{4_S7V`0H)%h$PUo(@hWtn$0cIzoB!G!jTg61x;nKuIQvevg{x4%H<$}r3Y z51BRC0Y{-T)wSc;Rery~X3A{7IRs%ATzK=ZRPbdRYJ7e#U|U)!qFbcQhr zN5C^71QYFxkW;tHW6HEE$fH%Wwq!;j+T{oY8Qh|d@&v5iQE*&qw*miPTDZ&|0=E+( z<(G84HTbF9y$wWo<9x~^^IH5_50|cO+(DAk0p7jh&cc737 z^mlw7$DzkvG+gbm9@nmq?W6L{B@l84m`NwGoeV6G z%}dCuxk%T9iA)*9ume8CAe;w&JSS@rs74LWo>ILwVTql7m-p=VkH(|PwqmoT^C!Z0 zW@mQBZMwY}m!f+}Tf_W~-=F@3SnZ>$B7A<}MY|og$L)5`~iJH-`>9K1X zqPym|xo0ne&MM4)OjcUoe|L_}LI8Q;nc>5{D9Nb*T-DS>m$F_CKJcclsn4`Hm`fW} zZo9|MFApSl4f1>PB;@!E=7nhiw~M&Ng1aNDzLO29e-PH;Dh*EXvvPcLom8B{X3q63 zPN5@Xb_6AN+xmC0NgbE(E0)a;w$Yy$M2(w#+{d(z;84!L?Ec66GP3JWIlK48*$Euv z>$$Psf*T1BqRH^VV4>5#4!gkV>GRCx(7V3K4G3CQs+06~ds`AezU7H4k~w66PO4%Z z{rUD`vURzNj^C=>;o$betqVb6zqK5Tl@{YKf7wZ#t}Xc$Es605mYFo}#0OXm`gC77 zF1jCliy++8=_bg1`w7GE&mbbw_bXoKrW}c_wdhB=Fj_(ly_zpS7o_e7IJbJuVm6e8 z)g4hM8^q#rh}Z9^S9FMB_OvvP?Hg9xSqSuIVQs_4%cRtzXK=3UE5``*tJaZimFhd? zcKh(dX_q4qAM~$Kj~((EIrb&8ZuE#Qvl$8wSen$x9XcbAfY@Hb&KI6&3EQ-GO$=Xf zqh*hmj!hwYC;wrAE0_JVBU1I%&AU3(1c(kFn*OFwu?J!%H-J2mGrNc9Ut>R}Tw{4o zrL91H7qYyS?#sB2O9(7%JNh{u;o_et8gY-8R>K!*83SC{S0Ti&yKOeU*1Z~4%QgE6 zuNDi*I&z6yopyP;$#b}9SJvWt_~@Hg2f>3@*=DCCWXCLx9X<8^Kc8X=c1o?636Snp z;jD)-;>Fojen+kk>ElA8K<8>S9_x&$kjx~l?9$q#bhg6Y8KrOIymt^*{$SDnP1kJh z6spd&2>ZMa#0!JVT-$3;DozrZRr<#AG_?D~n)A(NadAD$n+sj%FK$_Smf*iaxGYp-3ys<-M+jhA0^M2*+(>Ckyu?9M7neb&1L`G zrTS{uW+Bdyi$JU&InYb`p^#}0uq+4OmK08wnm(wP4LK`RRQ?aRTqTCG6f=@pIi?)9 zbd;pFY^vxMqhb`M;9Gcp8sx#sw~0bKJU1ubpgY-DxG97>`^RS?bTr{7XYSjV`1yO& za{>GD{Dl6%cD#lnQio>RI+uXrxJY}ChPMoK(L?fGujs?ZfG}SD{Gp%+I*d=%2=&KX zz&9H@HlLq8d!|E-CRz}s?jiAmGNpNyFZKhN|MlQ`mSAwG&IQ_3=`0~qXMp|eMg>8~ zaMNjGgQg*>6y>$8wM3RRn$Uo}K7HXX8HB)bjK9&9F3!EnyGYLg=UyM8vd|&3ptPBF z#~I?{D;xTaSXGFac5hm{x?$yZhPZ%}*HCvg98!VQtL$6u3E-nr%hAFh;Atc?x3HWAMfr2KwSJJlWg!D(YV%LGsZcWd-%_ye>I2Lt6S zK&;{3n%vggtVL^nI0F?exU(;j_?%a9lD^7HP#`LMlaw0w&|oj6m;n6JPY+p7% zc_WmrCP0l=<-(C2TOWAtJ8|KEahCdknmkB>`rprvt4~o#t~^y@N(gWW9(&74`uR9x zOYi^4F8j48%Y?jv{ieb!E6(EMencQn;&uNpIRZV+h)!4yvVS(=+$>?etcC(EUm7vV za}5u>?imQhd)^16B)lRVckEsiTVTJ~KWP*g`MLa0uBpjX!lVQDGu^%^SBNd=d~@&n z_1N88(SJW>&pKsdiTgds?*w~%4s;r=x$E#lS^es*- zz=ogM-FtGKF?Vap-Oi1pDxJ2kG?s>^ZRJkP}=^C5XZLKJ@z zf#s{;PEE%0k_TiF&CA${deRt$YcD~am~A4aHJ`+<&-hnGd@t z0gR1!2OW09OJ^sNrF|bd>}67tt+BkgQ-YB-`7qkKuq8=~L)ieUdY}E!{!YH@x44_O zW~x+5PJe(iR7c}DFC*|#8l|m4B_5f^dqFLJ>!)<;9(qSRt%d5I9xq~O({b?sK)7s5 z2Z4*iz-*GSO=WYQyU0>YqLg54c+mq?v5AXdbN4&E6G7sW{Nyj}i%@UF>}+(B8} z?9|V1)UGwejBK$ws}(Y+x5*)v54pR8V_q7ZwusiqnfTz00#))nCzAr0q2(%`!`PgD6e=|fuXbO23_Jy(W~Af!FI#}I3}m< zf!_!?zZ2T&eK>4|ouXj&IY?Rp0WJqUBs52^nlu>q2_`A&<8nyH-z)1jm{rYge{vnE zSnlzBI3Hq(wR{mH9H%Zw5?SRXM4xu4 zn*I`M48uH$y}j*C5OmVj0x!>rJr{T-yw~0jQDVzK0tGY8>Q|@ghD7H@Fru?TJrXfb zRKJ_YxvF5**(RWcS7Y7pE27Sar?R^ z7^-9Uas!t#{k|<1X!07?_6)}ki+Er2#nQ3ll&!{xQurPVvF-c%lyK->=(NkloKX)* z2cK&P2-saArWMz={tX#u%?DCX2Qh2D*^CNxa2t!3@kf8Ff9!yTKY++Hq7^MS<5Cv! zSmQl>eg)GTubL{X*5?vQXeV`!UqohP$`OU0+97l^CgUzb zS2AQAf`0bkrBYIbPW6^{kB@=~V|e*mNqvtFewXFF=L~xdPN}RZ#p~uR0ok~7YsDck zIBR>-qc6WohvPUH3+3{i!$=qGU$>@Rw*_)Y*PKDpoo_~EC@9w(+_{dJ@d*OKRCBl4 zChQwJ=!QN}@M5Wa@GVE( z{w{~x`Kiz~HM^^sH}Nn{?5NkhWM(BPS?Glvk{sgDRKxXWna;1o=j`TADLNG!uM1Ra zE(}~qbP+T$DP1ojcv`h&ZeCv9m7VV;F{_P?V%m|PqYja;y23Z<>8@nEjPjK8X)RyO z1-&ERxjQB#Gx$4AqZbZmR0rNUA1d&(mN~YWXCnfO6&a@D6U3$hT}*B;4JvQ9htUrd zrXWOIir*`R{AFX~5VRelK{qE0&Ek-UAVH4Dxt41*7|Fls9fPX-`ll@cZBi>QT#mhl zP$v_sZHFD~&m%tDGgXmbYS97`aX#h;LMlN`{0WO3gq%I+tH?@>UU2{%ete*C&sLAx zF)o?Y;OkFMke2eOF7i9V6MUs!m8f`c4-lJcFaCYLqSJ2T!4!ia+<7}oDdN&}J#+KX zEjTU`&;~)=BKHt{)GkI@TsX!ywd^sU@W_7C>6d*2Xs@a~2pAuZEk;%`V&GMdoz;c8ldFad_3s&$p?wC~4gChN>gB>&(jCHYd5DyDLua z-yLX<8{%Vi8-+XfngH9w7xM`WQThvA+amH35$D;h_?jHX4$4?5D*!2Vt%k|nG7;`B z!%tl^97JL|nqBqqjWPK5R!OzHasJ`BjqCKtBbP4P-XxW8vfFz<6*hcC0$tbifhrIpZ(DSYy)H~9%S#|yZirqYW3n#`sp|DdnuaK@fljAoOoOy~g4coXJ-4Mzv9f*F^Y>L z#p*1hc>w1ey@iP(yu2o+`;*(KHs;h-T}F`ti#tZjSD)YR=Z20IyNK4~;4_E6E2`{s zRQS|cd|`t)oT4wy*>XoLIxn|o1PU#p!a2_*tO?>2u)cu7$>Ar?xJU$ep9*C20}8HN z5n`#?iVoa%%Nxg~b1b%qh!#_xHmMk|^xIig`nyNRcSz7~5VNFLsI3(c=o4gTydLAZ zZ0qxPvr5MxiEYz*5?784*M^|h ze$4QY!&vU*4LOICZTE?1W+Q;g9m;7#z#`~myMvb+#hAzy* zjH}M<)pDa(-I8Y$vK6iPCnJHU^E8;E2OkgB@O_%NqD>f=E+(3G zBgTW}wSVa#nTo%Jo|w@H>7qd8dBqhe8@x>+dpBeAA2*lPZ3guf&8`G^-A_%~BjcVo zilRmRliD457mhBMu_^ooO!H$uQ}E)@6E?sEoBUB5P9(1HZ)ZXI*gbu#STKqW8_wxm z6>-;696@kAv($0LPQyTS+Kn$=)H7ThTUy(X{<0;H>3HIg5yE-HI7y3I!*6dXq~=9< zKnFf}1quR}1N;wlXg-)zZ`|2@->6?f`CIv*wc_6U-*t;BV_LUJes%pbT0)FeR5%Sk z{;(@o*1e_v>So`A%h$mix$>v)W`2dzOCRUIck*#bQ;NZn(!|#)>vsOro6zAPNQX-2 z^Kr1(Z`H(qo+;pc+47lhXEDY$DPLA<6I%2IF?5R*e`NpQ2QERokEkw=O33hBm|He1 zK-{2mBxQWS71;OnekZAci)WvTk6p)%?89pAP)-q81*1 z6`z3Rmdsm^JGD*58`P6}pGvpO$EDbtzj&VjRmH_`BUyy!i_weI9f{T&A+kQ*|sa55)QHm~$Sz{-g?%m>Gi5 zgl=Re;-IQr_?nsk!#XyqIHFd}rkzo)y>2B{`k(mml4Ip;bN1eSg| z6JtKL5&`}ierry|WN-vm=o@|Q;kt0O`H)QEej7V|!a2?lWCwjQGzN0JFTc|?Z{Ba^EkZg;eQS~WP{SE<{7xsiej?i0B6{XD#1*t{8?GsZ zTaHPpd`7&=wtr@)!o0ejFc|!a^=XZ4d#~bpA9;?OsVGP~&jy!hy10Vr$3dJMDI(~W z5D-hxd`9~?p~8fX6#(HktJCiQOa!JY8jA&8G%1{1$p-sHLG1_kJ-xbp-j_QYhO0!z zJtHk$#I>LGE3yiupSoR>%x(_#f{Xu``Tp0PtD;b|sJ`QF9;l_Ju6MNhb}^91dLOO{qlHo_;OSVO zsd{I~t8s@^5awyrJg*?}{M|28s6;5Eq9FsO}BkH9YY9&}06=o%+-Pxp$r^Nq71kO$9QTUm&n)z&-q$ zqgV>f{DW{e9f#%Rn+*wnm*bMP_YjC$ebL=W`am}y?)h6qT$_d--LswbvRV7v#YpNU z_xfpWgGy~{E8iy`REQtSK0$zhb*o>5_NR`N><MGmS&~5hf zXh^Q=!Wk{+f8E~*Z%JukJB1KC#nY%I%_2g%r~a+^uDIgNi0kHr$*6JXk%0j z-GQFFEQ4E>dAoleKEY6m1%gtE*5<)GNd?VW0{#>cS1(T13a6PzLL;xXjEZZ_DU!O{ zkiZK_SjzFRU#xPB^`M@q%fZlS6X&oA`LdJ`-j9}rJG#ZwE?n|!1UA>0jOfe46Ql5y zvw|YAseagW#0au1Fo!22FS=x8gV-1Ro@fOm@`QUSP@OT^W% zpD)Q)m1HTWpx!*jYFWNZOf5YB`}(HcO4k0G5uuBJ zoSm$pLw@)(J4s-tPkgF=IrOnix1>Q~NsJfiIbC5Hsj&JRSdWG7fH|err067H{>9eY zYq!vpx-p*lEDQ&+`t3uJLqqm{>@qGEgZ)^pr@l`j%+bhnb8hG4;Pi>CeN9yQ^YkT| z5j1DDonOEsG@=iaGp}Yv4*Qo(O%ny!7* zfa0$&2Uwmex~m@Q@=C`^1<)I32mISVIeUk_|3$%^ei6UlDO2jjWo?GsHM_?qU!EY$ zRb)CQzP>iQJ|3=Ij?iQ$Riy+?rn;|m0p?$T6+@Z!j$P4uB?&Y=G}~>HUdi!_v#Gge zx{7CuoG+Obe$d|YYPZ#_%-O{G2}Zg+3Y%iB>i8eU0mHvKAx&5D4UL(B9qr~(89`VBvY=?tBSiTL`fjt#DPE(at4dy zf1&duhUrDQ^MWHHXk^R zmYJfcHu&kKoo041LJYQohIHRdL8tdKE|hZ(iSvH$_|c zu7oGopnEZulkP_>m4TrW=*~WN5(Z)@{vp<72K=gIW$ZIzY39m4dM1~B*=+;@| zIbPn`lDV=%u2){yX4~0RBd&apy3sT7E4a5%k%36yryzFu=#ac>C??L|BB0?`j6PKK zx;;yMZyrWnzEP0C*$fxW9?1C85O@Bs+2`l<#mRy3u$#h%o_gUZK8`6w(&NUbzhAMJ zg$N16@6WUZT#ZD1YRV#D}C&Ek`Rk|oLoHOEJ2XFB--}%B{Dh{MUK2i!*hHQ)5q>ZXm$Jq7t2QbR}I|K z@1V=X!!{VNwzG;z9x|mDz#Pp`;C}>8E!zI&_0*u2Z&$|AhClRt(wPk#`~b^I9Ex6u zh-P;<`)C|Xj+7yZ4=NG_!x5CF{W(hk%qLf^^p%JC5%L#avu+sAohG zDaAM0Wt`ZXFZubhg$ypr2AWl`-YbO_vXt!$`hPio$_ajr<|2@o*R)7uT=ivj}nb-f=1L zURS&Pm=$Dmb=qXY@0maeqUb81s``Rw$;xiATIy*{+4gk;F)(BK7zQ%Fp!ynVh|Z z>=WG6WYp0mBLI5ef}n|?4HFNvV8%2v7Vd$bwd|EXK|GqCm!ywrZTX%k>Y)hxyDY9; z4HL1WXLj$Wvk@`B>>{51u3wfDc#cc-o6loitjepNbhZx!f=2<@4~j@7ML3Q^6v>>j ziw-^aj^VrHFG-(r4cRLBt53JDqGP&3jU${Av(vXaD_%;QCbE>iaeRr1kN%{LYsd)c z$gL0{z89_QB5&zn&s@Q{Z^e~0`ttiNv=$b7eY{^mlQK0LPanMD_qjYma1{Wp{{mWno}>mQRjI{OF1LgTLI zLyT&dfnbMSp zM~71e#Ya=Itp4eC!L0Y46`ExkZtus+oG)5+J8@ki~6O%y!t`6F%TG&99p8>xH%fwu|2%YVa`8_)N?=u=?E zlwy`l(_z~uXnpz_cx+6{yHIIeW%T%XB;VsuiVA14<#qRV2o{Rf54j7%IJ2DzNGx`4V94k!WKSRuW)IdFYP=1sxvJ{~9W znr2_!#G01GTLeSatOJsLF3xpXyTJrTRYEi)UdpIGr-Y_P@5{#q#rY+U1P3S9KOL)o z4z^@0J*@l3w?sNZX5D+NvMwb{CijlpgvR0aoWeCkQsWv#{$T%nT;dsKg>9t?YK;O{ zCLn$o^*KF=>Ws^1I~8F$U`W7_vt6#piW6#fyOa271h;$3Ta>rnL9)8iFv}#t_3dVlKvjo4I2D7@vt`jg{z54ywMZ;HKwy?-K*hB zV_I~s(0(F9U0+yDV2;$eyaVpTLMy^r9zE{4nvj>$k*xU(yq`K(NFK*- z@fg~9SQ?U4ijhbv?@@)6Li4TAp+D2$LNM-mx4ZCzL_0 zYxKTsw~ct;|K*v)B&`509VvFF-+NK_#4#LN-Y3qN4S*o(c_~A6nlc#`ES;;(IERhl zf$Dy}qsg%3!buxKG&z*kIF8pmv(aPMmTB>)kb)6OYw7l#cG^{w+K4z9$a2G7p`zBX#fQcgfGZHyCLk^35P;69?!6&HZg=iz)A zK#0mNTH-k^VC!0)GvpSp-!u}VwlcA_QLMHbHKD`bX+yQ~EjuAgq;*Z3du>BR%=>Kg8aD^Pf$`Xm;AiM=)cbx>lWe&s z6a*}%N%Lu#uQ&s$?hZAK^#!E9e0gTM*b-1k!PWnSpFtiO{l5l$fB64?IPAqQt`O7Bznds}+jr=w_j@R)j#X`i4H6{sCoOjFeQ9?x zUJ%iC|GD5XYl$b^oa)U8qDzP*rn?6@L00?kE00RQ#wAJzbTjzT|DS~n|6!CXCzhnY z+)PnV7u1Dd2e_Rs<;d=+vYVEf_N|ZIvFcyhf4VhoVL#Cj_JYV+btlm6GM#{C*xOLuq#v-a{c|FyMev$)LrOrqX#KQ^5# zHsvnT<$eSBngl`nF`PcX|1J}z>pP*Iv8y+F;tDDeGPC1X!YJG#u~o+DEFx`DcD z(AiqTDJ?7H1W`*NGUL0(U;e#cps&e%_NPBPbPNyxA+j=--s<(jYk=X)?dDkqYZc(#h2l^{qN(+iodOhlOEvjZ*@uUXT30=`nP>GOwf5!jf+2@ zG36t#){}R`q+Grphp34GJ(uP`$58?w7iyoO8c5!${)7Hj6eVBQ3u1cHWM&xY84Ew* z1L-^9D^EV!T`X|==l1h!>du{MFCHGAnr#cn=uB;*qxT2sJI?=pwRkYz-aCWjXR3f> zA&r#pz3;ZYEZ^SOCHm}>P0!zXw9>l~SnAVUiT%;CGX{}AM{SH|jX`+P_S_rPCF|e) zIuCb)wx;<~0UGpCIzTyDy!}@!)Y)$eb&6C$m132Df#=e^12<=@gEg(EXUFa*kl$c| z9O&dKR7eru{=qOH1?4!p?a6<1;vV{mw;vQ*LYu0omEOWyancO|X1=NZmUuQ>S?u02Fz&$_z#ZHSqho z5K4oKNB&XX!xD$*k%XJ|(XD~otj@F52`~m3qDOCcPar#sR-RcLC!P5HkMUrbRYVL7 z)wWiG=@vad#h{*L(@2+0h8a-D%a2+9@5;IKfkbfhZx@d6jr)AYLwD1x!NUtBzzLLz^~+%2i8KewZ#UO+LLX2t}Xx--6Q;eldgE zpoOL{Fu^qFLUMCaK9YzT+>md$O&aoVB!S_a-vGk5U<_80ihxDlYa&u=_NMhxnj?;0 z=8^T%i1I5^=C>^O;x$`Ngo_5`0?<#C3{nbt*TtEaoG3g1z`OX_w7DUXO^d0(q_H+f z^^J1!D=F}sgCj%^k`dfM@U6zpNbu1A@;8o*Lzff5YaU$qmiI}g8~L-3e3rQ#|Ij{V z@~exFeM3T31CYQ%!Kv`S?8#H*}y=lJ{W5fI}-Q z$2T3?_=Z@+rCZRt`!krD{g4F##@vhZUtK{fjk_&XxM_@LE&dJ}ZTWiO_?=jURlo!~ zw@&*iG^0;WebJj3+wWTI0p%0;^;dp|U3HL^g2m)8>R2rVKLiV$TH0X?kUx6+^f!m< zpRzRKEHtqqpa2$m;S+w)dlKO)_kJM{64r&q>}0sHso&TN3}F=bU7f-lbRLzJ>T83i z<8MG-+L3XtpHC-1`57xP;0vhY^uygE1gj? zI?MBQnF2Wv-{?NdRp+7>DNgwK{=SW*8FH9`c$!%< z2YM3M`Zkngy3F@lxYScb-Jc)??c#6V{o6bij?LGFnI^L6^Sn6`oM8FE0%~5A&3=*L zn9f)$4cNVNbqgCJxi*5%v)Z7ALd&OI;2u1qmbsc|ABy!`^uI>nbHF&)cyK$by{}%U z>HiAJf8z^DFGVn_vvouu-+8PHs8#(bVKr?!maE-HKj4e-)V*GUg>s89Lk@zFdFZ{V z!6)~lE~3qHP;J%Wp6S+P$p^!@O@KSGM~!TWl?zk;lD6T?i$Msef%v@wwSV;m;mla? zt4TTA=3K&n^cJr&uJIwXz$;r%W+6@5ZC_2~f~2<3o0 z4CyQEcVJOX)W(TQNCSSPrYAqNbqfbp<<8UK$)qks?3UbrH&p1P402S!kKFY!+g@U4u|}*fO*+8v{9!78^6U<{<s*=RyhX-Gi~UX`P28TUH}GIB!MM>&2UeJUm1gGosiiMcTCHuPvj=|9dmxZQ_wxzH zWqb=syZGMOa2Ni@(MN+Z;{}Mm*R$^t?B8HXV(jSdk2ow^IVgBj>32j#{z5&0Ke>8$ z`az}b@QG^eISe|d%pZV!D+k)nsGux}*$&=Up4~|+ZkZ}2Z_89DEt5ey__@1ud0Sf42;qE*boOL!7Zp9us-+T>w zUW-%f_QKsfjkNfM0NetvTr+bMvZMr~N}rY(Iw9>pJjSGUbE@zRSL`XBR6^-(V&Ac8 zeLeMN`OBqWE(`kuGd#byaPX*ATB%x3mHzl&ti5GaR$3Mxp6G$M$A0@B?r zjf4^+Dcv9?Al)UPgpyLyCDM(QbVxVS%~|)2&okche&3Jt0aEgM*HGV!~n$YGd?+ zUb8=mo*JudgqVpO;zaWHfrE9R-{{R)b7+YIIIvbc`}uHvvPuhn8~Rfk&rH=kZONhh z>HZ9F^;UFL^y=zrWkNLlFc)L4(!A%j?sN(Yf+=9nl{vxzo88lDQc88`38;W|dJ1fg zsOrEiqKrE0^D`c>Hi*v3UR4FsJ)(_khC&wn&iuvtnwsxBaQfQ;vs1pF^zoT>+{Ejp zUh3u9{XdOZyBn|?Tf8xD4gB;12t5W6_*PK<>JR0=v|8%rL+HSjlZjHbIF=C6=o>Y5 zD$DS+%a5W8%vNX~sArkJD+(5N=^r6!^02nKqV4S13Z)(@*)h3VI}veNTU8B-wi5~F7{NbdZK3JLe9ZSt?+hFTWGaF^YBNs#kT|>17?P@pQ zc_c{QGp5)haBD*YUr(x>Hp%J|KCtA?4MS;$04^Pdo1QMfYtW&v?iw)VO4H!vgvoU? z%4Zd)->9jM*J6U(HYE=16g7*ZnHf<83>a?4Qu=NO+M)i$z2E%my;>{On#!{90V@j7 z2R~LTkgh+?MR}O=3QZZc2RQrW5OPKj=8l#fw%!C_Bj;?3n&q<2>nU)CQD6uOPm z$Ly=;8$}6ahB79ww@eEi&#hNi;yqK;za!41kP@hW9?Xi0fa9Jvc!XkuttZ+cNcJE# zu{oSiuu#S$bAz2^&Ey@?%_|yJ4h3+?3=rAubpnW=4oFj$;r0)YE6NxH>=xSXKvz+U zyUxIE1TAK1Y@v0i5)B)~a3WA3poJ4WCWx3l!v_=eXOxhVL|HF%WtAzwnWo(NrX%v} z&ASd?x(RTsq~)OE6R?DV8gTV&r%x^myBTW#E?WAV&?+G7tb5WuIRd1z*;j}u<<^Ia zO}^51{>oB*sKc82_v1DBD_?X!8Qw%013Q#3=sYY51Y?7PP26Qipl+u~I&{Q84M3*X zN2cTt(bMNR?bgk=?K_->X|dJ2MPm&)jdYm~H@eIYKRdk1jHSutc0N)%>iJx)5qiU8;&^`}n+<~{0XRF^_{6BBUmXuNKO=0e03tVY z#nBgr7*n&+8hk6K23IH~ZW=JZJ^wpHJETt&!=Nr{z)FdWH|kRssRHt$XLhS{QGs_K zC<;qN(iK3eAPf!+d2Mg+`vFz{=>x?yKcMwWf$7%+Sd}WQ?T=ZLI>5)u<4;FiUlvaA zDTSLT^mtOWb&~%&tQ=%``>ohoA}o?tScqoJSIYIkYk!$tG-b?(-xe$bl$Ne*#m|5{ zSvZr9Tf&qrw209MGqB}`J3LM{d}ajjq&}tyBbYG$jvqpCSd&+O1cjb@<>+yX!I^vI zJ85?^Uj*2Yv_jj!mBG`3&Qgo<$O%_;De#=hac9y_`Mx?@sc9&kOchsqCJJ302tKXB zVU!YzQE*S6#2xH55X?{DoIP|rKdYv9|Delgn4WrO1t#~i*?BeLumefJk<;&riO)a> z{VkO*>-O6=N$tU&w8%R`%M4gx84G~8C%<`y-Cz#1RVuJM zP!iCOJnXqy-+L~qOZYU)_d4;nlFKHZ$DJ`P_zO3qmW%AoMxO`|i{Neg<1Y%V%C%d?Zoa88+#OBT&Qf~PVB3dT6Wa2H*k zLZQ;Lgil2(8E+Ma%dN9>Gzz?tlawXhgp%@ZHu?naa|i}Lz#*jp!Qul@+eZFMWt?NX z50Y#a$1VLkHt!)cZ-;8GQsJv$lCIzA$Lp1=%l42x8SJe-GgjcoE?WsSe(fK6^b{Jj z=s9PAwSgk^E4V}4+v*pKW?|Y#u7E3Gj@k-Ow;CcxKdUztcOrtWo0uyDC+N%JYUdb@ zsM}bVMqVF`m01db<8R9HlrCxqAxRaTt;YkrrX~D!(1%xXzJv&TH1~Ms)CMd-Up4rV z1Nr!SIU{6ZR-03o6OM$?aaL8IdhPg!!TBlPM;Z~GJ1SA-fRW=ahQIBDwV-shFnjpr z?uts3O75sL)#k_hZh|YdVRzq^1gc-%OK!%=-eumJ?cCTov|4Cok|OAif&5A8BZuXc zDpy3bvc(Y)A^hRCdejI%8(fb-`7NO(x&AF;1m_mviaGN9n#O3U<`{ zc1p*;?k(((2gZUSbKuepQL$-^e<+NQW!d#LDh7sF76VLJ>Z!XMLswDb%nsEoD6O%% zBWkBU4U*Eb!j`5YFboy$7LY{^T*F=Fobk4TL05;4BJobl*7MQ{zj7dz6$bzWx_5~z zTR1S_I0cFV`>%W2*2RT4!whnRbfsL4foVJG-|s-^TDDsQ;%+KhSrXL%j1hZO=If|f zSh==&c&$$xC={@qONA`^*KHsk^|q zLHh>nm(rO_qHh8r{%rwv+JKEO5ZbbdeCg^vd{7h`c)*u^uvvHZ+GVrIiXQoISAbA7 zy}}lUmUMp7hvy(0^C&SdL`>cVBy04sfq}1`K^i6kCrWNfx=g~L@jxH=T}weDW*1j; zMAFLm3bnm$A>-K8W^u|bj?>^HM9dv}c|J$J8GoznyUn98l&XXYC1U9c;II&`o@n@+ zs&p9mrBfsGn39XmDKHj~^ZRxOsaDC%FzYA|1#GFmzIsv#>z#-{R7?x)&rwj0ex&(e zf$2r$mVuNMgkJrpc`puCRdTf-g76-0c0>x|L$iC8W}`UjMRwI4$?VT5#DYz&H3#VV z+IkHhrn&k)f}CCiWCeV2oMvD8zbR`);gcihsP6Jl*b=oS(CZdJZwf`V+4h+3>1XKJ zcj+L|iN1zQ5pQ^%oR}!P0Q$WpJ${*TWQoqH2Od_I&*tuAfBfT%NbPF{eQ@S_z8%i& z^RQSq=|j31EmoAsymxknsetbAuVjc-2r1{M zofST{sH=6`V3DdF6-Ess(D%BtbflHTatc+?J^U||ssI97Px-Mqx=vd7w$AjhypidV z$-`rG!}#4LF5y2-0T#Dxu15aO(8&F=I#DcxL&o_Dr8|s5EINUmPxj%{?Y4GBD@IXl zDMk^*b>z&zwp`Xih#388%HJ9A7x;;ATheOs0o;uoya9H%OWa!T8c-sp5hev!iAD6& zY*q2!=B`wU2_}jMA`O0FR5Bwrk1XUu?|&Yvu@yQco@1q1(OC_Y)175Uyh3R5IVr5)VzJ zs_TW&&mLFvq0EcnkcI+AEp>v5!BbNWTb1x_tH(3QG6!9#>*;RkGCJTL3;;2}PS2IW zzYfwy{kkf3*gD_58LTQ^@@zZX-5QWmuQtv+ah?nR4rUnO17}g>*EKfWH}Jie?1Lu0 zSMZ&OC9^GYnr_EtUvJYZ_LJXo{@s;pHP=S{X8em+a|z;<@e`!^S9{?lXK+X*Z~4e> zaakgX+XtJBlYhP|4r02K)q>xG<$*-S%Mhz>r^)4gQ~ad+GAh#r@|@w)*QEflNi7dp zP(bh00^X~6d^T_SmVPg&w(5NHOOuJwcHwmi1ujRM$LTSZ&Y561tLnWFGfG(KxIWTA zK12M&RWLf7sKnsu&4abU>JXcaL4}&*M=>r5Pb-_X^K6#3fZ=1PY^B$2!39TK3G(b`8s8UtEq**6E-{NB;j$7(OCXmBPot--$h|mMb!7x^oY@YvBMw`F=mk=FL3_P zsCDs{pyPE07QaR`@SLI3`COQFxzqa<;!iNw?>gYTWH~>wm$yK1>=`B}^m4j|*dCIr zs;dV!PTg0Z&pixeF*wrLTRZLCu>`tX^p#_G)$`Nl4d?XU5M{o)L2y08#dX97-Ui~w z;|EY?g#B(RxQ$i|{q(uPNaH;xVKCn_|kQ?FRqJEJf^DR8VbD za>R$wGtt4(QKHW<7sc!tF=H;MCCFQx1@sTGep*AxBkIk7o)8JE93qy#q~^{uYmDdG zkjO-yqV3dS4Hix1(E{D5|YDi=YXPMz|X$Rs@llSGpd5LA?Ho{l$I% zl1RkIT8h;EvoO5F<&$?T5nn8vjP?JaVn}!G;6Tj{aS=a3ihqBP;vdrOdwy;g__)@6 zUlw3*lQWLGQ^F|35>J`B$bKkN__vylgD~#{Ts<;fBLDfLoit6b;(El zhTLg96Y>SBFC*`VTsbGUAYHh%5U-7@W^6A;R+eo4?9|zxfGMfX4}xRCDRV01$lw3( zAW+FfzgQkLnO$g32RvIeIO#UsDDql65B_=f-o8d7M?$HcS4{;ip7H3Fj7$ct7kfTa zTcy$AF@y0X$+N;EiAavjH1RM+sLK)cCm^C8;aS{&(;dw-AKu2njScD-Wt;Rvfn@!r zO*c?_yLGr@2+q&Tk0(hTLGJDXEw>*if2nfWmMQr(;ewdVDVP2`lb)DNjI14}L~qYY zoww|*B^bJL zx7xjzo54lu%p@gLvA#7m=jFif8!GpB1_~+qe5oUok`ZwemYR9L%~k&P&YKjKRp% z;HDWl;$D8eelar)>lqSF$*yFshat(_ir1H}O;d^o#xhe2czTC3YT`(Iq{^h#0fI83 zQ2rg3)dRIfXRtT*6b`(rKq22Z#(UpJXj47J&a}w2gpPc zV7N4=-H~w0*Y^&WvpHz?U76g^U1ftwc<*D)cSm1eQITpNT%(4g_VO?)V&qb~!Kc&G z;LmKF)lLif9T&HQ#m_R8`+D!*mz$)?bQbMvbggZ%(%SmCw&FIyAx-Z5xD(djyO zRq0CPVdOoRn|c>O&?PZTr;rxjLHLaT|TQ@;>4&jv71` zgwWk%XhX;(&d=%rNON#=Vqi(pEMC8l2v*Rx4gXnWljGw(4yTnPA1yTt|15MgJjV9& zgf78dE`hUdy5M4x{35vH041yRon=<>6w#ph4JgQ^1U7s)jjAP+Lrl7z@=AxlRaJl8 zC<$2%qt|&DJQg6jkV?J!RR3q1y0N7|hE8c#APJAcpGGNNt+HdFvIK5vms%`n=$9m))9Unq3e_6En}q1nK|$a>9$|c@BwA7~+Z&5C$OFyQ&ZdX3 z=8z_vO7dgFkww345}RI4)=bXpYab5$exG0Yg&dGNt;Kh|0#J)jbE{9*pAT#;w5%se z7!4hFIqcv6vXPkqJc7< zYL`|b zcM~u!^nr0fKSjcpV{YXe4keJ*+ywU7uEfzTt>Cdj!(FRw+ReYX2={0Z;#QoFEIc>o zL_X8P9fzw=az^bJY1(5L!zXwu#Vc{N(q+frv%L9pm++ZOsIjT$LB=QRAPrld!!6`M zC;Q9eF$)Smt>4g_8W!tHdd3Jjj}jKvd&rGpRnS;WI&txiiOG;f?i>FSK;9w~_a=7i zJ+)LrEs5|aaoEJYIz(*7^of-_k$_&eC61O-+bs4Nzl=Kgd&}V{{o=h~!IX#?G~*h{ zaorkSPh>G|aDV{+0|~DUKcY4U@goHqCVtpYkq;VD=E!*hLwSQ6EU*a_>kRtyDxvzV zl&d0zM}1Tl&W?Q8m_I;zrVk>-!f@M`VcYG?uw)%B(94FsMGkzhD6SKoP%W|QV=l{3 zBkuaB3Z)xCfY9P9ZC%r3pB+E82k@Q(BI5iV;>XemxD;ZgP^cv-&v77jZ@(HUHP-w$ zMJjA&(_jWEvD7<94RzQ;hv^r?RgTqZ(ov!i-jgkkZXfNMER9xXL-pqQe3#Cnmp_LL zHJzb$^}n!d#8DDSryyGB9>dyGiaa4aYkQI$!cA1qqk`8o+qx1p8;)~Jy>aenKpB>* z+Q}xG^{bp@xAqmK`U>~S^{uHh9=WQEm?ht%8V`~w7O`P> zR}^@S`f<&x3o5gbHLh+^wkE1=ztsy7HOF+pMtb^#K=1J3qAzaYW(d+i%2X#MVRrz2 zXB6mMq>h8hh}$#tN^M$E)XQ{BW;<2oHAA8%LPU=Lrp_c);T$|P z99K?NC`5uyopiwC(0yNHt?swkl%3H3HjsF9Qby^KvilEy5J((eGhPVdBYi>OC>Z6T2hna7r z60jlYSF4bA94>UMMz}R-a3DfssO!e#}7oG2A4sD+kmN2wU~vF`A`T@{Y%;oAG@X%9b5=VKX4~N-V9F@8 zO#*vOkvLZ8c%`F)_$sGe$*5LVL6oa)CXo!vT{Me1cj#Xz*{zS!XR9#$3)E2fHc5aj z1*h4!`pJ#)`bj~F{)}Ha?(K1`Aq@e_yi{-p_{Tu>U7F1e1b6 z1F11@t?O~um%VLp#4fbhuu!a&y%J*H9nl^Q9pYUBs3?2%t1WbUg`M!!;}KL4C_Hr2c- zeUIJTTe8<^I4jZkN-fysPrKQ#k16gWlH$Gv$q3HeBiFgDe{n3`+bi9%S>QuPtX|;a zU*%kSsl0v}9Ng!^j;_10xMCaA>4k;=NFm-fgnT~Da~hiyv2`oxN+dk0CgI%imHck> zyC#5Mq2Sdp#qf8_#QwZVVQN|LpXb<)ca5IU*mcZrD{0*!SeFNWBm+9Twu|9ITL_T>O0k(Sb>@CU2P&nBfGSp&vd zTSRJQOm3duc)vsjE#;kn#nvgvXcRcp9Q#HPF*$GFjKw%uU1GiSNd}<1{_IKNEaf~# z6BVWyOFc31I!}=_h}GFb)GH0kg|CrmZ_PbiELa-?S_1h0JZIcXe4=g@v+ugMlPSEF zew~a*!)E2r1me2Fu+LJ;o9NI`A|f;@iNZ^Ka=OkZvtbQq%MTB?hj03 z>+Va*0floZ3Z57{5wvu!n_A0$&LgrA+$6u`?Wen62t@FzW?`6v#JCRbYCP_zrqLUkm9-9&gN&3j7vlM zM+HE@UW>^jY$|OzvVcC^W>OG^xngR~g+CyQ*8-)G+2WWOL>wNK>U^}5nROnh9%T5| z-S1SHB*YXMUie6#;c)BWMi%J3mpFaT2VSYvCnpEsZl0-JY;j&&aQZ-%pxLB3fI#Qy z1_`-Big3Zidxp@+OvUz}Xx93pgEER0fouKEQ(=vN_i0@iF-_~tr~+UhOdErUbqu(P zfsAA3fBQk1SuXNK%{9PaCisQ5R0)eP+f9|Mw_*e0z zm6^@EPYi#5Q~%7ue{|z6O)}oH?EVU$H1t|P&^ABB&@pPVS%lX6;*{OFpb1;im{&Pi zZ5<>p*UBYxk^|Et5^pIKM1N---@g4s^}fYk?_a*sZmk z_q!{Uxd?&zcth$M6;{MF=@D)nd)aTKTdn`GWPb)wuwL1Ye!k)!do`&b_aAbX{ZtEa zZ2O@YSPJOt>n=~Eh*+nqc0U3N5oT)?S6-T2vc#+AU`B`#@u07uG3NwAb6k7Io#i%B z>rP886&3LzDoe8L?ey&PpDXE-W=YkT>GHxD95EqsAC{}oLQb8nh9F`&sBBVK+FQg> z2h3-)SX?K5`rKWN>kt}16~39#u$^={#LA|^h^&2QzcJYwu+V*SIubkWOZFP)-igr9 z_tz;W4Li-Z_eq{vw_DV%MGA_1;LMpUG+9tVkfktHE&^Kkrc5Y-)3T3RZ68u%}!@P#9`yLl&q7C|^rgk1iG3``uGIWOyOpXeTqLtxtfhZP;7a`Qwr&vhXk3Uzgz z-MlQ3Odf&hJ?adp&5KGWk=`%uH>pO&kn>FpFp}AU_89sILOw%zlvIsf&Mnxq;vT!D zos3lH_4%A~V;FEx)a7H_V&YWJwjsQMGYXPM%_&CC5MGu@*73%%fcDoC- zOk+x2=(H@YH9R}{)2FXAoL-HZD!DIcz!W8{OUoH(oMbE9ltwi+tuD~@z=s~~-_D9l zDT>FGr^@DDFZf%t)by>ITpC2p^z9A&uh{ubRSx6Dy^f0^-I!UvqGbuY4cbN?Q5Z!ssADd%>&5~CJ2Ag8VB8-D)B@02!MV}B)O{b3;8MuJJc@VYsb6(r!G9XYQ_7B}t;`dp`5g zs7)eb=a6$|4&*cJq(g-;4iQ@!_vg_1-dIyLNx6^3DH<57SSHTAdkZYaK21m?rcp{a z8Dg@2@dI#%Yv zu(+00UF9}z+!{Wod03-n*&0R(1qvF(JW%Kvbod6vN{8Cr=!xozwoe|eRO(xi#sYWB ztPaC%mU}c1T{NgAqpt_XAc|}hm?inLlBZYsTD>J$XTN@7RO)RyD4_ImDnd+rKTqD7 zL)bl9LO<)xE&lStd+>^?HHHa2&9f(F9svT z^(hW}uXNvpEaq_9R}sfuOl!e>{P^nNMcu-u<-dOe1Ns+syuU74dnS}F+Udkr zdgsBG*UlQdk~X*dBS3&L{|y-Oo>D^b9e7<9-<`-;eQ)q5mII)9?yWPFND$QmtL53h z|Hk9uDKOu7EI_$lYU}m`tqJJEm)e?KM9(lH%LjYmS}%S+5xS`A21JoL+5w-TEvX`g=ThV)o{^dcXA+C#%HSY9|#C zS=@1O6-aJA@ObTvj_z`@N#0>bwFr#pl$L)}T_<{nbGX=A)8>X3;`P$aqb0GXb^8HZ z%DZBsukY4*60gS&A&cn06?R%UHeP#Q1kBKRun~`v5=-FlO|uqveXPd+JXZ^YiAk=w z1680Hk*9es_0de79@}T=)LQEQ+eerl0to)r*3y6dCGv0;q2F$C=mjB6igiqqTKOxC z;CozY(VYogS3p#Au(Nb)b#3iex(O4NR1yV?b`s(i7h{xR`#Z5-y$PGbC1T=)#d|o& ziQa+19ROet_!+YM}N@6u8Nu*ma-PG4dxYRC=Kt^7w(=#*7d)>DdYYQ$am zy@~`n6{UNSjV-^h@QldetcE{o^rh#!cbD`o#uxeZxQ!)0w)@{$2mXD6oXZ7Wz;?XJ zRKsJMV}`9dc};&Du`#7rq2wMqMJrlbT9iASCm#WVP>3|(o-A{_qny>xUvq+X{hM6O zBOZFy%wQ;srkGd14`aTlUl?t77t}vyXBYFCe0aWN?Q*z9$og8kX%lybf;$QRcDl!- z+Trvl9aEH(gM&hO`ALRaCjHhtR4o3?-0#46bwNEiTzm(RAN&YN>+fA&T? z{s{)Ok%Fc70B}#K?-rB14GY2!4i4aud32Y}ltVJQUyc*8B+D2TQMy5ZBHSoB@Nt&XWLb8 zJ~bZn@k*r}d!YsH{|}^-IWI$nKx~ck`u7k^w{x7CCLG_dU(vIaa{ZsY>HPy*skarD zcV?Qfh3Z8+m1^-nI02F=3U@p;HT4dNMUm66vISes%@Ahh3JI@uDeV zd((>wnT{#}4t5lWjh&q+*jUJa;=jDcf?))nm#E-D%@2z8so7a?WQUUGk;$!GiZuQYY3+jR13#5b`!feDxI{F{yBnafSbt$CrZT|AUeg0od0Wqz{u!HoL# zMN6zYaA4~W{mbQ9%b`NYEA-0WaEOVCi(RIFKBHgcHvegZ5H3`@7B3g)yN=SBju+yB zs9a#4L7nmm+Yz%Oj*w>q@E3mq>n;q$A#0o%j~BXiE&=~kIFZv^eMQ zRyLt*T23E%tT8WNe$V=Z%6ew9k2Z|W?C3wcy-^IUoLgYRoNKDLaoecx+m)joQdBOk%4RK%$;0C^=;CtO`i|BUHI4cYZd89N3T>8(<8DnFFEP{eZo2bZ z+;9_|m*n%bwO|q40W16;2+s6yME%T`E3pG2Z?K-r0A)bO^ zdkcvJ$@xOd@8Wp=xA${$a>BUmwQW}WNg+UeS6=R;pgq|%i2uxL7W?VUZdYPLeo2Wy zmQvZ>|+X%>#y75^sSQL*X#fWQ$G0aS3eTdfn_0hgAxYNP%4Q-F%xC zF6t3L&`oa8)-N7<=W&#tHbjBNVRh1H=ABLA8<4OejU5-f($-X(=V_E`sm%>F$EK4(`lt0txHz{Cd}|0 zfsqpe0y;F~y9P}@XKBv@Ak0vBfeQhUOee=usO#pGc9qK^2}~oApWgnVsjp;}g&w`> zwdgBHjFaCV%*=EKc8pEY^tDm8KT{snBemPJQHuBbc3D)$Uuf+ZJ|v>RpcX_~n50Y> z5`6weT4G>(Zf%9uZ|!JjIR~YxZqj6+%|PMtxkYTPE(WYuk5Y>X3g}FIkgdb?#8m!9 z*G7{2et9yt;|E}fPl5Vs@pmZ!$y)Bu6N{tGX-Z&#T<%PGD(b?ueREXhn-YGt(|$9Q zEEIB$smE$u31Hrcj#m_KuC{8~td5c1X^Rm!ENmRmOQ*WF!UfrwNKmPQ%UC3X$dL}~ zCIcRgq_<(GrpUY$GT2h!h-5tGg3Jrs-JK6gD>y)5__6u<^siU|+d=iKJ7$lciL@!b z( zU`YRCHnVSfgX~jMs_a_)r0dv+D3gBIBEz8T-)mx6Z+mAq!)E??uLSe6!gl~Hoz=39mWKnHuf(DI6BY(RTTq| z3vAxx?Cfk~*)oesZDoTw!x`eopjfe*S%Gb8xE5A!bWVty;$Dby7A=*W>H* z!~MMu@@=LJYLCh6*C(3S$DQB)X}}01cB9g%G7#}TjUMR`BtTMe%Pp5jezacw}DI$`3q$UxD-_>*P~yEg#e_5gxv}_3w8PW-pjS zZSH+AlU{-SlyL$GAF{Fav_SqT04?{FHl1L7g7!qJOiBw3@kD~4TMl1MWo}T=lP?{g z&s=9)a61y%Z$P6`PcJVo`f7zDLq=#Us+7y5SwaE_dmuhg&m-|6c zTDN&mW~jiG74y!c4E;lhTu>{-aNw{gxr^{KYdGFy6c?I%Z*e~AiF~fg;LMtR8)I^Y< zdcfNISzmu;v~+{t0|m{^h+*aTxm=JCOH%y`P@5MAvT5-d)ZT%PC({4~o&4h!N0;X* zL_X-wcUWv70#G^#qO2Z2FH};ujR+q9DtqP?L3>{3{Wmp~#XwRiM@c3(;tM@8zSehf zbE{FSG^C~)9IgnOrg68*Ih$)Ipppv{JOy?(ujyz7?!p;Ys^kJ1?Qo&$BOPHt?seb1 zxwEuEUUKao|6nJ5v?4Jn=ZQbSU7#AfLC?x5fKp#10%o$FFaj^3>0e7S-}#Lo0hTms zKj?R8pr)qgy~3>6n+m|?;%F%o1cljJkLGTtH+_F}m>_f2zvl1Gj(2i0nW>rCKqJk# ztH`5!t!GIPsJ-0?c`v?p;K!ZhM^`9v6y^~*l%ZV7`$aavi(E*IT&Sr-t=8?Slim3( zz=fK&U&~QCUqe2nWpH^p8aEHm+np8xj%Qj}1=_~g=N9AiyBh#aXC6z(6+sFmJyhh( z1wq$tNsMmmAS3bApFe^012vNk!9V3tdO)-(1bDRIbxz0*@xfkDApw~`BPt*l)i4^=JVP7>kmlHDUvkS1rvfhZveq^RT|E1*-$ zzfZ(w>Ide**}WWf&=T%CxTAu7e!lqNj=> z57i4%5ekky5uw?qqs1lxG6`%VBhEAwN7SrILX-wbI-Nr2)>LrG!(30 zrjjXuwpHA1Us$--VM@J52?{g8^}c)4pyO`|WxfUcPG6WEQ$KyIIc(mN8Fs}6&bxmX zRW_7Eo=1ctfVMNc{jssFay_3QH65u0JvO&5?ZZRx5IfkY zEgAxXF9Rc^P)>{$DQw^ko@mF{m0l1!)*+)2m$QxtVfS3^jF4TvSrcQ_mCrH-_L*Y5Ie~u=RqO`2A`tu z=*>(lQf-i6HGt8=0Ge-)_zxL#;aZkqCJ2T-fXy-kdP{nv7obJ6y-fT4YUbA6@s2r4 zvEU4+4v>;xt`WLrgHJ6PDSx~JvQMSx zVyI+sgMmi6XSX6{ydPZ`TGEYsLtYSF*H=5+57bF!_8KVUYA~jW$F)uESxr&(ot<$x zbF#cg^3{ZIrw)&f`j%^>81yzsn>XE5>4~GG3B_Utd0GRw5Gy?k1kcYY$o8N^K^#3U zE4Unh>{EC{unQQx1vX1~Ia-xCfR-4mL+lP+j*XNz#%{_a8j6WE>$dAqLKi3}YSW*_ zKObCw$;hzt`mPZX`q9x*#xbz)K0%LZ&^Tj){BAqq+yFvi*)$TF9L-8aSBc-5nq=Tq zb3>)O`*ejX5wkzNGbfy_&m&}nmbP@>5i_GU|veSt9iCJ+o1u3v_BPhSetG>eVX z3B02V?s2^3XPP+zA?Y$NU{ITzoASj2nxLDSxPq%7t&2w&GZwh|sAEH)-hfU^5T)5h_11Kgt{hy^=Y|{nvYH@yl@GK-Wmbo`wr&#)ZZB~dP)tHu?iR!G%_->sL03!vI$IPq06E?0WQs+i^>Hz?cv8c9sf^se}@}u+@H7=Kf zaWF!epBs9u@VTA5t39&wO4Q;W*r8WlcL5*>b9Jp&rvxbRg5m#{Xj9728Va1nohVNQ zUK0=yeB^V#2lIvXzy(hlCd7U4!4H7O0574UMqftqQUBS-c%>Xq?PtV+U()!eF+izL zNM=eM93!wn*R3irkBWhpG4Ea22^zcn*WKD<)y@PUHxz~buW(d6j-m!Rm{M$9TnYs0 zq9;sZ!aG>#Nt4yi0T?r488{<#c@=QHl0bn40SY{}J5i#Ni65iOTbiE!R+!P*$*82@ zrbDJPbT_jA$-Gq%MjSrPd!28sZ5zul`lhBnO?uP&VL0uaU?nBpqvrs?n;C}tCIrO6 z?{RP8HktyQdl$q>ZI33(t-n@$q9tqt-;E!z$pR0~>CR5>^RJH(pvV7=N-4n+&Z(QW zZVD_BGBy^VO;if>jQJd00pO<~qJ;@*LW6|c@fth8kb_ft&^zSInd|A%Me3`XCkh7v zv)c*BZWW|y%5_`dr&u>UObDK^pzHMeBRl0dIg$9*M%qsymG=F$GjN-^dWw$ITvkwz z5Zc?U_x91{J#$;V#_hZ(d~Cl*@{HMLC|17I-u~Qkys`xJ!rN~x?Nqc<`qCtPd7gSSTh+v&+)k2L^0mqRIo zI9hSIIX=)3!D`HY%%ddnO-PWzg01o&)v=M(>4I|JsAo@15*}YdPxW1 zn0NES++2ELybxt41~sdOqy^-=!kVfT!v5h-1_BW9cmjYl4fPHBZvw)%SFUv5AR7|{ z{93S+BRV(_mPK7yJ$leyJY*9>_dSipBb$*nM6 zvYDZ83L^ZHlnn)JQDR~Qm>Y)52g}rNy8cLS=6$izV3lp`=qMM*kLFR@wk9#*+oPyA z?wbq^4Q2iB$o4z`=2V@~CQw3@^`HlU1;I5T${?)$)nPA~@D!@w#>fYyE*&>rr-kRC z>;GhPytgKwt-`1bqGFTnU@5~s#119#)ph0Um8Z>SGW%Ax(zM~e95s?f5JBk!F0teYJ7V0D(S5HBo z5AQ2Fi@t`0u_zl0CLdEXhvFZJ|p&CLXKTH(}?BDUd*XJOQM54I9HECn^s z7XN2piRNX}pYb^w=7Pq+%XP?z=v7Wg-A{g@0OF`D;iPDx5r1!Qbr=i&0Q0X4aER++ zdMdFBSX-Zl2KA?X)2`+O%N9@YvZ{x$8iJvpi@#DS*G_NYo?3al&J1%o=;4igpm`Zm zPmBT(2HPWoz_zG7?+m-WjyJYCX-1Y_E9ryU(-e(w0dz+2$a_f6mqXAto;|}5@JH5cz!R0g!dh2 zfXMZlIutm05rwf2q-mnBL(JW?M!Mb`&Kw>cyc!?5GgNC4Q`hW%c0x$V`d2N|kB*a3 zr{)E9nnaPH5UWy}&3l(gXJ=<$wHRJkr;a2ZuCcti#YH~|wS8M`2Eh!C+Ln^yLy^bp zI6Mxc7P=g*A`*lUg#w?`du`?ApCg>hUvVnRD*))+=_;UrV%zc=q#={8HA&+_ zgZjO6&%dO#DLNh>waNuXUOQQZI9#Mq|3R3j!sl1%^!Y^r1jdAA`H*5#fbm%v z3mcS#$;OX5-itry)6~?oWqqIT*(EH@ER*J%3KlZhu*WyS?xZxIzkO^k9{U&iCe1wSRBScW^>Q`$$ScT6^sBUra~TiAF};G!`#G zcx+(aZ^#$oXQCGtZ(jOGJ&evUUGVnD6FEG9Kk)Mlth;R)=v7V0TUP$Q zJP01WLayNo(w>Wj#l&PKZ~uLJS9*7-!me0395lB>0udi-arpCMN}cF?Sy>qZp!jTV zy}!<={8d8I>mr?r@Yg=SRSy71+eQ7a(a7@A=0`vPP_JH{8gqY*{VhiJ`~S%Q=Vp+g z4!v6bHSx*Mr>}ljUVPc6*@;pr4q_b{rvqg{*Mea zI>Hl_4%5p6QV0-$#h?wZkrF`yNE!Mvlg!xv_iK5%W4^FI|96W~hK^>4rKf)$7A-qO zA#RfHi@&YM>l^kVdHXto<2349w_`aR@qxmuKOj@3 zFvHg$g#11haBe*QN~Zb66fHH_2NnO}-BmI%inC zb%pYfo#`aDDnX22&28j>Ab&Ob{3v?D^LKaGr#Py(ZkI`iySXHnnAvXiaaje+fi&_i z5WjAZig0Y2zIl_m-pR-?H8T?gLVZL%dvZ`yJ@feF5bGsuTAN#2fs0Z|G_sM71*>QH zuv&5mNlX5KMM2^)_?Y=Juglx|bg6{`qiGT|>)Wi)9WicwHyXwVHqOtiD5;>`aO5{K zdjfKIw732N;C_*PdBhX@z0$T8ux7=IO-QJ3Xg~$m2!Ueb;_oZcGBUqPT*Vl)IS9_q z&K|R}vd&8(V~^9qw1VRN;^IEo=%=rABt4y5ak^D)dv@lwxw-S>1npspO%C$QYGED+ zM^P`1eb!?SBx1gHxP|{NBqYSnwl}Sn8A>~MyphDL4-aSWs^dLjVA#1)%gDeuJwN{e zmeew144z%3y_p!4x>6m|E-olOHQe3J)KCZxcAwq6XkvNIP^zk`ZA!4{YjJN*<*my| zOUE4|$JR2soD-i-AhF{5ZnU#Qncn_4(hTiX+3!IYZ+Qfz8&5ik)mg*ujYo>oQlZ*; zt#ZcmQrr^LGqRE5H-?+ffQaiQo9Ha8e;4`H$X~ZX8yG;)*$7oU>~EvxdJln43_@ke z%pcoRpHl@*w^x_+0Ec6y5JJ8i#Ik`&e%s;Oi*!!#;PL?*&J{-Y(`9NXPkv~9ZdgAm zC21CWUo7||`8~4kNB0JEwbEI>rq7f9k7?wB`(e+p>*K(b)f>8@d4z8l2W@uLI z5caP*ubQs)6xurL;ruIuzuo+C52}*|yoTOzjApaNuG*OCT9S0S@Ff;!z7Y0;}E6aiIDcyj5_q7BYTV{}J|Q ze%o^m(gWidIH)qDaX<#7rmjx7J%{`jH}z?W{7}Mo$bl|U@V?T0`SO|0SM>M{(_mn5 zF6NdN7YPN$+fp8}{Eru+iqbtXAwbwEguefL?rBFyN52fw)%du$Y-e;aF~rdYuSWpz zpHRR+#6NQQTmBCRR@}yKKZ;P}Mo)(c@#pdBTUIy7F7ff{C@;VqlRG_zNaQV4pjDH! z8Y)XIrq0Akb=^KT0e!&hV)jarq=XauoHkzxWAn@N z7d>U%O-f9h%(f!_GcnG!IFfFre=$LjugLF}VpGvqUv#KuTrn{*iH%2X%)fHw%GBJP z&pOW$g|>?`33vVtpd%}PARA7cZJF7MD7b~7=1o?4 zCgzfYwDL0%0Y0M&%9k%kbG*Pa(%t{IR%Wxr{o-TdOACt-K(9X=t~mjQ>HDkcB92h? zM(LiP7r2q9l3Jo&Jx@ByZt^=WK0f2fCVq{Pp{6Re-=TC)@yoL`5y zyB?0_FtBRaA>+Wdv-`Vquhg#7Dz)ki>QjjG1oVz~y?g`TLnX#e3tVSIAN1WDh)7ra zyG;iI7>$~@(lfzd%vT7kxoF&b7uQ4n@A=dTvARuELYFYbHYYn|71i91&Up*9tC$8u}bx(p}xmA#|tzSEYM*MxCb?6ZBb z4|g7vR`z>A1P-A*9CD%0HNYH;d~FzDujx292>_;gaxG@4)ME{k&uKZgBcl|QNGg9Z zB!*TrRMg4!ucL|Cm1N5^BqXG&rq^4Zv1+crg0sP5y0*?TXIsZj=k;)=J3rgoNzkxv zE4FD@d-%YTRQFrZz^PCi1Wq=9t;1{+8~U`@cXnN!Hf}DL!x#y%x>1rB|L*wo=F+sW zAH$K&IrWFT$J+V$OUaKmnORV925hc#l{zML>irD5C>7yUp`H+y`}$z=_YI+E(YP+o z&Xk^SMTAISU+(~RgI)hf6b649hN9{6r1-H;_Kt6p^}X(sXPMqRTk4ZY4!jt+=|x^c zi$gjdzHvVIhAbcY)b*1F(NGVlDJX6tn{=6Z@p*i2u47{x{Jc>dZViPm z1*AgW=f*MnPGxvco^ zF>Cr`5u80KUabat&5CkacJ$W>Vy!NTw>Z*{;$k%xzro-3b`v@#isiDrsxflU_aWCu41e^>CN!L1+eDX3R%j{0r0HOf%uTGdqK zWV~d)^5OX6knb1#t$dE=qN1Z}`}?us>bwSL7qG4Nh7k?ABR{;qsi~=$B5k(cGlz$C zZ7A*={ki#PahRsfl3Vh3RXg+{{sTf)YE#7D_=vFclioUPN zU)1=k+kJ11d0*!$rJ@I(+oK|nb z-o?9{4se%-sZYc&HZ%{hkZ+U#{P;3g=4UG1xq4%@C-=s(yPj9OdcJS72xxii-|2V` ze4stpti|~$5hwvKS=|^9h-rTnJ5qO^zCs6B;Kp}tX)kbfy(RWBqB{cTVvFN&30UYg zY@P93d8T*vLw#i4Ykghko8gX&<0jH57-WT*klsaj`Rp>Kw4-G5Z5Y9R~IMPnhB|QGsyu%cO7uM*rvSdSt}}qo(jK z-{(dT+}CH@a|Nw$1nmwkHfKK2rn>rhNshxEVDCk`er9 zbnowQeYhM$|5Whc=*aBcY2U^#GTrZ}By1x1 zN*{8^Y_FzgH{NrYWmC9jIqG;cG9GiWxI6Xsn@13L*@c#O_Fhc|Uo{5Bq(Ve8^U3L{ z>XopDkMq7@T?W3O4ndE-YXQco8yiEpbLS4a_pcBxDtEP$lPc0KEGVY9%uf8ukyrkf zn9O9^>-W7bo?SZMsOov_f;E8-Fz5`y0d_*K`@;BS8r#~MT$Q}cWtY#S`&*QJ884nA zG$4Gud_H7ca?q_QsbTh1@g%t$absg?d9O`z39kXd?(TGw3|Pi`}kCw%ACHUy9CJa3wT9e zHa&__hFO7_R0pCs%YjgVix4V|4Be=;-%M-yG z6$CO9@Sr~-6tuV$ecq#yQ&9ZfxBd%^Rgl*$sr0cCiZpd?x#Qis9(Li3Q8aY)Wm-dc zb*~Yy#Ukeb@N3!$DZ9B^U?l66ntEHKY}s^>Js5lCFu-4sOor@9gddY!A^; zeK6Y=s4&{)01E;FunbXZ>LJz<^n8}OMCfqw%6?&}V{9fDES=g=SN8Vmyy8a*X2Fd_ zM7M68;sZwBpP@(!J++^G>0NcPmVo*g!?0wpq6?Cg&7d=-Z9NP#wELcCZZI*r3SJ-* zpfFJc2Udr*k=x)Fjj=@nj94Qa>TNcDYk{>oE{jH?D755p29F~5AtBNo`z{X&%#N`4 zUFoSlNEtLv{wU+L*;u#yv;OEN^nC_3*$BWGK8V2bZ?EeEpy`>R=ei(Hv$1igU;G{l zFf4B6E(ePZjFaw#K3NtzKEA4ZTr4-T3kx&VHfS;oYCs35(yfltIl*L$Oi+N?D>}^f zAAC`K1v}U$LUt7JtMAbi)Ngcn5C#>CYHUUmpjS=$EE6_HSY8tjKn zgdq5Kw$rr-Q(Ib?>wV};KSxqt{oC*ZW-fpwY2rxL|004BiO9sMhn2DZdbClD4-L1U z5181@E=ny=$5X=GTu~63x$su#P;D}k7Vpfq1VPoR59gD`KI>rZbSq_LP5{-!z``?& zfKafode3@mufrUhXzT&9Z<3V9s#Em|{Dc$1^a8XQoWN0$CoaVB+S7p}j1MHVE}{t) zTa6>64YA4iX+(8@H8ws&%Y=cOH_Q#PzMDV;#lUxcuL0zJkTeVD_FVTfAQbe$i~3i^ zD>h)2sJT3-k``$uELR3|6#`Ilva?k;@L*rtn%`w&)-1jRM0IUEX?a~;9aXvCg~S-2 zkt$X?g5KIAJd-jY6UI2n(q@FX{TBBRM0m()s5pE9YerbiKr1NW)rKG?(LKMoQAEa(1C|4K)j-TXoyYJMr@}f?foR!6S3P&USbAl z2pI0O!&(?|prD|fY(e4*FaVdp=0kz=@_C8XZD3s9JtY^a8yO+)1hvBc{lD^&@p-Sh z+SmtSL(v)_4pP37t@SR~7!LDZA@Gya}G`>S>S+c)=icFva;Ld#F! zSy8Q>fzgTI^LPeDksltXeEm%oWo5{@-fOUscT$H(*LKD?^tY3|{DWX9U>@@EAw5h0 zBF{n4Yn6WU5HN*7qa?>ydil7_(+bs(zJw1vh)+&Vw>E19KuA|j_%PuCG|K|_Je^c0 zrY=_Q@9%p)7>|sMB)*D&Yh&vi1a2RQRi4Zx=3z3C>-n@yASU=E;Z={E%+s=i*JB_m zMpgKuBXwxLHDxNNcSD-WZNlrhpcyU&XYsS+4I%HKcpghcc_i5-;@&q9=IN=*Q&3RA z31;cOFu+30fnd{QqDsJQFxvuMCot?+oZ4?Cb;k% zi-|V2wt?$Y7n?FsxMBqlFP-EYP?1f?>-*WxZKHv&%~P}lpOVN(Oq$m&uL*Z(-}_G& zLR9IL=lL-?02|<`W=A;33)p+HnL{cD%GIGVd*kq0AZ#B&&ch2so{G}ajK_t{p=obm zRTy-Yg_IU{-I~Xc%0UL~?=dRnI++`nM37Ovn$eXz{YYrU1BidXNQr174OWkyov2XqXDY zBO$lpMDR91z96IKaf`pvv%9x?x`K&Yf!cWtPnrm-i{~I1HQSt>ICGhFQ}oY!^ggy3 zEnWjmj1El3Kf*Fk`800Kq&I~fxF=g6Hj$VuJh;8CFOW_=Mti^tsSXE#HSo&;M0lyo zF6Mlf&Kp2tGnrR_u|_n$<2F-Kq|qAUZbjTQC90Fnag*De+M> zFFqaiD_9F-rC9Gsh* z%jEC@o3&&wq7XTH_11|3i1l@x_;E=7(kc>k5r9ykL0%nvV7AGXmE^P_^QtzeiM+t% z|1P$MIY$BQV*xOW52s|QM7hJJo!x5rzv0tF`LE?dn>v2}3`U>T>2gLLYCg<;>@JnU z`ZYW(wjccWc+pW)V8E@Gr+78}DX9b$Cc1ihdNz+ZIql)mQA1-!I?!QO1GS38F`*m4 z0f{(-%T7R)DD0~uRBE^9UK;nd;BSDfER2k0n_;UgZKMpq?n{+PFOfBS82@OW{OllE zEF-AGb)OYNao!_glp(zTMVb;#5o6sVx{8PL33}%lf5uWjE~q{k=LL*QEeJ-eMqvRLLzl(K>--;ERn0OQ857aIibZpo_IOTyYbHj40j4VK+sxWOM8k`N zC#^R(Mqrzm222uEHkADQ{B_t4Cg6vmj8>4^F8-WM$OGvdoCK3zNhmY@cU||_RSm=7 z=9_J9<>uxhMJPvmYad}GWEL3{9qp5`!0ahg%%dnvaU31y|2c#k_5km)!~Git+Es4v z;hjME#&FnEihT%Mt_xZv2MF`Q!507o^#DJzv9qfKU+IA}t%S$I;CX`vaQ2Pwl@s<|~@eRmU7J+~u1&CR_ z9Sc!hL;zg*L;B21z*1ZSglWHMfe;3ah{=(3%JCW`Hcl-qy@wamm$2_(YX(XliR6KR zbP6Rt{ekF?mvjd2RHOeIY{IdTwxVNT0PXtqz}Pq`V0jF*rN0_$0ie*3AbdqJPupEJ zp0^M>=!JDkpewqKty%3M0R9p_5DAG$_`t8tkw}Z>+8JAR5AV_Iy>0kbFro}^dY{K~5JXT|GpoBqe-{)|hbfqaMRZb5CZClgX z>D9slTzGhVp(L2et3G-O$y5v)g&D=M0bvhJowWnLKfXF;V=`(z3G54PNNc6z-cIm|rhfcmi`q$p89V>pQEFLPogn?_fnCWbWCT@H8%> z74gb?QI_xW8%WUv*L|yH4=^Q!#!v6l(+3nE?jiGj)SH#6sGi3C`1bAE;o52TM}Z3jf(QjB7DKGSTSbnX)5*u;>phDyc;05`yb2O0kTi`~1c8IMnRh2t zcerK~;X_0JV3t1MH$wplRYIo|UKi_D^CD4Kv<0@N<|*q~5Sd`js=9Jj?oijC&Ha6E zzx1o~Y}YELR1JQ?YK-hk%tK0wPi36{qFNDP4>#LjwMeo`EOS^=Dvj6i(g`_aF4=7T z!cGk-Lc~TGUK;_Q7u9xv06jT0cD>!cHePW9{W{5;O`2k`pC*NZTVgnWVmWptM7Oa2 zv5@0(ic^$k;sqzCT=XMY!L>7RA0U-m>toJ!`;8Zs zkbly>O=jV17P8WNq z5n<{e@KMpP0dN&DtnS;&)BHUE^|O%sQA&O)Z4EPl+jWwM34^A@j=#aof84k^Sh2$^ z8214ja1?0yFRr%#C)!@>OY?}vbPQQpk;XkGcUHu_XR{=wtFns)qK zwtMX&`QJ$KXg*yisTHvc%?9qBY#cpX*gmmS3&8{ zp#1~&X#X4w7}Po_AtslX5oWUcD?dg zGm9o$IC6Zro8r3-#ZEUkH}&V9+SC3_AuB))__+;>f@7ZP9(`0ZlUi0H2%LskE${*u+&0( zTWo(_z+?IJqFTJwFkG{9^}z#Rdst&1df+z&lGP#M70bqc)7~UI7$GX9JoN5{pCm=X zt3+u?J$kbUEazdx>eJkS+NIS7`s?VRFHx-3%Pb&?MfFT&fB76bx5?}9pyz7QsrQ0Z zt+wW^_vX0{f^hCm+v_7Jaq^T z70KLkn?7J4;I2}dTbBIHaa+*+Q2-z*$`CgS|=6JQUlc*%&LQ6 z^gwt>s_>!~8t^@NbDN2kNC_t3BcH&$z_(nh)(frQy(~W!rV6t+R|*U}KF!d}Pd5}C zBQO#ufl@&B3N~wZ0il>rdy`ywTd=(HE&j~r?#@q$$^Oiw)~Vnr^!w4HWXF}g#qq31 zy=n4@;tKl(9?aX!Z(do)nU%Cf(^g_4gi1Hw9ZWD{;P%nVd=~sy=FS9D0Jnc8-`qE_ zas9Vu#L-9ZDHxyT33I#?h0K!s4hMu^Vmu#BF)8L_!+bvTw%}$deaH6PXbZyRryO?Z=;R1tAo=vyqhEL3;-(uu=nkz{KD6H>X^mjK ztyjr~|HEMkGn&UTMQiWu6egz*_)toTN3=(Y@Xw!=VIlY>4($xFtJ09b#_rvB(zlrs ztiDE%%!P!LW&{pZVVoSLGa7qdJa38)sBvv!nQuTQR@b;CqVnnpgB5ZHFb4iwsh zlj`%F8@C+p^nOw(4mI*B7dGyJ_*m%jkb*o$I7g#s3gqK(27C$9m(P0>e|ICj>W8B* zHeEsN1Bd6NMKbhT4HT|Umj3IM!M6so$GPBfWDkysxeJ8VJRrj(~sEsPdQN$8n;3BD%RvtZW zqRKe{_Piv~mR^7+r2KaQP6XXG%l0ge88&hO#wP=vJ;wKJV7Zr#{9nw1%i+n5N^uCm z8DRO5@_8H9nI{H(DYv&+RNLFzs>@TDp6+oaE<*{V zDxf~b1GMw!mS~Qsr^tWh(f9{6@M}-QQ#$@TSYUnsr=}n(DyrNk z;xg`UM!(Ou@i&H7@=RPW_`wh3G(E795KF=X^$*U+ApL#vf;Lv9`JCp(qEQ_1ulq?^ zLXti5uKn{%yzVcnpvb&1HFaI6I>DvSZewZc*7%c)w%zl; z3(fBne>T1=udFyG03Qre$V&11SilLsDZn7`^=E6{fZfDDN%3MdC)@bXH^8@d@(6;< zIXTz1u(`K4s6#|}4F=<{U0n3G`-fqo4vP`x&9^Wwt)G3buM1H1(XZdtKLL)1Q_928@CV2YDH5}v8C4~toQDGHp_XJFS*Q}WDFvpSHux486zS4z7(5WcAV2hVV4Hh{g za|4K>x;@L7Z;ox^psBkG9tPYa7dR)w-oHlnn3iLucLD=3x;-cFgNA`$qfz(e%a=BX zS!!%H`<*O>R(-;3i_EgJvI_qvQ=M#5h`a~*c;CzZPTBCmER)i#A92@Gc;Op3Drk%o zFlvlVxARitEI=z3LUO>u*tPo#9)9I`;g!XQ^(}s7d%=`?PC^D!eQbEr& zX&gyL0TTe2BX-XuTmJB`k!d*0ytJBGx`l7!Wa9uI@>JlJRK`jm3G|9&qsi?bVd{LO z((DK00ZD@W>Myg^i)5D?rrd)r!n~-GB>U6Vj4#o-w8_fO8hKs0x;I(#wi2D3el%O7 zfJm{W_V-prQOx+*Sid`*hLTKF{@#{<>LC30NY5W|p~%BQ+r855i&i%}%A_H!ZqOb^ z1o5V|(1Dzs)P0zI2tL9HVZFL{fBoAO7yrFUqMlBRAHGoi?>X`B(b2pAfw@E~eD9D~ zs`(}l!3CjrS(S#>yW zFbE66%?;n1$zvIR5Oqhp97 z1JQ=?846m_=xm4PCF_YFfsKk69IY7kKhY?PV~_6x!9G%L{h}J-)!p0w^d}4=`}x|D zf3PU~<$u47!FHPF_1Q_(-Ud@78Nqyi6ugGyWV-# z*L&Yq*XDenWs;;BBsj7Qy?P{h)f~B@xrB4E*naW-=YeHVDbdz{I9}=trsNT3gZ(CS)s!9hBXcYFEn`N%9YE2>AZG!(fj3Z+ruu3{PLu0|9vEZQcnV&;^J@l zg2RoEGHa0f@kKagAxPauX__QEq zR5O^-evsks+wqzGzlGckoOr;#1@-}qFN@jNP(i~!2|THUganuMZ$4rPWH_xTKKp-f z6QnHHfU^t0L-*Nx0{E{06CfE!1tM4moU2!xOzvI)bBdz>u1G%67KaV`FDUAey>)>s zApb~FQSpX<7G{&pKP~zsfbhseLHlwJ%^ z?$6%>k8~|9y&*q}Qh7ex#@@cc^ZaY$dprrGrW{l#56k^7KKg&x?}JDrn^p}hONz-x zq_=e=CJTH4359{2KX{pd@k)DhOy1+dz`uQDeG@U>eS zRK~|!hXq^|c+J25{pmz{WjCk1k6&6*uk?X;tKmP7IM2V2okFVgfX!^PFl4;+PaX~B zX(M)F+3^4Oq5D|Y6Zf}xuga3_NjU1z7r!Cgp|Kkbm?V;i?CjjI?OWo523VR`Cj@zU z!!Zrz%9W{=l`Ak1r;&-^FZcWFpvd~)J?F`ZuIcZ`zkK=fuX4jHAQL0B`1p8rq67;J z$g#J64VXWW7=C#1O{u*8TNaAPHh{JF2@mHYOKV!`^iQS#3N^0ypAt{kvP-Tvowy7)ZPL8Z2`R_SIgJ1jNS=0Bo=Z^atn>zz)43mNRr(eAn zDWLi?Ig>Cy0E4-W^{@Xvw~KW%hR6q)!}lSY1Y_dYJ6T%Pr>8%k=vQxs21)!T3y z#9+1RT3qbJpj9j#R%P^pPka75hZx=2GdwYJsPB4abg>^j3$%Z`0J9c2pwYEkuFgEl zaD8=S|61j<$B!x6+hyz8@30pS&w-K1@n)^u_#-~_ubcH&iu=Qf{_x3?JL_uKJK36_Cx7v3YQ_JGls1*ydmeU%Jr2sd+KqIX za<8ghU}Ycq#o)#j#d%`=6o0q${Fc~_bys}lpvc3h0GE?0iVuQg=l)4o_bYYOVzag^ zHYP8X$XcWg6+Api z4i*>Bc27W48}c6vu#xI1khPUoc-Dg6?n%dQBxENY&PwqAddHxJp0)Z3%%f}i`joA; zK>Rb9t(}(t=-x8~vE@Z!M)iU9e{OLH8VgHKcY&IxY&_3p5Gja#pH77%x40VsP%sp1 zf=8<-%~FC!w%NrI`El^~H~+iiXmMiT>&9+4GJ#Z;A`Nc9O9PfXVi*_w{B~<57!LA_ zWXP1uTZ)BKTO7!L5DSxny-N38{Ds2n;x+PT$Q`_xW+5*UT&Cv#Bd+cL>vDj91r-%l zbpNLR;v-n*l3lM=WB;>~`co~Ad7kV2JmTKm+4;CBwfvd-YIn8HEAP<&(EPxm7i)i~ z3NC5;;{hm~ZBIvTLu!`a%Aw2t>O1&>-aQuto)}a!jAJvvu0Y4{t}x~}KcNh^K?T1# zK$Nn3h+P%bG64yUX710mt6km?<>@T3z=+l9OZ@(3$WY{WAMw2k@>5IgLYwOVY5q~3 z`8>2Tnw~Yn<{diG)$K8%&$I)_k)FJowl*x}dF|V`$GW|QU1H*~ z`G%eJFMNh1cUZO2WMr~71k*`4H8s?IUz4Wa9a8x5I@2cLWAiU?;r!+~K}I7ThI-p0 zFT2DFjFBFCu46AhFdyiMpb_aq0cKbUTITy!|TBX1R;v{lxeXbQSTvMX(1XXCMlu96QG`$j-lU{>ksg`cIHxAW!S{ zd_?glMvb)C{L0z+;HQr4&~qBIJf5bJ`gvN)5_9ecu~W+|nngyOuvI30;V{L9RCr%i zbDqpAew&NuYBvs!4s-`6K2o_OCDMvZoog|#Hz&zP&WF*U?}}xw$&0b~=g*sDeH2GL zNC=n6pJlx7I*)(b?U^v0y*=n`?f`k~9Ufk^XE5<|oHU4BpdR`{8P#*|G~_2?D5;2W z;H?}A(Ty>1jbKz3?B8mhaoDJqXf(1)^CWg&8N_kq53YVZWg~F)^Y@lBWPH#GSR~E1 zewDbp2{Ca#d|fNOMUj|wz7H1pP3st7LW#hV_T@OiqmlG)Q9kD(bi0n}5tXF5FL)tM zv6wm;wsEo*_j>ofe4iYPrv|&nhjJ zyljMW%}ahS6RI?Cm^R6e-#4q*h2=xa(E2?{w0v<^pcTS$woTQbA34$~9g3wfzK)3y7>kNIAQ!GiP-gzLc`uF`3MaA*(@OI;7aM9x!pMAbEk#J02 z!9gq3-i-feL(6GP*YcE|!&d@7Ymd84S1q(!B5ea5{aXuo`ZlnJ;4ntD%rrhl>cOfi zDq|%}Oa~s;hrP15$NIB6PQEoWIsy%kCHCVHqpr=!eZi{DWunwi&Scl`!^`pV*1D#0r0Do)_s zL2b|a*u-b`u~9XvA=-%}peag++!0Shbrm52avaY2k!8+F8f zDl(7oWFgn}I?vI}WSwsgJDntOuCs%ITR0^!EtGCH#|a$>9&QB=3A6$)w5-{>fUwnI zmsRAjc!}e-N@Ge`SMa5XY)>eAfIdUD=j1FcFGHwfTsd3bauk0Tf6q`M#bnDSC~7*5 z*YoMm(FZ^11tK);YzTWa0ese%qnec_CHZDyJl&P%*&9& z55wYD3VpGc_4d>i4Nc!sa9@e;N$UtI+asinN?L0(Dm{TEq#obEqwDO}WGx7uotA^% z-=B!KFW3XRgS#h(1viUw9b5JvgY)g52vRt#l97=ys3VvG=}2xhUNZ9bJlj@ZE2uS= z0EguaEMu5d3^f+%K|(9zvmQ5fu|m2Ga_y%jHB7Jkb@4tbqSY>gW%9;}y0I z8?*|2-F5|Y5x>NBa)0iX;$`lKp}KF8+Cri16(rh62yr}8T`U2z4GmkSfx&TT$O37BOu>P{=1-3smAZg}S za-*y$JeFH(=ZVb_u=RU}X7st&_u7_HE?Nr*sqVpAK?6CXVK%VmCZ6Um6!xae(5k44 z-X-IoQ3r%Ux=q%Hq;^kgc;ZLpk3#M-$AYi>8>2qZ?x52Caj-a$oUD<1d=hO`olm|J zC{^G#@}~^ppkPtANG$e6mNAUjp6QuoJ<_fYTs^>MLq!X`H+e`s@}A!Y%^Sum%U{Jg zF&WsiFR2@sNU@v4)4&Cw4*YiZ_V$ogaBzam&b>xNg>$>ua@aq;i!2Kpt4w3C)D&wM zZC?O8P|<>Rn8c2QNqO40%5T*)(vA3B)#n8RpOBF?kn-b~dC=jyE9f;Y^nFb-H}$b=dtB z=<~`6Bxz&xfFeZ{TkMfkzicY*uqk)eLJNTIRnB#f)Xe9vqkV7P^2pfg2EImhym=ri zB2&`@lq&DRX&T`vBiA;WI<>bp?z1wKmtiDdwln(#CeOB~N2*1gElbhSomrWhjK{^O zHS51CbIu|x#Cb1AOqc{H)1NB}Z&i8XGy;LXdDus9@uC_%Af`}({^!J_>k$csR=yE* zp>lzg_1>frL&e0BG2oW% zu&|g^g|s5uIf6ejg<#}p%8REh(o2UC4&`bq))r?E+hw48;8I{(tBVf@rSi*frWqBS zYg>Hl_a&i@6*`aq&B4e{!N~;9xPf9a&$6-=!5R#@1 zZE8x#>&xLX&K@#TNo9++$9r<65341BB~x|Uj$b=dptYo#WR5=vnnLfu-*&OD0_#V= zb@cnaeaY6o6YcLG_6T<;-zE?*4eAuUrcGe7CC%(1c{VIQOwiH(hs~hXSTjy}ny& z*Y7c~$hqd^SQbt&*=Y+{wcii#(E_h{RXdq#H@~5$L*ZxP&_=mM7a*B}-aRjO{u8{5 z2O?z~S_k?sZ$iUj0H3xDb3Dbg&1S$ie4&rUfK8n;-`)dac474NjD9KCiuIwJO?+!- z@TH=A;alM+8F9xOI_)u!uu|K?o-A?9O|7k^F24*Q<-`u9>rx_p^M0DR4CRqiy8@J! z*Q?dCKP?Ze)Qr`=kZTCfl)`;sHIevnPkMV+O2407bgp$k^)v@8?h$zE7_p{imP;Yz zCc_he_=p~2hJS{!>A=ao<2G+4>QiU$V>3lwvvyZmmi zpKAU3j3wcX9@LK>#D!Bk``CFc`nH*w$9Q>w3Lb5gJloY1q|eRf=b5UzHh}4lR=IOR zMfR4sXgO2dfYZn)d*cKQjT%1@C%vjew9rK8My@-B&} z&CW4OGPWXz)I_~0D&`#FTl55!P;CT76%d%Q`AU1m2crsMP?&Q#EObUCcA!M;NSX36 z%rS6v*pQD^r{i+afDB_M0;F4o`Br+F6?l_g~_4_c;gTsD*WT2I0MgE+J zS|9%2`tWpc0l7 z=&8--6+T9+C7qn&H9P}#qpEtal7|SPw8j17Xp_Vk%Y!ZNn4^G!4l}beiItnmzNr>I zg!fI}@kND4Nlgt@>|c7WewEgxxq+THG4f*@$Mu%5I04QKqcl2m<@Kw7HZs*TIl=yD z5g5ch-i@)URItI;PBzH2`&Fd>vHT_r(TE7joSg`ZFw&tn1J9~M3eM>z+UVS$*#~m_ z9-p2qMe`VvfC7?C?{Hmf)MlZRrP8p(>UT|%xf)U*rAnY>bSQH1*KkffFY@2fo>RU?Uf1V9UKnAHms!30MHH8D#iGto#wQtr=hcl2c~3l7n}(@W>p z!e+cqku!dmulUtA4PDjb$x(AmpJ^zOI7OGk7W4~{>wYC;k%cFt*w!_nL;Hx(Ea|Wk z+lA4Z9mGM7FPMa1VDPtz$?@ud-xNLZgTp7*$1nm0EQ)t00wEq-UT}HNW1y8*IB({J zOfI@uVIc#TGsKR(_U`T04V`iaA{B}kdz6Rcn3g9H7^bqD)25d34#a+{!V7dA8ksXD z&M!i|32Ij3>46iDBf_IP zv^IY>g;k}qJiOBoPOD+2o>`VDg??$Dr#={dRK@8Waa=YJnjw0f)=VXZS1Y8mJmb7|)4+QN5ZP`-eDnzq#49?zQlmBd}!KYA0oI@dKH zKQr2pio$unBpbye!4hi6sl+FdA8i;>5f(O5_%f$9VdL#-#enk?TtV!r4Opco>$j*8 zffaOZ8m#rLwuSxW+Jz>8_vT(^IApDQbDW6>#ONsQU`^$fIorZ85GxgL~Riv|AxGKwvL1R+d<**ao{aAjGDaF^18YlKTW?o8HV}Cj~45C9^qM{pEg>AHOyX8Y@ zFrHj`eL?!u`_9vZw`p#i-DM>R zO_l%gqxd9$IS|G~55Dji^25*}P5PBdXQJUKWJJEK|8|B<962-T;Tc4a2MJ3uB=H#3 zI*pSmR>gYwx-B)!GdmiBr9Z(VCyQq~eA`!a@>PiXv(?$^`%%kqWkrt+MuJWjTUv4q z93-?!-+< zb<`H7d6(8MND=VOyovN41VLq0TFA(C+ zNKjUnZ_#kBawon2opy;48XRi#j7ZERYt)q=C`(_4Ta&8UeqfVKbE9J+&ZhIRmTs(c_YF=G}AsV@wCD}K29FeU~GZsH_qdShvW7Pe- z0N|(u_;|ySCG6fX48*TEz7_XsQ^FKwrNiR=9TA0ggKSI!9Jc#cOgIvc_wJ?;d$bmwzi{qox1km`=WRS1d7 zrtJ7OcPyCXMZD&W*pZP!1k)AUZRN{GZT~V#u&OvA^BKrcXtr2i zil=~sajnF12%|48u&Ew{T6H*fQi{sgt1yAw5@B17>A}YyQ$fU-KNW*wa zCiJZD>U#zYj!IkKfk4 z-kHA*{sRMIqnEvXv)XlAhzDbTNKZaXLqE4){f2oYoovzKma9~hw&!d(@}1<7%ptbi zHz`T#jW9;#%<_U>is#X&trO97es&x>sfsuQHCzuFOK6onQ=M`^MjVPsY%cAI`U(u@ z2abhN^tJ^J#lsCgNze2lJy`${8n8DBWb^-Idf>lx@BVGK$OsP+I`_hX-o51p=kl~scdfBF|yg3@$ z&HXL??!&^hA#Bwak=@;$)9>fs@aavLgRMu)6=SA^%w}~RwKFt5#O;L(H6xuy=AyA4 zZQm`??P(=<8HY@1q8EbB%NDzL>E>8{u}Qs0Us(~4tuOV_BYG08Fc>>z(V}XpIf-em z#on7Mq4Umvk)QC&V!{gogdvoy(M+f3lyq=Ba&9o$SV%QbYgugw@T>4Ptej8%P;`IO z{cVvV8%_+cLDE?a+Id{aLfHP$MjquC0dq`c?=aW6$&3OPx;PR4;*Ts*-y+$Q!SYLAKjOv%pb&zBanJ0Y@)aoqYL2sXy12HbwF ze3Ll7Lzd^?{I%_^mMp216P{4d1W&$6^z~QTlNdCY|3RDO-2X*tB*D=pM2m$pktSKQ zYNqvETix(gYr{GqUQ=r^5AG4B#lI85XWuU4Wan0lmw6K9YbVcGou_on{ZQ`NH&n0Q zG`YM;F3MEWNEXdE??a6;dU<>$ol<5M9oKu6wI244dpfq1reKUfgQ(v_C)IHD=4i7% z?N1dib&dgT4oSy}piepKJA?Ng(-mM=?aGFvgvbHPTy!)ZIgFqGw9UCV_7UWGKOLW~ zZ0$Qlf7C>WnU~o&ZxS=`!Sh_;Jh!_o?$Tk-IdDwEs{hQig z;?L%v?wAgOL5mlrrHlk}+`GNraZk+Bj!liPRJtCcJ6G}i4F4{(m)~z3dB$M()(qIe zWj(9Sw72C2l=oCJKE+iYW!XIp*>c~i^CaXpvhydCz(t(^ZI<0h1fEv;USkrPXV!#; z7R$Lw!s&iZEj6TpJ(rE*LPv&0B8`1`0LT4gw4a;9pi=5zu9r+HU4) z39mR7V+72b-BGllzFNF}h0Nj7(b}75*U19OcvH?;R%YKlGwWCzFHrv4ylf;Oa|J(h zm|?%XhXvZHdIBp@IsDqkjGjGy!*3fi*Y7Nu!$HjBf^%(Q=AQb6P#8S?`wdTE*&m@s_9i6 zLh5*SqOKYzf>MNRMh6f(7LK;r7I0nr%4f)Lx6twt$v)TAOd+zxp3~fUo-iTm42RYMR)R$p5sg4xeB3Q2#@dwb^&`RO={$P42XOu=` z@QLVwSjCIIOhcK4EXFMm0XKjl!prs8B|>hyn=bpL@9-H#*j{o&szznCmX=7T$vw!R zv>LaoYYA-vZ#zbB4Atn5!sXR7+rPOzWyI4BqFRq5)WH{7RpmXo-7T})B-O;gphZ`% zRD4=r>W*XZe0Y;P?xMe)@_n%knCUt7>^|j>-FTd({h`Uc zywf$5?D_qttmVxMU78+Cv4-x>YR`eTW zsjapx4OCH|85Wke-EZB+yN9f+g2^iu_pao=-Y7X0UK24dhM*bqqhJ(%MiY^<89Vm*Bl@m1&*Iua|JYvooM^m)+} z?u!Kq(k0($ipVtE1d{B#M<~I8BZ=av@KDA%%HiLYjHur>#QLs#W&yr$Bvw|eU?z9Z zYTQXlC>F-3ipRku{lymJ+T2gCjihHGJ{e_xbUf-8LIg(k`vIz&v{yFg_1VC_BaZua zvN*Qruw8Xldn-&+k%k5;oS3G*RC!YP1gFg#DO}{AKP|wFNLhI(RXq~)lVn0}3CkJM zX$1})*X7eNq$kx`XYbs5A&XWv`{n=$KC|e`{vev9X1tR+tLFyu6EP)u|5tJ6{ZIA( z|NoMaT~ua9W->AkNmj^C_FmbCBqe2UviHnh$KHgHL>y!%D=Qfh5i$;+`+2=yy?^-r z1K)mgDP8JZob!ADBCZ8)5_Ss*EwBym-cjUZC9EGSHdeDz?{)YPneM*A=5Y~oUQ{yv@Vkbxv2{5K zqv%^i z52P`mz}wi=Nvps5yFOupys59Rf2`DZ5NAQ-`#r~YJBqSrr*y1ijnQn&ozCqFyOKog z^$A}FVWJj(x13t}f<98?>U0~{22@N2FqQ$IRt)}+jp%B@2nI2lAEymO0O-{=zRxz5 zi^)vaHdFEXEiT@68I@FYBH(Ebt^o*wb3bqK!c)GX%&L}n7bBI#WvtU&X8FXnfD?ZJ zmvZ~9*U2HG#qa69lWw}6_qT7poa|uz$T@76^l*~oHNI0wOKjbpOatc;&D~|zEpF2D zftKQGuZ1YnL{75uFu3JB}Q)&$&NyFZ**AOHRnE{kq8FQL|66h-S2bfQX=`i}BL2roe@B$C&xk zI{jG4xev#vNgQ|W)j+mMTc9?Lk113 zl`B^p*gIliN1~Q(-T#T-%;+j z(S0Z=z0W*w4ngQWuX-G}ZuRmjzXeB0TcHXq@#FLp{3h7xp9Q9H9pCzNqa0p3X=ljY zMNAywD}k;vJA*Q160Opt!Pw<|4~mG2x4?0hMHk)VLcaEv?sWlO18=Q5kiI**td z1Fa!7(|SZdV$*l@Q&^TihMrZyn5z*l>CQ{^1!Ir=NFCcFV`vDD^4v;6eu06pf7I%@ zceh$pI%hfNKKg{KomF-Ja4R;PNc0Z}Sn4AD^SHaXUdZGWJ1B`VQ(0SoU;ALh zd(z#~XBLecdU7t<-d<poejaf=aNOCGCKH z!-6s8Ic00nV}uzh)tOI!#GLJc!Wk(vVZfNt;8d@&Sf@c!@DbIEHw0|%)YnE*lDIP} zOk{!}W6uSeL=3@c358rZt7dm+`qm%JJ{Q*EqX?i@<<5JLrL+jA)QVVZmt;M zKWT&6fnqN!5Wg_u1U}Q!QxK?OYjo_yT9H&f$X zN8|nCX&RcWm|YrgiCO%7O~bXtQ&C~}n%ZPrjAL_CzUOgfquffi&OV>%=m~vNGH&%8 zyIl0~V9(i2n3A=*FF|aTv;5WitYS*cV1=5l%lo3$LMhoHwGojI4?a8T0TI7QLrjBR zik4Eb?Au_$Mf+}{1=!_gxkcQTJa&tbdz#uq6L3jW@pH9Dr1$R0`T2?QyJ_uGm+0G? zd%h(9g7sS9r2kLSaczj|hW1`Na@E*Bc(pRl%&{T)bb(iY5NM63*&pFZU^3&yMq=vpf|3wI^i%AlQZ~LjEhkVI!2B z!^TZ77$EVj-g;NArK>@G{V~9F?B?@i4 z7Pk*APIu&7@mTvN34#tCR8*U1FCm0QPm9JAKYjvfIq~-a9hU(JgmG9QE#Hcin_`N_ zS^s7{L!b2Z^K1c>FZ`acSXBRG(TqVFF=iKe9>-!P$M83LYx8<-U|iD5Ne<>=f#?IxtVxZ641Y zE+=s5YA$9-K4$MyO^yT(#p&P=iobbu{`-KA;0MJB9P+PN+#_xGk?zpmdI=Zu%Bs4+ z6#ecvJM5S^S-4?PlC-wE`W_G^hU(lIHw1d}7yvvC>E5x~da#_=)Qz}LL)Ns)VHM}n z4U31Lf=MfEH)khlpt80y;8oG9 zNj^_c&tGd}#Qp=2$SUN0i*`%#SzP{6l31vm$visV0)bZR{^S5KJ7w8yp1|w^0V3ma z>(g48#^~snCmms-oxCQ0)FRqIRW6A+S>*bgmwlK89KT=VKIVEiy|2*IQ$VB_U%$qR z+{ImWf<#ag<6sN*zhpx_uxp$pTc7p+c3lH&j4>U@!>jD?@7t|LvAMe~ZOaw!SwUd( z+UiveBWL&$>Vry~I3c{`am{xzO;lVCT3Vy z(QPw3G9rW5EtHRwXVecPY4th-)h|<179(A!MVM=)>)kN7|IgUqh4STlX?*=gG?4sQ zl`|GNX8F~ByTh~w9_19`ZhK?RD^It-NkT|Dt|^OEWTFiQ0lvBNWI=Af6dj$-wQvSN z+uR+^3;?_|iid_T%dx>HfIxR=wq0m?_M^a8FGd3#cfcV;&IV~q_wz1Vin$5nizu)O+o40z?Gtn=Tlf~X5j@$CdoBN-6IF4QS zo+Y2%YItm70oyCM#8Sit9yrLh)qAUD`lP2{CAu{Fcv!ba#~9mT!$fwU+(v$2g16my zmmdhBKX>4^`T?>A)Dq5bA-TXyhPMnNOSZ7@3!lrTHIZNUrGc@}@AzJb`}L_v z5b0_Zlwz~)&)`>*3fIW9c>BIzTZqFQ=x`qj_4}{gC40%Arlv1Y#r1Ri`1yViX=wX6 zt!(itqzW|-nWQZULmpC3kM?9;F=Fi8k!!BzGQ+KBwv1PJUiBr;>H)EL6E#p3y%u?^t390wy)o)0CB(s3VutJK!u4|@$H+8vDYXDYh z1e{A$`&`sb_>>ZfAa`{9Qez_qiR|#ap4jYJQ~ww-H530?ylT^3@PmMgtrL0xPHUr* zD7~uU)Qd6D#lKLYS@}R6A?WYB%-HewhwGBt>B&`)WcY!rl;z{&Mz58s*s}+~b<*=n zPu*Nv^@TWAF{Q1=O~IsCT5}P@Sf<4I?+S_G(l!DNl)G)wtZ7d^t=nX$>>;Zyw#LJ7Y9lg{UnjpKGAx#zCY6G#Y zThQp*`#JL}U98@0a4v?~E3?Mtl$0ZqF|fJ08U3|j>%X=%JP4!N6^94a#{aw;sgvd> zRB?B6EAAVbBQzN{dho}yY!|FCzW43}go~?2lSO+?h*^nJ;euK<;+5BEmT$}a$x;~6 zO=Yusn|-pi9U;eKEmoCyYN+kMhmUwIJ7Q*+IQP9{;y8{7b^Xo=T9NTy4>A*CeTiHS zHTsT%2>YRufin)1m1OX;cVXOJt8Clpe`D;o)Qn|8L7772WZe3U0cg2GZz7_i8r(L( z)sFOU0bpzws0pyBb8d*5SjiAP`jOkk6-CH(`JpeMIu8#deS z@PN4*3L%<;i~T}uL=|xGANAbgUKs4|%;nN1At-{#O`tqEBefC7k!~TcxONdI0f9X9 zh#pS=v8$}F8uF4aHEPu}_CW5*+wZ#iv`OA+xf63sQR5i$xX5w4EVP|vZwAYU9gaV@ zy>ZXFUTzYkoaHDat#0yT1tcf6O$y z6DVI97&)2Qj>5-ph@==){``3_8?yLhsVb8 z(V)KVNtvAxr}PF#6l1nh0Vn17C(pZ}Fg*m5`j)2pZIk?U`3M7-v0I>3 z*0i!ie=&tb?w9YOND_@Gll#;G9 zKEyz<^~u~j2GC{bdGh5crkNlV;$zvnO0TLtNh^5XY`GI04EG=HSoGX&^8K5PHxOfi zM$wp|flBLpzIX07Oh*Pm+r!njVDxez`QMi~M#0al>Da^5|0m}7Zs|zB!!eCHUc!>) zq5Ht*{A%{v?3#6wqV*SevuSOqps67yuBE!0bVjPawoT6Vtt`xjbhq}lH8QJ2pBBK= zDq}4t7&j>Y`-}4Fms&SN#!WqBF;czxmyXN)}T~Zl12Z7-%>*Pn(sV*F~59n485T*xPK6+%VK)sdI+R)WD6p zd@8=wFvEt|TI{!D_;20DwITxeYB90uY>oG+qs!3{?LewNK9qHdYZ_6vb~bw+q=nTZ zXBglt4eWVZDx&L)((@nxKD=0wv%)9|dnZlyDAFp2{-&}Jx8jeF_<(Yh&(nQjudBn~QTzUefE192n4o2}JVODP5TX0(B)|V;;hdN=dbKAq6Pu2^> z9uYkIF{Fz(_c4ktdr$ECxg2Fp-I>0=JJW)!r7k#|smsx37}OA@wvVYyb&jAVI{Xxl zC<~pyBK>+ifB0Db(iL|Cf&^)9@$>II=v?pPuWxmzs)X1?I6eMr*Io;iWsz4MJu;-FgyAV3 z&_1OOf6w|!sLRpTO-zZ|*0g{7U{qak4p6;II~-p=x<1fPzWbEX2+Dqy7Dv`F0e)v@ zY?QMVySfj?Zuv!aZ{X4k?Wf*!4B$dwn{2EK$q(?H#P8gxbQ=@6#AQT+rE^5Jxa{Dc zWQeO;u_~+T9jGXu(U~lt$(&o2jfd<6GVIX7Syq$_;+lSxh=BtBeiAx%mu7W(f+9Y&5a1W9h(xfO5A=d%{*cH&C?@x zAlwx0q@G23*H+#nceQx;c=xr8wb$oTZC$>VQuRh>7y8?8)T|-}wp6;9q-OwdurQoY z%)G~UARfP4y@^set;lp~#W_6e zd46mXlvDJ|4kgV2F2Y^*~XA0ly-THs;|Q>Y<{*87(&E@;-2`7g?d6a zgyoo4mKHcq>Q+X1Mu9)WeX;vNxWu?kNw~c*MbMEdr|Cr0oq{ur(E5`Js=>r>JdA2U z=DqBz7`E_XdI1ab>EwBAx}?eWvVpNJO+$Dbo2?i9-w$B^X-Mq=#wge6(G6*vneeh> zJfb3ZLL!9=2Md8uK^7^}6`4taw9dyRDABI)ud~--vMIkaX;3XI35;hEJ`reqRUkkv-jULy+O@61Et~##vXP7u|~JNuk@!j5bYo`#N8=!_~)XceQJwV z8_cv|RG5RB{O(qb7u&OB^XI$ixz{Q1UY0g}@m z0-QAL1?zd8tZ)7X!i%YTfQGpSBKn_Rb^FwGk(sHn*N`n=j>r{Hw{S8g!1 zh`B*B4H^(uA7Q!3@3roPSj0-@@EZUPdxL^qB>-YevMX&eMj!ruwvy!8#nzf*ke9jP zgefcp1#~HBgvh)e%#?7!fr%5ABdvX}P{olc#HdUCRmQUO(|X|19dK!0{dCp9<4-& zhsGM3#%Me&R>;b0w%MJo=ENib(qMrvW?=sa-BjhVHUvJE&GLh^JZuzRePQt2h^!>~ z{d*j^pS}m4u~vuL1Yh8HlSRnl>%Dye?qrql_i~{{W|h$z!th^$JQ(={h!E`_qOB!sz@FXoM4 zRGZ#zbI1%`8~FhfOvg0)<5s`oc~dOhK~z~-`-YB07zK()=dEQj%{$+-zBdICBls2X zx$4YOG1hvPr^C1?xPDS2+4C%IdXo|PcfJp#zk)4Vaje+zg9mX3=|G5pOqFi)Ockuv za%!8bG7r!wZ@`ou*&-_tMdQU0bMZkN>+HSkJ|Z9|jVVz0CaXdOQF zAILG(f2UJ9(LxA=8b|wpOb+G5buJ%n%|D3t;%jnYiv=U3+{SlI@#>C`^Q1MiZqFxf zQ&Vuq6@5sh+=d8(*46bpr%Vro9hl`1sC^-g*cB*1A)L%8|VpZuG zG+YhV+B3vl@t-d9bO0sc*S8*)-+v4QXQJWI$_%_0E&OM(d5SUDpXzAW7@+Zg|D4e0 z887?AO@EcbZ1E7W*2pF(3L$)yR^#n!N|a`^4X%Y;R#|jri7v4%8=#!LZ|s-KNI?^J zd>@;x7?a2p^}L2pN(1F@6Z79FW~8u>r9f2Xt*ds!D+3IJ>2lak+Gyjl zYZ`g1`CtAxU#HO$K}T_c`piFtcCrZE3{cgE zO~E14rwM$Q)ORdvk?$LU`J`23AowBPdxiu_s6!P_6`wGH`o(yjoRBDrM&z>hKx!!P zJxfx3gS1P8-?ApN*D!JE7WsnO4>dsy?>p_3PqBDBR_@>0(&8nV1%w;_hTI?PR*xQ)F>vGyA7hT08c(zNMFjnm3JbaH=9gCU z@6N>%9)o+L?=`G-n0lw$|__M_?2dCXcmD33$d;4 z;mZ`S_jY&H>xxNZe2xa^EpFPqw{_5KyO%&q$S`-J4(Nj7^4hml*Il6RLY6>^L|TW| z+6@-XO|2@p>baAotK*ZleOi=``)<`B{G6}O#o zgDpe|+T*xvmkEjmuB77oX~^gcgtL{5OZj;ve>-61OuMdmC7f1}@KqeET)Uqf=$vS@ zugEO!ZKMG(Mab)jhpK6Jr37b)R^ti_lnp?2ItGEbT&|~@<;R=&_Uy`8hSf#jXC$mi znTL@(zxlBOlvY#{md>9Z5vIs|E%&r>8Hpc_p}VMV$xnDZM0AKHRbYR$Wx=ONQIP+Y zD~b5Z9RYIhSzQ~ULCrQcDOk1&|ShS-ZO&w6mc z&P%CZZT%7SE~qnZ&^^zw440K{|GRX7Wlhk|&Q530uDy2LJ95WzkfC2rMLZ4!g{bWQ zU4ihFPiS{&Zz z_s;Hrc0a;)hIS?cX}F(rpL5yypsTGxPQpxrg@r|~rTOeN78U^^78Z^L5drXu$u|Nn z;2&vkHAC+=Zg$>&R-U$4udKY?o!z{h9j!ThZ9Tml-CRWlqy@zIIUKyb-MwT41zrB< z8wA`u?FHGA79D^$A$Hd^^1{LbefjSTTgxXh84DW=OY52PTWHo^HhwbQ6ixIKHWd|9 zcRx*c@`L~WFwC3b;6)I;aQA!RPOPlzPQ(z=%$?zPH#_xW#5wVxr2S?|+KZtnFu-*Q z`MSQy_3smjFGD;qDitEm|M`m;WS+2v^MAd>$sLsM5%zz*L@eaM%JDxx^WRV7kTDbf zA8*46OTvmoVgH}+%*oxJ_y6YGv{dZP)$6z}wmAA9VA}3eH4@q4hU~^E?}GPdYuyF8 zIJrF|kF3VOzd9aKynp7c_rK0ztw)mL=6or%%6FgHajYQ9W+dm;CvGkNqt%|28pmbZTS;{A>2#YLjxox^$d%M6j7qbO2Fb)^ruUz+_e&u_2J zxDCp(Jkh8Gs!gwrLCy~}4?h`JTJI3v4}XzNvcHmT6N7BbMN#XvH@(QB;?k*MTHZ;YFK_a6YCs99rO$--^?d#%kt z>N^^qmmEwYU(<@-759zit4>HAS9M&*!SHbjDUz7Qiw59K;}5BhG}@RYoPUO}qjhXJ z3Tz*!nA-`MHc-vC30NRm$9g_Y)V%-ueD@cm7eQ^7%=n2tH0#G1VKxo1Nkzyb3uM+; zt&>54hKC#K~| zE^oUtUWB4wU7{1wO5 ztbWpj`Fz-;8q#2{0Qf)udf3VQJzTme+euiO_@zw;6Ay1vF z|J_0Y*Xd#v%6S|1-ojhsvS9c1yFHYk|~pS^P& z|6Xb_a8dQT#L$CDF{FhIEc5nxGT)=z;D1KX7`LO)3x1Iso#%`3|NitnyW9B;d&aYz zC+RZ#_SUZ}Iw4}+&`oIIHKvXIa+3Aqhx0|!qGE@!mU8>!AlDU{C(cI5phhv*dCg7j z5|+iFlULI2%VBbXC~%4(DRrv?^#0aCFbg3$dRXkSy!QL^&ROW@F!=Xp-b8QAqUxuI zFYNrtJ7r#ek$4qN#`M5tu1>E<6S#}}Wx_lolnPJ}W&|g9Yb-6JueZ`iJZltu`~2E% zp-IhdBGoc*AKR0Qe}L0G=IJRW`5yoIl-AsLSPYeq~^|kxmP`6 zk%_#2fNQImyrlc{eCsG-UHr`tZPGhQ-=KCW%aC%U`}UuQXF|x3+orlp0kfvr1=5;aQh0ON^>TZmxqhP5L?e~o=mGFSQ`5pLa>^fk zGOD)wJyUJZtrnM@|N2>{rG#}i5qQ9X^$=c;yn0@wU&;d%-6!A{INg(Q-;9-$HR7VC z)6{s54RX1;MUGCl-(+(swFaC8H^q$o;AmE&;5tZ|(<_W#lzy0$@Z@99;Y}6J4{jmf zJ;O)uio6dq@tP0&29%$bQ^FN)ItbZe*KVKPDW04juBiSps*YwR$P>ztK8c}<#-@7M zFZ-v)Q8YusrRnj@fYU93UIe(hnII=eIdmzB5x=mcyzSt82xLXEs8#Un!&zkLHH)g_ z^L58*NKISpmMnYlsfW94z{k1pci`0}RJC;Kz1qRGpvYla%()zVK-e0}*8jK$6+Tv| z@yiM;T4*oCtHq)PdSo-4B~2sgYPz6ycrv0)B8f{hD<2qga^KeaRln4@BN{u*Uswad zD&n>A(#0hB>NNF>q#H~~mt&7@mUZKGwH4MfG+V~!n?bqx`>o<+{_j}f(uZeLOHfCf2cc(LDi3JYn+cH2!*GSD zws~O;vs7IAJq<=^gKpAd+H?K6F0{gX`wte9y5zje-nDZ&l^$d)w}HdN@ro#kxJ z_E&@QMc;mkCl%Fs>Is=e^U5Wj$DZL|b7Kh+=T5M{(B0DdcgpyxG{a~?|3)iW4l8WB z#aLyM<^5gqX`bC{j6$;QK&3LBezb@{I-0NK{+Sz6h7`PwOMHADy;W>{&a}7 zXlPJoIwnkoIk^N~rXn)>oWc!qP*^4g*PYCY+j8Q;urgo1N)*ZNg-Gb# zO{@Tgf5$a9=SSiiM`(gV$c^D*i0tzUIT+Q0X&0i%EsM}w`mr8v7>pJ~$|1QbxFOpY zhYE_3rr4!~6Q^JI#gk4Ea?dB=f|Ph(U1{GhR6+<7pTb{#kx21CqkiWpm({yL?3KHs z$scMU%Eeb6e3E?dE3w%oo``d-#*tPlSE02#kzJ?Ceng=5{oiDA7Kw(l0=b}zq#D-+ zV-fq|uS>78Wm1x#80fc3=gJ52EJlDvH4Q&-v!*h?5DlWm zlNNukZ{FLlKRtf;yw&euzESKMU77fsQsZ}8IdU1@2=EJa1naL}0&?b`TFSJ%+Fwnk z!Se6rv2j5m z6_XEQS2sp;@Ut+4Aed2ic%_AloLOviDMQY`>W)+lhE3C(S^? zeZFGhu+aBB^U=jDVpDb5u-A;iPRs4#!0@pu6`dzUb9PRFvJ`@id&z1br!QKTfbQ7O z9(rSyfya2RZiKbE!DYmJB{GYLGJvaX2=4$Rb8<}?s`yisdxiYi!7s;lI1jukUH7BaUJFJ_CR<1ja6VUX-lE-_wXV;gjm|ORzMh#}6BE@TfVRoqxzV-#Cs9yH2P+8%v zHA2ILJn@nw?^Wg|5u6@Wt;--sNuv%t2$hZDY_w-SEJW095VT)@L4i_i`GE|+hD~Ef zIDJ%NKJQI$aGDa3M;yXIqDR4S{0-z5*@Wm7?*`MUsFxX&2-%t_?hV0ksZq5tBR(jS zoQc#H)v|n%wTOHon$9b9{!J%Wd$lLN*h>(lJm^z4+xwYUM@&LumW9R_a-+Q9doXY0 z71V&U(mYBa%)8QV{YL{{m7VX=i~ zzi|;O=V!szIcL2$*scpr-ymX(^jA-ID9#8jsnq>-u2Y!coJk%+ZfLMIzmjGR{S*C$ zpcs_ls(_yG2ET!pY`J-B(Yl7>%}%kP4^l-Wpbux*=CRvRtxkaKl9h93bpfTc$$%vr zN_;Cm`VWd8bTXQA2!{8RwL=lRqk@AcQa0JY-u(EP75^_zCK?Vy2Cix93P@WVeN)7N z>u7>pQ%wd^pB8F~l%hOCOPGYrSD?V8h`8pQRp>{E{D*dJwN3(+TU<+M$I^tO3*cPp zWPaON=%FC1un-q>QEO8aH1=P#Z%if9ci)(kCSG+C)ezsLF-O3sct9`d$_nOE`OHl- z65-AbVDCSh?HG%cmGJML#^LpF|E9 zX2P&Hj*app*pxt*KwhSgh7aXi0&857!6#2cD2G{+?#&70%>0;lNrMKXzoUB-cZF+6 zv20JjYJIL*K!dIxUQGZfPL6r^fw4BxfXAOS`;Ie|G1yXmuH0hDGK1p0V;(@%kDKkU z&ZaDUL& zPkPiZPa%#`@H=KPsqK()C7r{`imVreCC`4UROaoA9CSkNq_>!{LrHenTu4sHQBJA? zupHvsep+41q$pAb{ya2Re~j;58M}Wo+tC#0bZNIU;U7##Ag>x8~-k2YeJ1!ww4ZpdScP%yWqKaTZ7G@68{q0gCo zBwDcT=CvxlvOl7sB6hpQbG?YFpVRv-S=>_adG=!q5xc>kf)puD!0jsy+B3blyMDy^ zNp1C;k{(INSZr&iq?@__)oBJW<8&BFbSVaNRm zgP0{<`OTVr$FIbM+@6Zuvj}rn?h}HsJD)OGZtea2yf@P}$?ZZ#LuH3cnxGcXKjPT-$e$5Q4!}uu1=X|DM}&#WR^DKuU8h9v!P935ZlPBOtCSw z$8^z}B&K;v*kD%SM$03hD=PM9iaYJkhIxceP~$DUE7pInsFB+gdLN!8=dW`8?b6sQ zL(YZpY+?TAUGQK56n1XPxj4N?JLFRX)o8ge7$xL+*m^>r5f52;054 zr?_zXpMq}I=GHivh2}KGldXZR&r@Z$rNlXP&NC0*d=DyDr-#{yKqfe0PA@sAtbukW z=zqMHvJ|Sw^TePmY3PgO_C<`u2Q7S`59xhOA~vG|>JIR~#Rk6t)=+yZgF1NDti~4G zsce!J`>wiX)F^TM)k6}V@ecBM;97GX5(MKc%@M44V zEyJku1c^xd-U#~2Qeg(^0s3VGz|eOr_~p7jcf~})Qk-Y1np-6R#4BT={3|T+hyos% zp5QRtmm!*V*V7Uu0rQK-m$J5ml&G;f`z>b=XP!kac70KRhf75|07~mK+~B^_;=N^Ae%N)Q)QHnVv{3>yf}+>tR`|kY zmWUWm+wDKL9gvrXLk5T=LE!_)FlzqG<9Cm<>no=_PwnAbj{BblzZ?C%x8p zYBmzc<`Fv7!TQaqPY~teHp&p;v>1Ain=pl61ciw(*_pA-Pv;WDa}Dc`lrb8IeWjHP`{?8*8$Z`6hZ-Gs*c^h)AT z#(9+&f=2lSrvk9@`Ns!pVvgn2o4&kL)4eGaOld49qMemi6zW@b<;drS!34o{D|3g+ zvErwHN0(pZp4uXd-~Ot4O!1MDfP%IwhOj48Qp#s1c`%(i`HlU2gPZ*9QM8D1>dshU zO#hFeOmV+}r#8yE$e^g@TCyC{e&SQfwbv?b9TxBykj~-u#II^*j}}g`Tk6exzOV=E z7|l)zRYg8e7y2l0MP2WIejp#fb{Skz z)((mm#+$MZfze=jVQwy!i8)k7EkNlfN2Mn9+Q79ff@p0F8ok|6@K}W*ErOCP7pBdK z{TZDo27Qw3fzDQJ8=*|+G#_|=uyeEj)ZvRmd6McF`w>617j(@TZ#@*Z)HypF3pw~% z(;*wJLb(3?6ZeNQmb{>63(AOe8K0sMfO4;M6F2P|-VLKTCLYcb?tuNNR-oYuWYUCH zcvlj$58(vU;yV}DhEs)iV@sp=y1sk?0E6ZSBqv>Y{MkIod_gX*oHE%VOh-_*5weq+ z%mu$G3r3v(c7$uHd`TDlVhI^+JN|u0W-+j7K(b{WR>91xdcwbPrS?TFG~#8<5s3)X z)#ShZxzq8Yce|Fja%*aXE_{kM;2Mt>^q+4poNgf{!54?hL_aJ-x4t0n?*g;OD`{n4 zW!4nsq0ZlYe@UNzTqvw=N!FW=e@ldV2q$(B1x*R}T067Ic&97ydUHV~wgiNDEazjC zad+kWh7cDr<(EZ}W8dIqVh3K(wGbixHb1SC^k|y!c^;V|BL$Pl#4j#ZvdPh>`8rm6 z-LC$pT<{+agf{HGrm4s>jT>PCv(|ujZ7~5T>5?Nr04#PPQU3;vhN72NeKdVcTY|S@ zS(z+=Xe1(Zg?*~*;FNS;S(YvB72VHNRzM7e4^aZzP6q>*y0^(flh2KZtVywc>1hbS zX#cbU)Taixs)83c0D^FNYEn1h1$1l={ZJu z0xVfRP0n9rE>paCe|VuaaB31DRPq*Orbf*e^9Fb2#Ld2=+66y%Ip8}300FM44(JFl zJ`APeP2MITgA|4W4cVsIb3JZB91yo^QUHWIVo>x}_*rA<&5?$Dnv})P~-3YD9%2hg&`=SBYCte(9fsZA_>T=Zcj!OJ2v%(0j_1TIEB^g5wbTe zEMcK6&+{VnEoQt>Ba!N?!FIsTBvZ^{22i;2r)-8Ydk$^@(dZ1?0`)j^t9LbX8Of2a zzInhs<{G_z3|wh`kw`H2J6g^4?G?4{|Ji$gwJX?+2Fuh;p@An? z37mcZ0qaf`fItG+Y`vT%Hv+n3y`8xbQ>lU$i$8qB&f`u1kl=AOKaeIk)>gm>) z*@(eU-Hv~Ns59W3Bw%I?R-|AHx(lw!7|xcx4`qG)BUsD=*-{560MoTA5-!GsiLwX` zOD|9k_AdbG4uNJ6ryPBzfrmkYm9`25ULYU;rws1{ZBUV6Bm%ygWxX`F2`F}JAaFJ&vqwE zNrk`5Ovcl1IRS#~=q_fdt-Mz|NABoeL3glFB+mtX)fq)P)6$Wi22hXGS2=PsW!cv7 zIIwK$Q{{+@IvJn$bE~KXh)IKc{{B+vJvnj%;C~dZPU(R|{Khr9Bl)Gy3r!EZy+*!B zNTuxdCv;o;)yOE%&0uUhEkN-$>H1Ag4wj03`Trw9%i$yxB-yt})0%NB$;P9DiK z0>oj&%Dm@(DDO^qDIWAboScTVB0;`GJ<#SV3?@D{H9|q7mzr zJ~F1yYWqTm&%6cdo~?@dPhKB)QWXugRA_GmM25NF(hi>s)e}fU(l%s#{+7crUtgpR z*9D#nG<&7Lh#_Mtxm`;7BICQ~Af0Nd?b#ECjeFWe%@sWd?sa2E_kDU8ubNDGY>_2Z z=>l+~Dwa^w-_LSZ%XFgckX?adlZ3!9t_{>du}azxJD2yz`{V2~wrEuEITE;Zf}Y%8 zpc9wTK&K!zH&|(8-JMRtHA|q^znEno$(FT$@rk<<(TKo+eZ&I)@js=tsRqjS+Ghw$ znyHtQYeiD%N4jm^i!_04Klg-6ft!slZhKdBi}F`w!=+};D7TXyIFsmU4gd+?Tw=Kp zIkBFc|9E++0Da{kP|sAT6Gc#nsIndSe%9!LR&PGpN(4NMcShL36Z``>r!Q3N0RJUp zA86>lo1Btwi}l})0E24a-*N|T^NISx{pq-lz`(Y%3%)c-uStyqwH#%}5@$c!8G(bE zCAJkke~505E^&n?>+h1*IC+Kk$c3Y!Qve|yJ#xG{-S(ZbG#3xSJ#In= zxqUnNKqSpsg8DrwM>@$J9YW*-S7@{!VQY5c7-9C3L+L+}%MiTHck2{gjuhToQN30G zj~#O@aw6?(dO~imX1B=@?22Vx{KHJ1Vy6f@BT`6RrM{HsTI|wB?dVRroEduquB}84 z7_tOZ$lkDOow#=@&1sFi z$nXQaTO6mNyvpMOi_`IoeZWQ7lAcIFPrn}&y*ys`Dk?l7Xaf<#qhovG6cccxD=hyu z!hv-mVU8b;vUP62>bv+{$d={0AZdKrZBH{keW^rR@3H=-zzF(FnDlZ>8DgFSd!{AVdR+*fb&DVgJRHQrB_ z85Wwwo-LG2-+ai&R4O!EUVMp-;?1(Cw2oRh%2$oqikNxF5@t;o+~>hc5r5e) zr?I?w{8Su;D-APHM*`0@ z;e$*@y=ax0=bRAt(=EMV1HLaQ`IpTYWc3Xj!*8Jy;8RKsoJ8mC?_R8tp>I_bT0PLw zG~^joi24*fxKv+Fs{Y;Q-PD%=1&Z zHB-^(jwL+-@vs)s$oiS-ePZN44b`LF22Dv(r%C=+f?$b~NismUt~8BdZcCuNO8(o- zEFa)qq57$9gY;yc?6Q3CTlkPAP9k2EQ_40bz<3I_dDAd zFx)9IWAX_b0eTOAn-i$^{Z!fd*pQ+IT*BD|ma@rEgww~-L8tt`!j}V5SdE0|N+#~!dE&d?x^3w!gZsbb zLOUw3T1&ZpdUd&z@`{F*frG)3fa+J+do(wE+Ne9BI%W4wgCrs3>TsnibypvE%R?wF z-=)#m1R7`INdfXe6sY^OmX4TI)B<4TF4bhGQXbF5qe2bWgp#}hA2i2bLEdBZ(!n{g z$V`?`fTivxquzPhh)TWB$dEROaSjgnAW!j0q?V9y5zc?yb#WZdgmt+ApW^d%auu}e zmn;X~j(=D7n5;DG#hS~g2xeVZmm;*c9MA^TaGx&>b#MG}JN+zo6X~Z>?}t4xZWS&> z7-AD>1^(#5Nn;^!WduebPIPst02)L&YGON&;Y`cC$45#AoPq}S)1>TSAPXUJv9|M+TALbs&HLQx;i$WWkc3kA#jvLxSSDTB;rqVR zINOvItQYT=T*^!veFie=r+xLy&4X_xttCwKn!K3?{G#{cr+vvEzy0|$|4qp(-)t^5 z@^{?EkcnZmo(dm5E?gR6avQEE(84G>9nc6^tQq1?zOfZg2*wISSE^bYaUcqH;e}?9 zC(h6amzehPC2^G>3yrj58qAg1^5ldB3Rir5Fs!;@9M~~YQ;fZ>Pz?MKU+ziWkg-(_ z?O^Zpd&iy=U+dg)z#*alib?;M>5MGn9nW_|<{^Xfs*1BwygoX4% zAwQr+m{DS(v3GD+o(b7&5cDTMDzvgTUD<6mMh0d4Gl12)r?TMflb(zNzlIop=4CS$ zCsS7VE_LS$;EMWQzFY17P#8+R(@6r`R??(a&GB4ACq`bui?bf*=L0=NpzC$C-Cg^9 z*j=p0(sQH|OOLNtx3SOpVh1YWi#LntYF_hf*?ff+7kMxv!$i@$7pn8MF1x0_s(iE- zo5`b8vzHX;m+=fw#qn^g`wGEaA^e{p5Lsk(U{hx-OX4Vn{*eIymXR0IAn))rm|7w%_@Eld!`Ly zho8kVCFAGIw670XE%7F)jwWf9wg-_J(OM}{;SgJvB9b_g1kHXgLK(QY8CBF__&+z@ zklT)z6x1>}oNOE#Uu+#qS`(y6R`j#SzP(^e3wQg?TzvR5Jm zPmQX$8$xBrH?4D&()FLH6y_r6pQKLyUJA9SPR19i7ci{QMc(?h^m1al0tvlNu zpW?!1r$say>c#5Vb`wh|Ig2K7^`2Z*4lBWpx?NTGY}^>DhjTh&^**SxGjLcHQGc1z zwU)&_8ir4M>nnwM!I`#6x-MWB))(>i-Gst5X5k;k-xW+GDpJhJc;U}J%pt7&_fZmy zG}LchGnL;z78+uu3?Nfmz#kH^?FVe@;=1f*tVr@jLOcq$RVDPyXta zz*@k6;%Szkiu&3H>Gd5riEZ0-y;03FwpY@F^^onfKaX|y4fRr>;;yMe`i)NS742Jk z{-?A5+stK*m}5THxgfC?D*!KF2J21)4S>8V>f?JW+NogjJzxCrrO7xPOP)}* zEmFf-pp3DF+eNvijh^CAGPONDu8)$dp{gHYp7f93YWB+LB(H3wFIw5Mx*KffXDABU ztLVjK5op##<5O7=sYo2#Qb^Kq=teZ-kSK7!Qt&%ytkyORyV2@J+?ILupY4Vc)+=@! zAsm9TF?sFkh+KTq4W}$?ICY=$vcPt$D90v1X@P5xQL(F~SL!0nY!lc7eN(3`dp zT(kBE+Y;7y1q&Q^rRD2Fhu;(*SV`_+wNfd{++R8&+l?nOs9A!JfA_A`4J(x~8(2Q2jNiquvqr%%=IP%6l z_7&pBMCfZ{VEJN11A5qc9&)EF3-cJ^f58GNEcUaMlRfU|SZtP*@=^#K^GLbwHwjkx zpIb*(G3)~$JJ1PEXghsnw_Yy#;CYBxI|!fG#sH#N<{`nr*_Hq?i?K70e<{PGbM|ec zMPDDqr21p~7h=l7Ny7q__vW4*v#>7Xo%&fw?8*^j3Ha3cjUps;eeC>GgzIBO+bv9w z;JCRVS6b=NJ&xVFCnYj+pHx8Lg^bQ2_)LL)3)DU`oLA|vOgF!UC!*uQQ$8j%ei|D+ z!2w;GUA#CA0W&C7Scd@_twQ=iyLDwiFqe1Nhr5K{TT>fn73FhpKL2Ph>rrC~!vc`@ z8viP*Z#1I%#)6|IB@;EAUJ2=ix`0G!rbTR|N9*| zACBjEf#lsM)at6h@(^e8j}T6^p35#|y7vwImu)068&0OgQ5aTVXe9@#v| ztVkC`UOhf85v6(feC;1|iyPb?^d|4E=wyBlTn)<5Qmh(7v3-8y!g^>sWLHxSJ(@4a zeCF2LtMyp6#d9Jt&`w&cnTh>G+i9IewqKn-HCk5q7Uuf$Fbzq(-=3kW_iP~X&N$TM zuaCn{KklO*{<`Y zV5DYbEzL}kl@W_VP=j5X`lPc)kHwZuwHtV83Y$yJhsHR`2;6cMeZ8?m`Q*{8dnCcx zUoNbEIp>9#DOLhn?`N!$VJ3~94}Y9P&;C|`)AiKd(xp5KPWR_D?#at>h~y8=yEzEm zROH^=Tp%fr?*Q=3^PV+bWc}I_-42LJ?|xfUSSl}=uaoP~?Uwm+la*dtSogiu}BOtQ$ z`dC%-P(4fp8FK~8$BKUrtM#yFZSX&dX3hjsfMC{1ECT*QvYb5yp%!{LMlIDhLLou- znNW8}ARft!n;rpDe&$haDmDmUxUR@pT#ERzj8Hyg-MC64w^*WIleJik(aY5xyk!1| z6-?j>QhC!kM?O;Fwq*V;wet;CJo8}uuQxHL=AtMDK;>LfcG#qxV6R&RWJ;ru``h%r z>B=|%_GU`W*NF5)pof-#q*8WzurTMcbY^$=U%0dLSy_ilg>?_fjvApfnXISPR(k_8 zciXd%!`aP0H-n+*@=J=v8+9{Pt*EGPKZg1B=>$xl4O9bZ5Fpj1<9?w8qf-ej(I1k@ zqG(wYj_$Z-TUW|O%^u9Twf&`RS^ZC#F7NsZonue%{V+<3Mg_B2IYn2b-`|K^v>ZS0 zE*)S!dggk!nVaWi9ylcVSJi>e{9y7WvX5*Lo`9-u+iNJcZ&3|>B*67r`|w<9JJJ4w zgINMse((tOBgIS%?W|Kt*}1JbsSfxJbn8WMcAHAG@2M?Wz`y&=v+ft{ki1&RN6eL@ z7ts^gVRxDAL|2Y{pv@BUw#urDKwMvC*qcE>90=!3AH=x$>RJOD+`D=Me;m1;l}C~L z>9JFe^w+Znn5ee9(jA@%`GL z2BFZ0%;?ctr^WqIf2J@Fay8;)Nf2qNHF@;+`}RH(E^sohLw(Mg=MkeiQG`Xvr4f+V z+&VTMjQKD2nmQ%7Xg!97#+sF^%WTGZF0X+hnh2 zj>HvQA=mapIp=AiA;Pl(!8sKv*ZC&;nmH7Er7V}{U6@>)FabN<0OV)Iu{H6la1z^w z#m}pV^InN~mBL;)T^@WMdiytkbE(usQk;zA9BcKFi8S5q`BIzUUXb6#A*cAXQbxFM zoLMm-_N4)0`-Q2%9^Eg~#;V6f%k(C{UO(KAC-R3Kij{RbS+9^`jDjpJMHUr33ph8t zflK+<=_&s;#9`8NrPb~*HO&JV_7bhlAlzBW42c^vkTYs7plmd5YubS%ZI2z?lIL*o z!KT&fW$zaf5(H_=c4Xwbn$>EumPb7PLBd(z)dzIHq1t4Gv*xR zYu8{6xOw?^txruP2zduQ>U@UNbX9XKiP#T8z9>MjZ)w^DUdstITbHH(EsU1Y!-jIO*0H)&SLPLBY z#1m5+E-ljXMAa%nYNsrWn!GAu5%gIGc2-vEAYX%#dlgY`q}ny(RQ2F3zyUkX`u~J99-KUOVaJ z&6E+1DJ;+H`)PR0euD>kVk@k1Xxt~K{Fb^c#xHoSZ-r{^q}0Y5`bTXdv%|z3b=jHp zE(`mTg~YQY{>W7s@P)Qzqr*|zU9^jWol$vsP3(q#HS6*)4s*tuFza)r@; zX-ht%ZuEV55!$*71nPr6Jpx!CN_1tW^f)h{B5zKVhO^v^B_IRkwjl<6*dY@eO_s>$ zXENXpmNUj8unY~C$`%q+qIQG5sjyz_qyAIxW+Ceq0thyGljRm}X+)hLskziOfeCB7 zGx3X!91oD{{)Bd!3>f+PM*^vGX?mQ>SS8c(yr<}qYH!F3@)BdfeR;e4Q8eiY$bQTe z{Z!#OJ=|CdZ6%F@glREg2u2jGI-x(Ntg1SjNuNeKX zAi*jYM7G-Bxbr#vhLDo&D;tj>>A@8xLoDDtk)64U>+m>+M&6L>R;iA5tnHH#1p_nw zZ>Vg*hIKuw+%xKft`uRC&i@QrZ)d@GMKueJarBWYnN~SoDZ7Nfy>jp#F7gUP1@U z;Ep^xpoY3He7Me=%X_%IExNR~s9J$(_Oc8}7w`C(0m+UOuY4(`J8U^8z{ilIu-Y6{ z1xMTPX^-9~m18^6JmLKa*tD9!;}$aruRiY9>=trI0 zrn&}ZeU<7SN?~hR@2#J`LeRoU={p5j<@WfG;xru~K);`p59!su!d#v0{swUL9x@)A zK-hS5-=Zw zbt7xoKwU?~(T2^O1L^9~CL|l>v)G+<*{h>tnX}N%MJ$O9Ghw>_1}cFKeLxVWyUUXi ze2tfbi))1K|2TF(AeNz>Yn+sSH8&_>+B>$WJ@ zkrRbDTiwP;IiXR@kxRa;FGKJAi|ZVn%SB7-5N&QXiYJ)Rvv;~CMlDdS*eIS)fmX}v zpohhtESO0%i_^* z>}?8O9NtefISWo^H%DMKb1a)V9I!Mnkl;FFciCWVpv)@NijZU5n*!xRu^Bw@pqcQnwq{3M3?dW=_?Wrv}$Z||b-e5!yLAso@J zc?egdk^|RMc4CpCg&F$S2Mm(-W~%41dm}&?u$as2v(FjQlfrUFNl+G_=*k7*7Gef* zAd@^Xy(L7~m9%_OB4iFk0^iIT0GLv)h^sdcs_?J53Dq!i63~4Sr2r%}R;Y4#g^<>4 zEJBt_u}j1Qmpi{d_vSM<14)q*k_EtJ->&oQ^&n?^pWZC|z1Cr@Q^c=7hCPI?lXNz1 zlvceY2=*;Jf^6AnrPcB9^p{>@S+1lQhGePKpgd>t(Q1{fj9DJyzer4u7?BosVFh+s zuVF+(eyMRS*+&Q-TYYRVr4cc)X~NSw!v=<@#&7Qzs%+z_NJHUTx*N+#_MpT3pv!N; z{8oJLIqiEAbK6C92pPa2#-JIO&EM(Z*~=~=)=m>&fB|tbpbjI%jwv(HIWoHj?pKZz zKPyukgDyQEi>3?H1%gP)albaI4ho&e6ZB!=EsKC1X2;T*_PTT-2Yr1uDK>Xq4&B<96_Ja zSN*Z_WTS*suA>%C1S2*==f@$2gN!+YUK>l^`U-=zKgR*{_fl*LCw1P`11gy--w1X?OqI!5@q0Ni33OyORPfe9p?-PUB^HlQ>9;| z(jm2t#-UbGK#D?8FBfc=4H@hi6o^8J3}8{bb)D@jvwBK^d7mhO#a~kaCa;`axs22z zie?ad%e~9!t!BMhzgRY`a``+9wNL&K@j_ zbRC7H<_FWMZ;Re)mC8byAc11m7%P@1#{`y^!a2NlgGnQfY&9Y_eLp|*!0cga;q4K1 zeuvAPjR9wIb!b4Z*v*SO{_;7CIo`O}(|F~;*liA*hk+=+L9itr{;UQWCIjplL?0-G z#c~L|jf3rFX*~PsaB-=+)ya`M@M)rqA54hx{@ND_X%!mO&Ccvw($0=oU%1b>H{# zO}V)lllc2&?KOuv-9J_xc$~zfq%}r-ommg#yKnQS-~ahqxq%nn!I&BfVsvMRFKBW* z65%8A%#_o^I!INR@m4O5&`#L}WK_wc8&rfWUg7ni{cb(1s-iarQ=oI5-)?ua&sPLKA&cB@23dZ^p~pIl6$#d;tUj ze5cYAK|EzSKVaMPY^Ed(2ewOCBuasn+_$y__3_B_7ehN z=kzTh^}O4;E5=vAUzFfdCe(6Q)EDz_cS;A6v>Np1>u+x3n)6dxV3|P&B)T(Ij;XW2 z?vr)rZ`Vcwiym8U%Q;i!_oZha&Pf~b7Xy;+!Po34k-_2w7RgV*;!!`4YpMxel^jDJ zu~EM!6po;N-s_l$Mw#(poqVECBGyP?E;Ucqjq9K$ zPh)+sm?}Y-bvX&OdYsplJDESx}izU%+Qy8`kRA;M6f0hI{xcHf}n=hZ!JOF zVt-SrQ7;OTi6g26Y`lUP_bJdjar6TZjjGmYWPSOcQeH_Vi$nd=DVI&H2zG5sW~d_Q zywrd)dgwYYWgO#B?a*de&LaDZwby)P%9+#Fu@F-b+8!@sKg`OEVd8uxVBYd|pH_s~ zHxc!L)&Tr@{#M(D;6#zEhLGZ>i0!LgZg1hhMb%8L5r zJBi6UD?Ci4?exqd8>*g11`nvVX6#;$n%-~t9=HX6F0Qo&WZB`_OeP&~uHhXSKvHn< zd6`p2IUnA{CTdqwW&y0Iemy7tbI$WuD6N4V18C^bqi2!i}|5b@_=L$V8HsN#q7BYUepuD*SENhc5=>IS6Xf;gZ z{-B1I0Vloh(2W1BE`|!3lum{d=il3La)op{cY{=*@Ei??O`pUbn1n20{XXOn;^3x{ zdBQ^_=KKWn(14ELP_xeU)~LR`Ac2#Dmq6E>3OYGaV)Pv`7Dz>V`6qC*?l|77TXsKAZz<6mqp?5Fud#PCnzI$nTP*PJUAXa}4sfHay-bIAwAi$evy8~For=cP$b9l{!FzA zKCLQK;s+)O42am!HvqAs?6~ayp!n9` zw)-?{rByq-sl)ui;%bo@3kaR&gqu>E6nC7THNAqR#?J7LDml-z{RGky_M-|SOI&=} z|0aL#?<$G;Q{{c@XrN@D%MSuMz6PX*9AnEX(}x1E817{K!%kKHx9~Hejn&ImVTv$d zd1OmlgD7FzH9?q-gt4$ZGa>un@XQPwe!`^=PX_GWqBOA$_L?_?!kTiFT|zl?!ZYk) zdsWsw&s~-bVX9a$k@y?rdwtz*CQ)}Cpc>zOnG8|;I$(zABiR3=n4^pwES9`K{1-(N zGrTs3$3kP&w$$9{|00LkfrXNHX!WE`v}+0^p^+jd8_+cjjK)~?`~IdU`bhzM?>-_* z*{io)FueW{DU6GTyhB1!hSJf`1q zj8WrN06FQgK|8th12EsG7=|Bt=5ipqM8+J5PlRJvQbyBiD;MNqn= zTe=&RPLYt1g-WACACc4id^|8^Vs!DCh(Pr=*9@y-2Op zEAsS|d_ybIBHHG^Uu&V_-{iF3xAxS#me4S#WBJx01HTp9< zZo4J9?m6on)R<&v(v}Ofd5j@WkYM-kV$)`k7ws=}kx)ncnjGU32n~p*v2m^lnI)bV zTfz`4G%2F$(D+yom+EKPfNJ$Z-0E`c^!2G@SkHg*h?p~XHvP{@>xR|)6+@!pOpFVv zebfa~%^e@g{ImZjX2$mNUcWw`?P15!p|^1 z%mWX_S`uXIk0u51&=Qnm=U>f(^D1v1b1PWF{kg$`j%@5UNJ5QY+`Rq4?&bli*ld9# ze_S4&Nh5*yz_|0X#|>sLH=e|yAmJH|_(W|sj|8auVP<0(IgU(?lh zc$e>JRX@uT6EEG)&bKX|4L!y49bvj~LHb8;HBzm`Pz|dBJ^0)#GqyDd&Uz=%BDAJ_!&oU)!=MSQVI0(CSfzENY}(zLY2QZBbf}> ztyL_{wnZeMeMWHvS)rsRlkpLioV}y*KAB#yL#`THbZK^2Li!u1D88o-CM3k-RLAmm znSC9MV6|MC9B)k~i8|5Ve)!KP&`7JbIg78|@HyJJ4buGI6{lGH`}uRnS*Y#4QtE!U zR_G&W<~A93vCS$NS=r{O8FR>pWf>eP87&wXh?5ddCEKyEeG_nzXlJ7Cyg2FRBLx@x zw*rfg%zP~&w7VbY!Bpy!bBXTSZ9iTk{Ybq6n}HX<^H#{o8z^Ybm%-C<{`?F9jER!% ze8pcd2$YJBcsgG0r$WO@6P?wlib$^bjK{%=zGz()iEYcu;o_K#$Z8@Yt_y1S=#S^) zuL$CItAhS{(N2R)#?zlb2a^3kbG3^wi$`jm$h>Y&Bs9sL^6|8j!bCzPPk}BMCoYhJijb(h0x#s=$*#FOvy#B7-|3#6nT2O`4U?o0{{N!f zFd$##OSk(W;y-^F2pGW$C@j37(Vv~Ncf4}hm{6jSI|L+I?-;eqG82|6H z`Jch_f5*-LOsxMqr~c0}^#A92(=Y?FHRUqHde(S4*`pO7gJaMy^gznEt-=29qfbaD zD^bjm^3J~oMM2=gP^tdj%0NLB4=G}dN`(Q>%g7m=vmB6kOANy%Q3GVvM1Zp)RMyn9 zC8`&d=YX`71X)s03udsWC$(!=Szu=fEc!~r5-z*;(Xuz|cK%ECiys%K`-hVv)>^U? z0dAlsKg(R~Nyn|7W`=K3BZP#4>+K#^fIJ~wTvaVsm**^6P&To?6QB&IY zQ6dW>B$j~7xm{`YEy;7*UlWi;KWF9vCX)u%;yZ9=XMqw@%l9`j{v5~K)9GM=lQX&r zS_xKAwWmUCp#k<|I39bso*meDAO4{~fA-Z2rSM`XflNZ}bbvlomN^5jPK%Uc&*FZc`=884dIU02sa(1m z5)EyNeNYmq$ididB)iAR8C$ zk4?;+ke=Z=9lJ(W$brX0!g@EMR6p=BW|rubSKHztR3}rQh@b`pB4(>z1BPA#N+I@> z>G7J&{ElGHDHo%Tcu;(6Q(?yV;A@TwChX`+;^R*F4?An@O(KyEp{>;o+wxSb6?aGq zU8$hCuSW>w6BCb`>M3>ftF5-S}wH zMXd8Fm2=sfhgJ85IQur3$uHIF-4OghY#{_SkX;MKOkbqN3g)>+@)dl#YSZZw)fXS( zPPb{3uB30TE!coqS7+^P@$JPepC*8x?ZHXrO%mJBY)M^`*9?^uNs7Tx^u7bytWWl< zzY2A}agBU-DVVAU^@_(a`075>%0k5hAU|4=uP-!BOGr6gFKNiA(QN9I6E$~>&++WG~`*7nn4MP%Mcuh<@PXYaM>z{j>Su*8p z!(vTu;LN!q*_FW1$lBJEBb_-k0M1Ll0NLL~n#1c_=_Wuh*jr40e^^)JFuqm^M-XfM zyJ=kSk4VS~V&2ZY^R$|wE^E1rX_|cjku%z`_!K6vEBf9TkD~_EKQ4XE&=k5im(A$y z30#Y}P{XZTq{H!d0#ruV*cNz@xq-3=fwo25b&*=40Sv&k{c|z5rI;Fu!Bwz^Dqb8F z-bw!?GcXJl2C_A)F!}lKbj#{caQ;L$Tf4v9SA;#;DC|le-3zBXE|JW80Lfvr>`qC8 zB#Jd%o20zJy`X(-U3R(vlH~_ZkT&-435?M}&^v~Na|pJ-JbIr5N&vD=x5p+Mf*lfG zeffCC{)dGKpwl#ZT&Dc__Tc+u8PqX2-5E=6io%4-khf+alnp>BT4tDj{Nd4qWuYI) z3F{H~G>q_kJtaEK@!TUm+5sz%nk$sT%>y2``$Zp^E_(H0@$>XC{-JY%?}{=rMScyB zi+MUg@;(W+e(>&u+TdzzFf$Od_IAJ1^+eoQUj%6nPaBRxNASjO7u%bt=98Vacm;CI z3vpd46WXiO?^(xH#4F?0rW!LPlujAx@@eK?Lv7VgS`5_=-9QvL zaayIJcomMY*S+kP={PHdL?rE|whNl&R|(Wbw`xgisLT7@SnJ)8TH1Wtg2OzF|6$!s z#@G!kVh*9!eeXz0XvRL-a9j1|+;VcM*%BT@-+Fnj7}p_~&TDL>c%$AHwG6AgI$yHF zqqT82rit)%^}AYgk};XBP?=4vG3GNZfNtqrbu>ErgSIZaX1BkWJIo6Oaj+Q$b2Neo z*>riNA5FYS1N(K%V40zQEsbWjgfcqKKC=-Rc7W#?!ts?basrQ(S1*j=_Gs;N9Kn&; zQJO_plKSP@jYkyvZ`FvTBNn=WiJ1;l!nWbj26II`feqbWyINgLRXP+Mft;16|7zDB zY~JZG7T1^wf`4gBT<`RVsJCSnFMDB3o5f`TB|-HeRDpZZ|`Le%`blsL4O(Y#ckPoVhB+amY@F`Y^?JD3l*=%qHVvM7dBg@6g^jG4g84kL72A^N%GbcH-bhLl_3Y7zc$<-@vqg2Wl+t!G@J)TH4JJ28eMV zgPZ@*3933$Yg8&vz@cq?>j+{APGC2_ubHJ5chwjwd9q1bK*)sv3gG4dH+0=sHIq)# zBZeoULToo>@cqL(fCj84c-E6<7@wyJT(kS@l$R~T$HAZbSM&)JD%G4V;4zHlfcT<( zQ$(mQO4$tT?g!$z%&n3oI>l*zXRAfFSgi`bGJW{>Oz0VWK%eU_rSN0`f2RSyREy;+ zY-)v&1^-p&&ItsU_YvlDCC|9%D^K)3K68)qZPU^QoiS)VYU9cwwK!nFx!tfI$D_f= z3fRX#Hj*t)=0sTNL~GQ|$wD^T>Iw9D*`|d+OQU2(ae4DV)aKXQR2Vkuv#sHOqBO7= zcu~ONrXzlHkM3q}oLmcPIO&3ZXOY&98NOV;T8uk*!IMAU=1Pet?Nj%?@G&7hC!r z^bIsF-Li(>;)L7TmDU3amoKYMlEEFnYC!$it=gpzm@)5ugo~4jb2)2vHo&Tv3aGPp zb5vE0w-P?OG=bYY#9AXEn6GxK&i$cEJZ+(yKo-ppIohBseD52Q*}F7@G;36@I2NuY z{6FMaBWIB?N+2_6eM-OlEsjP%;A9pv)q#4LBd0Ucdb*P&xWS=F6H}a93`HF8IO49Y z$Jvf04O(tPm{gRj`DKX3AGq~2z*^e9SsAa5@A11CM^A^5#S+B1$Y$Y#9Mw}^UM1-Z#R`G`%`3Qu?RPVvRg*-jBw~c&u-QT0B_MXm?fI$$J=Cc(&5h> z8QRV5A*YoD9Dz}Yj2ts1++Xyp)5`c+-$`EL)Q1y?y?%{7zDvN?gt*TeaL|s>H2WTx zaRY;*1~%l;-Z!#zehr#$rqY^y0U0_D>w-P3aEIp~sb&21`XF)iV+%odIP|JCQ+9x; zyn1u0v1jW7>Ujr#5XakJUmm}i`~rU+CuWXvIzL^0vl%xpk%D;$5yQQDHz2zHI-h+A zG`?{dUJgypyK~By4h`wjz8>S*z6UFbr4a4cK;$H6{iESMjIZShG9> zB>I!mu%eWG-!01uK3?|v2^-L%-u6`EMp@~@A`jdnTJ+7ot+=$`Rv5X#!^>nNcaPJA_{AQE@pGTelilexcM#)Y59B~# zXpJB}aGQ7F)c`MGw83(uBF|?V~9Rz3`2520k07 za->$v{QQwAs@o@?_VbqY^2z7`HGTT|Ms|AZ0tA)UvSkZ@n!!g_CD{ybX2#|F z4>*C?aT?a8)DTF$F_%WjqR(l2uYp>dW}ZEu>&(k}b+NBWqpy+5VN)M)=@Z3Y=Qt_h z6Ax$~9|Jnr^+`>*x|7*zdrRZ!Vj=-R@JC!2PMap|clbTbr_{0~bg)iKfY^Fw*g5dIIpgyDcpU7U zwt&g-#-c#mr(Ce}qh1#Cq?QdZ*p3u$!RhpRCD?Sfb|-$@5ZEY<%aHGF;CR`*Y|H0+ zRAQ2`4dpkFWf;LGcEe?*IoksT^@Szr42nME>Bj%mI850$%d}EUI!@>{{sW-4Gy$Y7 zhqQ+~PKs&Z7cHFh><@5h0O%h>4$Y=CtQB=}{5^3w-I4H0Kcy(+)i_RmSRVgmL$l`I z52%#t*!;fBy&hhV<^J*tk*~)w9)3Xh%w(HsV#i%;d2l2+g$ZQ7k6(Dt88e@D)$7&I zk#8;IHLw-i^?Qa|jQP~Sp=cxQG6)2{UX$%?&UggM;$UeD9P^vW)4Uu&|*$8fWeWveun2Y~5XKZO}| z>HstR;b*s?1SCu&T{vDO!W8~s70JYMC+$}=oNYf!;ul}8444a{VQ{TheAou?HtW6D z%;K&KT2A(Y;5_^t*7}UsB+#^7*!?c;;n}*pBrZOk443THNwN<`=52$|QCsvUm&fKF zvrw&^WQ09(aJluPm=BQ-s7V@SW{_P$yFj=YB)SDbsDjs_Z+EUdYG~s>g3mq?w1v@7 zpkm-@#glmXig!HspO4MG2I!mKbiCm90i10$;%TQlD@@xI>`5ZoLMpkBMk)(DrX*(| z;9s*LD5w8;jM4q8%JR-7LfNc1@%wX!KZ@8e_S>0z$WFERG!hyhDpX+RxeK_YCf)BM z>#OIx!bCK4v_KM3YgDZ;ZzHm=R#s#8+WlJaog_vA6NVmOA*=L^Fyq^R*jcRlbbpCk zff?@tUa@6oLf#s7#7N6kJ8a4V__R`bGUpqDhuxu8lnPYO*9IRnx?m8{S{ze1$uNy^ zvg_Am8TzAa9LasA^_uc}-PUfcx-(-xT-NBbmf6xcvl?GI2shw=%~vQce8C4X;>=pu$U2LyZh>0y&?7Y z6&NL}=W)Phma6|y3aIhVM@ zQofd9cVe8zWz!N}fZOiZ+2}W>?J<&{LB)#{(Twr}cW>_KgDeyg}W!S>iTn5v^ z(}?Sq28>fmy6@t>Gzl!=$t#-r{g~kbL^r3`*)F#lnrdrLe9Jn@X0I+Wtyl74Gw2~R z_A~XQl`LcsgtCwZM3tlpD#$y;v?BJO7kV8Kb8)=niCb5u=o=s9OhpgMzTnBQXX5DB zj!@_OxKS!ro<3})rdRix;72>L#EV@GTj$w(s2 zn0r5@Dn7-|8JgC9q&VOV+iSUxqH9`G@>McS%Kd1AVzfChFlv9qA=ooKi#)>fOL^7% zTi{n^x$RLyy7{NLXv2_dZK7+QZHhrZKls0Rt2dS37z}OXjqac-|}t&y+@m6 z3J5%%1koP2;2I-mOFf#-M&kRaMbWh;s_&+rz5WkmOyQrv%1N*(gW>)LI#2qWq^&rnd8_0q-JLc;4%JiO=}R$D%)T|E5wrOPPMmII&nmwvWyRkq?NE1L~2h(Cd@SVZbp@W-4MYCJykC<)J|VMq!u}p(XU^J-TTwH zGY`EDtH~PTAxMv^l{N5*okbb2`_n}szx2u^3_~{C(D(K}+l%Rtly{7RbH6RI zFxJ>-*|}@;f%#$vlI`};?K zJ`XEsm`KWL(glE>h^8AvI4N~1W_LJef^imQ@K8rGY}t)sv#p^GfU9^16TZ|wcXaqgqK$nkzH5-j1|*rZF^so?fbRiO1{EUN*a>HiLzRMH|0ggk%MMtD7lqtz^PQ50O*bz^mY6!YEqly3Faz zAB*0)`o_qlEfDno-i#etdSOVf7o<2EeGOr13t`tkKMz-JD{s;wf9T;!*`ovk5DXyNndQ;9K74ZoJKqJ4g!IJyOYi>nx35#Grv(=&{$>{g^XFFSMB5 zEk3DwSeB|nc8T}bU?&%*ri?)+zvj;g;car}z7q^F*gH>Nk5X^Sy*`?$UN$WpiR9Zq zKPTVtN0z+JFKf1ploF&Oc89q%l9*5uw-q(8?++O-#!v6K*)Zy8qPEklwcgP#tbvf7 z#~9Xu^WQc5eH1%sr7*MjL%N9#p$NeP#3sD^26jJws-@s6#z<1%`j``@7py9NvpVjD zJG&o4kJwh(t|d)aSTmo2-`V^1!{~vyMq&J1|J*G|ey~D0AQc!)lH_6^&J9IwaCg)z zk8|r)&#Fwt0l+`z^&3%WUrLZ4Un%cjRM_K?Y9$cmh3 zv@*{UeU6fsp^vqT0ka9)d1e=)Ot(Y5YynH#sL!tn6$~jbK|R7a|2?pnR)2?lb)+)1 zL(?x29fe&5(piNlDGYLvWD#sThwGvSMxNj$)4Ieb9UwfaxItMjO>M3FVZu4m;@vXXqkaMcw&kwXSh0^zmRF zn7aPTQ-Z;8h2h3Utxsr%<;vdhF?M&WJGIBLIo>X}|4?Oj%W2zuzLp5THFfVhpU(*u zdzC5mE7`zcN=!Zid6h6cgM1}*Y|RT{-S06JOkTau@%F8k_AHm(KIBcfW8i7XH(PFN z1&dRAPwdDWwvJQwA#Wd=wncP9kxxh$V=Q~u&?_c#aU!NdO0R(8h5O(plq-b}J`fej z3a!(#t5+7xZZ}VIUiv$H$BGZr5>9x3V;)HXN51U#lPaaKvRfs9x1J2x{kA;I>e;e_ zJWnFye5TmR8OEj1`5O{?*6cIC3bP}hzTB|;7G>}wqZ3LAo9G2OOS&x)aF{5Z1oa;=ntT0toop|TvKYyx zUgGzJzO@)hAKTDjTxq4KAIX$bL7*YCBheWolVNBruPf^pem>v9Y`9#s(iSz_6(xQ$N%tY9&>R+|y90y{kZ-4!rMYuGn zqV=n8oETqI(k;Y8AuD*KP14gbd3Rn#w;Hn!Db$30uz1~+HvTGQ|?plpI{bb}l zXzHB3uYof6rJ=UmL^`@Z8;*G?)po=0^Dv_mnbx23m+UV88#g<53Up9gk(-keOOO!f zpf$K4O)*N=Tr=7Ym!yQ_E_Kf-%XUQ^+F5K*8NP0hN=T<^w-lbUeHu)j+Dk6xoQo3& z*t(+zp4&!-)Qr33vTr1Xw#|RO{v!YmwAhzV zff{=FOo6!ibUzS>=*^#WD<$t)=~Qs%72)zS1xC&ylRqH$CRK^|$XTE5Ol+fcqRhi2 ztR1Gx&HXwOQ3-E|J8hW$zD}IU`2C8bzBp z@nNkF(}tcm%|WbQ%GNimeq@P1jLnOi-7vWuo=56d%Y?-s2%4~F8AVu;54YjulV$YL z_t4#B~iAWJM9w2vnZa<+|>T!dMnl6kV0o7IBX0uC$1kZB3OnCzWmp&svJEwmz_adV?qz?npOTYsw`L(> zi%A>(_)pg_7TiB#d~N_D^*G?O8EwdWzSgfz%JXXZc@-=3p6`P%q(XSbLS`fq_s_s2 zo(k1Oeb(AwXK~B!h!xrOB};p zieYJ4+6B{jjS-)K?d4$;D!!ZUJ3I1T9#=|3o~+pD7^V$Y-iUIu9hBI7h=lEyr0%{F zobnf4`J(v-|8%0MiJ(1m6HXV7m~=wc6+Ew@{aPhq%aNxWwXtyy(Q#X&GybuAiK-XD z<&I(k@?f%F^V0IsTRO2jIL|)u?|ZX9)50LX10`lUvx(qHBgjb{W+Wg@QTue-Qh&_1 z8+N=dfAbHtqj9Wv(<9959jHn4ip?{Lf;JKc>05^5JrkpX%pN75fBoZNMRC4qVKZ76 zdXw|9MwMkBgKiEjcXmz zsdT=V`mYs=ZHGw#x<;pC4q;x%AYn(lqHa<(oR#gmM-E zd)4oaUbPJs@IQj2{2gr?j{GE~Zo;jC?yJ9a6unHc&C=MAZu-C4pJ_1$TAw+|x`|cx zM-~Sr{>E@v3e>977jA27v?{4Dv=PW^FJfXuLP2D;n(aQduG721JEWa^mM>WXgIl%B z3|OM)Yd;dDEgN{3!R793k0-HVBD`E`yA-{dsMax`gAY&499-6nS3I%O4SjG2y#2 z(^6N#{kY`a7IRRL$JGV;XtiCD%r-PUEQItnN5yOYwUw#HWrg@#dM0vwJUSoV;&7<- zy3c$quM4Nt7=h6KLuAcqqilI!q|q>2!(6{e1r1>OgZq5|E@6Q(lw3KxIaGek*w-W|zmHk)+_p ze7VjXf3Zk}6}7PKC!Q%HBWx8EE}2^AjRnDrn)G--85AVnR(fMsqU_I*8Mi0k+$vTddrl#BMZ5GN>&ssEu()ei#|^v1 zZM$Kv8xvvVafC=XlMf0Mw*CQe_58TwNi4-jG!2pq zPrhExEFckFz-QPRns`Qv=H6qBNf?-3AEq<$&MvnLUtT9a_t_k&Oy)RYNT17|nPg(% z&A}C58*+f+AJ#xQgE>RAKsRn*Ktmf8+wHP*@%e_pzOEgcnPXPH6;6EvlQQNBkevUx zlM>%lw82NkuuyBDYtx&>o^5=5u=FMN)n%VM+VMp9>VKuxQkDnTP*3~gYdRTn6!dl# z=n$vqmMUec-_eHtJ>I4S@#E8rQb|6=&e?uOFY?2(XM(V z{>5YSL&?)GcZbULSzvieW>5^Rp(7^F;Adqr4_?G$&U@P670q#4+1XlSiS;Nd8kGPb#BW%RRCD0;5YjWQjni)eN4q&TSuFcM%Wa!j*~T+ zxuwli&+$X@<{7bDcc3+>PHu?kFysja^$UTY`r3NPet@8ESi1RMHxzFE7*eQFyKjs< zI1~snMc4NSdT+S`#T2^K)DQC35<$fm$XkL=Q}MQ6gJ&_0Lu%2`0_)Mt)2F3J0>+-XQr5iTjHldR9-jDKRl%g>>1Ymmc6 z))c~eJtDAyTh={?MQWYQ7RU?naw^ol?0$3uCde*O#W%!?8BNQ$6PhYe7HbWI7ego9 z{hzxT!tJ*LhR2VQLzw}Y9+=9(u#Q2SqUIV=gA%F)pWz>%aTg)~;6FSA9&} z+w9YJe?DyLgqUlFZ7*N;kBQA}(CT%T8aK1hhAu!SF7TkagUOb<2+KsIZZHi5WkdB z!^wyC4k@!KVH2cns~^HzCi3s2cV*c>-Elp1jd1PZP@-Def=aB7-Ys?lexo^wu+d<~ zKCKOjZi$>u^$zjx+W}DHmMEXC_1ID(9=P$rBy^6bibA1@g1DRLe@jPp`_B4<9O z^A>}r&^?Us42``F0tK#Vf6)huC-PI_`wRgj`<&FOKQsb@L`I?2YgamNb3{)trr6Ci z>*<{zkyZVjs53A9tgmR8_rg&qx?MR>nIXrZRb`=WNR{8maFcO?qP;FMCYXj;t5YEj z`-9c;x}ffiXs-SDbJ*Tq{kFRC^J$9SXc>u=i97oyO(kSap0zOaFsszpR7z=F;j<7qcZE65y59w9hosnu-XeY! zmz}ax&iNM?`h|0!rm6aMI0f*UCku+x?RWkv?0JrP!-JVdY$ubFqQ7|{jsJUYsTlEV z2rD$bzb)}N@7+O5Ij0bKHeiU-9gh^dk@5}yW< zX@E%EonOqn;|F-CzygKNMdP>2est&yx?=FlBvde&c;E;1`*3`VrP7pzD*F(943?Br zj59|Zy*&vGV12Rl7_V+fUp?pitdlRKfaU$*y-q<$1h&t7tf;coz~OjE>sOPaffzcY z610fFpW3j2mg4gDg(?AEH;62m{UdUf6igOhuF0da(|X`=TyuM}_M36}oD#2+yXa*S+h5?1!6OOlLX4R_ya04sFbXa8qyh$jj? zL03~_{X(qhbc$Hze77oHU`e#lb%V2!np=g=I94vE#N|fF>FR<^w09Z$vkDDy6E_emboSuPoqKGL8A0KS zQP}oR?HxYv^`Uzi{Qgy0vYwC64ybH1wqO4H=n08k&_21$L}om+IInqA=<+))iS?e3 zbi}uNtzprH_ZwASOIgnZ8NGI{x;NfSWN4tEh7`Pu8J!fM`#%5G{^ngopUk^MbII} zWy!cJJTB8?Jl>j44h~i6KHm-`9Qela_m*B^!EJwjzM6m0;GV1$y%)F8r7wrjThGih zMG~a^vgGK5U{Ccq|KIl*opN!yjGiGr?|V9b!q3Hf*UFx<3iM1q#G*zQSX>N$ucrM) zkSM-b+q*PfBYs}z{~pp~#1P#l0Ckt5%-M>0G!fH35xWSx2K-#Ywc4`Wq&5Z3aTKpB=T#CJenZ)}r6ZXH{Vyw-1wb+7(@G9f04 z&+=DZS*NrDRf0RmPG{;LmmGv`Udu-(Sr(p)`Y*PI?^k*J?8f3|`|oWfLT`hf6DL)T z;1FfYgIDSvd(Sr|ZXBLWhzl+ptR@pZz5jPGsHvfhi3Rn`pYD;Soa~Z3-NI8#pijKX zuBUXgHA=y-`1jVB1wPuZDoh6Jf6=%ReYREau{`Co&i(#rX_{XuYc`jD6<=%jUn3({ zC2MXhDrj?)l%kXC8Zaw)|(5Iixv%)R~Boe++@eCYRQt(rOT zs&!9bzh|iWOjNZ@fynUYzqhi7vJ0XGjq3pEMX+Y5^=5fne%oWw&NI8^y`257PSNxF zsxgVn8)x-bbvkfW6G(b{uoi9p{!>QcoepGYXtDmJ*|8eVM-LngntA8~_oCZhQ0VWG zaUm9#FAW>^dbN+Pmw5F!@?ZXadPaN*15mLpclw{=G+TU>P~eVfRL&I^Iw#KUovk}{ z_Tc@Wuh;{R5xv09sFkr=^_n|l9r@qU+2Gu;+d&0k`IAe88N_koGkozywBJrkB#|NP zxc?4#Toj4r|K?u1=TO-b24LLbT*>00fLP-n)4yw10$EVdn0SBlwCEnl>UyZ08gA!= zNhyCVgn8JV{5`(V?pS8_fzp2$PX4U#H4pAmUoy40+np&tbe6{@Vnl|_|9xL1Y~eG5 zU9!yGlG~m7PE9Nk?lYyZ-2xA)?PkzTyuB^o?e0t8t+>3a2Z!X{zYB3f|IW!S6XYH0FZT9H#l7b~ zH=IxTCUNS@z=CH ziP=&e4ad6H{&2q4$r2=Zk}lA3dTQR(hWU>EH+hL#ddFX+e(35PASrx$ zIOcjbh@BzguB}A&UqCP?C$~-g+4Zmrnne#U3d5XBP327ceLNTkaU+gL zC6oio%*->N{m*L5@FOnSa*W-xQxH7=azwXu3SKK zD!8Pse(fRbT1Fk-Z|`T1{emW4Q9K%N3*n@5cICM-GCObx(&oYSMBRO5W{~(k4G44v z=zZg|=(HbGVj>*gS6{j8FU3aj;9%3kLM{hg;Dt$TVaRfG|5nq(D=&hWLRszf?_-Ys zL4AGs4*I>SLL8R<!VXUB4`8p)*4lv1vx}&@v$Z1mXPiY$|9z0}Q@OkoI>Jog>U| z4d+|+#8C^~sdu1T3me2Kabhzui20ntzI{?E1s=GY#HW_j#t8TzLXHTjx9F98rKgbY zWQ2@QDku}e_!JspGFJs$V${!XHy)QQ4YspYp;%t(8vEZqT>Fy_da{vhDoKwLsep_2 z^5)20Jii_Jg$+e?oSP}o1-cuAhZbLhf{6^NiRoco&VBTL?^nGp8HJ$jyGLbDAI=w0 z8q^=9|G2iPL9DJ#tA~3}7XMB>eVsu+pFs)n`yJ?_ z%%WR1L1dVvhI=L;lS7%;e;>NLyVl;)?LLDn;rZop%vn^4Kh|s*5SDnsb(sbkfKE_h z-5gM6M3#d+V(yzJ{UMb|@)N-#wIck|L8>`~_Yfq>o!~(Ow^ePSW1|X0wE0my9RF>= zaUtO^)LRsk7eDsN#INKND%=H@)?;Xka2t~C$?eg}o$z8Tz?`D}$@)S0HhL&sY(UfO zcAenXVO3*1d~7FpKHokpg3vjIETTyBO*iN)-N~X&!xXGpsZk+nJ`X%kR>SVbE=Z{4 z#9q9H+TsEj&d#Sskd%h~Dk@`W{VkF9m#j31<$nd{4mXT$<={!fu=YSm%AiJ1MHF)oVk4O#N zw=4k{Cty>3oFVC1_Io|=EkXjH0#+B;c$pE-TS!)Rf?;;EOcI&{cC3SP@!g_-M+`;5 za^8k8+pzsWez-+n&OJCYFoMP6x84;%l*=bQ!H8yufbeBWxNE~9faSI}^KexRIlt7& zYY?^Efm9QV&W9AACP;fBT7GjkQ-q1HPdC3==tEJ78J0*X8N2G{?NW!Ojev z!Ti2#dU?P1Ppe<#OoIbYrcYD$(&D%gWGi$?ElYQib6<#xUT+E-c1FWw?6c_ z|MX9LG;#BX3d>V5XQaLd}9Q^U3?s|D=1_l zr6BqBT$v_64rUk|w1cc_p!KGXz@tFC`(X)XS?JJeIz%*h%axkm3d_|t_s7Y7pnJPV zBAsP{sbEeGe$`@s>7K{;YtY#HUn88@e?Ah=slkTB$7>6cDx!gJ2yzD&!$PQeoS**ZdVF@qd!>bbg+8mCQbZ&hP zC~#_r7Xl#I<`A_|T){}J%4HryJ%_scYs?vWE{!3Ac&QX1g@Vm!Qi)+8Bht=ch6{zE_>*dQZ*e9e-p$+9&*^k-c zn7T-LEYnKZfo_9SYv?%Kk5bEM{yM^{nka|W_R;zU`E~=zpECSBOcXFz4;bQFAA6+( zefUQ2DiSK%m@ZV%--0V?j0e_}e@uHqZ|NeY3bJo`V#)7Nt6 zZP1Q@1(Xd5&=R=Vb#&$rD2AYN-Zj$_dXOz*^Xm~pAVtC&lMZf?t|%g=&9#fxaM?oe z5NLmPCZgao2^zX#cHKt02WG0#Pd3S522>p<$3q3T?4LSrt=u5pO_WP7fbyOFuzm?R zsAT94faon9qR2MeMBew~>$^~&zE}LoSIP$jpCX@D+javz?=2`J=D4*E+Gfx~9|5rS z5H)lbycVB$Fm6!3?K5?r@3^7UZw$zT@q1u+7Zy4VLC5{{=5E$Wc6J`_q0i;nwC!Dl zf$tLl_za7VH_1u49`C=%gx?F^0+w^XdM^fDL&8lOJlpAT)+T)BT%S z0MUqDKZbn30o*_YGKdbA`d~@02dgcf%O$w^4$nZmHAZs{bOr|~(f@G+B`2QD6#}79 z2^;pfVIUaY|8+tkn$TP$ey;}BgHc!jtbEUetZD2kz%Zf@_UMD!8{h;gIHut@3tMy( zw(kFWbT~ZvZxdWeBPbf`L)Z~=-3&ZzDCLaWgVmv;ehU-mIgQtFpXdRM!5$pi)zUr(om*oNrma+idjwDBF_>WVVe;5-=`{y5 z26!HC8G3wQ{+VfazSlEY13nF=6Y?^|`dA8%D33!Qcuo5GO4ttY`0#WL5zS6LR)4Y7 zuRBdO*z{zHCpXK*TVXt>`~*RO>X2FO$Y^jv*bfodNAT(h+&! z8VK!1pp`9!AN4kwxuW$rdVpJN*skL=MfVEVI5B;^bG=KwBNCb3K#_9exY=u0vwj*4BJ1jdquLjmQ`L5& zla(_ojdsjDDk}s|{8FuSvH@nE)RHn_hIwnW{A@=C+T~I<2%^!u*g(&P#e>y{c^`1Y zI9w+TLqz%kB>be>HZ?*PMG%OxkQ-Kq7;JNjTV|(36)P2{b-lmGhOOMnTi&LMK11^q zOxnSnx1LgwH2Bs8xMVy#>}d_=SD5qDBkxU9pHrIJ5)v2N{C@USWqp`gzD$WL$mQ!dGa0 z9fZ$`hHTj%WJI}KYF(KGVU_La%Dm?D@uA9sNQ=q;hpDrUt8#s!JtZJr(nvRwqBKZ{ zNGPR*fP@IrB`qzDgeVOvA}!q|El5d8gLH#5`_A_K?)_Z;^c+Rm@B2J6Yu5VCq>j^{ zLzUA{V<>cFvTCO|I_+$C==d=SH<|65jw*Z0q7ZJmo3mAVPzi9u6+hf^avdh3LA$tYSzSdZUW#&)$ zRC|sHVz%#deJR87>>4-Kf|*-KWlpVh8K^@rgN|i9@D{z)l6Za*-N8OUeUOf)tqt++ z_UE2EvwlyH5kT&p;;jF1+mL4QhXh;g`e?_-5MnO4MUF6RF)M zzHJuJmGC9(xo_|PnzToDgmvK?^UXTb@?AFa22`>t4|dH)*)MP0gPSzTo}hkgGS%cK z1tofacOE@s18u4ARD!oD1xyWVv*0c6`JCa)g>>uxR0{jNjhE_9@ff>D*MC<;m!Upd zXG&h5oWrWntlp~)nfQ~4UXfF#X#eegqRAPJd=(M)m_VKq-ysjnS*rw{~aBE2p{!$7Iijsti$;GDc@8sPOif+CY^h|#NT&+ zsZ_$l!Guevh&aoFm4!&A2-Wp9&23F-U5Jd1^^2hJ=`Eo-V4CHAh(gcQ{@7z<-MsBT z@}+46BOmr@B|&%kVcW|fZ4*lOg^}UYA9uGBGwo{}G6P?~gkT&s!WSl`)aLXZxEw=q z+zdcScts~M1$GDy$odS694VKBg(qPRijM1!CLCsT4W32;hS$SB5-_XmEwJW-L=0<+ z21m{tpzXgo+gXY?ep&Lg#aq~)fgL@e5}7Cm-jG_BZ17f5m3H*wc{`&yAD^q52CODj zJhZv}W$uSNRRdC>-mzNzo09+-%CuLS|R$IVp+-1+bZ+&w639g z9lg=NUS?cpfwO-`dbgc>)?};o4Txyn6ejW%erB7m99&IOh1@12nr2Rg=6-*V7;t~+ zF%(^FTw&jdxS}a<>&*yZl1)WwyMb7L)vnNjxA4#{WA$(OUCBB1G!F_65X6O-UqHl1*o&^ zJn`$#<0#2y@)WBXILbhP+&*ml6U%6+;m*QhRP7K)jTOs(SGPdlZKioJr}O9Z4z`@x zvcLweW(i|1wv?tU6Y(0@8+1^k=}5J*>zQF}{!{Z7yuzanbe6HUBIOKgFADTamjSR+ zey~Ych$ViAuKbERq4}s38FHi+{0tvDM2~Ezr{{0Y{JJ(nQUBYNsBFIkQ@{)(txcGp zEmwm+foDt)kI>B=D}Y(~=LQ~2!1rxpA>BI0T|;Q@lU4^M^;8#6i9*E62Pky6Sj!fQ z-RAoo0)qZru~_!KI~&sywZ~kUledM1NppUo^;h}PZRu5uFBl_975#R{##O?&L11mp z@H37L3g0x}5shasHEZCN5Oq4q#6hodTKV>2uCJ8wIpu#{60v&KO`ng5W@16_B@Zgo zy%xvRonJW`@)U`&$v4~gq+o1;gysTF>iOxtRNHn9C1nP5j#Iz$t;}Uf;G#1_&x7Mg zD~`&gSHuHZ=E0xb{9-^t@8+c~CjLO1^{9YS!lR_Xq-iF{N)9^-I;U=Ek+%|J!HE*5 zm7}<^hmPetya@fsuQcN59tTSK%=-lRNMK?Kr-yE5tTemEWGL}o2(pg_Rp~V%ri2$1 zrdSY*Z6x123wZ?cxz2C;P^nH}TqjvKNx-$Z6Ph4U?R!}Z3pYw&}am+)}Hf#zae|}V?}96W9G>>dXibX z4^sJ1wpKxdNwRwmfo8X@aoYriY8K@6b%&r?5YcYdEO@XvG3+qg;k?|#I-QQ$yEHG? zq|zic{n4@Qv3Ao-;kB8<2A;|0kU@LlcucUb?uNbfUo{B@%s59Zf`1 z$)I9cZasLLya~BDuwbunr7wl1z#&**e$3=JRCAfHlrEE z3{!OmC~Ip%DSmCE_S7ps{D`SBF^PiIgwTr*XBq+{GiQF!K83LxBwZ|O9NUabPeIw% zc|bkzNB`#6{r3Z&4=FGMu?TVY8a0bRm9Q?lF4ql|bUWdj^Os^$LeYf)P9}rQ5qZxv7MV^RYHo@mqF8EL>;Z z>C;mm^)gx*NxuIZR}qmA8`hqDP8Ijl)&6*K%&t} zugQiQDnA!A;$#qabR=FJ8@)TqZQV4Adp4}mfVjW)k7!|<93twu zx!Cy^$M0Y#B?=WJIDbJB6)S)U-vTlIEhypnXk>5nR<+?ZH?y*~nW{{~qZ5;Fs4-1w z^=jn}g-f*?WLJr3*9=i-HlHef`IcsjPhi#ryvqw{`M+)Zw$#Ce@)0(c-8(y`f4X8| z1Y2wWg{k(Hyb;jYc7qJ(>W)Wtsc6mew4ObPpbV{!=%>c+IS`|CnK8&JR__W~-q*7z zs>`jNsie^JY5sjPuDF>NA^jkAybQ9=KRO*DV2|(q5Q+BSvnKq>D*ES=f7{|EpU9ta zw!W?YeD9NJ(J!TZTTBm9MP5mP&g!%~XFoaf@S|$samP36%<0y=p)5Hsd~RLf6NQs+ zNz!G+qF)OihpnIsp9lhbYV4NSHFqFZMbNT51`87g$edHY6I@Z$cL~Rw zjGFeU2;^bTm?-5Fs=O)U7&u_j_09msS4=*yT;D3-`X%I#+IYGQB>kWfkxUk{a#IgG z7)x~a<&^%XsE3P4#k@?@;4Y9E8iZg_&*=g7#O|BJO-Y$3bT#sUSDe`yCLEez_{*p?5X3+i`Qb+~RQ(k~6^ilbJflO{oU2*x8ceq#k z5`e)7Fa|CV$SNc?nJJLC210_?sWteVb5;Qs1t z?aZNJJ$WrM3{;w*n(Dy2Ne14rc*EVuo2z`Vohu%OfG^S((n0asaiJ~q+YV(syB#S4 zI^-j{Merz&W@3n94wQJGz9F(utb5$>DhhOQMLAtyxT^qe!r3kv7-_~)w9yRi&65=y zBi4ezo3x7n-byUo92@%9chDFH&*5guRftG(J3fIlzAxbsV4%US)_t=o`CVKaR(AmU zIjj1l%fHqWC~CrrkcSFgnzP4Zv8z2q1|gadFQaUhmo{i?K58wJGxp=tOqx(=>*W86 z2S*aj1p8J^kt0(oNw@gfNOUOF4;HTfyWo{GX|OW&*N47DwMX7~0{1hZFA0*<8?aQt zNwwUU@=)+R^!fVQ9fw`z(_Og%31t)cI|aBno^ef2zraipTygDid_Mzu&It%r*rz@o zc5+(y77}=p%DJhmYL=(lbt>90bHwH(WxPTWH=o;!N#jjNS?qyC0&-32OZV5r3xqJ3 zM6n+Ilv7|*r3yL=OB0<(`VnobZ4LK#8fl)PK1kvT9)7XV5%pP|=Ba=; z_!3_3h+6)BRuz%5`&aNky9nQ%-Xz{fJGHzt|Rb>31wPk@SY$uwq_2;`b$zTd>;-FpGae=1^odS0x82Ol@ zJ*f;G|MVH)e+asu3JRha;rZY{Mk+fp2cDc7>C63jdvjk>ri- z{~2a5pc*nl?t4!jllR*3sqs8`Rh<|PzMo?Ta`E~PabpbGhfJjGzMmB>I#14m>bo*; zc}+$`$MGe6Im%Z?*@Nin&zy2cPAK6&|69+p8jLbyJbT~sJzZM z-1TNTLRj}JbJO-LcPPQlSQy)%HvA8j>(K*UT5>OeQ#mvE2I5x*%>J|6co4?_0l1y0 zEFIeq94(ej1T$(^o5NY`28~BOj*5li2!(YGly^}I<+dfgaV`s>|*nyX<#f+r} z&VCk?tKEUn|GA&;EwHFqbp9=TuxueR2i>`GJgaNE+W%|>Bm&6xf3(NKY2y$6+Nf7- zu3E<6wqgDw6P2CDK;iivNH?AxxzkH|oFJNx(~J)WmFRmj8FO-7QgA>9;sj4ZJkR69 zoZn4;8q{~6vldRdyonX@|IZXbnNX~$P}NHa@20P`8%9d$(tIG6)9Jo~L7nACNS8jG z@jbWWd{_johD4%;*ML{Rq&bb1#>lb&I-7KtK){tR3=NTQ9m{KM>n(8g&?hn}sHy(; z*t<302eHBUCK*%Ft8;;-t8H*~@<{g?bEMV%b@H5sg<4vRD3N4u8xi+9e8d%+-rn*e zP*=YH-w_}Wf&V(LjK3YJDiTa$Yp!zAs}E$1{GMBJ`!Fh z6|%%LD7TKAdV!qBI4-n9iC=_4Pb2MHk90EZ5c3%uK8Fg7GWlIS_DG(V5y({J7YG<%m>gelk~Sl3pA){u_vvk-?VeBuP2WoPcw7G z|Ljw;B$BW2*2Yh1P7hi=YdqF8Y8)?Xy)U}iNw_U-g#l^bnht7m7u2OaAVu@QU)A;1 zG?2TNf$gjf%6mA>uy*%kgK^Yg+J~&0;jAV~Ug%I`KzbcNYZMl?C{xzE_F{%pNSjtCMOY4P`!S~V;L?nM9exL;+2rG=%TwncwEH!wl z#^E_4Pd*0A^WC=0paA@i9v~Taf#H#~z$Ntf5c~{B4GM}x{o}Ml<0p69Xq)ss4uI;= z5f6a3Zt&umdg4!R#j{4RLI@J(qqKFiNV{(R)!9ZF)U3%(Lh#85<2}_wpCV->Rs)+x zPtsS8F&N*6;IHw4m{uz8e)ZdcN-Q_y!3?y_$T=5=7R-IZ>7~;(bbaZ5w!2?$-^Ift~#OJKM;}Y<2O-Q(|;4|goV-YYm zEFS?~d(eyN>Lu`Ao^{1CKEOvg`2q2;E|Ak$0&o`aj<-j36!GXOy={?>!L4!P{o{%k zPA9wf)@BhjqJCF~K}m08Wl&5dO2luPXWsy&+jAcrI{`u< zsKB`4abmdTE&^U^Pic%VLRZc&?~;%gR)38;|`l7kyKphbSR*XF6^N} zZk2=SRdD{+`CE9=tUWTplOTR{)+sJ);*Jp#0Ke7)@%7ZCE$FBp$3VHS{n^mI`6YUn zqsQG9Qd}0{-cMt|= zM3-r?$#I|wdG9IUJ^xi7W!Ugap>b_Sg<3d|GfliA34(7bG23*@aP1G}KIcB&{Km&r zoA>Vmi^FU$NV{WK+m|M;f!Nt{+XcW_7yNrRWyz||+r#Gbm+mQdL6_141ICYXp?~+` zNWAT6narCe$0G~xM9!N)c)L{LQ+l>J?l_crPfpb5EP<;4tS2;PrH?<|e|~uk@xYJJ z1n*C~lUg+Uqb6CbgTV%zok|eK;0)-=PgGl8$XNO4vWC;r33F5~Xde<=W*31aR$j@6I#l!lVk0tm`^wvd>>y!p3QMUg$h`8my`FF>u^HX3c3s z7pUev@ytVrJCJmNp?Xi}?sUmIuq3F$_=X+2iC9n_zY5F&fO+W4Ln~-zi+=;IbYlxp z5tTxCkG|^a0Jkp}a}9oqs_^-`>M0^&%Z>c}3p;XgIIIGA@HPWh5xQRI!_%l!P?jB$ zIxqeX3AZj!3Rs`+r(gJl@oSiN!;@Q{UIm#%xz_vd`0qhHIJ3u?L+i34Vt(3v_F*u0 zB*r*hfcqFkF5w+c7~5+0C3lK zY^=+*cNaw_o#83DldlkF|!P!cgr?!z$OM6Ff$&|uS ze;7H$zubmNBJ2kkFExK<2Zf0(6e{*%2gvf}0IUMoQf^?BdIG*FTS%ByNcvukZ=XRe zytW6wVZz6%E4HQb(HHGWAYX!y05G7hGN7HFoHhq}pb}pD3Ak&CWgVTplKG0-8bcS}KXTi1`;o9Kgw zzzS(_lX?N>xz{ln(tgG<^WyL9`^FYe1rQLr?*nhff#Vf0;m^`IA2t$7-LdMoO(O)) zj>U1^J9-?lF8dcFd8WIiK(|{ze#@$612zg+0E#39tz4Zf64ot3dtbH;W63|9;63#8CzVjj-&S9To*Y%Og7hdqmjqVf})!jLvoW$FXs1^JMno)=c)@YuW)AD4n4#bupWCX+QHH@5^ZyL-d@S~jr%6g<01kZs*4k`igWbbP zfQ>6T7QzZDZfI)X@-?2c3X&-T%6CNk069h$aWs>xgkk*ACmIol*jK}1FFiXQZJ&oy zFxB5^2`9Z5z6Uk1YQ9x_|Mw(!k4Mzu*XB)OXu)?1FkPg@^sUUFou*=y*BJL)38LvF zv$L>4Bj!38hmjRimAz8G$7E2wt@qKWo)`gawp8v(^4U%ViW=xnYejh_Oi)jcpi^Xx z^Y>FT9KN5Z^Xy630m6@j7~#8y+I!QrM_^;Jp=M%(15gk+BuCL5yeLA|CP;4S@E8}H zRL3@)kP~!B_tP)b`kttzFASht?n6!71)te{i`48a9dKMf*Xsa(ua4xK`7Hwbh0Jq| zOExza(A`>#QkDC0b|qBeS%^&ntPmHc+j8TKm>1xMGBSo0vp$z1uSLlr7?iO`r`{R~;$L{f^*XjNN5-@(Zx9ok!WiMY7 z9&eM*lqz`JV3qgqHtpFoNP*mt5F>PKSd^R#M{HFs9=$>@XWmTb_k;Z!J1W`p;4yyg z-q+=6IwN4}kFj!c2`$HqS%Kz$jiAYUUhWE8lddB<^!sn)l2Mvz=;!N7>zZj`$z|Ra z>I@>v<```1n{mvKyJH(F(P2<7FUf_J_e618=HZI3`F&|3* zIQ`%gUG=Nt!R!V;0kig!x_(27=#WtG*{ZcoGDvs81ZT9%e2ILY#aDV^DThwX213ySKM$ex@I!fDCFQLBIAMcXAg#J`QH>WqyK@FwY;7Kk*iJ;vB2N?5a7=$7(X<`cg0x=~3x#DK z<1;fjnYK8D2^45%wXS=Z9i?M?QeF30Y_52rXR*DRQs#%4LC_1Z1?sUl1Ni@o6aMMa zTMg!tpnv5b-yMBhxl+fYb(8n+NN|?na_Ic*)zXIlhNk^@aOf;l*3bTK8VFkde0q9u zFc4TAD2pdfx;v$diOqwTwb}#>a;|G7tHE~}%v={!OXW~iNs)UfbQl;=P2uj8^9aS{ zLb|HdX~tB3*_wRSUTQ8r&($>bT3(E%Ysy=UYLbaxbbX zvh_PygxkFU0I@(^Njfc_xkjBvI#KY2W4!CW)a8|B!VPnc5&5IJ-r_~E$UycA<_~2N zS=;uYOYj1taZo;2ndl5I8=W=dlBbMF`y90`s?!zY=ZHkX>1NHt)(^9)DfZD(FF!IU z6rpoJWpJ!A--4PopCi=W4czEy#uV()4T54&@indS#Vi6|e65rIDTNBqf=5blm-(DD zGmVL>W`zp@nOY3Pha6 zf-<9=j!`^QkQq^OoXd0H98$lU{ltICv6fK!p281zmt&-TlPg~6_JB;6Yl1K(`Ch(- zTyn!09 zxd;|O#i>AohW$}5g&o0Nk2DfqKL&{j=_r1*e1EEP>(I-RSvFI<*7n-kR9#)U?VzSD zVxnAXf0-|D1+0raLZ5v2n&`#3)ZbU&9NFJ%IlWxk`xl9W7%-=9eR(~i1&gb$4Mo6v z73GU&X?_CFkzox?JzoqJ+Y@>ou4Yl+0zZ0=ic$>imHOGy8t*dqqd4u4y`ip4(9^q4s`ghVG z_T!r_L`G0#ghuXBU<;w4qe%=k?3EL!&0P|-Rg3dunm0OKYh4H!{W>;IMf=o+P^11i z))m@(3Yeb@O;Mwagy%pAAejgYtK7k!!B1r-`h?ekdT2_i4e*xL!DG`PYB86vXa$FY zcX#aaLxOt});!-aX&~RWa;a2N9|o1u6mZ9EybIMtx*iB;PKjK$SbG}-({xTU`4~iR z$5&T@ODykPHgvfGwIKKlvXP8H#ql7ybZ_ zHDg#V#0qt=ru|P~x#=J~EGXFDLbWsQgN3X6dj%P&fqqLB{NlICU7;a+2D;aKA=8n( z!CDbTQCKffJT(q7=vLg&9}A3r;WV-1ShDViDra~pxn41iB*HeDv>@Wbu6pg|^#TK< zQ2O1VS%OWyHkq0FYai{-dE?pbQK8e$S?V=Wu65nX@gyMc%lMogg-OaDt0ht$0N0aU z#!E@`_M;F-v_-aPdSDIQ)JWjS3UMjLnSZS*HS0YrXC8pQxhktq^!42;KX^n^L32eUM)4os)ZvQm~b3hV23c=&y{+@yod!Oh|(90=2 zCgn`+?%?^qjdQs9@4-me9hZbf7ub^5ieD{x@b8P)pw~#ud7$Q?w5GzX_~^jsWFa`_ zOopn!O)V4Ov?@LYR=uxQkASiy-sT@?-qQ#D(q`*@CKJ^C;LHqWKFroK;(b9F;>21&p2}-o~hkvmPqvGTK!J(P%i0*EolI?@JYIXzK%?D!zu7M%mMvG zswbF8E`mJxzP@r-!~{r}j{EUHFA9?Ne&oq-CW=lSWI84M&Ts zwz@vJ&GmY@UJz^t*R%?(@$9P|Zcnq13~YBF&6MA>pK9c;-x`SSC+Dx_1FR=hdAXwM zhL;@&9#{9kOQtmI4A>)=`|zSY3fuA2mI`8*)6Zg*!whxkk{LRHg|h}B@11NnV987@ zz3;nI#4LeW$g|lSTnU7x3~f7^_CXdRcVwB+!9arjB#k&@CR7nQF(Bz` z{#ppDvGP4&yVSQ3Cjs;wzP2a0%|g5|Sd_XX$0Nix!WiIn=0$0*-5&b*_3A2-J9!xZ z^VCh_%1IIOVal>AdDbDVogBEWjz|ta5Y>@;1Bb4O*8hRDP5vzGNv?Y7tTfaEL)?=` zSIIX#W@{rKvP$isVUbwcs%2!Cz(N91WMW_ zbrVflRl4pvPk7CIJT{zH->@%|*pmnV_hNOoi-A>ks!s|(( zI%6fb?fwZRnO(1OnBiifjsB29rDCO5pr2*jiOn7lo6V5{)ca{->Q`6i@4e5rPUdPB zXBrq-3r8QIExt4NX%)ejI|5zc$Uy58L>xlZ`UAiRv7Q>te+y>PW1a-v;R02nztYSb zM7(ZtJJ6dKRkgxXvfc2Q&hO$R&d5OEIL9cvf7WAB9X}v=TKELnS?1rLJK40nb^!+c z!{TqbZT;&w$NXfYCsm(kk1Mz8&<28MFQKuy)j#=23dy!g;L;ImH|UaryIZS`USbbM z63d5oo1vMMTVg_Pu%sp@DjSS#={s8Qet;JFoPVKtEf?AaZ2;qWLK<>8*~B*qu0ed7wRQ_(0tbLkdjE>gNFW)@NKyeodd z_$R0b%7WqZm*Yf&rvi+8Twv|+|4ysYZRiNoxG3TOQvMQH#%5s?gb*eCb68v4)L-QH zFjtuoK9}9Ks**f;-atsD=%K&ZykE91lBg)Q2%0?vDuR z2!!rvFnXMyT04)bQh|-(Hgg(~BpBOJ_s+?2KSr#CnfIkj zKIxaeG4^Vqf_#~6taz58Elydr@mJ0gx5CLlAD`+%5)UluruQ^G>DsfQcuVS64+SXR zXIjz0Pg^_6MFg7%u~pkTqwLM{@b!Lt(lV~1P{9bX+HU=7@1AyK2YFh>Yz)p+z2{rQ z%$F^%gZJgKR`c>63!5=ct+wCUL)CP-82E5kV{27#aByccRU^S^GkiEVN-u*h8zhUI4J6p%cGCMpk+z za8!j5O%o!H7kL*Bx5UPoDXx=m;D-^;By#E9J16jCSryeQ(vNKLa#NfJIu`%qN}FO5 zv?wZJdAHqgZTa`Vt9|hSy(&BF8@SBWDg9<_SpH1wi?#xlci7;ZylTAWj*nv+YmsMr za$@3*_ter^er-*x@;T*x2_`()9X&p0v$J7c=|1_ewi9^)J2vuo??bGX^gzJYuZvwh=*gX^e7 zp!}2NUOmNmrtf>F&XnFJ4Xxp%N&>dZ(w^#k`-iB;(P?=!~IjfrmIwNblwNc<$Tb%$rixe4iB(oxd_jkH}*uKTQ69_)WU* z@Tc=ecvx82A$M70L1o34Ccl@5501WB4P?VUCFCI`YBja4XB7zL0+`i&>(d^UzsJv zrHr_QQ7)A^>Hw6=IpAVD*(m1Cd-%Di;PWkVHu8sv#8q(nLTrXMe=Lj&gNm9u%QukC z<6<0FiGWV|r-MuZF1gXi9-UI}3E9)cTOXJqC(Sy>5ys=3+5NeAM^0cWAdG}w0M~A8 zyFtIoml^(Mk=~w8i)tPs{b_ang*T_>b&GgZ$q;)6((>UqX3z6q2g*6jHJgGF7PZ1g zH}vZZ$9F037yhQzL6q+;?a#q7g>NP^(IALtIemQ7bn)-8b!ax<_>{MUjwZFxv;1y2 zCYoNn;7@pJaomE!uZ}2g4 zg0QK=L?+x^t=-+%LGxAu60>;%?fTTQ9|ufyHa97Tw=XZH8DN6qMBF5e*krOI@YWbn zGX7>+xVZ}~)rwf@CR})ic&xa?6!u^)5Y!Tg1^b=dG?{ifV>F8lgY}PLH5BEq;t1?l zzBl&j?0@G58%je%`c{xu**rzJh?OYgIO`L2&)|>L28RAJSygHa#a!HlCQboWC>;z0 zX^ksvZWR>T#jG}te6-MC|TKQ*4U@!0%vlPM{f0eHuHMLbr+E>0m26s24EB*%G0 zdhy8qbRfUQZ-tXe`|v&b&c#J6m+Ly^*N>fw1-n;k4;f&VR$DRN*h&2bc_hw;UE6NV z5Rh4S-5w#|nsk?46I{N%?R<84oyVk>$QkkEtaoboqq8{pDxVZFmRKCTvYn_${q~1P z=Kbw;D@>jzwJ4zk+ikBExJ_+s_uE#Mk8Tsg7psDR-n>q1xvuWxYv`}T!&S;~FyR>w zQ@#-xZ~|6VRB#faGOEsU2jzAv1pG*=F3GS!xDd=j~>yW3FynHy&5-J=| zeo9IB7WkSrDj5?lcU?@nt|LY~+t03<{oQUs_j0^E8>O6T@;-%kBdL1__XaPUeVWr! zP)-ghoHuCFO;udy5#7Td?i76ohlrN?k5||K{n@ISC#G6}|7$N}#hqK@lzJv#T#|rx z4flp(of};ouMehVWi0Yv%Him7^;?N|fk$yNLJVPTORM zqFb#=qYH7J+pA2=!dec^%%2(=PYO64HvYKYPm>^k0CwEU9bw_X;vF?wX*YRZZ^L3= za}n{nu1kgUTDOg;a8gCn)j34geX6@ulwn6ays6R=;OokMFdgFH)RTfze1HG%-z}P4 zWm6PfT-*ww7xLpwmS$l`vIT{OM?;F)ONs@_LI9P3aU>x~Vll3>I5&FQy1K9dy({Ac zI)oLiNC`dRO$QWq;=$s~Rbj05YnyZTe28etg3jfxZ8Ls=Y1C9-KumOm_bi=EY{At6v=d6fXZ-!?l33#6b^6DJI$zne zKi@$vJ&pSfLqr;g9Jj-PdN@tSl>JO)PzbBi(^j^*Gluf+ygi)s5w9d3e&X5x`ZSA) zQZSFN(tavZG3vucMd82a=S(*%yKd8;WW(V*9{NZ)rPfV|h`vspZEe-w*l;TC(0}`> zaEZd_n;*j0W#gBQ+s{htK@yOJLYkVSqi;&ShCqrrv`Oz^T8-xp9;*<@{kIMcX@6Xl z8iHDT5a>GG&|G8=Uulmt^?S20!sBLoMh@IW`s4)sC0cZK%jsPrH8B z7x8dbJriU1@79xv6#Ex1B47~-gHf%0MyNcICdBVyXpe@JpkYd@mH&t{s*~iKEE2Ntv-O6kqA)C0?!9i4H0UY3;mUhc&sVe}m2i;pBRfH7Uk%N=w8jS4eAtlUw zOwmbVSMPmV_M2CAEAL^ZKQ5pV=oa@N;Q%{4KXoPZ@%1FT%faSNhn*S8L`^O)Hzjxy z+1Evgiw!J1{iZ7I_Up3?ejLkJGggLctHiaxU^hnB+PB@EyT@Tz9V>xMDLzWS3$tXP z7F>hJ>Gij7LP&ZCLj25tX@pYp6`7%tT33eL#RH@|gdVe_P_OYT3k0LCj9DrelqtZs za0D1`#S7}NA%*owI&sBD0wxp^Q0tj?phBjA#U*crg4;K^d>`zI*Log&nylo3(PSgR z!Vzil2M+e7MN;<^K_;d%(KqR$42sy^C%aO=e9+L){`Z;Xf_K~fS$99g=jOIPjFnWX zghIQWW!%uwwVfb0`y*!uXjUjNgpQ{xSqJ+wlQF9<@C0C!hEh+w9OyEG6Q05FpEHf> zDwQ*&B!RBQF8)68L4pm5MFw)?DYO!};3H6iAEG0mVPLB(dShcT_#4{uE_Y^|VqpHz z{S$uo->wuP_WA~TbAW^7fhJvlOTXl4>Pi~4O;9d{!(=>5uXx7exStSuc^Ia;mY8=w zSo-lnFGR^qz?qPc5K=HkZu=3j%|CxC-)#FWk=^$*f%C0J9XO)g!Ki7_$lgA{nod)i zaW?SB%xg(14!J6PiV+QEP?VmUIs~d_sLQCw>pg`G4RJF45U5VGLRJru76#uGRq%tAbizJ#f-STO9&H( zb|5ZgUwyb?w>EjiXA9nf5Yh!#V#b9^yPJaL3y+!Q<(rbS+fcpEZJ|JAf@p5OW5!oK z-j;r;uMegakgssUBS3->fSEC>LUO0}Q_O z+30Sg`J)c5QLE32biO8ShM82@+<@6BPw#mc!36lcTH9F?Y3cZSWEEhymN1mDOn&s) z&eBeoSnANXa*8K(#$*s3>Hm0NG51H80FDgqT9v+F=U=q9&{k zO%zlrP1CDiX(|ro^Cb{aHF0SL(kBL38My|Cg_zq>z`n4L-d9JK)K|K-iE2Oc~wAovCnkG~;WsQtT7RuxnP> zROEe6G5>l_?^)C})pf+uzJVOpLfNf!<$fzH+INq-*r*N@90qS=6fh`m!u?}BaxQN@ zU0abl$`nmH_PS4A{&11ZNLM)s4Opk`uvcQ~yOztsRZ-d$EG+h{uIu!5R7HNax$^GY zzgvIZqS35-;B)`f>(15sFPa}u%3Dl~yi9v$zb!37(UmBK%dtnw3=D#zTfJwX;ymId zF9=_4hoE60ZPvxNoWB*KJ0QFNguPj!`Q8x4#=~sfC?IDZR+#wI^^`$-qHKAoYqv}A zR(5$WJj>6hQde7Ko~>+W-V3zAhWd6*WPn>SGU7lHasQTL3?N%{lEmKFPX$AzPr^(Y z)@bw)`^d-WmB3>u_&}{kRb3gK1_LBPdI$HU>{qw%$Mtcu5o0^&Pi2UjlEH-c6$3r4 zec+br{so8QHhe<`*C8bmAr^UM2H8|)vq5`e7I{KUrf`&d81`U0`FY~(>u2m*#S}zh zk;@c_dhf!W`^s3tca!eu-RcF!w>R=KBx%?mF$rifxVgCtO>mi*-bgD9&~oVKVRO~# zwSj5Lap6R3=|?iX?yq0FUgwy1?TtH!*=L-*Al-2^{RnwaXUkg+6m2;`5&h}MBk?&5 zE#6#}Vcp&RS7Aot5~D`T(3yzz8(yhLKCpYU`pyo>ZIPYKY7Jozyysz?*FIRThHPTM(SF^Nr!?b}pk|yr zCF6QqM68m_oru8`v&!a}0#ZhsVfG-njk8_W1Z}J&!}|A4R9r${yk}&VPa;M-J9teR zF{GuN@`NFU?1q>>uu`aov1HK`e zoRNo1UIP0*9Kg4dNn(3=>`QF%-q6r+7ml)V+kw6?`*_KOP^MO@D?$ z47_$9`X}WliI*SAhbO|rm<6i3CE`dn)%fJ(+EP8BkK#6H!h(&f z?52v|J(U@WiTPTf;gX#iRU7wf*1i5su$h7Q$>SDw1_ehSk@rh7`qxVo*>!X&Gz%RZcKopZG>0 zXxWGI@S#vm?Ct_ruK7{BaA^ljqew08#k&RbfDGe3l}H}JYajic{% z6cnOmU^)vXqbI;th)9ezFo1z5qXWc|CE*xScP;8_=u!4Rty-Jn@{6Xoettt)q0 zT9rwYuaiqDha?hZXE3Dv5X)AZQOfU4KU|=fg|kmQH`wiG7Ya?QzA|VRJo(EK=e3Hf35859SOzPLVTnC;Y^H>m(h0jP~-ENMzclz_+ ze*xzL^|5c|tQZ0fXuAMQ#B9PLbWQK}7TAdkVrui;vY z8uj!!9((cB$DU^+GzMkk>*YpdRbXh$2J>i{^Q+#ZLJo)l*JspO{;(0HHteJQIqC`i zKTb6&Fu&mfJwV}C6on8W$$1_EI%=Pd-I&db7;+4GzT)g?Y?;2kAEfnm03nO7GE)zD zb!66mCF;JNx{24HsCIBh*a|QxM?fI~ZBL^}he7ILM7->Rnv0A^3Q4JTMeW)7WSAkI zULUO%h3@J3`B#Y~W~B$I;_kXzeYjfME8ytlFll_@;@WKXnW=%tgSKfHbWFwzd0S!WGpLKiNL!R7>K{Y`Tdfi{^<6Bsxq zj@_Gn!GM+v4cvki&;*T&G_pAT$u}ayj&Q@mR>z)-WXeT-D3=jwT@D`1y`B@N+ROQN zO5#p0>~uNnj^O_HKxV*NfSb^^VkJ(0RT{mGXf(G%G){9otgJVlRC*tN?cw*X^};8T z(4$<8>60@5_-@t?c61#B>qEVaD)*h4eeZ1Mb2vBNi5}1!P&8<>ZlS*kViI#<7QbQ^ zyPWOnAL@!mxbIv%TXC_K|NJ$v0>7ZJV7V}{z9OVTr~NqT`A~aZLF{VYuMq>fQ;(&> z)l6Q!B3jsx7>Vo$ptt$h=C{y(-HFYgChl*3+l4 z+L%mcma>Pz&Q12S$MRI1jyziHY-dbuW@&+bS1F9WoCQv{| zNL%(+Z1|!L%P(<^3Gx<#yF@Fl&gXyKH$f``!*1e*E&XME=_LWiVD~M{ zetE)M?dR5Dp7pqdDAqufxBBgyyd9pQ_%v_e8}yDbzj|`KH5Cfaih?AY<%Y}73_cu3 z_bO~gb)t`E{SKC_FTtP+|4r2wBC|Jvq7}}y#wv>x#1wRlh*l_OJCEuvu7n}M4L~>R z7&tR}kGPn^$CS$rFl3oL;HokSocAZV!K;0LxlyG&BCFctmGSpK>Kn_pfm2m4o4dm1 zQIN9In%-rIy2GD7l}IXK${&x*?=^b!mZ57ToK|}-QUmpyQ+%5Gc36!J*IEqaE#KWa zR2M)fe?oy-;JYOyphQ?pLB);lwS9OC0WuVp0q76zq*>AH&Vl{>U|1Wgi2E zzplZRY&oMy5{k50$7`eu#~kEWbU@TAz~w`unTH+bPWZ2ZwwgyP=TaHrqzF{(JYS`ogG4c8RuDo;aKMw=Fsq2U@ ziO1kI-e-XLfV;rvV3Q!%95cAK52FIzzO<>>nVuqHMc5iLC%G~EH z_e?w@>f|zs&*Ua(r}Ch!EbRzG?^Erz#YN7e?+-n1wneXf)|-%fy7Jjo+RjF#)je(4ZnWsC*7W;Ql33G^6EFSccKXWlZXKVluLV z(n3ycZ!e878JU8n-SeHMZvQV}>GFVx5|AW#{W`M?7cSxaY{G0qm3*_KOru5b&|mPJ zDmf*{WD8l;h)?JJU@{0%q#Qhsa(M~Ldl-IdMM8vX{ihT{OxZ^}$ z9p=@8MDbx#L8+~+iGhJZ(@k&;R?t~|;JC{KWik6NXYf>92fK$1{4|$W4^L9{60~k) zxI(iUaZ~cV=UDGqr}g2!xEa|VB%uP~ZO(>T5PG+W9_8nSs#o50M+l#8dI59>7MAUJ z>F2|bedxCAnZ?fw8$(dc*!;0ke4n{}k0B9qg`QBXMPZf5=-KDGlN9Fge$PQF*mL-> z)aU%pL=B(Vj)IaS;tKsXuP$vGHWjQ2eN*F%_zvn<2{0{Bt8;RgVXu4$SxD0F}I)dz)D4OU?SXvltYV&pKBuX@9AmoP7W_- zvE%Q1-_OX*fU!_hW0?H({4brrOjEYpS=FrXTe@SUY|iMvx=Fr)az8^#j`t1s>pYlE zHxCNbrE)To8{Zd%kVJd6%b200P5`nhWxzy{J_f#yLU)H4q3L!R|mborr zTxOHE)xUg+noLUjUx#+yfj>~a$d2LlYp@~?Tw_qJPfT?hGOZ|3Z z<2g4MOv%Q*io&1#k_)#CKjde~0L?4NY*LCQ@5NJogBc;{v3?Ipq3Mg)u@#}s?MQl{Ol_7Wh<_xF-Va_yWzCt#D* zJ2Zq}A4+zs#Qr?ZXLKw=xf%9?H!y{e-6rqZuuBYwmfEx+@Y7z>gfLurBWU-k`HA!I zXz}wjCsELQz3a8Jc5UzFwUpWui~i)G&f`&&GeKJmF{{VSaPdxs*0z)IL=Cni^J_&U zP~J4j&HTY4`wJQ=fZ{W!_K~IkmK6xs8OpOGydseX$3wOhO&^nTRr<83B6nxJg~guuGMhv<$CQqahVE8e^`rY$(apOb}MkB^FiI(}i(Q|SFkVc;;rS_ZWow@ZW zNz~n^_~h94!q*SVc_zacf-^1s=O!|vnw3qEdU+wNg6hT8+I|R2I_0mc^q0xVb^_ER-Wuq?=A{+@U&6|x_gz0$rnk=ge9ak?xCMUIzcr|m#hmf=Mjx#ooNUd1wy8YXT4uJ&GjXUY z!u4FtbcfG^f8wbR;|ZU1WKe$9E%pl~Z<{ziDuwi#y%HGqK{YZV?;m{1{gyXN&w{)h zz1zy;RXa$txXSAcK9v|DGc)tGL28=BsasW2SSLiVuTr4?6*5yL({i={vnEb)iIU-i zq(Ykfd(GpysUJoH4(k*yn+w=}fs6KP=%~k4zC~>8tq{w}R$0eT3%;$j{^v}m8S1az zqpqJreAecWVm`nA~pCOfYP=gzA+fQxWy3132LH554c2{(j83ygu`x_Fo&Hwe^A4h9_i6 z$3$bOgSK<+w{kz2jJhRFO!(X35D<9B#T8e?%fkPU$)uF1t{XTDH+rx`GJ8S6>V|9l zyV;B9p2-YhomAQRrYbO?9%%mOY`fk!w|)kZU^QXg#H%BbIi4r9iTR=jKt1Yq~A~8>jm@FLK)K6*|WyCYf7#9(SB@dn{Ssc-D1R&~925qx2S+ zXRN4CpWt?@bJ9?dx8Fa>zW3;`|HDa&FBp;vLzz3(5fc$)d>!vv}?{CQ%Jbs+^hxr2XnM{`iZK0;SeEB@OxylD6e=vNX zD}LB){N&9A1A}DvLud(a)V8p1-=yz$KHBF1c^6|PKR>?|P?x?F+Sk<9uEBH|a3#At z0%HnAZD;JGL-8$2JGQd4p0BP93p>$xhaA_jUxgDe!X-!xjfU=?#H!MzFt`*b4luxw}BFkM}> zp4`DW3*;XtV`;o={Lj%_YJPQ; zz2kZPG4Pdf+L&3shm8(EQ#C|t3Vh_z>Lu64fxB18Z87t}&FO?z0&nU$D@(0+mKNVw zSH0ct@{YK2nKUb4)2Q!NzA)|2_f^kV^<(5@?Ew)5kGV)ff~{e%5|NXuxa=YyjlRKq z%{KoF5iCq~`t)agQQfs=3W;jnveoLkHO8o`k-V}IZ&^64QVU>&F!f%74!70HT@lZf z)LzHaozSDNzcgnc;Ji)&{hKe^_u}K?fA$w6jx$V9GVey+#5(=u=VNFIot_T+W%g5d zX1UV!nYO0qwhlC44VUc5IDSm%`)-ppG9Zt<&=~dQ(L$<<3N9Y=P3apJBeKR?{&*|4 zXWHaEJ)ds$r4{l;U&BI2{*v@|=Xh0lX(7ylWeOjxug{9tl5uht?Jz4vo<1nrC(l%= zx?F((96uy28?6DfQO?)o&@Oe+;96#Uh>s!!un5TnqC-nY>g|mjJ%LN{Z^lwj5DW>m zexHb^`SSz?8cHG)6Gh?Lo6l5Cqdi@v{Uo@Tw1i#3wZz8Y%MV}I=LQxQ*?D}(@i@

^^ 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 literal 0 HcmV?d00001 diff --git a/design/assets/checkpoint-fig2-granularity.png b/design/assets/checkpoint-fig2-granularity.png new file mode 100644 index 0000000000000000000000000000000000000000..988f6eaf84afe110a80acbddd7eee6020a834e3c GIT binary patch literal 98387 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 literal 0 HcmV?d00001 diff --git a/design/assets/checkpoint-fig3-enforcement-en.png b/design/assets/checkpoint-fig3-enforcement-en.png new file mode 100644 index 0000000000000000000000000000000000000000..ab8dc51ef3b92027b1a22e6b0b6caadf9abe8d29 GIT binary patch literal 74192 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 literal 0 HcmV?d00001 diff --git a/design/assets/checkpoint-fig3-enforcement.png b/design/assets/checkpoint-fig3-enforcement.png new file mode 100644 index 0000000000000000000000000000000000000000..fb081248492953cf3760d34839887c00c27d6d2a GIT binary patch literal 72021 zcmcG$hdW#U8#k_eXiIfcwY55oqE)J57oj#WYPZ$ctB6>kv}o0+)~FF%h}qg(b(u99 z5qs2##E2clc;5Z~uHW+?Jh?8HNFpcaocFo!*ZmrKYoMpW%FM+~M@Pr{NK?&-j_yAX zIy(A0r6w5p+{@d^!~LGPthm%| z0Y@JnPj85Xgxmjpfw+g4g9P9ENk{M|r#&^zz3J$#r2YFj@d)uMf$jty-6J&>W54v} z3C46|Jgt2_(u#|ebkXU$&h7uM-@eUe@P&oJlI_Ma-KDl)moMEtf1ZIwE#6(NL8ako za5-gU%6hf8w`X;E^>bwp@$k4W4uSmJi{wceU*V!_wG`~y}3(BRLtOguDgHVy=u^{$49U(eXp4rDz!|`rszX& z+cXWg9PPVURyi8h$n&4?J2gpn%4xF6e`k-FX*t=Ht$#_>tnA;``iIJ_lV^y=v)dKj z;+0RoEBfR{#7A>&ms$sqZiv6RRC+5j=v%DPQJoJ3`HPcxWz?k{>~Wt0HQm~8CfxTY zd-9lxnRSX6E>M;_u2=SD$^ZE9-1z-uG0UOD+DT%K+nD5_Cpena370%VmjC8&nNn{e zi`(okU6SIh_iwhEEn9<|SH|n_O?c}D491%%OE+gZyJu0eHnjuHc{#I7&LO^Dl&0A& z^rQ@xn8Rp`xpCJfDeoj&KJToIjgaxh_YTG>Ewr*m_w=9*Vc}MAE;3oji*-pVub9k9 zfAUMgZ!BN&tTlm~F=mTrpzd&IY}`+;55bUcbp_;0Wha@A_A(jksr!e(50cnd7N&VrV2 z;Vg}r$>P?j8}j5gg(uANwb(^%TbgLrDU$FKi|QiNLW8HYL+X^(EG6B$_X~=)0~Tuj z#O52Wv$O5D;)YF1EpFgJ#y^x)9-Ifu%j}zgp4V&^U|e z{BUEnp-7+`T#w^l`~Ku>yORFE?}r@8CqF8heMDGY_-$FYRDCqoXnM5v{hV(*Z*62y z=|??Crvj8F$~Dv5)_Sn|XbOSXmzyAjIJWh?VagoVD$K2*}t{iIvM*en|1SOyJa>%qPF7;pC*R`GlT zS^T_n&R5V=NYIZ#AdjArxh~{8B7phw?JI~ijOWa?vJ-`8+fmf*3Tc<2QErc>8G4bn zrn9<6M!zzSCj%)jcyo>pX^k&Gau{&lefC;6eK%6tV{+UE)C+2GlioY7cCG;!13ot* zm+QPl(^`(TXB*=xvg`AJZm-g@e6eM1hQj`$9oEJ+jzlOguXdmKwZF6SaU(G1qVLks zZ)r(EM)wa?{h9Z)tv;pWsuK)M!}H0u8RX!}*7aVNE&pSCvs^PY4?-vQ!QWHwPTsTE zsU67G7`vb`_#$TgiFJcdH8_qVpknRvjI^lbP)2VgTRH!H$5HwBr*@s5+GlX>j)3y< z;^xUS9lZN>jvd?|D(c49C{j=_MGXdOkz9ma_#UDqCOhh@bgueur{ask&z-tXNq)KX#BHQvq$^3x?kzO*4cOP7UInVW+`$?H zjXWEt~H%>-g$dy2z) zaD;J>?N0yQeAipNW*keu58AJds#2P%WwgNVBr)9c za^d#{eHw0p9&17h0s1Gs1)xM#VZ&=HS;OGilUw@s<+u29*SB!`jkA^qnW4$7h`(>e zaPrU)-1O+DyLNy63$I31I`nm}Ed{ohFg37Qu`IxINr$8c<@YD+IYGO}(z7;X6~)iz zKN8io6ujzuQ1~T>r810(wzHy(7}(;Up~2yc$P+Owc@9v3&R#S9b?O8%b9-fsu(9y_qomgZ?}ali13BRn zzGEvxx$U~E&Hg8_U=4P}JQK~DQ>f)~aqbCc)*?}~63^kyxg?)|{+*#P7JRT?U>xJm z7+pniZ?1M|piPazN10;Q{By~b_P;9{5K9)MKb^Fz=qm=}>lL+wb-m(U_6E9nv$1lD z?qM>=2bKL9{tKK0EHiu4z@Qut4@4g;$o-c5OxQ1%GV)ty>ch&|ohM&$;-0x@KSTZ= zo&DH*qiekG_=q+>_`AGiAA*;K5pikm4V{ch_`N1_QL&NkQC4AoyzaNwc5+H(Kk zz|M%n#QLI=zO~PQy6NX9Cj!ALh6NMhCe?0ZLja?=ykcrjAbZ%zN#3*ZpHFzS_aT4MRI6>rl*{M`B|lsd-pO4p@b8be!mTg{W!6g3@}@59x@PLd+q(+Fr`U?_ z6+H9fn%|5Y^>>&YqTX{G z^&y-$e4pcl!>rl@$QUx-IvMhgIv$YGi z#WGAxB7KroCt6tFN({Z2K9cjShHHFHKOA;nD~?aW^;dKX7IRhfai#xe=%{FF?*?cG zqhCWA0J4h^e^dS>h8_vY*qUmHwL)!6yKB8wYMdQ}S&lSQcf2yuitPrk&WVtVX7z}| z5+AP*Pd3wxFlDjR(IJfcrYMDkT6_P-3!DD} zJ((27U*5c9(cv=EdMxgRYs{&?=}-7Y&v;f3*~8-cx~s6aYg@ZLKbB!-6Rh;R^VC$u z)xHc?OT%GSEMc9OTMW1^cz1GI{e|DM-4_;uaGGTIyFeQ!)4iJGf?9n zb2>)E#l4CvApo7`(bCzKT>b>Lzx8MYDs6F?q8J=oGZoKa4r-xy_yZPa5@?{jbp!B-{!{2U{5jjCWv_fhsX zcXUsv$A~!Ox14&WO*(=g(*wv@5)=BK7N;MCZ>*N`9utw6*;~v!Ddf_}f6Bm(3+>W$ zCc=QNY3JnUN-f-r*VDSs!Eo2i($jjh&y8h1_xcS9#c_fz$7r{~ALaplz9&FaEl zG^Q!QcU4FHN2ziQlTMYvOAIo++iSDz(G2;&UxJK(xAvUw{0KQT@{Ol=1YkVI?`Q+3 zoQSDHgE^rLmHRa#Hh$Vw4St7Yh5UB96Bmgn5q(lnlRTRIrNQ)O$dp$5Tz%-Px6`aI z*@{?Z%R>aToOY_*Cm{0;o$+>G`-Sp?c(V+3!-;8~ktuL1jj<;}i3DDo5 zjx9qSOlaR`W0FMPTqs(frJT7==+|t0kzqLM&N$Yxx8P!?s+sH3rEJtLJ+5^rxGQ#l z`W4j;3=rapHmr?yaSB83_47lCzVV|4bI9!L!VV!??)%B z=SN3O4?4jmg#|EnJ{|9qtxNN+szwaYb$+yd56v8Sdn@C)%($nj)>oqtnAoXSGh5Wp z33tK$n$C_~1j^DuBfzx7{_%=JX3EElBPbPRwJEIpLGq18K?hNO;TVz07 ziaPW9luwx8i|v^x+g&sM*e=l8TZ962|Eg z^Yj)OFRCqg#D~6Ab*?Mnkqtte2X6PqIo*$K3)XxV6>@;%PEaVcdra(JW|rK;zA1@a z(nRzOVsS6Bl~tf}i0s%4s9TBVd7D202)1tDi@w+E1N-R{{HOXM=go5pTLwk#&QncC z`%7i_-^NPtehes)G)xdP2mSNC3YM4=I8^GGcjnd#44vuIB&>ZmwKvCx0&0%ecCpVy z|4~29pp+Z`$q+4tnyUJcjUh$k3OhqMnzsH-CnTO$59JOK5O~0ST>;YWlBy-deA|h? z_`Y5!eJ2A?u*i>cfp@*c(d0Hx4AzC0w~#y6&X8aexAP_HnbO+VSHAnCf`Zh5IV9-9 z4Z>}y^xWht-6i@;hf?s->dTz{NYdO#{j-6`TP@m+NXiQa1&DjXPwveoh_6B8c*+(oYD*ph|qL*9O(lmIu{unn*^Y`jd25-;V+A>%H*O zP$`nvCFHvX>o?EtUm1uYbMbc?xgl<&RW${feAAG;k>=61Z{dv-H6;2nliLaG*;x_H zoT8$ttY(#SZl)&<(b^K5Q2NkA_tUEifxZP6oR5ls6t3I02BQem7n&oWTzE}GJ9Tq~ zqpUYvmh2+NI@@vx&BGWMX^63?I^)CS+ve#Tfrzt%pH>HEp^bp(f-#mf_2f)nP(--n z{UD!=jkf_jLb7_sv=m(_ha`)r6$*TWNISm|&xuG(`p7G_s7`1zBbuMMvf_epH)eU1 zJ7$w>D~KT9A>T6=yN>5?*ys|Spcucdu>Z{^+*dklz(MIS=t*Er; z63m%OOXP{CcbXZ)c7slNl3K0b4Y4Sz8vCk2wsxnZsogaL(DM_!0(nL@LOv>l1zOTTyP2 zW>RKlUb|HRjp>u$yltj^Gd0WW4r4*$qYG~#_aI5mgM}$ZQ)*RbDtcMOEToW`rD5j# zv5P_ZQ%iwmo9{7B`>&SF^L0-0%9s=x>rq>n=^ghqtF}@^?&N;0YT1O?`YiTOq9kks zS4=qQOk+4?mp^}S>Rq>Puv(7Qjp;9ri0XX@vAupWj{SgVmOh$qkHxqAsaKoo zv<==HDGewT_yfRsnwOq#pX#}7WSgQGp_F7Wxv{=K=}ayO!xU-w9IVGPymTRyy)eHs zLk~al4yxKVU|TzPquHcjwP`ap;l%4NcCFsqLDx!OUykBC2I!-q?IfcW;^;ghBcjwm zKuzU5V~??tMti&luLt_1&*r&RA)y@a#{)U)MoEvTrpbdld*%Ls`|)n8o+FISF#Xry zyYh_Tq{x$udiES2Uf2xW`kl`KSz+&F!@*ceVh^`&=1{!}kpX?22rqJqw}@6DKs*xb zN`m;qu&fw?R;KK#Q!h<}{|w4VFZsAG@vyP78VeT|wEc%)+6a^3O6ZUJ$ebDs=t*pt zkTY>{yxi{L_J?Z&+a@fkuP<3OcIctOuLCREw9>MBIKK2RYw~m z#0TG;mM*Y~aS#&?oInsp#A?S~TMu^vyCf%_UEw}b=H=P3IesITEH0Xub2@JYXvfsJ zARETq@$?1`Hrr1QwBI|=^RJidWie$#CbFaWqaZvsvpTHTrBl_)QzUG+V2M&2O)q9c z0Ev2TQi#s8Tg>9o_4hM>Bjbf0V;_x^&hYiUAe;L)sBP%wl3?(v2_5F4$L3wWTQ444yX= zq;!dvd#0G%oc|%0G`MRrt;+eJPmyM&r9nQT)i$!*N*Q5$3z7m%pY&S#>zF$P=rZ!N z^>;ab(Tu4Q>Bz~7*vVEB)MT~P?KHRr3wPR{IBmmc(%vljk}r7oX(JdIVMoEV6=Yq{ zb$E^wzs9BL{wfB9kyJo&e%PUkAY;v7c0cm5%59ISBIVEYGF-w#noX^<1(d9?8uH`4 zS28xr+HI#%)WLXn+=BA`#NdLgD9kHI_FBMu;40D}{ahy0%9{^gN!000tP--F492j{N48OSv zt*%bc<&1UC5h_d>Dd9$BZA}WMN68p<(XXd7-%uRX(1KCO6zCj`B%PV{AS?& zjv+#-k5Wv6A;(J#1+J>-a|&JePPBh>66X36qP%{=g4!hV zxWu~;C=c`W4{dH5NF|{0St<-N>V|~p_&VGUV{M4kv|vi-YX1>o`u&c6xeYb{`H17O z%gaRoAAJd`ZW^X7=h^@d2A#TpKn{}`jp+F=tLhkG<3i$?XJ2owammxUO*GdJFN6SX)pU}V0rKdap4U8-23$5ynp3HT` zhi>-D$*h5b$po;VEVWVPmCYF||9SRMz&2jR+u5MDO{i2c%Nn(y)rm&rNsMk9IT%n^ z5rI1P?uRt0wtp`mTK)qxEiu>~htUPH!a^u`{N&E^$faRG4?m$yHiv>4pV7WIo%557 zhs~3tgQ{A?y^28*j#FQ6t~Ba<317qbq(3DWRddY4^i}dI0&oltFDhK4RJ9YXLm` z=kfx#0o2j1-9v#!M_aONrr+PU#;pWdW#aimAm{Xk0)a{Wg$-Oi2uEU?(CMfIe{Birz zL*=%_N&SyEV3Bkuza+cQY&SOdbPixP2$lV_c0 zl{f9kTOGQ1x4~3HmB59%*&H~%7$Gh)~=ghaSV38W_U=>3s zfzkn6g~kNU!<8XR4+J6m?WJ3okyT+Brj7=fzIJTQ&qnBOpDf_7&PX}-e?@bIm7lX~ zk3~>zYh0Zos42+b{iU|8{RCF@*fteRdAGW!A~gxWS;QYG{eF>Z$dB5JZ-hEc1y+R& z`pT0FZ>~OA+sQ23D?ks4tN~r8u|_rW?8*e?-nT}Z-Euk3iS~iu1VrLJMI z7cSk@@jNai@pLKoCtTKr8?3jq^9A%* z+Qr;B=9@_F)xn}uT}=ayMIl9@GJmior+;&B#5e}x)TQWs05b-65@eDR{iiB@k$GyqHa5LhXwgI3(Vt*8c{w;@PjWYj@d9URa+9u|n z|3<{5BENXM`I=Z9vUie@>&MCyR~NS?0r9{bg|;s%i7!ZAyW?F7woDitr}t(9UnI_1%{-eCDqMy|qAicyDnl9$qT9rwo1lUIBl z6QP5K;A4s+yDs-9n82N!y5cbwj)M@{6cb@ zTKA^zrKrT|0E-dbYUPB9T;Z=ke=A5EDnS+X=)<2ljjn=GauqYAmgD4+`faw-(eD$O$*lfRUV~eCXT+ z&itMVK+AW*y&@VGoGtVNq|~)>vxDMBOgXK0wnBT-7VpWQl`BbbVF}N1D*oHo^Lv?k z)pT%Z4@lw@+g7k3lUo`;>rA?JIPcpG73@r%|J!s9+~xL|@xvv%H+|74nv$@r>G|DkMs~MfD0G>> zg0MH)HD&dcE|YHlnYMq?{l;03(RWyV6$$A=y~PejdtM*cPANIAfQV|eto5S zCIP&n6op+UX3M%!l}Vq->9bIKCs_-p{;c31G=I_tvojM65>ki%*|ajzs9!L8pQi(K`B(f60cOHs1WuQk27;lCV{i3r%IT;Dmh^L z8voLMpy%4JT>O~g{-Pj_RKRcTsV%y`$4F2!>NkTiOEc=JPnu{p=%&?(YbK7QLYaNG zmi>$6l3loLJ!e*rlVdvHduFDY1ulU8fDBED{v+KM=o&ECMxcu(e=BrP!?62{Uh8^w zZs!H>Mz;iIm2BxBy7YuOmehc?5aTLb<8Yyd{YvFpY1U_#GDS5QR8_#GBEN*`r+&kJ z{S8k>Ad6KVttlF0nQzxfC0gK+QuDHYUuQn*E9zE%NT63<8RNg=w@#qoWL2gdbboG! z8QEls+{0m{Or*wVo4noDnA1}B6n{j;@-kLSeJ#Mo6{Y1iAdaRB%VtK0K(+2gA;s(G zjAY=3+o-DRfwk_*=YO1#eEE+AV^(z3I}YctdGhg={tLl|Y-nLqY@VsC0r3~N8+G9k zWxQT+V!$$H$BDCZniw(@PPA6-Q{u4X@Qd*xU3L!Vj^oNmtu1Godvih1N<}{ggPkpa zzNR-4VZ$aSKU{~T9to&yE{Iu8By4f@JAV>dy3vST#D;Mf9ZyT&-{zA(c?8;u7T#&c zH1)w_Hi3^@5S6y-69+VlpU)CW?=zx(H4pkq2ZqV`?;I`(2BN=rlPNLM?86CB?6_6I zR;|Pjt+%w3Z%$1~G$w19*}n->9PsV0o-)zU1hY?sk%lH(6;Ef_u7f=xs zb{9Pfx;gmd^P7INyp7qtCxeTMFGo4+3Nz?RfAi5(qAh!mcQnU}SqDC3m&7Ag5v9gY zXk#84^%ggZ_~#iDtI6qP1YBw@Kd14W;%-(qBnTi7Yk`O1?+SEMo~7M)^JN@m?=UMB zp+G9V=8F=tyyCBg!^FDFmY{_t>g8*#vHZtmSNc zPagjA3c%O;9UxIh_Ix#Lw+>Y+P0v5um`TncQI2%P;EQS(buX;`GiRV`S6gU&W>pG* zh=txA?}4I;bs;W#+isdDnb`D*fP6{YFn5`Fh@B_YnGnHw7r_%%#=Bm-6x+* z6!3U?(|C=gBL+BeG_wtDJo3!ct-l*)o5w6V=JSai7Yl?zxNHjSh`k*#8q#^yK?P<$ z!&6R!9nsK(gKm14ANsKRK~A$1Bc4)!%LkS#;2*he_0R-{Tge)N@NYE<#6V&F8y1#~Qrmql~qgPU5EK*S_DYDM6VknIRgaiqZz+ z4Lp8|GP<+9hkdKfYqFt%N~>3}Z>yJ>Q!;yMoJASeNqVq5B?_38Bc8Ae2c>%w;}< zou^PCp{!>cqsQi*rrKvE)0anY@bMCkNK;=D5-F-B!Y&AWk%@R=5HLwK(iLFBg=fu2 z22<%Xc&4o=6(w7|gE?nXi}wFH#DLy!uNuL#P|@R2tW&hB$*v&XORFeVIwzik zF}$iMEEB^H-u}=%We9BH>YR^=bXR8Js(Isd7z=D6BhY)I`oB`bsg8~+tfTO(GDTZd zZgC+w9xGR$SorTNBgaczvgKXGCrl3SZixsVX+;t-(Bbut!vL8Th;C zQ_218k&1qbw2SXB$vbDDdyZCc)cYfBCo`I3Q*8j!=jiMcUHIq zejq0rs-N1zR5CS<1s}2f*D2^v*1p+f$aAT+yu)%=Mn+U3>4Nx}0q>>Kyc}ZR zvJV4ePjy2QS;)Wr85H`ZJK;3HYwnpkO)>D=~X$NqZ^ha+B8&5yprk6FLPW zI%6s8+1Ho!JZ112PI$6$?Vl9W73Rcn4jovT6hfaf#iCP__;nil*?XY}0kdwRL`ZsX z9l#3ov^({-$sb=AvtzP$s+@4vJMY{ru)2uhDoJ!^5)g;onf*v|oZ?sJ25O39W0)Kg z*b7L3qyeM_4wLZ3iCLJIR=X5q^u%d(HR{723Wi{%0x#R=6{2{pYD1{o8e1m!pC2us6 zxiBIXih<}r+}WjmFFF)bQYB~34mmLVu;{IRd}2<^!uK(DQ>3@}?0103>gZGNInD$XK?ozeaP<|@UU zr^82iNYPDWMsz!(VxQPtRS}bG4BCrINSka2hOSR!NA={tC8c~Sv!N1~ZzJSq-%cWOwHITZK=oicg+#jHF~tWh1d z`4FvvII;^bVq9@DVN$vi{?e*ihB&>i;BWmI|IV#&FAcsqmTCitpV!V>WC& zC6T`uncJHOwf;Wf4-I3v)-HpkRfq5FO#}-E>UYX_3106k&GAEfLY5-SVd~v%-Ue5Ja>fuPvngKY|7BZhna( zJTyF~cLof!NY1P_^Q;-6$D6>f(10~%t_T+?EGOzp2$U?v+rTojlVh;1jOb*x_qe|O z$)!}xW)_5Oaa&n^@=%8Hq0iYt6mVKDRVZy=v9{Z`njADWWcuV>=h*5v8g==<4##N= zTx{agVrn^Q&;gCGZ#ZFw_=E^zBB(9(XTM_Uh-}zPK6MU5EeM;>@l`Gi&4}(O>$y^m zJfP{Hh}^=E?2|!rk`zf5?#eD?E%1I_QM|P$^wD4{5U$*pCZn{h%gnCjb>c4L8C5sJ zjbtD1SK*Tmh0)|>TfPro{JOlYz_e-vk1ft1d}-&qR55XC4hl@U&p-Wng1>6&_MbN0TCP_G!No%ysFP#e)?cMjT~lBC z7q@_hcku=Lr8tbv4Fc&=V0VY}CFd;vHopAtqPhpb1lcHisFfLKM$Ymd;}y?}Pa#PY zYy#0PY*@$Vo_2=q@0_{)VbeRS3U`QBG4(AdTK(BBbq?!gh-k2u3ZYA2w4FiDA zQ?03z-cr^2tj>%s_|P*gAq9FwOwsHGos^r9lLvrXQWvQ{Lro>IXgYW29%_Ngxn_nZtU#9XQ)TLd6nprT3r7R5p zktR=taE!L|hs>E~v0$cuO@-)tQM+9iK9ng0tX@@RY9u)f^auAx$xV{qFdtL}RhKS~NjiV{SZ;!C9QS4B7|l1I zPu^v_Jy2xdrD$~5c4gC3C6M!;eHAUxGolnzjMeliv@DFity?glZ1C>#Z|f7s;?@my zS;VksAH3mZZq4~bz?y=mU>~LBD=jwjq1?GO2 zc}O+>!0j%tOV^;$f_YsUvenb47l+&dGAO2tyiG7SvQ&LXxAYOD(hKB zoizIQSINdj_e$RtgXaf?i~%WTZRfi&*J^F>YwCqzR|y1L#5f)E7(0{wvg_~9ch(Wn z&?&ruiCg$wh*|xBDx$8L^ce8^#jN~_<1xWe81}+NG*=?26QF=Y8iTN4aB!pVitmV; z7OS46l}E^rUn^~Sa3lPib;xN_>G?#%BKEsazxh#9$o_!;^E2}_#s&HCIF>c;)H~mH zoyur{l|vXsYzpK35RVh?=HlH!TO>gaMv)eH9l@QpCSfSh%Apfvyvuk;mC?vN;OKqO zDhM_``7ilnWxy9Qnxp>C4H#?6o?=UcLYP9Gn>0Pw+Q4Pr>hDb!c;{HnF8vbtiPEis zySVI95p{h6X=hFB4|H7FU9pAAH}~)JN66E_Ln@yJK&ukBI||4n7hb_j<$gax2+bF1H{^Nn>*+5 z&Wc>!*K|`C*kxs}`^KjUpQ$^Qne?aj`K*K1)mK}%!p&B0qiVX;4BFO?4sn@(FGxsD z==-S~D-OKKp6+Bfvr5^JVRSdPRpX2V?&B9QwW@P$B?DXBXoin0pIa7Jxmi)= zRUeW}`*Fy%nlbl2&uM5o7TSMr32=kHuxVh4w?j_$OFeGB#mKOQ!c%RF`xoVx`o0d< zYlq6AGzL3O`wh@bZ!aDD5SUR@CPMfSSEnu86MyAa>n8AN1KxwBmVF!Z-Al2L80mZt z&hlKTXG+bKRIMr%kt%!gy~MoID$-!;(36np*>uK4!L(O?Q%B}EOyxqC`gueqB>n&o_;d6|;Y zJ{ht1>y{apUX{lLPwq{GWP;lNAyafqT2qSEU`3+zG4_lYql$4i7D?$Zwr0Y)IGxuZ zy5qTf^dOkT6|76qaXmhEfVP8KT$0PBmVK=n{m8}EPf0xV0}lU*7u?uim)>)qZ#r@Q zgOax?2OQvhztgL0OiSZ?*JKK85Ke=dTNooJV=eR*_PtF9LeSp)n*vKjs`Mi0h)JFC zPsFQa25OS|Z_PVDSkiZ$20i|HL~4M3M-D1-tiX_khcAe~&zYVDlB&x;%LdDRtQ}S$ zGL~vuYB3^9^zjn>)0X?;WR1*0!a0fuw~T$ssX`6j>tg8Q03qNzdewd1kf_F(l*pcw z%F$LF?Nntl1yfjfb#4;LO1!BX>os!O;-P8Ei=0`(Pm^h}f{Z7W{^dVlzLC9GolA;O zc@+wan9tyUjW)_0_L^6*HC@)-T*CyY{Q(`*xQ*LTz=~#V9uBNc?<^H9iT4qORmUcp z3Zu~%&~e5B#wu@;#Pog4uVnXXh)5NACn|cN-&F_IOA6$vMn}U((~R{o&Ka^>0lAg{R@{7B6yqhHS5Hu`XuY zGB>*eG6i1c#R8Qr+Fj4^n|>*8nP2X+=r09fNniI5*k3c7RH_n6rk9dhv>^q%E;rP< zeAZ!1n!te?P&RFc&z*%49>bq3T6p%#67&9h-{ewW@#@;Z3sS*tE$dr3=3!&qMg!GS z&A0D*7zm9U%|I|=!j2f_q%!Y-kW{@gJLAwL$Hf5jfyb_Q9aSd8)ZV`JoK2gpt^_m2 zyM$gP)jscQSy>#A6T5+x#Zq%ck?}eZX8k(6qk3sGru#Z^Ocw@Q46M~WNIn!ue&6G^ zdmc|u!abGNls%BK>!xCOgqn7|i>!AwyEkkmP#}hm?qCX#v6t&Wuz=p9We++HQYy?m zFVt4P0;@0mKOWzp-YZ{uss)8Z#r{*fz;(spI8@>^4f?mv-XVzE&`&hlUj8KS?^Q%w z+5{5y8(k`ac;9~m^8pns;Xat_f|NbW*Wi|&= zG5)pzW#HrA@6S>}Q7e&~6b_{onL`vHh(kvz9SX)_E40;W_cr{!LW03Fcj8B`SO@Nd`vGG_PuMhRhb$QiG8~CS* zWw{WB3qqqk+n>K;VjXV@0b1Bo>fXIR`PrSs zGLMm&ac?*9Pr*Q78Tx zcPds;46>m;{}^)m*QOd5VrrbZUQ1d^mTC+>+`kXkBAXaU z?dJn|W&1SW1de7D`pj_XHMF5hjBB1%hCCY+A)NaVC$ts7^%hU-(L~Y1_Z<_&?`;dc zRGURj<)Dp{F4#byT;JDoQIr2eRvyvi4C}JTZKZ1x6D&QN@e@tar_tJ#vrRdMUid+S z6XOOxlShC?E`9b>D@7!V$0Y?DlY6H6oT6s$O|3Xgi1U7IFD(_C@8X#U5t+PO67t>p zTyC~Yf#f;WQnr)}U0X1wLd=j;S7k^wL?YLaGra5Z>HNoE{?-65Qn!eQMe&YyKxT^M z{bm=$nza0b+@aikXi@h9{O4e4;PYXVnTw&AE$2+?vcF|V>r$neoJ5`VV&C|H@r=Rt zuFn|-;4=e3B!U$2HVWF6!J=s92&utT(JKG=^eF7j!QAs@1!TH&PwDA-{P%J6(+1wLh% z3K{}TMcgM!%)QOz)EHlyqo_SWNB2eLU%qngjb<@&ra$-D zHi#P)TRPi>8)`)Fe$!3t--?9k$^K+77^-?)A%B8S^PLmyf5A+;7tyD&|Np&d*7q+H zJgH#n83P=>lc;H)HT=Ybgxl5)gO_iA;@_bFX}&U&kL~`Z({F zdgp1FH3FabFAwRkSATH?AX=;VZBP52Pcx>Hoo7L6qXvwo&yEi^^=sraF=q~eg2`Do zb&3WgVp!D3LLP^0&;XdTuu`pNU!Im4uth2Imu?VCcatw1?r&GtPI9F#$a#Z80{quU zF6;Ind-=auXQAO#MDxNVf`Gp7vzTTcfKIvRU~$`#2J~Q8K*~IBp!^f8y+kAC|M`9z zxgMpYC}&{7I~`PL)xn515USDe;(690G;)v$n5&I7A!p z*cD)^XnfZ0Jm?~8BgUY$ZufjJOGTpLhBr76CFU(pSJ^#_(+V$yjKC*U#aRN|d6C0l zR;vg(I?bDI{^ePHGqBD1_R6BKxyPAW1um7=TA|L0&8D$iKoDDt14H@Rgxd=G*I*}0 zbkl}>cSeZA-hB!j72X6aPSzJ##mA%xRv+gHG>|MCuiyM+7hAoYWl|&FwD!;iBtU=A z6gGR5i>(`LlL}qpThAYZ#4reh9+y1*4&C|5@x9b>AV-Cm z@{)yl5p!4(NEMd@Suv1LbbcE56GWuOL8?k*Zp!4l!|vLQ`;@gm{8#N1gnRFzqHU*;dCY5EM?UwJu8cJzGg-N$I*Lr>#>Jbr{=Dzoo_=+% z?(1)m6sSn|-J4Gyo9hf|bc?fxi#DH=n3nV3V5?1#y9fhR(#pTdnngn10~j+v@KkPY z>*fj4H9v;ypTIov8TdLnKz7TDfQ?TSH5&zMlo%$S8MJ||b{&ya?L(~AqsJ@Coz)a} zhG&IasTmT(buVU@-w2YQ&o(~d`4>ZFyb3{(pT9bJ?u**mAWXkr?-7VkSv$bc?9vT~ zC%9M}KqLE(_ys{g2z-9iCurni3AcfqpTbnb+!D?BYfo(p{MIk7PF=k5;h0r}SAM`7 zj1PoW2Sif!GozLu|6JxDLPGN)y?$9GOo&+h2Q&ZCws3utTc3c1VdS6dZf}!Vd-~GR zXJE?DKhVv@gHCP_cHr$lw-mx^X%K72%6-4e(qkoM-UNZsL;qEz`mT)Duk^pj?i=C% z^k6pno)xBJK!e}yXB6)+f&6~S&*)C>i`ko`mZZsU0354s>Zay#yLJk>l_Po7aYnM=WTV0_A`>iMyb|6Ti_2jwC1x#}@zk@oa+I`wWmtn*>Qg z?hTKb7D4rQmn*?1XvF6|l%+IzgJrWerJ@AqBw5Uz8}9&i7DwP-a5W~pFUSlYg~p!A z6fWp(40Ghn?o+Z_!x&?~kGIB8<|-~d3}+LFnyi3#F9TP`7a;cx|KzY;4yJF~eq zAAFXhY2Zci6ZIG_>E+s~YYuxLe5bnuR@^AVxvd(zSlM5^9>L!uP>U6riU9uoQ0{Nb zi9Su?;W{{K1qSuN-I>SaDRk-gmeWjwx*H;56@oOh<30{>-(oL|_`?#jUWDPREi zB>v1$2psK*pNe3$VptCY)m3>=parD4N?k|1cD8T~M$qEH8Kaw_e>U4=Xu_&(-0&ek z;o~;JZ0D~cS>D4>^Q|y3rk8;a12WUW0~dSb?(Ew8>FxwPxv(-n3s$wzi*^WS7S%=-sRh`D8JDm zIC1xOs(Zm#-wp3u4k0}m3hf}Mg+lfS)%i^N6C3T!RfHISs8RbemG*|^Y0H%pUrNqK z*nHByR1>r^{`Z^&ZlM#L$+?(izqTk(nl;TT<-U#QOp>Mhmw!3Yl`#-w zt;5DM2QLK{qslf(Z!Xwk(gh#7L}Z3JP14S2v}2IB)wt{G=RUY6-O{%~ zmN;AH!@Dm{e6h%REoq?|Vh@pVQm<|D-waP*Sb(?h%T;u(8VQ)4u%gJ_ar$%ppEF}6 zJCaY2y=hQZi)PqY5dCATH+!T_?$%I|36AmmL)4UZqR7e-D~IbPR9}rdUIgi1;zOFF z&Gx%Q9so+>K47kp?yf+$N^M;VqJl3MSO!M73fk%t9O$NOB0prpK-)94s5MeUWHa}w z)d82M^%}Ih*8aJgy1A4yNRM2Rzi83mlVY6mAk{tVm$f{cbMC&)t7f&gRk*Ofi5OvQ zC8CXy!(PPiqkPQO5zxApxQ4s<))tDCfGc4KRKG^jb(=_7J0kuXR-;y0gj-&+c=;aw z+`z0Ati3e3>8;wAxsza~WQ$mP0)PMxeZOrGtbUn6JOVoVkGM+EwbF?@k^AL&j6^ zB@l6-{M?6O$>c22Tn$|8;MI(K4rR3H>hk87whupl<9IeX${uy1|hig)x8L;K%j4YqFEzfs(uG8fJxe0c{ywkJz$xX5jw0n8Zh zKzS~4x3q0^d40@a$yh2{Ju>w|j=&pBaDF)yhrh?H4_2zBXzrfCe<`m9CZQ`Y+_d%$d(nv!Zw7VtZ}_< z8DEL#sx=jlhCI`Z_H_$@w7Qco6ddsZcDPw%G~*66YpeC*Yc{PL)y_c$GpW9UX+D8~ z5B!Rv#!<6FC!bI^&@)SAr*C{d#ZwXKt`bY$n1v-m}}c89;u` zoc$GH!5pnv!M6xGZ3UY?;?y@UzM=HxVqd`>uPoY{SYgM2-P2A5E8*Yr+JlO2_Goym zcPZc&s@b)3`#V!*7GbiK1v0sRSCyzWM)Gs~0=On7kzg}YRe9K3=LHf03k_tKoemju zjt7Z+j+0-9(ySIro=r)fU!v!p?hU`K%DF%DK#Sj4aEp&ueapm?l4aT?k#;#@)d9%O z#)+9gEft({nQRJoIYjT|#{B51_m%(=1izNxl|Je`WX|I+i|QAmM0UxOf=9ziDM6s* zw#mA1~HAb$T`1elRuk7Z-9z9=0C_{6Th{5=7~9b2(J)? zGc`!57?&~6Ccw;`i}Cyou!=$8D$e=d@Y*qoLY-rOCB1Cfo*?|_s6SLIu$Z}XB6e9; zAE9iWv0YmbF_YlvoI942QfU*{4T+NA{IIawn1=~a7F^b}ns|{EEQ}rG&)8i-XJWbH z{SBJg-c=gzMz-{Jp-E-S!IIeBQFFq>rxt^`VQh#(B1xXiFGgP&7$J}9kB)hXHbdBI z@522`R;$xI=1D?~{sn^j#pMC%gKxSUT) z744X8)jlXrEr}QdIp^MXZJ2oeJ8TFO;zSywFrJSL)t4=kFQQIjc*`H=p*emNz;<*={Q1$esS01}EBf8b z#Fzmiw$FF~ApD+vkHh`iMz5JXWEV15%ewX5Z2`+(Wq`ff2b630kf0vJR;Y$r6~T}T z95W05^H=F&K*uYtH`79?ftpj@`5Ux_q6ot|q(#lt8YDNW$?H7^Zi`UXat6flTUO5L zm;@R#qVSZ#vPaG!z5{12o_$h2FITp@D0s9QTN%Zs)r&G_HT(rM>$)`MH=I7o9{M}sFY^7; znbSxU;-)j#a@tel+A}!cuTI|7t8|#+ERLLt!pCXtFcB_leW(J6tn&xgKFURJ+uZmt z+GV=*{b`yjIsy|QGB7e$)5>P7GQ+fEXM%pJCCXMPY*J)wbhzUDpbg4pq$4$d`Tx!0sIS{a#-DPQ1tl-Pv`K=R~SM zVAn=5CQ)96O*vZ5lcD0>i=CF}Fy0)#MQMN9`5xi!PzH|kSCDq9p}LvzdHuf!@L0HjC7+UD2Xr`O z3dm`Kd9OgENMx-4LQ7cn$j*^kt z{jVq-{eGX@J*n5`zJf#ubIVeW-fhg}&?#!R>RXyAdWc*a+13-D9n|U<#od+TmZR#_ zMDD7It$uMjda}7wv-5i89SzonF`8SaX^@)Jp$Z(7Ia-B|P8rv0`Aa9#UUA>w@JPJO zM`ROGi7hqbZ`u>dRb2-jK`q8JkMoA7T9o*7vQ3GnheL{Np4qkq z`5P?nac{wmMBV3-UuALh!r`*Xsf6+DCAnKFJP~yKxmnxsB5uvdTodudvB)bKmSL^~ z%H$XR__gXjNUUj&)wMGAUMmWATGU**G3=%rD@v!nx*1qE*?g_5L}gJX(K3o{w&UEX zEo1FK?cECCCgdDocLYQ>eY5BYb?(c+VXLfG4Z=pxIpUmO)}5hSnCnb!l%g1&t@S{T z-Q$R0lgzP9sFMJWqZd?rcT%oan;6P96^=H6sK&eLZQlF4GfHi-Yt1?gapF82 zIMqcKnvA~Pp1bM|Yj#aCC5wU*2qe<`{zeRvL%AuxOzTooZr^lrQC44PN_cV+Z*8k` zV&Dct19Dd*IWHI+Xvsq=911BAv(svyqI5>uD|86^c}adUul)^L-6wq1{Ig#SH-0|s zI+26@!O9^?Vb5=Ze2-*FhZV|?epQ!B8h5IuJzkIw&{d|NO+q%FbwjZZ=|j&?9odyC z-2%yBhzA!Cvser^{E)U^>(BI@(4CPFRpgq<%Phc~YX#n8yu@Erb9?|I^$HsLo>{B0 zy4v~gxf<9-v-BAQ=CEOT7Uv0 z3(dYZGgs};_h_dUC_%Nal(EgLVNObS%}gSwucAh+K%vIk>{{+x+MpQW1Db{jbKlnu z0&FgcbDlhYS^Ytp>zK8^k0-IGDS2Ge7d1Ntd-&+X+a*LUXKPU#&B!zMEBa`8_O1U3 z(R}2NA>UuG%o~luisaxJloR<-@BftR=6z9#<-M*=rJN|0Z<6V#`81|)Gg}5rQjQjW zdDk1&3f&6(GAM3riE_PRZ1u36D01L!Hk@j?Y`1K*W*H zHQ*lQe|C7_$vvbbq-}@4fNxV=Ug3| z(9!7me8gZv)rXei?VOO#@!a5yt)-D6_w@?4mPJ8t@ggGBxwqm=1cB0vIt%krwQH3!yh-3@8eHu%*a#Fgz_miCv^coUl%EFQfN|Z z)F>DFu7$gr6K|G&-Grv1i*SwZ&6a#uZF@@D6y$SGKIigp{M2C4Wju~~2vyv>eHJU> z=km_ye0#?l|n!5@O+2(VONEW>z!&F}znqh&QhQ$Yz`U7VT z1{7Js2HFliHS884OK}|Psopv|cUK3OR-#sE+x z5F%NNrSndsZZeAQaPoi7S@y_|6C3Tx9%uH&vgDX9NPPReDj(8UsOG(m?lLRH`}Ig^ zu}HXHdWaW&0~{f__ipK3+w1Md29zfHtaJ$7D=q3ZG+tNp+kbVfWru}gajrAWDmYvR zxfhyRpQYvCa~~7q?oO>pKH&?i;#9}d2}~=YA1WnlMo&BQ%SX{&{ndgF4_nlR4ncSV zd+g&zVm%@E7bibs3;|hpctFOsrTd!yk2t%dwA4dX&R-hBSl-i@1^mtDPH@lWaVjGG z_Qx(^nw_lv=;oZmjFuR@3%Tr_vQo|~PhF#tA}y`LVo=! z1Micd7Rps$!Gjz+lN*ZJ9b$iKP3*r;EqM+0iOg8K2Noq$3w5p1J~P&>WiUP!CC8DQ zNW=axXDfBw-A)_}ac?UP!&bdo?echsRAJ(qNeR(8kr_<4_JditsQVi(s2gmCN={zL z&if6b5)_Zw*1k!M@TWsraGq39#AL(ZfVq_SE43Z+#;bytCdY%(FX!S4`f$Ef4aqU> z*5vP4gVN^f6dF>Xeb7J=#QI_WP>g!Lv-AKu*B>;}<+Gjyx=EoDIh+=<1=O?YxU1?| zZ)G^A@@ntRSfK=R8PhDQi%R{9NBR2Gor6dva)+iX0f9U0riEw%dA6TOrDiki{*9gX zjamEj78BgsaWx!Xcm^BqUQu=*njyy6ccf2DUMou}AZ%4d^R=s_9x{#%a+j_G4{PPP zA{AO}=T&e(`nNLfpXB3W2d;xtzO^)JS3~!{`HH-O#-?*3vx*P7jTndMV*ntGID{WP zF19G*5(kP|Y>u)liudsww4bT@ZF;tCN2*kEzg3e*=B8&J57fQZH#XN}9#dpbWLEZR z{B~uLFCTIC`2H!>yhkl@7}7enwxGJrT3&Z(j?y_00_l_qC1h%`>V7im9-#ALpJ-F) zm`j;TH{D#N>F&64F4^7IT|Cn?uOUxEVEH5Z6Guf#NQOG2-iTT#GW1yP1wT@aIRsgH zs>UtbYpoI}SqhHKC=cr4PPDRqtj?RWCO5AmNp4ZANGbC)vKU=>sZi<&%V*E6O~J6@ zh_Ivl2Q(Qc3&zqZi|876n2r^WrjDmS$uM~yCby_{7@4BDzTekFinvx%6{+vABAJLY|{~r$N8SW z4z1Qr{IaBT)74AN-ym^38zU6cnC6~TI(~HId`VV$t{R9`4oBWak)V(1GwNw52v!u+ zbJjlm43eS7S-hg;794&`iH}g7Y$W+1$7wpXdn3>WY?il?wXx$lu}m0Qzpd0Q{oK)^ zM3)riq-PipUOC-o&yTWZd3@~0+`ybq~k zj>Za3Y?b>S0}7~0GoQq=M<$RUtZspL$u+ffB*~TTtt+)_rQU=v(zFJpFteHMyLXUe zKTe@pv$ub@$QT8@3cI-|h)}+vveUxky?2s!wsX^c?66f`${sbsE}8v)czmmHQo2u{ zhGiKyBjw`Nvmu=vPbg-4oieF)~jpSd);J~b^9Km zD&25b_N`eEx}Ryrv;#snV|g8X{TAI-N>9D*cw>>}fX;f0>H_@!V` zMHjlNc1p8JmVy>j!CSoUsKc7C!^nj=`#ID-_fE#~c)|&DeG?w_bjb*vb@kNt@$pb> zz;{#(qaMG~ku&@sA~D5i?LTKPww3GT7VF;mq;@3SuIIJZjFOUG?(OVvQT1oD(>_i> zO`c8g@#SFhc=u4=j%07ul2H0X$GNEnkRUl~ysNFxXTUxRQB(gEBJFcmf-9m92m&60 znXIk{p3b;BE&I-gA1R0bC^R>6&C^>AS0o!hWUhQT=Mzwl&G{?Ax0}(ipBZyJE9Bbl zQIh8GKg?5DtLJv}-7k=j=QPeQe_{KOxAro6o?_{uyH*VE+uGEv0z>&%x_JS9@+sO# zy}T9Izj6zw{mj`O=KU;WT675|@!)?^(_thfgNpNaXuWMQpFJzirT^A>zzP#tvY$Kg zR-?JM)5Wl~x+GXP8&3)-$YS5#=5K4hwdB9de?5r366c<>To!E@BPp3UI3fV#WZF-^ zy}3oU2u7KlG%8A3;-|&S!;a#kSNdXB+Wm7~(jOYGIJ6z4lx=#CRBdP$S*~A1*z&m_ zz&7j8X!mEWqh3?xnMn7_dzBtKQ7hFw!~&cX~LoqtM1bkLSu&r;M+w`NW2MqE)&-JzFNoTF|@Q6cnM=qS3bQog)KzFvO3$GqD-pOlJ;C)6YnInq7(;G}$yhNA6tDw=U^dnl3C zeBy7{wp7GCqW=D}#3=`>^x_ngD}}yySz!5PJ|6MGeQYuf^(wiJy!(4!!76X(^f+&`k}O~z9@sg3cEl$u&zaGQ}p9RN3-A78?b z5yCt}?*tNF1c}tfnWQp*@Fss(H@`u^yENs{{?a5<(f4mo>EiQ$;Jwd<-&S76d3$ND z$m<)Wia6yKzSRI3Ow3+krev7NQgaQm^Lm0hM~HtKWkFF+%~mdR40-JQ-ZJJw6l2P# zguaiufqJ|5XWYb92dGEgWI0CV$Vbf|N-}ms!I^-)-4u*XId1k+HC~`iK18vA{(PYm z=iE+;GOu+ZIfOe0ENQ7WqJ~SHw*?S)bS%CPSZEY;o9CnQ>brLlm+ghSxoDb@-ph zMO^&ZZzX9oFR1VopZ$LCnC|> zjf43^*STHO$FqHrblQ8MKjX;v@VxSvBf>T^5%?}W8bE;5s@%f_ij5+#*lr{$>J%s^ zJ*H`U+mRY3hdABysIyY4>7F~kkkei^Fn$y2?J_m@`5g*F!pALcr}9#i^6))mYAeE% zv;MgwNR3<}J(X*lnHGOwiac_aW)rz0uf-C1FGV$$l$qd!-5TjxKZ|nrPjiefqM=sT z?3{#j$&TLu*&>7cy0^YV#L;o|nUAsArFU5*UZxgXb9qjqh`(GoF0vb4YGjiK_zvQw&kwMA%;d*2{X=iPma)y(;xhaPvm zWG151h_Fraa(~_;t(nE&(-XCjrn*e*iV<8y>c5$;$m8_aJ-hg|F0qJ2J2B+hMCwk; zZs0+gc>cITq(yK2p^ObifU&VTTRZgu%}L!*L4@&!2i{RDv2cAhE*n(2{d7n9HI6(A zzw4N9lx^T{`YohPHSz78S?Y=|m;2spYlBzfZX~{mqV_Aam{Yk-ga6ePY3f<1-D)`I zt>WM7(epeSYZ74a>+0ZKRjhTrMOE&mO6L8!KJ~TbwbtXrpr=$xKS{EN2&XuaUy`c9 z9{T%9##&BTJR#5A!h+@`J}6al6MyRr_X)RK$by7~{g1#)45A?0H!gWnK4y`XI4z=g zU%xk~h)%P)N+XL!$h5JAuZx|>K<jxnWx>o?p4ZQ!%9xddP-jEwL0$PK3}F4 z4(KN@@G3_(WIT)z;hG@LMkfH%BSn_+{9$C$UC*7WD5sv-4VS8-S24!vJCsZ+6GTa9 z4)y%G=edi_xl)-aMdX{g(UH0_nW~Aup2r6NVDHaBz45D$kK4={vmlShTE8$fzRxHK zyLcw@l29|J#nb97=u8BX>Rk`;OIaeEAtcEM?x*z2QFINMl@H z>Nb~Qeo9tBj(fgl=KZ1e#8ln*-()9v&s(*#s@R{VWcDY&Fjt+PgAu-$9EF{=$dqHO zA9Z;!=7vJK^Qox&+eTf=vNE)#3)#%UdJy9BELxhs?E^G{_ZemPek4uWvR+1&$teHr ziW6l;HAoKmMJgyrIR}Mhj^k%*tTjpaq8F)Z!!~1V$fM0;tx9F6g#dD5Z*xC7>@dN+ z-c5_5cK_173Qwj*H@TTMmhjfWUBqM1M|{o9Cz3fwnQQU(jXOV@Qw&5{Y4)>VT9J4Kbcq=^Xob88-&WUJY3qBhV8Vb-&~`W zqfoP0LkF2TAJim^T)Z#WtCE+_nrX=LP@iJdb=P`xL4T4;*aQ}V&pn0Yj0LGD$!CCcNT9Z{DumP1H`MQmAjRTGU+&tIf#*kIc6 zCs%X~Ps$xRagzH!BUhn-pNMwzMGb{vdW_DBOrBrPr-~x=o+Ehk+Pio1m49Y6We74} z<)~q!wXNmn(>`m$Xe$ypJB=UjUQP3SUv*E!EN>sB2Pm zkA_M&APUX&u}~DRU3IHq^&XGG^^CvAs)XQb`3kcLNdGj?wO>PgucK z4(6RbZ}favt#3$ukqV)?;i>*fzTr6P2>Vf8+_LY`*-@7-r-lR4H$zDE2J9DDKfGVd zu_3pbtA6i)+ENmdEtTb}@V z2`G8I%5Ep?efeyu0*yzZDS1zQ%bO#0&V4`0zMj*LSK^@{kJnFaJ%O)PpW#9Vst?E# zUgu6^?5FGo=3Y+iV#;L83wx#GdQvVH?wiI8Uv0a`hw!^})4q2m2p7#Qp7MX;Glte|7lBgxT@gr$k3ZvDfF9SgEDF`0n;06!+(|w=S9fL z(KY*>9sMdgy3a+0W5_%z)-|O{fY3jyr#-2ccn;qb)WdccFk)Ioe4kOLStq5&r1)D| zP%q?$q|CcBUef3Enq?SPI{eUzx{cl;ZgZ?<81|##_RSWq4L=@H19COUw$HKxVib4dWTbM)cwq{x}Z!cfehs-va z;XyS1r`S}*oJ7=$J&`7OC?sxTO^;!YbW0Dgc}iaC#?c{5|LX+Ps-~YN%Pn@1i#54H z-JaU6Y7`WBniDjN>O0j0tq_Lo(D{(u;?&l7bv_l@NQw!bpYuT9s+L8FMR_eI8y0NE zjsEULN9Jf09)D;j@`3Imc}cbpF70cVM*h#qk){^vRqWH8#uUC~pw8HZ&VMTCR3>5U2>Qnv)ugj}eXQ#dI@2@(L-+ zCmQP&*Ti<>(eepySRGEoDXff}#qT#olj}7g3R(n2p$`5hEGkE|Zagm%eti3DQ+m4} zuSI0)3P_kf$)1jqZ^8%l3QxXKU{YO+%oUG9aT zqKn<4F8=9jwlUna)#EdOA)ChbnddxFc~rIcdx=}=YP6R3I#bly3ptyw%QVJl3nfcf z-(~G*aFQd|sYAL|B<72|g{5b{fEZcj@?&>|?tTdmV0R};6Z;+{Go04A)i04-Xz9Z% zgt5_n=yb5x>4*53;fZ~od^k-ryb&+%p^9v|l_ylaiK+M`<6L6ll(>a9vLM7o$MqI< z9rPveqJmGkq6e%|!E?J)c!ihI?f%UP8Y4XUu2~D^;}4r5@0$tZDZL`CLNBgf@rT1@ zzthEd;9bLd9n@~sW%_=OGY7XHf{t#KXgjix&D!wF?@}GytDYox&~7ynNJy^B8|BA8 zur(X2TOYbI>bbZ0`hCe*Rpamg0{+>eeeT;*8(V6A0sigLxA{$@V)HY$Xis>qydpPTxmY&fp@ zE?UwYgUF9`7wGFBwfN<1Ob z&_m-wGq{gv5;R%YWDW^>$k#GWS-~2F2RvM&7kbXB&nOM8?yd1uh+HP<%~xk=yGF82 zWy${pXsa{MwX^jGim7!0IfjpH(r}R4O?c%b3mb9{H@c&^2p0Nqzyx@nRus*+1+N zhGAV?^EI-8Q-!7b;|la3s+V5?Q1*ezqX&m(?@d*Y7kG3L-H;qG^Zn=S!zo zSxvz@*Sop8(RLH8tk(?=MALyiVA1!Z&tt1k_&07FiLqPPS56S>LZKLxFXazJT@ndo zqyHT5e8IB$v1Zc){)7@W?W}v31ml?U7p|gGQB6H22-JF_C*Z7y&#l7!d#4j!FnEP- zSmV=`;m2ecq{<`}lmvn($Ppq|wT6){%AaP_SH>G%O|810=7 z%W?Ag3*w34YQXU2P=CK5TKdRovAgnKUoYJC=B;5v%~F2pfhvsQ_uVID``8>!o?5;g$U!0QlGzB|OOh;xjoY zU{JsiWJb*V(=_Dyk0HaQwY<6wgSA6Xt0Z>IY0wIXVilCqrAOt&@*3!kzl^!f7?<}< zXKSPzHLmO~H&y*iN55cwomMifn0~|HoEA$7T^l)N%`qG~H9;Au79Is%sqJ~XAM{K4 z`Ft-d)e^?+w3JFbj~+km6nY(TrRR0m+h1rQRy}}TovFeH?fKi6+l;)m@oU{RT*;26 z;|96h_a60)foIBv9iLXWSI8UTg|t|-1~^hl^~sf4`_q_xefbA<5Ff(CiV8#ndL$`z zZ^|k?gw3dh9$lN6=*ilO>w;*3x_UHSbr#%bQFt zp$|~JP;m5QQ~Lb$7k}+?4%Wt;y??xcr?N|%pU=-vmh4sCRxTpL({4J}48^J?ziY_o zg0Sf^*BO`Ae?X@JxuB!bQT^Rp#e7xs4}t}%-7c{ITcK^LE4X zOd`CvwXp9;JeGaj1>Wcw;3lK&b55T_eVKih{$4`mCT=xv`Qw3MQx*0KVK35z>niFR zdK-nfLzAHO5O+$`RqBZ6P3mmTZ^0t?xVxz-k-^_~I&7I1%lEb$`<7PZ*V4HX@=U*8 zmaPEjwq}tnuQa2P%F$DT*6gnJ95QmTq*dSC^#xOBt1{W1G?(B!be}4)j;=-;nTuQF zj4v7N>`{N}6js~7I@PJzPRT4D-le`JIh={ z>$1(JIp(;io{$^bBZ_pOI{Dk~4-^13FP{ElSEZg*+W3x!iRhu9o@A^>dt`HA4MHZ0 zK5y)y4`+w5TRERfj(7Hit@1u2st-pR-zzig1vl`Bv22Jg1pR+ zxwa$HgZ19t;{bl8Y>ngR_~5cUn#~0C`a%SuuaGudCm;P+juaG15dmio#TNFP!Wij6 zHubFs*FX_UX&iWnl}&^#0OXy7^-{uusT{a@pNQM-5;Pd+KY>oAme(QhC=`ajfh|&X zljrW@_ndsz1;Ny7m;xP+h%cH%?NXoG#wi z_U?A0xM~woBlZ%*fOql|*KYMIEh0>Gsb3Nx2S=gae*)l;vMKVR1RK#kLB<$RY&tEr(G!uAln40s z;nj=x4-fV=nY`D#xJb7~`Z!kL4pq(jByYyoiB^rxcBKz5gDxoTML`>*hdb*QK9r1u zWK~X;{hl`s!2-j21~z8K{&L#~&5pBX;s<|w#lsL@of&qWq*$^YOi~9Iw@c=IOY0w! zTV7w0;oWcpH5Lw{{SDjUJ^)554TW~<9sniH=Vp@Y=fxTIUBlg}ILiV`G2o#20Eun6 zY#yR8a^M*>F3o_$)a?&IKXEFzhKA4ZZXIxTN8ETK;l6i>&wwJV^ud0tS<*>9b9D*O z7Y~&LJUuBeNKT8bwSIDXV3@ptiZPCM;AA3FaS8Y{F3Vd$6}abk2A zxa{+kPok=3pJmM*=_;`Kb-5NS@7`y1aje|{qYh4T6~>&L)H>^=f|mDZ&zpng%Ma5P1WUlBZ@?m}RQ1O>Eh(h6pZo;BwX8lRfDN%N z?Bwm(^`bNU6ra`lE~)h4OM>ih;lM|eH{x%iZyPIN!-R&d7iB{!R{45(eV76(-B(bG z4>goGz^U_v3JCehw>yIa0O@447gWe2B_ay4g5rD+o-ZR zFJ#TxRLLpD(wYQhhjFQ-hsq8(iBPUzny={DrHmO#Uw$22Q*Ps#JavpQra2n<4U0vf z(FPTCE{e*{Pc)xql7Hn84Kxt^PVt-7Hv&fw{+4OQkH3EG9v+rEPT-r0xz|M6>_-m12eR}EpX=Nnh|?$ylKpl}-C?Hp;2bH-s83S++Cz?`=_z zZ%K7)=>{!*}i4ja&qUeyo!B#Q= z?7PTsTpxIJ{Y)`A4Tt{XAcj*dNo-*MZKclk@2L?L9qtFOaq<6#KP7&=4cI1&^MwF^ypO?TQq0=dzwN$KBTGISR))WnKR*?KIG z2wDSYxc65!)_HM5&~V?KBLCN;K>fNy+VPQ(rh)k;rQHCdXuNT7yW!jZdTpuLDs}aG z5eHBX9}leF{d*B4l=or_+|mO*4+azje!l8>k7|inNaKzHgEkcJdcVZYu<}Qj>*kXt zdxw_F0AHe?MlbsG#csKEwW@F}+uyGkSJ$&91THuAm$TFti7pXPg6p%f6Q4pE92ZV* z{)y<&Z!;)eO!cb0Q+@K^Ap>ia$!y7KxgT$BE!l!(>-V0?Qx`bR1;-q+N-c)Ci^1ua z__F(MnzJI*Q1b66uk&O7vr2rwuH;YV@q0D!Juq%{Zn6CUqVM?~|Fx-d&CC`(h&2{x zO~Z=8&*Qd5U)^=&D-dxC{k`e$^iq zsYz^i(g|9iV@#Z*fDhPvcl(d0$3~PC5Dc%2rnSHM*#B=vJvU;z!EY3{^7(IuE$7w3zE1x;)GveE`s_8DQ|!*B;Ypjl zow|z+qb`OMpZ)Wl=6K62aPmV{n7rL(%YwxT!sz$lDW!a!{K4KTmEHeda@0+#6UQEJ z{1SCu`0&8%pbIQ;(GY-?c4s#0O|fe)WJDNNq8h~qUn~xl>vyG*p~tWJ0z0yQ@UZW= z!T(_iXCTB1GNJlQEWPEcwpu`J_cOxg2}nrwLQuT5)+u;g|K&L{QiFfzf4igR4bbNX zV8yo@8RXX~Gjhu_*s$X;sS)x>2CWN4b0GL9)Bj>cX1`t>sqO=LPOCsh=ZhRXux^fz z1A2L=tnKc>toZI~;K##573S+LSd}5l-bzPZ4x@~q>qY-#D>;`$$__-Pfc@1bS#bBO z%fmVUnZW16VDC*2M3>?f*WL*`PWOXQuN6evr9cEHI+*}1qHz%f?F|5aX87@O>K!GX zs_f!62<`F97kEt03Aj3T-@7^R_WG+4PbV$}35n#-pnKDy(WJ|LSurgYT_rgI6!eC3 zyeMm^>Izs+^U>dN&j88$k)3sqkh9+o|HnG<`Zd7wp3<7Q`f!290+8aTNClc-=zkc$ zOb^D>Lv^0jwcFF`mP2LM&8BsK9#;^l%1d6r9;t))vxUM$84TgTliC!%zm#Z(%ZqBx zaH|Erz$fSB(Meugu2g+eDnjM5`a%)rBrDiEy4$%&SG9{e1K}N=9^0!YMn7&%eK8 z4L44}qI--x{^{hm(B2xiJ75+S7kY6u^VIksPk5L_J847UPg!-Q#9Xc0v~OaZ_m#nA zb)4`>CKhB7wxED_=hZSboKK~Sa<>UqHR*}9+AT(64)Zl1E5{ZDE*m z+c|!_WR`JqI0#z-!FroYdK}a(cSpe13#@XDfk`8XJvpohGj0KDgk``ldSXtGJiKuZ zmCI`kmh8;8zg}}KcA7hv=P8>Qw3VY>9aeoA{Mg}9QS??}=tbw>7k?hjlJxn-YtPQj zY4Q^dr2WRb@*B2rJEKyqzgsBs!d~nq zV)j2}ML{Cdv}^>H{STG}m4n7=pe1!w6fBQTH;Ge7=!>r)yS$jC&L6j3UXM-XJcGp* zy&5qRk^y!t4(;5?-P!}jTMtwA;Q&zZL~R0~x*rNU^E$8H;lUWM zPC^;caC!91mnYRF;sBrh*m4Za*3K+|yu`~N^Ki6rDr5GVsh2m0IC#z!T~N4e*~Adx zOuf?aa&i$C*?|qgVTM;RWF*Xn=5*ae>uXp>iiwndpm7f(y(K^}C*OUeafJ70jIpyt zZha!AcVH79ives!#h%qASEHlp#vo4c1RxDiL|<8X6Jue9pAGTn)kN^HIggq=^Ge~j z?j7c$G08<SLCkrFvM^x^IBT1}uQnNiGmCfRTEm7S}Y@zEl`%Jqwt%|!2UuFbQzcV7oXn9ONb62jDi`E(k>ecd*ki=p?z2db1OA-rM zEz2QGZU$0t%(NGvo*xsfS4Hqt@DG|oFVxi2xAll z(WythK&1WP6A$XGgRpafhj-Q4m_G#%>V0-y2t8~AMWOzuXv;u)5Lk?e*gWb>9p zFYG1;N2@Qp=$9*Sup!{d5A#k;kP3nBiqoM^fswFb*~ce88s9%{i(3M=58V*a!vNT1 z6%-e5eF7|Fot4|)QI*yZUq1Q_|2AE~=k(pr)wWEKTRpGHc1*WWhxoJ<788^?+73pr z^_QTto5PMBRXYMZiDR%uFE)@x#GJIn6H03a>%I0H7uF;p#4L7ixKX9$FDYcNeNvzw6EgxNI&lmcUw1XvK|f$^qx zNo;OJL9iYa3kJYAb;NQEHUgd-t&%|gTs?-PY~stvC} zhb9z87NMtn%*zr0by1$bf7}!R?m2`$nKYbwohlIo(5D40A3P9>i-S6{QbI}ZHox!U z67p-R?~e3UXuy?(38jUdq5{>KI|V@>qtEalv(>Si#4!A)R+6NB-a@7gWpMbd&S7nvAd=zO~I@+{+4}E@QL_nUeCvn!y;1NnH%(m6?dJD?FXnC zkvx}i$gzWe;q`C0=k2*2ZJn5>|D<4}&z(;X_yH*|E5`1^g52|v!CI7TX5d-i2>vRm-k&KswAr%+;r=n}e*ZY$w$dTt zd>GBD(T%>W0g^-6QCj~=8>z}2_*~PS=9IW=yGK1|RH|oxD!sQE*%2-kCV75QWcuBO zGOzvPh7~Ihw#FcAdWSE3(D!@lRl$1P1;qr3!^?)nH{3GoU?J3lgqE0tX^3}>)_Pcf z3(+sb%nNLd!H-L!CTUm#!|c(Wr>!4A5zfGebUPo))jB2S6WPgTBUKavX6;5>jvpV( z!H?(V7Dl zfiLeQ?6k1T7O-Gx^lYe7C%oggRJ}irg&L$zwR2mx-+AH^YJq!N@hOyPsKQ=`b~Y%Q zNdkn{Xe-$CK0F6g`&NWWTISXrmnE?+$Cu~6=Ia-yznAnO)mV-P={dpcc3L@_a+aMb zCO1e0cda%TF0uKxtnE`ElZyw~uuzwjR=nzik>v!(q@$ z=*_d;UQ_5(M3DSaC44$xA^iOBWcqp4b#3I`o6V-nlZ!rNmz@u5pSH1kEWD{Jd(`ND zr^Qr(L8ED@ZsrI?%Zl#>j*w8@eEaJB`HHio#2hjRG{L;pc5wO3rQTz8kz*8Kr^TW7 zA!TE3>)nV`%Oa>QX+l}l)^znZ%*BXU?aULyFaDk$^D5*tcN~7jIQM9@8K=l^E|1NS zie#0~n=3HbXE3@LDZ!RW7JqA^oEWVz)3Vr|XYzqb{rn3xq>#-tEsKAmZ)Wa+KNYtV zMK!i9Om*A1%|M}l2lJz)mXw6~n9XMvJD4A@DF1robiX1fSipLp0C6!tH| zS$<)4on%91+r^C$H_*T7cpWDRbNktfdo`chfFTc)c|b#gn4#Zh*Go7HHIpLfU`+M2 z93^o$;S>Mm^ssW0SQ|W`%NwS!gi@uq(gPgwfN*NpZFmbRLUBP`o-?&-k3XJ-XDmB@ z!~Uq?U;z{jDrbnP4#Shty+4gEn{ziW8zi#K4+tl=>qhjb-p!2Kt<*2Ruv9;fw|xkc zSNHahR?X)WF9$5}oNcD8U@t$8qjfU*bd0383oUZ=W&0*}f-f}jXnB;zh zw5ht@d#&xn?({s*0ycg*=qsG`&(l8fE9b7;;nL)A*~!E`=5}k1KG4_k`SD^^*l%W< zno>ih^OAAvm&_7OLF1)#BH5do_>jKDZm%6)v5Oe@p;w88{87?~hLf`5aE+QnXIQ{y zk7ueL&uZQE^J2V&t&Nol6A6_r^(+;sAQ@E!oTp^iaa)+g4_l(S?`A5!5VwqKjHl0Zt8bCO}`X2b-88_bMS-U%wQGa9bUDsGYh>e+T z(DOO?fbWFP-|~^0qnRPpatx-LwTG9#?eq0wRkw$j+<#d>#lV$Q;3NrE)_*HloBf^jqOSD1vQwfi zD(}=_*b{?=!N_){QBdH7SYSNm>VH-d64%F?R?{s!66F!9Hc>b1CM`y)5?j7+?-hp+ zd+X*R2lAC+Utv(X_J69E+I3R>otVKid1+p!VemetuGw~(kbtMu7_?!6|D%ybi`fPuAH2PU9ao$*MBGSZw1IyiW&Ose<;5%L|g^T2a$Dq+rvFMiqJNx z3_t}-eu6Fg>B|fV2$ybf#PEEF#^iH9-%l!!kK2Iw1MS8n{25haYdjs^axml)?&_!+ z>~1=XD+IQ~+9y9Ab9_Z>k=QA+*hTdF#J5@z<5)OV3%8$ao4dC4{oxYNXn!wzEB|Tp z+OdC6T+p}1peRxLaV)gxn@Zf*$BwW?J%^)_*)&6V3oB0O4ly#KO1IEr*AcTGq^94Q z=Gfft?oWtKqmlPq4iX9ff=POiOi5%l00#fPwq)?$1$iG|5ZCxGalG7k6;F9-UqraHX1U z5!vcp*Ah{JsCbhM=b{_jbjmlMH3Z}wX0U6U!kjXy9~I@+UR=q8EGTH8`QyzF+Jb#H zrHy8%;tWgFOyB;}p?5!AuwZ<>w{pQ8sD}uUOFC4%QAvH4$bo>CDwmg%l1($%JzO=_ zPVcuPJxJCu+h1g_&K$=fAqO9I3Ij}fjk7A8+6z2*j!E0k5&Zs4rSwqh6GKCJRqN7Jwfs)-lVrui^@ao( z96{W}HJ&6JJ-`lMt&Kw*U?rE0q@i=sw!BONgnGvD`8<~&_#MTeR)hoF90r)Nj~SKn zjO^R{U$uskZ#Xg;hRx&M1P*+x{2dI0r3O<*7JL^b#-mv9twq8qTWQiAP)I(>4u1RmE4&cmCLhTP<5P`>oSG` zAidCgO~1piU3}f#p|N}6VGqT3fz0J|*SjAO354}XKyn)O$7Ekz(oR%2Pj2f~^tq5b z3q|}{_;dv|N%H9$HMF*`I#T<-L9K@|BN6kMT=3RHIbohb$K!3s*2wuh(*acoAXsLs z=HE!6s*m7!a{u)`;tHz9Ne`P8JGb3Djm8_N)*6}xdJzUjl7H`8<0~rDBNUTUzm|*) z+ww}!v8(bXvl9ZW_Ge=V4%dgoR>o?4ubzih?xIv?lxU3Eh=d@%?j%q#01;;ag{4>* z(1MaXg5&M>$CZ&NNSMu{ z&s~KDaF!1NBdG5UUdhBoAsGw|-w<9!qCEzL6}~+Qi%@k?cX>1svb4MeS}@KpwUIm$ z|J*@9X9OumQVkqv5C=JsJA|~u$4!2BL#@KYRXaFL)e9t-z7Mew7d#8Mc;O(q?Mje3ii_Gm402lz%va&gW;Ao=!@(;Yhc=Ee?$|qMY$&-s*9EOW zTFPx!SZx)3KIP6Xh`RQIQYNKC-&kw26~gnk-z2mT!gCVI##bm)Lgyxz4VU-8{8%Hz z^c>7)*>#!6D6GxZrW;lNPPAD`wXL3pSgY`?8@l#zmBxJ(M{ijU+ig$PWZKk(m!?RT z*R($q-&E={2}Qym3xzx7d&@Eh7V;Evfj3BBU{ekQmjum=P3J~s6cyTZS?_b7KywN7 zJ^?EMh^k+vojr~q+k?M%#CoLy4cMKcyNAzZY`n|v!#pcdi_0{Sv7EpN9j0&)6pd!` zBF58eU8-d`CMM>jLoV0d3T%F5EG-1Jdd+A2ZnVV z=KAyjj1ujFFuV{K_^0(3DZPvK%QY2o(Lcv5my;-SMInqsN9D%EdrqERfrLQEgiAZ1 zohew)cIm&{b+Ic+fkL?(i62AS(D*ooxUg^uF|*jxnv-O;k~pxUFGheP9`#?v%spKk zAsRR^8%hpX9iS!633q{O)k-hps5L2%mVaVOi&Mpii}I#9OzMrge_fG`AX#03Yhx%f zN?hQ; zJCbN3lI}S@@QVN@^v5$lwE{CYKsmB@Hnl!|!^0`W{=rl9;65n}nB-Ct`qtqRyj1f< zXkhRYt*lu3kz#N#$h_&i()l;x#fsl>!5`i`Ab_7rk9O^^54d7>!8xhV@q9*JQrf4tJ{8pD#1`)|q52lvKmMW&0J)Vf`e2CEA4gs9+Ri(Tem3V8mJc z3iRIYyseD)Npf+E{oNmnk}BaJpSHh<5HL-Xg+ZXjs@QWxYhrJId11?4BTF}SaG0fI zy!dt|+sa;juPKzxz}h)`mF{-V$mOd#Gpq4i=lSexHAt>uiyzFrI>nv`L3v{y+@2?x zhr|PS_U#@KmSt<5?ts~>}}bYzNMTf8g@eZqfmt`-4IZQNZRZc*<&Z!MS3`_zgO$08f@|M(s8 z!7G$-(Vb}u2E{feZ?{%qL0BZ83^u50mL4+Tv6<@azb==gy<`ByqYX3)1HDhy=wKHK~L*nW|be_if26s=dYJk91}T7#-eV|VjL|k=CFXxZQx3abR0Uz)16b_ z-pPs&4gGq`oYC9zKD!9ePxU|`1NYd;I0J@-f${-X`v?J7D?&3IK}GPJR}WL!6WCGq zr{55_3@NGf8^KAWhw#9 zz@(9F0*I*o)o{%87gH)629m$$_dRAB^SHyHv#4om?fd$G&bFnV_Eh|Ex8Rr2Sn;n9y3QeDKjeuTQn_(B|+1a$5d&``TSSamn;D*8I zYYswAoC`=VCV>vD^GoW}*p%m)jrAc+=(*-%=f1W5{bH`+UEc)D7(TASV^n*aHNcUn zal7DUcCBr32HkkkQ4tW0pxx9(AM;WEpm7>RHhM=Um&txvXm zziT@`>!OjB9H7aVJ;*AN6g91A%i}rk+@<*cq~ssN_#D$t zeLO_sUVQU_WDma0elCp>kP~n3ZxV{$PEdY_#t8mOD;qlsm9Ga7RJ8MphE~_FJ=5>U5g3xtZu%zA zW{{lCoHgnP5+Iq!08#(fRza4@IUm1({C0MkIV~4~NbFZ=KZfVJ?(h#pV&p;|A6f7}<+kF1xgId8rxj@@&nbp#ZaWIS-HCE61q zh<8hXi)x_KnE;#51N#ZOr5)h6S1ZtfVdYl7dG}meTH516vO!~EBbug<)j#NeNh-6a zhX^sm711uRBM~6k@=5$D&7~aR+VBpNZV(ET1IE9cpaz-x~Zp7a>8;c?R<==~)Y8+y}!;E~>slK`-QXu9pi%@%bHhqSDT> zNi#G#127kCcI@qui&9gc1+=P-4pL~Cz#hSaz}#oy3B1{7$#^oM`aO?pFaU^%{9XKUoYDJuCav@bzJ!_(`noCf#ffk7kV_Kc z)V=|@L2R(v^8l!|;03vseK?ZbcGsGgz5mfh#{tx+7N_1%D-_r0`rJTd8yXMiW=n#U z^WVJ3Txuom-uLRxl|0AU&54LSa{-@Rht&&;Un}3ZhnyVHflyR(vLo*QZ=2{jCs2QP z01M;|DJsR)SXa}8xqrfi|KTu(T;NOkR1gQ*UvfY~$%GyA_ zJ9Td(l_@~VF8-S(zZI<*cG_G4f!GhNrFK|BWPd?{JkS)vFeWWm$xr8z$4vd|-$hS= zwVJXBlL+6+2VG@N+9d!XZvU|dFF$saY)-{5Ai>ufgqTLZ6Vg-lv&oT2#%cWoqO3E9 z(g~aIKKaX{ID)U}%>tjhp@C)~`xH6$rnAz-} z?1_g5Np;l?yOqj!=?d^FE^}Af&SUg>Y-bO-2I_--0Cf}Jo#)}006sNo#O}z%+?Tn` z$4O|7BRCH^?9HR7nJ!bWX>;To=8M$}V45>^#6r7!a}86KL81YmDky|fpO*tMa|ZU$ z>^BKeR~Qdfx<&)+g6ttX=mBit=s0NzOz1QDFB>m;ZJxF1!oZTQ=RFoVpx?0Ou@w1sa@sPoN~PcCpQT&b+*`G1OsXg6OLoT3#EEzn|iE+NfD;4_?dM3VNMuBG{8Qq%nX!Q6FMYa;a~VZ4sY-n*i%{XPC1u&pMfJYeSG(CeEmS0es!8 z2{=2G(uLx-vaMyy;=xL`kOWxpBXNQD;V(<1Yy^R={|HPdtj5T161ey#a8DoX?OBiO zHhi-w zZOet^U@D4jU~mJ)=yG%SzLZ2&<8+2-(bR}Bl zdmKNfQ7^P(ABX1cbEo8k$g4XmRtz(MrtV-tIGoxae|E!x7VUDVnt3XhfmM+6=GesA zh~Gd@M1OIrdIdwWbnd5qo9)uhk{>rS9l+1C3IE-o%(@j+`sTkg=i{n}Ls-$N&`VVc zb@sn-l(+#FPv(1ukaxiR00cjD??dtaFV+%@Z}bopfOX4OBWa)+-qRMpT{^33$u&8E z#A=2ATqhz#0)(lKcG|YQDI8Xdao?wDQzSfs1~|Rn;_0l{Arxgha7hR--R_5An(tph z#yk+-~fy$w< zf)M8sHKYohX4%kzv;n>TB_$BPYgX$m%ABOUxt!;;+3K~fkm(dAoA8wBfLxO~U>TyD z3#FpBqy%7pAV3EIwZ6^*HddcBe?+h<<#hv~sW5kWy6S+=gIEJXNNByT$@n

mYnY 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

imTG&R;Z< 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

sae2LU#P=F3jx5cL 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

2u^iJxY_I<=y}G{2$*ItfO0d z{-DeJ6}_{LY&xLWC3!6x0q|0B;)2K{%KadJ(S|by;55!y;Jie(wm3L^_O#%^ zAk-h_s@QSswDs#onq-Fw2L*k~zg!CP4`IR=aeCLs2e=B_9C&of zfKNd#R@hCl_6(7@vZCOWaJ!th=KxhGmGXwG;ylt7g?s+}_mSV83%|un%2v_>UbSZW z`}sX*5bi(2zpkXK1=Uw14=v0O`UwW&qrKI4qjUb(tE*29&%)iqV@_YkJQja%i;)Tt zExU>m3){8O{}CONoBKd<*C9$7$OZFjkSzp!G0=#iJ~5ka?kJV_zoGN&yh~9Nl9m_z zyiu+y-1quF99cYB*v`$FvfzxFc;C#|l=H(ct^sF;q#O3daHqq-X5gZk6oADoHO+)6 zLe2O(fHe7lAJ4*QQDifNnL+Z|0u=wHiiV?PX(0jP0PcnuMBWRdaI7&;s8U(3e~xeM zzCcf(voPS4TGj>)IFY#=Og{}V;#iNplwPRsHlW^%B(c@ZLzO6_Es7cQc=R?1v4BPxTrx3{cYv%{00Jg z6~~A0w1viYH5^b~))&uX5i>YEp^!D~P4iSgL3yrDzz|n?lkx9w0&&MDDB7X(Z{&0; zg_+j}5RXdbE|{(SPX+U@_b?xY_eY;=z;Aybhb(UYQ`gIeI+n+8B_TEZ$_-b4K=YfUlS{b2wk)W5v^fcDBd5<{wTzp~|Ju?@|DWH| z{&UCwZ)~6c-{04R3f0>GW8aXZJT@ZYLsuLtf2JZG!Yi11Zhko#Yi%JBeIrA@!t_$L z81`A1ql^H)KT~SY_a0fzO$=FA*yW)Z^&$ff&ET)|6U)G}Bke^3_FP3Jq9$$mi?OLS z58aUio7%#I88%KfbSjcS1sEO|(;fYGx3f|48FHNKr8b+Y__GrrO7);x{pfS~!pv-s z(Lf6&&|=B2_=O)&9^`+YGwZAwo;(bG zf)`3ajVgARVPGkjS2i?bP`(C}&Az7p=WXbA?SdNm0SQSaIPWBvPaNSAFM}!#hDxF9`+tT7-4uyABk<6^@kqupEe-cl zf{%YC7-Yz*Q3J1U0+QMK`q$tu6o)g`ou@4v!mKX|#tBab5h1wG^i#1heXw*S9jeEn zj^AZ;C>ND??ZriMIf+J%@h!|olwR^LSe<%TacPmKJCtaeZOK~>7g zG4!vREN6Ol%ob=Zr8N#N%=ofU+qK0nN~`Z0Msi2;`)p(t4x*!O9pE&K^oZnyTe%34 zx=nt}vZD{b6|-tJ^AcMz5EDPa3n-Lo-kRGh>i_-qH_n7Vr_%1u@6viB`j}3kq`VD* zc;^>K-m4EBav!+rbqD{p&mJN z9gFcl#x{3%2jR5R-+4%I`?hhaE9S6&9QzqM!;$D)j=k9VjyV2qp;(bgY3Z;WVg$74 z*i=tIE19gh7Fe%d-2_lqVe_epsJ{9QC0f6Ao;FiGl zr~v5EKd{Y^%Qj8TRCp$~$qHrek(~9i7*|8 z$HPd`;om+Jw=}lTG(E<+ibz+{0E|Qfl#`Xl(O?hQJ6UxKKjV)$;lVzn+R)s;*Ye1G z*w~WUL%)&TfHpq0EwC;D@Xt95!f2%Ku#&3ixH>?YAP0CM6R*LiAewDgQ37N0+Peb~WRM<8W1CnI4bZ+*T;{#ppRyD#!n;z0Kq^4C-`q-y zcw8F5DOiFAi}?4c?jLL>IvzIZ%fWdJmweL;lva1Z!V0Ph!`WsVYzw-ImK3HhgBggo z6?fa8g=*>hI9F0$su4$W=bbOt?p%3eM`F7^o&ra-l(Ozsv&ux#(99I4 z=u%EMLUSupBm5;L;EJ)35cs^bPfVy`+d08-Ab>-gEt$SW6>lbq0qhUNFXrgv|Jvq9 ztf|h=D;{NnLH=50xQ1i%r6ryd^cczL{VBaVo6uPcwJ*N)oVZNq7u{R0UAa<9V`j#{ z5%XK-dDhMz*8HYShP<8J|59r}V(<1O31Cm`V0P;j#%_P*_v{qW5*=%9S5}Ox{1p z!M*ul^}cW-ton|7ZVYV={bB`rlKn8MLFH8vFF1ADPt^&d1nm<(Uto58AS4|q{Y3Sk z=Fof+c~R#?n+z)FKuQn8v7s(QSCyw+NZ%VHAQo+Q-Vfy_ci5bFwY0JdXD*%WY4XEX z;8;I4Xy(y#DQ<49g@PDBnm!nn5`2_Kw79*D1myKb(WiV=1lcCwr z+-92bP#aM54oNTqnO;iKxe8#+gF_}-xuuWK$i z94s}3t1AN5Po1q;qRtP8TbUCLH)*VGGokugI4$y*N)r9|dx^Y*HW4-?HL-8*X!ALO5Fe)>=l@G)7C zoEi6osa4p5h_2f&%`iXsaWI4PNS|POj@8ToBWkov4f@AgPS>N=38B5M>8Ytfit|Rt z^onBIJEe+4?(V)8Mmj|&3&PvCv3u4+MFyho(3S98QGg9Vu5{{!e1P?f+fK){s`?J( z$ZA0S+?4Xg>MqeqS?ih=ftCM<`)B$*S2M&L?eLk-EV8uX#NgHU-h22_ZobFuhrY?% zU+GB>*f53qi*6rIF;z03zlNM!vD@RLw?&Wpejkxynfg@JpNN{pg+-oLp)fc4w82-)#(6cVW~_etnnD z7-Q`iLKn$E8^C4+>8g058Xxd4iO{UddlMc=lkE`bd5ESk$aSMjNBfyTd01?m&YDzZ=4RE8 zZoBg|6IMw6z45+*IL5^i@_?iEg?83hT*=Gv3O_!583d(c2J(BLgfz$U;-+$qYj#3H z!WvDAEsASI9m%I$nln>=jUBJs;2RMKxj50Hd!*;bGcvlE??hg->{Mu$p&J%PLzQ1l ztBSn3TuMyF^jt6_o`%Im$>52da6QSPt6SsSrdVFHnyE?`=R1yj;^mUccoD&q zxA=`UhasIC{I+tg;c{i#7=aDbE3=h7Q@ae<7pSv0mX{%oXMCq4DIQ=Mq={s)g30Jp zFlK?Ulp>Mj+XW}fE#!qxh;ALXh}#S- z;{T*TVso0@GLuNzY80DM;0k%LQbEcXgY`Do*DNPe<{+Yn?>*F_@IOO%YQp);omXFn zc;BJRcws}Frk$Fz!(tZ*D>F*l#2ZDlK=F_j8;-Ti>txH-)wI3qOn;kUMf_Ds&cu3% zSzP6wGb(^rZ{(agzeiQ>O9y9E`Q+(>18dVX6?(^=OwbO3SVm8Jpt=BYYO%WGp8iUPQuzsqrQxQ&vZ1i^ZvTp z-Y2sOq|pxACwJ6p07j5Y;EJ)kk9146Kl_1xMm!^2?=+4-V%y8ubYy4xxPgF_G)b?d zD5yrml}`1*lZR~gA8f~21iz&NT;iQv!B$z_DF)I3`Bjoqrn4n-jutN=>TC!Q@}&-c zSJd|_pgF$GeEICzBnXw~MR_v*GDcn!;2}o-aIBFp^#2uMauhV6 z(m2P#eCq|ClvFhThM{S78>Y2A{r?vmwlG$w&1w~L|-nl8l4t*_&wlw zT17tB)(2FpC?_H?$bf|=Lfy5J!Z8s}t2g(H&1bIG@x%qAO}HpVI!3Z!^{rAbYe1d47yp34$9t@PqO{XjV%j*%YE zbIC+0!5OsVscDO91p4^`3D_xV=$`8siQ_9h^q(!da_vPraKa-S$pCbMg_6HF7r8!z zOqWQzMrJrx?Tp&L6Lb7$8M}NrzL-5$oB=Tqp)Rk zVk940VoTtV8Qf+S!@=I`J>1!FzIdc|hU$i3&FDsR zrir3*=f%jYGttq(3sm#TSd1^8+IMhbpXa@k#bc(bqE3myI`^ZoZ7x}Rh6mrj!JC(e zWo)i%*xBvtm`wiEj0M;0IBUspP|^*3XJo&HbT;}*X0V6sS)p~=+Po6_ge(oAPVzu1 z_d2giFs81u2JqcUIl_;v&Z>$~ibY@lyWyqyZr63b!dxKKH;rm;Y<$Emj^_;{xiJPG z2-A{{E+&(Q9QZDbOF3PLE`TwgW(sy;sm}OjNOa*jC(59#ca3q zA{cU@cmvDZniQ${7~|^R^nO&Eq%wYZWON->AGG$@lHz8W^~B}7qKblJ3yG<+zZ&cZ zOPw;%o-?3uT?jy*;JtKIk|N#yV!2<1H$s!Jyc}G50wrz+&@ACDc_i6m)^D*Lgf8YP z)z07JK%kDUpa@TWC33&}lbMxhVAHijn6cC@Ai=5fnPFp^nuIFl)`8dwrbkH%_nnTN zU-v*`N`mwJ)~su8jjo1g^eTt?_3aTZ+nuj)9ya>@?bcpW%g(1XpiTk(Es1kke+)vn zx4~4Hkm+Ngp(8cEA>N?{v#7F79mkENW}JOiH#sI%*KShSC>kxT0Ey3IB zfW1C4#(dg*JQJdQq52&zaxBwINJQcGibYOOc@esK9FjqCSy`25zfpzv zSPlIc;~F)+mqJKVi(Mql@5l5%U^eH8V8SjDlowRTKVou|IFQbz>t*{7f$Zw$Vdo?a zAm_>uCdZg@EGLVb1^7g^CbUh5-j)@acqZlB4Uqd}97{BVcZ27RQ3rXMR#t-u20<&P z4<7Pr?RUZ?P#x*^n?ZX*KQy;YqtZ0yGiWy&3TJ0TH#~2U{S1`fekkRYo0}_ohatVe zlc8|Jc|-s#!(bQ-cAS#_PZiU_=>JqC$_|{toa&DoB6{}%iGwkX0@Sfe+jp}IAuSM_ zEagYcE_QSRMEetxT?rHW!o$Nz)hynVgQ{#tWa`HC4CIu;mOoObWz#VN`49UMUVh*S z`t(LeO~h542xmVk!m0CkJW@g1Un7L7 z2CuAk8R4%sM?_~*33dcExs67LdWF$r`Z08+EK+4xsUYp*uq>(U#|Ky8j(g2b z=wGTI&tX4{7Z?9M7SYZ+|8AyyFf}7?=J)9=p@$Z63cCOia>$va=4??Fqk z3rgW28Si1#+_tP^1Y6`4U1j#LuA9T$r)!cg7^TMbl{erq=^ICUHH4OSzdd=k+c|0Q zcRv1N;?eyWBaH?L0ltz zMB8&Uo{KPSVIRkI{mvl`9e6b{@T9R>h)a|6(ZwB_A|n z^!gLZ$hR?_mM7}|!gWVPH(7D2h#_@uU0t0BcMyJwSw=>x4U_Y7&+u=sDQs}0=@?TFrSPYU)A_qy7KeH}glzZTU(1eUVbmijE%Vpb^YZh`lRj-k ze>I;#UgX$&cNr5sqluj4PFH`qF`hRedHq6C<#;F$A(dOP5@Jr>rEovEZA(3x#KAu( z62(;I3;cNQ%FA@1B%;u(J<2es&8F3+FYP+ig&78Op~)G9ysBbd+27h>YH-&1xwd%= z&azC_2Mfr}qZ@qqJca29V3YnTZdn>EbF+8e_&y*3#oKGu-Wp3yc{ZyHcQlMVrX$}> zCO0_x!i+unfH*j{XAW0t?V*-qM8prpvEt-PJXuwU==mZ-9fvhOYbfp{KM0ASnT6q% z+zgT}Qk@~?N=KZVQ+Lup^u!(2vUiZ#f)d$G-e&Os7(kab8zb zvCFXnYfZL|H{lIEj@ZN3>>6tz5{Q0ZE8$4W{Hcwo0y1EWf2tYASAn1M)+mm?x(~**OlqN%c{K%2YKhcaaJpvtIHkJgVoxE zDl{#3nwY9M>5E+_l%ZEYL0_XYix^0Ppn?qRWj=``2edBttIle-=of7Z*H=e(w+v^Y zr!fT9RSZsDkd16i-vw3Uu)~7SpqwJRiN^kZRL4*}k4b(toLjrmWBpAy6HIQEEq*6M zaRcg|0$gsSr&YIa&tNB+NvINovB|rhj9)70Coow^{b7cmZu-MsfJ&QIWEWx3R@Jua zmA{-G1QIEK*qS*7{eLtGk{}}3Eg0G5ES;;GH*nX`V4!qacQrD85i3UwDb>$CE-kYU zzTZ-)dK4Rc*9x6_%BF;L*$+ik*)H4ao$yTDJupI4Z_5{*Wk+tD%g5xg5nf-|h&$wp z>Fav3&F%tSV`HMz3y*9kMr(}CLb=NK z%Z=^d78{!yj%37Qt!$Kw4Kr(Nx4gam2S7YuWYO>fY!!s_CmF!raVV^dWT)4&VKB=< z4BIIKHFlw9!wYN@G6W|HKNbruUX4O)R*`Qc!YNGd6roXg*m8}4UTa%6iotk0c?+hM z)Fk(Z)3?b+f4atAs@nEAsegk1PI7v&+u-D=sJKKFlx8e;-JLob5q5i(Eqi@LF!O0^oMsizPc|;8&FtOo54e}Y5#yk0RFPxt8g<|xCBnz|P$gz?{0-^W z>E8PJ%H{&U^9M*#iVI%SM2`l%l^?Rv6&VF&BLdNC9!|5k{jzxOd9i`)u`^BHgo>1Y zC5NW}cnT|7uf!B;%3}N<38}z2+IwH!F;DHmEQAUzoj_}L1h_|_bykf z%KI6KHgWqk-^;swjrPyQe)89fay#EkR#B%+o?PiAiHpmvMIeMt?ovbwoU1$CIp$+; z62S*#%J#OD(X1cKRM)|Z%3y(A{pr2p1LL=Z0-Te>3{;u*tc*k=6p@&D%lYXF8)n;$ zEm@L4d;5^OpVNuc^8I5|hg-vGjQCR}vr>RIy`kfx?EmuUU1$+UQF?I{6rS46ipcj9 zQSt|?Q?BEcHs=Lny%P$ZhzLGVt24`enz$OQmnO}4mii=5+mpd~YvjAuV=3_n4*uN` z(JA6=yB{{@=CzG&%<>nf1s63r56mmupCNKwqY2oeJbm@I%uY|!}$*( zLz)Va#lzEJX1z55+beUwwm%q(mY2I6{93Y^TP}G2iudL%CV9;2h*h2`YUOjFtqS{( zZ~PBSOUpHveoGs=Q|4#YTx3F8(Iqkq`=Vbeuy7;-6``%XdD>@es2weH|`it}vz2y=ri6m7b zQ9xfNrF=tVB7V*7Kg`Q+%=J25c%-&vM;kyLr+gouE})6X#W(Bxs(hAWpsuz!ztTUO z@6R>6D^Vk4yKAVg|FrEA%~GkjwoUj<-bg%X_gnM($fKu(YTAS@WsPluD)imO#l@=Z zabMxqdSNIN{(44ULET32;Y4|DGM|S*`B>KRpX7pCRYwCa?0?F?z6wJLfv|w+Yr%rQ z!f+Uih6uq<1a6N5cArpCP;|81IGcZEG)k|xw+F#O3E!chNC?rcf*h;XEepJ}f4=ee zkEgXn=iqn!=ce$H>|BUueZ8rk-EGT|pFdT~CCcC)JN?Q+S7p|NbmO(P?I+tt=R=&) zOTCl=g4_bm&Xsj1GUVQVeuDM~YBrgHixXv5TqH80Lo}&#!68~BLP~ApTb_)a%<1r` z+HWe-U!L8bCZEl9KmKXapj_FX`ShIRk^Yf|R2+vtL(M&}vnNiP!{6}obT-e;T?l8m ztl6j^KLyr9Be8ejSy+dz5*u_Lx=xZhqkEkW-t&=*N20$Nc9ywtiH9EZ^O8mAE(cq; zF`n2SWFSGF|Eb3VSLprJmuF(`tg*9Bw{EQ*$FbYpunaP(R(WpNcvInAn;}t0%gB8F zmx7h@P1E@G6~`YZ8~qg|ldA~)cvm#fC--wUJ@M>zYEC&_^bypGGgJRmt+*bayU4U#D)a6Xuo)Qc+#<(8UjX~PNY~B~ zOS^rr-CciIY<&y(Q!n7~_3eV-Rqm6^F(ep~o!O^!go|G*gXq8C-vV{{x%po`{#bPr=iOx}#4EcaejBJ8lk11K9`6sD;4+wjhuF-Qo_!#{apQTS z5s6^3&>GI5aW^R_3s;+%qW>PAGCMty!14ktnZsj**mVP!yfyTxtpEP+>GOkDvau@&uooPgPpG4ycMVvwkk6OW+WMw&j79pEn~VS=j!k8 z-CoSH;CNSiu73MnB9tODs-<6_*wJeDM&xb=s)B$?9FW)^`I#`}e)F%S8cDR6(Mo|F8pnTU9= zofQ5XUB*0I>A?MoUQNImjUVy^b;E4q#RpIx1f`omBmjy@V3FRezV64jx;)(~ z;2T!HuST0?E(;NM$IFvwfMT48QpgX%yz1x?8KytLD$fmRIx!2rB{HXiaCl)nnAOY(#_+lzKcy}{y~k`C zV=ePvS=dM z&>OawRPU}TWY)wQ^e;~BM4nmO8cP}a89I{YehE+34d2q2SiNX!!%eQqs=WP4VCZ0- zOpwNBmV+#f{mb2N?&IJ};~s!FfUS=0MnvwLayK}X%+&spP*v!dZs zQ({jjofL9{<^Vpy)1DZW|M2U#e9>M6%Ws%V->qTLz;Qd(zQ<|!a%)ax>CQKtwo(2wuc%3pnJV4eyg#Mu7Cwpl>z+?e>4ojHqoXDDn2{c)Eh}{C zEt;97uk53?pA&=eF+6C(yYvUQz=dh)7vm3O!n0>F5zI!HL|-?ses&F}}+9PZgfMYAwqAFqwcGqo#WEsE3zk-_l{?*wIm; zuD1ry>dYl28?tAQ4+V4DWDtS3;&ZEq#0PWNPe}fO@?2}D^~XI{$IA5=V<{3&q3nnp zC9m?`+d@x$An!WEQL0DX1vgCh+okVdHIZH?yNbwZ9?WF2eWc_z7Ie(HfuW_;PM)8?aDM4Ggiod#H@3@Ymcwb_A2;+n9< zQtkq>n&qFUK)dfis6m(+tw}Rgs}O1PScl+&?{GpB|onnh>Hc{IOWJT=iR2 z$%ka(y`n(czJ40A8?w{^4Gi0-p0u+1%R;+jqodR9q3^9xdu%Uk*2CtuKYy8M^hOfS zqSRgFK4t|@$}CsKGdaYFW@22==9nfA5PuA3pmvsmWq&~~XEnU-bqbzhwgP3_>Y6cX zAVr(lKAS()yw9y?e) z->(=suw9qJYTw)YH}FV(852D+Di4>x-2&gE`cH?a{PT(#R3ntA?%3z!tZ)2tTw7ZY%Gi14H|He<>^Wk*sgfdTD4(Z1Wb)g50CkvX zcx&rtAqU~Ak1~|90XtmhSu8cv-uB`{8-q0>NV%h;SlLR$k#_e~EcK%}FSq%7Gw*&F zRx5LGaHK_E^h!-{KjC)VlO_xXqGM@2d=d;bs0%lKjVw8dYS)PK2YuwUAlB9UWNl44 zY!)HJS?rtQu^w*u^rAPOyO@gAhI}`I@>Yf%hR&#S2WNg9PpjLCFfEoub;2@Q^*a6l-cK2mN_3v{#GtDu7 z{;VqNC*(z7VePmVX+M$T+w|}1NpvoRKB!kmr^=Utv6w1}w$G}h3vpR0t}zpt>*tv+YLs zxuLYNM0obEpk!udK=dbQCMx=cHH}(2=d;9DLtxk7`^U!zZ)O8oUbfx-G$Tm4T+45MBdT%8~!JxQ-K+d7%+ClTZm5YX|T zcT|obZVJ9Gg76C4wXHeU3wyGmJ0YXQD>w8z6dBHNOxM)n8xSC6i!WvrF1^T6low{Z zy7b`%Pl%9^h>E>EhiHhsh@D%tb6bn_=ox%j_ZMHYXvw478SFO3d;L^xZRkRcx#<`q z^Q!bq;+0#soBiIJ~sKnQm0rn=`V)yo_oP3b}Q!|7K*f z{kLIkZPY)U#0cg{Y;D#YgC9+G%+5YE5)jps95z9xA1SG87JudVaDL7sK#jjduQtF9?q~ukxuSp?n__8S}_)6}DbF&Few}o~I z>17_t??w&}zk7zBzZ^ol&15=W>YprjSF84zJo!aez|71*(CbmP6YaxlF#1xYNhvI?)MDGwdy*QxPWIB}=<~ek-5Bf>H#JQjL`5>6 z6^du*3GEV&ja8H7b~)KA=+*mjJ=Kg%l|rn$g`PggkoHo#6%nO-nJdQNJ{%~C$4`&9 zkON=4U%7iu#qyl|wCj;90=K}Es{pNeFsGgu_aDW1BjH(Y)&^31-Dfp<#1ar>2wjPPiwEACx-v`BWeX+%uw@Jzw2 zA3sjN+eGgPZRh$X(aU{>16jclHWNVg1BFz#!cB zef&i1rL@6Z+$*kXvMH$P`g5E1a6|qOttOw$Za+ArPfbhT1y>1Xdp|bO=7KFBc3ikv z+r8$GeA-p9g=56h2Y1oj=Yx^tge0laLRk{*VYt*#YkfI5pw6&ZS{Zy#`m&U~n2pVT zIXUR7(_M{6mGs?HE-Xx=t<6JsA=FH4+wT2xpQV4%u;gi8VWTpX?*T;p)LP>aJ%fDL z9Rfha5yM095k{rh?5Y(P7RH_#ro&^=?&bcoG|OuV{!%*Gha8U&lOY)(V^e=&N94m$ zWwP?5{MPN6z-wL&h^}LU-K9T>H zR>SFkA)MTmMZo6H8nWgxXi(($!|74~{AduW%hc=Z$}F7yP~ugz@#|t-ocvUQ1Gn?( zxbIh|Tjn+v?sh20rSas7x^k_)ej);bFPHJiA00dB&T5a0IQKUf4KJ)F@)W3~_pO|O zcq;RpmY?cD(l-C6v+Cde)*XbQWir)bO$uSYzXZ6CM+SB@uO5BL^2;RK4}M7byCq|O zxZ31;Lc;y}><}(uR#54T+BGXOQ?If;WOhE;))o>LoQ8JL@4XzOx69vm;B2pxyzjQ?pW$KFA9v#+q)7N+>d*G0z3;8Yw*POQ*znn5#0B6q`IAVg<-YBXD zpF*s)g|>}htI{i2Uu!$XhJ(Ny^umYf$4Rct4lB<}%gdL;4h})~FyOT7V)yY?Rrc3J z%JQla{;sIWQq5RXz7T6tXKp9@p=`Bmy}QN}NURmzWUR5IjR{y@)gZ$occhssi4k&`Vob<4N7E(fc{_rHYs-`1QJ zu7W0m!*hhC+7kkJi|P)p_H$HCzV69mtf4<5s<1U}i7|71OCU~Rq`7{`!NYT3!}mm- zwYD35#Kp?zOOqIB#G?uA$U4<+Pe$L%vH?!8h_=zVD)9&lMU7X-7Ln}D$uT91=n0lq zgujX{$Ia*Bglhe+T24pbU64{MFke%$DDJc}?i_p?p4V?y5#`X{POd~^)ziX6!SZO1 zc+xqOkn226e}wj*`)MNmb3gqau+BWxF>z6HTWj9^wI(U*Z@ws5-&DBacIkms++xQS z=u}3Yw|a^&n=JK5AEJ7XV_l)USDlOLKv^F=F12+Ri>{@oe**HjoqwzO;X6eZo?fG>&UWD?S-((nA+Rb z%wh*P0z*-kJ2_E0>YXUFuJJyxg|6F}$!F8dI;^%=V8j2*fxrSF!WH;-O@d@z|aj!NH<7>bV&~lf^;Y$sC0Lyl!($@Lk~SPXaC-H z)_1=5A2_oXvxdF*diJw+Joj~95u>G{h>Jymg@S^DtE>doK|y(eKtXx#3PK0AY=B<< zw?p)Uyuk-uSKALh79KVz>J}f|oLoORIatzp+jw|7xVi{%i*gHc(%FCb;N~gD!{hva z4&Zk6u;ZagUaE@Z{)@t|?C zGoWA{k17yxzT|S8bu}Mw)DV1dr;bqqe8}k|z_OtK^Hq$~a&`6W|NH;HUkns6$p7mQ z5Ir=O{?zn;9izm@w1WMA2KjFndsKhG|JuMXknGFzzt2TRh=}~ZPYxrN+vxbeH-J(s zmD)-F*SU3Y{(qf}Wm26dT%*oyJ*;`ssKzpqT516pVq~8Jlmnsc`@Cp;2hEQ#L}){6S+N_k<;Kfo85uA>a{tTAnGqQ(AjgbH+`hZ zfb@q?8H$)r{zz-J@qS% z9i%hvxX5S94XtjkjvSp425Na?131)Tg6o?E0a0&%>dpHJ;|$)>hTK~W1%nSmeFOWV zaBW@tYVL2~x0zO%H=#p)otoY~@OI=%fbGs$5@9fIfc}*cqFsPnF-aZdZtku{Ax=ym z!8>?=y{U6PDV?LX((b2zywox-gTZ_yW}9-f*tDpAb2>z8ry;htl{r};?EIqz7)||P z##t@yC6{bL^xS^s0bezMpNa*-$*K^oqpQT+z-0Qkpjsn-njICd%x)4zqfV zWofpi^n>6!(yYlP&#=)cOLPOqIA#$|ZHp&$y%u3Mdb3b#QzukEn$BhCk*$#@mNjb{ z@NdaoQF6LS5#Lj*kQDip#vK$TbRY6~lTmDTP-4>NZ9A6PG+w4%G#*K6O6G&=Kf=5{ z_Ez7-StM4rm~e~|RJC*$P358VZ52ZR=WBG!T3JzI?Rgb!e6KWvUy(Mb{^qc5(sSzp z1E0oP?8C14uZ*sxcrrc))mf2vE~CcGpOrtHCZz71&d)}xtQ|}fUOXRW&M>z8^|`Ho zqDb^o^mKRm&hQ)o&Eo0GP31yP{;cYJkB(0R%qq@V;OcOGS^{Zo zc0M7#IX*yYmV07Hw}jmPH{E`-qvW~$Ssz_0`tCHAgS#ngXNWe05Gh3*keDsxDvV9a zEm&t4;($f+DsA1J$`-lg*|axTQ{?bx>k%iZ%Cuv__=7gNZjGh9fMq}B*Smh;x$jBi zF$zorZhxL%0y55iN;CI#x!;x+|%`|f>XW9+8?eK zcQ)ciS-b*eK?Kl#{+F^Kt8y)=_isGVlM5#D+A{Bs+jw?7qi`v0L-Z@&30;W7EEyFO z1^@ljPa-@fXc2Z^uoiKDf&DW6M%3n4S`Fp3%W^(1@V3d0l|h?#3**%I$MVEymx7Qc zEF5&Q<{&r|G?;G5Ea>vz&IoJ4*kAO_1eA%aa+m!1UHZ$5HPo%ZvI(HZ0tt&AXKzpTh}t3&$f-gCM{mGpq>wYfoOpy91moA*)U0e&Ix zhCDx>kuP998cI<#f^!t;C%lW611+&5QZTG*v;wo)rem*oYQVK?(+?G$p} zt2%}^qKOjx49C|)BfC>Y>OiGX8%@VAb^Vc9I|RO zR3{R73S7d0#Fp(*{lfj=pWSYO@V?v*R}%HXF*@6-*jx){~O zUq&bRkR?f8WjZWe^l%2PFecSET7u@aU=~?1CqtoX$_Y^4jTqKLO$n)oi@J+bbtbsF z$TAsH2=c--L{dU-UwWPGzIA&%Ad>`Q8g$uk(M8WB>@?TCH6Bp*-3aN;QA_1#&2*N3 zw{tmbCIYO@Ld>cPi$t!SH&GA&E5RNSC_M!ajhU zCHdt_SPjMtt%h_BFW}8V=Ct*V-AfV}R`qMW2}LJFRs*LwP21-xl~;qBeoLG}_3un# zuoBL&|dHE!`3XqPTk&#|o&jU{IP zbe!f@7y_Piizn(z==blI;&uw&GMKOGZw*=MNY3&@G%oIE*G-6xPR!y`EIH=;?PUFq z=S3Qhh+(=JtdTXJPu%WIDJ}vPkTUJU9g9AK6aY}L54>r-k4kZB>HmI)GH$RAja!>BhQ4jghBzI_7u_R0D*spP@K&Qef zHQk*Pc+W1A<<6iFS1!Uahi3}w)iJO@?|sZB!vRdgN2UDgE=0xVypeAb>)Wa>%AXJ^ z3ZeIDCCJ{J7~cX`O*i6&(C6^bGw|43r{TUz$zJAj)RKQ;=J`GZo46E$^*I*?)yFk{ zrGL>TGT{XwQoI|89M{eo4d!3f%fWZlUlBugx_Sw_y21|@tKlH!+)=aOn-Z^!!Cx#^ z%O0M^`dgb11g*oj&XW}HwV?n>ja_%F+CpwTnZC+MM*3fjNa-f; z{@?FK-*rl28&JvHut|gMj=Q>IfTg~WE0P!uo)jRW4e`0}5BFjd*pS>z^6Rjlg8e4n zaYHHe6!+Svx~8JSp+fb{A{``S{s`8}8$VD<{OEc1u`cN8F`$V3?fwVda)X)+WY5ar z)BxO*>Tj)LS=ap;hZ8$BzlX5X^=N(wL;Gp~A}EJIbr83ti%neiJGE2+4%H4nvI9DC z4q)O18m`}Uc1bu%s~@J-y^rq^o!FgEzDo~ir^`O8^R&6~e?5IXrzi5n|2&Q7$4O#1 zg0^#(I{Uiv3Dby12?qFhOWT58gC!=;`HB4S;kllbxX;_R(-(w?WBC1!4MbfK*@WJv z)zsAjk2i2c_oo_t6R*$9r~tbAcZIjPgh>yzO#wO#*z?U!*+?wWvlX&iM#WAC5la`j zQI=;tzsQxPAFqpA5Sd+e1W94{)LSnjf?JPD?b~VBp$cNnn;c+p}>}=e# zY@rYXZC1SttOn-!cx`cMoWek*3|mhnY1H?ktg%E1MBmlNyuOAm19-oV>a@Mdjgq3v zfaVI>;Uy6e_eMM)dTb(G0~H%2bS#3Whg1-qW?Cg;22G;kLS1X!2tzq5p%JYHQy;j+ zW6B_CzcFZhyXzg1_U7pX>f?yk0}XLaJZdpcThaK>9QwsoMeX_wRe6o^CTQ;Ntfc^y_tDO(toesah$n4s!dQM7) zh9Cea*Xo;;`qOF%Z$<`QOM@?d=tg-G_9f;hE-z9S0YG4WRE*9{(_e&&gy#e)14W8S zwVqrPExycL@>e}%&j0o;+7x&(l2141Y8d&0$&ru&5tn#D#iy;*r0F`%`x7baVy(FR za%L4~QMRs;%WWDPwbq6($ ziu^RMJPt=PiE^7KJln}x0;PtaH%-uAe^2sVJ1!e1a6Q_nytCs!rLLB^E$JaM1K94nQ7Xz~zFqsAo)Z>J@bci&JMC(T^*)VJ<$&Zk8( zEKZj1H##@Zamo21 zZ(tju5~MIfHT0$tpfhue29eW)h&G41tfhzN|COgsE9o!PVK(m4$+U%mZdKTvK!Zk*=xMHsOjjiITh%MKttg~r^cD{(yoEt~f&~v^}mYf$@TAhqtBYEOQAJtGba>qoL+`?lN znOc)trzwljFjelf^w4VJMW>{Lkcac5m400B>g$Dqtz^dUII7Cc8BtRmWR)F=*i3HK zC>>(JBdrIMmA8*~h$BRe)ll|)U03xy3KV7451z*&khr%qo--d6WwEjC+Eu~NT0li} znwy%8`O#O?^9tBCEa+qU~7T)`!#Qzp-w?d_v3YEomjPLkWWmy!dq<$rHq1|(LR&^Bp| z?Tpjbp8J4dNXWxdyM`G-SbCUwE~i0tw?a(n^5&HpM0GD_(0~Op%AMXKK7*(|T8@1z*tz-rHRdnq6jH@Nzd< zCOfdiasP4dN5mj9{vR0`ABr4Fv_f)s5`D<|xX>SY<-%qAALL9w{8iujs$QrSNR4B; z2u3sNjA25cac5aTBkekdI0bkfXGt5KZqW%~MVrKWdwRDz*`;t9x`NgSF3v|bv{<(bh)o4+6SQj)p2n#tg$TV(1TW`bLh|Hr#7u73d+BD&TWs9SaEV# z*HQ6h5^@@h|7)9W#r@%6w#qJ23OZ`Q{H#W@Ahq$7p<~ZOqJEH8Vw7+aTxPM7IUX%E zB>3a0J9YdHpv$xy5673XrwJ0&k&@q%h<|ZRdF!Wr4136#P4v+|fs~Q4rnP^6cj<4W zFfHB{@U>reJiDR1ZoGovBjYE|o;$>3bGkJt#B=0_ZTaSF%Si(aFRO8o}xMlzS zNqU3=mW4t8gGNfxgT_vD4xqt7@u~3wpXnsGgXn~w2jr^j=yLL)2Ch0omA%Rc983*! z^Vm!fSzr?)b&d>GelI}Tip|~JJ(eRfDcm1NTG8n=&(C43%hKKUz3uV#z?p)tS3-eu zC_zTyeOXsJcMi+zX4Vak!vMJ~e$p2%UwmB|MtW6dP9Q?6p3igXVJl90RwappKQYu) zgl17NuoQTPRH6FG&nA6k*3L3kvqU|=0W5~n8hWzh9w~xgCq}p!{byS~bdFE>$D5V~ zF>5J+STG6~)rLW>iPYHRL{@wS`Zi&lQ1CjF|I-8hb4JM+Uex#c{x4omTml4${iH}3 z!4tRfHB|S<8|9qPZd)tO-b_kC`{XQug_QJxz+kdJ-h8j+>H@#aBE#NaI?JaGhVW6#Rv; z36>(UsBK2s8S(`RQZU0oYPRxZJng){)0gN+QF^lY4yC2zZ3a=vA@Qp$>e&Vs}iECco7-MFtj`^Dm>6V4U1unm)zu&sad zinu3!Vpk_aM{-H89S1m3G3=$Tj|r&SWs20IQb{J~k-wj-1@LNhz%z!itn+F+18lZ} z{$SKnhJ-M3BN<@{@-~QYwZ5T1%T+o4nDbnag|`T~C;%AE^WId}liF0XX})Q@;!;ka zADsC34An_t-_TB8h5SWI*3J0*Ii3oGNx+ep@M7FU4pPeM*sDW%X*Dyt=0HPQ^3>z~ znBj^awjD3I;>sVNv4{I+QM6MUIGT4^I0#v-qrX%&R|i4QjLQ$c-OFS?sh$41*y72W zL&6A%F)4=s!#V8%+_8UMt94N$Z&Z`>BE=jCQ#+K+*SwD8yG++~de7cMgQNe0UOXF5 zF_x~`dA8#ZgYf8t5QB4l9#jkk;m6?6n{3BQp{6B|pB4C{cXTM0&2{VMjN3xlFcjFZQVt&`} zg?kJ1U0PcuLaqS%^IZ=+zb%7TX=oW`p~ZZ4Ev99-+(mIy1Uh8}^k7r`$J?K_0|8V7 zkM>fp>D)`G$ud^NwvGdzTU{R3F|xzCe||ipCg|y`CLx>$RkK@-F?hS1X)9Q0c1L|` zUCPz$ce!%NS?9{^`2gL%Dwe?B zCWmS?$XkL5exI39`%aPi6b0+$ShhWQi*wr`;Bw#Lg_X*@TfQ+d1Co(J%!V6W=9U`( zm!0YUa+eIV%gxV4EV@=X3Y5CN-(<7a7{J7M^b?3zO6^%=^Ke3~0z08qS912pqD9Dl+Y?&Xu@K~p_VHVG?F%PB0z zA&+0VE8vR>$FTV9*(1s8e_i}>fym}RCq0Y=K^NY{?kJs3he2ZeN++$bM1XGoGyy1*ZM zDmSHY{kd_AEFNy91^%J^HG9QDvVJ$M`wRnwB!-{Wj&j#MzX#YEM}$6A`0>RxHBifH ziFVJQL9`gQ-~Ty$h&>P78cvycJQv{|z+)(+=5Ah-ZBx+Q40^ox6W+v*W;i$EZ{#}1 zI;VGhB2e6_3lr49P|92GjUMiY%17ZdB%*VXZ)#~6cd z;BwcrCyNOgwXWO!s`9!;ap^5INc%h`c$gsqoFxy)pBND6bcNx08;R{4rwxV^7%UJI z(OcI4dXTN;ibuQ9X|SsE_LY&HKzdosn(xzt`e77k9SLyeja>xWeZsYWJ&I`<2skSx zl6?C5S?u@!vNy_*P8n!Kls@~>WV%%-zdEmOT|S=d_&)7(Hqwy9wb!6cn9y*if;JN;L11i?Ga9gU5%iWvT&9qpgtYHSFEr z9p*x?J4b0z02`(}CT?LNKOp)?jhMNYIz7)RXYRz+vknCUlrk?YT zFX;w#{Wtb=Zc4plBjF0Di=W7p6HwPhS+b)Bh-k@JK`>**K@dTvD9L&r`P>{M1%06J zL5YI2U8y?K_Rt!`X)McgvB}jHZiH4*?L-fKwo_riTxzUmmM;AWgCSMZIfXT)$aB9XH4imdcJ!}Pd8>DT zN5%}AuNEA~0Ts`gm``gTJq2@!)!`2GDeWNUU-1$?Jg3aBKVaq=^qXOw?L1!+5iU3u zG6TqJEN@1oK|9L*0tm^ZWEI*_fKOTwoe)xt$JiuNR0l)ov)bs$YdE!GkbzLykrtoT zJSRrSaJpMk@bSaYIz<}`zka~%yZwh!2JX!>Ko85N7H?cU9ro9J@JF-Bki6_hO*tUM zFWhwh1Tk8>fn&@qtdx&H7TmLm*>(P|96*$oBqymP01H{B<5T(zVv;y z!|EfSxJx%rAkoJNH%;N0^T{rAxI6~D*>zRQ8|P0`oMc@1X#{Tn9ld>ks=)g2#sa@W zz}2BezHgzC&T8CE&o}|l57cdjdO=cz7_~u5?wiYwi&}s70FFg~%SbBsQRJA%zkUn4 zY3Zaf$o>q=akjE95pj)ev>~b)N#WYF0P(&f1r;nrlf1;rUCkbZU=Y z%t+@f(V4N1J%g9Bhi!m`VFM-FUcE5fWZdY4l7w4jnD#Ak)K6#CVk%3*G%h3VhYmKL z9#7uDa2&GyzHgX(SaPJ=N%et$B1UlNYKt9bS^0}`7^xbZ=6kW~oEF*%W3x9&f*f{;jv~6@*wxtZW zQM?ESL(jWUhwLi(2*~6t}wnn&DrJX+EwB*ZU%Ja>8G7vm;}z63mvqBtjwxq z&gxLT_CNfFk+`4@(9lHCM&#WWg@l}==Hr=)9Io#&BgqDCAzFH20R^_+GT%F)9E3q; zo%c376-^38^aGz^<^r1$%D)zVFZN>RFx~0{l1;xUQG{WwRd2VU$0%2WvmPqB;H1R4 z+ApDco5Zc9Hc?)j{?$W5MBe%H8m#Dd<(z1&$NjO%9}sY!yq$g;6^)bdlb-7Ja7i4g=dBbF8`%V?DOy9CSg`afLK&&VkBS9;Rv z=;2{-GDvUN6DEvpt?Zaq>Lc)stY~^ZAFT0jkOc&?*oZ))MW@<7O?KCeO1wm0B~i3A zNbogZ#&nb|FwsKCIL%jDqC1u1KW5kn((+?uU181sycNOd3iXpHOFHJ#O=E-QkIZxh zZzd4veUL+8y3K1LEPau34oQCizffnA&|q|(#~&1)s6VKxdSACsWsT<1>F`Q9inPFe zLUbe}-6HX3|7X?FtzuFEKxRdxw+`ot^Ot(uk3jm^ZbE%E5e3x^{YF@@o0v7?T(t5w zJcg(^V$u04;@ItB0BdGAzgL5XNLYLt8}?OiNyXGrCUK+U6LFVLsoC~P=QFFJ?PiZp z#xtc1caJ_NTc^nx=2$|7HyGCKc;{t`BXi1zcJrQJJe(ol&<#gP;tG4cdco-Hj%)Iu z_52*5zJ=cYcr#KnY5Nov**KOutpP?J^NfA*?zEXmvJH>FO(c`k6axy7%3#=MS`>F8@xMYVUl=LJ|KH2w~7BAv#|Oct)yx( zs(K=u!L;0aW31+v**(^o@onCPD5?&>h}YkTH>w zD^oBS)>yO8W#v@sH+H-`4Rwoh2!bE&Iowdx zDmSpInuXI26X-DBZ_ppmEhD6o)JTQ5PX~qR-u<}?1?lYni4HB>qjJnOV_iw@>Gp#{ zPqC4B!h=hbFawxaO$1&!z7`lNgQZ>%cPllf+OedG=7E@A z36BsP(BO=`bP!~t+4WPTs3iGmeglaef>e`dQQuOnq+Nne606>?>G%^>;hmdXNe2DqPqCz-KyorcwjxMpgV zH&vlz9yifr7T-K1>`+es7a!+h)J^Ocl8lC}5F89w&P>!Ak_X`ej=Tur(R_Vg9|+zG4sAaXRC*sTSx}3DZe98kyTh7an;@&lK}ovLQNJc6 z5Nu{2s1Td$^@z&aVnjWKI6Soh6t>YAdns{)2CFbKkY>Y}vlJb%K28IBu^qqU7uST@ zNB+?l@zAN7!L1y^`SLXTn1K9U=|lx!5eR82gb=)+=K|TX1{};8%WX@_Xnl+6`zL89 zVT%!esYv5sllK*{c+4$%8`U$$oT`o6_gkw3(vuDS8h#D_LeDi-4_oREI{(igN9o!= z68j3o1Nc{NLab)noJT%>Y02MJ?>C%jhpOf@^E;BaSOo4g4yKd5YMQh9);pO zM_N^5K))85{*_gg=xo*B*3xWNWq4kM8PxJ?@--=DzniYV*8=#o z6xh>Ox#)+kod@ZiIjf|&SF{S6#KE6RKI{n`lEtLGh%b*$@!T4dO0XZogo%=@mOO{g(F)LiLP zZI^k^^PH8$Plpg}lr#8{~9$hom*HMV+W= z;)X^FYAi=eT8Avh zvtgU8DYjpZMK31Z?tRn-_=9j@de9}$qj}B^1NwdbpE+d@Dfc&ZdeM^F;C@S?zA=rS zw0LY4eL9WmN-#SOS}z(w!q~d?oAtFAuRy3EqZr-T?03(cyrLWNb8%)3Q~!xZ zzZnC{rBCzm4=rS&BcVO!0*(MIrD=JdMB|_4YZxUKX8)Zgl92DSX2Exe)B0rEeG2Qv zNy|y+-pz@LtQrfPG-zynP19w7q&)Q#rH-nMI)IU6W@#(t5~U^b42wOEc2n)2^W_CI zSf`^rBTbZw0-R7XMifM;AEmX2z*BV>Ka|i82d$r@)`|^UcI4merYO?Xkju_R&DrPD zhH+Mh10GU)gh_Vi=5EZtrr2&Yl|Vk4KC1f(hm`qR(Zxm^?m{vGiE_bA%4vvOIB3)j zyI0h4a~?ll+$mc=?q#H{24IOYb4+5wvWgw+r;+r37myNfyQ4Vhesh*#L`keIJvGKR z)EkLC_VL-nblu~U#KiO3o^gy5R(rWUWz~8{(R4P5EFbc zQzri#Uq#60_fld4Sq}B=7q+poZxUnwnXYkVq?#=H6hbGN3sYNfKsHdh)X25yTD10z zrf3ubAB%A-KHeA>>FuL!&D zG!xHRDjtrZ{)f?rzD%Z11)tajy^Cb_R=8tox;9i4m=+I(+GLVJSXKtsP! z)(?FX+ z`;`#+4}$DP+L}P_4(ntKJYDvjyfCV_fI~SQHz(>xxNS`zz9}d{g>jUrzDlguEHlt}@mP-4$ z%DqHOX}6{!RNpTQ7M-M`71lREwPvktT|l0?8K$nVoPK{@AeD00>~$c_SD;8P%TMzi zG4U|8;6B2f@$NJq1?+FaD+Q{C3BuH!C{;B{f(awf3}ZI<<5kjNh%G#Z88YYTZwVb; z?S-#AU-n?@n*}wDt95J8f27kM=Eg$JdCuJ>VKA2v9S~HTEKgSTL3eI*1csboQ7+EK zCb*EH+YxqM8Cx*@%wc9zg?dN$Ts3z&x`OP@D}uXP#UvWMkq9O3(`$G!69s}E_*X3dBr2%`O5jxu(rc9tvvay)M&3h@q8Xb)km z8yjD|-#O2#pupZ4%$E$9x3bc>s6-70-EXY)S-1ne{X(sm%Zg`*NpsW2JiwUZ^8?HZ zzbAZR6ihJbw{?jx2~?j~os(Z=stim0rV2~v4hlfV7*{wKF9>B}V+{W(0&nK2pep;e zby*Ct5x3pkVl0*)^g!kKDlOrF=frA2J-(}&q7yG(3(d9P9F0T@|1=$0s?6D-v)L11 zKF6?Px9d6R`?A(P?ogZ=2Os3@#Amr*ejWY3WzNN-hvxSv{xIxnHU$yO)!4yR&d~) zOeOm5yRg4l-HBw(`%XZ!d|FL1PfwTtEOoj|3w7UL1(!arDU`GlGG0vOO@{^dYiA5U zOt9qN#My?QQdGANzuVctplOztT*T?gz9|sG8lowA0mL|5^ZqTtKiV&q8w!XEE3YQ~ zL-ylOpy)jCG_Un$Cfa>5m9%2li+zZuolqiexPFw;AaTV6@BglQiH0c{kI9b#%zMA+iSXbIUiPrGX?8=~lW{rn@CAr-2y1$pcRQ(>+aIr~e z#ZOTB+{UZ*!=4kh`&``LHuPVD^ea}1eBAbL^4Kn@Wr@|$3JaEll0ksYGt?`eue5om z;nZz2`&9Wvh3w+%yUxHYYK+nX(_c~<=f@53+#n4tWzNOFF|9SVSRfY&;Ml7Onm8X$ z9uQU1)s*IXCt;nZBB(s{viOaPEoGEdM)YTwSNhzR2b1}Emr{J?taI17bq4b}OO70R zoRcR$wKi;%3MXg>gaLV9M15u}=Y&z<86TN~Kb{W-{{F73NZAkNA@1sf#2cjCQc-Sr zdcP2}{$@=M|K!D)(vs;VK83U5vqEl{=TqJTL;9>3uEtX$6C2`8v2lZ8C2ac{xr1h~@t!m6U!s;Z@ftykS*| zD7(hy;k3%Jx5(A*5*+SkG!YZ14tuZ1898Zzct>u8q{Z_%6)(PGrHnyd~)Jf z`HFvEKA88Tu-aD>jN<&%dw8Feup7%~HJCj_i?9?CgubiYV^Yb{5vZZ?v?Vo06}C*{ zzCNteL}m1Yy=SR)JlpcAa8C-4NI3tjZ<>;CN|i=&WH*sp36)Yv1>?UU8j+2#}{}z9E=Y{J&A`93+}EHb3VTuD&2g3taue zW<5hgVN&~edK0Kw_yREVZg=0&Vvx`4TTD)tp%EMe?+J`OA5v3%3Vf;2qHVmf!SDRw^UMo6L zO~3C9WWdVV6q(3V#-PJ2j>C22j1@HWJ@WdkI{%XS%S(T_XtxH($2xxWP!c|19wt$9O0T!f&ACLe975 zYR|G}88h>O{RfBjBJNtfTbs_;SrgpKrZ54M#kk4%Hp{Y(Zdi$jw6k zUWR%5>wTQ7+$eRyUcA{>#iUa80;!%7|Ka#AH`J6$6uN^H;ZCti(YA6>>XT|1Tdg{* zKN3hbRSA-4+blIfdF#k=#G`Z~$I^^d^r|msr!QseMVPZ;uf3kL z_OeNu5V9k;y_lpgv^?tqZ#<0NqYvpy7^54PD>PS1e56H-`A?@$2lv)!Jgf&lB}5HQbj2AQeFx*9lYnr&FcwG%;1-l~j0 z_Jl_li|3VHQjK))k^H;PxCwP1^HC=`&F5^S5OQfW{0RF~=?a#?JS{XdM7Lc;Fe#e)xkHz0Lw~nl#x*`ce zW}mYi$-gVeN5nXCvo7rR1>G0Rt~=VzT-UjiQWMnF3e5az-fwJj{l+j)$gs@tsSjd> zYsXh?0~Yx8Ksre4e%|NsMf2qF22*wPA1#??*aMKSYT~5Un77`EIE?6!@t)+dPFW-V zqKK|#8|~M3WRWPkGk3A;H-lc=!0rrfteDLj%vDJ@xjCD%DI81+8A{)=d?Id)GN%>5 z=SxS&qso+0j7-QuuH?8lqpOqw4#jWEM6$3a-NwTK2LR$}-FEpL&ZRU0t89HwI7UgH zujP6@!2i%60Ys469l#G9ZUSMrfOn@Gr3*s~O(C3LC$9mvX<7=vV)dm3!D2^IlrBmf zI$PFvw@s_Ti&Bq(!)*?^<@v6`AL9I{Sb47?1BX!orwOrGI1sW5k0i}+3OQJ~JiX|8 zxIL(HAbDl%@FW#{e_j7ymg@+J`e|}#7y2%g8`f>@N~5YF8tR!KN0@ow|k!r#M{9lN$V7d)ir#+j(xAIee@}g#Zvf84r&@^CL zU1XsvpBKmeDbS(KyH7f{m_4*#{87E)lk(ZU%|sf{+H=@&!I zN!NEkloWhzLK}D@a|f~V0HVC)H1IZW#LD=i?~tYU**|@=d8^&kF0=Giz>c1H)uC5z zkaFP&#M3117R*9S$dN4o_HUfz(%tDTYl%G-cUdwJN6!58iS96s4|)RdS$wp?`3x{? z`vM;zulnhc+i|A6rV40))Zgg=Ng&g!j+dMU$$LnHhWDSTX9MjfaA*DRu8x#DFpyt! zkglCA3Q>3bI5U;jNd*V|R)aX=v;2sz(?QT35=f*BPACFbEvg4zu_QfD|uc9u9c-8T%Z&Zgz4mx@r_3jm*XvkKhX4it1xL3-glos^`=ZR^V=p7 z6<)Prom04H>H}Ofe^k;X#w*)mdRsgkd6B%E3^VwiG?N``Vdq5tPptX7@6GBy?0vfE zN}!Qk=p1tGdPsk4DhPR;mztE;1JZyLhNAd!ZR&T8kmgpJMSaa>k+(a(adm0q{TRnC#~a#(Ciy!@uPY|hwV|t zLy8?tL~v*qA0F@y-j&UIBsk8ri|$i7EjDsJxL--!Cp-)SN_F!3twt8V=+R%@xOu-G zaqmw9Ch*IH+4%RDeWz-#ig3NCeWjM`x?vgETGq#a@Ao-J2s<~H zpdYuuU5wz_T>+HTDITlE%0!gvZT3?Pln&jSfAv@&lDnku-G1Wi_5B^#Y60k-Kf>-F zdu4yISl*_UMUV0%{S9Tdj2x_oUUmUf7hXu&HP22ba0q(1e7ia0nk(T~>zoh$;oao2 zJpH7h+W|xlGZhk!>z;N5V8e8OV(V2ijx~w&3NH@+we_W=gWhv0?Q@oh!yqg|sbWY? zi775YLc-yfYZBU)Q6Mjs!(7h^(Z(g5?f3jfJc6dHAA!_#Ljm&kKu$n{RZmxzN7*_h z=VxW>tNm4L@vTvxVs&@ixMDAYHRu`Z^UK{(Lhy(z69>Uk@gE&27Jw%rNJQHMdnxGF z(y{5wu)MGlgjKWR<`D9(1|jpfc(j4MvTk9&0|3=+J57znsyJ|o5r~*{9CEP_nGV0# zOvQ!2W2y*aYb*ViD_oh3$D|0jts*D2%|Jo?QTE-c{ld68A=m2xMw9uTPz9W+ zPr*R6%^L9;o`2xT_{c8a%SS-2rhs3d$i!teE$-U9`So5N3wtbc4LQZ-x#@Bkn zi)ukI|Kif;ymkXW&fBUsE9d`|FHL#Ur~PX->))Lz2 z_I zL$SZiY^+pTeKGNhYO~I=Fk%1iBd!YVa8*x&CRLI|Z;jJjRd|}XYre_eiXY5+F&jPY z083t&?U-Or+xm;dh_ukB=WK zFu;B!vmt^!?VvWOvM!N{j8bAyeLs%U__{5VCSY9);)y)};S~*{TR6JUNhH0u9;O$C zrt&auPCseEpZTGgFbC_@4a}MK7Qw zf+O;+0XL4t*jxXo6$!Idrx}lX?v3Sh-q~7fGz+t%TTrJ6^8t(00kS{6UnB%P$15on zU7~r15_@RJkBTD`!7yd|;L8gElq3AfIe=O|K zUhCNIv1t&f&qpM|7GJ|@nT#>`6d2^JGhbmlym_MqP_{FEcE$$LQtst_t3#grt*D+a zQyHQ50aqPk&Jo4uvxnBH|8B2ot8Ir-XLpxJ*-~YEJN1f@gr3=FqYlzJ*NDnQjT}Pe zFkFTI39luoJ=4b^P!&z2W`sTksV5t(=UvqAuPb+-*%ugh&sJJ}=Ry#>pUv&e8n*B? z{|V8=<9toY+MQf$!5ZhSQ~xKbwhIrNPfkAe!aFb>?XRLb@9>T7(8bt?0)#u%_-m+f z-Qj`g(zvs`E=%Nzzvv4DLE$&4i-5u@EBi`={59U*X9t!u;tz@Wy?I#d_olx)4g}1U zY3py`=jBwf`334S3$)-6`U(6#B@`8aVj^9fN^GLUH5vsogwk(7vu~vMXae`K)SC2U z5Jf?{)d-Y+7@#YZMPxN1e*bjn@+Zv4o{6j?m|9hZ$o}o~+wNmM&XD#Y6fmlD5;Cg0 zb%=Ekt%4E~(+Z3whC8&Kc8sZ*6NR|Di>~>-nT4s1+gL@L_t*yc_QYt5?i#-s;5|LO zDfKT43>}`uzQca9kBU-2YKu-nrAhA8=$308vprriRe&l}n`q%N*&b|nCa7-^UL{|! zNNN=fkJMx?a{KqLcNwN>hP6%Br4YvLi&b%{fLpZe+!+xohzLIPCUyu3W7vKSWLtli zO&9G6I}7dg!vkyOhduN*4#Zd&3=J?wC|$flb7OyiOLpU{fb`g2j5X>nTsJ%%8r04X zce)XwmsKx5_c~tUI6p?~2{mhu_nVf>W65jsglJc>WRVSf9;hE?vgqsSWpWIHaBQZ` z7sSM>35I2ss!}r8jE*jgAux^cm6i~bj~i4rGBLKXx@4B=g5%xX<$F7jufSf{Q@Y~T zwVdUjYfD3`Wh`EpOs0EMnSPaZ6i>hW3XQt44}DS6dvr{Ql19K+lhJF|6jogV+LQDW z{mM@~C5k4dJ5yb*kuIZt2u0R}ZNDI%X9l8iQI!hE?zr5Fe^9MP&<*)*V<;Z#*qDgu zYQWoKdF_Hk6d6KQ+tC$82F1hQ-Y7*_%qd$Rl-uk)acM`7YLh4|6ZFTT&cTfpPsRNz ze)kW5AEK!@jeJeZY5=l!3?h7Ep=gmTE1aa6YdJy_Lx0vfziUPfs~g~N%NzH3XZA7B%Ih~SWihVzax0eg8an5I1?GraAREi1re$90?IGbq?4Of_G}9NIUwD|G`=T5NrW^jk{&lhP@EB&Eu8cOD zfmGt0MDS`l)0*sG*>^O)7%nXaa_b^*mNWRvbtI2t#g;2zoM;*+Nldem{#eLLaZV?* z%^a!u8u(N;*?^*Dq-A0iQLdAuRY*CZWTt>)gIJZnOj$eDO~_fd~@?+(dOC!B4M z>8vBwHJB$UwWD3JL|`VPd+yH=>%XuyU%ksU**{{zHvXWx(}dh19;`gLhnmFEqg(|J zo8ug&^Yza;tp@kho`rFa;W0Dps5iSsfhJS=ccW41Xp_ScG*3Ez=!Ktek9Yjqu-Hn) z9%V(@0~p~@agy`3qT58Pu zKwb2p6YbBR1}fosuW$ofK@?N4Fh%O3EUC+t9V0}&-g}Z0c;XD#i{MZ)fAT_|#K(px(v85Ws*Ed6N|%6K_LDcvb+u;?uiFbfUN> zLZDY}Etm0EIzT*7dYdngAAUv^Y}0St&&S{}SEc+*K=OCfd``J6}H8CR6wPBX;O0H`^$!@|^w-((UWs zOJApYlO@5J5rNv|aqdGl+mGkh!y>scysr;DCQX6fH^#`Y;{z6({y a7a|uL{_R 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 literal 0 HcmV?d00001 diff --git a/design/assets/make_charts.py b/design/assets/make_charts.py new file mode 100644 index 00000000..a98c0122 --- /dev/null +++ b/design/assets/make_charts.py @@ -0,0 +1,268 @@ +# Charts for the checkpoint compute-gas accounting design doc. +# Single-hue emphasis scheme: neutral gray for context bars, blue (#2a78d6) for +# the highlighted entity, hatch texture for the pathological case. Recessive +# grid, no top/right spines, direct value labels, text in near-black ink. +# +# Emits every figure in two languages: `checkpoint-figN-*.png` (Chinese, used by +# the design doc) and `checkpoint-figN-*-en.png` (English). +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib import font_manager + +# CJK font setup (macOS) +for name in ["PingFang SC", "Hiragino Sans GB", "Arial Unicode MS"]: + if any(f.name == name for f in font_manager.fontManager.ttflist): + plt.rcParams["font.sans-serif"] = [name, "DejaVu Sans"] + break +plt.rcParams["axes.unicode_minus"] = False + +BLUE = "#2a78d6" +BLUE_DARK = "#104281" +GRAY = "#b3b1a7" +GRAY_DARK = "#6f6d66" +INK = "#1a1a19" +INK_2 = "#5c5a52" +GRID = "#e6e4dc" + +import os + +OUT = os.path.dirname(os.path.abspath(__file__)) + +TEXT = { + "zh": { + "fig1_ylabel_a": "占全程序 cycles(%)", + "fig1_title_a": "最热的廉价操作码吃掉的周期", + "fig1_parts": ["包装结构\n+ 记账", "单条限额\n检查跳转 jb", "操作码本体\n(其余)"], + "fig1_ylabel_b": "push1 内部占比(Ir / 采样归因)", + "fig1_title_b": "push1 内部:一半以上是计量税", + "fig1_suptitle": "逐操作码计量税解剖 —— 包装成本归零的全程序天花板:5–9% cycles", + "fig2_ylabel_a": "残余包装税(占执行操作码 %)", + "fig2_title_a": "税:粒度越细,重新上税越多", + "fig2_ylabel_b": "最大段长(k gas,主网观测)", + "fig2_title_b": "界:V1 观测值是假象 —— 结构上无界", + "fig2_annot": "对抗形状(纯算术循环)下\nV1 段长 = 剩余 EVM gas,无结构界", + "fig2_annot_xytext": (0.62, 30.5), + "fig2_annot_va": "baseline", + "fig2_suptitle": "检查点粒度两难(主网 1,000 笔轨迹重切):残余税与段长上界不可兼得 —— 除非换执法机制", + "fig3_schemes": [ + "Rex5\n逐操作码(现行)", + "V1 检查点\n(帧末结算)", + "V1.5\n(回跳结算)", + "V0 gas 钳制\n(最终方案)", + ], + "fig3_ylabel": "halt 时已记账 compute(k gas,log)", + "fig3_title": "执法精确性:detention cap 下 26 万 gas 纯算术循环的 halt 落点\n" + "(V1 overshoot 到帧末;V0 停在越限操作码执行前,零 overshoot)", + "fig4_schemes": ["逐操作码\n(改造前)", "V2", "V1.5", "V1", "V0\n(最终)", "原装 revm\n(下界)"], + "fig4_ylabel_a": "热循环用时(ms)", + "fig4_title_a": "解释器热循环(70 万廉价操作码):-50%,落在地板上", + "fig4_labels_b": ["逐操作码\n(rex5)", "V0\n(最终)", "原装 revm\n(下界)"], + "fig4_ylabel_b": "weth9 transfer 用时(μs)", + "fig4_title_b": "真实 ERC20 转账(同 run 对照)", + "fig4_suptitle": "最终效果(本地 wall-clock,同 run 内多方案对照;V0 = 检查点结算 + gas 钳制执法)", + }, + "en": { + "fig1_ylabel_a": "Share of whole-program cycles (%)", + "fig1_title_a": "Cycles consumed by the hottest cheap opcodes", + "fig1_parts": ["Wrapper\n+ accounting", "Per-op limit-check\nbranch (jb)", "Opcode body\n(rest)"], + "fig1_ylabel_b": "Breakdown inside push1 (Ir / sampled attribution)", + "fig1_title_b": "Inside push1: more than half is metering tax", + "fig1_suptitle": "Anatomy of the per-opcode metering tax — whole-program ceiling with wrapper cost at zero: 5–9% of cycles", + "fig2_ylabel_a": "Residual wrapper tax (% of executed opcodes)", + "fig2_title_a": "Tax: finer granularity re-taxes more", + "fig2_ylabel_b": "Max segment length (k gas, mainnet observed)", + "fig2_title_b": "Bound: V1's observed value is an illusion", + "fig2_annot": "Adversarial shape (pure arithmetic loop):\nV1 segment = all remaining EVM gas,\nno structural bound", + "fig2_annot_xytext": (0.55, 33.3), + "fig2_annot_va": "top", + "fig2_suptitle": "Checkpoint granularity dilemma (1,000 mainnet traces): residual tax vs segment bound — unless enforcement changes", + "fig3_schemes": [ + "Rex5\nper-opcode (current)", + "V1 checkpoints\n(frame-end settle)", + "V1.5\n(backward-jump settle)", + "V0 gas clamp\n(final)", + ], + "fig3_ylabel": "Compute recorded at halt (k gas, log)", + "fig3_title": "Enforcement exactness: halt point of a 260k-gas pure arithmetic loop under a detention cap\n" + "(V1 overshoots to frame end; V0 stops before the crossing opcode executes — zero overshoot)", + "fig4_schemes": ["Per-opcode\n(before)", "V2", "V1.5", "V1", "V0\n(final)", "Vanilla revm\n(floor)"], + "fig4_ylabel_a": "Hot-loop time (ms)", + "fig4_title_a": "Interpreter hot loop (700k cheap opcodes): -50%, landing on the floor", + "fig4_labels_b": ["Per-opcode\n(rex5)", "V0\n(final)", "Vanilla revm\n(floor)"], + "fig4_ylabel_b": "weth9 transfer time (μs)", + "fig4_title_b": "Real ERC20 transfer (same-run comparison)", + "fig4_suptitle": "Final effect (local wall-clock, schemes compared within one run; V0 = checkpoint settlement + gas-clamp enforcement)", + }, +} + + +def style_ax(ax, ymax=None): + ax.spines[["top", "right"]].set_visible(False) + ax.spines[["left", "bottom"]].set_color(GRAY) + ax.tick_params(colors=INK_2, labelsize=9) + ax.yaxis.grid(True, color=GRID, linewidth=0.8, zorder=0) + ax.set_axisbelow(True) + if ymax: + ax.set_ylim(0, ymax) + + +def bar_labels(ax, bars, fmt, dy=0.02, fontsize=9): + top = ax.get_ylim()[1] + for b in bars: + ax.text( + b.get_x() + b.get_width() / 2, + b.get_height() + top * dy, + fmt(b.get_height()), + ha="center", + va="bottom", + fontsize=fontsize, + color=INK, + ) + + +def fig1(t, suffix): + # The per-opcode metering tax: where the cycles go. + fig, (a, b) = plt.subplots(1, 2, figsize=(9.2, 3.4), dpi=160) + + ops = ["push1", "add", "pop"] + share = [18.27, 4.79, 4.09] + bars = a.bar(ops, share, width=0.52, color=[BLUE, GRAY, GRAY], zorder=3) + style_ax(a, ymax=22) + bar_labels(a, bars, lambda v: f"{v:.2f}%") + a.set_ylabel(t["fig1_ylabel_a"], fontsize=9, color=INK_2) + a.set_title(t["fig1_title_a"], fontsize=10.5, color=INK, pad=10) + + vals = [27, 25, 48] + colors = [BLUE, BLUE_DARK, GRAY] + bars = b.bar(t["fig1_parts"], vals, width=0.52, color=colors, zorder=3) + style_ax(b, ymax=58) + bar_labels(b, bars, lambda v: f"≈{v:.0f}%") + b.set_ylabel(t["fig1_ylabel_b"], fontsize=9, color=INK_2) + b.set_title(t["fig1_title_b"], fontsize=10.5, color=INK, pad=10) + + fig.suptitle(t["fig1_suptitle"], fontsize=11.5, color=INK, y=1.04) + fig.tight_layout() + fig.savefig(f"{OUT}/checkpoint-fig1-per-opcode-tax{suffix}.png", bbox_inches="tight") + plt.close(fig) + + +def fig2(t, suffix): + # Granularity dilemma: residual tax vs segment bound (mainnet n=1000). + fig, (a, b) = plt.subplots(1, 2, figsize=(9.2, 3.6), dpi=160) + + variants = ["V1", "V1.5", "V2", "V3"] + tax = [0.71, 3.87, 7.80, 14.39] + colors = [BLUE, BLUE, GRAY, GRAY] + bars = a.bar(variants, tax, width=0.5, color=colors, zorder=3) + style_ax(a, ymax=17) + bar_labels(a, bars, lambda v: f"{v:.2f}%") + a.set_ylabel(t["fig2_ylabel_a"], fontsize=9, color=INK_2) + a.set_title(t["fig2_title_a"], fontsize=10.5, color=INK, pad=10) + + seg_max = [27.151, 6.861, 6.772, 6.771] + bars = b.bar(variants, seg_max, width=0.5, color=[GRAY, BLUE, GRAY, GRAY], zorder=3) + bars[0].set_hatch("///") + bars[0].set_edgecolor(GRAY_DARK) + style_ax(b, ymax=34) + bar_labels(b, bars, lambda v: f"{v:,.1f}k") + b.set_ylabel(t["fig2_ylabel_b"], fontsize=9, color=INK_2) + b.set_title(t["fig2_title_b"], fontsize=10.5, color=INK, pad=10) + b.annotate( + t["fig2_annot"], + xy=(0, 27.8), + xytext=t["fig2_annot_xytext"], + va=t["fig2_annot_va"], + fontsize=8.5, + color=INK, + arrowprops=dict(arrowstyle="->", color=GRAY_DARK, lw=0.9), + ) + + fig.suptitle(t["fig2_suptitle"], fontsize=11.5, color=INK, y=1.04) + fig.tight_layout() + fig.savefig(f"{OUT}/checkpoint-fig2-granularity{suffix}.png", bbox_inches="tight") + plt.close(fig) + + +def fig3(t, suffix): + # Enforcement exactness: recorded compute at halt, detention cap scenario. + fig, ax = plt.subplots(figsize=(7.2, 3.6), dpi=160) + + halted_at = [22.0, 281.0, 22.0, 22.0] + colors = [GRAY, GRAY, GRAY, BLUE] + bars = ax.bar(t["fig3_schemes"], halted_at, width=0.5, color=colors, zorder=3) + bars[1].set_hatch("///") + bars[1].set_edgecolor(GRAY_DARK) + ax.set_yscale("log") + ax.set_ylim(10, 700) + ax.spines[["top", "right"]].set_visible(False) + ax.spines[["left", "bottom"]].set_color(GRAY) + ax.tick_params(colors=INK_2, labelsize=9) + ax.yaxis.grid(True, color=GRID, linewidth=0.8, zorder=0) + ax.set_axisbelow(True) + for b_, v in zip(bars, halted_at): + ax.text( + b_.get_x() + b_.get_width() / 2, + v * 1.12, + f"{v:.0f}k", + ha="center", + va="bottom", + fontsize=9.5, + color=INK, + ) + ax.axhline(22.0, color=BLUE_DARK, linestyle="--", linewidth=1.2, zorder=2) + ax.text(3.42, 19.0, "detention cap", fontsize=8.5, color=BLUE_DARK, ha="right") + ax.set_ylabel(t["fig3_ylabel"], fontsize=9, color=INK_2) + ax.set_title(t["fig3_title"], fontsize=10.5, color=INK, pad=10) + fig.tight_layout() + fig.savefig(f"{OUT}/checkpoint-fig3-enforcement{suffix}.png", bbox_inches="tight") + plt.close(fig) + + +def fig4(t, suffix): + # Final effect: wall-clock per scheme. + fig, (a, b) = plt.subplots( + 1, 2, figsize=(9.6, 3.7), dpi=160, gridspec_kw={"width_ratios": [1.35, 1]} + ) + + times = [1.87, 1.09, 1.11, 0.93, 0.93, 0.94] + colors = [GRAY, GRAY, GRAY, GRAY, BLUE, GRAY] + bars = a.bar(t["fig4_schemes"], times, width=0.55, color=colors, zorder=3) + bars[-1].set_alpha(0.45) + style_ax(a, ymax=2.2) + bar_labels(a, bars, lambda v: f"{v:.2f}") + a.set_ylabel(t["fig4_ylabel_a"], fontsize=9, color=INK_2) + a.set_title(t["fig4_title_a"], fontsize=10.5, color=INK, pad=10) + a.annotate( + "-50.3%", + xy=(4, 0.93), + xytext=(2.35, 1.62), + fontsize=11, + color=BLUE_DARK, + fontweight="bold", + arrowprops=dict(arrowstyle="->", color=BLUE_DARK, lw=1.1), + ) + + times_w = [9.40, 9.26, 8.86] + bars = b.bar(t["fig4_labels_b"], times_w, width=0.5, color=[GRAY, BLUE, GRAY], zorder=3) + bars[-1].set_alpha(0.45) + style_ax(b, ymax=11) + bar_labels(b, bars, lambda v: f"{v:.2f}") + b.set_ylabel(t["fig4_ylabel_b"], fontsize=9, color=INK_2) + b.set_title(t["fig4_title_b"], fontsize=10.5, color=INK, pad=10) + + fig.suptitle(t["fig4_suptitle"], fontsize=11.5, color=INK, y=1.04) + fig.tight_layout() + fig.savefig(f"{OUT}/checkpoint-fig4-final-effect{suffix}.png", bbox_inches="tight") + plt.close(fig) + + +for lang, suffix in [("zh", ""), ("en", "-en")]: + t = TEXT[lang] + fig1(t, suffix) + fig2(t, suffix) + fig3(t, suffix) + fig4(t, suffix) + +print("charts written") diff --git a/design/reference-checkpoint_accounting_tests.rs b/design/reference-checkpoint_accounting_tests.rs new file mode 100644 index 00000000..022a499b --- /dev/null +++ b/design/reference-checkpoint_accounting_tests.rs @@ -0,0 +1,238 @@ +//! REX6 checkpoint compute-gas accounting with V0 gas-clamp enforcement (prototype). +//! +//! Plain opcodes run the raw revm instructions with no per-opcode recording; compute gas +//! settles as an interpreter-gas delta at each checkpoint (storage-gas opcodes, CALL/CREATE +//! family, volatile opcodes, `GAS`, frame entry/exit). Enforcement inside plain segments is +//! the V0 gas clamp: the interpreter's visible gas is clamped to the compute headroom, so +//! revm's own per-opcode gas checks stop a crossing opcode at the clamp boundary *before it +//! executes* — zero overshoot. +//! +//! These tests pin the three sides of the design: +//! +//! - **Precision invariant**: non-exceeding transactions produce accounting totals (and receipts) +//! bit-identical to per-opcode recording, including under an active clamp (`GAS` observability). +//! - **Exact enforcement**: a crossing inside a plain segment halts at the crossing opcode with its +//! cost excluded — earlier than per-opcode enforcement, which executes the crossing opcode first. +//! - **Bounded loops**: a detention cap inside a checkpoint-free arithmetic loop is enforced by the +//! clamp, not deferred to frame end. + +use crate::common::{transact, transact_default, CALLER, CONTRACT}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP}; + +const ONE_ETH: u128 = 1_000_000_000_000_000_000; + +fn db_with_code(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) +} + +/// Measures the intrinsic compute gas (tx base cost) with a STOP-only contract. +fn intrinsic_compute_gas() -> u64 { + transact_default( + MegaSpecId::REX6, + db_with_code(BytecodeBuilder::default().append(STOP).build()), + ) + .compute_gas +} + +/// A countdown loop of cheap opcodes with no checkpoint inside the loop body: +/// +/// ```text +/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP +/// ``` +/// +/// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and +/// participates in the jump-target offset. +fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = code.len() as u8; + code.push(0x5b); // JUMPDEST + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(0x90); // SWAP1 + code.push(0x03); // SUB + code.push(0x80); // DUP1 + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(0x57); // JUMPI + code.push(0x00); // STOP + Bytes::from(code) +} + +/// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then +/// `SSTORE(7, 99)` and STOP. +fn plain_run_then_sstore_code(pairs: usize) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for _ in 0..pairs { + builder = builder.push_number(1u64); + builder = builder.append(POP); + } + builder + .push_u256(U256::from(99u64)) // value + .push_u256(U256::from(7u64)) // slot + .append(SSTORE) + .append(STOP) + .build() +} + +/// Precision invariant on a plain-opcode hot loop: with no limit crossing, checkpoint +/// accounting (REX6) must produce the same compute-gas total and receipt `gas_used` as +/// per-opcode accounting (REX5) — the segment delta telescopes over exactly the opcodes the +/// per-opcode wrappers would have recorded, and the loop's only settlement is the frame-end +/// checkpoint. +#[test] +fn test_checkpoint_plain_loop_totals_match_rex5() { + let code = countdown_loop_code_with_prefix(&[], 500); + let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); + let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); + + assert!(r5.is_success(), "REX5 loop must succeed: {:?}", r5.result); + assert!(r6.is_success(), "REX6 loop must succeed: {:?}", r6.result); + assert_eq!( + r5.compute_gas, r6.compute_gas, + "checkpoint totals must telescope to the per-opcode sum" + ); + assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); +} + +/// `GAS` observability under an active clamp: with a tight detention cap the interpreter's +/// visible gas is clamped for the whole post-access run, yet the value `GAS` pushes must be +/// the true remaining — the checkpoint prologue restores the clamp before the raw +/// instruction reads the counter. The returned word and the receipt must be bit-identical +/// to per-opcode REX5, where no clamp exists at all. +#[test] +fn test_v0_clamp_is_unobservable_via_gas_opcode() { + // TIMESTAMP; POP; GAS; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN + let code = + Bytes::from(vec![TIMESTAMP, POP, GAS, 0x60, 0x00, MSTORE, 0x60, 0x20, 0x60, 0x00, RETURN]); + // Detention cap 1,000: far below the interpreter's remaining gas, so the clamp is + // active at the GAS opcode; the tx itself stays far below the cap (non-exceeding). + let limits = |spec| { + let mut l = EvmTxRuntimeLimits::from_spec(spec); + l.block_env_access_compute_gas_limit = 1_000; + l + }; + + let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); + let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); + + assert!(r5.is_success(), "REX5 must succeed: {:?}", r5.result); + assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); + assert_eq!( + r5.result.output(), + r6.result.output(), + "GAS must push the true remaining under the clamp" + ); + assert_eq!(r5.compute_gas, r6.compute_gas, "compute totals must be identical"); + assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be identical"); +} + +/// V0 exact enforcement in a plain segment: the limit is placed mid-way through a straight +/// plain-opcode run. REX5 (per-opcode) executes the crossing opcode and then halts, so its +/// recorded usage exceeds the limit; REX6 (clamp) fake-OOGs the crossing opcode at the +/// clamp boundary *before executing it*, so its recorded usage stays at or below the limit +/// and the halt lands one opcode earlier — zero overshoot. +#[test] +fn test_v0_clamp_halts_at_crossing_opcode_without_executing_it() { + let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes + let intrinsic = intrinsic_compute_gas(); + + // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. + let compute_limit = intrinsic + 300; + let limits = + |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); + + let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); + let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); + + assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); + assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); + + // Per-opcode enforcement records the crossing opcode: usage strictly exceeds the limit. + assert!( + r5.compute_gas > compute_limit, + "REX5 executes the crossing opcode before halting; compute={} limit={compute_limit}", + r5.compute_gas + ); + // Clamp enforcement stops the crossing opcode before execution: usage stays at or + // below the limit. + assert!( + r6.compute_gas <= compute_limit, + "REX6 must not execute the crossing opcode; compute={} limit={compute_limit}", + r6.compute_gas + ); + assert!( + r6.compute_gas > compute_limit - 12, + "REX6 must stop at the clamp boundary, not earlier; compute={} limit={compute_limit}", + r6.compute_gas + ); +} + +/// V0 bounds detention inside a checkpoint-free plain loop — the shape that broke V1 +/// (which deferred enforcement to frame end, overshooting by the whole loop). The clamp +/// stops the loop at the cap boundary regardless of the absence of checkpoints. +#[test] +fn test_v0_clamp_bounds_detention_inside_plain_loop() { + // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a + // 10,000-iteration countdown loop (~260k gas) with no checkpoint inside. + let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); + let intrinsic = intrinsic_compute_gas(); + + let limits = |spec| { + let mut l = EvmTxRuntimeLimits::from_spec(spec); + l.block_env_access_compute_gas_limit = 1_000; + l + }; + + let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); + let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); + + assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); + assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); + + // The detained limit is usage-at-access + 1,000 ≈ intrinsic + TIMESTAMP + 1,000. + let detained_limit_upper = intrinsic + 2 + 1_000; + // Per-opcode enforcement executes the crossing opcode: usage strictly exceeds the cap. + assert!( + r5.compute_gas > detained_limit_upper - 12, + "REX5 must stop near the detention cap; compute={}", + r5.compute_gas + ); + // Clamp enforcement stops at the boundary without executing the crossing opcode, + // despite the loop containing no checkpoint at all. + assert!( + r6.compute_gas <= detained_limit_upper, + "REX6 must not overshoot the detention cap; compute={} cap≈{detained_limit_upper}", + r6.compute_gas + ); + assert!( + r6.compute_gas > detained_limit_upper - 30, + "REX6 must stop at the clamp boundary, not earlier; compute={}", + r6.compute_gas + ); + + // Attribution parity: a clamp-stopped detention exceed must classify as + // `VolatileDataAccessOutOfGas` exactly like per-opcode enforcement, even though the + // recorded usage never crossed the detained limit (the crossing opcode was stopped + // before executing). + let halt_reason = |label: &str, r: &crate::common::Outcome| match &r.result { + revm::context::result::ExecutionResult::Halt { reason, .. } => format!("{reason:?}"), + other => panic!("{label}: expected a halt, got {other:?}"), + }; + let r5_reason = halt_reason("REX5", &r5); + let r6_reason = halt_reason("REX6", &r6); + assert!( + r5_reason.starts_with("VolatileDataAccessOutOfGas"), + "REX5 must attribute the halt to volatile detention; got {r5_reason}" + ); + assert!( + r6_reason.starts_with("VolatileDataAccessOutOfGas"), + "REX6 clamp halt must keep the volatile detention attribution; got {r6_reason}" + ); +} diff --git a/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch b/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch new file mode 100644 index 00000000..7e774c5b --- /dev/null +++ b/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch @@ -0,0 +1,74 @@ +From a0115b1da6334f7e353a68cf50d049e48cac2b92 Mon Sep 17 00:00:00 2001 +From: RealiCZ +Date: Fri, 24 Jul 2026 18:10:25 +0800 +Subject: [PATCH] bench: add cheap-opcode interpreter hotloop workload to + transact + +--- + crates/mega-evm/benches/transact.rs | 47 ++++++++++++++++++++++++++++- + 1 file changed, 46 insertions(+), 1 deletion(-) + +diff --git a/crates/mega-evm/benches/transact.rs b/crates/mega-evm/benches/transact.rs +index 6636c33..1d4011d 100644 +--- a/crates/mega-evm/benches/transact.rs ++++ b/crates/mega-evm/benches/transact.rs +@@ -87,10 +87,55 @@ fn bench_weth9_transfer(c: &mut Criterion) { + group.finish(); + } + ++/// Builds a tight countdown loop of cheap opcodes: ++/// ++/// ```text ++/// PUSH3 iterations ++/// loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI ++/// STOP ++/// ``` ++/// ++/// Each iteration executes 7 opcodes for 26 gas (JUMPDEST 1 + PUSH1 3 + SWAP1 3 + ++/// SUB 3 + DUP1 3 + PUSH1 3 + JUMPI 10), all from the cheap-opcode family that ++/// dominates real interpreter workloads. ++fn hotloop_code(iterations: u32) -> Bytes { ++ let mut code = Vec::with_capacity(14); ++ // PUSH3 ++ code.push(0x62); ++ code.extend_from_slice(&iterations.to_be_bytes()[1..4]); ++ // loop target is the JUMPDEST right after the initial PUSH3 (offset 4). ++ let loop_target = code.len() as u8; ++ code.push(0x5b); // JUMPDEST ++ code.push(0x60); // PUSH1 ++ code.push(0x01); ++ code.push(0x90); // SWAP1 ++ code.push(0x03); // SUB ++ code.push(0x80); // DUP1 ++ code.push(0x60); // PUSH1 ++ code.push(loop_target); ++ code.push(0x57); // JUMPI ++ code.push(0x00); // STOP ++ Bytes::from(code) ++} ++ ++/// Benchmark a cheap-opcode-dense interpreter hot loop (~700k executed opcodes, ++/// ~2.6M gas), the workload shape where per-opcode gas-accounting overhead is ++/// the dominant tax. ++fn bench_interpreter_hotloop(c: &mut Criterion) { ++ let mut group = c.benchmark_group("interpreter_hotloop"); ++ let workload = Workload::single( ++ vec![Account::new(CALLEE).code(hotloop_code(100_000))], ++ TxSpec::call(CALLER, CALLEE), ++ ); ++ register_all(&mut group, &workload); ++ group.finish(); ++} ++ + criterion_group!( + benches, + bench_empty_transaction, + bench_simple_ether_transfer, +- bench_weth9_transfer ++ bench_weth9_transfer, ++ bench_interpreter_hotloop + ); + criterion_main!(benches); +-- +2.50.1 (Apple Git-155) + diff --git a/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch b/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch new file mode 100644 index 00000000..fa646334 --- /dev/null +++ b/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch @@ -0,0 +1,1021 @@ +From e1a2bde50aa6ccd8ce830b9626eaab087d914a04 Mon Sep 17 00:00:00 2001 +From: RealiCZ +Date: Fri, 24 Jul 2026 19:13:27 +0800 +Subject: [PATCH] feat(rex6): V0 gas-clamp enforcement for checkpoint + accounting + +Enforcement inside plain-opcode segments moves from checkpoint-deferred +settlement to the V0 gas clamp: at every checkpoint and frame entry/resume +the interpreter's visible gas is clamped to the compute headroom (the +tighter of the frame-local budget and the TX-level detained limit), so +revm's own per-opcode gas checks stop a crossing opcode at the clamp +boundary before it executes -- zero overshoot, zero per-opcode overhead. + +Checkpoint handlers gain a prologue (settle the open segment, restore the +clamp so CALL forwarding, GAS, and storage charges observe the true +counter) and an epilogue (re-clamp against the possibly-detained +headroom). GAS joins the checkpoint set so clamping is unobservable for +non-exceeding transactions. A clamp-induced fake OOG is restored and +reclassified at frame end as the corresponding limit exceed, preserving +the volatile-detention halt attribution. + +Non-exceeding transactions remain bit-identical to per-opcode accounting; +a limit-exceeding transaction now halts at the crossing opcode before it +executes, with that opcode's cost excluded from recorded usage. Specs +<= REX5 are byte-for-byte unchanged. +--- + crates/mega-evm/src/evm/execution.rs | 6 +- + crates/mega-evm/src/evm/instructions.rs | 255 ++++++++++++------ + crates/mega-evm/src/limit/compute_gas.rs | 25 ++ + crates/mega-evm/src/limit/limit.rs | 167 ++++++++++-- + .../tests/rex6/checkpoint_accounting.rs | 195 +++++++++----- + 5 files changed, 464 insertions(+), 184 deletions(-) + +diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs +index a34c6ea..d415477 100644 +--- a/crates/mega-evm/src/evm/execution.rs ++++ b/crates/mega-evm/src/evm/execution.rs +@@ -417,7 +417,7 @@ impl MegaEvm { + #[inline] + fn before_frame_run( + ctx: &MegaContext, +- frame: &EthFrame, ++ frame: &mut EthFrame, + ) -> Result, ContextDbError>> { + // Check if the additional limit is already exceeded, if so, we should immediately stop + // and synthesize an interpreter action. +@@ -454,6 +454,10 @@ impl MegaEvm { + let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); + + if let InterpreterAction::Return(interpreter_result) = action { ++ // REX6 V0 clamp: restore any hidden gas into the action's copy (and latch a ++ // clamp-induced fake OOG) before the code-deposit charge below observes it. ++ ctx.additional_limit.borrow_mut().restore_clamp_into_result(interpreter_result); ++ + // Charge storage gas cost for the number of bytes + if frame.data.is_create() && interpreter_result.is_ok() { + let code_deposit_storage_gas = constants::mini_rex::CODEDEPOSIT_STORAGE_GAS * +diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs +index f15fee4..be5e121 100644 +--- a/crates/mega-evm/src/evm/instructions.rs ++++ b/crates/mega-evm/src/evm/instructions.rs +@@ -380,24 +380,33 @@ mod rex6 { + use super::*; + + /// Returns the instruction table for the `REX6` spec — **checkpoint compute-gas +- /// accounting** (prototype). ++ /// accounting with V0 gas-clamp enforcement** (prototype). + /// + /// Unlike every earlier custom table, plain opcodes are wired to the raw revm mainnet + /// instructions with no per-opcode gas recording: the interpreter's own gas counter is + /// the accounting source, and compute gas settles as a segment delta at each +- /// checkpoint. The checkpoints are exactly the positions that had to stay wrapped +- /// anyway: ++ /// checkpoint. The checkpoints are the positions that had to stay wrapped anyway, plus ++ /// `GAS`: + /// - storage-gas opcodes (SSTORE, LOG0–LOG4, SELFDESTRUCT) and the CALL/CREATE family — their +- /// existing handler chains settle from the checkpoint baseline inside +- /// [`record_storage_compute_gas!`] (spec-dispatched, so ≤REX5 tables are untouched); +- /// - volatile / detention opcodes — `*_checkpoint` variants that run the raw instruction, +- /// settle the segment, then apply the detention cap; +- /// - frame entry/exit — `AdditionalLimit::before_frame_run` re-opens the window and +- /// `after_frame_run_instructions` settles the tail segment. ++ /// existing handler chains gain a [`checkpoint_prologue!`] (spec-dispatched, so ≤REX5 tables ++ /// are untouched); ++ /// - volatile / detention opcodes — `*_checkpoint` variants; ++ /// - `GAS` — [`compute_gas_ext::gas_checkpoint`], so the clamp is restored before the counter ++ /// is observed; ++ /// - frame entry/exit — `AdditionalLimit::before_frame_run` clamps and re-opens the window; the ++ /// frame-end hooks settle the tail and restore the clamp into the frame result. + /// +- /// Accounting totals telescope to the same per-transaction sums as per-opcode +- /// recording; only the halt position for limit-exceeding transactions coarsens to the +- /// next checkpoint. ++ /// **Enforcement (V0)**: at every checkpoint and frame entry/resume the interpreter's ++ /// visible gas is clamped to the compute headroom (the tighter of the frame-local ++ /// budget and the TX-level detained limit), so revm's own per-opcode gas checks ++ /// enforce the limits inside plain segments with zero per-opcode overhead and zero ++ /// overshoot: a crossing opcode fake-OOGs at the clamp boundary before executing and ++ /// is reclassified as the corresponding limit exceed at frame end. ++ /// ++ /// Accounting totals for non-exceeding transactions are bit-identical to per-opcode ++ /// recording (plain segments telescope; checkpoint bodies are recorded per-opcode); a ++ /// limit-exceeding transaction halts at the crossing opcode *before* it executes, with ++ /// that opcode's cost excluded from the recorded usage. + /// + /// The pre-existing REX6 behavior differences (canonical metering order, CREATE + /// `create_rex6` dispatch, SELFDESTRUCT existing-target accounting, CALL-family +@@ -430,6 +439,10 @@ mod rex6 { + table[SELFBALANCE as usize] = volatile_data_ext::selfbalance_checkpoint; + table[SLOAD as usize] = volatile_data_ext::sload_checkpoint; + ++ // V0 gas-clamp enforcement: GAS must be a checkpoint so the clamp is restored ++ // before the counter is observed. ++ table[GAS as usize] = compute_gas_ext::gas_checkpoint; ++ + // Storage-gas checkpoints — the same handler chains as REX5; under REX6 they + // settle from the checkpoint baseline internally. + table[SSTORE as usize] = additional_limit_ext::sstore; +@@ -480,6 +493,82 @@ macro_rules! run_inner_instruction_or_abort { + }; + } + ++/// REX6 checkpoint prologue. Runs at the top of every checkpoint handler, before any gas ++/// capture or gas-consuming work: ++/// ++/// 1. Settles the open plain-opcode segment — `baseline − remaining`, both on the clamped counter, ++/// telescoping exactly over the unwrapped opcodes since the last checkpoint. ++/// 2. Restores the clamp-hidden gas, so the checkpoint's body runs on the **true** counter: ++/// CALL-family forwarding math, the `GAS` opcode's pushed value, and storage-gas charges all ++/// observe real gas, keeping the clamp unobservable for non-exceeding transactions. ++/// 3. Re-opens the settlement window at the restored counter. ++/// ++/// Halts (returning from the enclosing handler) when the settlement surfaces a limit ++/// exceed — including one latched earlier by a non-compute mutation site. The restore has ++/// already happened on that path, so the frame result carries true gas. No-op before REX6. ++macro_rules! checkpoint_prologue { ++ ($context:expr $(,$ret:expr)?) => { ++ if $context.host.spec_id().is_enabled(MegaSpecId::REX6) { ++ let exceeding_result = { ++ let mut additional_limit = $context.host.additional_limit().borrow_mut(); ++ let remaining = $context.interpreter.gas.remaining(); ++ let segment = additional_limit.checkpoint_baseline().saturating_sub(remaining); ++ let hidden = additional_limit.checkpoint_restore_hidden(); ++ $context.interpreter.gas.erase_cost(hidden); ++ additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); ++ if additional_limit.record_compute_gas(segment) { ++ None ++ } else { ++ Some(additional_limit.exceeding_instruction_result()) ++ } ++ }; ++ if let Some(result) = exceeding_result { ++ $context.interpreter.halt(result); ++ return $($ret)?; ++ } ++ } ++ }; ++} ++ ++/// REX6 checkpoint epilogue for checkpoints whose frame keeps executing (SSTORE, LOG, ++/// volatile opcodes, `GAS`): re-applies the V0 gas clamp from the freshly settled usage — ++/// including any detention cap the checkpoint just installed — and re-opens the settlement ++/// window on the clamped counter. CALL/CREATE checkpoints skip this (the frame suspends ++/// and `before_frame_run` re-clamps on resume), as do frame-ending opcodes (the frame-end ++/// settlement restores instead). No-op before REX6. ++macro_rules! checkpoint_epilogue { ++ ($context:expr) => { ++ if $context.host.spec_id().is_enabled(MegaSpecId::REX6) { ++ let mut additional_limit = $context.host.additional_limit().borrow_mut(); ++ let hide = ++ additional_limit.checkpoint_clamp_amount($context.interpreter.gas.remaining()); ++ if hide > 0 { ++ let clamped = $context.interpreter.gas.record_cost(hide); ++ debug_assert!(clamped, "clamp amount exceeds remaining gas"); ++ } ++ additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); ++ } ++ }; ++} ++ ++/// Records a checkpoint opcode's own body gas (`$gas_before − remaining`) and re-opens the ++/// settlement window, enforcing the compute-gas limit exactly as the per-opcode wrappers ++/// did. Used by the REX6 checkpoint handlers for bodies that can never spawn a child frame ++/// (volatile opcodes, SLOAD, SELFBALANCE, `GAS`); the CALL/CREATE and storage-gas bodies ++/// use [`record_storage_compute_gas!`], which additionally excludes storage charges and ++/// forwarded child gas. ++macro_rules! record_checkpoint_body_compute_gas { ++ ($context:expr, $gas_before:expr) => { ++ let gas_after = $context.interpreter.gas.remaining(); ++ let gas_used = $gas_before.saturating_sub(gas_after); ++ { ++ let mut additional_limit = $context.host.additional_limit().borrow_mut(); ++ additional_limit.sync_checkpoint_baseline(gas_after); ++ compute_gas!($context.interpreter, additional_limit, gas_used); ++ } ++ }; ++} ++ + /// Records an opcode's compute gas in a single measurement window and enforces the compute-gas + /// limit. The REX6 storage-affecting handlers invoke it directly with the storage gas they + /// charged; plain opcodes use the leaner inline recording in +@@ -509,18 +598,12 @@ macro_rules! record_storage_compute_gas { + ($context:expr, $gas_before:expr, $storage_charged:expr) => {{ + let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); + let gas_after = $context.interpreter.gas.remaining(); +- // REX6 checkpoint accounting: the measurement window opens at the last checkpoint +- // (frame entry / resume or the previous checkpoint opcode), not at this opcode's own +- // start, so the unwrapped plain opcodes executed since then settle here in the same +- // recording. Only plain opcodes can run between two checkpoints, so no storage gas +- // or forwarded child gas is hiding in the extra window — the exclusions below stay +- // exact. Pre-REX6 keeps the per-opcode `$gas_before` window byte-for-byte. +- let window_start = if is_rex6 { +- $context.host.additional_limit().borrow().checkpoint_baseline() +- } else { +- $gas_before +- }; +- let mut gas_used = window_start.saturating_sub(gas_after).saturating_sub($storage_charged); ++ // The per-opcode `$gas_before` window applies on every spec. Under REX6 checkpoint ++ // accounting the plain segment preceding this opcode was already settled by ++ // [`checkpoint_prologue!`], which also restored the gas clamp, so `$gas_before` ++ // (captured after the prologue) lives on the true counter and the recorded amount ++ // is byte-identical to the pre-checkpoint per-opcode recording. ++ let mut gas_used = $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged); + // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` + // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the + // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit +@@ -1328,38 +1411,14 @@ pub mod volatile_data_ext { + + /* REX6 checkpoint-accounting volatile handlers. + +- Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints), +- but they run the raw revm instruction and settle the open segment — everything since +- the last checkpoint, measured on the interpreter's own gas counter — in one recording, +- instead of delegating to a per-opcode `compute_gas_ext` wrapper. The settlement runs +- before `apply_compute_gas_limit!` so a REX4+ relative detention cap is derived from the +- fully settled usage at the access point, exactly as the per-opcode order did. */ +- +- /// Settles the open checkpoint segment against the interpreter gas counter, re-opens +- /// the window, and halts (returning from the enclosing handler) when a limit — +- /// including a latched non-compute exceed — surfaces. +- macro_rules! settle_checkpoint_compute_gas { +- ($context:expr) => { +- let exceeding_result = { +- let gas_after = $context.interpreter.gas.remaining(); +- let mut additional_limit = $context.host.additional_limit().borrow_mut(); +- let gas_used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); +- additional_limit.sync_checkpoint_baseline(gas_after); +- if additional_limit.record_compute_gas(gas_used) { +- None +- } else { +- Some(additional_limit.exceeding_instruction_result()) +- } +- }; +- if let Some(result) = exceeding_result { +- $context.interpreter.halt(result); +- return; +- } +- }; +- } ++ Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints): ++ the prologue settles the open plain segment and restores the gas clamp, the raw revm ++ instruction runs on the true counter, the body's own gas is recorded per-opcode, the ++ detention cap is applied from the fully settled usage (exactly as the per-opcode order ++ did), and the epilogue re-clamps against the possibly-lowered headroom. */ + +- /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, raw +- /// revm instruction, segment settlement, detention cap. ++ /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, ++ /// prologue, raw revm instruction, body record, detention cap, epilogue. + macro_rules! wrap_checkpoint_detain_gas_unconditional { + ($fn_name:ident, $opcode_name:expr, $original_fn:path, $access_type:expr) => { + #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] +@@ -1378,15 +1437,18 @@ pub mod volatile_data_ext { + return; + } + ++ checkpoint_prologue!(context); ++ let gas_before = context.interpreter.gas.remaining(); + run_inner_instruction_or_abort!($original_fn, context); +- settle_checkpoint_compute_gas!(context); ++ record_checkpoint_body_compute_gas!(context, gas_before); + apply_compute_gas_limit!(context); ++ checkpoint_epilogue!(context); + } + }; + } + +- /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw +- /// revm instruction, segment settlement, detention cap. ++ /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, ++ /// prologue, raw revm instruction, body record, detention cap, epilogue. + macro_rules! wrap_checkpoint_detain_gas_conditional { + ($fn_name:ident, $opcode_name:expr, $original_fn:path) => { + #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] +@@ -1412,9 +1474,12 @@ pub mod volatile_data_ext { + } + } + ++ checkpoint_prologue!(context); ++ let gas_before = context.interpreter.gas.remaining(); + run_inner_instruction_or_abort!($original_fn, context); +- settle_checkpoint_compute_gas!(context); ++ record_checkpoint_body_compute_gas!(context, gas_before); + apply_compute_gas_limit!(context); ++ checkpoint_epilogue!(context); + } + }; + } +@@ -1511,9 +1576,12 @@ pub mod volatile_data_ext { + return; + } + ++ checkpoint_prologue!(context); ++ let gas_before = context.interpreter.gas.remaining(); + run_inner_instruction_or_abort!(instructions::host::sload, context); +- settle_checkpoint_compute_gas!(context); ++ record_checkpoint_body_compute_gas!(context, gas_before); + apply_compute_gas_limit!(context); ++ checkpoint_epilogue!(context); + } + + /// `SELFBALANCE` as a REX6 checkpoint. Same beneficiary-volatile handling as +@@ -1534,9 +1602,12 @@ pub mod volatile_data_ext { + return; + } + ++ checkpoint_prologue!(context); ++ let gas_before = context.interpreter.gas.remaining(); + run_inner_instruction_or_abort!(instructions::host::selfbalance, context); +- settle_checkpoint_compute_gas!(context); ++ record_checkpoint_body_compute_gas!(context, gas_before); + apply_compute_gas_limit!(context); ++ checkpoint_epilogue!(context); + } + } + +@@ -1709,6 +1780,9 @@ pub mod storage_gas_ext { + >( + context: InstructionContext<'_, H, WIRE>, + ) { ++ // REX6: settle the open segment and restore the clamp before any gas ++ // observation, so the forwarding math below sees the true counter. ++ checkpoint_prologue!(context); + // Captured at the very top so the single compute window covers all of the + // opcode's compute work. + let gas_before = context.interpreter.gas.remaining(); +@@ -2041,6 +2115,10 @@ pub mod storage_gas_ext { + >( + mut context: InstructionContext<'_, H, WIRE>, + ) { ++ // Settle the open segment and restore the clamp before any gas observation or the ++ // CREATE2 memory-expansion work below. ++ checkpoint_prologue!(context); ++ + // Canonical revm's `create` runs `require_non_staticcall!` before any operand read, + // memory work, address derivation, or storage-gas charge, so a static-frame + // `CREATE`/`CREATE2` halts here first. This unifies the halt reasons the prework below +@@ -2118,6 +2196,8 @@ pub mod storage_gas_ext { + >( + context: InstructionContext<'_, H, WIRE>, + ) { ++ // REX6: settle the open segment and restore the clamp before any gas observation. ++ checkpoint_prologue!(context); + // Captured at the very top so the single compute window covers the inner opcode. + let gas_before = context.interpreter.gas.remaining(); + let Some(len) = context.interpreter.stack.inspect::<1>() else { +@@ -2155,6 +2235,7 @@ pub mod storage_gas_ext { + // generic `instructions::host::log::` covers every valid call site. + run_inner_instruction_or_abort!(instructions::host::log::, context); + record_storage_compute_gas!(context, gas_before, storage_charged); ++ checkpoint_epilogue!(context); + } + + /// `SSTORE` opcode implementation modified from `revm` with compute gas tracking and +@@ -2178,6 +2259,8 @@ pub mod storage_gas_ext { + >( + context: InstructionContext<'_, H, WIRE>, + ) { ++ // REX6: settle the open segment and restore the clamp before any gas observation. ++ checkpoint_prologue!(context); + // Captured at the very top so the single compute window covers the inner opcode. + let gas_before = context.interpreter.gas.remaining(); + // The address to the underlying execution contract state +@@ -2229,6 +2312,7 @@ pub mod storage_gas_ext { + // EVM gas. + run_inner_instruction_or_abort!(instructions::host::sstore, context); + record_storage_compute_gas!(context, gas_before, storage_charged); ++ checkpoint_epilogue!(context); + } + + /// `SELFDESTRUCT` opcode implementation with storage gas metering for +@@ -2257,6 +2341,10 @@ pub mod storage_gas_ext { + >( + context: InstructionContext<'_, H, WIRE>, + ) { ++ // REX6: settle the open segment and restore the clamp before any gas observation ++ // (the storage charge below and the inner opcode both run on the true counter). ++ checkpoint_prologue!(context); ++ + // Inside a static frame, revm's inner SELFDESTRUCT halts on the + // static-context check without changing state. Skip the mega host work below + // (two account inspections, SALT account-creation pricing, the storage-gas +@@ -2305,20 +2393,7 @@ pub mod storage_gas_ext { + }; + let drained = + context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); +- let storage_charged = cost - drained; +- gas!(context.interpreter, storage_charged); +- +- // REX6 checkpoint accounting: this storage debit sits inside the window that +- // `compute_gas_ext::selfdestruct` closes later, so exclude it by lowering the +- // baseline now — the trailing settlement takes `baseline - remaining` with no +- // storage term of its own. +- if context.host.spec_id().is_enabled(MegaSpecId::REX6) { +- context +- .host +- .additional_limit() +- .borrow_mut() +- .deduct_checkpoint_baseline(storage_charged); +- } ++ gas!(context.interpreter, cost - drained); + + // Record resource usage for new beneficiary account + context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); +@@ -2587,17 +2662,14 @@ pub mod compute_gas_ext { + // Call the original instruction + run_inner_instruction_or_abort!(instructions::host::selfdestruct, context); + +- // REX6 checkpoint accounting: the window opens at the checkpoint baseline, folding +- // in the unwrapped plain opcodes since the last checkpoint. The beneficiary-creation +- // storage charge in `storage_gas_ext::selfdestruct` already lowered the baseline by +- // the charged amount, so no storage exclusion is needed here. Pre-REX6 keeps the +- // per-opcode `gas_before` window. ++ // The per-opcode `gas_before` window applies on every spec. Under REX6 the plain ++ // segment before this opcode was settled by the `checkpoint_prologue!` in ++ // `storage_gas_ext::selfdestruct` (which also restored the clamp), and the window ++ // is re-opened here so the frame-end settlement does not recount the body. + let is_rex6 = context.host.spec_id().is_enabled(MegaSpecId::REX6); + let gas_after = context.interpreter.gas.remaining(); + let mut additional_limit = context.host.additional_limit().borrow_mut(); +- let window_start = +- if is_rex6 { additional_limit.checkpoint_baseline() } else { gas_before }; +- let gas_used = window_start.saturating_sub(gas_after); ++ let gas_used = gas_before.saturating_sub(gas_after); + if is_rex6 { + additional_limit.sync_checkpoint_baseline(gas_after); + } +@@ -2605,6 +2677,23 @@ pub mod compute_gas_ext { + context.interpreter.halt(additional_limit.exceeding_instruction_result()); + } + } ++ ++ /// `GAS` opcode as a REX6 checkpoint. ++ /// ++ /// `GAS` must be a checkpoint under V0 gas-clamp enforcement: the prologue restores ++ /// the clamp-hidden gas before the raw instruction reads the counter, so the pushed ++ /// value equals the true remaining and clamping stays unobservable for transactions ++ /// that never exceed a limit. ++ #[inline] ++ pub fn gas_checkpoint( ++ context: InstructionContext<'_, H, WIRE>, ++ ) { ++ checkpoint_prologue!(context); ++ let gas_before = context.interpreter.gas.remaining(); ++ run_inner_instruction_or_abort!(instructions::system::gas, context); ++ record_checkpoint_body_compute_gas!(context, gas_before); ++ checkpoint_epilogue!(context); ++ } + } + + /// Trait to inspect the stack elements. +diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs +index d37e27c..ce3c2ee 100644 +--- a/crates/mega-evm/src/limit/compute_gas.rs ++++ b/crates/mega-evm/src/limit/compute_gas.rs +@@ -110,6 +110,31 @@ impl ComputeGasTracker { + self.detained_limit + } + ++ /// Returns the base (undetained) TX compute-gas limit. ++ pub(crate) fn base_tx_limit(&self) -> u64 { ++ self.frame_tracker.tx_limit() ++ } ++ ++ /// Returns the compute-gas headroom the V0 gas clamp may leave visible to the ++ /// interpreter, and whether the binding constraint is the frame-local budget (`true`) ++ /// or the TX-level (detained) limit (`false`). ++ /// ++ /// The headroom is the tighter of the current frame's remaining compute budget ++ /// (Rex4+) and the TX-level remaining under the effective (possibly detained) limit — ++ /// the same pair `check_limit` enforces, so gas hidden beyond this headroom can only ++ /// be reached by a transaction that would exceed a limit. ++ #[inline] ++ pub(crate) fn clamp_headroom(&self) -> (u64, bool) { ++ let tx_remaining = self.tx_limit().saturating_sub(self.tx_usage()); ++ if self.rex4_enabled { ++ let frame_remaining = self.frame_tracker.current_frame_remaining(); ++ if frame_remaining < tx_remaining { ++ return (frame_remaining, true); ++ } ++ } ++ (tx_remaining, false) ++ } ++ + /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained + /// limit is tighter than the base TX limit AND actual usage exceeds it. + pub(crate) fn is_detained_exceed(&self) -> bool { +diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs +index 1b01e61..54f3e27 100644 +--- a/crates/mega-evm/src/limit/limit.rs ++++ b/crates/mega-evm/src/limit/limit.rs +@@ -117,9 +117,29 @@ pub struct AdditionalLimit { + /// checkpoint, frame entry, or frame resume). Only meaningful while a frame is running + /// and only when `checkpoint_accounting` is active. Re-synced at every + /// `before_frame_run` (which covers both frame entry and every resume after a child +- /// frame's outcome is merged back), at every checkpoint settlement, and lowered by +- /// storage-gas charge sites that debit interpreter gas outside a settlement window. ++ /// frame's outcome is merged back) and at every checkpoint settlement. + checkpoint_baseline: u64, ++ ++ /// V0 gas-clamp enforcement: the portion of the executing frame's interpreter gas ++ /// hidden (clamped away) so revm's own per-opcode gas checks enforce the compute / ++ /// detention headroom inside plain-opcode segments. Non-zero only while the current ++ /// frame is inside a plain segment: every checkpoint restores it before running its ++ /// body (so CALL forwarding, `GAS`, and storage charges see the true counter) and ++ /// re-clamps on exit; the frame-end settlement restores it into the frame result. ++ clamp_hidden: u64, ++ ++ /// Whether the binding headroom at the last clamp was the frame-local compute budget ++ /// (`true`) or the TX-level (possibly detained) limit (`false`). Decides how a ++ /// clamp-induced fake OOG is reclassified: frame-local exceeds revert to the parent, ++ /// TX-level exceeds halt the transaction. ++ clamp_frame_local: bool, ++ ++ /// Whether a clamp-induced fake OOG was latched while gas detention was the binding ++ /// TX-level constraint. `is_detained_exceed` requires `used > detained_limit`, which a ++ /// clamp-stopped transaction never reaches (the crossing opcode's cost is excluded), ++ /// so the halt-reason attribution consults this flag to keep reporting ++ /// `VolatileDataAccessOutOfGas` exactly as per-opcode enforcement does. ++ clamp_latched_detained: bool, + } + + /// The usage of the additional limits. +@@ -149,6 +169,9 @@ impl AdditionalLimit { + storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), + checkpoint_accounting: spec.is_enabled(MegaSpecId::REX6), + checkpoint_baseline: 0, ++ clamp_hidden: 0, ++ clamp_frame_local: false, ++ clamp_latched_detained: false, + } + } + } +@@ -191,6 +214,9 @@ impl AdditionalLimit { + self.kv_update.reset(); + self.storage_call_stipend.reset(); + self.checkpoint_baseline = 0; ++ self.clamp_hidden = 0; ++ self.clamp_frame_local = false; ++ self.clamp_latched_detained = false; + } + + /// Interpreter gas remaining at the start of the current unsettled segment +@@ -209,14 +235,80 @@ impl AdditionalLimit { + self.checkpoint_baseline = remaining; + } + +- /// Lowers the baseline by `amount` to exclude a storage-gas debit from the open +- /// settlement window. Used by charge sites whose storage gas is not passed to the +- /// settlement macro directly (currently the SELFDESTRUCT beneficiary-creation charge, +- /// which is debited in `storage_gas_ext::selfdestruct` while the window is closed later +- /// in `compute_gas_ext::selfdestruct`). ++ /// Takes the outstanding clamp-hidden gas for restoration into the interpreter ++ /// counter. Every checkpoint calls this before running its body; the frame-end ++ /// settlement calls it before the frame result propagates. ++ #[inline] ++ pub(crate) fn checkpoint_restore_hidden(&mut self) -> u64 { ++ core::mem::take(&mut self.clamp_hidden) ++ } ++ ++ /// Computes the amount of interpreter gas to hide so the visible remaining equals the ++ /// compute headroom (the tighter of the frame-local budget and the TX-level detained ++ /// limit), records it as outstanding, and returns it for the caller to deduct from the ++ /// interpreter counter. Returns 0 — clamping disabled — for exempt transactions. ++ #[inline] ++ pub(crate) fn checkpoint_clamp_amount(&mut self, remaining: u64) -> u64 { ++ debug_assert_eq!(self.clamp_hidden, 0, "clamp applied while a clamp is outstanding"); ++ if self.has_exceeded_limit.is_exempt() { ++ return 0; ++ } ++ let (headroom, frame_local) = self.compute_gas.clamp_headroom(); ++ let hide = remaining.saturating_sub(headroom); ++ self.clamp_hidden = hide; ++ self.clamp_frame_local = frame_local; ++ hide ++ } ++ ++ /// Latches a clamp-induced fake OOG as a compute-gas limit exceed. ++ /// ++ /// The crossing opcode never executed (revm's own gas check stopped it at the clamp ++ /// boundary), so its cost is not in the recorded usage and the normal `check_limit` ++ /// pass sees usage at-or-below the limit. The latch is stamped directly, with ++ /// `frame_local` taken from the binding constraint at clamp time, so the existing ++ /// frame-result machinery (frame-local absorb to revert; TX-level mark + rescue) ++ /// produces the halt shape. + #[inline] +- pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { +- self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); ++ pub(crate) fn latch_clamp_exceed(&mut self) { ++ if self.has_exceeded_limit.within_limit() { ++ self.has_exceeded_limit = LimitCheck::ExceedsLimit { ++ kind: super::LimitKind::ComputeGas, ++ frame_local: self.clamp_frame_local, ++ limit: self.compute_gas.tx_limit(), ++ used: self.compute_gas.tx_usage(), ++ }; ++ // Preserve the volatile-detention attribution: when the binding TX-level ++ // constraint at clamp time was the detained limit, the halt must classify as ++ // `VolatileDataAccessOutOfGas` exactly as per-opcode enforcement would. ++ self.clamp_latched_detained = !self.clamp_frame_local && ++ self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); ++ } ++ } ++ ++ /// Restores any outstanding V0 clamp into the frame's final interpreter result and ++ /// latches a clamp-induced fake OOG. ++ /// ++ /// Must run before anything reads or charges the result's gas — in particular before ++ /// the execution-layer code-deposit storage charge, which would otherwise observe the ++ /// clamped copy and mis-fire an OOG on a non-exceeding CREATE frame. ++ /// ++ /// A clamp can only be outstanding when the frame ended inside a plain segment (every ++ /// checkpoint prologue restores it before its body), and an `OutOfGas` exit from such ++ /// a segment is a fake OOG: revm's own gas check stopped the crossing opcode at the ++ /// clamp boundary *before executing it* — exactly the V0 enforcement point. The latch ++ /// makes the existing frame-result machinery (frame-local absorb to revert; TX-level ++ /// mark + rescue on the restored gas) produce the halt shape. ++ pub(crate) fn restore_clamp_into_result(&mut self, result: &mut InterpreterResult) { ++ if !self.checkpoint_accounting { ++ return; ++ } ++ let hidden = self.checkpoint_restore_hidden(); ++ if hidden > 0 { ++ result.gas.erase_cost(hidden); ++ if result.result == InstructionResult::OutOfGas { ++ self.latch_clamp_exceed(); ++ } ++ } + } + + /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every +@@ -360,10 +452,15 @@ impl AdditionalLimit { + &self, + access_type: VolatileDataAccess, + ) -> Option { +- self.compute_gas.is_detained_exceed().then(|| MegaHaltReason::VolatileDataAccessOutOfGas { +- access_type, +- limit: self.compute_gas.detained_limit(), +- actual: self.compute_gas.tx_usage(), ++ // `is_detained_exceed` covers per-opcode enforcement (usage crossed the detained ++ // limit); `clamp_latched_detained` covers V0 clamp enforcement, where the crossing ++ // opcode was stopped before executing and usage stays at-or-below the limit. ++ (self.compute_gas.is_detained_exceed() || self.clamp_latched_detained).then(|| { ++ MegaHaltReason::VolatileDataAccessOutOfGas { ++ access_type, ++ limit: self.compute_gas.detained_limit(), ++ actual: self.compute_gas.tx_usage(), ++ } + }) + } + +@@ -710,16 +807,8 @@ impl AdditionalLimit { + /// indicating that the limit is exceeded. + pub(crate) fn before_frame_run( + &mut self, +- frame: &EthFrame, ++ frame: &mut EthFrame, + ) -> Option { +- // Checkpoint accounting: open the settlement window at the frame's current gas. +- // This hook runs both at frame entry and at every resume after a child frame's +- // outcome (including its returned gas) has been merged back into this frame's +- // interpreter, so the window start always sits at an instruction boundary. +- if self.checkpoint_accounting { +- self.checkpoint_baseline = frame.interpreter.gas.remaining(); +- } +- + self.state_growth.before_frame_run(frame); + self.data_size.before_frame_run(frame); + self.kv_update.before_frame_run(frame); +@@ -733,6 +822,22 @@ impl AdditionalLimit { + output, + )); + } ++ ++ // Checkpoint accounting: apply the V0 gas clamp and open the settlement window at ++ // the frame's (clamped) gas. This hook runs both at frame entry and at every ++ // resume after a child frame's outcome (including its returned gas) has been ++ // merged back into this frame's interpreter, so the window start always sits at an ++ // instruction boundary. No clamp is outstanding here: every suspension point ++ // (CALL/CREATE checkpoint prologue) and every frame end restores it first. ++ if self.checkpoint_accounting { ++ debug_assert_eq!(self.clamp_hidden, 0, "frame resumed with a clamp outstanding"); ++ let hide = self.checkpoint_clamp_amount(frame.interpreter.gas.remaining()); ++ if hide > 0 { ++ let clamped = frame.interpreter.gas.record_cost(hide); ++ debug_assert!(clamped, "clamp amount exceeds remaining gas"); ++ } ++ self.checkpoint_baseline = frame.interpreter.gas.remaining(); ++ } + None + } + +@@ -771,14 +876,18 @@ impl AdditionalLimit { + ) { + // Checkpoint accounting: the frame has produced its final action, so settle the + // tail segment (everything since the last checkpoint) against the interpreter's +- // gas counter. `frame.interpreter.gas` still holds the loop-exit value here — the +- // code-deposit storage charge in the execution-layer hook mutates only the action's +- // gas copy — so the delta telescopes exactly over the unwrapped plain opcodes. +- // A checkpoint that already settled and halted leaves `baseline == remaining` +- // (delta 0), and the CALL-family abort path's forwarded-gas `erase_cost` can only +- // raise `remaining` above the baseline, which the saturation turns into 0. ++ // gas counter. `frame.interpreter.gas` still holds the loop-exit value here (the ++ // clamp restore and the code-deposit storage charge both mutate only the action's ++ // gas copy), and both it and the baseline live in the same (clamped) domain, so ++ // the delta telescopes exactly over the unwrapped plain opcodes. A checkpoint that ++ // already settled and halted leaves `baseline == remaining` (delta 0), and the ++ // CALL-family abort path's forwarded-gas `erase_cost` can only raise `remaining` ++ // above the baseline, which the saturation turns into 0. ++ // + // Any limit exceed recorded here is latched and surfaced by the existing +- // frame-result marking below / in `before_frame_return_result`. ++ // frame-result marking below / in `before_frame_return_result`. The clamp restore ++ // itself happens earlier, in `restore_clamp_into_result`, before the ++ // execution-layer hook charges code-deposit storage against the action's gas. + if self.checkpoint_accounting { + if let InterpreterAction::Return(_) = action { + let remaining = frame.interpreter.gas.remaining(); +diff --git a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs +index 462a07f..022a499 100644 +--- a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs ++++ b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs +@@ -1,15 +1,20 @@ +-//! REX6 checkpoint compute-gas accounting (prototype). ++//! REX6 checkpoint compute-gas accounting with V0 gas-clamp enforcement (prototype). + //! +-//! Under checkpoint accounting, plain opcodes run the raw revm instructions with no per-opcode +-//! recording; compute gas settles as an interpreter-gas delta at each checkpoint (storage-gas +-//! opcodes, CALL/CREATE family, volatile opcodes, frame entry/exit). These tests pin the two +-//! sides of that trade: ++//! Plain opcodes run the raw revm instructions with no per-opcode recording; compute gas ++//! settles as an interpreter-gas delta at each checkpoint (storage-gas opcodes, CALL/CREATE ++//! family, volatile opcodes, `GAS`, frame entry/exit). Enforcement inside plain segments is ++//! the V0 gas clamp: the interpreter's visible gas is clamped to the compute headroom, so ++//! revm's own per-opcode gas checks stop a crossing opcode at the clamp boundary *before it ++//! executes* — zero overshoot. + //! +-//! - **Precision invariant**: per-transaction accounting totals are bit-identical to per-opcode +-//! recording (the interpreter gas counter telescopes over the unwrapped segment). +-//! - **Coarsened enforcement**: a limit crossing inside a plain-opcode segment surfaces at the +-//! *next checkpoint*, not at the crossing opcode, so limit-exceeding transactions overshoot by up +-//! to one segment. ++//! These tests pin the three sides of the design: ++//! ++//! - **Precision invariant**: non-exceeding transactions produce accounting totals (and receipts) ++//! bit-identical to per-opcode recording, including under an active clamp (`GAS` observability). ++//! - **Exact enforcement**: a crossing inside a plain segment halts at the crossing opcode with its ++//! cost excluded — earlier than per-opcode enforcement, which executes the crossing opcode first. ++//! - **Bounded loops**: a detention cap inside a checkpoint-free arithmetic loop is enforced by the ++//! clamp, not deferred to frame end. + + use crate::common::{transact, transact_default, CALLER, CONTRACT}; + use alloy_primitives::{Bytes, U256}; +@@ -17,7 +22,7 @@ use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, + }; +-use revm::bytecode::opcode::{POP, SSTORE, STOP, TIMESTAMP}; ++use revm::bytecode::opcode::{GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP}; + + const ONE_ETH: u128 = 1_000_000_000_000_000_000; + +@@ -27,10 +32,19 @@ fn db_with_code(code: Bytes) -> MemoryDatabase { + .account_code(CONTRACT, code) + } + ++/// Measures the intrinsic compute gas (tx base cost) with a STOP-only contract. ++fn intrinsic_compute_gas() -> u64 { ++ transact_default( ++ MegaSpecId::REX6, ++ db_with_code(BytecodeBuilder::default().append(STOP).build()), ++ ) ++ .compute_gas ++} ++ + /// A countdown loop of cheap opcodes with no checkpoint inside the loop body: + /// + /// ```text +-/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP ++/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP + /// ``` + /// + /// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and +@@ -51,12 +65,8 @@ fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { + Bytes::from(code) + } + +-fn countdown_loop_code(iterations: u16) -> Bytes { +- countdown_loop_code_with_prefix(&[], iterations) +-} +- + /// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then +-/// `SSTORE(7, 99)` (the first checkpoint in the code), then STOP. ++/// `SSTORE(7, 99)` and STOP. + fn plain_run_then_sstore_code(pairs: usize) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for _ in 0..pairs { +@@ -78,7 +88,7 @@ fn plain_run_then_sstore_code(pairs: usize) -> Bytes { + /// checkpoint. + #[test] + fn test_checkpoint_plain_loop_totals_match_rex5() { +- let code = countdown_loop_code(500); ++ let code = countdown_loop_code_with_prefix(&[], 500); + let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); + let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); + +@@ -91,22 +101,47 @@ fn test_checkpoint_plain_loop_totals_match_rex5() { + assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); + } + +-/// A compute-gas crossing inside a plain-opcode segment surfaces at the next checkpoint. +-/// +-/// The limit is placed mid-way through a straight plain-opcode run that ends in an SSTORE. +-/// REX5 (per-opcode) halts at the crossing opcode; REX6 (checkpoint) runs the rest of the +-/// segment and halts at the SSTORE settlement, recording the full segment — the same total a +-/// generous-limit run records up to that point. Both halt; the checkpoint run overshoots. ++/// `GAS` observability under an active clamp: with a tight detention cap the interpreter's ++/// visible gas is clamped for the whole post-access run, yet the value `GAS` pushes must be ++/// the true remaining — the checkpoint prologue restores the clamp before the raw ++/// instruction reads the counter. The returned word and the receipt must be bit-identical ++/// to per-opcode REX5, where no clamp exists at all. + #[test] +-fn test_checkpoint_halt_lands_at_next_checkpoint() { +- let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes ++fn test_v0_clamp_is_unobservable_via_gas_opcode() { ++ // TIMESTAMP; POP; GAS; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN ++ let code = ++ Bytes::from(vec![TIMESTAMP, POP, GAS, 0x60, 0x00, MSTORE, 0x60, 0x20, 0x60, 0x00, RETURN]); ++ // Detention cap 1,000: far below the interpreter's remaining gas, so the clamp is ++ // active at the GAS opcode; the tx itself stays far below the cap (non-exceeding). ++ let limits = |spec| { ++ let mut l = EvmTxRuntimeLimits::from_spec(spec); ++ l.block_env_access_compute_gas_limit = 1_000; ++ l ++ }; + +- // Intrinsic compute gas (tx base cost) measured with a STOP-only contract. +- let intrinsic = transact_default( +- MegaSpecId::REX6, +- db_with_code(BytecodeBuilder::default().append(STOP).build()), +- ) +- .compute_gas; ++ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); ++ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); ++ ++ assert!(r5.is_success(), "REX5 must succeed: {:?}", r5.result); ++ assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); ++ assert_eq!( ++ r5.result.output(), ++ r6.result.output(), ++ "GAS must push the true remaining under the clamp" ++ ); ++ assert_eq!(r5.compute_gas, r6.compute_gas, "compute totals must be identical"); ++ assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be identical"); ++} ++ ++/// V0 exact enforcement in a plain segment: the limit is placed mid-way through a straight ++/// plain-opcode run. REX5 (per-opcode) executes the crossing opcode and then halts, so its ++/// recorded usage exceeds the limit; REX6 (clamp) fake-OOGs the crossing opcode at the ++/// clamp boundary *before executing it*, so its recorded usage stays at or below the limit ++/// and the halt lands one opcode earlier — zero overshoot. ++#[test] ++fn test_v0_clamp_halts_at_crossing_opcode_without_executing_it() { ++ let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes ++ let intrinsic = intrinsic_compute_gas(); + + // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. + let compute_limit = intrinsic + 300; +@@ -114,72 +149,90 @@ fn test_checkpoint_halt_lands_at_next_checkpoint() { + |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); + + let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); +- let r6 = transact(MegaSpecId::REX6, db_with_code(code.clone()), limits(MegaSpecId::REX6)); +- let r6_full = transact_default(MegaSpecId::REX6, db_with_code(code)); ++ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); + + assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); + assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); + +- // REX5 stops within one opcode of the crossing. ++ // Per-opcode enforcement records the crossing opcode: usage strictly exceeds the limit. + assert!( +- r5.compute_gas <= compute_limit + 12, +- "REX5 must stop at the crossing opcode; compute={} limit={compute_limit}", ++ r5.compute_gas > compute_limit, ++ "REX5 executes the crossing opcode before halting; compute={} limit={compute_limit}", + r5.compute_gas + ); +- // REX6 records the whole segment up to (and including) the SSTORE checkpoint: identical +- // to what a run without the tight limit records at that point (SSTORE is the last +- // gas-consuming opcode, so the full-run total equals the at-checkpoint total). +- assert_eq!( +- r6.compute_gas, r6_full.compute_gas, +- "REX6 must settle the full segment at the checkpoint" ++ // Clamp enforcement stops the crossing opcode before execution: usage stays at or ++ // below the limit. ++ assert!( ++ r6.compute_gas <= compute_limit, ++ "REX6 must not execute the crossing opcode; compute={} limit={compute_limit}", ++ r6.compute_gas + ); + assert!( +- r6.compute_gas > r5.compute_gas, +- "checkpoint enforcement overshoots per-opcode enforcement; REX5={} REX6={}", +- r5.compute_gas, ++ r6.compute_gas > compute_limit - 12, ++ "REX6 must stop at the clamp boundary, not earlier; compute={} limit={compute_limit}", + r6.compute_gas + ); + } + +-/// Pins the V1 checkpoint-set gap: a loop of plain opcodes contains **no checkpoint**, so a +-/// detention cap crossing inside it is only enforced at the frame-end settlement — the +-/// overshoot is bounded by the interpreter gas budget, NOT by the ~73k straight-line +-/// code-size bound from the design spec ("a straight line cannot loop" only holds when +-/// JUMP/JUMPI are checkpoints, which V1 deliberately excludes). +-/// +-/// A ~14-byte contract overshoots the detention cap by >100k gas here. If the V1 set is kept +-/// for the final spec, the overshoot bound must be stated as the remaining interpreter gas; +-/// bounding it by code size requires JUMP/JUMPI checkpoints (V2) or another backstop. ++/// V0 bounds detention inside a checkpoint-free plain loop — the shape that broke V1 ++/// (which deferred enforcement to frame end, overshooting by the whole loop). The clamp ++/// stops the loop at the cap boundary regardless of the absence of checkpoints. + #[test] +-fn test_checkpoint_v1_loop_segment_is_not_code_size_bounded() { +- // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a 10,000-iteration +- // countdown loop (~260k gas) with no checkpoint inside. ++fn test_v0_clamp_bounds_detention_inside_plain_loop() { ++ // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a ++ // 10,000-iteration countdown loop (~260k gas) with no checkpoint inside. + let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); ++ let intrinsic = intrinsic_compute_gas(); + +- let mut limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6); +- limits.block_env_access_compute_gas_limit = 1_000; +- +- let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), { +- let mut l = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5); ++ let limits = |spec| { ++ let mut l = EvmTxRuntimeLimits::from_spec(spec); + l.block_env_access_compute_gas_limit = 1_000; + l +- }); +- let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits); ++ }; ++ ++ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); ++ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); + + assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); + assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); + +- // REX5 enforces within one opcode of the cap crossing (cap ≈ intrinsic + 1,000). ++ // The detained limit is usage-at-access + 1,000 ≈ intrinsic + TIMESTAMP + 1,000. ++ let detained_limit_upper = intrinsic + 2 + 1_000; ++ // Per-opcode enforcement executes the crossing opcode: usage strictly exceeds the cap. + assert!( +- r5.compute_gas < 30_000, ++ r5.compute_gas > detained_limit_upper - 12, + "REX5 must stop near the detention cap; compute={}", + r5.compute_gas + ); +- // REX6 runs the entire loop before any checkpoint settles: >100k gas past the cap from a +- // 16-byte contract — the overshoot is not bounded by code size under V1. ++ // Clamp enforcement stops at the boundary without executing the crossing opcode, ++ // despite the loop containing no checkpoint at all. + assert!( +- r6.compute_gas > 100_000, +- "REX6 V1 loop segment must overshoot far past the code-size bound; compute={}", ++ r6.compute_gas <= detained_limit_upper, ++ "REX6 must not overshoot the detention cap; compute={} cap≈{detained_limit_upper}", + r6.compute_gas + ); ++ assert!( ++ r6.compute_gas > detained_limit_upper - 30, ++ "REX6 must stop at the clamp boundary, not earlier; compute={}", ++ r6.compute_gas ++ ); ++ ++ // Attribution parity: a clamp-stopped detention exceed must classify as ++ // `VolatileDataAccessOutOfGas` exactly like per-opcode enforcement, even though the ++ // recorded usage never crossed the detained limit (the crossing opcode was stopped ++ // before executing). ++ let halt_reason = |label: &str, r: &crate::common::Outcome| match &r.result { ++ revm::context::result::ExecutionResult::Halt { reason, .. } => format!("{reason:?}"), ++ other => panic!("{label}: expected a halt, got {other:?}"), ++ }; ++ let r5_reason = halt_reason("REX5", &r5); ++ let r6_reason = halt_reason("REX6", &r6); ++ assert!( ++ r5_reason.starts_with("VolatileDataAccessOutOfGas"), ++ "REX5 must attribute the halt to volatile detention; got {r5_reason}" ++ ); ++ assert!( ++ r6_reason.starts_with("VolatileDataAccessOutOfGas"), ++ "REX6 clamp halt must keep the volatile detention attribution; got {r6_reason}" ++ ); + } +-- +2.50.1 (Apple Git-155) + diff --git a/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch b/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch new file mode 100644 index 00000000..b6aeab89 --- /dev/null +++ b/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch @@ -0,0 +1,732 @@ +From a6b0f32aa05900aad9cc1f01fabe2c55ab7c749c Mon Sep 17 00:00:00 2001 +From: RealiCZ +Date: Fri, 24 Jul 2026 18:10:33 +0800 +Subject: [PATCH] feat(rex6): prototype checkpoint-based compute-gas accounting + +Plain opcodes in the REX6 instruction table run the raw revm instructions +with no per-opcode recording; compute gas settles as an interpreter-gas +delta at each checkpoint (storage-gas opcodes, CALL/CREATE family, +volatile/detention opcodes, frame entry/exit). Accounting totals telescope +to the same per-transaction sums as per-opcode recording; only the halt +position for limit-exceeding transactions coarsens to the next checkpoint. +Specs <= REX5 are byte-for-byte unchanged. +--- + crates/mega-evm/src/evm/instructions.rs | 340 ++++++++++++++++-- + crates/mega-evm/src/limit/limit.rs | 69 ++++ + .../tests/rex6/checkpoint_accounting.rs | 185 ++++++++++ + crates/mega-evm/tests/rex6/main.rs | 1 + + 4 files changed, 575 insertions(+), 20 deletions(-) + create mode 100644 crates/mega-evm/tests/rex6/checkpoint_accounting.rs + +diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs +index 03c35ba..f15fee4 100644 +--- a/crates/mega-evm/src/evm/instructions.rs ++++ b/crates/mega-evm/src/evm/instructions.rs +@@ -379,22 +379,30 @@ mod rex5 { + mod rex6 { + use super::*; + +- /// Returns the instruction table for the `REX6` spec. ++ /// Returns the instruction table for the `REX6` spec — **checkpoint compute-gas ++ /// accounting** (prototype). + /// +- /// Changes from Rex5: the instruction *table* is unchanged (same handler functions as Rex5). +- /// Every Rex6 behavior difference is expressed as internal `spec.is_enabled(MegaSpecId::REX6)` +- /// dispatch inside the shared handlers, never as a swapped table entry: +- /// - the storage-affecting handlers (SSTORE, LOG, CALL-family, CREATE/CREATE2) charge storage +- /// gas, run their body, then record compute gas exactly once (via +- /// [`record_storage_compute_gas!`]) with the storage gas excluded; SELFDESTRUCT keeps its +- /// delegation to `compute_gas_ext::selfdestruct`, whose trailing all-dimension check records +- /// the same single compute window while latching the pre-recorded data/KV/state usage; +- /// - `storage_gas_ext::selfdestruct` additionally records existing-target balance-update +- /// accounting, and its outer volatile wrapper +- /// (`volatile_data_ext::selfdestruct_with_beneficiary_guard`) additionally guards the +- /// executing contract (source) against the beneficiary; +- /// - the CALL-family volatile wrappers, on the `disableVolatileDataAccess` path, resolve the +- /// stack target's one-hop EIP-7702 delegate before the beneficiary comparison. ++ /// Unlike every earlier custom table, plain opcodes are wired to the raw revm mainnet ++ /// instructions with no per-opcode gas recording: the interpreter's own gas counter is ++ /// the accounting source, and compute gas settles as a segment delta at each ++ /// checkpoint. The checkpoints are exactly the positions that had to stay wrapped ++ /// anyway: ++ /// - storage-gas opcodes (SSTORE, LOG0–LOG4, SELFDESTRUCT) and the CALL/CREATE family — their ++ /// existing handler chains settle from the checkpoint baseline inside ++ /// [`record_storage_compute_gas!`] (spec-dispatched, so ≤REX5 tables are untouched); ++ /// - volatile / detention opcodes — `*_checkpoint` variants that run the raw instruction, ++ /// settle the segment, then apply the detention cap; ++ /// - frame entry/exit — `AdditionalLimit::before_frame_run` re-opens the window and ++ /// `after_frame_run_instructions` settles the tail segment. ++ /// ++ /// Accounting totals telescope to the same per-transaction sums as per-opcode ++ /// recording; only the halt position for limit-exceeding transactions coarsens to the ++ /// next checkpoint. ++ /// ++ /// The pre-existing REX6 behavior differences (canonical metering order, CREATE ++ /// `create_rex6` dispatch, SELFDESTRUCT existing-target accounting, CALL-family ++ /// EIP-7702 delegate resolution on the disabled path) live as internal ++ /// `spec.is_enabled(MegaSpecId::REX6)` dispatch inside the shared handlers reused here. + pub(super) const fn instruction_table< + WIRE: InterpreterTypes, + H: HostExt + ContextTr + JournalInspectTr + ?Sized, +@@ -402,7 +410,43 @@ mod rex6 { + where + WIRE::Stack: StackInspectTr, + { +- rex5::instruction_table::() ++ use revm::bytecode::opcode::*; ++ let mut table = instructions::instruction_table::(); ++ ++ // Volatile / detention checkpoints (raw instruction + segment settlement + cap). ++ table[BALANCE as usize] = volatile_data_ext::balance_checkpoint; ++ table[EXTCODESIZE as usize] = volatile_data_ext::extcodesize_checkpoint; ++ table[EXTCODECOPY as usize] = volatile_data_ext::extcodecopy_checkpoint; ++ table[EXTCODEHASH as usize] = volatile_data_ext::extcodehash_checkpoint; ++ table[BLOCKHASH as usize] = volatile_data_ext::blockhash_checkpoint; ++ table[COINBASE as usize] = volatile_data_ext::coinbase_checkpoint; ++ table[TIMESTAMP as usize] = volatile_data_ext::timestamp_checkpoint; ++ table[NUMBER as usize] = volatile_data_ext::block_number_checkpoint; ++ table[DIFFICULTY as usize] = volatile_data_ext::difficulty_checkpoint; ++ table[GASLIMIT as usize] = volatile_data_ext::gas_limit_opcode_checkpoint; ++ table[BASEFEE as usize] = volatile_data_ext::basefee_checkpoint; ++ table[BLOBBASEFEE as usize] = volatile_data_ext::blobbasefee_checkpoint; ++ table[BLOBHASH as usize] = volatile_data_ext::blobhash_checkpoint; ++ table[SELFBALANCE as usize] = volatile_data_ext::selfbalance_checkpoint; ++ table[SLOAD as usize] = volatile_data_ext::sload_checkpoint; ++ ++ // Storage-gas checkpoints — the same handler chains as REX5; under REX6 they ++ // settle from the checkpoint baseline internally. ++ table[SSTORE as usize] = additional_limit_ext::sstore; ++ table[LOG0 as usize] = additional_limit_ext::log::<0, _, _>; ++ table[LOG1 as usize] = additional_limit_ext::log::<1, _, _>; ++ table[LOG2 as usize] = additional_limit_ext::log::<2, _, _>; ++ table[LOG3 as usize] = additional_limit_ext::log::<3, _, _>; ++ table[LOG4 as usize] = additional_limit_ext::log::<4, _, _>; ++ table[CREATE as usize] = forward_gas_ext::create; ++ table[CREATE2 as usize] = forward_gas_ext::create2; ++ table[CALL as usize] = volatile_data_ext::call; ++ table[STATICCALL as usize] = volatile_data_ext::static_call; ++ table[DELEGATECALL as usize] = volatile_data_ext::delegate_call; ++ table[CALLCODE as usize] = volatile_data_ext::call_code; ++ table[SELFDESTRUCT as usize] = volatile_data_ext::selfdestruct_with_beneficiary_guard; ++ ++ table + } + } + +@@ -463,8 +507,20 @@ macro_rules! run_inner_instruction_or_abort { + /// add gas to the tracker after the OOG was already set. + macro_rules! record_storage_compute_gas { + ($context:expr, $gas_before:expr, $storage_charged:expr) => {{ ++ let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); + let gas_after = $context.interpreter.gas.remaining(); +- let mut gas_used = $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged); ++ // REX6 checkpoint accounting: the measurement window opens at the last checkpoint ++ // (frame entry / resume or the previous checkpoint opcode), not at this opcode's own ++ // start, so the unwrapped plain opcodes executed since then settle here in the same ++ // recording. Only plain opcodes can run between two checkpoints, so no storage gas ++ // or forwarded child gas is hiding in the extra window — the exclusions below stay ++ // exact. Pre-REX6 keeps the per-opcode `$gas_before` window byte-for-byte. ++ let window_start = if is_rex6 { ++ $context.host.additional_limit().borrow().checkpoint_baseline() ++ } else { ++ $gas_before ++ }; ++ let mut gas_used = window_start.saturating_sub(gas_after).saturating_sub($storage_charged); + // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` + // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the + // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit +@@ -494,9 +550,15 @@ macro_rules! record_storage_compute_gas { + // On a compute-limit halt the pending child `NewFrame` is discarded (the child never runs), + // but revm already deducted the forwarded gas and the outer `forward_gas_ext` erase is + // skipped on this abort path. REX6+: return that gas to the parent before halting. +- let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); + let exceeding_result = { + let mut additional_limit = $context.host.additional_limit().borrow_mut(); ++ // Checkpoint accounting: re-open the window at this opcode's exit before the ++ // record, so a halt (here or at frame end) never re-settles this segment. The ++ // abort-path `erase_cost` below can only raise the interpreter gas above this ++ // baseline, which the frame-end settlement saturates to 0. ++ if is_rex6 { ++ additional_limit.sync_checkpoint_baseline(gas_after); ++ } + if additional_limit.record_compute_gas(gas_used) { + None + } else { +@@ -1263,6 +1325,219 @@ pub mod volatile_data_ext { + wrap_call_volatile_check!(static_call, "STATICCALL", forward_gas_ext::static_call); + wrap_call_volatile_check!(delegate_call, "DELEGATECALL", forward_gas_ext::delegate_call); + wrap_call_volatile_check!(call_code, "CALLCODE", forward_gas_ext::call_code); ++ ++ /* REX6 checkpoint-accounting volatile handlers. ++ ++ Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints), ++ but they run the raw revm instruction and settle the open segment — everything since ++ the last checkpoint, measured on the interpreter's own gas counter — in one recording, ++ instead of delegating to a per-opcode `compute_gas_ext` wrapper. The settlement runs ++ before `apply_compute_gas_limit!` so a REX4+ relative detention cap is derived from the ++ fully settled usage at the access point, exactly as the per-opcode order did. */ ++ ++ /// Settles the open checkpoint segment against the interpreter gas counter, re-opens ++ /// the window, and halts (returning from the enclosing handler) when a limit — ++ /// including a latched non-compute exceed — surfaces. ++ macro_rules! settle_checkpoint_compute_gas { ++ ($context:expr) => { ++ let exceeding_result = { ++ let gas_after = $context.interpreter.gas.remaining(); ++ let mut additional_limit = $context.host.additional_limit().borrow_mut(); ++ let gas_used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); ++ additional_limit.sync_checkpoint_baseline(gas_after); ++ if additional_limit.record_compute_gas(gas_used) { ++ None ++ } else { ++ Some(additional_limit.exceeding_instruction_result()) ++ } ++ }; ++ if let Some(result) = exceeding_result { ++ $context.interpreter.halt(result); ++ return; ++ } ++ }; ++ } ++ ++ /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, raw ++ /// revm instruction, segment settlement, detention cap. ++ macro_rules! wrap_checkpoint_detain_gas_unconditional { ++ ($fn_name:ident, $opcode_name:expr, $original_fn:path, $access_type:expr) => { ++ #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] ++ #[inline] ++ pub fn $fn_name( ++ context: InstructionContext<'_, H, WIRE>, ++ ) { ++ // Rex4+ (always enabled under REX6): revert before executing if volatile data ++ // access is disabled. ++ if context.host.volatile_access_disabled() { ++ context.interpreter.bytecode.set_action(InterpreterAction::new_return( ++ InstructionResult::Revert, ++ volatile_data_access_disabled_revert_data($access_type), ++ context.interpreter.gas, ++ )); ++ return; ++ } ++ ++ run_inner_instruction_or_abort!($original_fn, context); ++ settle_checkpoint_compute_gas!(context); ++ apply_compute_gas_limit!(context); ++ } ++ }; ++ } ++ ++ /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw ++ /// revm instruction, segment settlement, detention cap. ++ macro_rules! wrap_checkpoint_detain_gas_conditional { ++ ($fn_name:ident, $opcode_name:expr, $original_fn:path) => { ++ #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] ++ #[inline] ++ pub fn $fn_name< ++ WIRE: InterpreterTypes, ++ H: HostExt + ContextTr + JournalInspectTr + ?Sized, ++ >( ++ context: InstructionContext<'_, H, WIRE>, ++ ) { ++ if let Some(addr_word) = context.interpreter.stack.inspect::<0>() { ++ let target: Address = addr_word.into_address(); ++ let beneficiary = context.host.beneficiary_address(); ++ if target == beneficiary && context.host.volatile_access_disabled() { ++ context.interpreter.bytecode.set_action(InterpreterAction::new_return( ++ InstructionResult::Revert, ++ volatile_data_access_disabled_revert_data( ++ VolatileDataAccessType::Beneficiary, ++ ), ++ context.interpreter.gas, ++ )); ++ return; ++ } ++ } ++ ++ run_inner_instruction_or_abort!($original_fn, context); ++ settle_checkpoint_compute_gas!(context); ++ apply_compute_gas_limit!(context); ++ } ++ }; ++ } ++ ++ wrap_checkpoint_detain_gas_unconditional!( ++ timestamp_checkpoint, ++ "TIMESTAMP", ++ instructions::block_info::timestamp, ++ VolatileDataAccessType::Timestamp ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ block_number_checkpoint, ++ "NUMBER", ++ instructions::block_info::block_number, ++ VolatileDataAccessType::BlockNumber ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ difficulty_checkpoint, ++ "DIFFICULTY", ++ instructions::block_info::difficulty, ++ VolatileDataAccessType::Difficulty ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ gas_limit_opcode_checkpoint, ++ "GASLIMIT", ++ instructions::block_info::gaslimit, ++ VolatileDataAccessType::GasLimit ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ basefee_checkpoint, ++ "BASEFEE", ++ instructions::block_info::basefee, ++ VolatileDataAccessType::BaseFee ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ coinbase_checkpoint, ++ "COINBASE", ++ instructions::block_info::coinbase, ++ VolatileDataAccessType::Coinbase ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ blockhash_checkpoint, ++ "BLOCKHASH", ++ instructions::host::blockhash, ++ VolatileDataAccessType::BlockHash ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ blobbasefee_checkpoint, ++ "BLOBBASEFEE", ++ instructions::block_info::blob_basefee, ++ VolatileDataAccessType::BlobBaseFee ++ ); ++ wrap_checkpoint_detain_gas_unconditional!( ++ blobhash_checkpoint, ++ "BLOBHASH", ++ instructions::tx_info::blob_hash, ++ VolatileDataAccessType::BlobHash ++ ); ++ ++ wrap_checkpoint_detain_gas_conditional!( ++ balance_checkpoint, ++ "BALANCE", ++ instructions::host::balance ++ ); ++ wrap_checkpoint_detain_gas_conditional!( ++ extcodesize_checkpoint, ++ "EXTCODESIZE", ++ instructions::host::extcodesize ++ ); ++ wrap_checkpoint_detain_gas_conditional!( ++ extcodecopy_checkpoint, ++ "EXTCODECOPY", ++ instructions::host::extcodecopy ++ ); ++ wrap_checkpoint_detain_gas_conditional!( ++ extcodehash_checkpoint, ++ "EXTCODEHASH", ++ instructions::host::extcodehash ++ ); ++ ++ /// `SLOAD` as a REX6 checkpoint. Same oracle-volatile handling as [`sload`], but the ++ /// raw revm instruction runs unwrapped and the open segment settles here. ++ #[inline] ++ pub fn sload_checkpoint( ++ context: InstructionContext<'_, H, WIRE>, ++ ) { ++ let target = context.interpreter.input.target_address(); ++ if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { ++ context.interpreter.bytecode.set_action(InterpreterAction::new_return( ++ InstructionResult::Revert, ++ volatile_data_access_disabled_revert_data(VolatileDataAccessType::Oracle), ++ context.interpreter.gas, ++ )); ++ return; ++ } ++ ++ run_inner_instruction_or_abort!(instructions::host::sload, context); ++ settle_checkpoint_compute_gas!(context); ++ apply_compute_gas_limit!(context); ++ } ++ ++ /// `SELFBALANCE` as a REX6 checkpoint. Same beneficiary-volatile handling as ++ /// [`selfbalance`], but the raw revm instruction runs unwrapped and the open segment ++ /// settles here. ++ #[inline] ++ pub fn selfbalance_checkpoint( ++ context: InstructionContext<'_, H, WIRE>, ++ ) { ++ let target = context.interpreter.input.target_address(); ++ let beneficiary = context.host.beneficiary_address(); ++ if target == beneficiary && context.host.volatile_access_disabled() { ++ context.interpreter.bytecode.set_action(InterpreterAction::new_return( ++ InstructionResult::Revert, ++ volatile_data_access_disabled_revert_data(VolatileDataAccessType::Beneficiary), ++ context.interpreter.gas, ++ )); ++ return; ++ } ++ ++ run_inner_instruction_or_abort!(instructions::host::selfbalance, context); ++ settle_checkpoint_compute_gas!(context); ++ apply_compute_gas_limit!(context); ++ } + } + + /// Extends opcodes with additional limit (kv update limit, data limit, etc.) enforcement. +@@ -2030,7 +2305,20 @@ pub mod storage_gas_ext { + }; + let drained = + context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); +- gas!(context.interpreter, cost - drained); ++ let storage_charged = cost - drained; ++ gas!(context.interpreter, storage_charged); ++ ++ // REX6 checkpoint accounting: this storage debit sits inside the window that ++ // `compute_gas_ext::selfdestruct` closes later, so exclude it by lowering the ++ // baseline now — the trailing settlement takes `baseline - remaining` with no ++ // storage term of its own. ++ if context.host.spec_id().is_enabled(MegaSpecId::REX6) { ++ context ++ .host ++ .additional_limit() ++ .borrow_mut() ++ .deduct_checkpoint_baseline(storage_charged); ++ } + + // Record resource usage for new beneficiary account + context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); +@@ -2299,8 +2587,20 @@ pub mod compute_gas_ext { + // Call the original instruction + run_inner_instruction_or_abort!(instructions::host::selfdestruct, context); + +- let gas_used = gas_before.saturating_sub(context.interpreter.gas.remaining()); ++ // REX6 checkpoint accounting: the window opens at the checkpoint baseline, folding ++ // in the unwrapped plain opcodes since the last checkpoint. The beneficiary-creation ++ // storage charge in `storage_gas_ext::selfdestruct` already lowered the baseline by ++ // the charged amount, so no storage exclusion is needed here. Pre-REX6 keeps the ++ // per-opcode `gas_before` window. ++ let is_rex6 = context.host.spec_id().is_enabled(MegaSpecId::REX6); ++ let gas_after = context.interpreter.gas.remaining(); + let mut additional_limit = context.host.additional_limit().borrow_mut(); ++ let window_start = ++ if is_rex6 { additional_limit.checkpoint_baseline() } else { gas_before }; ++ let gas_used = window_start.saturating_sub(gas_after); ++ if is_rex6 { ++ additional_limit.sync_checkpoint_baseline(gas_after); ++ } + if !additional_limit.record_compute_gas_all_dims(gas_used) { + context.interpreter.halt(additional_limit.exceeding_instruction_result()); + } +diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs +index 2bcec8b..1b01e61 100644 +--- a/crates/mega-evm/src/limit/limit.rs ++++ b/crates/mega-evm/src/limit/limit.rs +@@ -107,6 +107,19 @@ pub struct AdditionalLimit { + + /// A tracker for the `STORAGE_CALL_STIPEND` granted to value-transferring calls (REX4+). + pub(crate) storage_call_stipend: storage_call_stipend::StorageCallStipendTracker, ++ ++ /// REX6+ checkpoint accounting: whether compute gas settles at checkpoints instead of ++ /// per opcode. Plain opcodes run unwrapped; the interpreter's own gas counter is read ++ /// at each checkpoint and the segment delta is recorded in one shot. ++ checkpoint_accounting: bool, ++ ++ /// Interpreter gas remaining at the start of the current unsettled segment (the last ++ /// checkpoint, frame entry, or frame resume). Only meaningful while a frame is running ++ /// and only when `checkpoint_accounting` is active. Re-synced at every ++ /// `before_frame_run` (which covers both frame entry and every resume after a child ++ /// frame's outcome is merged back), at every checkpoint settlement, and lowered by ++ /// storage-gas charge sites that debit interpreter gas outside a settlement window. ++ checkpoint_baseline: u64, + } + + /// The usage of the additional limits. +@@ -134,6 +147,8 @@ impl AdditionalLimit { + kv_update: kv_update::KVUpdateTracker::new(spec, limits.tx_kv_updates_limit), + compute_gas: compute_gas::ComputeGasTracker::new(spec, limits.tx_compute_gas_limit), + storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), ++ checkpoint_accounting: spec.is_enabled(MegaSpecId::REX6), ++ checkpoint_baseline: 0, + } + } + } +@@ -175,6 +190,33 @@ impl AdditionalLimit { + self.data_size.reset(); + self.kv_update.reset(); + self.storage_call_stipend.reset(); ++ self.checkpoint_baseline = 0; ++ } ++ ++ /// Interpreter gas remaining at the start of the current unsettled segment ++ /// (REX6+ checkpoint accounting). Checkpoint settlement sites use this instead of a ++ /// per-opcode `gas_before` capture, so the delta covers every unwrapped plain opcode ++ /// executed since the previous checkpoint. ++ #[inline] ++ pub(crate) fn checkpoint_baseline(&self) -> u64 { ++ self.checkpoint_baseline ++ } ++ ++ /// Re-opens the settlement window at `remaining`. Called by every checkpoint after it ++ /// settles, and by `before_frame_run` at frame entry / resume. ++ #[inline] ++ pub(crate) fn sync_checkpoint_baseline(&mut self, remaining: u64) { ++ self.checkpoint_baseline = remaining; ++ } ++ ++ /// Lowers the baseline by `amount` to exclude a storage-gas debit from the open ++ /// settlement window. Used by charge sites whose storage gas is not passed to the ++ /// settlement macro directly (currently the SELFDESTRUCT beneficiary-creation charge, ++ /// which is debited in `storage_gas_ext::selfdestruct` while the window is closed later ++ /// in `compute_gas_ext::selfdestruct`). ++ #[inline] ++ pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { ++ self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); + } + + /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every +@@ -670,6 +712,14 @@ impl AdditionalLimit { + &mut self, + frame: &EthFrame, + ) -> Option { ++ // Checkpoint accounting: open the settlement window at the frame's current gas. ++ // This hook runs both at frame entry and at every resume after a child frame's ++ // outcome (including its returned gas) has been merged back into this frame's ++ // interpreter, so the window start always sits at an instruction boundary. ++ if self.checkpoint_accounting { ++ self.checkpoint_baseline = frame.interpreter.gas.remaining(); ++ } ++ + self.state_growth.before_frame_run(frame); + self.data_size.before_frame_run(frame); + self.kv_update.before_frame_run(frame); +@@ -719,6 +769,25 @@ impl AdditionalLimit { + frame: &'a EthFrame, + action: &'a mut InterpreterAction, + ) { ++ // Checkpoint accounting: the frame has produced its final action, so settle the ++ // tail segment (everything since the last checkpoint) against the interpreter's ++ // gas counter. `frame.interpreter.gas` still holds the loop-exit value here — the ++ // code-deposit storage charge in the execution-layer hook mutates only the action's ++ // gas copy — so the delta telescopes exactly over the unwrapped plain opcodes. ++ // A checkpoint that already settled and halted leaves `baseline == remaining` ++ // (delta 0), and the CALL-family abort path's forwarded-gas `erase_cost` can only ++ // raise `remaining` above the baseline, which the saturation turns into 0. ++ // Any limit exceed recorded here is latched and surfaced by the existing ++ // frame-result marking below / in `before_frame_return_result`. ++ if self.checkpoint_accounting { ++ if let InterpreterAction::Return(_) = action { ++ let remaining = frame.interpreter.gas.remaining(); ++ let gas_used = self.checkpoint_baseline.saturating_sub(remaining); ++ self.checkpoint_baseline = remaining; ++ let _ = self.record_compute_gas(gas_used); ++ } ++ } ++ + self.state_growth.after_frame_run(frame, action); + self.data_size.after_frame_run(frame, action); + self.kv_update.after_frame_run(frame, action); +diff --git a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs +new file mode 100644 +index 0000000..462a07f +--- /dev/null ++++ b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs +@@ -0,0 +1,185 @@ ++//! REX6 checkpoint compute-gas accounting (prototype). ++//! ++//! Under checkpoint accounting, plain opcodes run the raw revm instructions with no per-opcode ++//! recording; compute gas settles as an interpreter-gas delta at each checkpoint (storage-gas ++//! opcodes, CALL/CREATE family, volatile opcodes, frame entry/exit). These tests pin the two ++//! sides of that trade: ++//! ++//! - **Precision invariant**: per-transaction accounting totals are bit-identical to per-opcode ++//! recording (the interpreter gas counter telescopes over the unwrapped segment). ++//! - **Coarsened enforcement**: a limit crossing inside a plain-opcode segment surfaces at the ++//! *next checkpoint*, not at the crossing opcode, so limit-exceeding transactions overshoot by up ++//! to one segment. ++ ++use crate::common::{transact, transact_default, CALLER, CONTRACT}; ++use alloy_primitives::{Bytes, U256}; ++use mega_evm::{ ++ test_utils::{BytecodeBuilder, MemoryDatabase}, ++ EvmTxRuntimeLimits, MegaSpecId, ++}; ++use revm::bytecode::opcode::{POP, SSTORE, STOP, TIMESTAMP}; ++ ++const ONE_ETH: u128 = 1_000_000_000_000_000_000; ++ ++fn db_with_code(code: Bytes) -> MemoryDatabase { ++ MemoryDatabase::default() ++ .account_balance(CALLER, U256::from(10 * ONE_ETH)) ++ .account_code(CONTRACT, code) ++} ++ ++/// A countdown loop of cheap opcodes with no checkpoint inside the loop body: ++/// ++/// ```text ++/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP ++/// ``` ++/// ++/// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and ++/// participates in the jump-target offset. ++fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { ++ let mut code = prefix.to_vec(); ++ code.push(0x61); // PUSH2 ++ code.extend_from_slice(&iterations.to_be_bytes()); ++ let loop_target = code.len() as u8; ++ code.push(0x5b); // JUMPDEST ++ code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 ++ code.push(0x90); // SWAP1 ++ code.push(0x03); // SUB ++ code.push(0x80); // DUP1 ++ code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop ++ code.push(0x57); // JUMPI ++ code.push(0x00); // STOP ++ Bytes::from(code) ++} ++ ++fn countdown_loop_code(iterations: u16) -> Bytes { ++ countdown_loop_code_with_prefix(&[], iterations) ++} ++ ++/// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then ++/// `SSTORE(7, 99)` (the first checkpoint in the code), then STOP. ++fn plain_run_then_sstore_code(pairs: usize) -> Bytes { ++ let mut builder = BytecodeBuilder::default(); ++ for _ in 0..pairs { ++ builder = builder.push_number(1u64); ++ builder = builder.append(POP); ++ } ++ builder ++ .push_u256(U256::from(99u64)) // value ++ .push_u256(U256::from(7u64)) // slot ++ .append(SSTORE) ++ .append(STOP) ++ .build() ++} ++ ++/// Precision invariant on a plain-opcode hot loop: with no limit crossing, checkpoint ++/// accounting (REX6) must produce the same compute-gas total and receipt `gas_used` as ++/// per-opcode accounting (REX5) — the segment delta telescopes over exactly the opcodes the ++/// per-opcode wrappers would have recorded, and the loop's only settlement is the frame-end ++/// checkpoint. ++#[test] ++fn test_checkpoint_plain_loop_totals_match_rex5() { ++ let code = countdown_loop_code(500); ++ let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); ++ let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); ++ ++ assert!(r5.is_success(), "REX5 loop must succeed: {:?}", r5.result); ++ assert!(r6.is_success(), "REX6 loop must succeed: {:?}", r6.result); ++ assert_eq!( ++ r5.compute_gas, r6.compute_gas, ++ "checkpoint totals must telescope to the per-opcode sum" ++ ); ++ assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); ++} ++ ++/// A compute-gas crossing inside a plain-opcode segment surfaces at the next checkpoint. ++/// ++/// The limit is placed mid-way through a straight plain-opcode run that ends in an SSTORE. ++/// REX5 (per-opcode) halts at the crossing opcode; REX6 (checkpoint) runs the rest of the ++/// segment and halts at the SSTORE settlement, recording the full segment — the same total a ++/// generous-limit run records up to that point. Both halt; the checkpoint run overshoots. ++#[test] ++fn test_checkpoint_halt_lands_at_next_checkpoint() { ++ let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes ++ ++ // Intrinsic compute gas (tx base cost) measured with a STOP-only contract. ++ let intrinsic = transact_default( ++ MegaSpecId::REX6, ++ db_with_code(BytecodeBuilder::default().append(STOP).build()), ++ ) ++ .compute_gas; ++ ++ // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. ++ let compute_limit = intrinsic + 300; ++ let limits = ++ |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); ++ ++ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); ++ let r6 = transact(MegaSpecId::REX6, db_with_code(code.clone()), limits(MegaSpecId::REX6)); ++ let r6_full = transact_default(MegaSpecId::REX6, db_with_code(code)); ++ ++ assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); ++ assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); ++ ++ // REX5 stops within one opcode of the crossing. ++ assert!( ++ r5.compute_gas <= compute_limit + 12, ++ "REX5 must stop at the crossing opcode; compute={} limit={compute_limit}", ++ r5.compute_gas ++ ); ++ // REX6 records the whole segment up to (and including) the SSTORE checkpoint: identical ++ // to what a run without the tight limit records at that point (SSTORE is the last ++ // gas-consuming opcode, so the full-run total equals the at-checkpoint total). ++ assert_eq!( ++ r6.compute_gas, r6_full.compute_gas, ++ "REX6 must settle the full segment at the checkpoint" ++ ); ++ assert!( ++ r6.compute_gas > r5.compute_gas, ++ "checkpoint enforcement overshoots per-opcode enforcement; REX5={} REX6={}", ++ r5.compute_gas, ++ r6.compute_gas ++ ); ++} ++ ++/// Pins the V1 checkpoint-set gap: a loop of plain opcodes contains **no checkpoint**, so a ++/// detention cap crossing inside it is only enforced at the frame-end settlement — the ++/// overshoot is bounded by the interpreter gas budget, NOT by the ~73k straight-line ++/// code-size bound from the design spec ("a straight line cannot loop" only holds when ++/// JUMP/JUMPI are checkpoints, which V1 deliberately excludes). ++/// ++/// A ~14-byte contract overshoots the detention cap by >100k gas here. If the V1 set is kept ++/// for the final spec, the overshoot bound must be stated as the remaining interpreter gas; ++/// bounding it by code size requires JUMP/JUMPI checkpoints (V2) or another backstop. ++#[test] ++fn test_checkpoint_v1_loop_segment_is_not_code_size_bounded() { ++ // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a 10,000-iteration ++ // countdown loop (~260k gas) with no checkpoint inside. ++ let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); ++ ++ let mut limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6); ++ limits.block_env_access_compute_gas_limit = 1_000; ++ ++ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), { ++ let mut l = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5); ++ l.block_env_access_compute_gas_limit = 1_000; ++ l ++ }); ++ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits); ++ ++ assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); ++ assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); ++ ++ // REX5 enforces within one opcode of the cap crossing (cap ≈ intrinsic + 1,000). ++ assert!( ++ r5.compute_gas < 30_000, ++ "REX5 must stop near the detention cap; compute={}", ++ r5.compute_gas ++ ); ++ // REX6 runs the entire loop before any checkpoint settles: >100k gas past the cap from a ++ // 16-byte contract — the overshoot is not bounded by code size under V1. ++ assert!( ++ r6.compute_gas > 100_000, ++ "REX6 V1 loop segment must overshoot far past the code-size bound; compute={}", ++ r6.compute_gas ++ ); ++} +diff --git a/crates/mega-evm/tests/rex6/main.rs b/crates/mega-evm/tests/rex6/main.rs +index 6a5160d..de86f86 100644 +--- a/crates/mega-evm/tests/rex6/main.rs ++++ b/crates/mega-evm/tests/rex6/main.rs +@@ -5,6 +5,7 @@ + //! authorities (not every recoverable one). + + mod beneficiary_detention; ++mod checkpoint_accounting; + mod common; + mod create2_metering_order; + mod create_frame_accounting; +-- +2.50.1 (Apple Git-155) + diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index 14472b91..286409de 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -335,12 +335,12 @@ reviewer = "improve-mutation-score (William Aaron Cheung)" # equivalent: weakening it lets an over-limit `exp_len` fall through to the short-circuit and be # charged the flat minimum, which `test_modexp_oversized_exp_len_halts_before_short_circuit` # rejects. The two mutants share identical mutation text, so this entry uses the full -# `file:line:col:` form to suppress only the line-135 one. +# `file:line:col:` form to suppress only the line-139 one. [[suppress]] kind = "line" category = "equivalent" file = "crates/mega-evm/src/evm/precompiles.rs" -mutant = "crates/mega-evm/src/evm/precompiles.rs:135:49: replace || with && in modexp::run_osaka_legacy" +mutant = "crates/mega-evm/src/evm/precompiles.rs:139:49: replace || with && in modexp::run_osaka_legacy" justification = "Equivalent: weakening the first || only skips the early halt when exactly one of base_len/mod_len exceeds the EIP-7823 limit while exp_len does not. The zero-base/zero-modulus short-circuit cannot fire there (the offending length is non-zero), so control reaches upstream osaka_run, whose identical EIP-7823 check returns Err(ModexpEip7823LimitSize); the wrapper maps it to the same PrecompileOutput::halt(ModexpEip7823LimitSize, reservoir) the unmutated line returns. Full suite green with the mutant applied." reviewer = "RealiCZ (cz)" From 4d3986fad434e9bd15503f768e095b7115f09408 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 17 Aug 2026 09:56:46 +0800 Subject: [PATCH 069/208] chore: remove accidentally committed design reference material --- ...heckpoint-compute-gas-accounting-design.md | 268 ----- .../checkpoint-fig1-per-opcode-tax-en.png | Bin 92676 -> 0 bytes .../assets/checkpoint-fig1-per-opcode-tax.png | Bin 86398 -> 0 bytes .../assets/checkpoint-fig2-granularity-en.png | Bin 100690 -> 0 bytes design/assets/checkpoint-fig2-granularity.png | Bin 98387 -> 0 bytes .../assets/checkpoint-fig3-enforcement-en.png | Bin 74192 -> 0 bytes design/assets/checkpoint-fig3-enforcement.png | Bin 72021 -> 0 bytes .../checkpoint-fig4-final-effect-en.png | Bin 91660 -> 0 bytes .../assets/checkpoint-fig4-final-effect.png | Bin 98653 -> 0 bytes design/assets/make_charts.py | 268 ----- .../reference-checkpoint_accounting_tests.rs | 238 ---- ...opcode-interpreter-hotloop-workload-.patch | 74 -- ...-clamp-enforcement-for-checkpoint-ac.patch | 1021 ----------------- ...ype-checkpoint-based-compute-gas-acc.patch | 732 ------------ 14 files changed, 2601 deletions(-) delete mode 100644 design/2026-07-24-checkpoint-compute-gas-accounting-design.md delete mode 100644 design/assets/checkpoint-fig1-per-opcode-tax-en.png delete mode 100644 design/assets/checkpoint-fig1-per-opcode-tax.png delete mode 100644 design/assets/checkpoint-fig2-granularity-en.png delete mode 100644 design/assets/checkpoint-fig2-granularity.png delete mode 100644 design/assets/checkpoint-fig3-enforcement-en.png delete mode 100644 design/assets/checkpoint-fig3-enforcement.png delete mode 100644 design/assets/checkpoint-fig4-final-effect-en.png delete mode 100644 design/assets/checkpoint-fig4-final-effect.png delete mode 100644 design/assets/make_charts.py delete mode 100644 design/reference-checkpoint_accounting_tests.rs delete mode 100644 design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch delete mode 100644 design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch delete mode 100644 design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch diff --git a/design/2026-07-24-checkpoint-compute-gas-accounting-design.md b/design/2026-07-24-checkpoint-compute-gas-accounting-design.md deleted file mode 100644 index 5a6be803..00000000 --- a/design/2026-07-24-checkpoint-compute-gas-accounting-design.md +++ /dev/null @@ -1,268 +0,0 @@ ---- -description: 检查点式 compute gas 记账 + V0 gas 钳制执法 —— 普通操作码零计量税、限额执法零 overshoot,热循环实测 -50%,以 REX7 发布。 ---- - -# 检查点式 Compute Gas 记账 —— 设计与验证报告(REX7) - -> 🕵️ **TL;DR** -> -> mega-evm 给每条操作码都包了一层 compute gas 记账,对廉价高频操作码这是一笔与本体相当甚至更高的固定税;PR #313 已在"逐位不变"约束下把能省的都省完,剩余 5–9% 全程序周期是"检查逐操作码存在"本身的成本——删掉它必须动不变式,因此以新 spec **REX7** 发布。 -> -> **目的**:普通操作码零计量税,同时不越限交易逐位等价、限额与 detention 执法不弱化。 -> -> **做了什么**: -> -> - **检查点结算**:约 140 个普通操作码直接接 revm 原装指令,compute gas 在检查点按解释器 gas 差值一次结算; -> - **粒度探索三轮收敛**:V1(无跳转检查点)→ 发现纯循环段无界、撤回 → V2 被 V1.5 支配淘汰 → **V0(gas 钳制执法)当选**; -> - **V0 可行性原型验证通过**:钳制解释器可见余量,revm 自身的逐操作码 gas 检查成为执法工具——零 overshoot,halt 提前到越限操作码执行前。 -> -> **结果如何**: -> -> - 解释器热循环(70 万廉价操作码)**1.87 ms → 0.93 ms(-50%)**,与原装 revm 地板持平;真实 ERC20 转账同 run 快于逐操作码记账; -> - **1,182 项既有测试零改动全绿**;激活钳制下 `GAS` 返回值、compute 总量、回执与逐操作码逐位一致; -> - 执法比现行更紧:越限操作码在执行前被拦下,detention 在无检查点纯循环内零 overshoot。 - -## 一、方法与方案 - -### 1.1 税在哪里:问题定位 - -MegaEVM 给每个操作码都包了一层 compute gas 记录(`evm/instructions.rs` 里的 `compute_gas_ext::*`):每执行一条指令,就把该操作码的 gas 记入 `AdditionalLimit` 并检查一次每交易 compute gas 限额。 -对昂贵操作码这笔记账开销可以忽略,但对真实执行流中占绝对多数的廉价高频操作码,它是一笔与操作码本体相当甚至更高的固定税。 - -PR #313(已合并,"trim per-opcode hot path")是对同一笔税的上一刀:把 `record_compute_gas` 从每操作码扫四维收窄为只查 compute 一维(其余三维挪到各自变更位点 latch,即现行资源限额检查协议的由来),并拆出精简包装宏让约 95 个简单操作码甩掉用不上的子帧 gas 分支。 -它的边界是**逐位不变**:halt 仍落在恰好越限的那个操作码上,行为零变化,因此无需新 spec。 - -| | PR #313 | 本设计(检查点记账 + V0 钳制) | -| -------- | ---------------------------------------- | --------------------------------------------------------------------------------- | -| 思路 | 逐操作码检查**做便宜**(少查维、精简宏) | 普通操作码的检查**整个移除**,结算挪到检查点、执法交给 revm 自身的 gas 检查 | -| 不变式 | halt 落点逐位不变 | 不越限交易逐位相同;越限交易 halt 提前到越限操作码执行前(V0 钳制,零 overshoot) | -| spec | 不需要 | 必须 REX7 | -| 收益量级 | 常数因子级 | 5–9% 全程序周期 | - -两者是接力关系:本轮全部测量的基线已包含 #313 的成果,剩余的税(如 `jb` 检查跳转占 `push1` 采样 25%)是"检查存在本身"的成本;自动补丁搜索在"逐位不变"约束下确证已无可削(见 2.1),所以需要动不变式本身。 -本设计的 latch 协议调整也直接搭在 #313 建立的 latch 机制上——只是把浮出点从"下一次 `record_compute_gas`"挪到"下一个检查点"。 - -![逐操作码计量税解剖](assets/checkpoint-fig1-per-opcode-tax.png) - -| 指标 | 值 | -| ---------------------------------------------- | -----------------------: | -| `push1` 占全程序 cycles / instructions / Ir | 18.27% / 23.72% / 20.08% | -| `add` 占 cycles / instructions | 4.79% / 6.49% | -| `pop` 占 cycles / instructions | 4.09% / 5.59% | -| `push1` 内部记账包装占比(Ir 归因) | 约 27% 的 push1 | -| `push1` 内部记账包装占比(`perf annotate` 带) | 35–50% 的采样 | -| 单条限额检查跳转 `jb` | 25.05% 的 push1 采样 | -| **包装成本归零时的全程序天花板** | **5–9% 的 cycles** | - -这不是微架构病理:全局分支 miss 率仅 0.093%,`push1` 只占分支 miss 的 0.79%、icache miss 的 0.78%,稳态 page fault 为零。 -成本就是逐操作码检查的比较/记录/跳转指令本身被执行了——这也是任何原位改写都删不掉它的原因:按现行语义,检查必须逐操作码存在。 - -注意范围:`push1` 只是测量中最大的单项代表——它是真实字节码中出现频率最高的操作码(每个常量入栈、每个跳转目标偏移都是一条 PUSH)。 -同样的固定税落在**每一个**被包装的普通操作码上(表中 `add`、`pop` 呈现完全同构的占比结构),**5–9% 的天花板是整个包装族的合计,不是 `push1` 单项**。 -昂贵操作码(`SSTORE` 等)不在此列:它们的包装成本相对操作码本体可忽略(检查点处约 99% 的 gas 质量是 `SSTORE` 本体),且它们在新方案中本来就作为检查点继续保留包装。 - -### 1.2 方案:检查点结算 - -REX7 下,指令表把**全部约 140 个普通操作码直接接到 revm 原装指令**——无包装、无逐操作码记录。 -Compute gas 在每个检查点结算: - -```text -段内 compute gas = 解释器 gas(上一检查点) - 解释器 gas(当前) - - 段内已单独记录的非 compute gas -``` - -检查点 = 本来就必须包装的位置(storage gas 操作码 `SSTORE`/`LOG0`–`LOG4`/`SELFDESTRUCT`、`CALL` 族、`CREATE`/`CREATE2`、volatile/detention 操作码)+ `GAS`(V0 需要,见 1.3)+ 帧进出。 -由于所有记录非 compute gas 的位置(storage gas 附加费)本身就是检查点,减项是精确的。 -结算值记入 `ComputeGasTracker`,随后执行完整的四维限额检查,浮出任何已 latch 的越限。 - -**精确性不变式:不越限交易的记账总量与逐操作码记账逐位相同。** -解释器的 gas 计数器本来就计量每个操作码(含内存扩展等动态部分);按差值结算得到完全相同的总和。 -从不越限的交易——绝对多数——逐字节不受影响:gas 相同、回执相同、状态相同。 -越限交易的 halt 语义由执法机制决定,见 1.3。 - -### 1.3 执法:V0 gas 钳制 - -结算解决"记多少",执法解决"何时停"。 -朴素的"检查点才检查"会让越限交易多跑一段(overshoot),而段长上界正是整个探索的主战场(见 2.2);最终当选的执法机制是 **V0 gas 钳制**,它让 overshoot 概念整个消失: - -- 在每个检查点和帧进出/续跑处,把解释器**可见**剩余 gas 钳到 compute 余量(帧级预算与 TX 级含 detention 余量的较小值,并记住绑定方),差额隐藏; -- 普通段内只有纯 compute 操作码消耗 gas,revm 自身的逐操作码 gas 检查就成了执法工具——越限操作码在钳制边界"假 OOG"、**执行前**被拦下,零附加税、零 overshoot; -- 下一检查点先恢复隐藏余量再跑本体,`CALL` 转发、`GAS` 读数、storage 扣费全部看到真实计数器;帧末恢复隐藏量入帧结果,假 OOG 按绑定方重分类(帧级 → revert、TX 级 → halt),detention 归因保持 `VolatileDataAccessOutOfGas` 与现行一致。 - -代价是设计复杂度,三处必须处理:`GAS` 操作码必须入检查点集(否则钳制值可被合约观测,破坏精确性不变式);假 OOG 需在帧结果层识别、还原并重分类;`CALL` 族的 63/64 转发必须在检查点先恢复真实余量再计算。 -**可行性验证已通过**(实现侧原型,检查点原型分支),验证细节与证据见 2.4。 - -### 1.4 协议调整与不改什么 - -- **资源限额检查协议**:变更位点仍然立即 latch 越限(规则 1 不变——这些位点全是检查点);latch 的浮出点从下一次 `record_compute_gas` 改为下一个检查点;规则 1 的 `debug_assert` 相应移位。 -- **Detention**:volatile 操作码保持包装并照旧设 cap;V0 下钳制余量含 detention 余量,执法精确到操作码、无 overshoot。 -- **Halt 落点规则(V0,已定)**:越限操作码在执行前于钳制边界假 OOG,帧末还原隐藏余量并按绑定方重分类(帧级预算绑定为帧 revert,TX 级/detention 绑定为 TX halt,detention 归因保持 `VolatileDataAccessOutOfGas`),剩余 gas 照旧保留用于退款。 - **双超界角点已裁定:compute 判定优先**——越限操作码同时超出真实 EVM 余量与 compute 余量时,判为 compute/detention halt(带 rescue 退款)而非 EVM OOG 燃尽。 - 理由:帧末无法区分二者(OOG 时无操作码成本信息);该角点仅在两类余量于同一操作码同时耗尽的刀尖出现;方向优待发送者且不引入新的可套利面——想避免燃尽的调用方本就可以主动 REVERT。 -- **Gas 泄漏路径**(系统合约拦截、限额越限时的 gas rescue、帧返回):`CALL` 族检查点在 `frame_init` _之前_ 结算调用方的段并恢复钳制,因此拦截短路、rescue 路径和 63/64 转发看到的都是已结算、真实域的状态;帧返回检查点在子帧 gas 合并回父帧前结算子帧并恢复钳制。 -- **检查点集合修正**:按长度计费的动态成本操作码 {`KECCAK256`、`CALLDATACOPY`、`CODECOPY`、`RETURNDATACOPY`、`MCOPY`} 单个即可烧掉远超码长界的 gas;V2/V1.5 类方案必须把它们入检查点集(主网执行占比 <0.72%,包装税可忽略),V0 钳制对此天然免疫。 - `EXTCODECOPY` 已在 volatile 检查点集内,`EXP` 动态部分封顶 1,600 gas 可忽略;剩余的 `MLOAD`/`MSTORE` 一次性内存扩张是现行逐操作码执法同样存在的单操作码敞口,V0 下同样被钳制精确拦截。 -- **不动 revm**:MegaEVM 本来就注入自己的指令表;本设计只改变哪些表项带包装,解释器循环、revm 原生 gas 计量、全部 revm 指令实现保持原装,revm 的 gas 计数器就是结算数据源与执法工具。 -- **所有既有 spec 逐字节冻结**:逐操作码表继续为 ≤ REX6 接线;检查点表只为 REX7 接线,沿用现有的按 spec 指令表模式。 -- **Storage gas、data size、KV 更新、state growth 四路记账**保持现有记录位点和语义不变;这些位点按构造就是检查点。 - ---- - -## 二、观测与探索记录 - -### 2.1 测量方法 - -五条互相独立的手段交叉验证: - -1. **硬件计数器背离研究**(AMD EPYC 9754,钉核,每计数器组 5 轮取 median/MAD):逐函数的 cycles、instructions、IPC、分支 miss、L1 icache miss、page fault,并与 Callgrind Ir 份额交叉对照。 -2. **`push1` 指令级解剖**(Callgrind 文件/行归因 + `perf annotate`):最热操作码里,操作码本体和记账包装各占多少。 -3. **检查点 overshoot 模拟**:回放逐操作码 gas 轨迹,按候选检查点粒度切段(V1 = 仅现有必包装位置;V2 = V1 + `JUMP`/`JUMPI`;V3 = V2 + `JUMPDEST`;后补 V1.5 = 仅回跳结算),测量段长的 gas 加权分布——即越限 halt 最晚会落到多远——覆盖合成/probe 语料和 **1,000 笔真实主网交易**(chain id 4326,区块 22,085,597–22,086,184,`debug_traceTransaction`,共 9,737,035 个执行操作码;n=100 与 n=1000 之间分布已收敛)。 -4. **自动补丁搜索**(可编辑面在通过变异敏感度门、并把差分 oracle 强化到可观测四维用量与 halt 标签之后,扩展到 `limit/limit.rs`、`limit/compute_gas.rs`、`limit/frame_limit.rs`):没有找到任何安全的原位优化——证明这笔税是结构性的,不是实现失误。 -5. **实现侧原型差分**(检查点原型分支):把每个候选真实接进 REX6 指令表,用同一套 criterion 基准组(vanilla revm / op-revm / equivalence / rex4 / rex5 / rex6 同 run 对照)测 wall-clock,用全量测试套件(1,182 项,含 REX5↔REX6 逐位平价断言)做行为差分——模拟给方向,原型给实证。 - -三类硬件指标在探索中的分工与读数: - -- **IPC(周期对指令的背离)是主力**,两个核心发现都由它给出。 - 主探针全局 IPC 3.42(SSTORE/LOG 负载 2.823,CREATE 负载 3.366);逐函数背离比给出 `push1` = 0.77(高 IPC 纯计算,税是真周期,Ir 看得见——支撑本设计的收益估计),以及反方向的 `HashMap::get_mut` = 9.02×、`hashbrown::rustc_entry` = 4.9×、`FoldHasher::hash` = 4.15×(低 IPC 访存瓶颈,合计约 12% 周期——Ir 失明的状态访问局部性矿脉,归属大头在 revm 上游,另列一线,不在本设计范围内)。 -- **Cache miss 用于证伪**:`push1` 的 L1 icache miss 份额仅 0.78%(对比其 18.27% 周期),驳掉"包装宏代码膨胀撑爆 I-cache"假设;同族的分支 miss 全局仅 0.093%,`push1` 占 miss 的 0.79%,派发表不进榜,驳掉"解释器派发打爆分支预测器"假设。 -- **Page fault 用于排除**:major fault 为 0,稳态(第 2–5 秒)0 faults/s,与全部发现无关。 - -### 2.2 探索主线:粒度的三次收敛 - -#### 第一轮:overshoot 模拟初判 V1 - -检查点方案下,普通操作码不再包装;越限交易的 halt 落到下一个检查点,最多多执行一段。 -段长的 gas 加权分布: - -| 来源 | 变体 | p50 / p90 / p99 / max(gas) | 残余包装操作占比 | 可收回收益 | -| ----------- | ---- | ----------------------------: | ---------------: | -------------: | -| 合成+probe | V1 | 45,512 / — / 50,002 / 50,002 | 3.02% | 4.85–8.73% | -| 合成+probe | V2 | 184 / — / 50,002 / 50,002 | 4.04% | 4.80–8.64% | -| 合成+probe | V3 | 184 / — / 50,002 / 50,002 | 5.26% | 4.74–8.53% | -| 主网 n=1000 | V1 | 665 / 4,325 / 26,935 / 27,151 | **0.71%** | **4.96–8.94%** | -| 主网 n=1000 | V2 | 57 / 168 / 6,706 / 6,772 | 7.80% | 4.61–8.30% | -| 主网 n=1000 | V3 | 60 / 168 / 6,705 / 6,771 | 14.39% | 4.28–7.71% | -| 主网 n=1000 | V1.5 | 194 / 317 / 6,753 / 6,861 | 3.87%(等效)\* | 4.81–8.65% | - -\* V1.5 等效残余税 = V1 位点完整结算 0.66% + 回跳完整结算 1.91% + 非回跳跳转的轻量方向判断 5.19%×0.25(按轻量成本 ≈ 完整结算 25% 的点估计折算)。 -回跳占执行流 1.91%,全部跳转占 7.10%;码长界经验验证:250,080 个段检查,0 反例。 - -![检查点粒度两难](assets/checkpoint-fig2-granularity.png) - -真实合约代码是跳转密集的:把 `JUMP`/`JUMPI`(V2)乃至 `JUMPDEST`(V3)设为检查点,等于给主网 7.8% / 14.4% 的执行操作码重新上税——恰恰是要消除的廉价操作码税;V1 让 99.3% 的执行操作码完全免税。 -初版据此裁定 V1,并推导"24 KB 合约按 1 字节 3 gas 直线执行约 73k gas、直线无法成环"的码长上界。 -原型第一轮(V1 接线)实测:热循环 1.87 ms → 0.93 ms(-50%),落在原装 revm 地板上——收益侧完全兑现。 - -#### 第二轮:循环洞——V1 裁定撤回 - -**猜想**:73k 码长上界对 V1 成立("直线无法成环")。 -**验证**:该推理只在 `JUMP`/`JUMPI` 是检查点时成立,而 V1 恰恰不含跳转——一个十几字节的纯算术循环(`JUMPDEST; …; JUMPI` 回跳)内部没有任何 V1 检查点,段会跨越任意多次迭代。 -实现侧测试钉住:detention cap 1000 下,逐操作码记账(Rex5)在 cap 附近停住;V1 原型把 26 万 gas 的循环整个跑完,直到帧末结算才 halt。 -**结论**:V1 的段长上界实际是**剩余 EVM gas**,不是码长;compute 限额对循环密集代码退化为建议值,detention 有效界变成 `cap + 剩余 gas`,破坏"访问易变数据后快速终止"的并行目标。 -测量数据中的 `synth_jump_loop` V1 段长 45,512 也是同一现象——它受负载形状约束,不是结构界。 -**V1 裁定据此撤回**,粒度重开为待定问题。 - -#### 第三轮:候选重开与逐一定夺 - -- **V2(`JUMP`/`JUMPI` 全设检查点)**:段无法跨越跳转,码长上界恢复;原型实测热循环 1.09 ms(对 V1 回吐约三分之一收益)。 - **被 V1.5 支配淘汰**:两者结构上界相同(码长界),主网实测尾部几乎相同(max 6,861 vs 6,772),而 V1.5 的完整结算税(2.57%)远低于 V2(7.80%)——V2 相对 V1.5 没有任何优势维度。 -- **仅回跳结算(V1.5)**:`JUMP`/`JUMPI` 仍包装但只在回跳(目标 < 当前 PC)时完整结算;只经前向跳转的段内 PC 单调前进,段操作数受码长约束;每次循环迭代付一次结算——这是任何要给循环设界的方案的内禀成本。 - 主网重切:回跳占执行流 1.91%,段长 p50/p99/max = 194/6,753/6,861,码长界 250,080 段零反例,等效残余税 3.87%,可收回 4.81–8.65%;最长段即算术热循环体(POP/MULMOD/SWAP/DUP),正是靠回跳边界切开——机制按设计工作。 - 原型实测:最坏形状(回跳占跳转 100% 的热循环)1.11 ms,与 V2 同价;功能面上钉住 V1 循环洞的测试按预期翻转——halt 从帧末(compute 281k)收回到回跳处(compute 22k ≈ cap + 一次迭代)。 - µs 级单笔 ERC20 负载对各粒度变体无区分度(代码布局噪声 ±5% 盖过差异),残余税定值以主网轨迹重切为准。 -- **码长界的第二个洞(检查点集合修正)**:即便有了跳转检查点,"1 字节 3 gas"的推导也只对常数费用操作码成立——`KECCAK256`(6 gas/字)等长度计费操作码单个即可烧掉远超码长界的 gas(24 KB 直线塞满 1 MiB 输入的 `KECCAK256`,单段约 6 亿 gas)。 - V2/V1.5 须把五个长度计费操作码并入检查点集(主网占比 <0.72%,代价可忽略);`MLOAD`/`MSTORE` 的一次性内存扩张无法经济地检查点化,只能表述为"与现行逐操作码执法等同、不因本设计引入的单操作码敞口"。 - 这把 V2/V1.5 的上界钉成了**相对式**:相对逐操作码执法的额外 overshoot ≤ 约 73k,绝对界仍然做不到。 -- **V0(gas 钳制执法)**:对上述全部问题天然免疫——无 overshoot、无段长界问题、无动态计费敞口,普通操作码零附加税,且比现行执法更紧(越限操作码执行前拦下 vs 现行执行完才检查)。 - 按既定规则(可行性验证通过则选 V0)完成原型验证后,**粒度封定为 V0**:零 overshoot、比现行执法更紧、热循环贴 V1/原装 revm 地板、残余税最低(V1 位点 0.71% + `GAS` ≲0.1%)。 - V1.5 作为已测完备的回退方案归档(回跳结算,4.81–8.65% 收回,变体补丁留存)。 - -![执法精确性对比](assets/checkpoint-fig3-enforcement.png) - -### 2.3 主网数据佐证(热度与限额逼近度) - -REX7 归类下的主网执行流(1,000 笔):**普通操作码占执行次数 92.20%**(gas 质量仅 13.26%——gas 被 LOG/SLOAD/SSTORE 主导),检查点类占 7.34% 次数 / 57.63% gas,volatile 类 0.47% / 29.12%。 -5–9% 天花板的前提(绝大多数执行次数免税)在真链按执行次数口径成立;gas 质量口径与包装税无关,不作前提。 - -长度计费五操作码合计执行占比 < 0.72%(`KECCAK256` 0.626% 为主,其余 ≤0.073%),本窗单次最大 gas 仅 716——入检查点集的包装税实测可忽略。 -本窗未出现"单次烧穿码长界"的极端大拷贝(样本偏小输入),理论敞口仍在,检查点集修正保留。 - -限额逼近度:单笔 compute 总量 p50/p99/max = 56,495 / 485,145 / 2,279,067,对 200M per-tx 限额仅 0.028% / 0.24% / 1.14%;**99.9% 的交易触碰 volatile 数据**,post-access 用量对 20M cap 为 2.4% / 11.4%(p99/max)。 -含义:真实流量远离一切 binding cap——**overshoot 是正确性上界的设计问题,不是高频用户可见行为**(触限案例 hard 0、near 4)。 -这不弱化上界要求(对抗形状仍需容纳),但说明执法机制选择不会造成现网可感知的行为面变化。 - -### 2.4 原型实测效果(V0) - -![最终效果](assets/checkpoint-fig4-final-effect.png) - -各方案在解释器热循环基准(70 万廉价操作码,本地 wall-clock,criterion 同 run 内含 vanilla revm / equivalence / rex5 对照行): - -| 方案 | 热循环用时 | vs 逐操作码 | 执法语义 | -| -------------------- | ----------: | ----------: | ---------------------------- | -| 逐操作码(改造前) | 1.87 ms | — | halt 在越限操作码(执行后) | -| V2(跳转全结算) | 1.09 ms | -42% | overshoot ≤ 码长界(相对式) | -| V1.5(仅回跳结算) | 1.11 ms | -40% | overshoot ≤ 码长界(相对式) | -| V1(无跳转检查点) | 0.93 ms | -50% | overshoot 无界(已否决) | -| **V0(钳制,最终)** | **0.93 ms** | **-50%** | **零 overshoot,halt 提前** | -| 原装 revm(下界) | 0.93 ms | — | — | - -V0 与 V1 同速——钳制只在检查点付费,普通段零附加税;同 run 的 rex5(逐操作码代表)与 equivalence 行波动 <2%,是测量有效性的对照。 -真实 ERC20 转账(weth9 transfer,同 run):V0 9.26 µs,快于逐操作码 rex5 的 9.40 µs,对 equivalence(8.86 µs)残余差距约 4.5%——来自帧钩子、storage gas 检查点、拦截器分发等非指令税。 - -V0 等价与执法证据清单: - -- **不越限逐位等价**:全量 1,182 项测试零改动全绿(含 REX5↔REX6 平价套件的 compute_gas / gas_used 逐位断言)、workspace 1,325 全绿;该主张经监督者独立复跑核实(passed 1,182 / failed 0 / exit 0);专项测试证明激活钳制下 `GAS` 返回值、compute 总量、回执与 Rex5 逐位一致。 -- **执法精确**:专项测试证明越限交易 halt 在越限操作码执行前(Rex5 记入越限操作码、usage 超限;V0 的 usage 停在限额内),detention 在无检查点纯循环内零 overshoot,halt reason 保持 `VolatileDataAccessOutOfGas` 归因。 -- **泄漏路径**:拦截、rescue、帧返回三条路径由既有测试覆盖,钳制在帧边界处恒为已恢复状态(`CALL` 检查点在 `frame_init` 前恢复,帧末结算恢复入帧结果)。 -- **门禁**:clippy 零新增警告、fmt、riscv no_std、cargo bench 本地全部通过。 - -### 2.5 相关工作对照:为什么不采用 evmone 的 basic block 校验 - -evmone(advanced 解释器)的高效 gas 计算算法:装载期把字节码切成 basic block(`JUMPDEST` 开块,`JUMP`/`JUMPI`/终结指令收块),预计算每块的静态 gas 总和与栈需求,运行时**块入口一次校验**代替逐操作码校验;`GAS`/`CALL` 需要精确 gas-left,用每指令附带的修正常数还原,不为它们切块。 -评估结论:思想已殊途同归,机制不适用于我们的任何一个维度,不采用。 - -先记三个同源点: - -- 他们的 basic block ≈ 我们的段(块界恰好是我们淘汰的 V2/V3 粒度); -- 他们的 `GAS`/`CALL` 修正常数(预计算)≈ 我们的钳制恢复恒等式"真实 = 可见 + 隐藏"(运行时维护); -- 他们"块入口预扣整块 gas、halt 提前到块入口,因 OOG 燃尽帧 gas 且回滚全部效果而无共识影响"的论证,与 V0"halt 提前到越限操作码执行前"的裁定同构——主流实现的现成先例。 - -不采用的原因,按维度: - -1. **mega compute 维——V0 已经比它更粗、且更强。** - 块预计算的前提是费用静态可知(每操作码基础费是编译期常数)。 - V0 钳制下普通段内零检查零记账,结算位点(V1 位点 0.71% + `GAS`)比块界(主网 7.8–14.4%)稀疏一个量级;块界当结算点我们实测过(热循环 +17%)。 - 预计算省的是"段总额的计算",而我们检查点成本的大头是记账机器(RefCell + 记录 + 四维浮出),不在计算上;且钳制执法根本不需要段界,"块入口一次校验"没有需求对象。 -2. **revm 原生 gas/栈检查(地板本身)——检查长在够不着的地方。** - 我们贴着的 equivalence 地板内部,是 revm 每条指令函数体自带的 `gas!` 扣费与栈检查——不在解释器循环里,换指令表消不掉。 - 仿照 evmone 必须给约 140 个普通操作码提供"无检查体"的自制实现,外加按 code hash 缓存的装载期块分析和 EVM gas 的 `GAS`/`CALL` 修正值。 - 技术可行、共识论证成立(同上第三个同源点),但等于用约 140 份手抄的共识关键指令体替换 revm 原装——掏空本设计"revm 原装指令即正确性来源"的信任论证,且每次 revm 升级都要逐一重审。 - 归入 revm 上游 / 自研解释器长线(见后续方向),不属于 REX7。 -3. **storage gas 维——无静态成分可预计算,且成本结构不是"检查"。** - SSTORE 收不收、收多少取决于槽的原值/现值/新值与 SALT 桶容量,CALL 的新账户费取决于目标账户空否与转账值,LOG 的费取决于栈上操作数——全部是运行时信息,分析期连"要不要收费"都判定不了,块预计算没有对象。 - τ_state(≈68 ns/SSTORE,`sstore_100` 仍 1.51× op-revm)的构成是状态访问(SALT 桶查找、账户/槽 inspect、journal HashMap——即硬件计数器研究的低 IPC 访存矿脉)加四维记账机器;"限额检查"经 #313 降维 + latch 后只占零头,消检查省不出可见收益。 - 这些维度也不是 gas 计价的,钳制式执法同样用不上;data size 等限额是 DoS 防线,执法不能推迟到帧末。 - 评估为收益不大,不立项;存量杠杆是消 CALL 族 wrapper 的重复 host 查询与 revm 上游存储路径升级(pin 之后上游已把存储路径优化约 1.5×)。 - ---- - -## 三、后续方向 - -- **REX7 转正**:本轮验证在 REX6 原型分支上完成;按既定流程引入 `MegaSpecId::REX7`(spec 枚举、hardfork 映射、constants 段),REX6 回滚为逐操作码并冻结,检查点 + V0 机制整体平移到 REX7 门控,补 `docs/spec/upgrades/rex7.md` 升级页。 -- **Tracing 兼容**:是否保留可选的逐操作码记账模式,供依赖逐操作码 compute gas 归因的调试/追踪工具使用。 -- **测试计划(REX7 化)**:spec 门控的钳制执法测试(每类检查点、双超界角点、假 OOG 重分类)、四维 latch 浮出测试、三条 gas 泄漏路径测试、断言旧 spec 逐字节相同的差分回放。 -- **CodSpeed 量化**:PR 化后由 CodSpeed 指令数报告量化实际收益对 5–9% 天花板的兑现程度(本地 wall-clock 已验证方向与量级)。 -- **上界常数**:随 V0 当选作废(无 overshoot 即无上界常数);若未来回退 V1.5,按相对式表述(相对现行执法额外 ≤ 约 73k)。 -- **升级 revm 基座**:硬件计数器研究另行发现的低 IPC 状态访问矿脉(约 12% 周期,`HashMap`/hash 访存)归属 revm 上游,另列一线跟进;上游在我们 pin 的版本之后已把存储路径优化约 1.5×。 -- **压 revm 地板本身(长线)**:消 revm 原生逐操作码 gas/栈检查的 evmone 式块级校验是已验证的路线,但只能走 revm 上游或自研解释器(不适用原因与代价见 2.5);立项前先做"无检查指令体"消融实验测天花板。 - -## 附录 - -### 原型分支 - -检查点原型分支 `cz/feat/rex7-checkpoint-accounting`(基于 REX6 预览线):热循环基准、V1 原型、V1.5/V2 变体实测、V0 钳制实现与全部专项测试;图表由 `assets/` 下的 Python 脚本产物提供。 - -### 数据出处 - -以上全部数字来自 2026 年 7 月的测量:硬件计数器背离研究、`push1` 解剖、自动补丁搜索穷尽运行、检查点 overshoot 模拟(合成语料 + 1,000 笔主网交易)、主网轨迹重切(回跳/热度/限额逼近度)、以及检查点原型分支的实现侧差分与基准。 -含 SHA-256 清单的原始证据树已归档,可按需提供。 diff --git a/design/assets/checkpoint-fig1-per-opcode-tax-en.png b/design/assets/checkpoint-fig1-per-opcode-tax-en.png deleted file mode 100644 index b26993647d5f4e1b0aff3f3039fd3d0aa087cb3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92676 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%0AxkJ(tIPu#V&k~1>XUAcn4j$@fus&W+C#Lv@`AvU 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

^^ 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 diff --git a/design/assets/checkpoint-fig2-granularity.png b/design/assets/checkpoint-fig2-granularity.png deleted file mode 100644 index 988f6eaf84afe110a80acbddd7eee6020a834e3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98387 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 diff --git a/design/assets/checkpoint-fig3-enforcement-en.png b/design/assets/checkpoint-fig3-enforcement-en.png deleted file mode 100644 index ab8dc51ef3b92027b1a22e6b0b6caadf9abe8d29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74192 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 diff --git a/design/assets/checkpoint-fig3-enforcement.png b/design/assets/checkpoint-fig3-enforcement.png deleted file mode 100644 index fb081248492953cf3760d34839887c00c27d6d2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72021 zcmcG$hdW#U8#k_eXiIfcwY55oqE)J57oj#WYPZ$ctB6>kv}o0+)~FF%h}qg(b(u99 z5qs2##E2clc;5Z~uHW+?Jh?8HNFpcaocFo!*ZmrKYoMpW%FM+~M@Pr{NK?&-j_yAX zIy(A0r6w5p+{@d^!~LGPthm%| z0Y@JnPj85Xgxmjpfw+g4g9P9ENk{M|r#&^zz3J$#r2YFj@d)uMf$jty-6J&>W54v} z3C46|Jgt2_(u#|ebkXU$&h7uM-@eUe@P&oJlI_Ma-KDl)moMEtf1ZIwE#6(NL8ako za5-gU%6hf8w`X;E^>bwp@$k4W4uSmJi{wceU*V!_wG`~y}3(BRLtOguDgHVy=u^{$49U(eXp4rDz!|`rszX& z+cXWg9PPVURyi8h$n&4?J2gpn%4xF6e`k-FX*t=Ht$#_>tnA;``iIJ_lV^y=v)dKj z;+0RoEBfR{#7A>&ms$sqZiv6RRC+5j=v%DPQJoJ3`HPcxWz?k{>~Wt0HQm~8CfxTY zd-9lxnRSX6E>M;_u2=SD$^ZE9-1z-uG0UOD+DT%K+nD5_Cpena370%VmjC8&nNn{e zi`(okU6SIh_iwhEEn9<|SH|n_O?c}D491%%OE+gZyJu0eHnjuHc{#I7&LO^Dl&0A& z^rQ@xn8Rp`xpCJfDeoj&KJToIjgaxh_YTG>Ewr*m_w=9*Vc}MAE;3oji*-pVub9k9 zfAUMgZ!BN&tTlm~F=mTrpzd&IY}`+;55bUcbp_;0Wha@A_A(jksr!e(50cnd7N&VrV2 z;Vg}r$>P?j8}j5gg(uANwb(^%TbgLrDU$FKi|QiNLW8HYL+X^(EG6B$_X~=)0~Tuj z#O52Wv$O5D;)YF1EpFgJ#y^x)9-Ifu%j}zgp4V&^U|e z{BUEnp-7+`T#w^l`~Ku>yORFE?}r@8CqF8heMDGY_-$FYRDCqoXnM5v{hV(*Z*62y z=|??Crvj8F$~Dv5)_Sn|XbOSXmzyAjIJWh?VagoVD$K2*}t{iIvM*en|1SOyJa>%qPF7;pC*R`GlT zS^T_n&R5V=NYIZ#AdjArxh~{8B7phw?JI~ijOWa?vJ-`8+fmf*3Tc<2QErc>8G4bn zrn9<6M!zzSCj%)jcyo>pX^k&Gau{&lefC;6eK%6tV{+UE)C+2GlioY7cCG;!13ot* zm+QPl(^`(TXB*=xvg`AJZm-g@e6eM1hQj`$9oEJ+jzlOguXdmKwZF6SaU(G1qVLks zZ)r(EM)wa?{h9Z)tv;pWsuK)M!}H0u8RX!}*7aVNE&pSCvs^PY4?-vQ!QWHwPTsTE zsU67G7`vb`_#$TgiFJcdH8_qVpknRvjI^lbP)2VgTRH!H$5HwBr*@s5+GlX>j)3y< z;^xUS9lZN>jvd?|D(c49C{j=_MGXdOkz9ma_#UDqCOhh@bgueur{ask&z-tXNq)KX#BHQvq$^3x?kzO*4cOP7UInVW+`$?H zjXWEt~H%>-g$dy2z) zaD;J>?N0yQeAipNW*keu58AJds#2P%WwgNVBr)9c za^d#{eHw0p9&17h0s1Gs1)xM#VZ&=HS;OGilUw@s<+u29*SB!`jkA^qnW4$7h`(>e zaPrU)-1O+DyLNy63$I31I`nm}Ed{ohFg37Qu`IxINr$8c<@YD+IYGO}(z7;X6~)iz zKN8io6ujzuQ1~T>r810(wzHy(7}(;Up~2yc$P+Owc@9v3&R#S9b?O8%b9-fsu(9y_qomgZ?}ali13BRn zzGEvxx$U~E&Hg8_U=4P}JQK~DQ>f)~aqbCc)*?}~63^kyxg?)|{+*#P7JRT?U>xJm z7+pniZ?1M|piPazN10;Q{By~b_P;9{5K9)MKb^Fz=qm=}>lL+wb-m(U_6E9nv$1lD z?qM>=2bKL9{tKK0EHiu4z@Qut4@4g;$o-c5OxQ1%GV)ty>ch&|ohM&$;-0x@KSTZ= zo&DH*qiekG_=q+>_`AGiAA*;K5pikm4V{ch_`N1_QL&NkQC4AoyzaNwc5+H(Kk zz|M%n#QLI=zO~PQy6NX9Cj!ALh6NMhCe?0ZLja?=ykcrjAbZ%zN#3*ZpHFzS_aT4MRI6>rl*{M`B|lsd-pO4p@b8be!mTg{W!6g3@}@59x@PLd+q(+Fr`U?_ z6+H9fn%|5Y^>>&YqTX{G z^&y-$e4pcl!>rl@$QUx-IvMhgIv$YGi z#WGAxB7KroCt6tFN({Z2K9cjShHHFHKOA;nD~?aW^;dKX7IRhfai#xe=%{FF?*?cG zqhCWA0J4h^e^dS>h8_vY*qUmHwL)!6yKB8wYMdQ}S&lSQcf2yuitPrk&WVtVX7z}| z5+AP*Pd3wxFlDjR(IJfcrYMDkT6_P-3!DD} zJ((27U*5c9(cv=EdMxgRYs{&?=}-7Y&v;f3*~8-cx~s6aYg@ZLKbB!-6Rh;R^VC$u z)xHc?OT%GSEMc9OTMW1^cz1GI{e|DM-4_;uaGGTIyFeQ!)4iJGf?9n zb2>)E#l4CvApo7`(bCzKT>b>Lzx8MYDs6F?q8J=oGZoKa4r-xy_yZPa5@?{jbp!B-{!{2U{5jjCWv_fhsX zcXUsv$A~!Ox14&WO*(=g(*wv@5)=BK7N;MCZ>*N`9utw6*;~v!Ddf_}f6Bm(3+>W$ zCc=QNY3JnUN-f-r*VDSs!Eo2i($jjh&y8h1_xcS9#c_fz$7r{~ALaplz9&FaEl zG^Q!QcU4FHN2ziQlTMYvOAIo++iSDz(G2;&UxJK(xAvUw{0KQT@{Ol=1YkVI?`Q+3 zoQSDHgE^rLmHRa#Hh$Vw4St7Yh5UB96Bmgn5q(lnlRTRIrNQ)O$dp$5Tz%-Px6`aI z*@{?Z%R>aToOY_*Cm{0;o$+>G`-Sp?c(V+3!-;8~ktuL1jj<;}i3DDo5 zjx9qSOlaR`W0FMPTqs(frJT7==+|t0kzqLM&N$Yxx8P!?s+sH3rEJtLJ+5^rxGQ#l z`W4j;3=rapHmr?yaSB83_47lCzVV|4bI9!L!VV!??)%B z=SN3O4?4jmg#|EnJ{|9qtxNN+szwaYb$+yd56v8Sdn@C)%($nj)>oqtnAoXSGh5Wp z33tK$n$C_~1j^DuBfzx7{_%=JX3EElBPbPRwJEIpLGq18K?hNO;TVz07 ziaPW9luwx8i|v^x+g&sM*e=l8TZ962|Eg z^Yj)OFRCqg#D~6Ab*?Mnkqtte2X6PqIo*$K3)XxV6>@;%PEaVcdra(JW|rK;zA1@a z(nRzOVsS6Bl~tf}i0s%4s9TBVd7D202)1tDi@w+E1N-R{{HOXM=go5pTLwk#&QncC z`%7i_-^NPtehes)G)xdP2mSNC3YM4=I8^GGcjnd#44vuIB&>ZmwKvCx0&0%ecCpVy z|4~29pp+Z`$q+4tnyUJcjUh$k3OhqMnzsH-CnTO$59JOK5O~0ST>;YWlBy-deA|h? z_`Y5!eJ2A?u*i>cfp@*c(d0Hx4AzC0w~#y6&X8aexAP_HnbO+VSHAnCf`Zh5IV9-9 z4Z>}y^xWht-6i@;hf?s->dTz{NYdO#{j-6`TP@m+NXiQa1&DjXPwveoh_6B8c*+(oYD*ph|qL*9O(lmIu{unn*^Y`jd25-;V+A>%H*O zP$`nvCFHvX>o?EtUm1uYbMbc?xgl<&RW${feAAG;k>=61Z{dv-H6;2nliLaG*;x_H zoT8$ttY(#SZl)&<(b^K5Q2NkA_tUEifxZP6oR5ls6t3I02BQem7n&oWTzE}GJ9Tq~ zqpUYvmh2+NI@@vx&BGWMX^63?I^)CS+ve#Tfrzt%pH>HEp^bp(f-#mf_2f)nP(--n z{UD!=jkf_jLb7_sv=m(_ha`)r6$*TWNISm|&xuG(`p7G_s7`1zBbuMMvf_epH)eU1 zJ7$w>D~KT9A>T6=yN>5?*ys|Spcucdu>Z{^+*dklz(MIS=t*Er; z63m%OOXP{CcbXZ)c7slNl3K0b4Y4Sz8vCk2wsxnZsogaL(DM_!0(nL@LOv>l1zOTTyP2 zW>RKlUb|HRjp>u$yltj^Gd0WW4r4*$qYG~#_aI5mgM}$ZQ)*RbDtcMOEToW`rD5j# zv5P_ZQ%iwmo9{7B`>&SF^L0-0%9s=x>rq>n=^ghqtF}@^?&N;0YT1O?`YiTOq9kks zS4=qQOk+4?mp^}S>Rq>Puv(7Qjp;9ri0XX@vAupWj{SgVmOh$qkHxqAsaKoo zv<==HDGewT_yfRsnwOq#pX#}7WSgQGp_F7Wxv{=K=}ayO!xU-w9IVGPymTRyy)eHs zLk~al4yxKVU|TzPquHcjwP`ap;l%4NcCFsqLDx!OUykBC2I!-q?IfcW;^;ghBcjwm zKuzU5V~??tMti&luLt_1&*r&RA)y@a#{)U)MoEvTrpbdld*%Ls`|)n8o+FISF#Xry zyYh_Tq{x$udiES2Uf2xW`kl`KSz+&F!@*ceVh^`&=1{!}kpX?22rqJqw}@6DKs*xb zN`m;qu&fw?R;KK#Q!h<}{|w4VFZsAG@vyP78VeT|wEc%)+6a^3O6ZUJ$ebDs=t*pt zkTY>{yxi{L_J?Z&+a@fkuP<3OcIctOuLCREw9>MBIKK2RYw~m z#0TG;mM*Y~aS#&?oInsp#A?S~TMu^vyCf%_UEw}b=H=P3IesITEH0Xub2@JYXvfsJ zARETq@$?1`Hrr1QwBI|=^RJidWie$#CbFaWqaZvsvpTHTrBl_)QzUG+V2M&2O)q9c z0Ev2TQi#s8Tg>9o_4hM>Bjbf0V;_x^&hYiUAe;L)sBP%wl3?(v2_5F4$L3wWTQ444yX= zq;!dvd#0G%oc|%0G`MRrt;+eJPmyM&r9nQT)i$!*N*Q5$3z7m%pY&S#>zF$P=rZ!N z^>;ab(Tu4Q>Bz~7*vVEB)MT~P?KHRr3wPR{IBmmc(%vljk}r7oX(JdIVMoEV6=Yq{ zb$E^wzs9BL{wfB9kyJo&e%PUkAY;v7c0cm5%59ISBIVEYGF-w#noX^<1(d9?8uH`4 zS28xr+HI#%)WLXn+=BA`#NdLgD9kHI_FBMu;40D}{ahy0%9{^gN!000tP--F492j{N48OSv zt*%bc<&1UC5h_d>Dd9$BZA}WMN68p<(XXd7-%uRX(1KCO6zCj`B%PV{AS?& zjv+#-k5Wv6A;(J#1+J>-a|&JePPBh>66X36qP%{=g4!hV zxWu~;C=c`W4{dH5NF|{0St<-N>V|~p_&VGUV{M4kv|vi-YX1>o`u&c6xeYb{`H17O z%gaRoAAJd`ZW^X7=h^@d2A#TpKn{}`jp+F=tLhkG<3i$?XJ2owammxUO*GdJFN6SX)pU}V0rKdap4U8-23$5ynp3HT` zhi>-D$*h5b$po;VEVWVPmCYF||9SRMz&2jR+u5MDO{i2c%Nn(y)rm&rNsMk9IT%n^ z5rI1P?uRt0wtp`mTK)qxEiu>~htUPH!a^u`{N&E^$faRG4?m$yHiv>4pV7WIo%557 zhs~3tgQ{A?y^28*j#FQ6t~Ba<317qbq(3DWRddY4^i}dI0&oltFDhK4RJ9YXLm` z=kfx#0o2j1-9v#!M_aONrr+PU#;pWdW#aimAm{Xk0)a{Wg$-Oi2uEU?(CMfIe{Birz zL*=%_N&SyEV3Bkuza+cQY&SOdbPixP2$lV_c0 zl{f9kTOGQ1x4~3HmB59%*&H~%7$Gh)~=ghaSV38W_U=>3s zfzkn6g~kNU!<8XR4+J6m?WJ3okyT+Brj7=fzIJTQ&qnBOpDf_7&PX}-e?@bIm7lX~ zk3~>zYh0Zos42+b{iU|8{RCF@*fteRdAGW!A~gxWS;QYG{eF>Z$dB5JZ-hEc1y+R& z`pT0FZ>~OA+sQ23D?ks4tN~r8u|_rW?8*e?-nT}Z-Euk3iS~iu1VrLJMI z7cSk@@jNai@pLKoCtTKr8?3jq^9A%* z+Qr;B=9@_F)xn}uT}=ayMIl9@GJmior+;&B#5e}x)TQWs05b-65@eDR{iiB@k$GyqHa5LhXwgI3(Vt*8c{w;@PjWYj@d9URa+9u|n z|3<{5BENXM`I=Z9vUie@>&MCyR~NS?0r9{bg|;s%i7!ZAyW?F7woDitr}t(9UnI_1%{-eCDqMy|qAicyDnl9$qT9rwo1lUIBl z6QP5K;A4s+yDs-9n82N!y5cbwj)M@{6cb@ zTKA^zrKrT|0E-dbYUPB9T;Z=ke=A5EDnS+X=)<2ljjn=GauqYAmgD4+`faw-(eD$O$*lfRUV~eCXT+ z&itMVK+AW*y&@VGoGtVNq|~)>vxDMBOgXK0wnBT-7VpWQl`BbbVF}N1D*oHo^Lv?k z)pT%Z4@lw@+g7k3lUo`;>rA?JIPcpG73@r%|J!s9+~xL|@xvv%H+|74nv$@r>G|DkMs~MfD0G>> zg0MH)HD&dcE|YHlnYMq?{l;03(RWyV6$$A=y~PejdtM*cPANIAfQV|eto5S zCIP&n6op+UX3M%!l}Vq->9bIKCs_-p{;c31G=I_tvojM65>ki%*|ajzs9!L8pQi(K`B(f60cOHs1WuQk27;lCV{i3r%IT;Dmh^L z8voLMpy%4JT>O~g{-Pj_RKRcTsV%y`$4F2!>NkTiOEc=JPnu{p=%&?(YbK7QLYaNG zmi>$6l3loLJ!e*rlVdvHduFDY1ulU8fDBED{v+KM=o&ECMxcu(e=BrP!?62{Uh8^w zZs!H>Mz;iIm2BxBy7YuOmehc?5aTLb<8Yyd{YvFpY1U_#GDS5QR8_#GBEN*`r+&kJ z{S8k>Ad6KVttlF0nQzxfC0gK+QuDHYUuQn*E9zE%NT63<8RNg=w@#qoWL2gdbboG! z8QEls+{0m{Or*wVo4noDnA1}B6n{j;@-kLSeJ#Mo6{Y1iAdaRB%VtK0K(+2gA;s(G zjAY=3+o-DRfwk_*=YO1#eEE+AV^(z3I}YctdGhg={tLl|Y-nLqY@VsC0r3~N8+G9k zWxQT+V!$$H$BDCZniw(@PPA6-Q{u4X@Qd*xU3L!Vj^oNmtu1Godvih1N<}{ggPkpa zzNR-4VZ$aSKU{~T9to&yE{Iu8By4f@JAV>dy3vST#D;Mf9ZyT&-{zA(c?8;u7T#&c zH1)w_Hi3^@5S6y-69+VlpU)CW?=zx(H4pkq2ZqV`?;I`(2BN=rlPNLM?86CB?6_6I zR;|Pjt+%w3Z%$1~G$w19*}n->9PsV0o-)zU1hY?sk%lH(6;Ef_u7f=xs zb{9Pfx;gmd^P7INyp7qtCxeTMFGo4+3Nz?RfAi5(qAh!mcQnU}SqDC3m&7Ag5v9gY zXk#84^%ggZ_~#iDtI6qP1YBw@Kd14W;%-(qBnTi7Yk`O1?+SEMo~7M)^JN@m?=UMB zp+G9V=8F=tyyCBg!^FDFmY{_t>g8*#vHZtmSNc zPagjA3c%O;9UxIh_Ix#Lw+>Y+P0v5um`TncQI2%P;EQS(buX;`GiRV`S6gU&W>pG* zh=txA?}4I;bs;W#+isdDnb`D*fP6{YFn5`Fh@B_YnGnHw7r_%%#=Bm-6x+* z6!3U?(|C=gBL+BeG_wtDJo3!ct-l*)o5w6V=JSai7Yl?zxNHjSh`k*#8q#^yK?P<$ z!&6R!9nsK(gKm14ANsKRK~A$1Bc4)!%LkS#;2*he_0R-{Tge)N@NYE<#6V&F8y1#~Qrmql~qgPU5EK*S_DYDM6VknIRgaiqZz+ z4Lp8|GP<+9hkdKfYqFt%N~>3}Z>yJ>Q!;yMoJASeNqVq5B?_38Bc8Ae2c>%w;}< zou^PCp{!>cqsQi*rrKvE)0anY@bMCkNK;=D5-F-B!Y&AWk%@R=5HLwK(iLFBg=fu2 z22<%Xc&4o=6(w7|gE?nXi}wFH#DLy!uNuL#P|@R2tW&hB$*v&XORFeVIwzik zF}$iMEEB^H-u}=%We9BH>YR^=bXR8Js(Isd7z=D6BhY)I`oB`bsg8~+tfTO(GDTZd zZgC+w9xGR$SorTNBgaczvgKXGCrl3SZixsVX+;t-(Bbut!vL8Th;C zQ_218k&1qbw2SXB$vbDDdyZCc)cYfBCo`I3Q*8j!=jiMcUHIq zejq0rs-N1zR5CS<1s}2f*D2^v*1p+f$aAT+yu)%=Mn+U3>4Nx}0q>>Kyc}ZR zvJV4ePjy2QS;)Wr85H`ZJK;3HYwnpkO)>D=~X$NqZ^ha+B8&5yprk6FLPW zI%6s8+1Ho!JZ112PI$6$?Vl9W73Rcn4jovT6hfaf#iCP__;nil*?XY}0kdwRL`ZsX z9l#3ov^({-$sb=AvtzP$s+@4vJMY{ru)2uhDoJ!^5)g;onf*v|oZ?sJ25O39W0)Kg z*b7L3qyeM_4wLZ3iCLJIR=X5q^u%d(HR{723Wi{%0x#R=6{2{pYD1{o8e1m!pC2us6 zxiBIXih<}r+}WjmFFF)bQYB~34mmLVu;{IRd}2<^!uK(DQ>3@}?0103>gZGNInD$XK?ozeaP<|@UU zr^82iNYPDWMsz!(VxQPtRS}bG4BCrINSka2hOSR!NA={tC8c~Sv!N1~ZzJSq-%cWOwHITZK=oicg+#jHF~tWh1d z`4FvvII;^bVq9@DVN$vi{?e*ihB&>i;BWmI|IV#&FAcsqmTCitpV!V>WC& zC6T`uncJHOwf;Wf4-I3v)-HpkRfq5FO#}-E>UYX_3106k&GAEfLY5-SVd~v%-Ue5Ja>fuPvngKY|7BZhna( zJTyF~cLof!NY1P_^Q;-6$D6>f(10~%t_T+?EGOzp2$U?v+rTojlVh;1jOb*x_qe|O z$)!}xW)_5Oaa&n^@=%8Hq0iYt6mVKDRVZy=v9{Z`njADWWcuV>=h*5v8g==<4##N= zTx{agVrn^Q&;gCGZ#ZFw_=E^zBB(9(XTM_Uh-}zPK6MU5EeM;>@l`Gi&4}(O>$y^m zJfP{Hh}^=E?2|!rk`zf5?#eD?E%1I_QM|P$^wD4{5U$*pCZn{h%gnCjb>c4L8C5sJ zjbtD1SK*Tmh0)|>TfPro{JOlYz_e-vk1ft1d}-&qR55XC4hl@U&p-Wng1>6&_MbN0TCP_G!No%ysFP#e)?cMjT~lBC z7q@_hcku=Lr8tbv4Fc&=V0VY}CFd;vHopAtqPhpb1lcHisFfLKM$Ymd;}y?}Pa#PY zYy#0PY*@$Vo_2=q@0_{)VbeRS3U`QBG4(AdTK(BBbq?!gh-k2u3ZYA2w4FiDA zQ?03z-cr^2tj>%s_|P*gAq9FwOwsHGos^r9lLvrXQWvQ{Lro>IXgYW29%_Ngxn_nZtU#9XQ)TLd6nprT3r7R5p zktR=taE!L|hs>E~v0$cuO@-)tQM+9iK9ng0tX@@RY9u)f^auAx$xV{qFdtL}RhKS~NjiV{SZ;!C9QS4B7|l1I zPu^v_Jy2xdrD$~5c4gC3C6M!;eHAUxGolnzjMeliv@DFity?glZ1C>#Z|f7s;?@my zS;VksAH3mZZq4~bz?y=mU>~LBD=jwjq1?GO2 zc}O+>!0j%tOV^;$f_YsUvenb47l+&dGAO2tyiG7SvQ&LXxAYOD(hKB zoizIQSINdj_e$RtgXaf?i~%WTZRfi&*J^F>YwCqzR|y1L#5f)E7(0{wvg_~9ch(Wn z&?&ruiCg$wh*|xBDx$8L^ce8^#jN~_<1xWe81}+NG*=?26QF=Y8iTN4aB!pVitmV; z7OS46l}E^rUn^~Sa3lPib;xN_>G?#%BKEsazxh#9$o_!;^E2}_#s&HCIF>c;)H~mH zoyur{l|vXsYzpK35RVh?=HlH!TO>gaMv)eH9l@QpCSfSh%Apfvyvuk;mC?vN;OKqO zDhM_``7ilnWxy9Qnxp>C4H#?6o?=UcLYP9Gn>0Pw+Q4Pr>hDb!c;{HnF8vbtiPEis zySVI95p{h6X=hFB4|H7FU9pAAH}~)JN66E_Ln@yJK&ukBI||4n7hb_j<$gax2+bF1H{^Nn>*+5 z&Wc>!*K|`C*kxs}`^KjUpQ$^Qne?aj`K*K1)mK}%!p&B0qiVX;4BFO?4sn@(FGxsD z==-S~D-OKKp6+Bfvr5^JVRSdPRpX2V?&B9QwW@P$B?DXBXoin0pIa7Jxmi)= zRUeW}`*Fy%nlbl2&uM5o7TSMr32=kHuxVh4w?j_$OFeGB#mKOQ!c%RF`xoVx`o0d< zYlq6AGzL3O`wh@bZ!aDD5SUR@CPMfSSEnu86MyAa>n8AN1KxwBmVF!Z-Al2L80mZt z&hlKTXG+bKRIMr%kt%!gy~MoID$-!;(36np*>uK4!L(O?Q%B}EOyxqC`gueqB>n&o_;d6|;Y zJ{ht1>y{apUX{lLPwq{GWP;lNAyafqT2qSEU`3+zG4_lYql$4i7D?$Zwr0Y)IGxuZ zy5qTf^dOkT6|76qaXmhEfVP8KT$0PBmVK=n{m8}EPf0xV0}lU*7u?uim)>)qZ#r@Q zgOax?2OQvhztgL0OiSZ?*JKK85Ke=dTNooJV=eR*_PtF9LeSp)n*vKjs`Mi0h)JFC zPsFQa25OS|Z_PVDSkiZ$20i|HL~4M3M-D1-tiX_khcAe~&zYVDlB&x;%LdDRtQ}S$ zGL~vuYB3^9^zjn>)0X?;WR1*0!a0fuw~T$ssX`6j>tg8Q03qNzdewd1kf_F(l*pcw z%F$LF?Nntl1yfjfb#4;LO1!BX>os!O;-P8Ei=0`(Pm^h}f{Z7W{^dVlzLC9GolA;O zc@+wan9tyUjW)_0_L^6*HC@)-T*CyY{Q(`*xQ*LTz=~#V9uBNc?<^H9iT4qORmUcp z3Zu~%&~e5B#wu@;#Pog4uVnXXh)5NACn|cN-&F_IOA6$vMn}U((~R{o&Ka^>0lAg{R@{7B6yqhHS5Hu`XuY zGB>*eG6i1c#R8Qr+Fj4^n|>*8nP2X+=r09fNniI5*k3c7RH_n6rk9dhv>^q%E;rP< zeAZ!1n!te?P&RFc&z*%49>bq3T6p%#67&9h-{ewW@#@;Z3sS*tE$dr3=3!&qMg!GS z&A0D*7zm9U%|I|=!j2f_q%!Y-kW{@gJLAwL$Hf5jfyb_Q9aSd8)ZV`JoK2gpt^_m2 zyM$gP)jscQSy>#A6T5+x#Zq%ck?}eZX8k(6qk3sGru#Z^Ocw@Q46M~WNIn!ue&6G^ zdmc|u!abGNls%BK>!xCOgqn7|i>!AwyEkkmP#}hm?qCX#v6t&Wuz=p9We++HQYy?m zFVt4P0;@0mKOWzp-YZ{uss)8Z#r{*fz;(spI8@>^4f?mv-XVzE&`&hlUj8KS?^Q%w z+5{5y8(k`ac;9~m^8pns;Xat_f|NbW*Wi|&= zG5)pzW#HrA@6S>}Q7e&~6b_{onL`vHh(kvz9SX)_E40;W_cr{!LW03Fcj8B`SO@Nd`vGG_PuMhRhb$QiG8~CS* zWw{WB3qqqk+n>K;VjXV@0b1Bo>fXIR`PrSs zGLMm&ac?*9Pr*Q78Tx zcPds;46>m;{}^)m*QOd5VrrbZUQ1d^mTC+>+`kXkBAXaU z?dJn|W&1SW1de7D`pj_XHMF5hjBB1%hCCY+A)NaVC$ts7^%hU-(L~Y1_Z<_&?`;dc zRGURj<)Dp{F4#byT;JDoQIr2eRvyvi4C}JTZKZ1x6D&QN@e@tar_tJ#vrRdMUid+S z6XOOxlShC?E`9b>D@7!V$0Y?DlY6H6oT6s$O|3Xgi1U7IFD(_C@8X#U5t+PO67t>p zTyC~Yf#f;WQnr)}U0X1wLd=j;S7k^wL?YLaGra5Z>HNoE{?-65Qn!eQMe&YyKxT^M z{bm=$nza0b+@aikXi@h9{O4e4;PYXVnTw&AE$2+?vcF|V>r$neoJ5`VV&C|H@r=Rt zuFn|-;4=e3B!U$2HVWF6!J=s92&utT(JKG=^eF7j!QAs@1!TH&PwDA-{P%J6(+1wLh% z3K{}TMcgM!%)QOz)EHlyqo_SWNB2eLU%qngjb<@&ra$-D zHi#P)TRPi>8)`)Fe$!3t--?9k$^K+77^-?)A%B8S^PLmyf5A+;7tyD&|Np&d*7q+H zJgH#n83P=>lc;H)HT=Ybgxl5)gO_iA;@_bFX}&U&kL~`Z({F zdgp1FH3FabFAwRkSATH?AX=;VZBP52Pcx>Hoo7L6qXvwo&yEi^^=sraF=q~eg2`Do zb&3WgVp!D3LLP^0&;XdTuu`pNU!Im4uth2Imu?VCcatw1?r&GtPI9F#$a#Z80{quU zF6;Ind-=auXQAO#MDxNVf`Gp7vzTTcfKIvRU~$`#2J~Q8K*~IBp!^f8y+kAC|M`9z zxgMpYC}&{7I~`PL)xn515USDe;(690G;)v$n5&I7A!p z*cD)^XnfZ0Jm?~8BgUY$ZufjJOGTpLhBr76CFU(pSJ^#_(+V$yjKC*U#aRN|d6C0l zR;vg(I?bDI{^ePHGqBD1_R6BKxyPAW1um7=TA|L0&8D$iKoDDt14H@Rgxd=G*I*}0 zbkl}>cSeZA-hB!j72X6aPSzJ##mA%xRv+gHG>|MCuiyM+7hAoYWl|&FwD!;iBtU=A z6gGR5i>(`LlL}qpThAYZ#4reh9+y1*4&C|5@x9b>AV-Cm z@{)yl5p!4(NEMd@Suv1LbbcE56GWuOL8?k*Zp!4l!|vLQ`;@gm{8#N1gnRFzqHU*;dCY5EM?UwJu8cJzGg-N$I*Lr>#>Jbr{=Dzoo_=+% z?(1)m6sSn|-J4Gyo9hf|bc?fxi#DH=n3nV3V5?1#y9fhR(#pTdnngn10~j+v@KkPY z>*fj4H9v;ypTIov8TdLnKz7TDfQ?TSH5&zMlo%$S8MJ||b{&ya?L(~AqsJ@Coz)a} zhG&IasTmT(buVU@-w2YQ&o(~d`4>ZFyb3{(pT9bJ?u**mAWXkr?-7VkSv$bc?9vT~ zC%9M}KqLE(_ys{g2z-9iCurni3AcfqpTbnb+!D?BYfo(p{MIk7PF=k5;h0r}SAM`7 zj1PoW2Sif!GozLu|6JxDLPGN)y?$9GOo&+h2Q&ZCws3utTc3c1VdS6dZf}!Vd-~GR zXJE?DKhVv@gHCP_cHr$lw-mx^X%K72%6-4e(qkoM-UNZsL;qEz`mT)Duk^pj?i=C% z^k6pno)xBJK!e}yXB6)+f&6~S&*)C>i`ko`mZZsU0354s>Zay#yLJk>l_Po7aYnM=WTV0_A`>iMyb|6Ti_2jwC1x#}@zk@oa+I`wWmtn*>Qg z?hTKb7D4rQmn*?1XvF6|l%+IzgJrWerJ@AqBw5Uz8}9&i7DwP-a5W~pFUSlYg~p!A z6fWp(40Ghn?o+Z_!x&?~kGIB8<|-~d3}+LFnyi3#F9TP`7a;cx|KzY;4yJF~eq zAAFXhY2Zci6ZIG_>E+s~YYuxLe5bnuR@^AVxvd(zSlM5^9>L!uP>U6riU9uoQ0{Nb zi9Su?;W{{K1qSuN-I>SaDRk-gmeWjwx*H;56@oOh<30{>-(oL|_`?#jUWDPREi zB>v1$2psK*pNe3$VptCY)m3>=parD4N?k|1cD8T~M$qEH8Kaw_e>U4=Xu_&(-0&ek z;o~;JZ0D~cS>D4>^Q|y3rk8;a12WUW0~dSb?(Ew8>FxwPxv(-n3s$wzi*^WS7S%=-sRh`D8JDm zIC1xOs(Zm#-wp3u4k0}m3hf}Mg+lfS)%i^N6C3T!RfHISs8RbemG*|^Y0H%pUrNqK z*nHByR1>r^{`Z^&ZlM#L$+?(izqTk(nl;TT<-U#QOp>Mhmw!3Yl`#-w zt;5DM2QLK{qslf(Z!Xwk(gh#7L}Z3JP14S2v}2IB)wt{G=RUY6-O{%~ zmN;AH!@Dm{e6h%REoq?|Vh@pVQm<|D-waP*Sb(?h%T;u(8VQ)4u%gJ_ar$%ppEF}6 zJCaY2y=hQZi)PqY5dCATH+!T_?$%I|36AmmL)4UZqR7e-D~IbPR9}rdUIgi1;zOFF z&Gx%Q9so+>K47kp?yf+$N^M;VqJl3MSO!M73fk%t9O$NOB0prpK-)94s5MeUWHa}w z)d82M^%}Ih*8aJgy1A4yNRM2Rzi83mlVY6mAk{tVm$f{cbMC&)t7f&gRk*Ofi5OvQ zC8CXy!(PPiqkPQO5zxApxQ4s<))tDCfGc4KRKG^jb(=_7J0kuXR-;y0gj-&+c=;aw z+`z0Ati3e3>8;wAxsza~WQ$mP0)PMxeZOrGtbUn6JOVoVkGM+EwbF?@k^AL&j6^ zB@l6-{M?6O$>c22Tn$|8;MI(K4rR3H>hk87whupl<9IeX${uy1|hig)x8L;K%j4YqFEzfs(uG8fJxe0c{ywkJz$xX5jw0n8Zh zKzS~4x3q0^d40@a$yh2{Ju>w|j=&pBaDF)yhrh?H4_2zBXzrfCe<`m9CZQ`Y+_d%$d(nv!Zw7VtZ}_< z8DEL#sx=jlhCI`Z_H_$@w7Qco6ddsZcDPw%G~*66YpeC*Yc{PL)y_c$GpW9UX+D8~ z5B!Rv#!<6FC!bI^&@)SAr*C{d#ZwXKt`bY$n1v-m}}c89;u` zoc$GH!5pnv!M6xGZ3UY?;?y@UzM=HxVqd`>uPoY{SYgM2-P2A5E8*Yr+JlO2_Goym zcPZc&s@b)3`#V!*7GbiK1v0sRSCyzWM)Gs~0=On7kzg}YRe9K3=LHf03k_tKoemju zjt7Z+j+0-9(ySIro=r)fU!v!p?hU`K%DF%DK#Sj4aEp&ueapm?l4aT?k#;#@)d9%O z#)+9gEft({nQRJoIYjT|#{B51_m%(=1izNxl|Je`WX|I+i|QAmM0UxOf=9ziDM6s* zw#mA1~HAb$T`1elRuk7Z-9z9=0C_{6Th{5=7~9b2(J)? zGc`!57?&~6Ccw;`i}Cyou!=$8D$e=d@Y*qoLY-rOCB1Cfo*?|_s6SLIu$Z}XB6e9; zAE9iWv0YmbF_YlvoI942QfU*{4T+NA{IIawn1=~a7F^b}ns|{EEQ}rG&)8i-XJWbH z{SBJg-c=gzMz-{Jp-E-S!IIeBQFFq>rxt^`VQh#(B1xXiFGgP&7$J}9kB)hXHbdBI z@522`R;$xI=1D?~{sn^j#pMC%gKxSUT) z744X8)jlXrEr}QdIp^MXZJ2oeJ8TFO;zSywFrJSL)t4=kFQQIjc*`H=p*emNz;<*={Q1$esS01}EBf8b z#Fzmiw$FF~ApD+vkHh`iMz5JXWEV15%ewX5Z2`+(Wq`ff2b630kf0vJR;Y$r6~T}T z95W05^H=F&K*uYtH`79?ftpj@`5Ux_q6ot|q(#lt8YDNW$?H7^Zi`UXat6flTUO5L zm;@R#qVSZ#vPaG!z5{12o_$h2FITp@D0s9QTN%Zs)r&G_HT(rM>$)`MH=I7o9{M}sFY^7; znbSxU;-)j#a@tel+A}!cuTI|7t8|#+ERLLt!pCXtFcB_leW(J6tn&xgKFURJ+uZmt z+GV=*{b`yjIsy|QGB7e$)5>P7GQ+fEXM%pJCCXMPY*J)wbhzUDpbg4pq$4$d`Tx!0sIS{a#-DPQ1tl-Pv`K=R~SM zVAn=5CQ)96O*vZ5lcD0>i=CF}Fy0)#MQMN9`5xi!PzH|kSCDq9p}LvzdHuf!@L0HjC7+UD2Xr`O z3dm`Kd9OgENMx-4LQ7cn$j*^kt z{jVq-{eGX@J*n5`zJf#ubIVeW-fhg}&?#!R>RXyAdWc*a+13-D9n|U<#od+TmZR#_ zMDD7It$uMjda}7wv-5i89SzonF`8SaX^@)Jp$Z(7Ia-B|P8rv0`Aa9#UUA>w@JPJO zM`ROGi7hqbZ`u>dRb2-jK`q8JkMoA7T9o*7vQ3GnheL{Np4qkq z`5P?nac{wmMBV3-UuALh!r`*Xsf6+DCAnKFJP~yKxmnxsB5uvdTodudvB)bKmSL^~ z%H$XR__gXjNUUj&)wMGAUMmWATGU**G3=%rD@v!nx*1qE*?g_5L}gJX(K3o{w&UEX zEo1FK?cECCCgdDocLYQ>eY5BYb?(c+VXLfG4Z=pxIpUmO)}5hSnCnb!l%g1&t@S{T z-Q$R0lgzP9sFMJWqZd?rcT%oan;6P96^=H6sK&eLZQlF4GfHi-Yt1?gapF82 zIMqcKnvA~Pp1bM|Yj#aCC5wU*2qe<`{zeRvL%AuxOzTooZr^lrQC44PN_cV+Z*8k` zV&Dct19Dd*IWHI+Xvsq=911BAv(svyqI5>uD|86^c}adUul)^L-6wq1{Ig#SH-0|s zI+26@!O9^?Vb5=Ze2-*FhZV|?epQ!B8h5IuJzkIw&{d|NO+q%FbwjZZ=|j&?9odyC z-2%yBhzA!Cvser^{E)U^>(BI@(4CPFRpgq<%Phc~YX#n8yu@Erb9?|I^$HsLo>{B0 zy4v~gxf<9-v-BAQ=CEOT7Uv0 z3(dYZGgs};_h_dUC_%Nal(EgLVNObS%}gSwucAh+K%vIk>{{+x+MpQW1Db{jbKlnu z0&FgcbDlhYS^Ytp>zK8^k0-IGDS2Ge7d1Ntd-&+X+a*LUXKPU#&B!zMEBa`8_O1U3 z(R}2NA>UuG%o~luisaxJloR<-@BftR=6z9#<-M*=rJN|0Z<6V#`81|)Gg}5rQjQjW zdDk1&3f&6(GAM3riE_PRZ1u36D01L!Hk@j?Y`1K*W*H zHQ*lQe|C7_$vvbbq-}@4fNxV=Ug3| z(9!7me8gZv)rXei?VOO#@!a5yt)-D6_w@?4mPJ8t@ggGBxwqm=1cB0vIt%krwQH3!yh-3@8eHu%*a#Fgz_miCv^coUl%EFQfN|Z z)F>DFu7$gr6K|G&-Grv1i*SwZ&6a#uZF@@D6y$SGKIigp{M2C4Wju~~2vyv>eHJU> z=km_ye0#?l|n!5@O+2(VONEW>z!&F}znqh&QhQ$Yz`U7VT z1{7Js2HFliHS884OK}|Psopv|cUK3OR-#sE+x z5F%NNrSndsZZeAQaPoi7S@y_|6C3Tx9%uH&vgDX9NPPReDj(8UsOG(m?lLRH`}Ig^ zu}HXHdWaW&0~{f__ipK3+w1Md29zfHtaJ$7D=q3ZG+tNp+kbVfWru}gajrAWDmYvR zxfhyRpQYvCa~~7q?oO>pKH&?i;#9}d2}~=YA1WnlMo&BQ%SX{&{ndgF4_nlR4ncSV zd+g&zVm%@E7bibs3;|hpctFOsrTd!yk2t%dwA4dX&R-hBSl-i@1^mtDPH@lWaVjGG z_Qx(^nw_lv=;oZmjFuR@3%Tr_vQo|~PhF#tA}y`LVo=! z1Micd7Rps$!Gjz+lN*ZJ9b$iKP3*r;EqM+0iOg8K2Noq$3w5p1J~P&>WiUP!CC8DQ zNW=axXDfBw-A)_}ac?UP!&bdo?echsRAJ(qNeR(8kr_<4_JditsQVi(s2gmCN={zL z&if6b5)_Zw*1k!M@TWsraGq39#AL(ZfVq_SE43Z+#;bytCdY%(FX!S4`f$Ef4aqU> z*5vP4gVN^f6dF>Xeb7J=#QI_WP>g!Lv-AKu*B>;}<+Gjyx=EoDIh+=<1=O?YxU1?| zZ)G^A@@ntRSfK=R8PhDQi%R{9NBR2Gor6dva)+iX0f9U0riEw%dA6TOrDiki{*9gX zjamEj78BgsaWx!Xcm^BqUQu=*njyy6ccf2DUMou}AZ%4d^R=s_9x{#%a+j_G4{PPP zA{AO}=T&e(`nNLfpXB3W2d;xtzO^)JS3~!{`HH-O#-?*3vx*P7jTndMV*ntGID{WP zF19G*5(kP|Y>u)liudsww4bT@ZF;tCN2*kEzg3e*=B8&J57fQZH#XN}9#dpbWLEZR z{B~uLFCTIC`2H!>yhkl@7}7enwxGJrT3&Z(j?y_00_l_qC1h%`>V7im9-#ALpJ-F) zm`j;TH{D#N>F&64F4^7IT|Cn?uOUxEVEH5Z6Guf#NQOG2-iTT#GW1yP1wT@aIRsgH zs>UtbYpoI}SqhHKC=cr4PPDRqtj?RWCO5AmNp4ZANGbC)vKU=>sZi<&%V*E6O~J6@ zh_Ivl2Q(Qc3&zqZi|876n2r^WrjDmS$uM~yCby_{7@4BDzTekFinvx%6{+vABAJLY|{~r$N8SW z4z1Qr{IaBT)74AN-ym^38zU6cnC6~TI(~HId`VV$t{R9`4oBWak)V(1GwNw52v!u+ zbJjlm43eS7S-hg;794&`iH}g7Y$W+1$7wpXdn3>WY?il?wXx$lu}m0Qzpd0Q{oK)^ zM3)riq-PipUOC-o&yTWZd3@~0+`ybq~k zj>Za3Y?b>S0}7~0GoQq=M<$RUtZspL$u+ffB*~TTtt+)_rQU=v(zFJpFteHMyLXUe zKTe@pv$ub@$QT8@3cI-|h)}+vveUxky?2s!wsX^c?66f`${sbsE}8v)czmmHQo2u{ zhGiKyBjw`Nvmu=vPbg-4oieF)~jpSd);J~b^9Km zD&25b_N`eEx}Ryrv;#snV|g8X{TAI-N>9D*cw>>}fX;f0>H_@!V` zMHjlNc1p8JmVy>j!CSoUsKc7C!^nj=`#ID-_fE#~c)|&DeG?w_bjb*vb@kNt@$pb> zz;{#(qaMG~ku&@sA~D5i?LTKPww3GT7VF;mq;@3SuIIJZjFOUG?(OVvQT1oD(>_i> zO`c8g@#SFhc=u4=j%07ul2H0X$GNEnkRUl~ysNFxXTUxRQB(gEBJFcmf-9m92m&60 znXIk{p3b;BE&I-gA1R0bC^R>6&C^>AS0o!hWUhQT=Mzwl&G{?Ax0}(ipBZyJE9Bbl zQIh8GKg?5DtLJv}-7k=j=QPeQe_{KOxAro6o?_{uyH*VE+uGEv0z>&%x_JS9@+sO# zy}T9Izj6zw{mj`O=KU;WT675|@!)?^(_thfgNpNaXuWMQpFJzirT^A>zzP#tvY$Kg zR-?JM)5Wl~x+GXP8&3)-$YS5#=5K4hwdB9de?5r366c<>To!E@BPp3UI3fV#WZF-^ zy}3oU2u7KlG%8A3;-|&S!;a#kSNdXB+Wm7~(jOYGIJ6z4lx=#CRBdP$S*~A1*z&m_ zz&7j8X!mEWqh3?xnMn7_dzBtKQ7hFw!~&cX~LoqtM1bkLSu&r;M+w`NW2MqE)&-JzFNoTF|@Q6cnM=qS3bQog)KzFvO3$GqD-pOlJ;C)6YnInq7(;G}$yhNA6tDw=U^dnl3C zeBy7{wp7GCqW=D}#3=`>^x_ngD}}yySz!5PJ|6MGeQYuf^(wiJy!(4!!76X(^f+&`k}O~z9@sg3cEl$u&zaGQ}p9RN3-A78?b z5yCt}?*tNF1c}tfnWQp*@Fss(H@`u^yENs{{?a5<(f4mo>EiQ$;Jwd<-&S76d3$ND z$m<)Wia6yKzSRI3Ow3+krev7NQgaQm^Lm0hM~HtKWkFF+%~mdR40-JQ-ZJJw6l2P# zguaiufqJ|5XWYb92dGEgWI0CV$Vbf|N-}ms!I^-)-4u*XId1k+HC~`iK18vA{(PYm z=iE+;GOu+ZIfOe0ENQ7WqJ~SHw*?S)bS%CPSZEY;o9CnQ>brLlm+ghSxoDb@-ph zMO^&ZZzX9oFR1VopZ$LCnC|> zjf43^*STHO$FqHrblQ8MKjX;v@VxSvBf>T^5%?}W8bE;5s@%f_ij5+#*lr{$>J%s^ zJ*H`U+mRY3hdABysIyY4>7F~kkkei^Fn$y2?J_m@`5g*F!pALcr}9#i^6))mYAeE% zv;MgwNR3<}J(X*lnHGOwiac_aW)rz0uf-C1FGV$$l$qd!-5TjxKZ|nrPjiefqM=sT z?3{#j$&TLu*&>7cy0^YV#L;o|nUAsArFU5*UZxgXb9qjqh`(GoF0vb4YGjiK_zvQw&kwMA%;d*2{X=iPma)y(;xhaPvm zWG151h_Fraa(~_;t(nE&(-XCjrn*e*iV<8y>c5$;$m8_aJ-hg|F0qJ2J2B+hMCwk; zZs0+gc>cITq(yK2p^ObifU&VTTRZgu%}L!*L4@&!2i{RDv2cAhE*n(2{d7n9HI6(A zzw4N9lx^T{`YohPHSz78S?Y=|m;2spYlBzfZX~{mqV_Aam{Yk-ga6ePY3f<1-D)`I zt>WM7(epeSYZ74a>+0ZKRjhTrMOE&mO6L8!KJ~TbwbtXrpr=$xKS{EN2&XuaUy`c9 z9{T%9##&BTJR#5A!h+@`J}6al6MyRr_X)RK$by7~{g1#)45A?0H!gWnK4y`XI4z=g zU%xk~h)%P)N+XL!$h5JAuZx|>K<jxnWx>o?p4ZQ!%9xddP-jEwL0$PK3}F4 z4(KN@@G3_(WIT)z;hG@LMkfH%BSn_+{9$C$UC*7WD5sv-4VS8-S24!vJCsZ+6GTa9 z4)y%G=edi_xl)-aMdX{g(UH0_nW~Aup2r6NVDHaBz45D$kK4={vmlShTE8$fzRxHK zyLcw@l29|J#nb97=u8BX>Rk`;OIaeEAtcEM?x*z2QFINMl@H z>Nb~Qeo9tBj(fgl=KZ1e#8ln*-()9v&s(*#s@R{VWcDY&Fjt+PgAu-$9EF{=$dqHO zA9Z;!=7vJK^Qox&+eTf=vNE)#3)#%UdJy9BELxhs?E^G{_ZemPek4uWvR+1&$teHr ziW6l;HAoKmMJgyrIR}Mhj^k%*tTjpaq8F)Z!!~1V$fM0;tx9F6g#dD5Z*xC7>@dN+ z-c5_5cK_173Qwj*H@TTMmhjfWUBqM1M|{o9Cz3fwnQQU(jXOV@Qw&5{Y4)>VT9J4Kbcq=^Xob88-&WUJY3qBhV8Vb-&~`W zqfoP0LkF2TAJim^T)Z#WtCE+_nrX=LP@iJdb=P`xL4T4;*aQ}V&pn0Yj0LGD$!CCcNT9Z{DumP1H`MQmAjRTGU+&tIf#*kIc6 zCs%X~Ps$xRagzH!BUhn-pNMwzMGb{vdW_DBOrBrPr-~x=o+Ehk+Pio1m49Y6We74} z<)~q!wXNmn(>`m$Xe$ypJB=UjUQP3SUv*E!EN>sB2Pm zkA_M&APUX&u}~DRU3IHq^&XGG^^CvAs)XQb`3kcLNdGj?wO>PgucK z4(6RbZ}favt#3$ukqV)?;i>*fzTr6P2>Vf8+_LY`*-@7-r-lR4H$zDE2J9DDKfGVd zu_3pbtA6i)+ENmdEtTb}@V z2`G8I%5Ep?efeyu0*yzZDS1zQ%bO#0&V4`0zMj*LSK^@{kJnFaJ%O)PpW#9Vst?E# zUgu6^?5FGo=3Y+iV#;L83wx#GdQvVH?wiI8Uv0a`hw!^})4q2m2p7#Qp7MX;Glte|7lBgxT@gr$k3ZvDfF9SgEDF`0n;06!+(|w=S9fL z(KY*>9sMdgy3a+0W5_%z)-|O{fY3jyr#-2ccn;qb)WdccFk)Ioe4kOLStq5&r1)D| zP%q?$q|CcBUef3Enq?SPI{eUzx{cl;ZgZ?<81|##_RSWq4L=@H19COUw$HKxVib4dWTbM)cwq{x}Z!cfehs-va z;XyS1r`S}*oJ7=$J&`7OC?sxTO^;!YbW0Dgc}iaC#?c{5|LX+Ps-~YN%Pn@1i#54H z-JaU6Y7`WBniDjN>O0j0tq_Lo(D{(u;?&l7bv_l@NQw!bpYuT9s+L8FMR_eI8y0NE zjsEULN9Jf09)D;j@`3Imc}cbpF70cVM*h#qk){^vRqWH8#uUC~pw8HZ&VMTCR3>5U2>Qnv)ugj}eXQ#dI@2@(L-+ zCmQP&*Ti<>(eepySRGEoDXff}#qT#olj}7g3R(n2p$`5hEGkE|Zagm%eti3DQ+m4} zuSI0)3P_kf$)1jqZ^8%l3QxXKU{YO+%oUG9aT zqKn<4F8=9jwlUna)#EdOA)ChbnddxFc~rIcdx=}=YP6R3I#bly3ptyw%QVJl3nfcf z-(~G*aFQd|sYAL|B<72|g{5b{fEZcj@?&>|?tTdmV0R};6Z;+{Go04A)i04-Xz9Z% zgt5_n=yb5x>4*53;fZ~od^k-ryb&+%p^9v|l_ylaiK+M`<6L6ll(>a9vLM7o$MqI< z9rPveqJmGkq6e%|!E?J)c!ihI?f%UP8Y4XUu2~D^;}4r5@0$tZDZL`CLNBgf@rT1@ zzthEd;9bLd9n@~sW%_=OGY7XHf{t#KXgjix&D!wF?@}GytDYox&~7ynNJy^B8|BA8 zur(X2TOYbI>bbZ0`hCe*Rpamg0{+>eeeT;*8(V6A0sigLxA{$@V)HY$Xis>qydpPTxmY&fp@ zE?UwYgUF9`7wGFBwfN<1Ob z&_m-wGq{gv5;R%YWDW^>$k#GWS-~2F2RvM&7kbXB&nOM8?yd1uh+HP<%~xk=yGF82 zWy${pXsa{MwX^jGim7!0IfjpH(r}R4O?c%b3mb9{H@c&^2p0Nqzyx@nRus*+1+N zhGAV?^EI-8Q-!7b;|la3s+V5?Q1*ezqX&m(?@d*Y7kG3L-H;qG^Zn=S!zo zSxvz@*Sop8(RLH8tk(?=MALyiVA1!Z&tt1k_&07FiLqPPS56S>LZKLxFXazJT@ndo zqyHT5e8IB$v1Zc){)7@W?W}v31ml?U7p|gGQB6H22-JF_C*Z7y&#l7!d#4j!FnEP- zSmV=`;m2ecq{<`}lmvn($Ppq|wT6){%AaP_SH>G%O|810=7 z%W?Ag3*w34YQXU2P=CK5TKdRovAgnKUoYJC=B;5v%~F2pfhvsQ_uVID``8>!o?5;g$U!0QlGzB|OOh;xjoY zU{JsiWJb*V(=_Dyk0HaQwY<6wgSA6Xt0Z>IY0wIXVilCqrAOt&@*3!kzl^!f7?<}< zXKSPzHLmO~H&y*iN55cwomMifn0~|HoEA$7T^l)N%`qG~H9;Au79Is%sqJ~XAM{K4 z`Ft-d)e^?+w3JFbj~+km6nY(TrRR0m+h1rQRy}}TovFeH?fKi6+l;)m@oU{RT*;26 z;|96h_a60)foIBv9iLXWSI8UTg|t|-1~^hl^~sf4`_q_xefbA<5Ff(CiV8#ndL$`z zZ^|k?gw3dh9$lN6=*ilO>w;*3x_UHSbr#%bQFt zp$|~JP;m5QQ~Lb$7k}+?4%Wt;y??xcr?N|%pU=-vmh4sCRxTpL({4J}48^J?ziY_o zg0Sf^*BO`Ae?X@JxuB!bQT^Rp#e7xs4}t}%-7c{ITcK^LE4X zOd`CvwXp9;JeGaj1>Wcw;3lK&b55T_eVKih{$4`mCT=xv`Qw3MQx*0KVK35z>niFR zdK-nfLzAHO5O+$`RqBZ6P3mmTZ^0t?xVxz-k-^_~I&7I1%lEb$`<7PZ*V4HX@=U*8 zmaPEjwq}tnuQa2P%F$DT*6gnJ95QmTq*dSC^#xOBt1{W1G?(B!be}4)j;=-;nTuQF zj4v7N>`{N}6js~7I@PJzPRT4D-le`JIh={ z>$1(JIp(;io{$^bBZ_pOI{Dk~4-^13FP{ElSEZg*+W3x!iRhu9o@A^>dt`HA4MHZ0 zK5y)y4`+w5TRERfj(7Hit@1u2st-pR-zzig1vl`Bv22Jg1pR+ zxwa$HgZ19t;{bl8Y>ngR_~5cUn#~0C`a%SuuaGudCm;P+juaG15dmio#TNFP!Wij6 zHubFs*FX_UX&iWnl}&^#0OXy7^-{uusT{a@pNQM-5;Pd+KY>oAme(QhC=`ajfh|&X zljrW@_ndsz1;Ny7m;xP+h%cH%?NXoG#wi z_U?A0xM~woBlZ%*fOql|*KYMIEh0>Gsb3Nx2S=gae*)l;vMKVR1RK#kLB<$RY&tEr(G!uAln40s z;nj=x4-fV=nY`D#xJb7~`Z!kL4pq(jByYyoiB^rxcBKz5gDxoTML`>*hdb*QK9r1u zWK~X;{hl`s!2-j21~z8K{&L#~&5pBX;s<|w#lsL@of&qWq*$^YOi~9Iw@c=IOY0w! zTV7w0;oWcpH5Lw{{SDjUJ^)554TW~<9sniH=Vp@Y=fxTIUBlg}ILiV`G2o#20Eun6 zY#yR8a^M*>F3o_$)a?&IKXEFzhKA4ZZXIxTN8ETK;l6i>&wwJV^ud0tS<*>9b9D*O z7Y~&LJUuBeNKT8bwSIDXV3@ptiZPCM;AA3FaS8Y{F3Vd$6}abk2A zxa{+kPok=3pJmM*=_;`Kb-5NS@7`y1aje|{qYh4T6~>&L)H>^=f|mDZ&zpng%Ma5P1WUlBZ@?m}RQ1O>Eh(h6pZo;BwX8lRfDN%N z?Bwm(^`bNU6ra`lE~)h4OM>ih;lM|eH{x%iZyPIN!-R&d7iB{!R{45(eV76(-B(bG z4>goGz^U_v3JCehw>yIa0O@447gWe2B_ay4g5rD+o-ZR zFJ#TxRLLpD(wYQhhjFQ-hsq8(iBPUzny={DrHmO#Uw$22Q*Ps#JavpQra2n<4U0vf z(FPTCE{e*{Pc)xql7Hn84Kxt^PVt-7Hv&fw{+4OQkH3EG9v+rEPT-r0xz|M6>_-m12eR}EpX=Nnh|?$ylKpl}-C?Hp;2bH-s83S++Cz?`=_z zZ%K7)=>{!*}i4ja&qUeyo!B#Q= z?7PTsTpxIJ{Y)`A4Tt{XAcj*dNo-*MZKclk@2L?L9qtFOaq<6#KP7&=4cI1&^MwF^ypO?TQq0=dzwN$KBTGISR))WnKR*?KIG z2wDSYxc65!)_HM5&~V?KBLCN;K>fNy+VPQ(rh)k;rQHCdXuNT7yW!jZdTpuLDs}aG z5eHBX9}leF{d*B4l=or_+|mO*4+azje!l8>k7|inNaKzHgEkcJdcVZYu<}Qj>*kXt zdxw_F0AHe?MlbsG#csKEwW@F}+uyGkSJ$&91THuAm$TFti7pXPg6p%f6Q4pE92ZV* z{)y<&Z!;)eO!cb0Q+@K^Ap>ia$!y7KxgT$BE!l!(>-V0?Qx`bR1;-q+N-c)Ci^1ua z__F(MnzJI*Q1b66uk&O7vr2rwuH;YV@q0D!Juq%{Zn6CUqVM?~|Fx-d&CC`(h&2{x zO~Z=8&*Qd5U)^=&D-dxC{k`e$^iq zsYz^i(g|9iV@#Z*fDhPvcl(d0$3~PC5Dc%2rnSHM*#B=vJvU;z!EY3{^7(IuE$7w3zE1x;)GveE`s_8DQ|!*B;Ypjl zow|z+qb`OMpZ)Wl=6K62aPmV{n7rL(%YwxT!sz$lDW!a!{K4KTmEHeda@0+#6UQEJ z{1SCu`0&8%pbIQ;(GY-?c4s#0O|fe)WJDNNq8h~qUn~xl>vyG*p~tWJ0z0yQ@UZW= z!T(_iXCTB1GNJlQEWPEcwpu`J_cOxg2}nrwLQuT5)+u;g|K&L{QiFfzf4igR4bbNX zV8yo@8RXX~Gjhu_*s$X;sS)x>2CWN4b0GL9)Bj>cX1`t>sqO=LPOCsh=ZhRXux^fz z1A2L=tnKc>toZI~;K##573S+LSd}5l-bzPZ4x@~q>qY-#D>;`$$__-Pfc@1bS#bBO z%fmVUnZW16VDC*2M3>?f*WL*`PWOXQuN6evr9cEHI+*}1qHz%f?F|5aX87@O>K!GX zs_f!62<`F97kEt03Aj3T-@7^R_WG+4PbV$}35n#-pnKDy(WJ|LSurgYT_rgI6!eC3 zyeMm^>Izs+^U>dN&j88$k)3sqkh9+o|HnG<`Zd7wp3<7Q`f!290+8aTNClc-=zkc$ zOb^D>Lv^0jwcFF`mP2LM&8BsK9#;^l%1d6r9;t))vxUM$84TgTliC!%zm#Z(%ZqBx zaH|Erz$fSB(Meugu2g+eDnjM5`a%)rBrDiEy4$%&SG9{e1K}N=9^0!YMn7&%eK8 z4L44}qI--x{^{hm(B2xiJ75+S7kY6u^VIksPk5L_J847UPg!-Q#9Xc0v~OaZ_m#nA zb)4`>CKhB7wxED_=hZSboKK~Sa<>UqHR*}9+AT(64)Zl1E5{ZDE*m z+c|!_WR`JqI0#z-!FroYdK}a(cSpe13#@XDfk`8XJvpohGj0KDgk``ldSXtGJiKuZ zmCI`kmh8;8zg}}KcA7hv=P8>Qw3VY>9aeoA{Mg}9QS??}=tbw>7k?hjlJxn-YtPQj zY4Q^dr2WRb@*B2rJEKyqzgsBs!d~nq zV)j2}ML{Cdv}^>H{STG}m4n7=pe1!w6fBQTH;Ge7=!>r)yS$jC&L6j3UXM-XJcGp* zy&5qRk^y!t4(;5?-P!}jTMtwA;Q&zZL~R0~x*rNU^E$8H;lUWM zPC^;caC!91mnYRF;sBrh*m4Za*3K+|yu`~N^Ki6rDr5GVsh2m0IC#z!T~N4e*~Adx zOuf?aa&i$C*?|qgVTM;RWF*Xn=5*ae>uXp>iiwndpm7f(y(K^}C*OUeafJ70jIpyt zZha!AcVH79ives!#h%qASEHlp#vo4c1RxDiL|<8X6Jue9pAGTn)kN^HIggq=^Ge~j z?j7c$G08<SLCkrFvM^x^IBT1}uQnNiGmCfRTEm7S}Y@zEl`%Jqwt%|!2UuFbQzcV7oXn9ONb62jDi`E(k>ecd*ki=p?z2db1OA-rM zEz2QGZU$0t%(NGvo*xsfS4Hqt@DG|oFVxi2xAll z(WythK&1WP6A$XGgRpafhj-Q4m_G#%>V0-y2t8~AMWOzuXv;u)5Lk?e*gWb>9p zFYG1;N2@Qp=$9*Sup!{d5A#k;kP3nBiqoM^fswFb*~ce88s9%{i(3M=58V*a!vNT1 z6%-e5eF7|Fot4|)QI*yZUq1Q_|2AE~=k(pr)wWEKTRpGHc1*WWhxoJ<788^?+73pr z^_QTto5PMBRXYMZiDR%uFE)@x#GJIn6H03a>%I0H7uF;p#4L7ixKX9$FDYcNeNvzw6EgxNI&lmcUw1XvK|f$^qx zNo;OJL9iYa3kJYAb;NQEHUgd-t&%|gTs?-PY~stvC} zhb9z87NMtn%*zr0by1$bf7}!R?m2`$nKYbwohlIo(5D40A3P9>i-S6{QbI}ZHox!U z67p-R?~e3UXuy?(38jUdq5{>KI|V@>qtEalv(>Si#4!A)R+6NB-a@7gWpMbd&S7nvAd=zO~I@+{+4}E@QL_nUeCvn!y;1NnH%(m6?dJD?FXnC zkvx}i$gzWe;q`C0=k2*2ZJn5>|D<4}&z(;X_yH*|E5`1^g52|v!CI7TX5d-i2>vRm-k&KswAr%+;r=n}e*ZY$w$dTt zd>GBD(T%>W0g^-6QCj~=8>z}2_*~PS=9IW=yGK1|RH|oxD!sQE*%2-kCV75QWcuBO zGOzvPh7~Ihw#FcAdWSE3(D!@lRl$1P1;qr3!^?)nH{3GoU?J3lgqE0tX^3}>)_Pcf z3(+sb%nNLd!H-L!CTUm#!|c(Wr>!4A5zfGebUPo))jB2S6WPgTBUKavX6;5>jvpV( z!H?(V7Dl zfiLeQ?6k1T7O-Gx^lYe7C%oggRJ}irg&L$zwR2mx-+AH^YJq!N@hOyPsKQ=`b~Y%Q zNdkn{Xe-$CK0F6g`&NWWTISXrmnE?+$Cu~6=Ia-yznAnO)mV-P={dpcc3L@_a+aMb zCO1e0cda%TF0uKxtnE`ElZyw~uuzwjR=nzik>v!(q@$ z=*_d;UQ_5(M3DSaC44$xA^iOBWcqp4b#3I`o6V-nlZ!rNmz@u5pSH1kEWD{Jd(`ND zr^Qr(L8ED@ZsrI?%Zl#>j*w8@eEaJB`HHio#2hjRG{L;pc5wO3rQTz8kz*8Kr^TW7 zA!TE3>)nV`%Oa>QX+l}l)^znZ%*BXU?aULyFaDk$^D5*tcN~7jIQM9@8K=l^E|1NS zie#0~n=3HbXE3@LDZ!RW7JqA^oEWVz)3Vr|XYzqb{rn3xq>#-tEsKAmZ)Wa+KNYtV zMK!i9Om*A1%|M}l2lJz)mXw6~n9XMvJD4A@DF1robiX1fSipLp0C6!tH| zS$<)4on%91+r^C$H_*T7cpWDRbNktfdo`chfFTc)c|b#gn4#Zh*Go7HHIpLfU`+M2 z93^o$;S>Mm^ssW0SQ|W`%NwS!gi@uq(gPgwfN*NpZFmbRLUBP`o-?&-k3XJ-XDmB@ z!~Uq?U;z{jDrbnP4#Shty+4gEn{ziW8zi#K4+tl=>qhjb-p!2Kt<*2Ruv9;fw|xkc zSNHahR?X)WF9$5}oNcD8U@t$8qjfU*bd0383oUZ=W&0*}f-f}jXnB;zh zw5ht@d#&xn?({s*0ycg*=qsG`&(l8fE9b7;;nL)A*~!E`=5}k1KG4_k`SD^^*l%W< zno>ih^OAAvm&_7OLF1)#BH5do_>jKDZm%6)v5Oe@p;w88{87?~hLf`5aE+QnXIQ{y zk7ueL&uZQE^J2V&t&Nol6A6_r^(+;sAQ@E!oTp^iaa)+g4_l(S?`A5!5VwqKjHl0Zt8bCO}`X2b-88_bMS-U%wQGa9bUDsGYh>e+T z(DOO?fbWFP-|~^0qnRPpatx-LwTG9#?eq0wRkw$j+<#d>#lV$Q;3NrE)_*HloBf^jqOSD1vQwfi zD(}=_*b{?=!N_){QBdH7SYSNm>VH-d64%F?R?{s!66F!9Hc>b1CM`y)5?j7+?-hp+ zd+X*R2lAC+Utv(X_J69E+I3R>otVKid1+p!VemetuGw~(kbtMu7_?!6|D%ybi`fPuAH2PU9ao$*MBGSZw1IyiW&Ose<;5%L|g^T2a$Dq+rvFMiqJNx z3_t}-eu6Fg>B|fV2$ybf#PEEF#^iH9-%l!!kK2Iw1MS8n{25haYdjs^axml)?&_!+ z>~1=XD+IQ~+9y9Ab9_Z>k=QA+*hTdF#J5@z<5)OV3%8$ao4dC4{oxYNXn!wzEB|Tp z+OdC6T+p}1peRxLaV)gxn@Zf*$BwW?J%^)_*)&6V3oB0O4ly#KO1IEr*AcTGq^94Q z=Gfft?oWtKqmlPq4iX9ff=POiOi5%l00#fPwq)?$1$iG|5ZCxGalG7k6;F9-UqraHX1U z5!vcp*Ah{JsCbhM=b{_jbjmlMH3Z}wX0U6U!kjXy9~I@+UR=q8EGTH8`QyzF+Jb#H zrHy8%;tWgFOyB;}p?5!AuwZ<>w{pQ8sD}uUOFC4%QAvH4$bo>CDwmg%l1($%JzO=_ zPVcuPJxJCu+h1g_&K$=fAqO9I3Ij}fjk7A8+6z2*j!E0k5&Zs4rSwqh6GKCJRqN7Jwfs)-lVrui^@ao( z96{W}HJ&6JJ-`lMt&Kw*U?rE0q@i=sw!BONgnGvD`8<~&_#MTeR)hoF90r)Nj~SKn zjO^R{U$uskZ#Xg;hRx&M1P*+x{2dI0r3O<*7JL^b#-mv9twq8qTWQiAP)I(>4u1RmE4&cmCLhTP<5P`>oSG` zAidCgO~1piU3}f#p|N}6VGqT3fz0J|*SjAO354}XKyn)O$7Ekz(oR%2Pj2f~^tq5b z3q|}{_;dv|N%H9$HMF*`I#T<-L9K@|BN6kMT=3RHIbohb$K!3s*2wuh(*acoAXsLs z=HE!6s*m7!a{u)`;tHz9Ne`P8JGb3Djm8_N)*6}xdJzUjl7H`8<0~rDBNUTUzm|*) z+ww}!v8(bXvl9ZW_Ge=V4%dgoR>o?4ubzih?xIv?lxU3Eh=d@%?j%q#01;;ag{4>* z(1MaXg5&M>$CZ&NNSMu{ z&s~KDaF!1NBdG5UUdhBoAsGw|-w<9!qCEzL6}~+Qi%@k?cX>1svb4MeS}@KpwUIm$ z|J*@9X9OumQVkqv5C=JsJA|~u$4!2BL#@KYRXaFL)e9t-z7Mew7d#8Mc;O(q?Mje3ii_Gm402lz%va&gW;Ao=!@(;Yhc=Ee?$|qMY$&-s*9EOW zTFPx!SZx)3KIP6Xh`RQIQYNKC-&kw26~gnk-z2mT!gCVI##bm)Lgyxz4VU-8{8%Hz z^c>7)*>#!6D6GxZrW;lNPPAD`wXL3pSgY`?8@l#zmBxJ(M{ijU+ig$PWZKk(m!?RT z*R($q-&E={2}Qym3xzx7d&@Eh7V;Evfj3BBU{ekQmjum=P3J~s6cyTZS?_b7KywN7 zJ^?EMh^k+vojr~q+k?M%#CoLy4cMKcyNAzZY`n|v!#pcdi_0{Sv7EpN9j0&)6pd!` zBF58eU8-d`CMM>jLoV0d3T%F5EG-1Jdd+A2ZnVV z=KAyjj1ujFFuV{K_^0(3DZPvK%QY2o(Lcv5my;-SMInqsN9D%EdrqERfrLQEgiAZ1 zohew)cIm&{b+Ic+fkL?(i62AS(D*ooxUg^uF|*jxnv-O;k~pxUFGheP9`#?v%spKk zAsRR^8%hpX9iS!633q{O)k-hps5L2%mVaVOi&Mpii}I#9OzMrge_fG`AX#03Yhx%f zN?hQ; zJCbN3lI}S@@QVN@^v5$lwE{CYKsmB@Hnl!|!^0`W{=rl9;65n}nB-Ct`qtqRyj1f< zXkhRYt*lu3kz#N#$h_&i()l;x#fsl>!5`i`Ab_7rk9O^^54d7>!8xhV@q9*JQrf4tJ{8pD#1`)|q52lvKmMW&0J)Vf`e2CEA4gs9+Ri(Tem3V8mJc z3iRIYyseD)Npf+E{oNmnk}BaJpSHh<5HL-Xg+ZXjs@QWxYhrJId11?4BTF}SaG0fI zy!dt|+sa;juPKzxz}h)`mF{-V$mOd#Gpq4i=lSexHAt>uiyzFrI>nv`L3v{y+@2?x zhr|PS_U#@KmSt<5?ts~>}}bYzNMTf8g@eZqfmt`-4IZQNZRZc*<&Z!MS3`_zgO$08f@|M(s8 z!7G$-(Vb}u2E{feZ?{%qL0BZ83^u50mL4+Tv6<@azb==gy<`ByqYX3)1HDhy=wKHK~L*nW|be_if26s=dYJk91}T7#-eV|VjL|k=CFXxZQx3abR0Uz)16b_ z-pPs&4gGq`oYC9zKD!9ePxU|`1NYd;I0J@-f${-X`v?J7D?&3IK}GPJR}WL!6WCGq zr{55_3@NGf8^KAWhw#9 zz@(9F0*I*o)o{%87gH)629m$$_dRAB^SHyHv#4om?fd$G&bFnV_Eh|Ex8Rr2Sn;n9y3QeDKjeuTQn_(B|+1a$5d&``TSSamn;D*8I zYYswAoC`=VCV>vD^GoW}*p%m)jrAc+=(*-%=f1W5{bH`+UEc)D7(TASV^n*aHNcUn zal7DUcCBr32HkkkQ4tW0pxx9(AM;WEpm7>RHhM=Um&txvXm zziT@`>!OjB9H7aVJ;*AN6g91A%i}rk+@<*cq~ssN_#D$t zeLO_sUVQU_WDma0elCp>kP~n3ZxV{$PEdY_#t8mOD;qlsm9Ga7RJ8MphE~_FJ=5>U5g3xtZu%zA zW{{lCoHgnP5+Iq!08#(fRza4@IUm1({C0MkIV~4~NbFZ=KZfVJ?(h#pV&p;|A6f7}<+kF1xgId8rxj@@&nbp#ZaWIS-HCE61q zh<8hXi)x_KnE;#51N#ZOr5)h6S1ZtfVdYl7dG}meTH516vO!~EBbug<)j#NeNh-6a zhX^sm711uRBM~6k@=5$D&7~aR+VBpNZV(ET1IE9cpaz-x~Zp7a>8;c?R<==~)Y8+y}!;E~>slK`-QXu9pi%@%bHhqSDT> zNi#G#127kCcI@qui&9gc1+=P-4pL~Cz#hSaz}#oy3B1{7$#^oM`aO?pFaU^%{9XKUoYDJuCav@bzJ!_(`noCf#ffk7kV_Kc z)V=|@L2R(v^8l!|;03vseK?ZbcGsGgz5mfh#{tx+7N_1%D-_r0`rJTd8yXMiW=n#U z^WVJ3Txuom-uLRxl|0AU&54LSa{-@Rht&&;Un}3ZhnyVHflyR(vLo*QZ=2{jCs2QP z01M;|DJsR)SXa}8xqrfi|KTu(T;NOkR1gQ*UvfY~$%GyA_ zJ9Td(l_@~VF8-S(zZI<*cG_G4f!GhNrFK|BWPd?{JkS)vFeWWm$xr8z$4vd|-$hS= zwVJXBlL+6+2VG@N+9d!XZvU|dFF$saY)-{5Ai>ufgqTLZ6Vg-lv&oT2#%cWoqO3E9 z(g~aIKKaX{ID)U}%>tjhp@C)~`xH6$rnAz-} z?1_g5Np;l?yOqj!=?d^FE^}Af&SUg>Y-bO-2I_--0Cf}Jo#)}006sNo#O}z%+?Tn` z$4O|7BRCH^?9HR7nJ!bWX>;To=8M$}V45>^#6r7!a}86KL81YmDky|fpO*tMa|ZU$ z>^BKeR~Qdfx<&)+g6ttX=mBit=s0NzOz1QDFB>m;ZJxF1!oZTQ=RFoVpx?0Ou@w1sa@sPoN~PcCpQT&b+*`G1OsXg6OLoT3#EEzn|iE+NfD;4_?dM3VNMuBG{8Qq%nX!Q6FMYa;a~VZ4sY-n*i%{XPC1u&pMfJYeSG(CeEmS0es!8 z2{=2G(uLx-vaMyy;=xL`kOWxpBXNQD;V(<1Yy^R={|HPdtj5T161ey#a8DoX?OBiO zHhi-w zZOet^U@D4jU~mJ)=yG%SzLZ2&<8+2-(bR}Bl zdmKNfQ7^P(ABX1cbEo8k$g4XmRtz(MrtV-tIGoxae|E!x7VUDVnt3XhfmM+6=GesA zh~Gd@M1OIrdIdwWbnd5qo9)uhk{>rS9l+1C3IE-o%(@j+`sTkg=i{n}Ls-$N&`VVc zb@sn-l(+#FPv(1ukaxiR00cjD??dtaFV+%@Z}bopfOX4OBWa)+-qRMpT{^33$u&8E z#A=2ATqhz#0)(lKcG|YQDI8Xdao?wDQzSfs1~|Rn;_0l{Arxgha7hR--R_5An(tph z#yk+-~fy$w< zf)M8sHKYohX4%kzv;n>TB_$BPYgX$m%ABOUxt!;;+3K~fkm(dAoA8wBfLxO~U>TyD z3#FpBqy%7pAV3EIwZ6^*HddcBe?+h<<#hv~sW5kWy6S+=gIEJXNNByT$@n

mYnY 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

imTG&R;Z< zSicSnF9B!)s2csCxizBB z(7Sf6?^ySD7Zg|9K{iloxrx6MS$lVTmBT5Sg}W@Pl~b@0)J>#%>Swag(RADkW0Ltb zfh?ye>@{0f|FWz`?VwuA!Hq)7&Q+0Q zntI<=D#2}DD+sT9WD0(@w+~r-a0_{Fg2dzISiaz@YOwGQ^mWyY9(AY=#5(UycI{Uo zB6%9#R~M(65z&UDg&J0xFQ^RZ6S9j_+9ts*BVv z;X!^rR&4{z=%O4<3}3qBvtRqUJJTR7-p;fWqb`DU@f%3WO%320L1Xy+^|u*kMcgiT zAZI-axSXnX;xbmb%*y`g)#)#{azp4zrP;n)tAY(sqcuw0(b5vWnO^R@pjU((6GXyO zsh6*1(uD^JL`ev(LK4x*NP2ZR+lo%G`+?JKO~Lgx*aTYQ+!OWE&GJo2r9T3N|0@)g z;lSq7tvsv6+Os<)P|k1#?Ri6nBJ7PqiOFGM_i{cB!-p(cqsn}hO`-u8FSEmsjPJnT zj><70;3F__02EN~32db1+vD$CCxJI@pjODEUmQJ%mCcN)bOX z#!`?miT7<23{~{SBdj?bKRvp=ik^#8Lg(o1l3T`2{fkq*ITTIu^^s#KyyDWIPuLB* z?jGg#x3Em6dtdjsfV$RlhA++Gc;R9Nq+wYGSzgzw%2}!r48+)c7q@+GI_-J0Gpr_S zr+@H3X15oH-$-2DW(xJx8NcJ+tY&?EL*>Z#%}x|Th%RBeh)6H4Q(j9q(NibjhhBh3 zS;eS+CBE!^*#{-A*t?3@a#{fZFOErvAkM}#5`;*IjBk8gv&kz==H4-Z;HDinSWerG zt6>Oqqkj1ES7i$1^MD<`nqvuCW!VdKzv7eJ<=#;n^cve7O9#$yE9b#~Jb0-`aaKW9 zFZlC;si25msFZYWLU6Yr)q8lgX3ul5ny(9;P@Km?Xp@1jGAzR?IA27Oh%!iI%E%3o2jl&JD}2mqDA`@4wcQUt5D%ZbGux$6DPutk>;W`D zSWvVqDq$-T$WzIckP!OE4}iaUG?55?fzrYrNlsr&_OzL)FHGtYVK-SzOFstW#dh8$5a!dIYz|CfBg6Yg;@i$sQ1alDtOH0D z0bNkG4-qG8dCb2Ik1}dBy2nu}IIY!kTcCinUTX;=pAbtm(nj4?I2xGJ-HBy2rHvQg z`av+Z%C)lce5S(8k50Qx@Pq0fY5*9QQ>^#?>e`Mr6OoY#mAQ2Z9S2JK?Q3YpTtNBI z6rCB*w1-!M)3R1ZCEai&uoSXx&)Iw@xNkQ`xL9~dYa6obYtC(%h=e`@I5bPp3QJVK zd4M}jRv{qC3vA74116LTtHNCZ?RT9owiECRl(}DE@-;Fs`Jc1{)a>^xbO(-W^Nf-f z`Yk!V1i$Z#tsnnORC20AJC%MyU^IL8JlUVK3b7t=0~5`tU3Y+qHf%p^IUZLYg1cb9<$(ysXg|+GM-#8(4MQ)Q4 zt{%EMMq4_2Y>hep>=PplmoBh!{?hk`nX&~0+Z{$A^#H0a8<3~RqaSUy*et}$DeUB! zPzfNuf?h!NKKsJ+q;QbIL;-h!fz-3{P3RLk>3=$6JP2njCF`8w9Z>lF??+23&j+qo z`ah8=U&KiBUN_McEnMxqf1-(yi8%;p+I7&KvoQ-ncMZ0-YX0%=)3o68z3(6gz9$&3 z3##l3kTJ6cX*ze|IpPjK=1ad-`uE(DB3MaymL~U%I&AWT$$a?{e%kKLf?Aat&=V6r za)+Umg)-A+50KI`yskx9FN$C=*SZ=b!*1U=`npQJTt&_%Xv+C=Sjf!7hBbCCVjywS zf@cdfi5!E;B};Y=nJr1SMNdsBBXBs5^0MuM)p2STgq28Tid{&6>$*3~0ag!ST`u!I ze2(EEv&SBTnf4We*$eZ&`2tifdC&h0Q`)o5hKfBAiyzq**M4D{9rngkEqCU=1ruC- zA4dG+GD{{x9{F@C!_QRW&=dW(uAZXgxxK1IS)fvyBbu|)V1dx6COPI=E*ttmSH(o0 zCsUY&5zy%--1fL&L>%6V32p4|`x7&r*>3&Bs-XNH&%qU}A?NdHAPwfF`36836Ktf) zq@csxD2T>X#?NUyvhDl)I3xeWFGq9pghw?~HNqqHxc&@9sY~{vWG|4&^&59p24cux2OnIv3DWP zJkW9o|A?1h_w_0kZ0E^~5ZA9g0i~-LU_Q_!Is8h^5T1^@{!z+hBm(b?~!g&T<;-0Zc@)TY=*&sxbn2sGREWO75j4wTqVWVAc8)k^Dt zJ%Nn0Y(5*6-R|(}2mfSIGLK`-Ve}Bqb`5Ykfz~p!O5R)Gp>VsOuCQz1Jw42XC>Xj9 zHb3DoUR#2G<(>wHwu*5-BYFiy!!TCm>FR!L zxS_s1ucGk;e)(SR;d7x)Bxza);bt5>ygij}>9t@cm(XvoTK<+Jd~S;a@49%!_1-V+ zuM2e_F!K#arC*nZ{xKa{A@i%7XV}T+ zpoFDm(&eR##pP*d;=$}0#qa@$1-Hra2#^}h8(AP;ns!^y&aHGxHNyCOa^SvPU!ZXo zyPp;?BS_j%RmGLeP3e7c_!gtW^6J%Mwk50 z$G${UAmUM+_TMK705kuaD)9dj5b$4A!Mfi6evs2ZKtMx5KtOWDKm)GS?YG1NKZM=h z>$qt;esS|Ob+JVFXzJ!<=jdiEdeR=)lJ%%=M0g&f3k*$yJ1#+y4K&fy>dw zikl{3)f%`7rqc&qR|EvI^#8sP72QLU5D*a%6y>Bpdu1GEqSg^>kq$P7GH?=2ui$D- zudIAfZp?}N&Jc`+L=^1Cwn9gzp_K0yg*Clmc%uJu(Q%RLdop@N+V=2Le)Ts@)bq=D zhH&*(M!PXK78Vwf3djRc{`y>^;qrLve6@11HzB(?f4tJVtySc(A_CWy zkEh|RWB{KqwykQ)+0g>8Ywo<&yA+Qa^}CwYh6|?)_)E6M$b9%EJwa$b7>7IOG6U1r zPDajN1$iDDxJzuOpEgNiiqQ$$dYi1aWs8Y;b_G7e+f>qcM~;>nspDR5)*!rY`zacQ zDmKI4nQJX3vOe0*RWN7!UzbpcdZ;k4>o+>6Um;O%Yi(-%s29Etldu}kz$9nqGH4=L z?Wbp2ce5lKt+kr4INu)DXm(*{)|WziYuu6X_;@j~Vfc;+P0a6BZ6ukUom0OdA%>JY zd_+9&Zi`_mUm=Bz$80EHAwG#v)Z=jehbYSN8ielVWG(UaTa6EL%%3uZ;Cv3tP3MbW zE5q)dSUzRY$%f;oyX)o2VR~O5Hb36&WfO7geU3oP3wZKTn);CVCiZi&nlvUT0-yG~ z=c*#x2Chet^`L62$B`_2mbCC=dg1F9zq@ngLY1@`PxXBHWSwfW;irciW`||3^V0kW z)iy8Z9j!jNf9RZVbmS4E$6<=OiiLdRM&qgv`~N=V=TuGE#Hd#+Y~GbFJ8a)M^#vlf zd>*%MrFx<{bZb5UXK4BR2j}@gWv5cUeC&$E&784dscOb6?yPc-YU?S6e}DE~6*Ngd zbE)Oz^g+orfse_>XEpvA+ehNzkni7ob)uX=odL1rGi)~A*j@KQYkH;Udv?IRI{9e~ z3Fn(IY|@6u75FS{=l0C}JD=6361CjCUgvzy-j2>!U-QrRr}AeyDfw-gh7%PG^zox^ zTX?;j$q-#ocIP+ym4`A#8!drI{BaFFho3DucD_|GDE(Yg5b$&_Fu3Y>@ez1(Z8bpw z&v$fbLT3??{|F`%nW>QcAFdOV-fH@|9Y5Y-Z!gpq5wU#gBN%Z*$L2QAT*a(1>6RJJ zmV^vdAga{KKi_WQ@8ARm*Loa@?4(Y`1zS%&UfZ_s4kxnkx@>>AI>DE)-nzItUT$V& zR0x1&yweFIW*+&C%SE8W6V+}T3?siMXHbHiqQPzmjZWU?)yE4tn{}R_%u`=p^vBcw zPqfFIZ5QyH1NrW@&pweSR_LW;vmOKF${sIKkh`Qe+3 zn9o7E_SY;(vgOev)x%?XYZyN}xGbSuzp?ZNK1yF@`TI+~@=*xB1jrp@|2n7xXOG4i z7mg)LTAki$)*tPL(Pk$1&M*x{bOH3KyP^?@Q^hPX@1dKGYU{L6yoc)}%?mCP&m~cm zE(KbDj^8QVDaq{GN=pq#GKal{ToHi+^1jI6MysI&_3uoI41|bJ92=IO2R#Vf&c(dX zL;WINmQsMKv(EY7=dUSwiCo92GIrmJAGX}+L1haE*W1p{@KH`p94d=KS><9#9&v!Q zgWl}p{!cymO8aetx;3QYBvRufR1S?{)=xZ@-|2Ai@8sc?nS2xx zGqe4o+sJ9P!-`(xxNe(Q)IEoOLm5#1*tUJi9?|irPG2UZr}N%pr}wiZtN@A5Rr_U3 z<6?ciJNtJm4ry-lNaBw9xpWl3O2^X)l|6; zl3}C9{0u)UROBiQTMJ3BkY;^LtspCTOQOaSeRrXggVMj=68-Iqc{(QI19s z^E#D!iva`&o^B>0B4s9Z2m*M(xWiAEy7%OGwH+oF0=%Za)_yiXsYKp=HxKtd_*?Ge z{{yC7S-Qf&V}?>`z$1Y@QkdQU>eP?#%c$Cm!(v_9+V7A5HO`YDgJ4wL1Z0qvd4Jsx zbpl?Z8f+Qi`4bN&rm|S@N#~l}lkfB69Tz12IvVxu`qNGNBGpo>XXWF=Nhce$pf>qM zrw_>riKMhj#zbb1XO$iZACteKd1H{G#= zN{}bcOsqcx+K|&djyU=o1};B;c!(#!3;N0bHT8{$?NUSeJx$cRuI>PB7gg9@=h< z2>-3LHf9Jp_s}=%WQjG;^5v@_Ou({H_k0sgE`fie^iSqE>Son<0(LG-*?o;ZqgJpLouixvdlK7_HN1inasLSp@y(rvoo^G~gTNTdjW(>bEgcT?z4xIny z?!4u+aj*UIQHy#jQ^!8G-pUrudmS`3=ly>g?PAOT-p*0)ry^SdMz&LQ)pOjAb2tzpndV|JOo!6NeTU}q4 zcx&bldtkPhF40tQ|2V4rK87Tjg!AX`a9qla4qwMLS?e1@$9tsjHhC55j`#lVC#!~I z`c0A}JSJTT{P;Fi1+=b+S(G>3t(ZpQ2Raf=FMU^2;I1{uOZg%)oMNZfFT6$eITL8L zw^}rln0(*5u<4G41{^P)+Z@|G`|t&kHt^Q?9hN_g4YM-BJAokr&$UooEeG=GT%kmg zChqH@Rb-u*dP2zej$w>275Q{Uf%Zz{W$?lOX63Qe(foO>*|q4NrUrRjc8y(a-7ww5 z1Z9np(0jI2Yo@ik9u#Nh$KU(>Oi{<^7cYSRe78pnl=pZF$GVKrz2y%7)?2I!#OBWv zCtmV(#G(9s%Qk4>8?q%nsP29xnd>BY@(oXiX`8!1__{a_rNHNrADsF3I-S%fgq*=DO9>vGfNY;J!q{+vOMefE4xc}dEc5Crwdj-D!3&!4%- z!Oi8O_oRCxgyVI6xj!YF8Q*e~bbm!ztd$VC6kR<}zSQtku}bKIoWy0gxSQ(n+T_u> zUawLa6 zqD6Ds-4EyKEsXfyIKF$c7HWzn@3u^7Icwg~A-Kz?z4Zhn^~^0)7}e){#mQUELY;A| zXPO5YWlvG>^IsISS7fMZ)!_1In!Uy zaI9&I*@qIX^ApIOjfc9QXD&9pdJJNDuus6h|0P_}MJUJ_l3y^pN#u{J7bsa;cJlqv zuRE-uKAlX-@$HU_IR6X-YXPGj1#jopE7MwfWHY=juh}Y7Wd4qyW8w)Vu^uu>_)qvQ zZ?=h(@XgtKBJXS+&*WFn|HcV>pEC`AV{G+78+!5K7_2{~zJGm*4H^E0q4?`TVzqm^ zKzU|9hN)%`PUeU$&^qx(c_!msI}M}Ota#gzK; zj+Sot>h%!OHrl)|N}?kIF{SMXplNu)BB26IclT$5w6ia++e5hxSTQIi(t0UG<2n9UzFY?PC>M7e&bOx~2>s+VEF{Xdoh zop*NgYRs<%Jn%vZl@iwNqlnoHi8w$_g{s*`Sg%=gLIod5UIdX$$$C)4F4e57{K;eQ zZ_i$p;(-Y0Xe|4qlc}t?{p`+U1(x!qTR=#9$0bLR1UwXA;+!uCIsw<-c5Xt)|jh&SP8F-_@Sv|cZ_ z2a~xWe%HFjvmJ-%Z5{p(Y(>~z0vA}sCX-DtC^dk&e)PwX{{E$yNNwib6Br!zZ@M7n z{Cdez8R$&2zAY|0r4&_DY0hiUPa&wD#>IN|rMx3F!Y=cdLT?lUdDC}Ng?uiH?>P*b zBF7%2Mf*LxFy3PvC5w8}L{W?Wuq6c`r55B*(S+KOAV(LU*}0sk0;Q~f-kMa<({bw* zTzo7`T)|~;oO)v^g7^a+-uu`7b>0^w=Q}A4cf5Xx1EWA&EWL@U`N3rXkvy^u%jZfRYKXeikQN&QB ztrtt_n{hqX#czr<{;v&5Y+56`_g<0&4UpRn^s7oA8%&o=wDFHnxrS+KvG(mR8oRK>Ip z&aZQ5=)dORZ>A|Hvz1$rKyz<<#iAOl`hc!t?xNYI3N^Im2H^e=AjL+EAx{MZRFAl{ z5@K=zXDFA3WRE1=cphlEqx&cOo@8GK;%T`xi#{a-?IVIL*!Tx`M{EWHGfs3*>q!TD zv&)V~4fTM)BhX}fXQD!GLN81b#1g*^_7p=3NVS3FShtO0WV@QFZaq6zxDT0U8vow`gc}jFNFt{BwFuI#{tzbFTmpyVVBFDksTMmq@3wNpMURH6 za|CzCtql1$(2MlId+&E-ouCF;E*Wp0j-1DY3)6J~ER^bP2m1D!AR!y{hQ|$1X2eK9 z1wxw%=mSB<8KRy>!VvW}%|aFMO=Ak`qxf}L_;I-B^}izjndNl;uLAt|CT${4>p6!D zd^!N#MX&sWBN<5J#f*r6_#4b#oU9^~?qIgEWyzOEPJnsPIJvl(KxgmGL%RwmP$NqO z606*zjp6;OX7~@^n`7AdR%R|92yCv>lM!A`Q9}SHH5Y__D8|;#`DEJE-v(`NQOlKq zKLJb#xo&4bW|>jD((Fnk;o9O(E4JbPG_#&3b&rhr=iq>l4s{rq)7^N+Uv7mMV+D1b ze{?TFJ@|mF#g>!|na)vWuojDRr#uVt{3u*-vTnE`Hk4S|`5dk!gO5yiDPKojtFyV0 z%ihbgP;IWj?_h%0#{j;xnxTX)_eMbS7l{}1tFxdrqPrxIcw5-_*t_W~t)9vNV%PG= zrUf=A{^RpLUGHF0NgZ!2D(47F($I8f)#zI1bE)$ z@*6o1xuB9fI!9S?_^X%`>D{desBZ)DKd1Yb z$j;nkih5!xWr^vkg<*3wSp7dIPs0_6u0Gq~m#E?6BiC#&t85Lcj1=XkD-(IzpxKhJrq`DSTm3P8%i z;7TLvBu@RmIXL9g=+xpqGrI;!mDW>va2i^_Ns;}(<@y4HkcXqj9Duef^$dYLoO4u2 zQFhhujy(+9Jhb}V)r{Q$%%Jv~)o3dHeo=P7=LtKDKZPHqn_c%bI>~v=ntI(M@SZfN ze`iY?PXg?sU7TUN4}UR$lHgvzIA9;Q(%~=4st|VwC#yJH??z7F+8fW*n(I3QD8&V; z-ulz08cXGsvds0H3^pw#tvS^!F=rv88VgMKkB7yw=*g(Ho(qJGm+!_xQJvb8AiV@ie5xRZLIk~ zxwJ|xCV>KN>AC`tHr&N3Y14)7zXn+|vYC~4a{#c4_5@Hn0(0`Zr<8S!mz0p$qD&F@ z&x7YXzhPOvzkiI;PlT!ZI{^2aaowP%5pwD(wEHg^145bypAA)jETV=PG;2%(=nFVE zfZ{Bcjktz;H9D-=jbB}hhkDSS6>zVoUFrl~FBPlHrgwN;?2hG>YQc)v>KxZTjVTmo zi+UcxBg6t9)*s&4 zXuu{q{3Wq{0500*DSsRYBa-vgqowU!*$A1esQ@lnay3UGFbq7F13daAs))monvEu! ziC*8h$t~K&pXBZbf7J)4;>RYpO#T#}pcAv6R@fRt9?v4i90ly=a~AnVVyg!tXP*E% zx1_`I&u&N#FsGpsvwfZ{Y+o1um?^^itDSNww-tah;=ja?7HS7K49)yLWQru)9su3H zUbq8b(cB$+!*PQF>bLxU6o@PP0D$$IfD3wE|0z;kqUwF2_bpIP`D^MRFA1DMoNZ;( zH2_k@e7K;nChn0~$(fB1^qY?m*n@b$o?Tf_YUPd_Ba7-YF>goS#HRP@vizjSX}fN# zTy_`#;h?c+cShb?uGSq7%f_=8e!|k>lzCn|)C%o5=sI$3t<{s8H7|E>6Ts6#ZRnST z;zfDJln2TODq5wV@c@!D53UI?C5?Au&rc7@SwU+HjaR4Bnq?ODP!=D%TOdL{9#Lnyu*~|`mVG! zx6R7~8U#>EuKSt*H>hy~5wK)gG zTnj$|xef>4dm_LONPrFzJaS$ROJ-yE#GLmqtxpzTokWl-1VNa9m(UHT4$)WK0~lkZ zf$FB=!g3tr3Fj84J)&8I+?wL2Urb7xV)yb~W=7_*1CLmN-n>mc;*b4(e!Ne~R@HFY z@Uc%D74J4S<-+6b*_Lg`ln;Xhy(qb*9rR8Qt*e%tT*S>flBVb=?AnJ3ZuGmc-RJ7E zNy7hIqhGzbk6Gg7`!`_$Rx_@Xv}G0yZIt`0dfL^HFKvwbD_{I)Q(9(DDCP$4fO)w3 zU+ryY7B_GZFgrYJ$bhd@W51)5zb~~j5c>32==xAXXeV%x*yQMk?!`ccl$rzV2yXQq z*`U!7Gx(ZL)vjf|FX|QCGj-`HYKK?{H`aY%BOT&sw4gsOzn zF=(<^<|<7WXky}K-F+Jv^lI$|p!N#)@;JYSO$w}rlS({r_t$+S|DfId?>%BG0T%|Go9nBHuUT#HBSt?>T z=@V6AU3J_QS(qf*O775n$lgq1NqfTPZ?DpaZwv1AY9ZCeFKB4kzH}fduWo1a%yr=$ zJK3BLk1=qLfKjK%kskcNN$Stu4Z%AW0NeG2Gih3+M|EMNbYVVtwMCC-ip|auIuXNP7QozH~&e# z29aF-Tmjo#}_Bn5ryFs-Ch=J&YJ$i z6ME4gu2<;BoUH=P5>JJC^|k_Q@igLk{>J(8?nVA8+~L8}TpWWE#_o;T5TpDGM3#1P zCokEK-VEC$R5wU1?@xsNAXZAK-^K>PWSk1r3LGbO$uh(SLRu7cmC*#B7d-oArWo-@ z1pb()78;suGOw(>E8RP&{;R@*1gdJZot^HkqNHiz-E9`IUo38(F8%mlB+Zm)rNaSy zYa?z$jR$e6OUtDu0@>;>U}n~PUrHZ+uJS=+_XEVmaz+<1svJw&j6o7MFjDx~?p!R6uCkjQ@#oILZ_TmSy@*uC5xxAGL~e|N^= zp$y`&!EL}dP2UBGwF0>)qQbd;0aCl5OYIs;1N0>sUaRrLcjMwcmZldx5db_t?J0VfKya+u~2Byhx-WR(O@M|mT6FhyVWmNsFQ?dDT zJKE!3jGh#e12rd(iInOVaD_o`!;6V>xxWw*ZbO%O%oIQxG3z(j(XW}OPfi#7{(OrC z^HKcy&Wkbuc3Xd(*L(eVe}I;LQthtAVm{z2FZpgjy4T~Y9jxcC^wwX)(2A}fvsvQy z=1COYe;ptaA;cfbg3C?3UXB*)8y{C9Xs{$-Ukz^Lz9({8W#hcS!? zbIx(0Mr%mu@W2;iOk(q^F$fq=wIetW5lp4t0V5FjqEehJJ5Vb_83drMgl)a+93ywC zf%>nI>sGNzSjv@7@eXZZc^x>n7Bm1%8irxxQ>H%9EjM8Fp_Wl0&oQh z#5B9jb4<;Bcwk7MB+<{=(n3^rr~>Ab zIe^yPEw9p^MNt$0$N1|8lm;%+>~FF9nzH zewMIVtSiB451hoh_g^=Pgp@sW)=K;>nkce$ka;w-;<@RyJ#;1I6r`8$&}~j+R(=C0 zgGt{Q6&8k&5@3ij%DRZU6dUGKac#r+8|)%)y%OJitGRsw0NcSViv=-#Ij0d6Xd}C51VqOad=D+CHTjkaBfjDv+-nRp_ zc+?^y?I~HVgx526c1WqBrmv(h-j+|?!niaGl$ZhP=AF29BXiv!Hmy=2DusvN3G^ZN zf<1mV*GCVUv*P!y{`2lz%>W=7C}~H$p$3`c*oZ3VFwda&AZ$$NJK5V)f@>bgj)1Zn(DHe|jCumlE{kACpE4q$xKH;{>XKTlxBjcP z*4-HuX#O7QHB%q50%iZNVs!OPQRK5F0!;#ERm4!nKE4K|=oG$?FMva%>5bW*8Ao97 zx-IWDiAoPaB!TQ}%z%HlOG_~!KDTD++z<-a6$D(Z-J_Wrg#jMJy?CXkXJ zU~uKQGEN}5n0<}_AJeL!U*TGZ&bN6d?wq*gDfR`*J*sSZs_|w9HRTXQ@-!;^67C_L z#0QkmjZ2{>$eMSLHQ!Fx$PfH?E5$OG3^@!GVQvnNhv$7 zN%j*N!ui{LtdH6Ly9Q~48Cb}Oi_A%csvr?tS;`qr0QHEMqC*gOUOC5_CeiR(M2c>ZVL1ASI z+Q3anVNmLSpK7L9yrc+Jb zd5hs_xIja;bCxmW8F+hsubUZkHX~7)^3)c%ro$=3^QRZu)ooKxHdahY#}dQ0(nSLq z)_<-t4-Zp`PQ9);r-lpc!i}z7^E=^0*tcc72`M7}JipH-><-1edA-D%0-k#9JqW+E}9jokQe<^JliA$px0+I-Y1lnn2!Yz(s3SF_U8R!qp zIbpw#c#$zOG@OFT1j%Mv2Y++Ci|GzfNG$syfF) zW<|j#^EE;6u{pfc@@LS6iJzvLB@=?9d|O0@S8k&Fbimd@@;5B(K|Pc`t^WIa!BQBe z8osOaGv1rG=O|39@-^a2J+qMFt#z$S=FbmRq(hJDHx=N;v{;^J3f0VV>%TYe%{)~{ z3fII#k=VPtIt)5G(6w*Rv;2;*2fSC6gr=^@IIvSsN}c%Uq;eWm;WmXTy~Rb^Zz!~v z9vJX--rCAPKR?i>8}et>#Xz1h!5RrDQ^aZ+ZO#~>15_LM2|43;zE_{noRdKl`U~M2eeDlP}MN&W` zla1pvAj?o8!!#YUP$l~g>M+R>R-;13%);1xvqS*ogO*xE1d>n(~EW_>2IMZv0sosnOQXJ)A1 z!(=_Xn(T>r=;{u5ZKf?8>Iys!o(Ib89&S#wAr)N=1inwa9ILgvv4X)TF`<8+8Ahp) zItZAfJ&BwA?p*!>Sdq8Ev;Yab*&Tz`k4yu?xH#>2U|)_3m;$h2qJ;nb?at3M!!+srafMkWd)Nd=vbs^^>m}-jI1CG!%3P0!Wvlp82)W2(_q3v7z{N2v=Jd zF2#(tkzUz#i5BdKp7c)+-Sd~5Y{6NNFQLd6Bp8?gIfbQOW?bRpRrrR6H5vid(^LCj zd$g`n!U?8osFB48SDSD5^-Kfwn_@B8>i4MfUpngXo}Ioo4hyX}_{?^A)b|CpiSvbJ zX9_Ew*Lq1#l&ULff#TV}LLm6L1GCXekal$KP%T<#uy(HI+-m06m!+x%^h-|FOcDEL zrM{nZjmh?ovR${nai^rYvtQcDJs*w3=N|8u?|tT7!~r*(H!&zY`@9Hx!c8E6frl81 zJB^2U$rEaWi1B24#kKx9Bxqx^ADfFBX+F*EXpw(5C8PX19{wW_#Os2FF*pm!T+~88 zv=aFIg(~R+ABPY4ZFNrc&K*9TSNN@trV*MRhR9Yst_83ivi6t(BAp@mD?tE^WH=fJ z2+M`z+-U&*G^<9bE$X|icuD5Gyx(_#)vUEXvS)$c?6%L?G2IvdY?9}b2!_|IOjYgcgmM^+DfIAmC@|5~*84UL838T3pyieoW+`Zu}S z7aqfn27{R2V1OgcwImLC^a4Jm3yiUsP=~} zLc^OEn+c4sZu1{W2=EXcr8pey6ob^_5$gftS7GM*o$Fq7G|Mkl+QZy7m;NU$s$5L5 zKI@FtvQKmD+L!Rm(^Y1KeB3N#BVkW!Bp=itqD#G(j!+s~6}Uni)i=$eXDyyNu}m>~ zCaK}ujR?g;{e?@EV^umF`T_Qae?9Xd^OH`6`>9o4@jA(Xw^4^Vhw%|Vi7mtGB@Qry z!7>nsm=Sst(t-~9u}^R{5!e|$->pGmZd2&S~+`RQrf)7LL;As#I;#$gshU28D6385X^4GF%B*2sO zU~zC!_vwZm@ju@aVy!)sWsg~s2FJCHS@;;{tBxCMt$ef%^p%w$DzV+ND;V^u6qv{B zCBOc)V~&rd;MW6G&C(t;6TxKIeQyuBHtb9_`*}jXaOxgm0L^oxe2~COl$?^2E=n%|G_-EDS=i5%`>O-m ziWc4qen1wtyj0*Ksa2EpN%8f{mA!2d#M3{y^&;LbKTT3Jg@hbOP;XAc#B_5gza8+( zhDUyN1j$A;UcG_G5m)ZoZJ+@KQgfIDSt@DV;h+YXRXs$6%!0c}_{@>U zCJ_b*`X~n2EVMUJtB3&imOXtUDT+`=3?Ah6l)h;1889x-`jphl zWIzrtWnfM(W>%sGx`lFGR*bU9`DWJ$B2ak-;xU%yvs5QGS>cppd6#G)fjeLV17Pht zK-a{I@{Vf>XGA!`s&6UloPhW*?@&(bUra@JsUfhQ*Sfz6w!GtK2#2T6xz$E4tNro&F<+XD4`au*p?KE zAy67fLHV9_&NgE#Cw23lqh}J9H6~@K7Z!dFo){zxtzJ-y3kf*{O3tJex&SNUBh95_lnCy$sBB|BIurnS=x61gX4UtvQ&-o>D^uk=R{Z_U_oFn z(X9>tD5Lg(IHYYQ-QjmPTf|O3_yMTk$u~so^wmDUu`tSA_r|qgpT3!V3eozGfRs5q zqB~;`NSg(pG8b)sH~T--Dv`w!qXQZb5d=BJ42Z5|x*TmCy0jERulVXsT*dDM5rpoc z#CY;_bQ=o^^ZmB`Try6fvm=t1<^S1WsIfs+dO`CmKqpTCw21fn*QTgM75MC?)uTLnYLu5!CyjF#alV_QaGEf1lu$+HM(+ z;yybv+NJi=?^rxqJ8JYH7So;2y<#HPlU*1*3SPmLqQeXnAAh1y#^;g=X%_Q1nAX!m z%R6SW+j(#|GIP{ErkbowRJ#ziKGMzU4u5%;8gvaa?S}&DTC0jok!8!vbq@&eLiSkhhesDfojE7Z@5^X2l|_PyVP%CWFiM zM3MzpR?hbRjY|oQp5Ya#q~)!alNSqy%E18#d{IqS=b6>hU34c2dKg-QG$b^sei~Z5LC2*nc-mUf0e8`$mT{m z>RkVI&f9XQJd!REF6_BKnOlfWjc8E}FpR9}fmxM|E{1>*sVWTr_G&T>4@aSY&KSbn z?t6n*U(+<5l3wNp{MKW6ihqo*3M&S1LRm8>B5Ar@X zxvfT0JU~M#BBEYMS@V)~6x%~a$J2=I_+Mm##XHbCs*F$%0uG|}={h%6!{t9Kd2a(u zvzF2OyRj}ToOkEU%0GtIyF;kWHcazE7(x+g>6H@PPIsEAXJ4wZHbe0BM9ZxEnmB|Y844{G1THox!PW@xAVBz_M^eV#wJfj=D}654qbtW@HK#H)H>k{)vt-foHXNRR;P?Pd*ppCMejTM5CVozr?>4 za?Nr3qMGf~=pxHrr4xj;^T+7z3_blMchzDY;Ml{Hhs;oKelRzia(3fq6$FDk;WIqj zq7MSr*Af~2Cs;}3N2EJc{I*FNR7VdiNk^)ZNBQ@!H-E@)U!R>*i~f}@`jk0C;&J(^ zOszKCX(KJ%^zR>HauESu4|=J8jSw(dL+&A^wm2MO{+>~6+R1Jnd(er9ZkGn<2C#W* zJ1_9x-(|$A4@j7-?fR3ej_sm)?$KO0UzdzEU7roMYqd&wA!2~!L0NgR45TPvJaSaX zf4bSC9&MiNOE6UoI27Fk^ebDJwS{dYvR}BWeks%`u`dIZc| zo@cWq2f!-*`J97MZCb4pFtvg$XT}8rCD0J5&3REwePxjcLVy@zH|2NT9ev?0vf3(^ z+{5r(O^I`~Syud#@v`zSf8gN2`OoZ=aR;C0AoT>CaElN8+muUg)Y&&5YoJ6i@y%Ov zhWo|I++MqZ!W5S*-wFE{Dl*?mtBqgp=aC));xh0#_1@v@wijzlwuXp4&0tuNgDi#! z(mk=8tpI_Q*Lso;P@v=beSNm;q4-tyE9uRXBMC_Pw*|_i$sy!df!-@xTm*fJgC~d< z3}r2Km4am;`@dyrKB{C^Pt9XX#yKkOzL31^fFt6n0V1jKKs_wp^3_ybk#EG;MtWAXl(fob{2h1FNJ<46D7Rv{Q%4( zv&~L#-Z`$0N^=JXVQ;Aa;Lt6j`h>wq_QO28vPB>8JIt`)gnhw#CG_)ZoSsR2HfKT_ zvItmvzaP<>$w?#P_Iep)EijgN`^7?J6AUxu*#_cZo?inwfW98W>uCu|M9z4_l zY#k)yy4L%DARoncGNPw_-4msEl{}e{>{lXbyvID~!=HV_h?L-5rGxDUEUzT@j$sU7 zABu%n#26-j@q+Eil7^a63@C8WOkFw5ln`uAYf{tIx$P`Bhj_l>G;A4_E_}bwLFPYK zuD|D$_283wu-0Vjx^D$MZEzgdrRkt>PEe`Uwu9@B^?4lQZBFCE)GUKu?X;Op&m3vP zyd&9(?qw{i_V(C3U}?)1uUE(>qtd@b*>l_7(}J+JC5}eCB|02Y1h9H}yZ~Bf(yMXt zAs-!_$Qp9*%b}GUyOp@BCYzZQ5a~jrRr?=QkzbN$`gn-q`ztM$`c9scOaG2n>sMs* zSa|!8Jp!J^BQEF-JdT%j$qoOby39xi;;0qMr@NWLID2%OqtGxr^z{r){PFHW1u z1I^fCvyXO$O^pd8MiYeJMr|;eIqX$YBf4?VfR>GdlWf@ z6i2oL(leb7_GI_Hruq(;KI*SX!#P;Rvi+tN%5~~UF*el0zM+OZ{ovG3QlJjcdBqYJ zQ3ib<5s}-2W2e}o#BPV*-)ZV6jXYgQ~h19-;}#b_X^!J$Qj)ob*IGb-*h3C{oVPF)*6CZ@K~T4 z;`;)H_+#5J?1nE}nY~gd8%0L#_aix2sPp&a$1PXwt^U|BtEo!*eW^4l{kT^w4TozG zski4|m8S{$nLs_u9V~meq1Glex4XYy`liufM^3DVf`+m+g3d%OpWegtb%%}*S&0;y zRNM$0NwIw*E9D|E7+T4Ky*32_2Lh%r{tgY_momMNHGpk`KWkI0uW_(1UJNTWl<}H~wtK&ZufIv*QattyFF&Kng-B1wWOOg(O7PNDm1)nRN2* zZ@xq-+Cdvnj(jnWBsP_eCr|KYie}pi4i*<2jsDi(-6k+Ae<5BXS6oy|;VM|9YZ#JM z4EjcGP6KxVHXsF_#z?)TQkF_pt(kAz>LVW~`>*s^CzFOp_bnkiJ z=S#)$nq+ht#FjSzl&spG>2iNtlryQ!TNIyB=G#tx2RB!9{Vpge+souu#Zs98E_KS z?!SqRt#0JK3T}Q_zP=y<3(5$0lLg5^)b4Y0=<7Ct)q{L6Y%uDT0x*qkIG0&^-j@)P zSyVsZDhYyr?@hc^^hFbE9$|Hriq$ca{WLwSuiQ%)Y)?76g4>W4i)Uh&2g=|wt}Zus zqpgd$!&}^qfT=0@73l!uXkug|^pwk*cE2dK2-qwjZuP)WkOLLvo&XDG*8Jh7%W|u9 zLp<{x@xlbI3!`bZb`7Nm0$azvb&fY11x5QV>E$w^e~9D!>Z$>o$lH)a-AGjELaM$R z7xZ5$Uv(+z1tGM;>m<@U?yMW*o_Hiy0;joDEjJiWgF)?iyzD(2p?mr2VW0+?&aS3tZ|bY4*>7a& zz!nc|R|Zxii`RqK1UReT`lHfwlX4rGxpGriygC=Hz}Y#CGiljGnV*q+&%p+|i;w zsej8e4ot_FsAL}KQT`;oO|+cK3xhYWH-*>Y+2h;O+`oplXKlu`?Zi-P=I^m+yOeVC zmWZB6Au9;)G97|~Ya4Z{^XOekEvPw|dQxNV!;!m;-T%e49V@XogXe&>R7ZmdVF5d| z#zzG2HxGg3$r1@%Z<3hl$s)DN)%LxV9p$X$YqA0CeM_g;e=7`?fyKu>qS!{qH6x&y z{|X54XtUKit!M>IW{RP%7+qVrjjtCd(N?4d<=8EyJH^<`D1c4i>{4kG*gKvk^Mr`a zz}p0CD9~!&g`Bwm=|gZDgvn_|4z{`$znOX) z81dpboT*JyB~|c3z{xxfnErmxXXvF4lJK!cd8L5tq`G<^LMJ0ds@6tgqA*}RIXW8w zYM<#;HW#W5sg`W2u}J45Os9~6)#6oQ6gBNrDuzdN6dFFzP_F%k5wlWirIb`=xk0m% zw+m0&I{pcDFk*wKxeTz18PUt9(~hrwGE7Zp@bvs3!2eHnA@J;T=WoCk4{NZi^m^ZS zZEA@yC)M0_8+Nq)pX+xkHTln&At+AT6u4vb_D(lWIy`uW`Uys~n=dN3^MXi%(< zm<%yzITv3>+PL-Fir-mQr_UClOrWP8x-U?cJ%=TJoc3{ZHLmCw6Mxq=-hN>eRefd( zFi;jQGNCxAEMt%dd7~(?nAzujq%g02xTleU#osQHE~hm@FOp^*p?DOv&7>Fdz`sk* z3r!#<+nQWc8zh(nDRhA>1RA}#@!MV}t$ahDWZ4}}V+SbB&#ka^s$f%8e~MNF6E`{P z^dR2~oobDG>`;pk2IOAM-H&DSfM%bpH#OIIV&yO_pDV6Fb})bg)tx7EcG1RuFD&URJ54P=hF@nMhA4 z*(E@yXbj_SH1)RSc(%^p(0eAcY7Zbl5ck3yE}Uk}+MG!1kjZ*L9bHqAR4QcV3EQvC ziSD^9jw5kau^W0A+i(Z8%UTrXL3K9b0o@7KDrPoGux0=FFGs>+m#+It3^&g1=Syo8u?mP|&&>Cpz((eaOA0rAQ6M@ybnP~F(C8@kol2m=Dk=mW2XV5U@3 zUZ_KhUoLb3-LC`gV9obID!Lhe;$|=VA;}qU>kZXM(6xzQXzwu15GBZ`OD*1;z%}R7 zb?hDO({R(PS(-z<(0s%VvFhnp=Ry{|=HoO7$j_c@vFhyI)wgKpV#m2VI_ACuY#;Mw z5hfyLSDcP}tC6iFcbhDRzrz~PSm@&xeq)=`vwQ19o>Nm0pa;cYFBLDzhxXn_zgEBK zSnwmuGT9@ck_F5u8ob<#%E2g(Fs8X7P9(#p6yso5jP;d!<%vOGU>iIBXXi#}@GAst z7|2TYdXUZTu&DuSf$~R7jnTeFv6`tRBxoF|)fhfwGZXiEXyU%zj9+vB;|Rso>e$07 zHwPwg11y`>hv16P?-C4|gEsR{OsdJK&JuFlajQ4W+jd~oe{K&e+z5;~qPql5yy<0~qXhFx~z>;8-+e6y9R_8>~@h%~Rj%zSOXmEEaQy zjrZn~lH(^R{bf1LbE?zL2*36`_Y(l=OQ|Be-Cg7~3Y5yX5pj3T<;(*^NWZZ>4u6%- zO~d>_AQ{Z5HvcnRkt`%iGSPgetJrMt_a8xfF{?hcVqDXF0Z>F#bM91s*FBt+@%ZV>5^?jGrG z&gOgH_gUvTf5BPjd46#%T#I35&;IOv#ryrbj4nSbrM^v;lPK#z&M>l_1ByQYc!+>G znjI{P(>rxx(ucyN%pdOktlflxr*{<-Vz`6=45w|XL4pS%!XP;d#f%x-xRX7LP zFoe!z$1mXh@<62ml1NCe5D5zY(D+8cgdDNj5G+ZpKZnwC5f8o=q>KOB`-!&DHvzib zCM~e?kHO%ziA)Dl*C2ehy6|JC#dq^)m=uE3M-UoVq(p?m`oh&<+jiM*GD_>nPKNQS z$u9JmHm<%`BSSXQ7?{*B#&zHxd-SBM)H_>fNnm&FDy;r{Y(+SYy4jP5ZqpIsr$2-> zAVv+yeWEekxND1JStrNA-6o(br4si!$2wpjASxL_kU_$<{LP9T!EiHX~xFTEcf7dYI8X$?IqKt8*_<;8kP%K!$SsuSN{9TmF=V&!-zQOIK z+IIV053D{(QY%2*`MPS`r^0q-40Zq{?QDRs00ZwZ|C;;e zWjsT@c^}LOxUrsrN+)mgIj|=G03NB=Wl%)rubjn?u%g)-yTTTaZh)d#FDz|I;I&YB z*%L$lpE^A7wWM&b_yBuu)Q<4kzbPFHeGpG$szYxl&VWHA$MHEfW&jzdLEfxP1m0ff z?IKW=SHhQof;;|j85Fi3J-19+1&MfON{>h-pL5#+U%(9E7N_OyZg8{7m5<(hWGwv$ zn*`_xprg$I8tUauTvSQo-ls4Id91>lsr)zkpb!YJDyp0M_5D0vUGnbj?bRZ<$Qetm zDJKuCK?{9FyQUB4ni>A&L|@Jf^p>oL2Gd2qe%nfY@qubA0!dSwVQ8*wlD6kQTRvLl zF?;~b7yN_Oe$XG%st3Zy?NKk7S-SYsdpbVLKVP(f2g%F@C9VKR;;Jkpr!27T`8;vuSw%mDOC2gHO4D8DSkKM}ByaCENP; zv96HtM;D)OU?~&f&F+t_%4B77A;Dt3)BkzfL&Em^#D6$2q<+60z7ucv=%+ra7+8C zpQ;&PlC)f!mZeDsEGo1W8n*^@#Zu~kz7V@+&NuuNE+et8Ky=5x!3;uOyn~g(((vDP zPHRknV)VMn6LW(gFyRQWH2Q!|F;}m0lNS`QqsRu2u0dP-wZe${MaU@|?9I{5CLlP~pQ&h@kan1>Vswp_s&M zIj{Q@^C#ecjQ#bv9uYpELHGxVw5#I9p|HR#B(-h|eruIr6wJgoS)e}0YtjEU-=MxC zx2+K{yYw0+B>O$ngxU`%&6W#i6(sNLYOhs6Z?i6uotOK=7*oB^MI0!CcVwmiyT-H8 z`%MabXr&WHda{GYK!vNdG2iInpd^e8cHicwGJ_MLnt$`^QfV(uV zc7AzPfT)@|B(>butSr55=Lm4U@wPjx+{q@L)u&Sw0{;b|8q?s-U;2i!~W%eEk7 z>LGEDeOrx3QzB^I(^i3x6|n|ROe~epEN)=;qK4Qnwb;27z5F@^Z-~I7#=>#{589JN zF5|^RPP8bgH52eB!1)K9q8J4L*V6C++?$=Z_sl9J7Y($&N8M1Zeb5|5TWInc--O&R zffK~dw?FZ>KyS;-(Tk)@X%!qbuNK|c(9lSDC-y40&yQM77A#p{mSqFH;6PL`E~@@b{c*c z0PC<~NF}2LIoTd%%$3Xq`*C~y_h;#N#BiKrKJPGx)9w2O-z=S}og}3$>!g=yfggzX z|GpeZ=9y%(Q;(wH_w+1#M@l=>_QfoafLyoxA1AO_CW6krg7bPGJz$x+=uX}&c;rlC zT^+Q3r+cK{3Eu;553nsTG6Ufe+dP6cIeB|OQ_;7bg&Sf3SPSlK23vp)^o+lNebzsb zU$cUB>ccnZM(`IX@lzhzSoI1b_p)+Iiv)OdsJ+^uyQ> z@X+W+kWO4afC?wG(`py5X%|NIpSHSSBKC5i#_NSd`WSNvo3?kE4(r0kb+NOxp(2H(kpSl9_|=965?zBMa;!k_%;xl4k} zby~OkVeU*m!)40|f_!~Jo2hMFYTP=satVC1KVGE@sPxy9 z1i~r!JV1&?N%r>l_5E@B>t5?3hn%&V`PjdG309jN5CTI(c^sOK!m)TFV0~b}e(nRY zBq5~hrws3#7edVzc={Aa0aTqe{ZgAfvHuEa2tKaI3Yvya;G@VvG!47`Vg_VLJLIHB zr5e5N&X#LVfPWG8<4YIW1a=KGs9U`YTX*`p%uOTyb^(v_JgM`qc2}glWVf~jYoPW= z{6z9o8(aLp%MTbcK__L^vmb#sj19d998L?Ig7iUHB)5Rsm#2YblJXM4j2Gu@Q3wSX zh4MS7&vs|Vc_)Zu*xI%~9W6joDTZW1c`609e<;QCuGgW_56@QHu#@w>PXGmf9|JX+ zfjaNA0_&T1EQqj|utZ#Es`ctfG;@bU^5a}QFCua8vfEe*w3HwMW2s1T$K!S56~jaf zOq^A)Ei^@NW8eya6UhibV2Qw^p%jFKnr?*d><6CwiPiw%0fZe8N*sc7-P&IcLxP=- zD-eVb!a^v^Wo_A1>>y}iI$CSZ?YKAHbwuq*GHvSeb)aJBahC}X>|9L$*Tg0j(My|w zxl&c77V7kbo`f+w-3+va;gui8->(>w$S4VE=D)TeWuX!7Wsog_h@f;}FtNz2;&g3v zceMauA?at5-_ef_-vpFwO>$yF^%Pz`Wt{4c)@6sRsgiV9oD+41NNc7B9| zuruKigM&vaVVj~sG8}2y$+s#}C(*A{Zi>o(olAp&NegI zc(AEsA>{`|)xB9XFY;YZInIz;bMc^8*%j2 zzl@rHZ?8_ebu_dg`3cQ^Kdr;Ci3FPs&;PJoJfcqO_TkL(eJ<|{Hw5h5or9L0XEHR+j;)ix0h=M zlPoabK}toW{xiTkc-z5sYH^1*N@K|UCL$|0;K9FOVfijgT{_EovrV?~a{aUVgl0WQiMWm&Q#B2P^SOXGyEN;L(WG$FON3_~nB?9DJnT0fOn64#hr54d3e?rw ze`&hj`XG^?L9J}a<4(^*+oQ)c9CB=ybcpXqo$0`D^Y`tU*{Rd1TTJY^=*t+C(#+Jt zV-4uMY&v5W|03;T7^#LBj|06X?Iu0bwH(8%u35LgL#Mal^;mu+2iU*M;-SMv`GTFF zg;kgEd5$#Ink>a=dlb9Id*R|fPzZh;A=Qt?YVk+hb9UW)8bP<(Eb?sWI z3PN^LRqAGg*26VG3{&R*iP$HR8~`PmJT4Z8iSta^)MF0^I{isG4y#*%CDA?EzW8VZ zZeA6!l}Af9uk5*Qonz27-bKW1Y=abDTqFxS`R8WSjE=J-pR`|dq)$TS#LA+ltPRpY z)WRPQ#Z!3bghXAxQ|kfzx*Pu z+W_W3l?1-MaNh`D5rC-p4A={z;yV9ofQAI~3QrW5E@$h{*QE~=& zSY$nMljUK8(jAPk0&D1!CMT#n%-F7(E>n($9bbT&se+M{D;BTHo(L zrzb8b=2|{<-_3LcdIVy+VdUI2vR`>9)y0dSTDED1P$PkEX`1~x)M*;{!Qb9Vp-Ek3 z)^-j-`Om!z3!%OdF@g11BJmmejsb?fWr(NnNxm*4WG{;Pjr!Ipk#2?jg0%G_+`RyH+b3LwN(CpKg5kyK1Q0`2?@_db-FtWd8A*#?&l&mfb_ z{BgCiiB!-iey24lpviek-3RSS?>^6L8U2(=Y(^!skexOXwfEWD$Mk{^c&+yop^vHN zhQxk^b2e@Dhm#->i^v*|)5KsZPy+&up$eDO49|V0dba%lQADRnw4n@4G5+6?R>X~d zCq{EOZ8~zg_0g(4IKJLj+tK3U7Jkkmk@+xMh?~i3V!@M;0$8apfo7V;4L%WH^BsSV1 zo3S6tN|z{hfoG@W+&pcf5#DWZY}R01<=6x8rV3HmaM6$;qK>g3`cr8S4r4OusNDcx z7_a*-#9}&KG9c)i*lw+jDUr=gMegq0#jkhr!6VNhiOtGHsEliZOTN#+4VVP7t-Rt& zC5$VeF4ekbD;)C-|DExUjtGeUs8B^=*nQEI_6{dPRfB zt+4p>Gtn*aq%Xdc7sR~P$4K-%1X#URZL*Xy`tMqN>p?(jSRCFh8hV0GJULq}S-5V9 zQ5WRcMKnD1F%H;h7*P^q}SpGgzXASYdUTn@MMpi)$ zZ3nd?<#+~pK#U$M7y!E2cw9>VcmS{#4wVgp;RlgjNEv2+GH!6ytF8tYYrzjWZUpl7 zX(S7*JCZey*{4L!vdbO1M<>HJ=D^Y_;8u8pkHU{3!p&l~0c7m2lHv)5W0?X$&Jzzp z_up$>UzL30%Bk0=X6=C=X0pbEHBc!7-2}IA5qvf4FbIX&I!KL@J_{K(xWc_wdt!fG z<94W+7|1G--~8~!uLKw6@-)Ut_5(wx#Mwbb(cnFW>^q5miU zP#(lJ0u<}S$@?_Z?%*3?II5j6$4EcPBJ%8u4C1q3C-OD&nhY~49v6;|m5`HN;xBU{ zWfll_Dj^=&jJu!=WkyVdc!zHG70*n-WfOGz_ z3DvZb8thEtF=c*8owqf1`r^tjunDtc`oLb0O%J!u!XhRQxJ6{S@-=h9;h(wT{7=ae#QkD^~ix5pFB#b7^w=IoZE|y(0wokQymoCKIOg@m0@%yjMWHPSk0Z2Hx z=~iG`^s#t=eWM26IohtNT;pRCb(TKX$sM5f;65>>pcY!IH?nLmUXR>kF^}((a98qj z8q0dJ;2Fr)UpJ~8QfX$(Y576i(ZqjIdPLu37eZ-v7MoMq6hTpHf^Wsp@2LBU^ZvZ> z21kfAIWMq;cPH&3yRatMcKRcD4MWtX^~vW+_lK0P>j$n26y|bbT94$>T;z4(8rL%# z@1H-}>`!|694eFGT7@oTGh!LPPPInVfy-z>P8A0f02h4vu?>YH?rMIJj*pj`wd(!HEh#O<-N=N&9THl|Gg zir5m87Jft^A~cJBz0dy2^C?ecOfqxgH$Ka_>lW)tHnur%i1GR|*& z@gUq%%oAnm`=RDhK)jCK;J6Yz$m9FzWbEgwoc<4Cg2S0$c_*MJr2Ua+eGm^(BR zOo#oDlLNma5-}hr-K6L^&SY& zg!hQ^%uY3%LO>Rd>V-8+b&XVFvhqHuG)>XGsLCRLp5_$88 zJKKa@Bkf202s*crUd;u`Sw4!-`-9okBU&@wp~pK4iDYwG54D1Hv}++w#Tr znGK*{-RBZ{-w-oxPr6_(S^(@=FpGKy)eF!eNP0oIXV7YPa~|YW3Z7s41@r?nmlU+& z;B%vbx5|^++&TbVrk{jrCI*~JzpSxEqPX|HHu_0FeJ*U|!=G*SaJVkuI!@%Ws{)l= zxK9=BARvm_^#`vH_tfw8#8O(%cb0=fz7SJb?_<5f1(tgF0C+BJGhL}Dy^BNRE&T;0 zdHS_+XtbsX*cA+VK}wJOQUz#*nx?>QimFv=^eSn_ z|JDs%=h#9nAqfT}pX$;LJ_-vJ2SDxclAwA_zu))N!Qc`sxcFqgt1vceJE*Bqz+?&upe&@=Irx3uh$Cnf;y>nO5QY0-!JdDlisv^asgZ2 z$hcX+ghp?k|LUH#hYZ6XRmcjV=rtV(g4i5UZqq9Krwuf99Ex_W1-37hE2+mGRo&Cm z9|rZ#Xw)LEeJhXz!nIqF*n4ZK?{nMFPd|JO$mrzhI}$=`o)>z*wsZ}S>#LVKK{VQc z*qd69>a7Vd+gcTGwyR;{F%=y9NB1|cCawVO=O`7oha4QGFpopgoBA=1l=>sVmOc=x zzkH0<4~;Z+&hHd!xlR#*d`T&@>K;S>=EuLVTfRn)~xF^VgY~wzN8Uy^_WMkJQf}wGU2j12E zz)%G2QL+1aq3Iy8;6xEvabvy<66w7H&zA>5;!&;7MG2k5 zs0{Tz8ZPgX-4PZ?U(gK`_zYno!C6*N47-zx3&Qyv(ac8=U{DaTthaOOfeM*1X1syz zgI3&uQg-h-1SSgJ6W-W7azn9_;?s$KY#vLpo zmFkZEoYkh&1B6le4+nF#!yENrcWSnVt^Fxbx3Wa&uL4z2I){;ikwvQyFU;G#kY(eJ z#CI+t)N@{ENmE;QCKIX-SlPD6e}hT@q>yipD3184eMzsc>}%fn6@ z_&YGiqN0g5)cf}R;qtIujJ=hyXRkKa6BLzGav$cYxg%rtSw_XU7vkt^!X)ZG(nht2 zImnz1JrR$ zRtYe;^pvL#9<@P7E<_K3*Ib4TiTPPn;i?cTZv0F?DTfNnlg(Ff3T}yaLGn_UxG~{9 zEj%V2C@*uq3RJa8?Q%r~7D~ocYu{mpKfAeZ7pt>hn1yxIa3{}-{`|^iY;S#e_N0#1 z`~xJ?k+yI3+(JrxEt${kYqNbYD)YT7#9j$I?dHMI<~zZCMB%wJji z1Lo`;s}Rt&%r{!=rw{BV7HYAb;T>OtJ=GvPWzQMfeGHV=9oea+hoj#+OSm%j#G7I*3my<(fT3Cn{O z&`vq!?3yR;EXR~(1E@Y+5L{JN`N$kt?u(oOc*li!I+hZ<@sZXzT{zWsPY+{|fpgsWc#k3b{Ug5jA8SkghS8 z4NT531~WpsAv!BP$u&JU`33Tfsg4*y60dBc_jO10~@}?UM7okk39T znpraP)pkT#dSyu?b?Jns@U!?%4+>jxkPDV#J%sQ!P$E;#jT79X&nYbcO7@0zBV$L4 z&|L5*OGiQbtVNk$lHbL#;}Ca-u1cGK4)d2%+7jC>Hl-AN27S1Kz2L=6z(JhXj$xi2 zv!XrFI+?Nbv#UX~)`-B!huS`f)Ckn*u2Jiq88*3uKEsM@g`|m1O2iw2{-xQaeo7EGZdWao>NkkK5b`H`w3y*hO zwM}ypt%+^97R2P-d2=AQrStqv)dmosg)u#=`fiDadu`C&*d?HE8;vSL#?fJ0kGznUN+xv_0Px3*1JB|hF3zfOr!^G}(#mz2gg1-~8x zg9%a$W;0S5^aC=3X7BEwgE_@(Mk2PK#iwhQ@-!W9=gPkQl6|9n77ASBqM;~b6%^cY z#M>bNuo%(b9pdP$Z9e!-E9|H2T*bE>hUlVDci9g7(5lg_oq-W#|DjfDP61TPgU{BR{Dpze+V#Ip3NOe$D5jmV z?1#H`?v8aBb4^AW>s&`Hl4Koabf;(?MszVYYdx>1Iy$$nrqjLjzAmZYosjlP3?0W@ z&2kb1_v}))rcIU1`r_B)WO)*F;M&a#-J1+_d>4f7_B6$oebc@}GEt@f$`B_8@BZxV zjGoTdt_o| zOR4RW@QXK@z2jX(#UvlKi$WHybgy)}{*HiWZQs*Ki4U}|(O1HD=TZqnSrwH`rVk1J zYX8yyFg^>vfeD`0IX(D0N_-aeu=laOsO%W`4<#taun9Zt#vrvom9dnq>9{G!PXeyO zw^yst^go4}+~&#?Jb1|Hehn1jQG+)RSdz~_f62~5?h3d->H3EpsTeM)L-#&eb5th6 zLs6q0n#lZ8`_Eb(T1jq_{hiqC@~_WN!h86HWQ}itG~sQ)P+O+yC!YNZE+|5U3eC^3 z=Vlem=`YYj>iDWMw>SttwF-Ih3T5FYNO`?x=?due30zwn6de2~g{`D9o25#RmfhS= zdUTo=&uQpQDoKeH+j#KmY5XaZ0-tGLBj1d)l+%z42HAK%Eo z5X|B2(fASljvX{jYz#S~ephV%hzgkb&N_a4?QZ8^jEcw6|5y+!` z2_!v9ZK$0;o5xJ$OALn0n&9qd2d(?e`4h^S25015c8a$0mHsi)--lxUY8aR-^Rp=j zJWz}6)6*xBo-1gst4jq)71>z+u0G?S>ucGCxoA=2U(s z25p;_gvX_(@nKx^T&~qW3IbsEbvp-dX7K8fV{tQu$rg);w0FGs97- zq!f{vx{wEF-Mn^4A`56iJv`j$VcL~piPovMUsN%C7=yNh8*+JRsm@83{09(5+;g;V zp&3&alE$cTvgr5?d}LA%4tvF9=&o})Ld=Th;?c&Y&r}=Kv2WWg*k&GC(KfQD`a#C0 zr=N)x$Gc*jINymhf>dZqWu>j(c!Duz<1mg<`V25uxiW&gB-;;0u0+d_h7ShXOeP)T z+pyTH46>Rpuc_(j8F83(H4(#53O|;0pl(PdFtVe6yDPx*aU~LM((#ZYpWo?4aKr9o z4}HF-o63=L!n@-vjXA!RA@0-jlgnFMd+Go()m>mvC0@nVEYp$HVfokJm;9kzKiBh! zhz;?H_L($dH?4LOw42*q8$vBlv9M&;#clj-pOat_n1oA6;;lwc+4AWobyj;XI0!j< zgqX%^eHUcgMOhi z2K1Mql@N;WPGLs>o0cj~9V_&b#_g-w$z~#E6{`0qRq%4$jl-(47a`Ew~V)I1YtDBm*bDVk-V{{{ZC%TN*_u&qOnU8*k!~G>=N^u4{Y`qGE z(E<&bT*sd(!}QwfSjR~h`<65MTe8HTda;GnGZeY8dMvRY%6)a$shxB#*Q^aQGua;D z(n4~AG@fh)=l}#_wrc_y=f_r$s+=}>=&qRf>ux)@=q&EfeW0_fQN`0B{OAradacN{ zR>yGDyZWWNt%xw1B_HC1aT}e9Lmux9E{m5(jIqfX&h;!P;OKnbE0h5>Aek75cHu@_ z7*}jf`D2$cATr^x;{8Q1;LhFy*{%T1v zpyA!`64dtraSzjEv3nWUUW5{RUyc7hA|NKurvOnwI7X+Tl`%1uYq`EWVbK_>gLADn zUw35NgJf&0Fy|_8Z?ZwFEmd!w3Y@%eSMFNw+|B#W4W2X2eQLaZcqe@PwB=k&{U|r~ zIu#rS7C!(tLHI76C+3~i@OUX(Lv!niT7pJWkTI!Vl$o@H)*G&X?6&>b30E#u4VR3Fc>L8o_oCrRr6~ z3FYd5KM*p8|Dg@oO#)r>XEVY@8?=_5j3GDr=Wgg89_p7NWq-3|D0Lc&jatmmVT`h7AL){SgAalqOqe! ziJXT$e0{|8Rg{|!X5t1D#|W|@RU}!yqOXD*F!cy64edws%uUy1vStrv4W-A1d2k5@M-kZ%UK=q?ymxHhttay zl^IFLWoYss*rMKo%SUVNW|gF%Z{W%nAjk5!QOjI#&2GWOFRC5@LT1peBIS-@hzTKF zPj82OoJ@M3MbR;qyvd&2$$emA!5Z`X9J(JIbG%o}INv~nqpVvV=lkwF4r#rovoTq~ zu0ekYA@I7}+QR_9B9)YNt;VkP*mvM?L^Y zFQ13%cJ<@iroTW&vIL&Ui7%s(NT%OROr^zM<+P7uu%wln^|CTo7zm$S?%w0jz5``k z=6RFus3eWR-@f3qEf|Uoe}*B>t?+Qz$@J$RP_3!G`Vnbn$3hiLBi`?vAkS-}N}$0Y z#&0BD7O|pHdhh;-`ZUr5)WT_2leOVBtCQX zQQF4$QiW)7Y6CLB(26*G9tna^#*yqmK8tgJ?DmNoKXS?@T#X^^*_-#IDgzkW21q_( zFCUh)>ckxmt4qBV|Kz(f1Hwi)ap5ulbUW4@$}#CoNV&uQJnh{(1&3=wGGjWT z4q%6#Ea74AiMdkqlX-vq1i1KgP9h2opruHO40ka!FTfsqe|=YJ`IqKsYq&UoSk&F| ziyhOuidL4;7*M}>wt=J|?UTd8k`6}T7K?BZu?TX_U!twGsD`#95AwEa3Uyh=z!SnbvRO&LZzf} z>k|j3t%cU6DeTnb_aI$J;l-%6oBK;2{sK*{$ToUM1R8i>JM<<(7Qj7;=St9Mj`pP8Av!%i*Me8JV9H;Ye zS|QBG+AIrcli5u=u2n5JYp3U@Yo6fi5MukTNWuam$;)Nr=%cQ4KYS`7sbYk{vS6m{ zjFDn}k7{hh!r3oe#`_u>K#=}!l71D3A_?E42jRA7QRxCyz8;piM1?UfAt598qg$ga znp0AxWs0vE*2Fa>#7Z@j`PqrxqnG{93qyH!0*;j(NNdU5_I?)(aJ5nu#K>;*oqt5> z=Lcw0(sAm!kwGG@98MrBE%3KAR&<0yUp}pQ!xgTH-YPKDo0 z{ZLMc5G@tj8!RLN|{4`QXrWyYRSe;uQhihP`ncrC}VRIDGeW0$*a>{K#;vzBT=MRwegPy3Qr{f4cuL8kTg1NbyA-$=Ha}I?2DI#7bD6*?>PAkFiB_+@Swp(}TE8kY^+gW}#(nyES5Z5rnxeROv zh5jEaOy`eCEWPaxThG$qO{G5LoToQ#mK*~2Xg$J3-o2}+m8je>(fAPD)=Kp>INXbl zl5t`OSc4AASTW7k=j&Z2X>sa%2#WXreinrysvNrPx<~HBd7rG%zI9G?)X`3iwj+tYlU27>vwMY_t5HlbsNCv0zZL7tiyLvm;?+YUOTMY0_d zV)$sTuC|tiJk4Hcv2h_{RwGiA(qL``!)UgA5>PAS>%Bq>VL(GEqLG>z&y~gLe=rAC zn1WrUWee5aY|^?$4{`l&=%<#U5cIzXMzz>COLV8Gp-(3S;XfQ#I{G28?VeL53;nVm zwVo#z4M7Kxbn4X z=?!B|Uf8K8+#_p#Ql1fnR6Pyz^(U;-C_Lv*ocH4Xx1Qt4-UKvRx2pz4aUneT9YSe{ zRsrq59l}$*D-5%Ad>DI}we9qRuKDaWUC=Rj^N2Bot$a5A=x78Z#8@XMm!FaRCb&6u zGSEauN0j*vG+cT>an25^XrIAWde#nX=yz~@Hw3O`zir~X``uouCiLCYe8a|?cP6-q zbLmX45yE*P#93lM@2B}wwOw?88I0ga0`>#e_Gcgk@4x&Yi3S~T7C-|&!(o%OKqnGB z{Hl7OZC3)Yr?*DofKXg8nb|I*EKBP123a0iW)K*Ma>1~vf`!L%lCjvj%6LHtz_cbc_G?9*l67F<^uHub;nc@qen_8iVz8TBX}%kZ~0uUV6jvg^E}PaiRaA zZyjw~e`mN~uDjdxC%46zhv_|}9Lz<29JEKLNcl|p8{9qQ;kdnAaEhL>rGSxte1=|N zinS7d3&*=E-P>+}&n)bJ=ze0X1g0;%{odJh4`!2U{c?0?SSQ}}tVCJY;MK6q(~qI# zhGgyCUo3slno1r9mM?f2(raW?ib7I1A`>4l7?zwQje@_RaN*T9qm^-M9@z71Uuo}(Vz(oJms?-I%QK6a^GtY`iP1G+Cw^as1+kzt+pC| z`kz`$7SjdyWFF?y%cq4nWzf3Q!osjUJ(9=4Z!rLD4d%>V^m*`Mt~M{yY>bvqpzTM) zKHcYW=Lq#!2T!ii7oV;dtaK_)Z<9Zs=5zt5yC{(1QU6i27rJfeUjCpZt>wv++V;#~L_O7ZGP*%v7M2Hs|RN^oe) z83*5^MNnQ7yUKE`hu9&V6cCCJEqE{-h){k_+es*;0M=phJ&ZOsSu@3v!dBN#URm1l zkW|7K_7D_0U8q!-ltvkpenY-{{o@S|FsjVHt~LK{YGKeuhZTgxu2i<#CvlWO<66XG z>_Z2ks}vLgP&5-qIm-?=lcG+z$qnhyAptxtT)i?Wn!+KYT_~O|@efeePHGp`E~(|= zuux2luK#{x4!WDbpE%6M`2fsbVDRt{MQ+FGr!xM+hu$Tp6r;$MmC}nUD_!s>UG^bF z(WDC?s(zF44w86815J~IU=`Z#Lv(qhrMSbguzSor_*(xt&q+!qYGJf%K$oFGFyQ&4 zeCzw((@66#FgK^DA#rbaV1!giX)KkkJmLS)2K|_nN}vk5xjA?3x}TOznV**+%hO&w zGJ{-qr$^W1>M16e;qL)BnoT%=Mh=@BGdPCa@F~xDCQTUL9?H)+gTufV_ix1p;q;W4ML@EZ6Y3o!noO+u;Ml+FFXB~v=ea$2`Dtt1Ov1g?L`+JwwNt3wMp&8nCnvXkvvcXa7B#Wik^oDf_2*o;oqv2~&# z2y9#W9_#39Vs@&WR_FSaC%du_CY(JbY)n3i`fZ+AmSpaZ?Y}W|>WwURI8v&}|N9IE zQE#*JI>V9REcto~=!P{E6Ql}%iP?K{LcI9AN7sN>%|fV_&N$Q1Dc znnhBfy<7 z4O!qk*TJTM`CvJdx>I-1#Uzzjg`8XX2>)C-f(tLaVvRC%ILAZufMY~kEEBYym}x$_ zt|dJE4x+09iBCMJya+heuu`4?X|C>m=gO`*?^>1^d0lBZRv)E81VY=;F4#xU(mhW$ zpIe`?nG-a%s-=tMJI$vW8X*jGn{>zl6<8kpf!h~Y+zrW5Cqy)yI54aF_4PAz?9lk2`;dA{Z!B}S9G(UJt6r_|lPB>>cD?ryene-)9#oV|&G~K?K0BI2FNn!DO#~6l3AWc@(j#BXDQIgD)}~jY{-Xt}qW!?l zWqL#<6WsCPQCP^oCDq$AggAY@+ga0`=yBk~@gV)sA=+~`$l%&!-9)xNES14DmH_6l z@Iz#!n3;;6z0J#{D8r|9&mif20{w9VFEjzxWWOIkgp%b>OT;71`hs<1I^M*UCHyJ3 zD!?Ep6+0_#ldXz6o9<-_#bX!d424gVCBZ{CzzD)iR6DqjRl6QnE zgiK(HS(xG~;7ufS$#<08dZ=USLWGmRWfsj!vUBQ+(3p1A{imgdJO7 zd}{U$OR5H*gm^4IrVn3Nk88U4WjQU^`tVJtz-kxZ3qEA3;V$_s}V9 znJ{RWqd5;oLSIy+TxXZ)#{j^5++#_a! zBY}D&T?}W!eAQB3i0E{yvK(SmKS0eFxMLLQ*J?BSP8`)_+0q5QAc|A$a-<9;kWtEf z4O|$9oJjPXS-4!TRKY-Wnb?Q&8H@OFF$n(WA4CKfjQ{(G|NJ-N|HhOS0?bRhKO$#} z|Gh$8RPa~-SrdlQjz)wo51@f2S(K;yuhoNL;GVJkXBqbi{`pYa9wg z2*gADpB4YlZ!;le$_D-~>yC*4P+I@>%K!5+2><70{%aZkzp^%wOP;`+BIa}c_Kxy9 zKCKwW0R!RV*9vGa&-6tCZ0lzj@bFqoo-*b3vrT_T9czf%xD5H066i`7> zK|xd$q(MMHLApyCq(w>uL^`EQMG=)Q$r0)9Zcr%!B_)UM96E+M&**RO{q66ZzYl-t zb?NZVde?f^)A#+TS1Y-*XdZ|FFeJOvDnZOu8#)o0WQnOG0o14PRa&0G;OL@yg{_qQ z=Q~A90{a`th$yz`4k!smuMFkOk5xJ_nsxN6=RLG(PY|%WBh2!j+v`tlJb~hSl@u0~+ha_B-}xWEFEC7o}=0Ife_%D3mG8{Ypl2O&#Bd z8Fd^>V`&-EfzRrUpJ7hLOn)w3LAf;htCu}dmfv)d6xc?x4A+=fc%kG;mbRvt!@13E zgc5E7dfGgJoH0Uwmk5m%tBhi@!*#a1y?1aKL`2PaxnWkK9Bf+Zh-Nbodg=f zaA3oPQ6V~PXGy1h zOrS5#K5Sh5i@0&06{IPQ>Z$pj0JNDNu}8%KdE9qLw;Kx7N|HVuxqMbSHBPvA>=6n4 zy+X6Kfn=?$O)hP}>=?t02_)uN2h5hj&lS7<_G(WOi7{>a%%ZvSU-z&1L+#;2*&*iE z{amPF{rAccQcp4Ub(R+_*90$FC*a0C`>vSX6Tuv;XJ|6iZKEKE{$c}jWBiyP@aj#q zP^@y_erJ91(z|W{I!8hTS!;*_8h>N-eEDSAkYU7=vX!j7LV&C9w`e>h27m`Wb{ZyCFAMam=m0<%4Q*q$ zd|3H8QA|bBhBDcBxHnj^wMVmOEN#VO`1XE$Ks%Z>h>TWXm{`?ST1>*OqaeuV=nRP^;@%*=%9{+ptaIhD!^_u3)xv(45xbPdq|u7S=&a4 z`2vC@Hw*&)HbBtP!^q(nKy?BeR4g_(pbmWxX^&svlblL|uQNGV)`Mu6oa+0J@=K6N zZ1;V7zfYEWHyn_t^25b*sen-ohl%C_K!E-;K|mv#!+;gwDN$opHZ5b-EkHa0u2ep2 zq(Jx_zr zc>1%k`xQkWvwVN%Zw5@wIrajFrRj6+2;>pJ96~f@b)-sU76W|9q0p5E7jr8isbngq z3}9XqKz{Y+tl(u*+O{9>uSnhGvt~hD{kHiv!5Vw@0~2{~3?h$=$PJ^F4@+ z1YAg^h5;<&?Op%uCuP}Qn-aIMvr`}zmEg4*xocNzT(dGxPA}^N#OteS-Tn{`$DgLd z=&%loLLW3IuyS>TLQB70K{xgNXO$y)lhy&aSh;9^2RE{Cdiw1)7i!elaj93P>`kNP zE(RuO%JiRv&B(HTyLfSg&rUz)z&7Cw2=!WV?Ra%*nXvX(X|+Hy z=6wBnwoyZyA;4qs4mHJDGc|@nEm|YYWnl@Myk@% z*Kk%dqL&N?ajDBa9ZmP9jvY|iCkL$U!#Rc(|IDVBeE0)px9hacQ+81L*kR(H?@nZQ zcU%Z(jp}HNQp5;QVhUx=mwL-Ge#VL@XKUrH?!Dx*9if9kd*Kpij)4)Vq5ZpJ4At^R z|59JE(}K#RleJ^2GwA4b>^(kgk{D!hB^st7?fAnsv_EisL7Srby6{k;l2XlvKN5f1sKPN*&gyPO(flvZ)-tb_lV@%tlhfp3eAEN-) zd%d%3Gz8iif8G|Orae7d#SY*nCV+kAeSJvdtQ*Ufmgap-48fLTMS&Ag3{Z%8L?-Y% zNP>B!2g4f<^>hU&5=DtL{fyq+S<(R7uWHazkLESgbw1E)RSoW)<@n|M#lXNS*BU5?jUG|uy+bdojbI$!+< zr=^pG_~q@j3gq6R?t6Mb_Cx`syd5CV{R$~Ww-{F>|lR~Y5)>Q!Hjy!OTq#D+FU}@X1oPsAO+fOV8 zbF-Wme@OtV&r>JjqpPS}w?(}k$MM>*0wusBq|HdF8nn97tPJMGKoE8RTfX#p+GH8= zPh3#Yi!wGM*4#oLU%Ej@!DWhCJaJy)Zp+fZ@T0rHuU_aDyWHc&(+M>D zQ5tj-ctjPzTYwM^XVc+0JeUd5;$o)<2tp+2$akv;g?LHc=Lz6jwa5gqItbhsXY3w` zqIbB53yt4Au>K={v~#E_aK5nj0Lb02v;bAkOW~0)PXee8Cs9uTYZwLT-A*94noD#| z*vKS+mBw78$Za8~pb}i4e@(+c$BcvuRYMf_7x7oGxu8%Bv~b#OMNG;QIzP~$tP~(w z%3$8DPup7Pcg5TB`-dnjlsa?+n;x*fc4r7%`q0iBN!BI+rEpK+>~j^wU6=qEIpKZe zxq1-?-X?l`p}S+4JGea-r@nRT>vzSQ-R<-e5}c+(-D_YlU8cbuQ%IKFXkRH1GIEg6 ze{VJ>0o2K))#SH^4?a_n~i#Qb!j7LEHxXTik&?Yfh#ZPml)u>c*?k;FHqHc zeYk{qwg$^r1w@GqP&_Jkz<&YQK4#!uOy%~wOv)sEM1=>#;lj|zm{7|BEBT`xin{8D zThnR4&?STFQ!Pm~FAybVf3g#b#hXQ0XfP{4e-bpv#1OyQq2N*&DP1xf=txul^3>RU zCGRZnX#X|NIA(AbB0i{)-Xz;UgxSqe8NQ$=)YCuNaOFVfY6q0*(xB0!*tA`}EUm{< z2n8-EupDrK0{C!`6-&(wQWQqEDAEmrWCSR9MyTAVzf-uubH#-L#{05naV~G~EcIb0 z5Oi*14rWN^lrjO|2?a?t04O|jqBPb5#B{pLt;R+Ska{V6xXJ>o6w;2<-yAZ`S_E*6 z^$7_%p;}A_IkF<*BtAR)+n+!Jv+jv&gIGR!_Ty8Q_HMkx)-!ug*~b=L(29Q-8s1}I zVuawQFng6R4NOX(e|^lL^S|5^xI_G^C15Y7M>;T~ZK{QUGMJ$5e7I}~wJ}39-nox$ zALc%qeMd2>r#z?1QsZ{}5{wzt>&?<2IXpf7&Gp5|&b>nXihLAn8OV$Q?`gFPA>X)% zmuX4b#WNlY9Ml~;Eut|#2yX_NkI|3tIGeAnjh842uw)Nq&Xh&G{!?s8e(^AkDgpPV z7ed6-$VAmY5~XFYcC5p|R02c0Vh5N7 zkxt|za}=@z@aKl~&}E~>w!fxZ#pR}^2@2+Jfm)d*PkS6se22)>=aK$l+fY#IAhZX> zbeRwikYKCBh?3;>-_>O%i&~3DpowBY%;TejLUryRmQ#THsx=i5rtjA5AIM7#zDzeO za}$6EQzc28k_Q2bFc5Wl70+@b4CXZTnHTno08+K3anFTUAM|M91+EV-NHtUb*lcj3iwjyaU{VWTHUT(*~@eMt@?!Uy> zE}==be@jHJsrT=;`5!!dU4e`qcDi13LiLgfc?gEk6tL)|JeChYtVnEvsldp9)lR>7 zx+g^Ni6*~F*V?J8(!nIEBpn$4c#$S)&T#ikPYyN)=-5r@p7~2thn|NW zJNhsb0o_&9vp~-n&F?VX1~#4Qs2${O4kV?6qR3%}Dco>U0P~_SZ7q1$x!8UUxVd4qx<3$u*C6N&b&Sp!qBcya@0eTdcc* z(Crh~YhVXb230kWx=nIhI28I#1-(LKbbz8j0yskNfbn^BzxT>Vbkmz-Y+VZl^`}(D zn0(r;VM4-<*->!s)egCwP0#!LLS?2%DN#^a(D}s%*J1e;X*P(nYCwfbWou^9L0#J{ z?g9>acB0Oc%W?Kj---_K4FQP=JMfcYb4;Ym(jEag?z?_p2!Hx3vo(?H9m60gR=9Na z{Xnk=itTH&xIv}8AuKqJY2Yj&X349JduyZ2%5e|0gWp1}RS`ykD${HDRZI}6+yhF$Lpte%fZ6~owtH#`c#TCM zHFkxVs$c^`ko4BBvpNg}BhW4NGoDYC0t2{~yk;EZUS%MV*jYyhKCl{85;&J2aF*-2 zX*1V9_0^U`a4}vvuD0NGELo=@q#seUI#$)WwDtMEi2{~XhnhBw=dk?#)$0kk=dS<< zs~5y`iG9eHKOx^gXv+_lX*Ayb3h~jwEy1&L{%7z{wih_v?pD&N-76n-FR8PI;Pf^^ z>Yx7(*`P-G`b&lMUtnP!ehv0hbn=OUWu2ZTL%5FaC*Xwievvp6F!LwT@9hV`2y8Dx z#^Kv7%95P_1bNuUkP=4NsV4CMp88zVUeN@e$Se@|vEDBd>c)ALu0%l|HrdG;&<2W; zG8ou!44})K7E15?lY#Ikq00Z#LfChgwmgaQ@?05Yw{*n`zY0Vb15=_s2(cI#8=F&0 z1??Bw$SF}WCZ$M_RXJGB-yoaYoTJL?uo!No46p5pR#eC#?|E8dTq?YO&dA@3pgvPD zJ9>WY&1Es05wFP|A7Jt&bU2SQwy=+^05V?_GpJ5+!Qj-W{d;i!T(0XO>e-Q=?Vkt< zm4B8076ne{WS6`~i??10z(*YcdO-oewwk2tziPXI%GnHNRXDl$!-TRs z;2}mq{ve=PgWz_-xv5KGI&qg)-pn9lavpKle*AaP=V`ByVso~>_RyUWb#k2;fA#9U zQQA#NsYF@P7}mN?sXvsgH3McQF0TVV^~^`K5H?3MD9336c~Y9okGn?o`oI#A=hFST z%-g8o4B0MBS~LT#lL|>YnAasR(6hI8tnvKk=VZzk{Cq&gWc-LM48(#C_p0@Obi_A- z{&?&mTG>w2vIsbu#z1F8$H5j-=mr{w@-QNNMr0uw!wED%`^PUs5h+$f99UM*EJ$8- z31DHNa4$8$8=t>C^3jd>e<$qaaGZemak=DS(9LuqoK2Af;D|>1Q03~#`h)<7qagno z@zaw-v!0|nEURw_;v>Ig0M37IG4|N}&)x&KJ(O?4O$um6O)lQj}qg=4^|&vBd~j?Y6wU_MWT=Qb571+XCf}7sv-;lD!>6gY&h8UcOD{V z@tprx5kk(duh!kE9VRsarHK}-$I9uha$G5wWxpJRIDyClC%~BoUSp5T)h&4_dXyRS z09wO9t>%)tQHy%XF)7C}fIAz?W96gWeQx^J|M762X9FMHrwq-@2ts&0+wW2@IN(jb~narIPL19Xf*SkPEXG5Vh1?K3hm3ro3_k##>4r`)Vc)zC6f{sM0zO%ar$?>f%GXc~kvr@b!IJYu0&9y}-~Ln{@?`ND}NN z3ziZQwzL!6E=t>7ve^HtsDpRIe@3_5x(cgRDAt%&6245IK&Yhm7S|H!GD~gIuR!xW zm`yP6NoofU8S7^lPbVtB8Iav@2;c7zG!``2KEl%9>%zcrW*86?2|1E?#zE4C|GQQc zibKG*zcP@M0p<-x>@N2Vt-J`8{Q@+*%OU=^FIGeR{y)3!|NrN-iOcthU%?QjPRJh) zG;AGiHl3;Hse?@U5gKwAN+C1HU?j6VZ#21v+!k@>=e%=A&R}C?fXz_k!vDGA%hN2s z8N?4zA8`T6zZ8lwZ;vxL+Pk$nSoMa$n+an5mxn^fR{8ox(%N^8R(BGHof4^;|Bp9@ ze78g+|JmgMySoyk*t$Wrmxy)YH#hosqYJ#2V@hA81L^KRt7R#WaqpTs_)Q-WwI!yJ zeivL)bMev_zf*@=5yM{8R~b+TTlDTbo5>`q|9eL(RpIHVdEhU?*nP7{(mu+xSL-x^ zN={6>*r%A>VMn>d>Pt4kz;Glx-Rg^_G+vVl{ExNxyUbTV#C)kqV1$lPuX&-t`BPRh zNHMGi!oW|Xd~U1xt5HHRO1upQc}p_MMJPgO`2J@{NVWBA-6T}d#~~x%y*4JOu!SkGt3b~m!U?{s zwov@vrOC|(5aV*XL%Qkbu|;(4wu*Yz}dK^e96Lrt`aJnpu%E9-eB^e3Ar09c|>R%M8;N zqM!kSHa_+{r%?pt)fJi~H{_SN<`^YjSF}{7{#`Qzc(RR(ymlirP+N&1@!Y+CNeoQ5 zRN2w<&iNoxF&{4$qvQ_^%F+7kWA%(euGSF_CiPLIjGc0$E{QE^C;whNw5`1?N5^z8 z*P_@CNevkZ&Grc92Gg!+gI@EtR2N>jnX>Hm(0HGJv}WW>6+SE)Xgc>HO+DX> z5$tnvE$ok*7|~zZ+~RgV@7DD3dJn{tjFzrI&rsg}AxO_r0$l1eJ4_QP`-oyjlc$Vg z+n4~i>G+6NIJ2D(%cC!aou0uIe8Z?e?{+&|YzD&Ug<5KGu6%gErSkWd%6+ki{=cQZ zni?WP%>o~K7K8NHa5}A1u8;e#0zW&Jx!)Az-?Bd!^n`vmPmWF-=L*o7okL!v211sg zzcNuf@{4xz9NfGZ2pi>7n7Y`dPC?fw;S=9v3M6GNa=^z)I5XunQ?PkF!=U)~nsY?)^8!u}Ayq8bsl66SHU&iiXZ*iUmzG?-y!l%BmTws7BY8;m-1Q}+ z{}CTfYr>S#TGTSxd{~mKsqpW-|0x&9-kSeUW~yrSf7;7 zRv3}LJKIjivAJU-SSq)SH9jFE#5ufe(OE3n$I8`oIFj4_#gfV<| z+HW$c@;QDb?155x{rWX!u$cFre*SnhCnlkWLp4b>MtpKB!W{er$@78EV=z+XXcsM) zu5>CoTLauHZ71$dvuhP(?I{5yb2Yz_ZE!rzWeNX8b%(H$3hz1obya?Ml>gDsaK`vP z3dCPSu1=z zpW5=t@2Yt9nGmQNMrN_?bqsoE%=PZpgfHej4(CyR#uCOCRGJ%Y^sMgCk=XG%Ka0;3 zSMlQH|DHN&_1Od|06xlMFkaRkUf)sQEzNv`;$Egc z?5ZYiRXu$v$#_mB`tIAlV+eXBZt~hPiFGBioH_QT`~D#&5}S$vF8j8@u3He=Z};XN z5^xm2<}jdyvbN3TnTC6r?vK;jFPxfGIbp(TZ;uNX&HOnxI=9{4(6N18BAoFhU6v+8 zgs|6%rps+nwiVaJ$k-fAG0sABGCY+rAon#Qg#0||%P z*V4PX!eR-EAdMh4lJR~{=Y=aVlNiLlQiz!6o|3?&O$#VChk!$_A`j+iE{N}^j?7J% zCQ=P_F$MxRu*#6nRzC(Zy^7iWS!qr~Brr@x)5nbt4xoT_|F*Ci;_-K6siR>?kI}&j zEyZRFzWk93<>6^WoI0zoU_AM0%X91K``Z<1zz(Y1gnZZL~>FU2ar%{9w`l44@=cc`IOeU zZun~u{0}?*8+|SuJ7kz-|H^O#Y3{{D}Y;fQ3Vye{{GV&q>eyseS-FBJN41Qf@wcq)Tv1T@9|6@(< zI%kb)2X!lFo1Vjj=1`d{-6TdzNJ~@ba-klM5FYeREl4-6ze$7O8uP6cnHT2UgN#%r8I_op)`-H==fi0+G!Oej<2gtLg$Tr8;8Zqmb(9XBI8V_Bh@mt}%KQUzBwB%ytVQPKF;Fhv5YdA=HHz zUr)sPRYT7%JiI4`dB}X_)cHh^qQ5*>xso3C;xx^+?jdV|ZUu+UKzHu9zzDf^b+du{ zv!n+;Coih(Pir5%IPh`19`#SpbN!3WU|(+S_gfzOZ$0aXy0vfGn#}D8Y6>+w>{Qlq zI-c9kXj6PPec|@eLU?2Oy0xocx^sPL2QAJSFLbehH^=^I14mnrkc!+1|NYrcl6Z;LxkEj^smrVoB?a zBxw)42H8sYy`8L5E2OUDdV(o=Y zmXSlLPG~xm{B}5T(Qc8Xm~ri6l=c{(!348Kb0zIVH@k#u4kmT9Aqe3J?ikzlY-C91 zfsD}H?h7?D1*9PFA1H*`AaDb#D(E7nCL$7Orku{ZVe;iwrh@ItY*8+TbeP9uMaX;Z8QBT%1hFkB>- zcV62&6Vj<$->E6!dFaxHnWB85QR3&Et@G!>eiNSooBVLMM%uHACH1?iH_O;2E5J4_ zZC^(z$8^xj`(p%rrFeai55LNOWL4y&RwMji^4-mV=(#E_BPu=3_ycqroDcq2(|>jD zKU-^$8hg1*F|+%k_+Vv!u1-HoD){?HDk;W4d5`X-M7|J{XS|$Lb);&cCBl!(kbp}Q zEz@h{NqXXYy=nY7M1;e9OW0y|t8W481qiqvmQ#v3DGRU=C)M7O64bVrqe)=a;5sz3 zwxSR;DRDGy#N9N^*X*s(vS?3a$x_m-mz~ca<-8EUqgHb+d*?HUFJ;41rywSBr%u

sae2LU#P=F3jx5cL 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

2u^iJxY_I<=y}G{2$*ItfO0d z{-DeJ6}_{LY&xLWC3!6x0q|0B;)2K{%KadJ(S|by;55!y;Jie(wm3L^_O#%^ zAk-h_s@QSswDs#onq-Fw2L*k~zg!CP4`IR=aeCLs2e=B_9C&of zfKNd#R@hCl_6(7@vZCOWaJ!th=KxhGmGXwG;ylt7g?s+}_mSV83%|un%2v_>UbSZW z`}sX*5bi(2zpkXK1=Uw14=v0O`UwW&qrKI4qjUb(tE*29&%)iqV@_YkJQja%i;)Tt zExU>m3){8O{}CONoBKd<*C9$7$OZFjkSzp!G0=#iJ~5ka?kJV_zoGN&yh~9Nl9m_z zyiu+y-1quF99cYB*v`$FvfzxFc;C#|l=H(ct^sF;q#O3daHqq-X5gZk6oADoHO+)6 zLe2O(fHe7lAJ4*QQDifNnL+Z|0u=wHiiV?PX(0jP0PcnuMBWRdaI7&;s8U(3e~xeM zzCcf(voPS4TGj>)IFY#=Og{}V;#iNplwPRsHlW^%B(c@ZLzO6_Es7cQc=R?1v4BPxTrx3{cYv%{00Jg z6~~A0w1viYH5^b~))&uX5i>YEp^!D~P4iSgL3yrDzz|n?lkx9w0&&MDDB7X(Z{&0; zg_+j}5RXdbE|{(SPX+U@_b?xY_eY;=z;Aybhb(UYQ`gIeI+n+8B_TEZ$_-b4K=YfUlS{b2wk)W5v^fcDBd5<{wTzp~|Ju?@|DWH| z{&UCwZ)~6c-{04R3f0>GW8aXZJT@ZYLsuLtf2JZG!Yi11Zhko#Yi%JBeIrA@!t_$L z81`A1ql^H)KT~SY_a0fzO$=FA*yW)Z^&$ff&ET)|6U)G}Bke^3_FP3Jq9$$mi?OLS z58aUio7%#I88%KfbSjcS1sEO|(;fYGx3f|48FHNKr8b+Y__GrrO7);x{pfS~!pv-s z(Lf6&&|=B2_=O)&9^`+YGwZAwo;(bG zf)`3ajVgARVPGkjS2i?bP`(C}&Az7p=WXbA?SdNm0SQSaIPWBvPaNSAFM}!#hDxF9`+tT7-4uyABk<6^@kqupEe-cl zf{%YC7-Yz*Q3J1U0+QMK`q$tu6o)g`ou@4v!mKX|#tBab5h1wG^i#1heXw*S9jeEn zj^AZ;C>ND??ZriMIf+J%@h!|olwR^LSe<%TacPmKJCtaeZOK~>7g zG4!vREN6Ol%ob=Zr8N#N%=ofU+qK0nN~`Z0Msi2;`)p(t4x*!O9pE&K^oZnyTe%34 zx=nt}vZD{b6|-tJ^AcMz5EDPa3n-Lo-kRGh>i_-qH_n7Vr_%1u@6viB`j}3kq`VD* zc;^>K-m4EBav!+rbqD{p&mJN z9gFcl#x{3%2jR5R-+4%I`?hhaE9S6&9QzqM!;$D)j=k9VjyV2qp;(bgY3Z;WVg$74 z*i=tIE19gh7Fe%d-2_lqVe_epsJ{9QC0f6Ao;FiGl zr~v5EKd{Y^%Qj8TRCp$~$qHrek(~9i7*|8 z$HPd`;om+Jw=}lTG(E<+ibz+{0E|Qfl#`Xl(O?hQJ6UxKKjV)$;lVzn+R)s;*Ye1G z*w~WUL%)&TfHpq0EwC;D@Xt95!f2%Ku#&3ixH>?YAP0CM6R*LiAewDgQ37N0+Peb~WRM<8W1CnI4bZ+*T;{#ppRyD#!n;z0Kq^4C-`q-y zcw8F5DOiFAi}?4c?jLL>IvzIZ%fWdJmweL;lva1Z!V0Ph!`WsVYzw-ImK3HhgBggo z6?fa8g=*>hI9F0$su4$W=bbOt?p%3eM`F7^o&ra-l(Ozsv&ux#(99I4 z=u%EMLUSupBm5;L;EJ)35cs^bPfVy`+d08-Ab>-gEt$SW6>lbq0qhUNFXrgv|Jvq9 ztf|h=D;{NnLH=50xQ1i%r6ryd^cczL{VBaVo6uPcwJ*N)oVZNq7u{R0UAa<9V`j#{ z5%XK-dDhMz*8HYShP<8J|59r}V(<1O31Cm`V0P;j#%_P*_v{qW5*=%9S5}Ox{1p z!M*ul^}cW-ton|7ZVYV={bB`rlKn8MLFH8vFF1ADPt^&d1nm<(Uto58AS4|q{Y3Sk z=Fof+c~R#?n+z)FKuQn8v7s(QSCyw+NZ%VHAQo+Q-Vfy_ci5bFwY0JdXD*%WY4XEX z;8;I4Xy(y#DQ<49g@PDBnm!nn5`2_Kw79*D1myKb(WiV=1lcCwr z+-92bP#aM54oNTqnO;iKxe8#+gF_}-xuuWK$i z94s}3t1AN5Po1q;qRtP8TbUCLH)*VGGokugI4$y*N)r9|dx^Y*HW4-?HL-8*X!ALO5Fe)>=l@G)7C zoEi6osa4p5h_2f&%`iXsaWI4PNS|POj@8ToBWkov4f@AgPS>N=38B5M>8Ytfit|Rt z^onBIJEe+4?(V)8Mmj|&3&PvCv3u4+MFyho(3S98QGg9Vu5{{!e1P?f+fK){s`?J( z$ZA0S+?4Xg>MqeqS?ih=ftCM<`)B$*S2M&L?eLk-EV8uX#NgHU-h22_ZobFuhrY?% zU+GB>*f53qi*6rIF;z03zlNM!vD@RLw?&Wpejkxynfg@JpNN{pg+-oLp)fc4w82-)#(6cVW~_etnnD z7-Q`iLKn$E8^C4+>8g058Xxd4iO{UddlMc=lkE`bd5ESk$aSMjNBfyTd01?m&YDzZ=4RE8 zZoBg|6IMw6z45+*IL5^i@_?iEg?83hT*=Gv3O_!583d(c2J(BLgfz$U;-+$qYj#3H z!WvDAEsASI9m%I$nln>=jUBJs;2RMKxj50Hd!*;bGcvlE??hg->{Mu$p&J%PLzQ1l ztBSn3TuMyF^jt6_o`%Im$>52da6QSPt6SsSrdVFHnyE?`=R1yj;^mUccoD&q zxA=`UhasIC{I+tg;c{i#7=aDbE3=h7Q@ae<7pSv0mX{%oXMCq4DIQ=Mq={s)g30Jp zFlK?Ulp>Mj+XW}fE#!qxh;ALXh}#S- z;{T*TVso0@GLuNzY80DM;0k%LQbEcXgY`Do*DNPe<{+Yn?>*F_@IOO%YQp);omXFn zc;BJRcws}Frk$Fz!(tZ*D>F*l#2ZDlK=F_j8;-Ti>txH-)wI3qOn;kUMf_Ds&cu3% zSzP6wGb(^rZ{(agzeiQ>O9y9E`Q+(>18dVX6?(^=OwbO3SVm8Jpt=BYYO%WGp8iUPQuzsqrQxQ&vZ1i^ZvTp z-Y2sOq|pxACwJ6p07j5Y;EJ)kk9146Kl_1xMm!^2?=+4-V%y8ubYy4xxPgF_G)b?d zD5yrml}`1*lZR~gA8f~21iz&NT;iQv!B$z_DF)I3`Bjoqrn4n-jutN=>TC!Q@}&-c zSJd|_pgF$GeEICzBnXw~MR_v*GDcn!;2}o-aIBFp^#2uMauhV6 z(m2P#eCq|ClvFhThM{S78>Y2A{r?vmwlG$w&1w~L|-nl8l4t*_&wlw zT17tB)(2FpC?_H?$bf|=Lfy5J!Z8s}t2g(H&1bIG@x%qAO}HpVI!3Z!^{rAbYe1d47yp34$9t@PqO{XjV%j*%YE zbIC+0!5OsVscDO91p4^`3D_xV=$`8siQ_9h^q(!da_vPraKa-S$pCbMg_6HF7r8!z zOqWQzMrJrx?Tp&L6Lb7$8M}NrzL-5$oB=Tqp)Rk zVk940VoTtV8Qf+S!@=I`J>1!FzIdc|hU$i3&FDsR zrir3*=f%jYGttq(3sm#TSd1^8+IMhbpXa@k#bc(bqE3myI`^ZoZ7x}Rh6mrj!JC(e zWo)i%*xBvtm`wiEj0M;0IBUspP|^*3XJo&HbT;}*X0V6sS)p~=+Po6_ge(oAPVzu1 z_d2giFs81u2JqcUIl_;v&Z>$~ibY@lyWyqyZr63b!dxKKH;rm;Y<$Emj^_;{xiJPG z2-A{{E+&(Q9QZDbOF3PLE`TwgW(sy;sm}OjNOa*jC(59#ca3q zA{cU@cmvDZniQ${7~|^R^nO&Eq%wYZWON->AGG$@lHz8W^~B}7qKblJ3yG<+zZ&cZ zOPw;%o-?3uT?jy*;JtKIk|N#yV!2<1H$s!Jyc}G50wrz+&@ACDc_i6m)^D*Lgf8YP z)z07JK%kDUpa@TWC33&}lbMxhVAHijn6cC@Ai=5fnPFp^nuIFl)`8dwrbkH%_nnTN zU-v*`N`mwJ)~su8jjo1g^eTt?_3aTZ+nuj)9ya>@?bcpW%g(1XpiTk(Es1kke+)vn zx4~4Hkm+Ngp(8cEA>N?{v#7F79mkENW}JOiH#sI%*KShSC>kxT0Ey3IB zfW1C4#(dg*JQJdQq52&zaxBwINJQcGibYOOc@esK9FjqCSy`25zfpzv zSPlIc;~F)+mqJKVi(Mql@5l5%U^eH8V8SjDlowRTKVou|IFQbz>t*{7f$Zw$Vdo?a zAm_>uCdZg@EGLVb1^7g^CbUh5-j)@acqZlB4Uqd}97{BVcZ27RQ3rXMR#t-u20<&P z4<7Pr?RUZ?P#x*^n?ZX*KQy;YqtZ0yGiWy&3TJ0TH#~2U{S1`fekkRYo0}_ohatVe zlc8|Jc|-s#!(bQ-cAS#_PZiU_=>JqC$_|{toa&DoB6{}%iGwkX0@Sfe+jp}IAuSM_ zEagYcE_QSRMEetxT?rHW!o$Nz)hynVgQ{#tWa`HC4CIu;mOoObWz#VN`49UMUVh*S z`t(LeO~h542xmVk!m0CkJW@g1Un7L7 z2CuAk8R4%sM?_~*33dcExs67LdWF$r`Z08+EK+4xsUYp*uq>(U#|Ky8j(g2b z=wGTI&tX4{7Z?9M7SYZ+|8AyyFf}7?=J)9=p@$Z63cCOia>$va=4??Fqk z3rgW28Si1#+_tP^1Y6`4U1j#LuA9T$r)!cg7^TMbl{erq=^ICUHH4OSzdd=k+c|0Q zcRv1N;?eyWBaH?L0ltz zMB8&Uo{KPSVIRkI{mvl`9e6b{@T9R>h)a|6(ZwB_A|n z^!gLZ$hR?_mM7}|!gWVPH(7D2h#_@uU0t0BcMyJwSw=>x4U_Y7&+u=sDQs}0=@?TFrSPYU)A_qy7KeH}glzZTU(1eUVbmijE%Vpb^YZh`lRj-k ze>I;#UgX$&cNr5sqluj4PFH`qF`hRedHq6C<#;F$A(dOP5@Jr>rEovEZA(3x#KAu( z62(;I3;cNQ%FA@1B%;u(J<2es&8F3+FYP+ig&78Op~)G9ysBbd+27h>YH-&1xwd%= z&azC_2Mfr}qZ@qqJca29V3YnTZdn>EbF+8e_&y*3#oKGu-Wp3yc{ZyHcQlMVrX$}> zCO0_x!i+unfH*j{XAW0t?V*-qM8prpvEt-PJXuwU==mZ-9fvhOYbfp{KM0ASnT6q% z+zgT}Qk@~?N=KZVQ+Lup^u!(2vUiZ#f)d$G-e&Os7(kab8zb zvCFXnYfZL|H{lIEj@ZN3>>6tz5{Q0ZE8$4W{Hcwo0y1EWf2tYASAn1M)+mm?x(~**OlqN%c{K%2YKhcaaJpvtIHkJgVoxE zDl{#3nwY9M>5E+_l%ZEYL0_XYix^0Ppn?qRWj=``2edBttIle-=of7Z*H=e(w+v^Y zr!fT9RSZsDkd16i-vw3Uu)~7SpqwJRiN^kZRL4*}k4b(toLjrmWBpAy6HIQEEq*6M zaRcg|0$gsSr&YIa&tNB+NvINovB|rhj9)70Coow^{b7cmZu-MsfJ&QIWEWx3R@Jua zmA{-G1QIEK*qS*7{eLtGk{}}3Eg0G5ES;;GH*nX`V4!qacQrD85i3UwDb>$CE-kYU zzTZ-)dK4Rc*9x6_%BF;L*$+ik*)H4ao$yTDJupI4Z_5{*Wk+tD%g5xg5nf-|h&$wp z>Fav3&F%tSV`HMz3y*9kMr(}CLb=NK z%Z=^d78{!yj%37Qt!$Kw4Kr(Nx4gam2S7YuWYO>fY!!s_CmF!raVV^dWT)4&VKB=< z4BIIKHFlw9!wYN@G6W|HKNbruUX4O)R*`Qc!YNGd6roXg*m8}4UTa%6iotk0c?+hM z)Fk(Z)3?b+f4atAs@nEAsegk1PI7v&+u-D=sJKKFlx8e;-JLob5q5i(Eqi@LF!O0^oMsizPc|;8&FtOo54e}Y5#yk0RFPxt8g<|xCBnz|P$gz?{0-^W z>E8PJ%H{&U^9M*#iVI%SM2`l%l^?Rv6&VF&BLdNC9!|5k{jzxOd9i`)u`^BHgo>1Y zC5NW}cnT|7uf!B;%3}N<38}z2+IwH!F;DHmEQAUzoj_}L1h_|_bykf z%KI6KHgWqk-^;swjrPyQe)89fay#EkR#B%+o?PiAiHpmvMIeMt?ovbwoU1$CIp$+; z62S*#%J#OD(X1cKRM)|Z%3y(A{pr2p1LL=Z0-Te>3{;u*tc*k=6p@&D%lYXF8)n;$ zEm@L4d;5^OpVNuc^8I5|hg-vGjQCR}vr>RIy`kfx?EmuUU1$+UQF?I{6rS46ipcj9 zQSt|?Q?BEcHs=Lny%P$ZhzLGVt24`enz$OQmnO}4mii=5+mpd~YvjAuV=3_n4*uN` z(JA6=yB{{@=CzG&%<>nf1s63r56mmupCNKwqY2oeJbm@I%uY|!}$*( zLz)Va#lzEJX1z55+beUwwm%q(mY2I6{93Y^TP}G2iudL%CV9;2h*h2`YUOjFtqS{( zZ~PBSOUpHveoGs=Q|4#YTx3F8(Iqkq`=Vbeuy7;-6``%XdD>@es2weH|`it}vz2y=ri6m7b zQ9xfNrF=tVB7V*7Kg`Q+%=J25c%-&vM;kyLr+gouE})6X#W(Bxs(hAWpsuz!ztTUO z@6R>6D^Vk4yKAVg|FrEA%~GkjwoUj<-bg%X_gnM($fKu(YTAS@WsPluD)imO#l@=Z zabMxqdSNIN{(44ULET32;Y4|DGM|S*`B>KRpX7pCRYwCa?0?F?z6wJLfv|w+Yr%rQ z!f+Uih6uq<1a6N5cArpCP;|81IGcZEG)k|xw+F#O3E!chNC?rcf*h;XEepJ}f4=ee zkEgXn=iqn!=ce$H>|BUueZ8rk-EGT|pFdT~CCcC)JN?Q+S7p|NbmO(P?I+tt=R=&) zOTCl=g4_bm&Xsj1GUVQVeuDM~YBrgHixXv5TqH80Lo}&#!68~BLP~ApTb_)a%<1r` z+HWe-U!L8bCZEl9KmKXapj_FX`ShIRk^Yf|R2+vtL(M&}vnNiP!{6}obT-e;T?l8m ztl6j^KLyr9Be8ejSy+dz5*u_Lx=xZhqkEkW-t&=*N20$Nc9ywtiH9EZ^O8mAE(cq; zF`n2SWFSGF|Eb3VSLprJmuF(`tg*9Bw{EQ*$FbYpunaP(R(WpNcvInAn;}t0%gB8F zmx7h@P1E@G6~`YZ8~qg|ldA~)cvm#fC--wUJ@M>zYEC&_^bypGGgJRmt+*bayU4U#D)a6Xuo)Qc+#<(8UjX~PNY~B~ zOS^rr-CciIY<&y(Q!n7~_3eV-Rqm6^F(ep~o!O^!go|G*gXq8C-vV{{x%po`{#bPr=iOx}#4EcaejBJ8lk11K9`6sD;4+wjhuF-Qo_!#{apQTS z5s6^3&>GI5aW^R_3s;+%qW>PAGCMty!14ktnZsj**mVP!yfyTxtpEP+>GOkDvau@&uooPgPpG4ycMVvwkk6OW+WMw&j79pEn~VS=j!k8 z-CoSH;CNSiu73MnB9tODs-<6_*wJeDM&xb=s)B$?9FW)^`I#`}e)F%S8cDR6(Mo|F8pnTU9= zofQ5XUB*0I>A?MoUQNImjUVy^b;E4q#RpIx1f`omBmjy@V3FRezV64jx;)(~ z;2T!HuST0?E(;NM$IFvwfMT48QpgX%yz1x?8KytLD$fmRIx!2rB{HXiaCl)nnAOY(#_+lzKcy}{y~k`C zV=ePvS=dM z&>OawRPU}TWY)wQ^e;~BM4nmO8cP}a89I{YehE+34d2q2SiNX!!%eQqs=WP4VCZ0- zOpwNBmV+#f{mb2N?&IJ};~s!FfUS=0MnvwLayK}X%+&spP*v!dZs zQ({jjofL9{<^Vpy)1DZW|M2U#e9>M6%Ws%V->qTLz;Qd(zQ<|!a%)ax>CQKtwo(2wuc%3pnJV4eyg#Mu7Cwpl>z+?e>4ojHqoXDDn2{c)Eh}{C zEt;97uk53?pA&=eF+6C(yYvUQz=dh)7vm3O!n0>F5zI!HL|-?ses&F}}+9PZgfMYAwqAFqwcGqo#WEsE3zk-_l{?*wIm; zuD1ry>dYl28?tAQ4+V4DWDtS3;&ZEq#0PWNPe}fO@?2}D^~XI{$IA5=V<{3&q3nnp zC9m?`+d@x$An!WEQL0DX1vgCh+okVdHIZH?yNbwZ9?WF2eWc_z7Ie(HfuW_;PM)8?aDM4Ggiod#H@3@Ymcwb_A2;+n9< zQtkq>n&qFUK)dfis6m(+tw}Rgs}O1PScl+&?{GpB|onnh>Hc{IOWJT=iR2 z$%ka(y`n(czJ40A8?w{^4Gi0-p0u+1%R;+jqodR9q3^9xdu%Uk*2CtuKYy8M^hOfS zqSRgFK4t|@$}CsKGdaYFW@22==9nfA5PuA3pmvsmWq&~~XEnU-bqbzhwgP3_>Y6cX zAVr(lKAS()yw9y?e) z->(=suw9qJYTw)YH}FV(852D+Di4>x-2&gE`cH?a{PT(#R3ntA?%3z!tZ)2tTw7ZY%Gi14H|He<>^Wk*sgfdTD4(Z1Wb)g50CkvX zcx&rtAqU~Ak1~|90XtmhSu8cv-uB`{8-q0>NV%h;SlLR$k#_e~EcK%}FSq%7Gw*&F zRx5LGaHK_E^h!-{KjC)VlO_xXqGM@2d=d;bs0%lKjVw8dYS)PK2YuwUAlB9UWNl44 zY!)HJS?rtQu^w*u^rAPOyO@gAhI}`I@>Yf%hR&#S2WNg9PpjLCFfEoub;2@Q^*a6l-cK2mN_3v{#GtDu7 z{;VqNC*(z7VePmVX+M$T+w|}1NpvoRKB!kmr^=Utv6w1}w$G}h3vpR0t}zpt>*tv+YLs zxuLYNM0obEpk!udK=dbQCMx=cHH}(2=d;9DLtxk7`^U!zZ)O8oUbfx-G$Tm4T+45MBdT%8~!JxQ-K+d7%+ClTZm5YX|T zcT|obZVJ9Gg76C4wXHeU3wyGmJ0YXQD>w8z6dBHNOxM)n8xSC6i!WvrF1^T6low{Z zy7b`%Pl%9^h>E>EhiHhsh@D%tb6bn_=ox%j_ZMHYXvw478SFO3d;L^xZRkRcx#<`q z^Q!bq;+0#soBiIJ~sKnQm0rn=`V)yo_oP3b}Q!|7K*f z{kLIkZPY)U#0cg{Y;D#YgC9+G%+5YE5)jps95z9xA1SG87JudVaDL7sK#jjduQtF9?q~ukxuSp?n__8S}_)6}DbF&Few}o~I z>17_t??w&}zk7zBzZ^ol&15=W>YprjSF84zJo!aez|71*(CbmP6YaxlF#1xYNhvI?)MDGwdy*QxPWIB}=<~ek-5Bf>H#JQjL`5>6 z6^du*3GEV&ja8H7b~)KA=+*mjJ=Kg%l|rn$g`PggkoHo#6%nO-nJdQNJ{%~C$4`&9 zkON=4U%7iu#qyl|wCj;90=K}Es{pNeFsGgu_aDW1BjH(Y)&^31-Dfp<#1ar>2wjPPiwEACx-v`BWeX+%uw@Jzw2 zA3sjN+eGgPZRh$X(aU{>16jclHWNVg1BFz#!cB zef&i1rL@6Z+$*kXvMH$P`g5E1a6|qOttOw$Za+ArPfbhT1y>1Xdp|bO=7KFBc3ikv z+r8$GeA-p9g=56h2Y1oj=Yx^tge0laLRk{*VYt*#YkfI5pw6&ZS{Zy#`m&U~n2pVT zIXUR7(_M{6mGs?HE-Xx=t<6JsA=FH4+wT2xpQV4%u;gi8VWTpX?*T;p)LP>aJ%fDL z9Rfha5yM095k{rh?5Y(P7RH_#ro&^=?&bcoG|OuV{!%*Gha8U&lOY)(V^e=&N94m$ zWwP?5{MPN6z-wL&h^}LU-K9T>H zR>SFkA)MTmMZo6H8nWgxXi(($!|74~{AduW%hc=Z$}F7yP~ugz@#|t-ocvUQ1Gn?( zxbIh|Tjn+v?sh20rSas7x^k_)ej);bFPHJiA00dB&T5a0IQKUf4KJ)F@)W3~_pO|O zcq;RpmY?cD(l-C6v+Cde)*XbQWir)bO$uSYzXZ6CM+SB@uO5BL^2;RK4}M7byCq|O zxZ31;Lc;y}><}(uR#54T+BGXOQ?If;WOhE;))o>LoQ8JL@4XzOx69vm;B2pxyzjQ?pW$KFA9v#+q)7N+>d*G0z3;8Yw*POQ*znn5#0B6q`IAVg<-YBXD zpF*s)g|>}htI{i2Uu!$XhJ(Ny^umYf$4Rct4lB<}%gdL;4h})~FyOT7V)yY?Rrc3J z%JQla{;sIWQq5RXz7T6tXKp9@p=`Bmy}QN}NURmzWUR5IjR{y@)gZ$occhssi4k&`Vob<4N7E(fc{_rHYs-`1QJ zu7W0m!*hhC+7kkJi|P)p_H$HCzV69mtf4<5s<1U}i7|71OCU~Rq`7{`!NYT3!}mm- zwYD35#Kp?zOOqIB#G?uA$U4<+Pe$L%vH?!8h_=zVD)9&lMU7X-7Ln}D$uT91=n0lq zgujX{$Ia*Bglhe+T24pbU64{MFke%$DDJc}?i_p?p4V?y5#`X{POd~^)ziX6!SZO1 zc+xqOkn226e}wj*`)MNmb3gqau+BWxF>z6HTWj9^wI(U*Z@ws5-&DBacIkms++xQS z=u}3Yw|a^&n=JK5AEJ7XV_l)USDlOLKv^F=F12+Ri>{@oe**HjoqwzO;X6eZo?fG>&UWD?S-((nA+Rb z%wh*P0z*-kJ2_E0>YXUFuJJyxg|6F}$!F8dI;^%=V8j2*fxrSF!WH;-O@d@z|aj!NH<7>bV&~lf^;Y$sC0Lyl!($@Lk~SPXaC-H z)_1=5A2_oXvxdF*diJw+Joj~95u>G{h>Jymg@S^DtE>doK|y(eKtXx#3PK0AY=B<< zw?p)Uyuk-uSKALh79KVz>J}f|oLoORIatzp+jw|7xVi{%i*gHc(%FCb;N~gD!{hva z4&Zk6u;ZagUaE@Z{)@t|?C zGoWA{k17yxzT|S8bu}Mw)DV1dr;bqqe8}k|z_OtK^Hq$~a&`6W|NH;HUkns6$p7mQ z5Ir=O{?zn;9izm@w1WMA2KjFndsKhG|JuMXknGFzzt2TRh=}~ZPYxrN+vxbeH-J(s zmD)-F*SU3Y{(qf}Wm26dT%*oyJ*;`ssKzpqT516pVq~8Jlmnsc`@Cp;2hEQ#L}){6S+N_k<;Kfo85uA>a{tTAnGqQ(AjgbH+`hZ zfb@q?8H$)r{zz-J@qS% z9i%hvxX5S94XtjkjvSp425Na?131)Tg6o?E0a0&%>dpHJ;|$)>hTK~W1%nSmeFOWV zaBW@tYVL2~x0zO%H=#p)otoY~@OI=%fbGs$5@9fIfc}*cqFsPnF-aZdZtku{Ax=ym z!8>?=y{U6PDV?LX((b2zywox-gTZ_yW}9-f*tDpAb2>z8ry;htl{r};?EIqz7)||P z##t@yC6{bL^xS^s0bezMpNa*-$*K^oqpQT+z-0Qkpjsn-njICd%x)4zqfV zWofpi^n>6!(yYlP&#=)cOLPOqIA#$|ZHp&$y%u3Mdb3b#QzukEn$BhCk*$#@mNjb{ z@NdaoQF6LS5#Lj*kQDip#vK$TbRY6~lTmDTP-4>NZ9A6PG+w4%G#*K6O6G&=Kf=5{ z_Ez7-StM4rm~e~|RJC*$P358VZ52ZR=WBG!T3JzI?Rgb!e6KWvUy(Mb{^qc5(sSzp z1E0oP?8C14uZ*sxcrrc))mf2vE~CcGpOrtHCZz71&d)}xtQ|}fUOXRW&M>z8^|`Ho zqDb^o^mKRm&hQ)o&Eo0GP31yP{;cYJkB(0R%qq@V;OcOGS^{Zo zc0M7#IX*yYmV07Hw}jmPH{E`-qvW~$Ssz_0`tCHAgS#ngXNWe05Gh3*keDsxDvV9a zEm&t4;($f+DsA1J$`-lg*|axTQ{?bx>k%iZ%Cuv__=7gNZjGh9fMq}B*Smh;x$jBi zF$zorZhxL%0y55iN;CI#x!;x+|%`|f>XW9+8?eK zcQ)ciS-b*eK?Kl#{+F^Kt8y)=_isGVlM5#D+A{Bs+jw?7qi`v0L-Z@&30;W7EEyFO z1^@ljPa-@fXc2Z^uoiKDf&DW6M%3n4S`Fp3%W^(1@V3d0l|h?#3**%I$MVEymx7Qc zEF5&Q<{&r|G?;G5Ea>vz&IoJ4*kAO_1eA%aa+m!1UHZ$5HPo%ZvI(HZ0tt&AXKzpTh}t3&$f-gCM{mGpq>wYfoOpy91moA*)U0e&Ix zhCDx>kuP998cI<#f^!t;C%lW611+&5QZTG*v;wo)rem*oYQVK?(+?G$p} zt2%}^qKOjx49C|)BfC>Y>OiGX8%@VAb^Vc9I|RO zR3{R73S7d0#Fp(*{lfj=pWSYO@V?v*R}%HXF*@6-*jx){~O zUq&bRkR?f8WjZWe^l%2PFecSET7u@aU=~?1CqtoX$_Y^4jTqKLO$n)oi@J+bbtbsF z$TAsH2=c--L{dU-UwWPGzIA&%Ad>`Q8g$uk(M8WB>@?TCH6Bp*-3aN;QA_1#&2*N3 zw{tmbCIYO@Ld>cPi$t!SH&GA&E5RNSC_M!ajhU zCHdt_SPjMtt%h_BFW}8V=Ct*V-AfV}R`qMW2}LJFRs*LwP21-xl~;qBeoLG}_3un# zuoBL&|dHE!`3XqPTk&#|o&jU{IP zbe!f@7y_Piizn(z==blI;&uw&GMKOGZw*=MNY3&@G%oIE*G-6xPR!y`EIH=;?PUFq z=S3Qhh+(=JtdTXJPu%WIDJ}vPkTUJU9g9AK6aY}L54>r-k4kZB>HmI)GH$RAja!>BhQ4jghBzI_7u_R0D*spP@K&Qef zHQk*Pc+W1A<<6iFS1!Uahi3}w)iJO@?|sZB!vRdgN2UDgE=0xVypeAb>)Wa>%AXJ^ z3ZeIDCCJ{J7~cX`O*i6&(C6^bGw|43r{TUz$zJAj)RKQ;=J`GZo46E$^*I*?)yFk{ zrGL>TGT{XwQoI|89M{eo4d!3f%fWZlUlBugx_Sw_y21|@tKlH!+)=aOn-Z^!!Cx#^ z%O0M^`dgb11g*oj&XW}HwV?n>ja_%F+CpwTnZC+MM*3fjNa-f; z{@?FK-*rl28&JvHut|gMj=Q>IfTg~WE0P!uo)jRW4e`0}5BFjd*pS>z^6Rjlg8e4n zaYHHe6!+Svx~8JSp+fb{A{``S{s`8}8$VD<{OEc1u`cN8F`$V3?fwVda)X)+WY5ar z)BxO*>Tj)LS=ap;hZ8$BzlX5X^=N(wL;Gp~A}EJIbr83ti%neiJGE2+4%H4nvI9DC z4q)O18m`}Uc1bu%s~@J-y^rq^o!FgEzDo~ir^`O8^R&6~e?5IXrzi5n|2&Q7$4O#1 zg0^#(I{Uiv3Dby12?qFhOWT58gC!=;`HB4S;kllbxX;_R(-(w?WBC1!4MbfK*@WJv z)zsAjk2i2c_oo_t6R*$9r~tbAcZIjPgh>yzO#wO#*z?U!*+?wWvlX&iM#WAC5la`j zQI=;tzsQxPAFqpA5Sd+e1W94{)LSnjf?JPD?b~VBp$cNnn;c+p}>}=e# zY@rYXZC1SttOn-!cx`cMoWek*3|mhnY1H?ktg%E1MBmlNyuOAm19-oV>a@Mdjgq3v zfaVI>;Uy6e_eMM)dTb(G0~H%2bS#3Whg1-qW?Cg;22G;kLS1X!2tzq5p%JYHQy;j+ zW6B_CzcFZhyXzg1_U7pX>f?yk0}XLaJZdpcThaK>9QwsoMeX_wRe6o^CTQ;Ntfc^y_tDO(toesah$n4s!dQM7) zh9Cea*Xo;;`qOF%Z$<`QOM@?d=tg-G_9f;hE-z9S0YG4WRE*9{(_e&&gy#e)14W8S zwVqrPExycL@>e}%&j0o;+7x&(l2141Y8d&0$&ru&5tn#D#iy;*r0F`%`x7baVy(FR za%L4~QMRs;%WWDPwbq6($ ziu^RMJPt=PiE^7KJln}x0;PtaH%-uAe^2sVJ1!e1a6Q_nytCs!rLLB^E$JaM1K94nQ7Xz~zFqsAo)Z>J@bci&JMC(T^*)VJ<$&Zk8( zEKZj1H##@Zamo21 zZ(tju5~MIfHT0$tpfhue29eW)h&G41tfhzN|COgsE9o!PVK(m4$+U%mZdKTvK!Zk*=xMHsOjjiITh%MKttg~r^cD{(yoEt~f&~v^}mYf$@TAhqtBYEOQAJtGba>qoL+`?lN znOc)trzwljFjelf^w4VJMW>{Lkcac5m400B>g$Dqtz^dUII7Cc8BtRmWR)F=*i3HK zC>>(JBdrIMmA8*~h$BRe)ll|)U03xy3KV7451z*&khr%qo--d6WwEjC+Eu~NT0li} znwy%8`O#O?^9tBCEa+qU~7T)`!#Qzp-w?d_v3YEomjPLkWWmy!dq<$rHq1|(LR&^Bp| z?Tpjbp8J4dNXWxdyM`G-SbCUwE~i0tw?a(n^5&HpM0GD_(0~Op%AMXKK7*(|T8@1z*tz-rHRdnq6jH@Nzd< zCOfdiasP4dN5mj9{vR0`ABr4Fv_f)s5`D<|xX>SY<-%qAALL9w{8iujs$QrSNR4B; z2u3sNjA25cac5aTBkekdI0bkfXGt5KZqW%~MVrKWdwRDz*`;t9x`NgSF3v|bv{<(bh)o4+6SQj)p2n#tg$TV(1TW`bLh|Hr#7u73d+BD&TWs9SaEV# z*HQ6h5^@@h|7)9W#r@%6w#qJ23OZ`Q{H#W@Ahq$7p<~ZOqJEH8Vw7+aTxPM7IUX%E zB>3a0J9YdHpv$xy5673XrwJ0&k&@q%h<|ZRdF!Wr4136#P4v+|fs~Q4rnP^6cj<4W zFfHB{@U>reJiDR1ZoGovBjYE|o;$>3bGkJt#B=0_ZTaSF%Si(aFRO8o}xMlzS zNqU3=mW4t8gGNfxgT_vD4xqt7@u~3wpXnsGgXn~w2jr^j=yLL)2Ch0omA%Rc983*! z^Vm!fSzr?)b&d>GelI}Tip|~JJ(eRfDcm1NTG8n=&(C43%hKKUz3uV#z?p)tS3-eu zC_zTyeOXsJcMi+zX4Vak!vMJ~e$p2%UwmB|MtW6dP9Q?6p3igXVJl90RwappKQYu) zgl17NuoQTPRH6FG&nA6k*3L3kvqU|=0W5~n8hWzh9w~xgCq}p!{byS~bdFE>$D5V~ zF>5J+STG6~)rLW>iPYHRL{@wS`Zi&lQ1CjF|I-8hb4JM+Uex#c{x4omTml4${iH}3 z!4tRfHB|S<8|9qPZd)tO-b_kC`{XQug_QJxz+kdJ-h8j+>H@#aBE#NaI?JaGhVW6#Rv; z36>(UsBK2s8S(`RQZU0oYPRxZJng){)0gN+QF^lY4yC2zZ3a=vA@Qp$>e&Vs}iECco7-MFtj`^Dm>6V4U1unm)zu&sad zinu3!Vpk_aM{-H89S1m3G3=$Tj|r&SWs20IQb{J~k-wj-1@LNhz%z!itn+F+18lZ} z{$SKnhJ-M3BN<@{@-~QYwZ5T1%T+o4nDbnag|`T~C;%AE^WId}liF0XX})Q@;!;ka zADsC34An_t-_TB8h5SWI*3J0*Ii3oGNx+ep@M7FU4pPeM*sDW%X*Dyt=0HPQ^3>z~ znBj^awjD3I;>sVNv4{I+QM6MUIGT4^I0#v-qrX%&R|i4QjLQ$c-OFS?sh$41*y72W zL&6A%F)4=s!#V8%+_8UMt94N$Z&Z`>BE=jCQ#+K+*SwD8yG++~de7cMgQNe0UOXF5 zF_x~`dA8#ZgYf8t5QB4l9#jkk;m6?6n{3BQp{6B|pB4C{cXTM0&2{VMjN3xlFcjFZQVt&`} zg?kJ1U0PcuLaqS%^IZ=+zb%7TX=oW`p~ZZ4Ev99-+(mIy1Uh8}^k7r`$J?K_0|8V7 zkM>fp>D)`G$ud^NwvGdzTU{R3F|xzCe||ipCg|y`CLx>$RkK@-F?hS1X)9Q0c1L|` zUCPz$ce!%NS?9{^`2gL%Dwe?B zCWmS?$XkL5exI39`%aPi6b0+$ShhWQi*wr`;Bw#Lg_X*@TfQ+d1Co(J%!V6W=9U`( zm!0YUa+eIV%gxV4EV@=X3Y5CN-(<7a7{J7M^b?3zO6^%=^Ke3~0z08qS912pqD9Dl+Y?&Xu@K~p_VHVG?F%PB0z zA&+0VE8vR>$FTV9*(1s8e_i}>fym}RCq0Y=K^NY{?kJs3he2ZeN++$bM1XGoGyy1*ZM zDmSHY{kd_AEFNy91^%J^HG9QDvVJ$M`wRnwB!-{Wj&j#MzX#YEM}$6A`0>RxHBifH ziFVJQL9`gQ-~Ty$h&>P78cvycJQv{|z+)(+=5Ah-ZBx+Q40^ox6W+v*W;i$EZ{#}1 zI;VGhB2e6_3lr49P|92GjUMiY%17ZdB%*VXZ)#~6cd z;BwcrCyNOgwXWO!s`9!;ap^5INc%h`c$gsqoFxy)pBND6bcNx08;R{4rwxV^7%UJI z(OcI4dXTN;ibuQ9X|SsE_LY&HKzdosn(xzt`e77k9SLyeja>xWeZsYWJ&I`<2skSx zl6?C5S?u@!vNy_*P8n!Kls@~>WV%%-zdEmOT|S=d_&)7(Hqwy9wb!6cn9y*if;JN;L11i?Ga9gU5%iWvT&9qpgtYHSFEr z9p*x?J4b0z02`(}CT?LNKOp)?jhMNYIz7)RXYRz+vknCUlrk?YT zFX;w#{Wtb=Zc4plBjF0Di=W7p6HwPhS+b)Bh-k@JK`>**K@dTvD9L&r`P>{M1%06J zL5YI2U8y?K_Rt!`X)McgvB}jHZiH4*?L-fKwo_riTxzUmmM;AWgCSMZIfXT)$aB9XH4imdcJ!}Pd8>DT zN5%}AuNEA~0Ts`gm``gTJq2@!)!`2GDeWNUU-1$?Jg3aBKVaq=^qXOw?L1!+5iU3u zG6TqJEN@1oK|9L*0tm^ZWEI*_fKOTwoe)xt$JiuNR0l)ov)bs$YdE!GkbzLykrtoT zJSRrSaJpMk@bSaYIz<}`zka~%yZwh!2JX!>Ko85N7H?cU9ro9J@JF-Bki6_hO*tUM zFWhwh1Tk8>fn&@qtdx&H7TmLm*>(P|96*$oBqymP01H{B<5T(zVv;y z!|EfSxJx%rAkoJNH%;N0^T{rAxI6~D*>zRQ8|P0`oMc@1X#{Tn9ld>ks=)g2#sa@W zz}2BezHgzC&T8CE&o}|l57cdjdO=cz7_~u5?wiYwi&}s70FFg~%SbBsQRJA%zkUn4 zY3Zaf$o>q=akjE95pj)ev>~b)N#WYF0P(&f1r;nrlf1;rUCkbZU=Y z%t+@f(V4N1J%g9Bhi!m`VFM-FUcE5fWZdY4l7w4jnD#Ak)K6#CVk%3*G%h3VhYmKL z9#7uDa2&GyzHgX(SaPJ=N%et$B1UlNYKt9bS^0}`7^xbZ=6kW~oEF*%W3x9&f*f{;jv~6@*wxtZW zQM?ESL(jWUhwLi(2*~6t}wnn&DrJX+EwB*ZU%Ja>8G7vm;}z63mvqBtjwxq z&gxLT_CNfFk+`4@(9lHCM&#WWg@l}==Hr=)9Io#&BgqDCAzFH20R^_+GT%F)9E3q; zo%c376-^38^aGz^<^r1$%D)zVFZN>RFx~0{l1;xUQG{WwRd2VU$0%2WvmPqB;H1R4 z+ApDco5Zc9Hc?)j{?$W5MBe%H8m#Dd<(z1&$NjO%9}sY!yq$g;6^)bdlb-7Ja7i4g=dBbF8`%V?DOy9CSg`afLK&&VkBS9;Rv z=;2{-GDvUN6DEvpt?Zaq>Lc)stY~^ZAFT0jkOc&?*oZ))MW@<7O?KCeO1wm0B~i3A zNbogZ#&nb|FwsKCIL%jDqC1u1KW5kn((+?uU181sycNOd3iXpHOFHJ#O=E-QkIZxh zZzd4veUL+8y3K1LEPau34oQCizffnA&|q|(#~&1)s6VKxdSACsWsT<1>F`Q9inPFe zLUbe}-6HX3|7X?FtzuFEKxRdxw+`ot^Ot(uk3jm^ZbE%E5e3x^{YF@@o0v7?T(t5w zJcg(^V$u04;@ItB0BdGAzgL5XNLYLt8}?OiNyXGrCUK+U6LFVLsoC~P=QFFJ?PiZp z#xtc1caJ_NTc^nx=2$|7HyGCKc;{t`BXi1zcJrQJJe(ol&<#gP;tG4cdco-Hj%)Iu z_52*5zJ=cYcr#KnY5Nov**KOutpP?J^NfA*?zEXmvJH>FO(c`k6axy7%3#=MS`>F8@xMYVUl=LJ|KH2w~7BAv#|Oct)yx( zs(K=u!L;0aW31+v**(^o@onCPD5?&>h}YkTH>w zD^oBS)>yO8W#v@sH+H-`4Rwoh2!bE&Iowdx zDmSpInuXI26X-DBZ_ppmEhD6o)JTQ5PX~qR-u<}?1?lYni4HB>qjJnOV_iw@>Gp#{ zPqC4B!h=hbFawxaO$1&!z7`lNgQZ>%cPllf+OedG=7E@A z36BsP(BO=`bP!~t+4WPTs3iGmeglaef>e`dQQuOnq+Nne606>?>G%^>;hmdXNe2DqPqCz-KyorcwjxMpgV zH&vlz9yifr7T-K1>`+es7a!+h)J^Ocl8lC}5F89w&P>!Ak_X`ej=Tur(R_Vg9|+zG4sAaXRC*sTSx}3DZe98kyTh7an;@&lK}ovLQNJc6 z5Nu{2s1Td$^@z&aVnjWKI6Soh6t>YAdns{)2CFbKkY>Y}vlJb%K28IBu^qqU7uST@ zNB+?l@zAN7!L1y^`SLXTn1K9U=|lx!5eR82gb=)+=K|TX1{};8%WX@_Xnl+6`zL89 zVT%!esYv5sllK*{c+4$%8`U$$oT`o6_gkw3(vuDS8h#D_LeDi-4_oREI{(igN9o!= z68j3o1Nc{NLab)noJT%>Y02MJ?>C%jhpOf@^E;BaSOo4g4yKd5YMQh9);pO zM_N^5K))85{*_gg=xo*B*3xWNWq4kM8PxJ?@--=DzniYV*8=#o z6xh>Ox#)+kod@ZiIjf|&SF{S6#KE6RKI{n`lEtLGh%b*$@!T4dO0XZogo%=@mOO{g(F)LiLP zZI^k^^PH8$Plpg}lr#8{~9$hom*HMV+W= z;)X^FYAi=eT8Avh zvtgU8DYjpZMK31Z?tRn-_=9j@de9}$qj}B^1NwdbpE+d@Dfc&ZdeM^F;C@S?zA=rS zw0LY4eL9WmN-#SOS}z(w!q~d?oAtFAuRy3EqZr-T?03(cyrLWNb8%)3Q~!xZ zzZnC{rBCzm4=rS&BcVO!0*(MIrD=JdMB|_4YZxUKX8)Zgl92DSX2Exe)B0rEeG2Qv zNy|y+-pz@LtQrfPG-zynP19w7q&)Q#rH-nMI)IU6W@#(t5~U^b42wOEc2n)2^W_CI zSf`^rBTbZw0-R7XMifM;AEmX2z*BV>Ka|i82d$r@)`|^UcI4merYO?Xkju_R&DrPD zhH+Mh10GU)gh_Vi=5EZtrr2&Yl|Vk4KC1f(hm`qR(Zxm^?m{vGiE_bA%4vvOIB3)j zyI0h4a~?ll+$mc=?q#H{24IOYb4+5wvWgw+r;+r37myNfyQ4Vhesh*#L`keIJvGKR z)EkLC_VL-nblu~U#KiO3o^gy5R(rWUWz~8{(R4P5EFbc zQzri#Uq#60_fld4Sq}B=7q+poZxUnwnXYkVq?#=H6hbGN3sYNfKsHdh)X25yTD10z zrf3ubAB%A-KHeA>>FuL!&D zG!xHRDjtrZ{)f?rzD%Z11)tajy^Cb_R=8tox;9i4m=+I(+GLVJSXKtsP! z)(?FX+ z`;`#+4}$DP+L}P_4(ntKJYDvjyfCV_fI~SQHz(>xxNS`zz9}d{g>jUrzDlguEHlt}@mP-4$ z%DqHOX}6{!RNpTQ7M-M`71lREwPvktT|l0?8K$nVoPK{@AeD00>~$c_SD;8P%TMzi zG4U|8;6B2f@$NJq1?+FaD+Q{C3BuH!C{;B{f(awf3}ZI<<5kjNh%G#Z88YYTZwVb; z?S-#AU-n?@n*}wDt95J8f27kM=Eg$JdCuJ>VKA2v9S~HTEKgSTL3eI*1csboQ7+EK zCb*EH+YxqM8Cx*@%wc9zg?dN$Ts3z&x`OP@D}uXP#UvWMkq9O3(`$G!69s}E_*X3dBr2%`O5jxu(rc9tvvay)M&3h@q8Xb)km z8yjD|-#O2#pupZ4%$E$9x3bc>s6-70-EXY)S-1ne{X(sm%Zg`*NpsW2JiwUZ^8?HZ zzbAZR6ihJbw{?jx2~?j~os(Z=stim0rV2~v4hlfV7*{wKF9>B}V+{W(0&nK2pep;e zby*Ct5x3pkVl0*)^g!kKDlOrF=frA2J-(}&q7yG(3(d9P9F0T@|1=$0s?6D-v)L11 zKF6?Px9d6R`?A(P?ogZ=2Os3@#Amr*ejWY3WzNN-hvxSv{xIxnHU$yO)!4yR&d~) zOeOm5yRg4l-HBw(`%XZ!d|FL1PfwTtEOoj|3w7UL1(!arDU`GlGG0vOO@{^dYiA5U zOt9qN#My?QQdGANzuVctplOztT*T?gz9|sG8lowA0mL|5^ZqTtKiV&q8w!XEE3YQ~ zL-ylOpy)jCG_Un$Cfa>5m9%2li+zZuolqiexPFw;AaTV6@BglQiH0c{kI9b#%zMA+iSXbIUiPrGX?8=~lW{rn@CAr-2y1$pcRQ(>+aIr~e z#ZOTB+{UZ*!=4kh`&``LHuPVD^ea}1eBAbL^4Kn@Wr@|$3JaEll0ksYGt?`eue5om z;nZz2`&9Wvh3w+%yUxHYYK+nX(_c~<=f@53+#n4tWzNOFF|9SVSRfY&;Ml7Onm8X$ z9uQU1)s*IXCt;nZBB(s{viOaPEoGEdM)YTwSNhzR2b1}Emr{J?taI17bq4b}OO70R zoRcR$wKi;%3MXg>gaLV9M15u}=Y&z<86TN~Kb{W-{{F73NZAkNA@1sf#2cjCQc-Sr zdcP2}{$@=M|K!D)(vs;VK83U5vqEl{=TqJTL;9>3uEtX$6C2`8v2lZ8C2ac{xr1h~@t!m6U!s;Z@ftykS*| zD7(hy;k3%Jx5(A*5*+SkG!YZ14tuZ1898Zzct>u8q{Z_%6)(PGrHnyd~)Jf z`HFvEKA88Tu-aD>jN<&%dw8Feup7%~HJCj_i?9?CgubiYV^Yb{5vZZ?v?Vo06}C*{ zzCNteL}m1Yy=SR)JlpcAa8C-4NI3tjZ<>;CN|i=&WH*sp36)Yv1>?UU8j+2#}{}z9E=Y{J&A`93+}EHb3VTuD&2g3taue zW<5hgVN&~edK0Kw_yREVZg=0&Vvx`4TTD)tp%EMe?+J`OA5v3%3Vf;2qHVmf!SDRw^UMo6L zO~3C9WWdVV6q(3V#-PJ2j>C22j1@HWJ@WdkI{%XS%S(T_XtxH($2xxWP!c|19wt$9O0T!f&ACLe975 zYR|G}88h>O{RfBjBJNtfTbs_;SrgpKrZ54M#kk4%Hp{Y(Zdi$jw6k zUWR%5>wTQ7+$eRyUcA{>#iUa80;!%7|Ka#AH`J6$6uN^H;ZCti(YA6>>XT|1Tdg{* zKN3hbRSA-4+blIfdF#k=#G`Z~$I^^d^r|msr!QseMVPZ;uf3kL z_OeNu5V9k;y_lpgv^?tqZ#<0NqYvpy7^54PD>PS1e56H-`A?@$2lv)!Jgf&lB}5HQbj2AQeFx*9lYnr&FcwG%;1-l~j0 z_Jl_li|3VHQjK))k^H;PxCwP1^HC=`&F5^S5OQfW{0RF~=?a#?JS{XdM7Lc;Fe#e)xkHz0Lw~nl#x*`ce zW}mYi$-gVeN5nXCvo7rR1>G0Rt~=VzT-UjiQWMnF3e5az-fwJj{l+j)$gs@tsSjd> zYsXh?0~Yx8Ksre4e%|NsMf2qF22*wPA1#??*aMKSYT~5Un77`EIE?6!@t)+dPFW-V zqKK|#8|~M3WRWPkGk3A;H-lc=!0rrfteDLj%vDJ@xjCD%DI81+8A{)=d?Id)GN%>5 z=SxS&qso+0j7-QuuH?8lqpOqw4#jWEM6$3a-NwTK2LR$}-FEpL&ZRU0t89HwI7UgH zujP6@!2i%60Ys469l#G9ZUSMrfOn@Gr3*s~O(C3LC$9mvX<7=vV)dm3!D2^IlrBmf zI$PFvw@s_Ti&Bq(!)*?^<@v6`AL9I{Sb47?1BX!orwOrGI1sW5k0i}+3OQJ~JiX|8 zxIL(HAbDl%@FW#{e_j7ymg@+J`e|}#7y2%g8`f>@N~5YF8tR!KN0@ow|k!r#M{9lN$V7d)ir#+j(xAIee@}g#Zvf84r&@^CL zU1XsvpBKmeDbS(KyH7f{m_4*#{87E)lk(ZU%|sf{+H=@&!I zN!NEkloWhzLK}D@a|f~V0HVC)H1IZW#LD=i?~tYU**|@=d8^&kF0=Giz>c1H)uC5z zkaFP&#M3117R*9S$dN4o_HUfz(%tDTYl%G-cUdwJN6!58iS96s4|)RdS$wp?`3x{? z`vM;zulnhc+i|A6rV40))Zgg=Ng&g!j+dMU$$LnHhWDSTX9MjfaA*DRu8x#DFpyt! zkglCA3Q>3bI5U;jNd*V|R)aX=v;2sz(?QT35=f*BPACFbEvg4zu_QfD|uc9u9c-8T%Z&Zgz4mx@r_3jm*XvkKhX4it1xL3-glos^`=ZR^V=p7 z6<)Prom04H>H}Ofe^k;X#w*)mdRsgkd6B%E3^VwiG?N``Vdq5tPptX7@6GBy?0vfE zN}!Qk=p1tGdPsk4DhPR;mztE;1JZyLhNAd!ZR&T8kmgpJMSaa>k+(a(adm0q{TRnC#~a#(Ciy!@uPY|hwV|t zLy8?tL~v*qA0F@y-j&UIBsk8ri|$i7EjDsJxL--!Cp-)SN_F!3twt8V=+R%@xOu-G zaqmw9Ch*IH+4%RDeWz-#ig3NCeWjM`x?vgETGq#a@Ao-J2s<~H zpdYuuU5wz_T>+HTDITlE%0!gvZT3?Pln&jSfAv@&lDnku-G1Wi_5B^#Y60k-Kf>-F zdu4yISl*_UMUV0%{S9Tdj2x_oUUmUf7hXu&HP22ba0q(1e7ia0nk(T~>zoh$;oao2 zJpH7h+W|xlGZhk!>z;N5V8e8OV(V2ijx~w&3NH@+we_W=gWhv0?Q@oh!yqg|sbWY? zi775YLc-yfYZBU)Q6Mjs!(7h^(Z(g5?f3jfJc6dHAA!_#Ljm&kKu$n{RZmxzN7*_h z=VxW>tNm4L@vTvxVs&@ixMDAYHRu`Z^UK{(Lhy(z69>Uk@gE&27Jw%rNJQHMdnxGF z(y{5wu)MGlgjKWR<`D9(1|jpfc(j4MvTk9&0|3=+J57znsyJ|o5r~*{9CEP_nGV0# zOvQ!2W2y*aYb*ViD_oh3$D|0jts*D2%|Jo?QTE-c{ld68A=m2xMw9uTPz9W+ zPr*R6%^L9;o`2xT_{c8a%SS-2rhs3d$i!teE$-U9`So5N3wtbc4LQZ-x#@Bkn zi)ukI|Kif;ymkXW&fBUsE9d`|FHL#Ur~PX->))Lz2 z_I zL$SZiY^+pTeKGNhYO~I=Fk%1iBd!YVa8*x&CRLI|Z;jJjRd|}XYre_eiXY5+F&jPY z083t&?U-Or+xm;dh_ukB=WK zFu;B!vmt^!?VvWOvM!N{j8bAyeLs%U__{5VCSY9);)y)};S~*{TR6JUNhH0u9;O$C zrt&auPCseEpZTGgFbC_@4a}MK7Qw zf+O;+0XL4t*jxXo6$!Idrx}lX?v3Sh-q~7fGz+t%TTrJ6^8t(00kS{6UnB%P$15on zU7~r15_@RJkBTD`!7yd|;L8gElq3AfIe=O|K zUhCNIv1t&f&qpM|7GJ|@nT#>`6d2^JGhbmlym_MqP_{FEcE$$LQtst_t3#grt*D+a zQyHQ50aqPk&Jo4uvxnBH|8B2ot8Ir-XLpxJ*-~YEJN1f@gr3=FqYlzJ*NDnQjT}Pe zFkFTI39luoJ=4b^P!&z2W`sTksV5t(=UvqAuPb+-*%ugh&sJJ}=Ry#>pUv&e8n*B? z{|V8=<9toY+MQf$!5ZhSQ~xKbwhIrNPfkAe!aFb>?XRLb@9>T7(8bt?0)#u%_-m+f z-Qj`g(zvs`E=%Nzzvv4DLE$&4i-5u@EBi`={59U*X9t!u;tz@Wy?I#d_olx)4g}1U zY3py`=jBwf`334S3$)-6`U(6#B@`8aVj^9fN^GLUH5vsogwk(7vu~vMXae`K)SC2U z5Jf?{)d-Y+7@#YZMPxN1e*bjn@+Zv4o{6j?m|9hZ$o}o~+wNmM&XD#Y6fmlD5;Cg0 zb%=Ekt%4E~(+Z3whC8&Kc8sZ*6NR|Di>~>-nT4s1+gL@L_t*yc_QYt5?i#-s;5|LO zDfKT43>}`uzQca9kBU-2YKu-nrAhA8=$308vprriRe&l}n`q%N*&b|nCa7-^UL{|! zNNN=fkJMx?a{KqLcNwN>hP6%Br4YvLi&b%{fLpZe+!+xohzLIPCUyu3W7vKSWLtli zO&9G6I}7dg!vkyOhduN*4#Zd&3=J?wC|$flb7OyiOLpU{fb`g2j5X>nTsJ%%8r04X zce)XwmsKx5_c~tUI6p?~2{mhu_nVf>W65jsglJc>WRVSf9;hE?vgqsSWpWIHaBQZ` z7sSM>35I2ss!}r8jE*jgAux^cm6i~bj~i4rGBLKXx@4B=g5%xX<$F7jufSf{Q@Y~T zwVdUjYfD3`Wh`EpOs0EMnSPaZ6i>hW3XQt44}DS6dvr{Ql19K+lhJF|6jogV+LQDW z{mM@~C5k4dJ5yb*kuIZt2u0R}ZNDI%X9l8iQI!hE?zr5Fe^9MP&<*)*V<;Z#*qDgu zYQWoKdF_Hk6d6KQ+tC$82F1hQ-Y7*_%qd$Rl-uk)acM`7YLh4|6ZFTT&cTfpPsRNz ze)kW5AEK!@jeJeZY5=l!3?h7Ep=gmTE1aa6YdJy_Lx0vfziUPfs~g~N%NzH3XZA7B%Ih~SWihVzax0eg8an5I1?GraAREi1re$90?IGbq?4Of_G}9NIUwD|G`=T5NrW^jk{&lhP@EB&Eu8cOD zfmGt0MDS`l)0*sG*>^O)7%nXaa_b^*mNWRvbtI2t#g;2zoM;*+Nldem{#eLLaZV?* z%^a!u8u(N;*?^*Dq-A0iQLdAuRY*CZWTt>)gIJZnOj$eDO~_fd~@?+(dOC!B4M z>8vBwHJB$UwWD3JL|`VPd+yH=>%XuyU%ksU**{{zHvXWx(}dh19;`gLhnmFEqg(|J zo8ug&^Yza;tp@kho`rFa;W0Dps5iSsfhJS=ccW41Xp_ScG*3Ez=!Ktek9Yjqu-Hn) z9%V(@0~p~@agy`3qT58Pu zKwb2p6YbBR1}fosuW$ofK@?N4Fh%O3EUC+t9V0}&-g}Z0c;XD#i{MZ)fAT_|#K(px(v85Ws*Ed6N|%6K_LDcvb+u;?uiFbfUN> zLZDY}Etm0EIzT*7dYdngAAUv^Y}0St&&S{}SEc+*K=OCfd``J6}H8CR6wPBX;O0H`^$!@|^w-((UWs zOJApYlO@5J5rNv|aqdGl+mGkh!y>scysr;DCQX6fH^#`Y;{z6({y a7a|uL{_R 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 diff --git a/design/assets/make_charts.py b/design/assets/make_charts.py deleted file mode 100644 index a98c0122..00000000 --- a/design/assets/make_charts.py +++ /dev/null @@ -1,268 +0,0 @@ -# Charts for the checkpoint compute-gas accounting design doc. -# Single-hue emphasis scheme: neutral gray for context bars, blue (#2a78d6) for -# the highlighted entity, hatch texture for the pathological case. Recessive -# grid, no top/right spines, direct value labels, text in near-black ink. -# -# Emits every figure in two languages: `checkpoint-figN-*.png` (Chinese, used by -# the design doc) and `checkpoint-figN-*-en.png` (English). -import matplotlib - -matplotlib.use("Agg") -import matplotlib.pyplot as plt -from matplotlib import font_manager - -# CJK font setup (macOS) -for name in ["PingFang SC", "Hiragino Sans GB", "Arial Unicode MS"]: - if any(f.name == name for f in font_manager.fontManager.ttflist): - plt.rcParams["font.sans-serif"] = [name, "DejaVu Sans"] - break -plt.rcParams["axes.unicode_minus"] = False - -BLUE = "#2a78d6" -BLUE_DARK = "#104281" -GRAY = "#b3b1a7" -GRAY_DARK = "#6f6d66" -INK = "#1a1a19" -INK_2 = "#5c5a52" -GRID = "#e6e4dc" - -import os - -OUT = os.path.dirname(os.path.abspath(__file__)) - -TEXT = { - "zh": { - "fig1_ylabel_a": "占全程序 cycles(%)", - "fig1_title_a": "最热的廉价操作码吃掉的周期", - "fig1_parts": ["包装结构\n+ 记账", "单条限额\n检查跳转 jb", "操作码本体\n(其余)"], - "fig1_ylabel_b": "push1 内部占比(Ir / 采样归因)", - "fig1_title_b": "push1 内部:一半以上是计量税", - "fig1_suptitle": "逐操作码计量税解剖 —— 包装成本归零的全程序天花板:5–9% cycles", - "fig2_ylabel_a": "残余包装税(占执行操作码 %)", - "fig2_title_a": "税:粒度越细,重新上税越多", - "fig2_ylabel_b": "最大段长(k gas,主网观测)", - "fig2_title_b": "界:V1 观测值是假象 —— 结构上无界", - "fig2_annot": "对抗形状(纯算术循环)下\nV1 段长 = 剩余 EVM gas,无结构界", - "fig2_annot_xytext": (0.62, 30.5), - "fig2_annot_va": "baseline", - "fig2_suptitle": "检查点粒度两难(主网 1,000 笔轨迹重切):残余税与段长上界不可兼得 —— 除非换执法机制", - "fig3_schemes": [ - "Rex5\n逐操作码(现行)", - "V1 检查点\n(帧末结算)", - "V1.5\n(回跳结算)", - "V0 gas 钳制\n(最终方案)", - ], - "fig3_ylabel": "halt 时已记账 compute(k gas,log)", - "fig3_title": "执法精确性:detention cap 下 26 万 gas 纯算术循环的 halt 落点\n" - "(V1 overshoot 到帧末;V0 停在越限操作码执行前,零 overshoot)", - "fig4_schemes": ["逐操作码\n(改造前)", "V2", "V1.5", "V1", "V0\n(最终)", "原装 revm\n(下界)"], - "fig4_ylabel_a": "热循环用时(ms)", - "fig4_title_a": "解释器热循环(70 万廉价操作码):-50%,落在地板上", - "fig4_labels_b": ["逐操作码\n(rex5)", "V0\n(最终)", "原装 revm\n(下界)"], - "fig4_ylabel_b": "weth9 transfer 用时(μs)", - "fig4_title_b": "真实 ERC20 转账(同 run 对照)", - "fig4_suptitle": "最终效果(本地 wall-clock,同 run 内多方案对照;V0 = 检查点结算 + gas 钳制执法)", - }, - "en": { - "fig1_ylabel_a": "Share of whole-program cycles (%)", - "fig1_title_a": "Cycles consumed by the hottest cheap opcodes", - "fig1_parts": ["Wrapper\n+ accounting", "Per-op limit-check\nbranch (jb)", "Opcode body\n(rest)"], - "fig1_ylabel_b": "Breakdown inside push1 (Ir / sampled attribution)", - "fig1_title_b": "Inside push1: more than half is metering tax", - "fig1_suptitle": "Anatomy of the per-opcode metering tax — whole-program ceiling with wrapper cost at zero: 5–9% of cycles", - "fig2_ylabel_a": "Residual wrapper tax (% of executed opcodes)", - "fig2_title_a": "Tax: finer granularity re-taxes more", - "fig2_ylabel_b": "Max segment length (k gas, mainnet observed)", - "fig2_title_b": "Bound: V1's observed value is an illusion", - "fig2_annot": "Adversarial shape (pure arithmetic loop):\nV1 segment = all remaining EVM gas,\nno structural bound", - "fig2_annot_xytext": (0.55, 33.3), - "fig2_annot_va": "top", - "fig2_suptitle": "Checkpoint granularity dilemma (1,000 mainnet traces): residual tax vs segment bound — unless enforcement changes", - "fig3_schemes": [ - "Rex5\nper-opcode (current)", - "V1 checkpoints\n(frame-end settle)", - "V1.5\n(backward-jump settle)", - "V0 gas clamp\n(final)", - ], - "fig3_ylabel": "Compute recorded at halt (k gas, log)", - "fig3_title": "Enforcement exactness: halt point of a 260k-gas pure arithmetic loop under a detention cap\n" - "(V1 overshoots to frame end; V0 stops before the crossing opcode executes — zero overshoot)", - "fig4_schemes": ["Per-opcode\n(before)", "V2", "V1.5", "V1", "V0\n(final)", "Vanilla revm\n(floor)"], - "fig4_ylabel_a": "Hot-loop time (ms)", - "fig4_title_a": "Interpreter hot loop (700k cheap opcodes): -50%, landing on the floor", - "fig4_labels_b": ["Per-opcode\n(rex5)", "V0\n(final)", "Vanilla revm\n(floor)"], - "fig4_ylabel_b": "weth9 transfer time (μs)", - "fig4_title_b": "Real ERC20 transfer (same-run comparison)", - "fig4_suptitle": "Final effect (local wall-clock, schemes compared within one run; V0 = checkpoint settlement + gas-clamp enforcement)", - }, -} - - -def style_ax(ax, ymax=None): - ax.spines[["top", "right"]].set_visible(False) - ax.spines[["left", "bottom"]].set_color(GRAY) - ax.tick_params(colors=INK_2, labelsize=9) - ax.yaxis.grid(True, color=GRID, linewidth=0.8, zorder=0) - ax.set_axisbelow(True) - if ymax: - ax.set_ylim(0, ymax) - - -def bar_labels(ax, bars, fmt, dy=0.02, fontsize=9): - top = ax.get_ylim()[1] - for b in bars: - ax.text( - b.get_x() + b.get_width() / 2, - b.get_height() + top * dy, - fmt(b.get_height()), - ha="center", - va="bottom", - fontsize=fontsize, - color=INK, - ) - - -def fig1(t, suffix): - # The per-opcode metering tax: where the cycles go. - fig, (a, b) = plt.subplots(1, 2, figsize=(9.2, 3.4), dpi=160) - - ops = ["push1", "add", "pop"] - share = [18.27, 4.79, 4.09] - bars = a.bar(ops, share, width=0.52, color=[BLUE, GRAY, GRAY], zorder=3) - style_ax(a, ymax=22) - bar_labels(a, bars, lambda v: f"{v:.2f}%") - a.set_ylabel(t["fig1_ylabel_a"], fontsize=9, color=INK_2) - a.set_title(t["fig1_title_a"], fontsize=10.5, color=INK, pad=10) - - vals = [27, 25, 48] - colors = [BLUE, BLUE_DARK, GRAY] - bars = b.bar(t["fig1_parts"], vals, width=0.52, color=colors, zorder=3) - style_ax(b, ymax=58) - bar_labels(b, bars, lambda v: f"≈{v:.0f}%") - b.set_ylabel(t["fig1_ylabel_b"], fontsize=9, color=INK_2) - b.set_title(t["fig1_title_b"], fontsize=10.5, color=INK, pad=10) - - fig.suptitle(t["fig1_suptitle"], fontsize=11.5, color=INK, y=1.04) - fig.tight_layout() - fig.savefig(f"{OUT}/checkpoint-fig1-per-opcode-tax{suffix}.png", bbox_inches="tight") - plt.close(fig) - - -def fig2(t, suffix): - # Granularity dilemma: residual tax vs segment bound (mainnet n=1000). - fig, (a, b) = plt.subplots(1, 2, figsize=(9.2, 3.6), dpi=160) - - variants = ["V1", "V1.5", "V2", "V3"] - tax = [0.71, 3.87, 7.80, 14.39] - colors = [BLUE, BLUE, GRAY, GRAY] - bars = a.bar(variants, tax, width=0.5, color=colors, zorder=3) - style_ax(a, ymax=17) - bar_labels(a, bars, lambda v: f"{v:.2f}%") - a.set_ylabel(t["fig2_ylabel_a"], fontsize=9, color=INK_2) - a.set_title(t["fig2_title_a"], fontsize=10.5, color=INK, pad=10) - - seg_max = [27.151, 6.861, 6.772, 6.771] - bars = b.bar(variants, seg_max, width=0.5, color=[GRAY, BLUE, GRAY, GRAY], zorder=3) - bars[0].set_hatch("///") - bars[0].set_edgecolor(GRAY_DARK) - style_ax(b, ymax=34) - bar_labels(b, bars, lambda v: f"{v:,.1f}k") - b.set_ylabel(t["fig2_ylabel_b"], fontsize=9, color=INK_2) - b.set_title(t["fig2_title_b"], fontsize=10.5, color=INK, pad=10) - b.annotate( - t["fig2_annot"], - xy=(0, 27.8), - xytext=t["fig2_annot_xytext"], - va=t["fig2_annot_va"], - fontsize=8.5, - color=INK, - arrowprops=dict(arrowstyle="->", color=GRAY_DARK, lw=0.9), - ) - - fig.suptitle(t["fig2_suptitle"], fontsize=11.5, color=INK, y=1.04) - fig.tight_layout() - fig.savefig(f"{OUT}/checkpoint-fig2-granularity{suffix}.png", bbox_inches="tight") - plt.close(fig) - - -def fig3(t, suffix): - # Enforcement exactness: recorded compute at halt, detention cap scenario. - fig, ax = plt.subplots(figsize=(7.2, 3.6), dpi=160) - - halted_at = [22.0, 281.0, 22.0, 22.0] - colors = [GRAY, GRAY, GRAY, BLUE] - bars = ax.bar(t["fig3_schemes"], halted_at, width=0.5, color=colors, zorder=3) - bars[1].set_hatch("///") - bars[1].set_edgecolor(GRAY_DARK) - ax.set_yscale("log") - ax.set_ylim(10, 700) - ax.spines[["top", "right"]].set_visible(False) - ax.spines[["left", "bottom"]].set_color(GRAY) - ax.tick_params(colors=INK_2, labelsize=9) - ax.yaxis.grid(True, color=GRID, linewidth=0.8, zorder=0) - ax.set_axisbelow(True) - for b_, v in zip(bars, halted_at): - ax.text( - b_.get_x() + b_.get_width() / 2, - v * 1.12, - f"{v:.0f}k", - ha="center", - va="bottom", - fontsize=9.5, - color=INK, - ) - ax.axhline(22.0, color=BLUE_DARK, linestyle="--", linewidth=1.2, zorder=2) - ax.text(3.42, 19.0, "detention cap", fontsize=8.5, color=BLUE_DARK, ha="right") - ax.set_ylabel(t["fig3_ylabel"], fontsize=9, color=INK_2) - ax.set_title(t["fig3_title"], fontsize=10.5, color=INK, pad=10) - fig.tight_layout() - fig.savefig(f"{OUT}/checkpoint-fig3-enforcement{suffix}.png", bbox_inches="tight") - plt.close(fig) - - -def fig4(t, suffix): - # Final effect: wall-clock per scheme. - fig, (a, b) = plt.subplots( - 1, 2, figsize=(9.6, 3.7), dpi=160, gridspec_kw={"width_ratios": [1.35, 1]} - ) - - times = [1.87, 1.09, 1.11, 0.93, 0.93, 0.94] - colors = [GRAY, GRAY, GRAY, GRAY, BLUE, GRAY] - bars = a.bar(t["fig4_schemes"], times, width=0.55, color=colors, zorder=3) - bars[-1].set_alpha(0.45) - style_ax(a, ymax=2.2) - bar_labels(a, bars, lambda v: f"{v:.2f}") - a.set_ylabel(t["fig4_ylabel_a"], fontsize=9, color=INK_2) - a.set_title(t["fig4_title_a"], fontsize=10.5, color=INK, pad=10) - a.annotate( - "-50.3%", - xy=(4, 0.93), - xytext=(2.35, 1.62), - fontsize=11, - color=BLUE_DARK, - fontweight="bold", - arrowprops=dict(arrowstyle="->", color=BLUE_DARK, lw=1.1), - ) - - times_w = [9.40, 9.26, 8.86] - bars = b.bar(t["fig4_labels_b"], times_w, width=0.5, color=[GRAY, BLUE, GRAY], zorder=3) - bars[-1].set_alpha(0.45) - style_ax(b, ymax=11) - bar_labels(b, bars, lambda v: f"{v:.2f}") - b.set_ylabel(t["fig4_ylabel_b"], fontsize=9, color=INK_2) - b.set_title(t["fig4_title_b"], fontsize=10.5, color=INK, pad=10) - - fig.suptitle(t["fig4_suptitle"], fontsize=11.5, color=INK, y=1.04) - fig.tight_layout() - fig.savefig(f"{OUT}/checkpoint-fig4-final-effect{suffix}.png", bbox_inches="tight") - plt.close(fig) - - -for lang, suffix in [("zh", ""), ("en", "-en")]: - t = TEXT[lang] - fig1(t, suffix) - fig2(t, suffix) - fig3(t, suffix) - fig4(t, suffix) - -print("charts written") diff --git a/design/reference-checkpoint_accounting_tests.rs b/design/reference-checkpoint_accounting_tests.rs deleted file mode 100644 index 022a499b..00000000 --- a/design/reference-checkpoint_accounting_tests.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! REX6 checkpoint compute-gas accounting with V0 gas-clamp enforcement (prototype). -//! -//! Plain opcodes run the raw revm instructions with no per-opcode recording; compute gas -//! settles as an interpreter-gas delta at each checkpoint (storage-gas opcodes, CALL/CREATE -//! family, volatile opcodes, `GAS`, frame entry/exit). Enforcement inside plain segments is -//! the V0 gas clamp: the interpreter's visible gas is clamped to the compute headroom, so -//! revm's own per-opcode gas checks stop a crossing opcode at the clamp boundary *before it -//! executes* — zero overshoot. -//! -//! These tests pin the three sides of the design: -//! -//! - **Precision invariant**: non-exceeding transactions produce accounting totals (and receipts) -//! bit-identical to per-opcode recording, including under an active clamp (`GAS` observability). -//! - **Exact enforcement**: a crossing inside a plain segment halts at the crossing opcode with its -//! cost excluded — earlier than per-opcode enforcement, which executes the crossing opcode first. -//! - **Bounded loops**: a detention cap inside a checkpoint-free arithmetic loop is enforced by the -//! clamp, not deferred to frame end. - -use crate::common::{transact, transact_default, CALLER, CONTRACT}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaSpecId, -}; -use revm::bytecode::opcode::{GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP}; - -const ONE_ETH: u128 = 1_000_000_000_000_000_000; - -fn db_with_code(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) -} - -/// Measures the intrinsic compute gas (tx base cost) with a STOP-only contract. -fn intrinsic_compute_gas() -> u64 { - transact_default( - MegaSpecId::REX6, - db_with_code(BytecodeBuilder::default().append(STOP).build()), - ) - .compute_gas -} - -/// A countdown loop of cheap opcodes with no checkpoint inside the loop body: -/// -/// ```text -/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP -/// ``` -/// -/// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and -/// participates in the jump-target offset. -fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { - let mut code = prefix.to_vec(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = code.len() as u8; - code.push(0x5b); // JUMPDEST - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(0x90); // SWAP1 - code.push(0x03); // SUB - code.push(0x80); // DUP1 - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(0x57); // JUMPI - code.push(0x00); // STOP - Bytes::from(code) -} - -/// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then -/// `SSTORE(7, 99)` and STOP. -fn plain_run_then_sstore_code(pairs: usize) -> Bytes { - let mut builder = BytecodeBuilder::default(); - for _ in 0..pairs { - builder = builder.push_number(1u64); - builder = builder.append(POP); - } - builder - .push_u256(U256::from(99u64)) // value - .push_u256(U256::from(7u64)) // slot - .append(SSTORE) - .append(STOP) - .build() -} - -/// Precision invariant on a plain-opcode hot loop: with no limit crossing, checkpoint -/// accounting (REX6) must produce the same compute-gas total and receipt `gas_used` as -/// per-opcode accounting (REX5) — the segment delta telescopes over exactly the opcodes the -/// per-opcode wrappers would have recorded, and the loop's only settlement is the frame-end -/// checkpoint. -#[test] -fn test_checkpoint_plain_loop_totals_match_rex5() { - let code = countdown_loop_code_with_prefix(&[], 500); - let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); - let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); - - assert!(r5.is_success(), "REX5 loop must succeed: {:?}", r5.result); - assert!(r6.is_success(), "REX6 loop must succeed: {:?}", r6.result); - assert_eq!( - r5.compute_gas, r6.compute_gas, - "checkpoint totals must telescope to the per-opcode sum" - ); - assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); -} - -/// `GAS` observability under an active clamp: with a tight detention cap the interpreter's -/// visible gas is clamped for the whole post-access run, yet the value `GAS` pushes must be -/// the true remaining — the checkpoint prologue restores the clamp before the raw -/// instruction reads the counter. The returned word and the receipt must be bit-identical -/// to per-opcode REX5, where no clamp exists at all. -#[test] -fn test_v0_clamp_is_unobservable_via_gas_opcode() { - // TIMESTAMP; POP; GAS; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN - let code = - Bytes::from(vec![TIMESTAMP, POP, GAS, 0x60, 0x00, MSTORE, 0x60, 0x20, 0x60, 0x00, RETURN]); - // Detention cap 1,000: far below the interpreter's remaining gas, so the clamp is - // active at the GAS opcode; the tx itself stays far below the cap (non-exceeding). - let limits = |spec| { - let mut l = EvmTxRuntimeLimits::from_spec(spec); - l.block_env_access_compute_gas_limit = 1_000; - l - }; - - let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); - let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); - - assert!(r5.is_success(), "REX5 must succeed: {:?}", r5.result); - assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); - assert_eq!( - r5.result.output(), - r6.result.output(), - "GAS must push the true remaining under the clamp" - ); - assert_eq!(r5.compute_gas, r6.compute_gas, "compute totals must be identical"); - assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be identical"); -} - -/// V0 exact enforcement in a plain segment: the limit is placed mid-way through a straight -/// plain-opcode run. REX5 (per-opcode) executes the crossing opcode and then halts, so its -/// recorded usage exceeds the limit; REX6 (clamp) fake-OOGs the crossing opcode at the -/// clamp boundary *before executing it*, so its recorded usage stays at or below the limit -/// and the halt lands one opcode earlier — zero overshoot. -#[test] -fn test_v0_clamp_halts_at_crossing_opcode_without_executing_it() { - let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes - let intrinsic = intrinsic_compute_gas(); - - // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. - let compute_limit = intrinsic + 300; - let limits = - |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); - - let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); - let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); - - assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); - assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); - - // Per-opcode enforcement records the crossing opcode: usage strictly exceeds the limit. - assert!( - r5.compute_gas > compute_limit, - "REX5 executes the crossing opcode before halting; compute={} limit={compute_limit}", - r5.compute_gas - ); - // Clamp enforcement stops the crossing opcode before execution: usage stays at or - // below the limit. - assert!( - r6.compute_gas <= compute_limit, - "REX6 must not execute the crossing opcode; compute={} limit={compute_limit}", - r6.compute_gas - ); - assert!( - r6.compute_gas > compute_limit - 12, - "REX6 must stop at the clamp boundary, not earlier; compute={} limit={compute_limit}", - r6.compute_gas - ); -} - -/// V0 bounds detention inside a checkpoint-free plain loop — the shape that broke V1 -/// (which deferred enforcement to frame end, overshooting by the whole loop). The clamp -/// stops the loop at the cap boundary regardless of the absence of checkpoints. -#[test] -fn test_v0_clamp_bounds_detention_inside_plain_loop() { - // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a - // 10,000-iteration countdown loop (~260k gas) with no checkpoint inside. - let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); - let intrinsic = intrinsic_compute_gas(); - - let limits = |spec| { - let mut l = EvmTxRuntimeLimits::from_spec(spec); - l.block_env_access_compute_gas_limit = 1_000; - l - }; - - let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); - let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); - - assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); - assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); - - // The detained limit is usage-at-access + 1,000 ≈ intrinsic + TIMESTAMP + 1,000. - let detained_limit_upper = intrinsic + 2 + 1_000; - // Per-opcode enforcement executes the crossing opcode: usage strictly exceeds the cap. - assert!( - r5.compute_gas > detained_limit_upper - 12, - "REX5 must stop near the detention cap; compute={}", - r5.compute_gas - ); - // Clamp enforcement stops at the boundary without executing the crossing opcode, - // despite the loop containing no checkpoint at all. - assert!( - r6.compute_gas <= detained_limit_upper, - "REX6 must not overshoot the detention cap; compute={} cap≈{detained_limit_upper}", - r6.compute_gas - ); - assert!( - r6.compute_gas > detained_limit_upper - 30, - "REX6 must stop at the clamp boundary, not earlier; compute={}", - r6.compute_gas - ); - - // Attribution parity: a clamp-stopped detention exceed must classify as - // `VolatileDataAccessOutOfGas` exactly like per-opcode enforcement, even though the - // recorded usage never crossed the detained limit (the crossing opcode was stopped - // before executing). - let halt_reason = |label: &str, r: &crate::common::Outcome| match &r.result { - revm::context::result::ExecutionResult::Halt { reason, .. } => format!("{reason:?}"), - other => panic!("{label}: expected a halt, got {other:?}"), - }; - let r5_reason = halt_reason("REX5", &r5); - let r6_reason = halt_reason("REX6", &r6); - assert!( - r5_reason.starts_with("VolatileDataAccessOutOfGas"), - "REX5 must attribute the halt to volatile detention; got {r5_reason}" - ); - assert!( - r6_reason.starts_with("VolatileDataAccessOutOfGas"), - "REX6 clamp halt must keep the volatile detention attribution; got {r6_reason}" - ); -} diff --git a/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch b/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch deleted file mode 100644 index 7e774c5b..00000000 --- a/design/reference-patches/0001-bench-add-cheap-opcode-interpreter-hotloop-workload-.patch +++ /dev/null @@ -1,74 +0,0 @@ -From a0115b1da6334f7e353a68cf50d049e48cac2b92 Mon Sep 17 00:00:00 2001 -From: RealiCZ -Date: Fri, 24 Jul 2026 18:10:25 +0800 -Subject: [PATCH] bench: add cheap-opcode interpreter hotloop workload to - transact - ---- - crates/mega-evm/benches/transact.rs | 47 ++++++++++++++++++++++++++++- - 1 file changed, 46 insertions(+), 1 deletion(-) - -diff --git a/crates/mega-evm/benches/transact.rs b/crates/mega-evm/benches/transact.rs -index 6636c33..1d4011d 100644 ---- a/crates/mega-evm/benches/transact.rs -+++ b/crates/mega-evm/benches/transact.rs -@@ -87,10 +87,55 @@ fn bench_weth9_transfer(c: &mut Criterion) { - group.finish(); - } - -+/// Builds a tight countdown loop of cheap opcodes: -+/// -+/// ```text -+/// PUSH3 iterations -+/// loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI -+/// STOP -+/// ``` -+/// -+/// Each iteration executes 7 opcodes for 26 gas (JUMPDEST 1 + PUSH1 3 + SWAP1 3 + -+/// SUB 3 + DUP1 3 + PUSH1 3 + JUMPI 10), all from the cheap-opcode family that -+/// dominates real interpreter workloads. -+fn hotloop_code(iterations: u32) -> Bytes { -+ let mut code = Vec::with_capacity(14); -+ // PUSH3 -+ code.push(0x62); -+ code.extend_from_slice(&iterations.to_be_bytes()[1..4]); -+ // loop target is the JUMPDEST right after the initial PUSH3 (offset 4). -+ let loop_target = code.len() as u8; -+ code.push(0x5b); // JUMPDEST -+ code.push(0x60); // PUSH1 -+ code.push(0x01); -+ code.push(0x90); // SWAP1 -+ code.push(0x03); // SUB -+ code.push(0x80); // DUP1 -+ code.push(0x60); // PUSH1 -+ code.push(loop_target); -+ code.push(0x57); // JUMPI -+ code.push(0x00); // STOP -+ Bytes::from(code) -+} -+ -+/// Benchmark a cheap-opcode-dense interpreter hot loop (~700k executed opcodes, -+/// ~2.6M gas), the workload shape where per-opcode gas-accounting overhead is -+/// the dominant tax. -+fn bench_interpreter_hotloop(c: &mut Criterion) { -+ let mut group = c.benchmark_group("interpreter_hotloop"); -+ let workload = Workload::single( -+ vec![Account::new(CALLEE).code(hotloop_code(100_000))], -+ TxSpec::call(CALLER, CALLEE), -+ ); -+ register_all(&mut group, &workload); -+ group.finish(); -+} -+ - criterion_group!( - benches, - bench_empty_transaction, - bench_simple_ether_transfer, -- bench_weth9_transfer -+ bench_weth9_transfer, -+ bench_interpreter_hotloop - ); - criterion_main!(benches); --- -2.50.1 (Apple Git-155) - diff --git a/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch b/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch deleted file mode 100644 index fa646334..00000000 --- a/design/reference-patches/0001-feat-rex6-V0-gas-clamp-enforcement-for-checkpoint-ac.patch +++ /dev/null @@ -1,1021 +0,0 @@ -From e1a2bde50aa6ccd8ce830b9626eaab087d914a04 Mon Sep 17 00:00:00 2001 -From: RealiCZ -Date: Fri, 24 Jul 2026 19:13:27 +0800 -Subject: [PATCH] feat(rex6): V0 gas-clamp enforcement for checkpoint - accounting - -Enforcement inside plain-opcode segments moves from checkpoint-deferred -settlement to the V0 gas clamp: at every checkpoint and frame entry/resume -the interpreter's visible gas is clamped to the compute headroom (the -tighter of the frame-local budget and the TX-level detained limit), so -revm's own per-opcode gas checks stop a crossing opcode at the clamp -boundary before it executes -- zero overshoot, zero per-opcode overhead. - -Checkpoint handlers gain a prologue (settle the open segment, restore the -clamp so CALL forwarding, GAS, and storage charges observe the true -counter) and an epilogue (re-clamp against the possibly-detained -headroom). GAS joins the checkpoint set so clamping is unobservable for -non-exceeding transactions. A clamp-induced fake OOG is restored and -reclassified at frame end as the corresponding limit exceed, preserving -the volatile-detention halt attribution. - -Non-exceeding transactions remain bit-identical to per-opcode accounting; -a limit-exceeding transaction now halts at the crossing opcode before it -executes, with that opcode's cost excluded from recorded usage. Specs -<= REX5 are byte-for-byte unchanged. ---- - crates/mega-evm/src/evm/execution.rs | 6 +- - crates/mega-evm/src/evm/instructions.rs | 255 ++++++++++++------ - crates/mega-evm/src/limit/compute_gas.rs | 25 ++ - crates/mega-evm/src/limit/limit.rs | 167 ++++++++++-- - .../tests/rex6/checkpoint_accounting.rs | 195 +++++++++----- - 5 files changed, 464 insertions(+), 184 deletions(-) - -diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs -index a34c6ea..d415477 100644 ---- a/crates/mega-evm/src/evm/execution.rs -+++ b/crates/mega-evm/src/evm/execution.rs -@@ -417,7 +417,7 @@ impl MegaEvm { - #[inline] - fn before_frame_run( - ctx: &MegaContext, -- frame: &EthFrame, -+ frame: &mut EthFrame, - ) -> Result, ContextDbError>> { - // Check if the additional limit is already exceeded, if so, we should immediately stop - // and synthesize an interpreter action. -@@ -454,6 +454,10 @@ impl MegaEvm { - let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); - - if let InterpreterAction::Return(interpreter_result) = action { -+ // REX6 V0 clamp: restore any hidden gas into the action's copy (and latch a -+ // clamp-induced fake OOG) before the code-deposit charge below observes it. -+ ctx.additional_limit.borrow_mut().restore_clamp_into_result(interpreter_result); -+ - // Charge storage gas cost for the number of bytes - if frame.data.is_create() && interpreter_result.is_ok() { - let code_deposit_storage_gas = constants::mini_rex::CODEDEPOSIT_STORAGE_GAS * -diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs -index f15fee4..be5e121 100644 ---- a/crates/mega-evm/src/evm/instructions.rs -+++ b/crates/mega-evm/src/evm/instructions.rs -@@ -380,24 +380,33 @@ mod rex6 { - use super::*; - - /// Returns the instruction table for the `REX6` spec — **checkpoint compute-gas -- /// accounting** (prototype). -+ /// accounting with V0 gas-clamp enforcement** (prototype). - /// - /// Unlike every earlier custom table, plain opcodes are wired to the raw revm mainnet - /// instructions with no per-opcode gas recording: the interpreter's own gas counter is - /// the accounting source, and compute gas settles as a segment delta at each -- /// checkpoint. The checkpoints are exactly the positions that had to stay wrapped -- /// anyway: -+ /// checkpoint. The checkpoints are the positions that had to stay wrapped anyway, plus -+ /// `GAS`: - /// - storage-gas opcodes (SSTORE, LOG0–LOG4, SELFDESTRUCT) and the CALL/CREATE family — their -- /// existing handler chains settle from the checkpoint baseline inside -- /// [`record_storage_compute_gas!`] (spec-dispatched, so ≤REX5 tables are untouched); -- /// - volatile / detention opcodes — `*_checkpoint` variants that run the raw instruction, -- /// settle the segment, then apply the detention cap; -- /// - frame entry/exit — `AdditionalLimit::before_frame_run` re-opens the window and -- /// `after_frame_run_instructions` settles the tail segment. -+ /// existing handler chains gain a [`checkpoint_prologue!`] (spec-dispatched, so ≤REX5 tables -+ /// are untouched); -+ /// - volatile / detention opcodes — `*_checkpoint` variants; -+ /// - `GAS` — [`compute_gas_ext::gas_checkpoint`], so the clamp is restored before the counter -+ /// is observed; -+ /// - frame entry/exit — `AdditionalLimit::before_frame_run` clamps and re-opens the window; the -+ /// frame-end hooks settle the tail and restore the clamp into the frame result. - /// -- /// Accounting totals telescope to the same per-transaction sums as per-opcode -- /// recording; only the halt position for limit-exceeding transactions coarsens to the -- /// next checkpoint. -+ /// **Enforcement (V0)**: at every checkpoint and frame entry/resume the interpreter's -+ /// visible gas is clamped to the compute headroom (the tighter of the frame-local -+ /// budget and the TX-level detained limit), so revm's own per-opcode gas checks -+ /// enforce the limits inside plain segments with zero per-opcode overhead and zero -+ /// overshoot: a crossing opcode fake-OOGs at the clamp boundary before executing and -+ /// is reclassified as the corresponding limit exceed at frame end. -+ /// -+ /// Accounting totals for non-exceeding transactions are bit-identical to per-opcode -+ /// recording (plain segments telescope; checkpoint bodies are recorded per-opcode); a -+ /// limit-exceeding transaction halts at the crossing opcode *before* it executes, with -+ /// that opcode's cost excluded from the recorded usage. - /// - /// The pre-existing REX6 behavior differences (canonical metering order, CREATE - /// `create_rex6` dispatch, SELFDESTRUCT existing-target accounting, CALL-family -@@ -430,6 +439,10 @@ mod rex6 { - table[SELFBALANCE as usize] = volatile_data_ext::selfbalance_checkpoint; - table[SLOAD as usize] = volatile_data_ext::sload_checkpoint; - -+ // V0 gas-clamp enforcement: GAS must be a checkpoint so the clamp is restored -+ // before the counter is observed. -+ table[GAS as usize] = compute_gas_ext::gas_checkpoint; -+ - // Storage-gas checkpoints — the same handler chains as REX5; under REX6 they - // settle from the checkpoint baseline internally. - table[SSTORE as usize] = additional_limit_ext::sstore; -@@ -480,6 +493,82 @@ macro_rules! run_inner_instruction_or_abort { - }; - } - -+/// REX6 checkpoint prologue. Runs at the top of every checkpoint handler, before any gas -+/// capture or gas-consuming work: -+/// -+/// 1. Settles the open plain-opcode segment — `baseline − remaining`, both on the clamped counter, -+/// telescoping exactly over the unwrapped opcodes since the last checkpoint. -+/// 2. Restores the clamp-hidden gas, so the checkpoint's body runs on the **true** counter: -+/// CALL-family forwarding math, the `GAS` opcode's pushed value, and storage-gas charges all -+/// observe real gas, keeping the clamp unobservable for non-exceeding transactions. -+/// 3. Re-opens the settlement window at the restored counter. -+/// -+/// Halts (returning from the enclosing handler) when the settlement surfaces a limit -+/// exceed — including one latched earlier by a non-compute mutation site. The restore has -+/// already happened on that path, so the frame result carries true gas. No-op before REX6. -+macro_rules! checkpoint_prologue { -+ ($context:expr $(,$ret:expr)?) => { -+ if $context.host.spec_id().is_enabled(MegaSpecId::REX6) { -+ let exceeding_result = { -+ let mut additional_limit = $context.host.additional_limit().borrow_mut(); -+ let remaining = $context.interpreter.gas.remaining(); -+ let segment = additional_limit.checkpoint_baseline().saturating_sub(remaining); -+ let hidden = additional_limit.checkpoint_restore_hidden(); -+ $context.interpreter.gas.erase_cost(hidden); -+ additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); -+ if additional_limit.record_compute_gas(segment) { -+ None -+ } else { -+ Some(additional_limit.exceeding_instruction_result()) -+ } -+ }; -+ if let Some(result) = exceeding_result { -+ $context.interpreter.halt(result); -+ return $($ret)?; -+ } -+ } -+ }; -+} -+ -+/// REX6 checkpoint epilogue for checkpoints whose frame keeps executing (SSTORE, LOG, -+/// volatile opcodes, `GAS`): re-applies the V0 gas clamp from the freshly settled usage — -+/// including any detention cap the checkpoint just installed — and re-opens the settlement -+/// window on the clamped counter. CALL/CREATE checkpoints skip this (the frame suspends -+/// and `before_frame_run` re-clamps on resume), as do frame-ending opcodes (the frame-end -+/// settlement restores instead). No-op before REX6. -+macro_rules! checkpoint_epilogue { -+ ($context:expr) => { -+ if $context.host.spec_id().is_enabled(MegaSpecId::REX6) { -+ let mut additional_limit = $context.host.additional_limit().borrow_mut(); -+ let hide = -+ additional_limit.checkpoint_clamp_amount($context.interpreter.gas.remaining()); -+ if hide > 0 { -+ let clamped = $context.interpreter.gas.record_cost(hide); -+ debug_assert!(clamped, "clamp amount exceeds remaining gas"); -+ } -+ additional_limit.sync_checkpoint_baseline($context.interpreter.gas.remaining()); -+ } -+ }; -+} -+ -+/// Records a checkpoint opcode's own body gas (`$gas_before − remaining`) and re-opens the -+/// settlement window, enforcing the compute-gas limit exactly as the per-opcode wrappers -+/// did. Used by the REX6 checkpoint handlers for bodies that can never spawn a child frame -+/// (volatile opcodes, SLOAD, SELFBALANCE, `GAS`); the CALL/CREATE and storage-gas bodies -+/// use [`record_storage_compute_gas!`], which additionally excludes storage charges and -+/// forwarded child gas. -+macro_rules! record_checkpoint_body_compute_gas { -+ ($context:expr, $gas_before:expr) => { -+ let gas_after = $context.interpreter.gas.remaining(); -+ let gas_used = $gas_before.saturating_sub(gas_after); -+ { -+ let mut additional_limit = $context.host.additional_limit().borrow_mut(); -+ additional_limit.sync_checkpoint_baseline(gas_after); -+ compute_gas!($context.interpreter, additional_limit, gas_used); -+ } -+ }; -+} -+ - /// Records an opcode's compute gas in a single measurement window and enforces the compute-gas - /// limit. The REX6 storage-affecting handlers invoke it directly with the storage gas they - /// charged; plain opcodes use the leaner inline recording in -@@ -509,18 +598,12 @@ macro_rules! record_storage_compute_gas { - ($context:expr, $gas_before:expr, $storage_charged:expr) => {{ - let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); - let gas_after = $context.interpreter.gas.remaining(); -- // REX6 checkpoint accounting: the measurement window opens at the last checkpoint -- // (frame entry / resume or the previous checkpoint opcode), not at this opcode's own -- // start, so the unwrapped plain opcodes executed since then settle here in the same -- // recording. Only plain opcodes can run between two checkpoints, so no storage gas -- // or forwarded child gas is hiding in the extra window — the exclusions below stay -- // exact. Pre-REX6 keeps the per-opcode `$gas_before` window byte-for-byte. -- let window_start = if is_rex6 { -- $context.host.additional_limit().borrow().checkpoint_baseline() -- } else { -- $gas_before -- }; -- let mut gas_used = window_start.saturating_sub(gas_after).saturating_sub($storage_charged); -+ // The per-opcode `$gas_before` window applies on every spec. Under REX6 checkpoint -+ // accounting the plain segment preceding this opcode was already settled by -+ // [`checkpoint_prologue!`], which also restored the gas clamp, so `$gas_before` -+ // (captured after the prologue) lives on the true counter and the recorded amount -+ // is byte-identical to the pre-checkpoint per-opcode recording. -+ let mut gas_used = $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged); - // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` - // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the - // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit -@@ -1328,38 +1411,14 @@ pub mod volatile_data_ext { - - /* REX6 checkpoint-accounting volatile handlers. - -- Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints), -- but they run the raw revm instruction and settle the open segment — everything since -- the last checkpoint, measured on the interpreter's own gas counter — in one recording, -- instead of delegating to a per-opcode `compute_gas_ext` wrapper. The settlement runs -- before `apply_compute_gas_limit!` so a REX4+ relative detention cap is derived from the -- fully settled usage at the access point, exactly as the per-opcode order did. */ -- -- /// Settles the open checkpoint segment against the interpreter gas counter, re-opens -- /// the window, and halts (returning from the enclosing handler) when a limit — -- /// including a latched non-compute exceed — surfaces. -- macro_rules! settle_checkpoint_compute_gas { -- ($context:expr) => { -- let exceeding_result = { -- let gas_after = $context.interpreter.gas.remaining(); -- let mut additional_limit = $context.host.additional_limit().borrow_mut(); -- let gas_used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); -- additional_limit.sync_checkpoint_baseline(gas_after); -- if additional_limit.record_compute_gas(gas_used) { -- None -- } else { -- Some(additional_limit.exceeding_instruction_result()) -- } -- }; -- if let Some(result) = exceeding_result { -- $context.interpreter.halt(result); -- return; -- } -- }; -- } -+ Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints): -+ the prologue settles the open plain segment and restores the gas clamp, the raw revm -+ instruction runs on the true counter, the body's own gas is recorded per-opcode, the -+ detention cap is applied from the fully settled usage (exactly as the per-opcode order -+ did), and the epilogue re-clamps against the possibly-lowered headroom. */ - -- /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, raw -- /// revm instruction, segment settlement, detention cap. -+ /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, -+ /// prologue, raw revm instruction, body record, detention cap, epilogue. - macro_rules! wrap_checkpoint_detain_gas_unconditional { - ($fn_name:ident, $opcode_name:expr, $original_fn:path, $access_type:expr) => { - #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] -@@ -1378,15 +1437,18 @@ pub mod volatile_data_ext { - return; - } - -+ checkpoint_prologue!(context); -+ let gas_before = context.interpreter.gas.remaining(); - run_inner_instruction_or_abort!($original_fn, context); -- settle_checkpoint_compute_gas!(context); -+ record_checkpoint_body_compute_gas!(context, gas_before); - apply_compute_gas_limit!(context); -+ checkpoint_epilogue!(context); - } - }; - } - -- /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw -- /// revm instruction, segment settlement, detention cap. -+ /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, -+ /// prologue, raw revm instruction, body record, detention cap, epilogue. - macro_rules! wrap_checkpoint_detain_gas_conditional { - ($fn_name:ident, $opcode_name:expr, $original_fn:path) => { - #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] -@@ -1412,9 +1474,12 @@ pub mod volatile_data_ext { - } - } - -+ checkpoint_prologue!(context); -+ let gas_before = context.interpreter.gas.remaining(); - run_inner_instruction_or_abort!($original_fn, context); -- settle_checkpoint_compute_gas!(context); -+ record_checkpoint_body_compute_gas!(context, gas_before); - apply_compute_gas_limit!(context); -+ checkpoint_epilogue!(context); - } - }; - } -@@ -1511,9 +1576,12 @@ pub mod volatile_data_ext { - return; - } - -+ checkpoint_prologue!(context); -+ let gas_before = context.interpreter.gas.remaining(); - run_inner_instruction_or_abort!(instructions::host::sload, context); -- settle_checkpoint_compute_gas!(context); -+ record_checkpoint_body_compute_gas!(context, gas_before); - apply_compute_gas_limit!(context); -+ checkpoint_epilogue!(context); - } - - /// `SELFBALANCE` as a REX6 checkpoint. Same beneficiary-volatile handling as -@@ -1534,9 +1602,12 @@ pub mod volatile_data_ext { - return; - } - -+ checkpoint_prologue!(context); -+ let gas_before = context.interpreter.gas.remaining(); - run_inner_instruction_or_abort!(instructions::host::selfbalance, context); -- settle_checkpoint_compute_gas!(context); -+ record_checkpoint_body_compute_gas!(context, gas_before); - apply_compute_gas_limit!(context); -+ checkpoint_epilogue!(context); - } - } - -@@ -1709,6 +1780,9 @@ pub mod storage_gas_ext { - >( - context: InstructionContext<'_, H, WIRE>, - ) { -+ // REX6: settle the open segment and restore the clamp before any gas -+ // observation, so the forwarding math below sees the true counter. -+ checkpoint_prologue!(context); - // Captured at the very top so the single compute window covers all of the - // opcode's compute work. - let gas_before = context.interpreter.gas.remaining(); -@@ -2041,6 +2115,10 @@ pub mod storage_gas_ext { - >( - mut context: InstructionContext<'_, H, WIRE>, - ) { -+ // Settle the open segment and restore the clamp before any gas observation or the -+ // CREATE2 memory-expansion work below. -+ checkpoint_prologue!(context); -+ - // Canonical revm's `create` runs `require_non_staticcall!` before any operand read, - // memory work, address derivation, or storage-gas charge, so a static-frame - // `CREATE`/`CREATE2` halts here first. This unifies the halt reasons the prework below -@@ -2118,6 +2196,8 @@ pub mod storage_gas_ext { - >( - context: InstructionContext<'_, H, WIRE>, - ) { -+ // REX6: settle the open segment and restore the clamp before any gas observation. -+ checkpoint_prologue!(context); - // Captured at the very top so the single compute window covers the inner opcode. - let gas_before = context.interpreter.gas.remaining(); - let Some(len) = context.interpreter.stack.inspect::<1>() else { -@@ -2155,6 +2235,7 @@ pub mod storage_gas_ext { - // generic `instructions::host::log::` covers every valid call site. - run_inner_instruction_or_abort!(instructions::host::log::, context); - record_storage_compute_gas!(context, gas_before, storage_charged); -+ checkpoint_epilogue!(context); - } - - /// `SSTORE` opcode implementation modified from `revm` with compute gas tracking and -@@ -2178,6 +2259,8 @@ pub mod storage_gas_ext { - >( - context: InstructionContext<'_, H, WIRE>, - ) { -+ // REX6: settle the open segment and restore the clamp before any gas observation. -+ checkpoint_prologue!(context); - // Captured at the very top so the single compute window covers the inner opcode. - let gas_before = context.interpreter.gas.remaining(); - // The address to the underlying execution contract state -@@ -2229,6 +2312,7 @@ pub mod storage_gas_ext { - // EVM gas. - run_inner_instruction_or_abort!(instructions::host::sstore, context); - record_storage_compute_gas!(context, gas_before, storage_charged); -+ checkpoint_epilogue!(context); - } - - /// `SELFDESTRUCT` opcode implementation with storage gas metering for -@@ -2257,6 +2341,10 @@ pub mod storage_gas_ext { - >( - context: InstructionContext<'_, H, WIRE>, - ) { -+ // REX6: settle the open segment and restore the clamp before any gas observation -+ // (the storage charge below and the inner opcode both run on the true counter). -+ checkpoint_prologue!(context); -+ - // Inside a static frame, revm's inner SELFDESTRUCT halts on the - // static-context check without changing state. Skip the mega host work below - // (two account inspections, SALT account-creation pricing, the storage-gas -@@ -2305,20 +2393,7 @@ pub mod storage_gas_ext { - }; - let drained = - context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); -- let storage_charged = cost - drained; -- gas!(context.interpreter, storage_charged); -- -- // REX6 checkpoint accounting: this storage debit sits inside the window that -- // `compute_gas_ext::selfdestruct` closes later, so exclude it by lowering the -- // baseline now — the trailing settlement takes `baseline - remaining` with no -- // storage term of its own. -- if context.host.spec_id().is_enabled(MegaSpecId::REX6) { -- context -- .host -- .additional_limit() -- .borrow_mut() -- .deduct_checkpoint_baseline(storage_charged); -- } -+ gas!(context.interpreter, cost - drained); - - // Record resource usage for new beneficiary account - context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); -@@ -2587,17 +2662,14 @@ pub mod compute_gas_ext { - // Call the original instruction - run_inner_instruction_or_abort!(instructions::host::selfdestruct, context); - -- // REX6 checkpoint accounting: the window opens at the checkpoint baseline, folding -- // in the unwrapped plain opcodes since the last checkpoint. The beneficiary-creation -- // storage charge in `storage_gas_ext::selfdestruct` already lowered the baseline by -- // the charged amount, so no storage exclusion is needed here. Pre-REX6 keeps the -- // per-opcode `gas_before` window. -+ // The per-opcode `gas_before` window applies on every spec. Under REX6 the plain -+ // segment before this opcode was settled by the `checkpoint_prologue!` in -+ // `storage_gas_ext::selfdestruct` (which also restored the clamp), and the window -+ // is re-opened here so the frame-end settlement does not recount the body. - let is_rex6 = context.host.spec_id().is_enabled(MegaSpecId::REX6); - let gas_after = context.interpreter.gas.remaining(); - let mut additional_limit = context.host.additional_limit().borrow_mut(); -- let window_start = -- if is_rex6 { additional_limit.checkpoint_baseline() } else { gas_before }; -- let gas_used = window_start.saturating_sub(gas_after); -+ let gas_used = gas_before.saturating_sub(gas_after); - if is_rex6 { - additional_limit.sync_checkpoint_baseline(gas_after); - } -@@ -2605,6 +2677,23 @@ pub mod compute_gas_ext { - context.interpreter.halt(additional_limit.exceeding_instruction_result()); - } - } -+ -+ /// `GAS` opcode as a REX6 checkpoint. -+ /// -+ /// `GAS` must be a checkpoint under V0 gas-clamp enforcement: the prologue restores -+ /// the clamp-hidden gas before the raw instruction reads the counter, so the pushed -+ /// value equals the true remaining and clamping stays unobservable for transactions -+ /// that never exceed a limit. -+ #[inline] -+ pub fn gas_checkpoint( -+ context: InstructionContext<'_, H, WIRE>, -+ ) { -+ checkpoint_prologue!(context); -+ let gas_before = context.interpreter.gas.remaining(); -+ run_inner_instruction_or_abort!(instructions::system::gas, context); -+ record_checkpoint_body_compute_gas!(context, gas_before); -+ checkpoint_epilogue!(context); -+ } - } - - /// Trait to inspect the stack elements. -diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs -index d37e27c..ce3c2ee 100644 ---- a/crates/mega-evm/src/limit/compute_gas.rs -+++ b/crates/mega-evm/src/limit/compute_gas.rs -@@ -110,6 +110,31 @@ impl ComputeGasTracker { - self.detained_limit - } - -+ /// Returns the base (undetained) TX compute-gas limit. -+ pub(crate) fn base_tx_limit(&self) -> u64 { -+ self.frame_tracker.tx_limit() -+ } -+ -+ /// Returns the compute-gas headroom the V0 gas clamp may leave visible to the -+ /// interpreter, and whether the binding constraint is the frame-local budget (`true`) -+ /// or the TX-level (detained) limit (`false`). -+ /// -+ /// The headroom is the tighter of the current frame's remaining compute budget -+ /// (Rex4+) and the TX-level remaining under the effective (possibly detained) limit — -+ /// the same pair `check_limit` enforces, so gas hidden beyond this headroom can only -+ /// be reached by a transaction that would exceed a limit. -+ #[inline] -+ pub(crate) fn clamp_headroom(&self) -> (u64, bool) { -+ let tx_remaining = self.tx_limit().saturating_sub(self.tx_usage()); -+ if self.rex4_enabled { -+ let frame_remaining = self.frame_tracker.current_frame_remaining(); -+ if frame_remaining < tx_remaining { -+ return (frame_remaining, true); -+ } -+ } -+ (tx_remaining, false) -+ } -+ - /// Returns `true` when gas detention is the binding TX-level constraint, i.e., the detained - /// limit is tighter than the base TX limit AND actual usage exceeds it. - pub(crate) fn is_detained_exceed(&self) -> bool { -diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs -index 1b01e61..54f3e27 100644 ---- a/crates/mega-evm/src/limit/limit.rs -+++ b/crates/mega-evm/src/limit/limit.rs -@@ -117,9 +117,29 @@ pub struct AdditionalLimit { - /// checkpoint, frame entry, or frame resume). Only meaningful while a frame is running - /// and only when `checkpoint_accounting` is active. Re-synced at every - /// `before_frame_run` (which covers both frame entry and every resume after a child -- /// frame's outcome is merged back), at every checkpoint settlement, and lowered by -- /// storage-gas charge sites that debit interpreter gas outside a settlement window. -+ /// frame's outcome is merged back) and at every checkpoint settlement. - checkpoint_baseline: u64, -+ -+ /// V0 gas-clamp enforcement: the portion of the executing frame's interpreter gas -+ /// hidden (clamped away) so revm's own per-opcode gas checks enforce the compute / -+ /// detention headroom inside plain-opcode segments. Non-zero only while the current -+ /// frame is inside a plain segment: every checkpoint restores it before running its -+ /// body (so CALL forwarding, `GAS`, and storage charges see the true counter) and -+ /// re-clamps on exit; the frame-end settlement restores it into the frame result. -+ clamp_hidden: u64, -+ -+ /// Whether the binding headroom at the last clamp was the frame-local compute budget -+ /// (`true`) or the TX-level (possibly detained) limit (`false`). Decides how a -+ /// clamp-induced fake OOG is reclassified: frame-local exceeds revert to the parent, -+ /// TX-level exceeds halt the transaction. -+ clamp_frame_local: bool, -+ -+ /// Whether a clamp-induced fake OOG was latched while gas detention was the binding -+ /// TX-level constraint. `is_detained_exceed` requires `used > detained_limit`, which a -+ /// clamp-stopped transaction never reaches (the crossing opcode's cost is excluded), -+ /// so the halt-reason attribution consults this flag to keep reporting -+ /// `VolatileDataAccessOutOfGas` exactly as per-opcode enforcement does. -+ clamp_latched_detained: bool, - } - - /// The usage of the additional limits. -@@ -149,6 +169,9 @@ impl AdditionalLimit { - storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), - checkpoint_accounting: spec.is_enabled(MegaSpecId::REX6), - checkpoint_baseline: 0, -+ clamp_hidden: 0, -+ clamp_frame_local: false, -+ clamp_latched_detained: false, - } - } - } -@@ -191,6 +214,9 @@ impl AdditionalLimit { - self.kv_update.reset(); - self.storage_call_stipend.reset(); - self.checkpoint_baseline = 0; -+ self.clamp_hidden = 0; -+ self.clamp_frame_local = false; -+ self.clamp_latched_detained = false; - } - - /// Interpreter gas remaining at the start of the current unsettled segment -@@ -209,14 +235,80 @@ impl AdditionalLimit { - self.checkpoint_baseline = remaining; - } - -- /// Lowers the baseline by `amount` to exclude a storage-gas debit from the open -- /// settlement window. Used by charge sites whose storage gas is not passed to the -- /// settlement macro directly (currently the SELFDESTRUCT beneficiary-creation charge, -- /// which is debited in `storage_gas_ext::selfdestruct` while the window is closed later -- /// in `compute_gas_ext::selfdestruct`). -+ /// Takes the outstanding clamp-hidden gas for restoration into the interpreter -+ /// counter. Every checkpoint calls this before running its body; the frame-end -+ /// settlement calls it before the frame result propagates. -+ #[inline] -+ pub(crate) fn checkpoint_restore_hidden(&mut self) -> u64 { -+ core::mem::take(&mut self.clamp_hidden) -+ } -+ -+ /// Computes the amount of interpreter gas to hide so the visible remaining equals the -+ /// compute headroom (the tighter of the frame-local budget and the TX-level detained -+ /// limit), records it as outstanding, and returns it for the caller to deduct from the -+ /// interpreter counter. Returns 0 — clamping disabled — for exempt transactions. -+ #[inline] -+ pub(crate) fn checkpoint_clamp_amount(&mut self, remaining: u64) -> u64 { -+ debug_assert_eq!(self.clamp_hidden, 0, "clamp applied while a clamp is outstanding"); -+ if self.has_exceeded_limit.is_exempt() { -+ return 0; -+ } -+ let (headroom, frame_local) = self.compute_gas.clamp_headroom(); -+ let hide = remaining.saturating_sub(headroom); -+ self.clamp_hidden = hide; -+ self.clamp_frame_local = frame_local; -+ hide -+ } -+ -+ /// Latches a clamp-induced fake OOG as a compute-gas limit exceed. -+ /// -+ /// The crossing opcode never executed (revm's own gas check stopped it at the clamp -+ /// boundary), so its cost is not in the recorded usage and the normal `check_limit` -+ /// pass sees usage at-or-below the limit. The latch is stamped directly, with -+ /// `frame_local` taken from the binding constraint at clamp time, so the existing -+ /// frame-result machinery (frame-local absorb to revert; TX-level mark + rescue) -+ /// produces the halt shape. - #[inline] -- pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { -- self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); -+ pub(crate) fn latch_clamp_exceed(&mut self) { -+ if self.has_exceeded_limit.within_limit() { -+ self.has_exceeded_limit = LimitCheck::ExceedsLimit { -+ kind: super::LimitKind::ComputeGas, -+ frame_local: self.clamp_frame_local, -+ limit: self.compute_gas.tx_limit(), -+ used: self.compute_gas.tx_usage(), -+ }; -+ // Preserve the volatile-detention attribution: when the binding TX-level -+ // constraint at clamp time was the detained limit, the halt must classify as -+ // `VolatileDataAccessOutOfGas` exactly as per-opcode enforcement would. -+ self.clamp_latched_detained = !self.clamp_frame_local && -+ self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(); -+ } -+ } -+ -+ /// Restores any outstanding V0 clamp into the frame's final interpreter result and -+ /// latches a clamp-induced fake OOG. -+ /// -+ /// Must run before anything reads or charges the result's gas — in particular before -+ /// the execution-layer code-deposit storage charge, which would otherwise observe the -+ /// clamped copy and mis-fire an OOG on a non-exceeding CREATE frame. -+ /// -+ /// A clamp can only be outstanding when the frame ended inside a plain segment (every -+ /// checkpoint prologue restores it before its body), and an `OutOfGas` exit from such -+ /// a segment is a fake OOG: revm's own gas check stopped the crossing opcode at the -+ /// clamp boundary *before executing it* — exactly the V0 enforcement point. The latch -+ /// makes the existing frame-result machinery (frame-local absorb to revert; TX-level -+ /// mark + rescue on the restored gas) produce the halt shape. -+ pub(crate) fn restore_clamp_into_result(&mut self, result: &mut InterpreterResult) { -+ if !self.checkpoint_accounting { -+ return; -+ } -+ let hidden = self.checkpoint_restore_hidden(); -+ if hidden > 0 { -+ result.gas.erase_cost(hidden); -+ if result.result == InstructionResult::OutOfGas { -+ self.latch_clamp_exceed(); -+ } -+ } - } - - /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every -@@ -360,10 +452,15 @@ impl AdditionalLimit { - &self, - access_type: VolatileDataAccess, - ) -> Option { -- self.compute_gas.is_detained_exceed().then(|| MegaHaltReason::VolatileDataAccessOutOfGas { -- access_type, -- limit: self.compute_gas.detained_limit(), -- actual: self.compute_gas.tx_usage(), -+ // `is_detained_exceed` covers per-opcode enforcement (usage crossed the detained -+ // limit); `clamp_latched_detained` covers V0 clamp enforcement, where the crossing -+ // opcode was stopped before executing and usage stays at-or-below the limit. -+ (self.compute_gas.is_detained_exceed() || self.clamp_latched_detained).then(|| { -+ MegaHaltReason::VolatileDataAccessOutOfGas { -+ access_type, -+ limit: self.compute_gas.detained_limit(), -+ actual: self.compute_gas.tx_usage(), -+ } - }) - } - -@@ -710,16 +807,8 @@ impl AdditionalLimit { - /// indicating that the limit is exceeded. - pub(crate) fn before_frame_run( - &mut self, -- frame: &EthFrame, -+ frame: &mut EthFrame, - ) -> Option { -- // Checkpoint accounting: open the settlement window at the frame's current gas. -- // This hook runs both at frame entry and at every resume after a child frame's -- // outcome (including its returned gas) has been merged back into this frame's -- // interpreter, so the window start always sits at an instruction boundary. -- if self.checkpoint_accounting { -- self.checkpoint_baseline = frame.interpreter.gas.remaining(); -- } -- - self.state_growth.before_frame_run(frame); - self.data_size.before_frame_run(frame); - self.kv_update.before_frame_run(frame); -@@ -733,6 +822,22 @@ impl AdditionalLimit { - output, - )); - } -+ -+ // Checkpoint accounting: apply the V0 gas clamp and open the settlement window at -+ // the frame's (clamped) gas. This hook runs both at frame entry and at every -+ // resume after a child frame's outcome (including its returned gas) has been -+ // merged back into this frame's interpreter, so the window start always sits at an -+ // instruction boundary. No clamp is outstanding here: every suspension point -+ // (CALL/CREATE checkpoint prologue) and every frame end restores it first. -+ if self.checkpoint_accounting { -+ debug_assert_eq!(self.clamp_hidden, 0, "frame resumed with a clamp outstanding"); -+ let hide = self.checkpoint_clamp_amount(frame.interpreter.gas.remaining()); -+ if hide > 0 { -+ let clamped = frame.interpreter.gas.record_cost(hide); -+ debug_assert!(clamped, "clamp amount exceeds remaining gas"); -+ } -+ self.checkpoint_baseline = frame.interpreter.gas.remaining(); -+ } - None - } - -@@ -771,14 +876,18 @@ impl AdditionalLimit { - ) { - // Checkpoint accounting: the frame has produced its final action, so settle the - // tail segment (everything since the last checkpoint) against the interpreter's -- // gas counter. `frame.interpreter.gas` still holds the loop-exit value here — the -- // code-deposit storage charge in the execution-layer hook mutates only the action's -- // gas copy — so the delta telescopes exactly over the unwrapped plain opcodes. -- // A checkpoint that already settled and halted leaves `baseline == remaining` -- // (delta 0), and the CALL-family abort path's forwarded-gas `erase_cost` can only -- // raise `remaining` above the baseline, which the saturation turns into 0. -+ // gas counter. `frame.interpreter.gas` still holds the loop-exit value here (the -+ // clamp restore and the code-deposit storage charge both mutate only the action's -+ // gas copy), and both it and the baseline live in the same (clamped) domain, so -+ // the delta telescopes exactly over the unwrapped plain opcodes. A checkpoint that -+ // already settled and halted leaves `baseline == remaining` (delta 0), and the -+ // CALL-family abort path's forwarded-gas `erase_cost` can only raise `remaining` -+ // above the baseline, which the saturation turns into 0. -+ // - // Any limit exceed recorded here is latched and surfaced by the existing -- // frame-result marking below / in `before_frame_return_result`. -+ // frame-result marking below / in `before_frame_return_result`. The clamp restore -+ // itself happens earlier, in `restore_clamp_into_result`, before the -+ // execution-layer hook charges code-deposit storage against the action's gas. - if self.checkpoint_accounting { - if let InterpreterAction::Return(_) = action { - let remaining = frame.interpreter.gas.remaining(); -diff --git a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs -index 462a07f..022a499 100644 ---- a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs -+++ b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs -@@ -1,15 +1,20 @@ --//! REX6 checkpoint compute-gas accounting (prototype). -+//! REX6 checkpoint compute-gas accounting with V0 gas-clamp enforcement (prototype). - //! --//! Under checkpoint accounting, plain opcodes run the raw revm instructions with no per-opcode --//! recording; compute gas settles as an interpreter-gas delta at each checkpoint (storage-gas --//! opcodes, CALL/CREATE family, volatile opcodes, frame entry/exit). These tests pin the two --//! sides of that trade: -+//! Plain opcodes run the raw revm instructions with no per-opcode recording; compute gas -+//! settles as an interpreter-gas delta at each checkpoint (storage-gas opcodes, CALL/CREATE -+//! family, volatile opcodes, `GAS`, frame entry/exit). Enforcement inside plain segments is -+//! the V0 gas clamp: the interpreter's visible gas is clamped to the compute headroom, so -+//! revm's own per-opcode gas checks stop a crossing opcode at the clamp boundary *before it -+//! executes* — zero overshoot. - //! --//! - **Precision invariant**: per-transaction accounting totals are bit-identical to per-opcode --//! recording (the interpreter gas counter telescopes over the unwrapped segment). --//! - **Coarsened enforcement**: a limit crossing inside a plain-opcode segment surfaces at the --//! *next checkpoint*, not at the crossing opcode, so limit-exceeding transactions overshoot by up --//! to one segment. -+//! These tests pin the three sides of the design: -+//! -+//! - **Precision invariant**: non-exceeding transactions produce accounting totals (and receipts) -+//! bit-identical to per-opcode recording, including under an active clamp (`GAS` observability). -+//! - **Exact enforcement**: a crossing inside a plain segment halts at the crossing opcode with its -+//! cost excluded — earlier than per-opcode enforcement, which executes the crossing opcode first. -+//! - **Bounded loops**: a detention cap inside a checkpoint-free arithmetic loop is enforced by the -+//! clamp, not deferred to frame end. - - use crate::common::{transact, transact_default, CALLER, CONTRACT}; - use alloy_primitives::{Bytes, U256}; -@@ -17,7 +22,7 @@ use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaSpecId, - }; --use revm::bytecode::opcode::{POP, SSTORE, STOP, TIMESTAMP}; -+use revm::bytecode::opcode::{GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP}; - - const ONE_ETH: u128 = 1_000_000_000_000_000_000; - -@@ -27,10 +32,19 @@ fn db_with_code(code: Bytes) -> MemoryDatabase { - .account_code(CONTRACT, code) - } - -+/// Measures the intrinsic compute gas (tx base cost) with a STOP-only contract. -+fn intrinsic_compute_gas() -> u64 { -+ transact_default( -+ MegaSpecId::REX6, -+ db_with_code(BytecodeBuilder::default().append(STOP).build()), -+ ) -+ .compute_gas -+} -+ - /// A countdown loop of cheap opcodes with no checkpoint inside the loop body: - /// - /// ```text --/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP -+/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP - /// ``` - /// - /// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and -@@ -51,12 +65,8 @@ fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { - Bytes::from(code) - } - --fn countdown_loop_code(iterations: u16) -> Bytes { -- countdown_loop_code_with_prefix(&[], iterations) --} -- - /// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then --/// `SSTORE(7, 99)` (the first checkpoint in the code), then STOP. -+/// `SSTORE(7, 99)` and STOP. - fn plain_run_then_sstore_code(pairs: usize) -> Bytes { - let mut builder = BytecodeBuilder::default(); - for _ in 0..pairs { -@@ -78,7 +88,7 @@ fn plain_run_then_sstore_code(pairs: usize) -> Bytes { - /// checkpoint. - #[test] - fn test_checkpoint_plain_loop_totals_match_rex5() { -- let code = countdown_loop_code(500); -+ let code = countdown_loop_code_with_prefix(&[], 500); - let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); - let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); - -@@ -91,22 +101,47 @@ fn test_checkpoint_plain_loop_totals_match_rex5() { - assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); - } - --/// A compute-gas crossing inside a plain-opcode segment surfaces at the next checkpoint. --/// --/// The limit is placed mid-way through a straight plain-opcode run that ends in an SSTORE. --/// REX5 (per-opcode) halts at the crossing opcode; REX6 (checkpoint) runs the rest of the --/// segment and halts at the SSTORE settlement, recording the full segment — the same total a --/// generous-limit run records up to that point. Both halt; the checkpoint run overshoots. -+/// `GAS` observability under an active clamp: with a tight detention cap the interpreter's -+/// visible gas is clamped for the whole post-access run, yet the value `GAS` pushes must be -+/// the true remaining — the checkpoint prologue restores the clamp before the raw -+/// instruction reads the counter. The returned word and the receipt must be bit-identical -+/// to per-opcode REX5, where no clamp exists at all. - #[test] --fn test_checkpoint_halt_lands_at_next_checkpoint() { -- let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes -+fn test_v0_clamp_is_unobservable_via_gas_opcode() { -+ // TIMESTAMP; POP; GAS; PUSH1 0; MSTORE; PUSH1 32; PUSH1 0; RETURN -+ let code = -+ Bytes::from(vec![TIMESTAMP, POP, GAS, 0x60, 0x00, MSTORE, 0x60, 0x20, 0x60, 0x00, RETURN]); -+ // Detention cap 1,000: far below the interpreter's remaining gas, so the clamp is -+ // active at the GAS opcode; the tx itself stays far below the cap (non-exceeding). -+ let limits = |spec| { -+ let mut l = EvmTxRuntimeLimits::from_spec(spec); -+ l.block_env_access_compute_gas_limit = 1_000; -+ l -+ }; - -- // Intrinsic compute gas (tx base cost) measured with a STOP-only contract. -- let intrinsic = transact_default( -- MegaSpecId::REX6, -- db_with_code(BytecodeBuilder::default().append(STOP).build()), -- ) -- .compute_gas; -+ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); -+ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); -+ -+ assert!(r5.is_success(), "REX5 must succeed: {:?}", r5.result); -+ assert!(r6.is_success(), "REX6 must succeed: {:?}", r6.result); -+ assert_eq!( -+ r5.result.output(), -+ r6.result.output(), -+ "GAS must push the true remaining under the clamp" -+ ); -+ assert_eq!(r5.compute_gas, r6.compute_gas, "compute totals must be identical"); -+ assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be identical"); -+} -+ -+/// V0 exact enforcement in a plain segment: the limit is placed mid-way through a straight -+/// plain-opcode run. REX5 (per-opcode) executes the crossing opcode and then halts, so its -+/// recorded usage exceeds the limit; REX6 (clamp) fake-OOGs the crossing opcode at the -+/// clamp boundary *before executing it*, so its recorded usage stays at or below the limit -+/// and the halt lands one opcode earlier — zero overshoot. -+#[test] -+fn test_v0_clamp_halts_at_crossing_opcode_without_executing_it() { -+ let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes -+ let intrinsic = intrinsic_compute_gas(); - - // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. - let compute_limit = intrinsic + 300; -@@ -114,72 +149,90 @@ fn test_checkpoint_halt_lands_at_next_checkpoint() { - |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); - - let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); -- let r6 = transact(MegaSpecId::REX6, db_with_code(code.clone()), limits(MegaSpecId::REX6)); -- let r6_full = transact_default(MegaSpecId::REX6, db_with_code(code)); -+ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); - - assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); - assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); - -- // REX5 stops within one opcode of the crossing. -+ // Per-opcode enforcement records the crossing opcode: usage strictly exceeds the limit. - assert!( -- r5.compute_gas <= compute_limit + 12, -- "REX5 must stop at the crossing opcode; compute={} limit={compute_limit}", -+ r5.compute_gas > compute_limit, -+ "REX5 executes the crossing opcode before halting; compute={} limit={compute_limit}", - r5.compute_gas - ); -- // REX6 records the whole segment up to (and including) the SSTORE checkpoint: identical -- // to what a run without the tight limit records at that point (SSTORE is the last -- // gas-consuming opcode, so the full-run total equals the at-checkpoint total). -- assert_eq!( -- r6.compute_gas, r6_full.compute_gas, -- "REX6 must settle the full segment at the checkpoint" -+ // Clamp enforcement stops the crossing opcode before execution: usage stays at or -+ // below the limit. -+ assert!( -+ r6.compute_gas <= compute_limit, -+ "REX6 must not execute the crossing opcode; compute={} limit={compute_limit}", -+ r6.compute_gas - ); - assert!( -- r6.compute_gas > r5.compute_gas, -- "checkpoint enforcement overshoots per-opcode enforcement; REX5={} REX6={}", -- r5.compute_gas, -+ r6.compute_gas > compute_limit - 12, -+ "REX6 must stop at the clamp boundary, not earlier; compute={} limit={compute_limit}", - r6.compute_gas - ); - } - --/// Pins the V1 checkpoint-set gap: a loop of plain opcodes contains **no checkpoint**, so a --/// detention cap crossing inside it is only enforced at the frame-end settlement — the --/// overshoot is bounded by the interpreter gas budget, NOT by the ~73k straight-line --/// code-size bound from the design spec ("a straight line cannot loop" only holds when --/// JUMP/JUMPI are checkpoints, which V1 deliberately excludes). --/// --/// A ~14-byte contract overshoots the detention cap by >100k gas here. If the V1 set is kept --/// for the final spec, the overshoot bound must be stated as the remaining interpreter gas; --/// bounding it by code size requires JUMP/JUMPI checkpoints (V2) or another backstop. -+/// V0 bounds detention inside a checkpoint-free plain loop — the shape that broke V1 -+/// (which deferred enforcement to frame end, overshooting by the whole loop). The clamp -+/// stops the loop at the cap boundary regardless of the absence of checkpoints. - #[test] --fn test_checkpoint_v1_loop_segment_is_not_code_size_bounded() { -- // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a 10,000-iteration -- // countdown loop (~260k gas) with no checkpoint inside. -+fn test_v0_clamp_bounds_detention_inside_plain_loop() { -+ // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a -+ // 10,000-iteration countdown loop (~260k gas) with no checkpoint inside. - let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); -+ let intrinsic = intrinsic_compute_gas(); - -- let mut limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6); -- limits.block_env_access_compute_gas_limit = 1_000; -- -- let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), { -- let mut l = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5); -+ let limits = |spec| { -+ let mut l = EvmTxRuntimeLimits::from_spec(spec); - l.block_env_access_compute_gas_limit = 1_000; - l -- }); -- let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits); -+ }; -+ -+ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); -+ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits(MegaSpecId::REX6)); - - assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); - assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); - -- // REX5 enforces within one opcode of the cap crossing (cap ≈ intrinsic + 1,000). -+ // The detained limit is usage-at-access + 1,000 ≈ intrinsic + TIMESTAMP + 1,000. -+ let detained_limit_upper = intrinsic + 2 + 1_000; -+ // Per-opcode enforcement executes the crossing opcode: usage strictly exceeds the cap. - assert!( -- r5.compute_gas < 30_000, -+ r5.compute_gas > detained_limit_upper - 12, - "REX5 must stop near the detention cap; compute={}", - r5.compute_gas - ); -- // REX6 runs the entire loop before any checkpoint settles: >100k gas past the cap from a -- // 16-byte contract — the overshoot is not bounded by code size under V1. -+ // Clamp enforcement stops at the boundary without executing the crossing opcode, -+ // despite the loop containing no checkpoint at all. - assert!( -- r6.compute_gas > 100_000, -- "REX6 V1 loop segment must overshoot far past the code-size bound; compute={}", -+ r6.compute_gas <= detained_limit_upper, -+ "REX6 must not overshoot the detention cap; compute={} cap≈{detained_limit_upper}", - r6.compute_gas - ); -+ assert!( -+ r6.compute_gas > detained_limit_upper - 30, -+ "REX6 must stop at the clamp boundary, not earlier; compute={}", -+ r6.compute_gas -+ ); -+ -+ // Attribution parity: a clamp-stopped detention exceed must classify as -+ // `VolatileDataAccessOutOfGas` exactly like per-opcode enforcement, even though the -+ // recorded usage never crossed the detained limit (the crossing opcode was stopped -+ // before executing). -+ let halt_reason = |label: &str, r: &crate::common::Outcome| match &r.result { -+ revm::context::result::ExecutionResult::Halt { reason, .. } => format!("{reason:?}"), -+ other => panic!("{label}: expected a halt, got {other:?}"), -+ }; -+ let r5_reason = halt_reason("REX5", &r5); -+ let r6_reason = halt_reason("REX6", &r6); -+ assert!( -+ r5_reason.starts_with("VolatileDataAccessOutOfGas"), -+ "REX5 must attribute the halt to volatile detention; got {r5_reason}" -+ ); -+ assert!( -+ r6_reason.starts_with("VolatileDataAccessOutOfGas"), -+ "REX6 clamp halt must keep the volatile detention attribution; got {r6_reason}" -+ ); - } --- -2.50.1 (Apple Git-155) - diff --git a/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch b/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch deleted file mode 100644 index b6aeab89..00000000 --- a/design/reference-patches/0001-feat-rex6-prototype-checkpoint-based-compute-gas-acc.patch +++ /dev/null @@ -1,732 +0,0 @@ -From a6b0f32aa05900aad9cc1f01fabe2c55ab7c749c Mon Sep 17 00:00:00 2001 -From: RealiCZ -Date: Fri, 24 Jul 2026 18:10:33 +0800 -Subject: [PATCH] feat(rex6): prototype checkpoint-based compute-gas accounting - -Plain opcodes in the REX6 instruction table run the raw revm instructions -with no per-opcode recording; compute gas settles as an interpreter-gas -delta at each checkpoint (storage-gas opcodes, CALL/CREATE family, -volatile/detention opcodes, frame entry/exit). Accounting totals telescope -to the same per-transaction sums as per-opcode recording; only the halt -position for limit-exceeding transactions coarsens to the next checkpoint. -Specs <= REX5 are byte-for-byte unchanged. ---- - crates/mega-evm/src/evm/instructions.rs | 340 ++++++++++++++++-- - crates/mega-evm/src/limit/limit.rs | 69 ++++ - .../tests/rex6/checkpoint_accounting.rs | 185 ++++++++++ - crates/mega-evm/tests/rex6/main.rs | 1 + - 4 files changed, 575 insertions(+), 20 deletions(-) - create mode 100644 crates/mega-evm/tests/rex6/checkpoint_accounting.rs - -diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs -index 03c35ba..f15fee4 100644 ---- a/crates/mega-evm/src/evm/instructions.rs -+++ b/crates/mega-evm/src/evm/instructions.rs -@@ -379,22 +379,30 @@ mod rex5 { - mod rex6 { - use super::*; - -- /// Returns the instruction table for the `REX6` spec. -+ /// Returns the instruction table for the `REX6` spec — **checkpoint compute-gas -+ /// accounting** (prototype). - /// -- /// Changes from Rex5: the instruction *table* is unchanged (same handler functions as Rex5). -- /// Every Rex6 behavior difference is expressed as internal `spec.is_enabled(MegaSpecId::REX6)` -- /// dispatch inside the shared handlers, never as a swapped table entry: -- /// - the storage-affecting handlers (SSTORE, LOG, CALL-family, CREATE/CREATE2) charge storage -- /// gas, run their body, then record compute gas exactly once (via -- /// [`record_storage_compute_gas!`]) with the storage gas excluded; SELFDESTRUCT keeps its -- /// delegation to `compute_gas_ext::selfdestruct`, whose trailing all-dimension check records -- /// the same single compute window while latching the pre-recorded data/KV/state usage; -- /// - `storage_gas_ext::selfdestruct` additionally records existing-target balance-update -- /// accounting, and its outer volatile wrapper -- /// (`volatile_data_ext::selfdestruct_with_beneficiary_guard`) additionally guards the -- /// executing contract (source) against the beneficiary; -- /// - the CALL-family volatile wrappers, on the `disableVolatileDataAccess` path, resolve the -- /// stack target's one-hop EIP-7702 delegate before the beneficiary comparison. -+ /// Unlike every earlier custom table, plain opcodes are wired to the raw revm mainnet -+ /// instructions with no per-opcode gas recording: the interpreter's own gas counter is -+ /// the accounting source, and compute gas settles as a segment delta at each -+ /// checkpoint. The checkpoints are exactly the positions that had to stay wrapped -+ /// anyway: -+ /// - storage-gas opcodes (SSTORE, LOG0–LOG4, SELFDESTRUCT) and the CALL/CREATE family — their -+ /// existing handler chains settle from the checkpoint baseline inside -+ /// [`record_storage_compute_gas!`] (spec-dispatched, so ≤REX5 tables are untouched); -+ /// - volatile / detention opcodes — `*_checkpoint` variants that run the raw instruction, -+ /// settle the segment, then apply the detention cap; -+ /// - frame entry/exit — `AdditionalLimit::before_frame_run` re-opens the window and -+ /// `after_frame_run_instructions` settles the tail segment. -+ /// -+ /// Accounting totals telescope to the same per-transaction sums as per-opcode -+ /// recording; only the halt position for limit-exceeding transactions coarsens to the -+ /// next checkpoint. -+ /// -+ /// The pre-existing REX6 behavior differences (canonical metering order, CREATE -+ /// `create_rex6` dispatch, SELFDESTRUCT existing-target accounting, CALL-family -+ /// EIP-7702 delegate resolution on the disabled path) live as internal -+ /// `spec.is_enabled(MegaSpecId::REX6)` dispatch inside the shared handlers reused here. - pub(super) const fn instruction_table< - WIRE: InterpreterTypes, - H: HostExt + ContextTr + JournalInspectTr + ?Sized, -@@ -402,7 +410,43 @@ mod rex6 { - where - WIRE::Stack: StackInspectTr, - { -- rex5::instruction_table::() -+ use revm::bytecode::opcode::*; -+ let mut table = instructions::instruction_table::(); -+ -+ // Volatile / detention checkpoints (raw instruction + segment settlement + cap). -+ table[BALANCE as usize] = volatile_data_ext::balance_checkpoint; -+ table[EXTCODESIZE as usize] = volatile_data_ext::extcodesize_checkpoint; -+ table[EXTCODECOPY as usize] = volatile_data_ext::extcodecopy_checkpoint; -+ table[EXTCODEHASH as usize] = volatile_data_ext::extcodehash_checkpoint; -+ table[BLOCKHASH as usize] = volatile_data_ext::blockhash_checkpoint; -+ table[COINBASE as usize] = volatile_data_ext::coinbase_checkpoint; -+ table[TIMESTAMP as usize] = volatile_data_ext::timestamp_checkpoint; -+ table[NUMBER as usize] = volatile_data_ext::block_number_checkpoint; -+ table[DIFFICULTY as usize] = volatile_data_ext::difficulty_checkpoint; -+ table[GASLIMIT as usize] = volatile_data_ext::gas_limit_opcode_checkpoint; -+ table[BASEFEE as usize] = volatile_data_ext::basefee_checkpoint; -+ table[BLOBBASEFEE as usize] = volatile_data_ext::blobbasefee_checkpoint; -+ table[BLOBHASH as usize] = volatile_data_ext::blobhash_checkpoint; -+ table[SELFBALANCE as usize] = volatile_data_ext::selfbalance_checkpoint; -+ table[SLOAD as usize] = volatile_data_ext::sload_checkpoint; -+ -+ // Storage-gas checkpoints — the same handler chains as REX5; under REX6 they -+ // settle from the checkpoint baseline internally. -+ table[SSTORE as usize] = additional_limit_ext::sstore; -+ table[LOG0 as usize] = additional_limit_ext::log::<0, _, _>; -+ table[LOG1 as usize] = additional_limit_ext::log::<1, _, _>; -+ table[LOG2 as usize] = additional_limit_ext::log::<2, _, _>; -+ table[LOG3 as usize] = additional_limit_ext::log::<3, _, _>; -+ table[LOG4 as usize] = additional_limit_ext::log::<4, _, _>; -+ table[CREATE as usize] = forward_gas_ext::create; -+ table[CREATE2 as usize] = forward_gas_ext::create2; -+ table[CALL as usize] = volatile_data_ext::call; -+ table[STATICCALL as usize] = volatile_data_ext::static_call; -+ table[DELEGATECALL as usize] = volatile_data_ext::delegate_call; -+ table[CALLCODE as usize] = volatile_data_ext::call_code; -+ table[SELFDESTRUCT as usize] = volatile_data_ext::selfdestruct_with_beneficiary_guard; -+ -+ table - } - } - -@@ -463,8 +507,20 @@ macro_rules! run_inner_instruction_or_abort { - /// add gas to the tracker after the OOG was already set. - macro_rules! record_storage_compute_gas { - ($context:expr, $gas_before:expr, $storage_charged:expr) => {{ -+ let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); - let gas_after = $context.interpreter.gas.remaining(); -- let mut gas_used = $gas_before.saturating_sub(gas_after).saturating_sub($storage_charged); -+ // REX6 checkpoint accounting: the measurement window opens at the last checkpoint -+ // (frame entry / resume or the previous checkpoint opcode), not at this opcode's own -+ // start, so the unwrapped plain opcodes executed since then settle here in the same -+ // recording. Only plain opcodes can run between two checkpoints, so no storage gas -+ // or forwarded child gas is hiding in the extra window — the exclusions below stay -+ // exact. Pre-REX6 keeps the per-opcode `$gas_before` window byte-for-byte. -+ let window_start = if is_rex6 { -+ $context.host.additional_limit().borrow().checkpoint_baseline() -+ } else { -+ $gas_before -+ }; -+ let mut gas_used = window_start.saturating_sub(gas_after).saturating_sub($storage_charged); - // Exclude gas forwarded to a child frame. REX5+ excludes the revm-side `CALL_STIPEND` - // (added by value-transferring CALL/CALLCODE without deducting from the parent) so the - // parent's compute gas is not under-counted; pre-REX5 subtracts the full child gas limit -@@ -494,9 +550,15 @@ macro_rules! record_storage_compute_gas { - // On a compute-limit halt the pending child `NewFrame` is discarded (the child never runs), - // but revm already deducted the forwarded gas and the outer `forward_gas_ext` erase is - // skipped on this abort path. REX6+: return that gas to the parent before halting. -- let is_rex6 = $context.host.spec_id().is_enabled(MegaSpecId::REX6); - let exceeding_result = { - let mut additional_limit = $context.host.additional_limit().borrow_mut(); -+ // Checkpoint accounting: re-open the window at this opcode's exit before the -+ // record, so a halt (here or at frame end) never re-settles this segment. The -+ // abort-path `erase_cost` below can only raise the interpreter gas above this -+ // baseline, which the frame-end settlement saturates to 0. -+ if is_rex6 { -+ additional_limit.sync_checkpoint_baseline(gas_after); -+ } - if additional_limit.record_compute_gas(gas_used) { - None - } else { -@@ -1263,6 +1325,219 @@ pub mod volatile_data_ext { - wrap_call_volatile_check!(static_call, "STATICCALL", forward_gas_ext::static_call); - wrap_call_volatile_check!(delegate_call, "DELEGATECALL", forward_gas_ext::delegate_call); - wrap_call_volatile_check!(call_code, "CALLCODE", forward_gas_ext::call_code); -+ -+ /* REX6 checkpoint-accounting volatile handlers. -+ -+ Under checkpoint accounting the volatile opcodes stay wrapped (they are checkpoints), -+ but they run the raw revm instruction and settle the open segment — everything since -+ the last checkpoint, measured on the interpreter's own gas counter — in one recording, -+ instead of delegating to a per-opcode `compute_gas_ext` wrapper. The settlement runs -+ before `apply_compute_gas_limit!` so a REX4+ relative detention cap is derived from the -+ fully settled usage at the access point, exactly as the per-opcode order did. */ -+ -+ /// Settles the open checkpoint segment against the interpreter gas counter, re-opens -+ /// the window, and halts (returning from the enclosing handler) when a limit — -+ /// including a latched non-compute exceed — surfaces. -+ macro_rules! settle_checkpoint_compute_gas { -+ ($context:expr) => { -+ let exceeding_result = { -+ let gas_after = $context.interpreter.gas.remaining(); -+ let mut additional_limit = $context.host.additional_limit().borrow_mut(); -+ let gas_used = additional_limit.checkpoint_baseline().saturating_sub(gas_after); -+ additional_limit.sync_checkpoint_baseline(gas_after); -+ if additional_limit.record_compute_gas(gas_used) { -+ None -+ } else { -+ Some(additional_limit.exceeding_instruction_result()) -+ } -+ }; -+ if let Some(result) = exceeding_result { -+ $context.interpreter.halt(result); -+ return; -+ } -+ }; -+ } -+ -+ /// Checkpoint variant of [`wrap_op_detain_gas_unconditional`]: disabled-check, raw -+ /// revm instruction, segment settlement, detention cap. -+ macro_rules! wrap_checkpoint_detain_gas_unconditional { -+ ($fn_name:ident, $opcode_name:expr, $original_fn:path, $access_type:expr) => { -+ #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] -+ #[inline] -+ pub fn $fn_name( -+ context: InstructionContext<'_, H, WIRE>, -+ ) { -+ // Rex4+ (always enabled under REX6): revert before executing if volatile data -+ // access is disabled. -+ if context.host.volatile_access_disabled() { -+ context.interpreter.bytecode.set_action(InterpreterAction::new_return( -+ InstructionResult::Revert, -+ volatile_data_access_disabled_revert_data($access_type), -+ context.interpreter.gas, -+ )); -+ return; -+ } -+ -+ run_inner_instruction_or_abort!($original_fn, context); -+ settle_checkpoint_compute_gas!(context); -+ apply_compute_gas_limit!(context); -+ } -+ }; -+ } -+ -+ /// Checkpoint variant of [`wrap_op_detain_gas_conditional`]: beneficiary peek, raw -+ /// revm instruction, segment settlement, detention cap. -+ macro_rules! wrap_checkpoint_detain_gas_conditional { -+ ($fn_name:ident, $opcode_name:expr, $original_fn:path) => { -+ #[doc = concat!("`", $opcode_name, "` opcode as a REX6 checkpoint: raw instruction, segment settlement, gas detention.")] -+ #[inline] -+ pub fn $fn_name< -+ WIRE: InterpreterTypes, -+ H: HostExt + ContextTr + JournalInspectTr + ?Sized, -+ >( -+ context: InstructionContext<'_, H, WIRE>, -+ ) { -+ if let Some(addr_word) = context.interpreter.stack.inspect::<0>() { -+ let target: Address = addr_word.into_address(); -+ let beneficiary = context.host.beneficiary_address(); -+ if target == beneficiary && context.host.volatile_access_disabled() { -+ context.interpreter.bytecode.set_action(InterpreterAction::new_return( -+ InstructionResult::Revert, -+ volatile_data_access_disabled_revert_data( -+ VolatileDataAccessType::Beneficiary, -+ ), -+ context.interpreter.gas, -+ )); -+ return; -+ } -+ } -+ -+ run_inner_instruction_or_abort!($original_fn, context); -+ settle_checkpoint_compute_gas!(context); -+ apply_compute_gas_limit!(context); -+ } -+ }; -+ } -+ -+ wrap_checkpoint_detain_gas_unconditional!( -+ timestamp_checkpoint, -+ "TIMESTAMP", -+ instructions::block_info::timestamp, -+ VolatileDataAccessType::Timestamp -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ block_number_checkpoint, -+ "NUMBER", -+ instructions::block_info::block_number, -+ VolatileDataAccessType::BlockNumber -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ difficulty_checkpoint, -+ "DIFFICULTY", -+ instructions::block_info::difficulty, -+ VolatileDataAccessType::Difficulty -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ gas_limit_opcode_checkpoint, -+ "GASLIMIT", -+ instructions::block_info::gaslimit, -+ VolatileDataAccessType::GasLimit -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ basefee_checkpoint, -+ "BASEFEE", -+ instructions::block_info::basefee, -+ VolatileDataAccessType::BaseFee -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ coinbase_checkpoint, -+ "COINBASE", -+ instructions::block_info::coinbase, -+ VolatileDataAccessType::Coinbase -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ blockhash_checkpoint, -+ "BLOCKHASH", -+ instructions::host::blockhash, -+ VolatileDataAccessType::BlockHash -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ blobbasefee_checkpoint, -+ "BLOBBASEFEE", -+ instructions::block_info::blob_basefee, -+ VolatileDataAccessType::BlobBaseFee -+ ); -+ wrap_checkpoint_detain_gas_unconditional!( -+ blobhash_checkpoint, -+ "BLOBHASH", -+ instructions::tx_info::blob_hash, -+ VolatileDataAccessType::BlobHash -+ ); -+ -+ wrap_checkpoint_detain_gas_conditional!( -+ balance_checkpoint, -+ "BALANCE", -+ instructions::host::balance -+ ); -+ wrap_checkpoint_detain_gas_conditional!( -+ extcodesize_checkpoint, -+ "EXTCODESIZE", -+ instructions::host::extcodesize -+ ); -+ wrap_checkpoint_detain_gas_conditional!( -+ extcodecopy_checkpoint, -+ "EXTCODECOPY", -+ instructions::host::extcodecopy -+ ); -+ wrap_checkpoint_detain_gas_conditional!( -+ extcodehash_checkpoint, -+ "EXTCODEHASH", -+ instructions::host::extcodehash -+ ); -+ -+ /// `SLOAD` as a REX6 checkpoint. Same oracle-volatile handling as [`sload`], but the -+ /// raw revm instruction runs unwrapped and the open segment settles here. -+ #[inline] -+ pub fn sload_checkpoint( -+ context: InstructionContext<'_, H, WIRE>, -+ ) { -+ let target = context.interpreter.input.target_address(); -+ if target == ORACLE_CONTRACT_ADDRESS && context.host.volatile_access_disabled() { -+ context.interpreter.bytecode.set_action(InterpreterAction::new_return( -+ InstructionResult::Revert, -+ volatile_data_access_disabled_revert_data(VolatileDataAccessType::Oracle), -+ context.interpreter.gas, -+ )); -+ return; -+ } -+ -+ run_inner_instruction_or_abort!(instructions::host::sload, context); -+ settle_checkpoint_compute_gas!(context); -+ apply_compute_gas_limit!(context); -+ } -+ -+ /// `SELFBALANCE` as a REX6 checkpoint. Same beneficiary-volatile handling as -+ /// [`selfbalance`], but the raw revm instruction runs unwrapped and the open segment -+ /// settles here. -+ #[inline] -+ pub fn selfbalance_checkpoint( -+ context: InstructionContext<'_, H, WIRE>, -+ ) { -+ let target = context.interpreter.input.target_address(); -+ let beneficiary = context.host.beneficiary_address(); -+ if target == beneficiary && context.host.volatile_access_disabled() { -+ context.interpreter.bytecode.set_action(InterpreterAction::new_return( -+ InstructionResult::Revert, -+ volatile_data_access_disabled_revert_data(VolatileDataAccessType::Beneficiary), -+ context.interpreter.gas, -+ )); -+ return; -+ } -+ -+ run_inner_instruction_or_abort!(instructions::host::selfbalance, context); -+ settle_checkpoint_compute_gas!(context); -+ apply_compute_gas_limit!(context); -+ } - } - - /// Extends opcodes with additional limit (kv update limit, data limit, etc.) enforcement. -@@ -2030,7 +2305,20 @@ pub mod storage_gas_ext { - }; - let drained = - context.host.additional_limit().borrow_mut().try_consume_storage_stipend(cost); -- gas!(context.interpreter, cost - drained); -+ let storage_charged = cost - drained; -+ gas!(context.interpreter, storage_charged); -+ -+ // REX6 checkpoint accounting: this storage debit sits inside the window that -+ // `compute_gas_ext::selfdestruct` closes later, so exclude it by lowering the -+ // baseline now — the trailing settlement takes `baseline - remaining` with no -+ // storage term of its own. -+ if context.host.spec_id().is_enabled(MegaSpecId::REX6) { -+ context -+ .host -+ .additional_limit() -+ .borrow_mut() -+ .deduct_checkpoint_baseline(storage_charged); -+ } - - // Record resource usage for new beneficiary account - context.host.additional_limit().borrow_mut().on_selfdestruct_new_account(); -@@ -2299,8 +2587,20 @@ pub mod compute_gas_ext { - // Call the original instruction - run_inner_instruction_or_abort!(instructions::host::selfdestruct, context); - -- let gas_used = gas_before.saturating_sub(context.interpreter.gas.remaining()); -+ // REX6 checkpoint accounting: the window opens at the checkpoint baseline, folding -+ // in the unwrapped plain opcodes since the last checkpoint. The beneficiary-creation -+ // storage charge in `storage_gas_ext::selfdestruct` already lowered the baseline by -+ // the charged amount, so no storage exclusion is needed here. Pre-REX6 keeps the -+ // per-opcode `gas_before` window. -+ let is_rex6 = context.host.spec_id().is_enabled(MegaSpecId::REX6); -+ let gas_after = context.interpreter.gas.remaining(); - let mut additional_limit = context.host.additional_limit().borrow_mut(); -+ let window_start = -+ if is_rex6 { additional_limit.checkpoint_baseline() } else { gas_before }; -+ let gas_used = window_start.saturating_sub(gas_after); -+ if is_rex6 { -+ additional_limit.sync_checkpoint_baseline(gas_after); -+ } - if !additional_limit.record_compute_gas_all_dims(gas_used) { - context.interpreter.halt(additional_limit.exceeding_instruction_result()); - } -diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs -index 2bcec8b..1b01e61 100644 ---- a/crates/mega-evm/src/limit/limit.rs -+++ b/crates/mega-evm/src/limit/limit.rs -@@ -107,6 +107,19 @@ pub struct AdditionalLimit { - - /// A tracker for the `STORAGE_CALL_STIPEND` granted to value-transferring calls (REX4+). - pub(crate) storage_call_stipend: storage_call_stipend::StorageCallStipendTracker, -+ -+ /// REX6+ checkpoint accounting: whether compute gas settles at checkpoints instead of -+ /// per opcode. Plain opcodes run unwrapped; the interpreter's own gas counter is read -+ /// at each checkpoint and the segment delta is recorded in one shot. -+ checkpoint_accounting: bool, -+ -+ /// Interpreter gas remaining at the start of the current unsettled segment (the last -+ /// checkpoint, frame entry, or frame resume). Only meaningful while a frame is running -+ /// and only when `checkpoint_accounting` is active. Re-synced at every -+ /// `before_frame_run` (which covers both frame entry and every resume after a child -+ /// frame's outcome is merged back), at every checkpoint settlement, and lowered by -+ /// storage-gas charge sites that debit interpreter gas outside a settlement window. -+ checkpoint_baseline: u64, - } - - /// The usage of the additional limits. -@@ -134,6 +147,8 @@ impl AdditionalLimit { - kv_update: kv_update::KVUpdateTracker::new(spec, limits.tx_kv_updates_limit), - compute_gas: compute_gas::ComputeGasTracker::new(spec, limits.tx_compute_gas_limit), - storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), -+ checkpoint_accounting: spec.is_enabled(MegaSpecId::REX6), -+ checkpoint_baseline: 0, - } - } - } -@@ -175,6 +190,33 @@ impl AdditionalLimit { - self.data_size.reset(); - self.kv_update.reset(); - self.storage_call_stipend.reset(); -+ self.checkpoint_baseline = 0; -+ } -+ -+ /// Interpreter gas remaining at the start of the current unsettled segment -+ /// (REX6+ checkpoint accounting). Checkpoint settlement sites use this instead of a -+ /// per-opcode `gas_before` capture, so the delta covers every unwrapped plain opcode -+ /// executed since the previous checkpoint. -+ #[inline] -+ pub(crate) fn checkpoint_baseline(&self) -> u64 { -+ self.checkpoint_baseline -+ } -+ -+ /// Re-opens the settlement window at `remaining`. Called by every checkpoint after it -+ /// settles, and by `before_frame_run` at frame entry / resume. -+ #[inline] -+ pub(crate) fn sync_checkpoint_baseline(&mut self, remaining: u64) { -+ self.checkpoint_baseline = remaining; -+ } -+ -+ /// Lowers the baseline by `amount` to exclude a storage-gas debit from the open -+ /// settlement window. Used by charge sites whose storage gas is not passed to the -+ /// settlement macro directly (currently the SELFDESTRUCT beneficiary-creation charge, -+ /// which is debited in `storage_gas_ext::selfdestruct` while the window is closed later -+ /// in `compute_gas_ext::selfdestruct`). -+ #[inline] -+ pub(crate) fn deduct_checkpoint_baseline(&mut self, amount: u64) { -+ self.checkpoint_baseline = self.checkpoint_baseline.saturating_sub(amount); - } - - /// Test-only setter for [`has_exceeded_limit`](Self::has_exceeded_limit). Bypasses every -@@ -670,6 +712,14 @@ impl AdditionalLimit { - &mut self, - frame: &EthFrame, - ) -> Option { -+ // Checkpoint accounting: open the settlement window at the frame's current gas. -+ // This hook runs both at frame entry and at every resume after a child frame's -+ // outcome (including its returned gas) has been merged back into this frame's -+ // interpreter, so the window start always sits at an instruction boundary. -+ if self.checkpoint_accounting { -+ self.checkpoint_baseline = frame.interpreter.gas.remaining(); -+ } -+ - self.state_growth.before_frame_run(frame); - self.data_size.before_frame_run(frame); - self.kv_update.before_frame_run(frame); -@@ -719,6 +769,25 @@ impl AdditionalLimit { - frame: &'a EthFrame, - action: &'a mut InterpreterAction, - ) { -+ // Checkpoint accounting: the frame has produced its final action, so settle the -+ // tail segment (everything since the last checkpoint) against the interpreter's -+ // gas counter. `frame.interpreter.gas` still holds the loop-exit value here — the -+ // code-deposit storage charge in the execution-layer hook mutates only the action's -+ // gas copy — so the delta telescopes exactly over the unwrapped plain opcodes. -+ // A checkpoint that already settled and halted leaves `baseline == remaining` -+ // (delta 0), and the CALL-family abort path's forwarded-gas `erase_cost` can only -+ // raise `remaining` above the baseline, which the saturation turns into 0. -+ // Any limit exceed recorded here is latched and surfaced by the existing -+ // frame-result marking below / in `before_frame_return_result`. -+ if self.checkpoint_accounting { -+ if let InterpreterAction::Return(_) = action { -+ let remaining = frame.interpreter.gas.remaining(); -+ let gas_used = self.checkpoint_baseline.saturating_sub(remaining); -+ self.checkpoint_baseline = remaining; -+ let _ = self.record_compute_gas(gas_used); -+ } -+ } -+ - self.state_growth.after_frame_run(frame, action); - self.data_size.after_frame_run(frame, action); - self.kv_update.after_frame_run(frame, action); -diff --git a/crates/mega-evm/tests/rex6/checkpoint_accounting.rs b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs -new file mode 100644 -index 0000000..462a07f ---- /dev/null -+++ b/crates/mega-evm/tests/rex6/checkpoint_accounting.rs -@@ -0,0 +1,185 @@ -+//! REX6 checkpoint compute-gas accounting (prototype). -+//! -+//! Under checkpoint accounting, plain opcodes run the raw revm instructions with no per-opcode -+//! recording; compute gas settles as an interpreter-gas delta at each checkpoint (storage-gas -+//! opcodes, CALL/CREATE family, volatile opcodes, frame entry/exit). These tests pin the two -+//! sides of that trade: -+//! -+//! - **Precision invariant**: per-transaction accounting totals are bit-identical to per-opcode -+//! recording (the interpreter gas counter telescopes over the unwrapped segment). -+//! - **Coarsened enforcement**: a limit crossing inside a plain-opcode segment surfaces at the -+//! *next checkpoint*, not at the crossing opcode, so limit-exceeding transactions overshoot by up -+//! to one segment. -+ -+use crate::common::{transact, transact_default, CALLER, CONTRACT}; -+use alloy_primitives::{Bytes, U256}; -+use mega_evm::{ -+ test_utils::{BytecodeBuilder, MemoryDatabase}, -+ EvmTxRuntimeLimits, MegaSpecId, -+}; -+use revm::bytecode::opcode::{POP, SSTORE, STOP, TIMESTAMP}; -+ -+const ONE_ETH: u128 = 1_000_000_000_000_000_000; -+ -+fn db_with_code(code: Bytes) -> MemoryDatabase { -+ MemoryDatabase::default() -+ .account_balance(CALLER, U256::from(10 * ONE_ETH)) -+ .account_code(CONTRACT, code) -+} -+ -+/// A countdown loop of cheap opcodes with no checkpoint inside the loop body: -+/// -+/// ```text -+/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP -+/// ``` -+/// -+/// Each iteration executes 7 plain opcodes for 26 gas. `prefix` is prepended verbatim and -+/// participates in the jump-target offset. -+fn countdown_loop_code_with_prefix(prefix: &[u8], iterations: u16) -> Bytes { -+ let mut code = prefix.to_vec(); -+ code.push(0x61); // PUSH2 -+ code.extend_from_slice(&iterations.to_be_bytes()); -+ let loop_target = code.len() as u8; -+ code.push(0x5b); // JUMPDEST -+ code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 -+ code.push(0x90); // SWAP1 -+ code.push(0x03); // SUB -+ code.push(0x80); // DUP1 -+ code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop -+ code.push(0x57); // JUMPI -+ code.push(0x00); // STOP -+ Bytes::from(code) -+} -+ -+fn countdown_loop_code(iterations: u16) -> Bytes { -+ countdown_loop_code_with_prefix(&[], iterations) -+} -+ -+/// A straight line of `pairs` PUSH1/POP pairs (5 gas, 2 plain opcodes each), then -+/// `SSTORE(7, 99)` (the first checkpoint in the code), then STOP. -+fn plain_run_then_sstore_code(pairs: usize) -> Bytes { -+ let mut builder = BytecodeBuilder::default(); -+ for _ in 0..pairs { -+ builder = builder.push_number(1u64); -+ builder = builder.append(POP); -+ } -+ builder -+ .push_u256(U256::from(99u64)) // value -+ .push_u256(U256::from(7u64)) // slot -+ .append(SSTORE) -+ .append(STOP) -+ .build() -+} -+ -+/// Precision invariant on a plain-opcode hot loop: with no limit crossing, checkpoint -+/// accounting (REX6) must produce the same compute-gas total and receipt `gas_used` as -+/// per-opcode accounting (REX5) — the segment delta telescopes over exactly the opcodes the -+/// per-opcode wrappers would have recorded, and the loop's only settlement is the frame-end -+/// checkpoint. -+#[test] -+fn test_checkpoint_plain_loop_totals_match_rex5() { -+ let code = countdown_loop_code(500); -+ let r5 = transact_default(MegaSpecId::REX5, db_with_code(code.clone())); -+ let r6 = transact_default(MegaSpecId::REX6, db_with_code(code)); -+ -+ assert!(r5.is_success(), "REX5 loop must succeed: {:?}", r5.result); -+ assert!(r6.is_success(), "REX6 loop must succeed: {:?}", r6.result); -+ assert_eq!( -+ r5.compute_gas, r6.compute_gas, -+ "checkpoint totals must telescope to the per-opcode sum" -+ ); -+ assert_eq!(r5.gas_used, r6.gas_used, "receipt gas must be unchanged"); -+} -+ -+/// A compute-gas crossing inside a plain-opcode segment surfaces at the next checkpoint. -+/// -+/// The limit is placed mid-way through a straight plain-opcode run that ends in an SSTORE. -+/// REX5 (per-opcode) halts at the crossing opcode; REX6 (checkpoint) runs the rest of the -+/// segment and halts at the SSTORE settlement, recording the full segment — the same total a -+/// generous-limit run records up to that point. Both halt; the checkpoint run overshoots. -+#[test] -+fn test_checkpoint_halt_lands_at_next_checkpoint() { -+ let code = plain_run_then_sstore_code(200); // 200 * 5 = 1,000 gas of plain opcodes -+ -+ // Intrinsic compute gas (tx base cost) measured with a STOP-only contract. -+ let intrinsic = transact_default( -+ MegaSpecId::REX6, -+ db_with_code(BytecodeBuilder::default().append(STOP).build()), -+ ) -+ .compute_gas; -+ -+ // Trip the limit ~300 gas into the 1,000-gas plain run, well before the SSTORE. -+ let compute_limit = intrinsic + 300; -+ let limits = -+ |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(compute_limit); -+ -+ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), limits(MegaSpecId::REX5)); -+ let r6 = transact(MegaSpecId::REX6, db_with_code(code.clone()), limits(MegaSpecId::REX6)); -+ let r6_full = transact_default(MegaSpecId::REX6, db_with_code(code)); -+ -+ assert!(!r5.is_success(), "REX5 must stop on the tight compute limit: {:?}", r5.result); -+ assert!(!r6.is_success(), "REX6 must stop on the tight compute limit: {:?}", r6.result); -+ -+ // REX5 stops within one opcode of the crossing. -+ assert!( -+ r5.compute_gas <= compute_limit + 12, -+ "REX5 must stop at the crossing opcode; compute={} limit={compute_limit}", -+ r5.compute_gas -+ ); -+ // REX6 records the whole segment up to (and including) the SSTORE checkpoint: identical -+ // to what a run without the tight limit records at that point (SSTORE is the last -+ // gas-consuming opcode, so the full-run total equals the at-checkpoint total). -+ assert_eq!( -+ r6.compute_gas, r6_full.compute_gas, -+ "REX6 must settle the full segment at the checkpoint" -+ ); -+ assert!( -+ r6.compute_gas > r5.compute_gas, -+ "checkpoint enforcement overshoots per-opcode enforcement; REX5={} REX6={}", -+ r5.compute_gas, -+ r6.compute_gas -+ ); -+} -+ -+/// Pins the V1 checkpoint-set gap: a loop of plain opcodes contains **no checkpoint**, so a -+/// detention cap crossing inside it is only enforced at the frame-end settlement — the -+/// overshoot is bounded by the interpreter gas budget, NOT by the ~73k straight-line -+/// code-size bound from the design spec ("a straight line cannot loop" only holds when -+/// JUMP/JUMPI are checkpoints, which V1 deliberately excludes). -+/// -+/// A ~14-byte contract overshoots the detention cap by >100k gas here. If the V1 set is kept -+/// for the final spec, the overshoot bound must be stated as the remaining interpreter gas; -+/// bounding it by code size requires JUMP/JUMPI checkpoints (V2) or another backstop. -+#[test] -+fn test_checkpoint_v1_loop_segment_is_not_code_size_bounded() { -+ // TIMESTAMP marks volatile access (detention cap = usage + 1,000), then a 10,000-iteration -+ // countdown loop (~260k gas) with no checkpoint inside. -+ let code = countdown_loop_code_with_prefix(&[TIMESTAMP, POP], 10_000); -+ -+ let mut limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6); -+ limits.block_env_access_compute_gas_limit = 1_000; -+ -+ let r5 = transact(MegaSpecId::REX5, db_with_code(code.clone()), { -+ let mut l = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX5); -+ l.block_env_access_compute_gas_limit = 1_000; -+ l -+ }); -+ let r6 = transact(MegaSpecId::REX6, db_with_code(code), limits); -+ -+ assert!(!r5.is_success(), "REX5 must halt on the detention cap: {:?}", r5.result); -+ assert!(!r6.is_success(), "REX6 must halt on the detention cap: {:?}", r6.result); -+ -+ // REX5 enforces within one opcode of the cap crossing (cap ≈ intrinsic + 1,000). -+ assert!( -+ r5.compute_gas < 30_000, -+ "REX5 must stop near the detention cap; compute={}", -+ r5.compute_gas -+ ); -+ // REX6 runs the entire loop before any checkpoint settles: >100k gas past the cap from a -+ // 16-byte contract — the overshoot is not bounded by code size under V1. -+ assert!( -+ r6.compute_gas > 100_000, -+ "REX6 V1 loop segment must overshoot far past the code-size bound; compute={}", -+ r6.compute_gas -+ ); -+} -diff --git a/crates/mega-evm/tests/rex6/main.rs b/crates/mega-evm/tests/rex6/main.rs -index 6a5160d..de86f86 100644 ---- a/crates/mega-evm/tests/rex6/main.rs -+++ b/crates/mega-evm/tests/rex6/main.rs -@@ -5,6 +5,7 @@ - //! authorities (not every recoverable one). - - mod beneficiary_detention; -+mod checkpoint_accounting; - mod common; - mod create2_metering_order; - mod create_frame_accounting; --- -2.50.1 (Apple Git-155) - From 7d8ac5180191afe421daf333801d9e9855230f87 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 17 Aug 2026 10:57:41 +0800 Subject: [PATCH 070/208] docs: split multi-sentence lines, name the KZG input length, fix two claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One sentence per line in the roster entries, the conservation-law terms and the clamp procedure — the `minted_stipends` term ran six sentences on one line. The KZG boundary's 192 becomes `KZG_POINT_EVALUATION_INPUT_LENGTH` in the compute-gas constants table, referenced from both pages that described the boundary with a bare number. The exceptional-halt carve-out claimed a frame's whole budget settles as compute gas while its own executed definition subtracts storage gas charged before the abort. The dichotomy now covers the compute part only, and the storage charge is stated as belonging to neither half. The precompile mirror comment named alloy-evm 0.37.1; the workspace pins 0.36.0. It now names the pinned version and records that 0.37.1's `run` body is byte-identical, so the upgrade-comparison obligation stands without implying the mirror is stale. --- crates/mega-evm/src/evm/precompiles.rs | 7 ++-- docs/spec/evm/compute-gas.md | 44 ++++++++++++++++---------- docs/spec/overview.md | 6 ++-- docs/spec/upgrades/rex7.md | 16 ++++++---- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 204c5b69..8b1acf55 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -232,7 +232,8 @@ impl Default for MegaPrecompiles { /// `InterpreterResult`. /// /// This is a mirror of `>>::run` -/// (`alloy_evm::precompiles`, alloy-evm 0.37.1) rather than a call into it. Delegating would +/// (`alloy_evm::precompiles`, alloy-evm 0.36.0 — the pinned version) rather than a call into it. +/// Delegating would /// hand back only the converted `InterpreterResult`, and the conversion folds every non-out-of-gas /// halt into one opaque `PrecompileError` code — so the caller could no longer tell a doorway /// reject apart from a failure raised mid-computation, which is exactly the distinction the @@ -242,7 +243,9 @@ impl Default for MegaPrecompiles { /// /// Upgrading alloy-evm obliges a re-read of that upstream function: a new `PrecompileInput` /// field, a different dispatch address, a result cache, or any other added step must be mirrored -/// here, or this silently stops being the same call. +/// here, or this silently stops being the same call. The next published version, 0.37.1, was read +/// against this one: its `run` body is byte-identical, and the differences in that module are in +/// how `PrecompilesMap` stores its dynamic lookup, which this function does not touch. fn run_precompile_capturing_halt( precompiles: &PrecompilesMap, context: &mut MegaInnerContext, diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index aae786dc..6bd0a52c 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -509,9 +509,10 @@ When the crossing opcode would exhaust both the true remaining EVM gas and the c #### Exceptional-halt frame carve-out A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. -A node MUST settle that whole budget as compute gas, split into two parts that are accounted differently: +A node MUST settle that budget as compute gas, apart from any MegaETH storage gas a checkpoint body charged before aborting: that charge was taken on the storage-gas lane, stays there, and belongs to neither part below. +A node MUST split what remains into two parts that are accounted differently: -- **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. +- **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, net of that storage charge. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. - **Destroyed** — the budget the frame never spent and never handed back. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). @@ -528,10 +529,18 @@ Two of those three are recorded as they happen, so a node MUST derive the third: `destroyed = spent + minted_stipends − storage_gas − executed_compute` -- `spent` — the EVM gas the transaction's envelope burnt, read once, at the moment the envelope is final: after the transaction's gas accounting has settled and any resource-limit gas rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied. Those two move the number the receipt reports without anything having been burnt, so a node MUST NOT read `spent` after them. Gas rescued for the sender, and gas the clamp was hiding, are both out of the envelope by this point and MUST NOT be added back. -- `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints. The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whatever becomes of the stipend afterwards. The mint is created when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from that point rather than from the child frame running: an invocation turned away at frame entry — for want of balance, or at the call-depth limit — hands the whole child budget back to the caller with the stipend inside it, which shrinks the envelope against recorded work by exactly as much as a child that ran and returned it would. An invocation a node halts before handing it to the EVM, which is what a compute-gas limit reached at the call site does, mints nothing and a node MUST NOT count it. A node MUST add the total back; without it the two sides of the law disagree by exactly that amount. -- `storage_gas` — the MegaETH storage gas the transaction was charged: the storage-gas share of intrinsic gas, the in-frame storage-gas surcharges, the code-deposit charge, and the charges a system contract invocation takes outside an EVM frame. At a nested-execution boundary this term takes the **difference** between what the nested execution cost the outer gas counter and what it recorded as compute, which can be negative when the nested execution's own EIP-3529 refund outgrew its storage gas; a node MUST NOT clamp that contribution at zero. -- `executed_compute` — the compute gas the transaction is recorded as having performed, fixed site by site by the rules below, and the quantity every resource limit is evaluated against. It equals the reported total less the destroyed part, but that identity is a consequence of the law rather than a definition of either side. +- `spent` — the EVM gas the transaction's envelope burnt, read once, at the moment the envelope is final: after the transaction's gas accounting has settled and any resource-limit gas rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied. + Those two move the number the receipt reports without anything having been burnt, so a node MUST NOT read `spent` after them. + Gas rescued for the sender, and gas the clamp was hiding, are both out of the envelope by this point and MUST NOT be added back. +- `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints. + The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whatever becomes of the stipend afterwards. + The mint is created when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from that point rather than from the child frame running: an invocation turned away at frame entry — for want of balance, or at the call-depth limit — hands the whole child budget back to the caller with the stipend inside it, which shrinks the envelope against recorded work by exactly as much as a child that ran and returned it would. + An invocation a node halts before handing it to the EVM, which is what a compute-gas limit reached at the call site does, mints nothing and a node MUST NOT count it. + A node MUST add the total back; without it the two sides of the law disagree by exactly that amount. +- `storage_gas` — the MegaETH storage gas the transaction was charged: the storage-gas share of intrinsic gas, the in-frame storage-gas surcharges, the code-deposit charge, and the charges a system contract invocation takes outside an EVM frame. + At a nested-execution boundary this term takes the **difference** between what the nested execution cost the outer gas counter and what it recorded as compute, which can be negative when the nested execution's own EIP-3529 refund outgrew its storage gas; a node MUST NOT clamp that contribution at zero. +- `executed_compute` — the compute gas the transaction is recorded as having performed, fixed site by site by the rules below, and the quantity every resource limit is evaluated against. + It equals the reported total less the destroyed part, but that identity is a consequence of the law rather than a definition of either side. The result is the number a node MUST report as the transaction's destroyed compute gas. A node MUST NOT report a negative result: the law cannot produce one on this spec, and a node that computes one MUST report zero rather than a wrapped value. @@ -546,7 +555,7 @@ A precompile invocation that fails is the same split, taken at the precompile re A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. - **Executed** — the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work. - For KZG the dividing line is its own input-length check, which runs before the commitment is read: an input whose length is not 192 bytes is turned away before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost regardless of how far it got. + For KZG the dividing line is its own input-length check, which runs before the commitment is read: an input whose length is not `KZG_POINT_EVALUATION_INPUT_LENGTH` is turned away before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost regardless of how far it got. A node MUST price an unrecognised non-out-of-gas KZG failure as verification under way, so an unfamiliar failure can only over-charge. A node MUST record the executed part through the ordinary enforcing path. - **Destroyed** — the rest of the call's gas limit: the caller-supplied envelope minus the executed part. @@ -606,16 +615,17 @@ See [Resource Accounting](resource-accounting.md#revert-behavior). ## Constants -| Constant | Value | Spec | Description | -| ------------------------------- | ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- | -| `TX_COMPUTE_GAS_LIMIT` | 200,000,000 | Rex onward | Maximum compute gas per transaction from Rex onward | -| `TX_COMPUTE_GAS_LIMIT` | 1,000,000,000 | MiniRex | Maximum compute gas per transaction under MiniRex | -| `FRAME_LIMIT_NUMERATOR` | 98 | Rex4 onward | Numerator of the per-call-frame budget forwarding fraction | -| `FRAME_LIMIT_DENOMINATOR` | 100 | Rex4 onward | Denominator of the per-call-frame budget forwarding fraction | -| `CALL_STIPEND` | 2,300 | All | Standard EVM value-transfer call stipend, inherited unchanged | -| `CODEDEPOSIT` | 200 | All | Standard EVM per-byte code-deposit gas, inherited unchanged | -| `KEYLESS_DEPLOY_OVERHEAD_GAS` | 100,000 | Rex2 onward | Fixed dispatch overhead for a keyless deploy | -| `KZG_POINT_EVALUATION_GAS_COST` | 100,000 | MiniRex onward | MegaETH's fixed-cost override for the KZG point-evaluation precompile (defined in [Precompiles](precompiles.md)) | +| Constant | Value | Spec | Description | +| ----------------------------------- | ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- | +| `TX_COMPUTE_GAS_LIMIT` | 200,000,000 | Rex onward | Maximum compute gas per transaction from Rex onward | +| `TX_COMPUTE_GAS_LIMIT` | 1,000,000,000 | MiniRex | Maximum compute gas per transaction under MiniRex | +| `FRAME_LIMIT_NUMERATOR` | 98 | Rex4 onward | Numerator of the per-call-frame budget forwarding fraction | +| `FRAME_LIMIT_DENOMINATOR` | 100 | Rex4 onward | Denominator of the per-call-frame budget forwarding fraction | +| `CALL_STIPEND` | 2,300 | All | Standard EVM value-transfer call stipend, inherited unchanged | +| `CODEDEPOSIT` | 200 | All | Standard EVM per-byte code-deposit gas, inherited unchanged | +| `KEYLESS_DEPLOY_OVERHEAD_GAS` | 100,000 | Rex2 onward | Fixed dispatch overhead for a keyless deploy | +| `KZG_POINT_EVALUATION_GAS_COST` | 100,000 | MiniRex onward | MegaETH's fixed-cost override for the KZG point-evaluation precompile (defined in [Precompiles](precompiles.md)) | +| `KZG_POINT_EVALUATION_INPUT_LENGTH` | 192 | All | Required input length in bytes of the KZG point-evaluation precompile, inherited unchanged | The gas detention caps that lower the effective compute gas limit are defined in [Gas Detention](gas-detention.md). diff --git a/docs/spec/overview.md b/docs/spec/overview.md index 4eaa0dc9..f0718b74 100644 --- a/docs/spec/overview.md +++ b/docs/spec/overview.md @@ -62,7 +62,8 @@ A new spec may add behavior, but it never changes what an existing frozen spec d Contracts deployed under a given spec will continue to behave identically, regardless of future upgrades. {% endhint %} -- **EQUIVALENCE** — Baseline. Full Optimism Isthmus compatibility with block environment access tracking for parallel execution. +- **EQUIVALENCE** — Baseline. + Full Optimism Isthmus compatibility with block environment access tracking for parallel execution. - **MINI_REX** — Dual gas model, multidimensional resource limits, gas detention, 98/100 gas forwarding, SELFDESTRUCT disabled, Oracle and Timestamp system contracts. - **REX** — Revised storage gas economics (`base × (multiplier − 1)`), transaction intrinsic storage gas, state growth tracking, consistent CALL-like opcode behavior. - **REX1** — Fix: compute gas limit reset between transactions. @@ -71,7 +72,8 @@ Contracts deployed under a given spec will continue to behave identically, regar - **REX4** — Per-call-frame resource budgets, relative gas detention, [storage gas stipend](glossary.md#storage-gas-stipend), MegaAccessControl and MegaLimitControl system contracts. - **REX5** — SequencerRegistry system contract, Oracle v2.0.0 with dynamic system address, caller-account update deduplication, storage-gas-stipend separated-allowance model, value-transfer CALL/CALLCODE parent compute-gas attribution, CREATE code-deposit compute-gas atomicity, EIP-2935/EIP-4788 pre-block gas floor with fail-closed block rejection, CREATE2 empty-initcode short-circuit, KeylessDeploy trailing-bytes rejection and empty-code log forwarding. - **REX6** — Unified per-opcode gas metering order, consolidated EIP-7702 authorization accounting, CREATE-frame accounting corrections, KeylessDeploy sandbox hardening, post-execution fee-reward accounting, system-originated transaction metering exemption, extended beneficiary detention coverage, and SequencerRegistry v2.0.0 rotation hardening. -- **REX7** — The **unstable** spec, currently open for development. Checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints. +- **REX7** — The **unstable** spec, currently open for development. + Checkpoint-settled compute gas accounting with gas-clamp enforcement; plain opcodes record no compute gas between checkpoints. See [Hardforks and Specs](hardfork-spec.md) for full details. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 3cc80811..79571f45 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -30,10 +30,10 @@ Rex7 also makes two guard- and detention-related choices that Rex6 does not: - A detention mark is produced when the target account is loaded, so a frame that cannot afford the fees charged before that load produces no mark. Two deliberate accounting carve-outs remain. -A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its entire EVM-gas budget as compute gas, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. +A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its whole EVM-gas budget as compute gas, apart from storage gas it had already been charged, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. The reported destroyed total is derived from a conservation law over what the transaction spent rather than summed from the sites that destroyed it, so an envelope lost anywhere lands in it whether or not a site was written to book it. -A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own 192-byte length check included) enforces, and the unused caller-supplied envelope is destroyed. +A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own input-length check included) enforces, and the unused caller-supplied envelope is destroyed. The generic error arm therefore stops enforcing the whole forwarded amount, which is an intentional enforcement difference from Rex6; the Rex5 forwarded-gas cap still prevents the precompile from performing more work than the remaining compute budget. ## What Changed @@ -83,9 +83,9 @@ The interpreter's gas counter already meters every opcode; settling by segment r **Exceptional-halt frame carve-out.** A frame that ends in an exceptional halt — ordinary out-of-gas, memory out-of-gas, stack underflow or overflow, invalid jump, unknown opcode, and every other error result — returns none of its remaining budget. The top-level frame's whole envelope is spent by the transaction's final gas accounting, and an inner frame's remainder is never handed back to its caller. -A node MUST settle that whole budget as compute gas, in two parts that are accounted differently. +A node MUST settle that budget as compute gas in two parts that are accounted differently, except for any MegaETH storage gas a checkpoint body charged before aborting: that charge stays storage gas and is in neither part. -The **executed** part is the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, less any storage gas a checkpoint body charged before aborting. +The **executed** part is the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, net of that storage charge. A node MUST record it through the ordinary path, so it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against — exactly as the same opcodes would if the frame had returned normally. A parent frame keeps executing after it absorbs a failed child; excluding the child's work from enforcement would let the code that follows spend the same compute headroom a second time. @@ -94,7 +94,8 @@ A node MUST record it in the transaction's reported compute-gas total and in blo It is bounded by the sender's gas envelope rather than by the compute limit, so it can carry the reported total past that limit; halting on it would rescue gas the EVM has already destroyed and change a receipt this carve-out requires to stay identical. **The destroyed total is derived, not summed.** -Rex7 does not define a transaction's destroyed compute gas as the sum of what its halted frames, precompiles and system-contract invocations booked. It defines it as a conservation law over the gas the transaction spent: +Rex7 does not define a transaction's destroyed compute gas as the sum of what its halted frames, precompiles and system-contract invocations booked. +It defines it as a conservation law over the gas the transaction spent: `destroyed = spent + minted_stipends − storage_gas − executed_compute` @@ -118,7 +119,7 @@ When a nested execution merges its usage into an outer one, which today is only A precompile invocation that fails is the same split, taken at the precompile recording site rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. The **executed** part is the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work (malformed input, or a wrapper out-of-gas that never reached verification). -For KZG the boundary is its own input-length check, which runs before the commitment is read: an input whose length is not 192 bytes is a rejection before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost, however far it got. +For KZG the boundary is its own input-length check, which runs before the commitment is read: an input whose length is not `KZG_POINT_EVALUATION_INPUT_LENGTH` is a rejection before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost, however far it got. A node MUST price an unrecognised non-out-of-gas KZG failure as verification under way, so that an unfamiliar failure can only over-charge. A node MUST record the executed part through the ordinary enforcing path. The **destroyed** part is the rest of the call's gas limit — the caller-supplied envelope, not the Rex5-capped effective gas limit. @@ -161,7 +162,8 @@ Under Rex7, a node MUST enforce compute-gas and detention limits inside plain-op At each checkpoint, after settlement and after the checkpoint body has recorded its own compute gas (and after any detention cap the checkpoint installs), and again at frame entry and resume, a node MUST: 1. Compute the remaining compute headroom as the minimum of the current frame's remaining per-frame compute budget and the transaction-level remaining budget under the effective limit (including detention). -2. When the interpreter's true remaining gas is at or above that headroom, put the clamp in force for the segment that follows: hide the excess from the interpreter and remember which constraint bound the clamp — the frame-local budget or the transaction-level / detained limit — together with that constraint's own limit value. Equality is a binding clamp that hides nothing, not the absence of a clamp. +2. When the interpreter's true remaining gas is at or above that headroom, put the clamp in force for the segment that follows: hide the excess from the interpreter and remember which constraint bound the clamp — the frame-local budget or the transaction-level / detained limit — together with that constraint's own limit value. + Equality is a binding clamp that hides nothing, not the absence of a clamp. 3. When the true remaining gas is below the headroom, no clamp is in force: the frame's own gas runs out ahead of the compute headroom, and an out-of-gas inside the segment is the inherited EVM's own rather than a resource-limit exceed. 4. Leave the true remaining gas available again before the next checkpoint body runs, before `GAS` is observed, before call-gas forwarding is computed, and before storage-gas charges are taken, so those sites always see the unclamped counter. From 5a51e1f9348548597f920e1bd8f08b934550392d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 17 Aug 2026 14:31:58 +0800 Subject: [PATCH 071/208] docs(precompiles): require explicit work accounting for future halting precompiles --- AGENTS.md | 5 +++++ crates/mega-evm/src/evm/precompiles.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index b71669f0..1594ea1f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -343,6 +343,11 @@ When the agent is requested to implement a new feature or bug fix, it should con Do not expect these schemes to trigger system contract interception. - **System contract interceptor tests must cover boundary behaviors.** Include tests for normal intercepted path, non-zero value behavior, unknown selector fallback, and CALL vs DELEGATECALL/CALLCODE interception boundaries. +- **A new precompile that can halt after doing work must record that work explicitly.** + Under REX7+ the generic precompile-halt arm books zero executed compute gas and destroys the whole forwarded envelope, because every wired precompile halts only on a pre-work input rejection. + A precompile with a do-work-then-halt path (a future verification precompile that halts after running its check) would leave that work unenforced, so a caller could repeat the failure without the transaction- or block-level compute limits ever accounting for it. + Express failure-after-work as a `revert` instead of a halt (the revert arm records actual spend), or give the precompile its own recording arm the way KZG does. + Only code that compiles into the node can register a precompile, so this is a rule for future authors, not an on-chain attack surface. - **Respect `no_std` in `mega-evm` crate.** Do not use `std::` directly. Follow the existing pattern: `#[cfg(not(feature = "std"))] use alloc as std;` then `use std::{vec::Vec, ...};`. diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 8b1acf55..0714781d 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -450,6 +450,11 @@ impl PrecompileProvider Date: Tue, 18 Aug 2026 11:22:39 +0800 Subject: [PATCH 072/208] fix(rex7): key the KZG accounting arm on precompile identity REX7 takes the KZG fixed-fee arm only when the dispatched precompile's id is KzgPointEvaluation. A Custom override at the KZG address falls through to the generic halt arm. Frozen specs keep the address-only match. --- AGENTS.md | 1 + crates/mega-evm/src/evm/precompiles.rs | 168 ++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1594ea1f..486b35ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -348,6 +348,7 @@ When the agent is requested to implement a new feature or bug fix, it should con A precompile with a do-work-then-halt path (a future verification precompile that halts after running its check) would leave that work unenforced, so a caller could repeat the failure without the transaction- or block-level compute limits ever accounting for it. Express failure-after-work as a `revert` instead of a halt (the revert arm records actual spend), or give the precompile its own recording arm the way KZG does. Only code that compiles into the node can register a precompile, so this is a rule for future authors, not an on-chain attack surface. + Do not override the KZG address — or any precompile that has its own recording arm — without updating that arm to match the replacement; a substitute registered under `PrecompileId::KzgPointEvaluation` is a new precompile and must ship its own arm. - **Respect `no_std` in `mega-evm` crate.** Do not use `std::` directly. Follow the existing pattern: `#[cfg(not(feature = "std"))] use alloc as std;` then `use std::{vec::Vec, ...};`. diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 0714781d..7ede0220 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -24,7 +24,7 @@ use revm::{ context_interface::ContextTr, handler::{precompile_output_to_interpreter_result, EthPrecompiles, PrecompileProvider}, interpreter::{CallInputs, Gas, InterpreterResult}, - precompile::{PrecompileHalt, Precompiles}, + precompile::{PrecompileHalt, PrecompileId, Precompiles}, primitives::{Address, AddressSet, HashMap}, }; @@ -329,6 +329,11 @@ impl PrecompileProvider PrecompileProvider= GAS_COST`): the call got through the gas gate and into the upstream // body, so how far it got decides what was performed. Address match uses // `bytecode_address` (see above) so DELEGATECALL/CALLCODE to KZG still hit this arm. - // Through REX6 the fixed cost is charged for every halt this arm sees and is the - // whole recorded charge; REX7 splits it by halt reason (below), keeping the performed - // part on the enforcing lane and booking the rest of the forwarded envelope — the - // caller-supplied `gas_limit`, not the REX5-capped effective limit — as destroyed. - // The REX5 cap still prevents the precompile from *doing* more work than the - // remaining compute budget; the cap gap is part of the forwarded envelope, not work, - // so it belongs with the destroyed remainder. - // * All other error paths (non-KZG, or KZG with `limit() < GAS_COST` meaning the - // wrapper's pre-check itself OOG'd before the body could run): after the spend_all - // undo above, `total_gas_spent() == 0` again. Through REX6 the parent still + // Through REX6 the arm is address-only and the fixed cost is charged for every halt + // it sees. REX7 also requires the dispatched precompile's identity to be + // `KzgPointEvaluation` — a `Custom` (or any other) override at the KZG address is a + // different implementation and falls through to the generic arm — then splits the + // charge by halt reason (below), keeping the performed part on the enforcing lane and + // booking the rest of the forwarded envelope — the caller-supplied `gas_limit`, not + // the REX5-capped effective limit — as destroyed. The REX5 cap still prevents the + // precompile from *doing* more work than the remaining compute budget; the cap gap is + // part of the forwarded envelope, not work, so it belongs with the destroyed + // remainder. + // * All other error paths (non-KZG, KZG with `limit() < GAS_COST` meaning the wrapper's + // pre-check itself OOG'd before the body could run, or — REX7 only — a + // non-`KzgPointEvaluation` implementation sitting at the KZG address): after the + // spend_all undo above, `total_gas_spent() == 0` again. Through REX6 the parent still // permanently loses the forwarded amount, so those specs record `limit()` as // enforcing usage to match the EVM-gas burn. REX7 treats the same path as // performed-zero / destroyed-all: no work ran, so nothing enforces, and the forwarded @@ -400,12 +409,19 @@ impl PrecompileProvider= kzg_point_evaluation::GAS_COST; + // REX7 keys the fixed-fee arm on the dispatched identity, not just the address. + // Frozen specs keep the address-only match, including a Custom override of KZG. + let take_kzg_fixed_fee_arm = if is_rex7 { + address_clears_kzg_gas_gate && is_kzg_identity + } else { + address_clears_kzg_gas_gate + }; let mut additional_limit = context.additional_limit.borrow_mut(); if output.result.is_ok_or_revert() { additional_limit.record_compute_gas(output.gas.total_gas_spent()); - } else if address == kzg_point_evaluation::ADDRESS && - output.gas.limit() >= kzg_point_evaluation::GAS_COST - { + } else if take_kzg_fixed_fee_arm { // Inside the KZG body the halt reason marks how much of the fixed-price work // the call bought before failing. `BlobInvalidInputLength` is the doorway: // the input length is checked first, so a rejected length means the @@ -455,6 +471,10 @@ impl PrecompileProvider (InstructionResult, u64, u64) { + let mut db = MemoryDatabase::default(); + let mut context = MegaContext::new(&mut db, spec); + let mut precompiles_map = + PrecompilesMap::from_static(MegaPrecompiles::new_with_spec(spec).precompiles()); + precompiles_map.apply_precompile(&address, move |_| { + Some(DynPrecompile::new(id, |input| { + Ok(PrecompileOutput::halt(PrecompileHalt::OutOfGas, input.reservoir)) + })) + }); + let inputs = InputsImpl { + target_address: address, + bytecode_address: Some(address), + caller_address: address, + input: revm::interpreter::CallInput::Bytes(Bytes::new()), + call_value: Default::default(), + }; + + let output = precompiles_map + .run(&mut context, &call_inputs(&inputs, address, true, forwarded_gas)) + .expect("run ok") + .expect("Some output"); + assert!(!output.result.is_ok_or_revert(), "the probe must halt; got {:?}", output.result,); + + let additional = context.additional_limit.borrow(); + (output.result, additional.get_usage().compute_gas, additional.burned_compute_gas()) + } + + /// REX7: a Custom override of the KZG address is not the wired KZG implementation, so + /// the fixed-fee arm must not run. The generic arm books executed 0 and destroys the + /// whole forwarded envelope. + #[test] + fn test_rex7_custom_override_at_kzg_address_takes_the_generic_halt_arm() { + let kzg = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded = 1_000_000u64; + + let (result, reported, destroyed) = record_dyn_precompile_halt( + MegaSpecId::REX7, + kzg, + PrecompileId::Custom("kzg-override-halt".into()), + forwarded, + ); + + assert!( + matches!(result, InstructionResult::PrecompileOOG), + "the override's cheap halt must surface as PrecompileOOG; got {result:?}", + ); + assert_eq!(reported, forwarded, "reported compute is the whole forwarded envelope"); + assert_eq!(destroyed, forwarded, "the generic arm destroys the whole envelope"); + assert_eq!(reported - destroyed, 0, "executed work must be zero, not the KZG fixed fee"); + } + + /// Frozen pin of the same override: REX6 still keys the fixed-fee arm on address alone, + /// so a Custom halt at the KZG address is charged as wired KZG. + #[test] + fn test_rex6_custom_override_at_kzg_address_still_takes_the_kzg_arm() { + let kzg = revm::precompile::kzg_point_evaluation::ADDRESS; + let forwarded = 1_000_000u64; + + let (result, reported, destroyed) = record_dyn_precompile_halt( + MegaSpecId::REX6, + kzg, + PrecompileId::Custom("kzg-override-halt".into()), + forwarded, + ); + + assert!( + matches!(result, InstructionResult::PrecompileOOG), + "the override's cheap halt must surface as PrecompileOOG; got {result:?}", + ); + assert_eq!(reported, GAS_COST, "REX6 charges the KZG fixed fee for an address match"); + assert_eq!(destroyed, 0, "REX6 has no destroyed lane"); + } + + /// REX7: a Custom dyn-precompile halt at a non-KZG address is the generic arm — + /// executed 0, whole envelope destroyed. The parity matrix only pins the revert shape. + #[test] + fn test_rex7_custom_dyn_halt_at_plain_address_takes_the_generic_halt_arm() { + let forwarded = 1_000_000u64; + + let (result, reported, destroyed) = record_dyn_precompile_halt( + MegaSpecId::REX7, + DYN_HALT_ADDRESS, + PrecompileId::Custom("plain-halt".into()), + forwarded, + ); + + assert!( + matches!(result, InstructionResult::PrecompileOOG), + "the dyn halt must surface as PrecompileOOG; got {result:?}", + ); + assert_eq!(reported, forwarded, "reported compute is the whole forwarded envelope"); + assert_eq!(destroyed, forwarded, "the generic arm destroys the whole envelope"); + assert_eq!(reported - destroyed, 0, "executed work must be zero"); + } + // ── seam parity: the mirror must still be the upstream call ────────────────────── // // `run_precompile_capturing_halt` reimplements alloy-evm's `PrecompilesMap::run` so the From 594d50e703bee5162d85effdf35208f5378dff83 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 18 Aug 2026 11:45:28 +0800 Subject: [PATCH 073/208] test(rex7): reconcile the tracker lanes with the receipt on every transaction A REX7 transaction's reported compute total, the MegaETH storage gas it was charged, and the CALL_STIPEND the EVM minted into its child frames are the terms the destroyed remainder is derived from, so once settlement has run they must add back up to the envelope the receipt reports. Nothing checked that. The settlement site's own derived-versus-booked cross-check cannot: it is happy whenever settlement runs, and says nothing about a settlement that never ran or an envelope decided after it. Assert the identity in two places. The shared REX7 test helpers now funnel every transaction through one assembly point that checks it, which turns the whole suite into a checker rather than only the tests written to look at gas. A debug-only assertion at the transaction-outcome construction point extends the same check to every corpus that runs through the crate in a debug build. Anchoring on the pre-refund envelope is what keeps the identity correction-free: the EIP-3529 refund and the EIP-7623 floor move the receipt's number without anyone having burnt the difference, and both are carried as their own result fields rather than folded into the envelope. --- crates/mega-evm/src/evm/mod.rs | 68 ++++++- crates/mega-evm/tests/rex7/burn_split.rs | 17 +- crates/mega-evm/tests/rex7/common.rs | 173 +++++++++++++++--- .../tests/rex7/guard_pass_static_gas.rs | 19 +- 4 files changed, 220 insertions(+), 57 deletions(-) diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 2d683602..091c48d6 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -68,7 +68,7 @@ use revm::{ ExecuteEvm, InspectEvm, Inspector, Journal, }; -use crate::{BucketId, ExternalEnvTypes, LimitUsage, MegaTransaction}; +use crate::{AdditionalLimit, BucketId, ExternalEnvTypes, LimitUsage, MegaTransaction}; /// The main EVM implementation for the `MegaETH` chain. /// @@ -359,10 +359,12 @@ where } else { ExecuteEvm::transact(self, tx)? }; + let is_inside_sandbox = self.ctx().is_inside_sandbox(); + let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); let LimitUsage { data_size, kv_updates, compute_gas, state_growth } = additional_limit.get_usage(); - Ok(MegaTransactionOutcome { + let outcome = MegaTransactionOutcome { result_and_state, data_size, kv_updates, @@ -370,7 +372,9 @@ where compute_gas_destroyed: additional_limit.destroyed_compute_gas(), compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, - }) + }; + debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); + Ok(outcome) } /// Inspect a transaction and return the outcome. The inspector used is the one set up already @@ -392,10 +396,12 @@ where tx: MegaTransaction, ) -> Result> { let result_and_state = InspectEvm::inspect_tx(self, tx)?; + let is_inside_sandbox = self.ctx().is_inside_sandbox(); + let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); let LimitUsage { data_size, kv_updates, compute_gas, state_growth } = additional_limit.get_usage(); - Ok(MegaTransactionOutcome { + let outcome = MegaTransactionOutcome { result_and_state, data_size, kv_updates, @@ -403,7 +409,9 @@ where compute_gas_destroyed: additional_limit.destroyed_compute_gas(), compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, - }) + }; + debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); + Ok(outcome) } /// Get the bucket IDs used during transaction execution. @@ -416,6 +424,56 @@ where } } +/// Debug-only check that a transaction's tracker lanes account for the whole envelope its receipt +/// reports (REX7+; before REX7 there is no destroyed lane and no non-compute lane, so there is +/// nothing to reconcile). +/// +/// The reported compute total, the `MegaETH` storage gas, and the `CALL_STIPEND` the EVM minted +/// into child frames are the three terms the destroyed remainder is derived from, so once +/// settlement has run they must add back up to the envelope the transaction burnt: +/// +/// ```text +/// compute_gas_used + non_compute_gas − minted_call_stipend == total_gas_spent +/// ``` +/// +/// The EIP-3529 refund and the EIP-7623 floor move the number a receipt reports without anyone +/// having burnt the difference; both are carried on the result as their own fields and applied +/// after the envelope is final, so the envelope this compares against is unaffected by either. +/// +/// What this catches that the settlement site's own cross-check cannot: a result whose envelope is +/// decided *after* settlement, or a path that produces a receipt without settling at all. Both +/// leave the settlement site's derived-versus-booked comparison perfectly happy and the reported +/// total wrong. A failed OP deposit is such a path — its receipt is rebuilt to report the whole +/// gas limit at the outermost error boundary — and is settled explicitly there. +/// +/// Skipped inside a keyless-deploy sandbox: a sandbox transaction never settles a derivation of +/// its own, because the law is stated over an outer transaction's final envelope and the sandbox's +/// gas is a charge inside its parent's. +#[inline] +fn debug_assert_envelope_accounted( + spec: MegaSpecId, + is_inside_sandbox: bool, + additional_limit: &AdditionalLimit, + outcome: &MegaTransactionOutcome, +) { + if cfg!(debug_assertions) && spec.is_enabled(MegaSpecId::REX7) && !is_inside_sandbox { + let envelope = outcome.result_and_state.result.gas().total_gas_spent(); + let accounted = i128::from(outcome.compute_gas_used) + additional_limit.non_compute_gas() - + i128::from(additional_limit.minted_call_stipend()); + debug_assert!( + accounted == i128::from(envelope), + "the tracker lanes must account for the whole receipt envelope: \ + accounted {accounted} vs envelope {envelope} \ + (compute {}, non-compute {}, minted stipend {}, destroyed {}, enforced {})", + outcome.compute_gas_used, + additional_limit.non_compute_gas(), + additional_limit.minted_call_stipend(), + outcome.compute_gas_destroyed, + outcome.compute_gas_enforced, + ); + } +} + impl MegaEvm { /// Get the block hashes used during transaction execution. /// diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 490f5021..ee0ad9d9 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -25,7 +25,7 @@ //! which side of the enforcing boundary each part lands on. use crate::common::{ - transact, transact_default, transact_tx, transact_with_bucket_capacity, + finish, transact, transact_default, transact_tx, transact_with_bucket_capacity, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, }; use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; @@ -437,21 +437,14 @@ fn transact_create_reject( booked_destroyed, ) }; - let gas_used = outcome.result_and_state.result.tx_gas_used(); - Outcome { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - destroyed: outcome.compute_gas_destroyed, + finish( + MegaSpecId::REX7, + outcome, detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed, - state: outcome.result_and_state.state, - } + ) } /// Runtime length the CREATE cases deploy — small enough that the per-byte code-deposit storage diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 773234e7..bcb3b782 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -3,7 +3,7 @@ use alloy_primitives::{address, Address, Bytes, B256, U256}; use mega_evm::{ test_utils::MemoryDatabase, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaHaltReason, - MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, }; use revm::{ context::{result::ExecutionResult, tx::TxEnvBuilder, TxEnv}, @@ -40,6 +40,12 @@ pub(crate) struct Outcome { /// The part of [`compute_gas`](Self::compute_gas) an exceptionally halted frame destroyed /// rather than performed (REX7+, else 0). pub(crate) destroyed: u64, + /// Post-tx enforced compute gas — the part of [`compute_gas`](Self::compute_gas) every limit + /// comparison and the block's admission counter run against. + pub(crate) enforced_lane: u64, + /// Receipt envelope before the EIP-3529 refund and the EIP-7623 floor: exactly the number + /// settlement derives the destroyed total from. + pub(crate) total_gas_spent: u64, /// Post-tx detained compute gas limit — equal to the configured TX limit unless volatile /// access lowered it. pub(crate) detained_compute_gas_limit: u64, @@ -69,8 +75,12 @@ impl Outcome { } /// The part of the reported compute total that a resource limit is evaluated against. + /// + /// Read from the tracker's own lane rather than subtracted from the reported total; the two + /// agree because [`assert_terminal_identity`] checks that they do on every transaction the + /// helpers in this module run. pub(crate) fn enforced(&self) -> u64 { - self.compute_gas - self.destroyed + self.enforced_lane } /// Reads a storage slot out of the produced state, defaulting to zero when the transaction @@ -128,8 +138,33 @@ pub(crate) fn transact_with_gas_limit( booked_destroyed, ) }; + finish( + spec, + outcome, + detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) +} + +/// Assembles an [`Outcome`] from what a transaction reported and checks the terminal identity +/// before handing it back. +/// +/// Every helper in this module funnels through here, so every REX7 transaction the suite runs — +/// not just the ones written to look at gas — is a check that the tracker lanes reconcile with the +/// receipt the transaction produced. +pub(crate) fn finish( + spec: MegaSpecId, + outcome: MegaTransactionOutcome, + detained_compute_gas_limit: u64, + non_compute_gas: i128, + minted_call_stipend: u64, + booked_destroyed: u64, +) -> Outcome { let gas_used = outcome.result_and_state.result.tx_gas_used(); - Outcome { + let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); + let outcome = Outcome { result: outcome.result_and_state.result, compute_gas: outcome.compute_gas_used, data_size: outcome.data_size, @@ -137,12 +172,96 @@ pub(crate) fn transact_with_gas_limit( state_growth: outcome.state_growth_used, gas_used, destroyed: outcome.compute_gas_destroyed, + enforced_lane: outcome.compute_gas_enforced, + total_gas_spent, detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed, state: outcome.result_and_state.state, + }; + assert_terminal_identity(spec, &outcome); + outcome +} + +/// The identity every REX7 transaction that produces a receipt must satisfy, connecting what the +/// trackers hold to the number the receipt reports. +/// +/// # The identity +/// +/// For one transaction, write +/// +/// ```text +/// C = compute_gas reported compute total +/// E = enforced_lane the part limits and block admission compare against +/// D = destroyed the part that is reported and accounted but never enforced +/// N = non_compute_gas MegaETH storage gas plus the sandbox boundary residue (signed) +/// M = minted_call_stipend CALL_STIPEND minted into child frames and never debited from a caller +/// S = total_gas_spent the receipt envelope, before the refund and the floor +/// R = the receipt's raw refund +/// F = the receipt's EIP-7623 floor gas +/// ``` +/// +/// then +/// +/// ```text +/// (1) C = E + D +/// (2) C + N − M = S +/// (3) receipt gas_used = max(S − R, F) +/// ``` +/// +/// (1) is the split of the reported total. (2) is the conservation law rearranged: settlement +/// defines `D = S + M − N − E`, so `S = D + E + N − M`, and substituting (1) gives `S = C + N − M`. +/// (3) is how a receipt's gas number is built from its envelope. +/// +/// # Why (2) needs no refund or floor correction +/// +/// The EIP-3529 refund and the EIP-7623 floor both move the number the receipt reports without +/// anyone having burnt the difference. Both are applied strictly after the envelope is final, and +/// both are carried on the result as their own fields rather than folded into the envelope, so +/// anchoring on `S` — the same value settlement reads — keeps them out of the identity entirely. +/// Substituting (2) into (3) gives the receipt-level form, which is what a reader normally wants: +/// +/// ```text +/// receipt gas_used = max(C + N − M − R, F) +/// ``` +/// +/// # What it catches +/// +/// (2) fails whenever a transaction's envelope moves without a `MegaETH` site accounting for it — +/// a settlement that never ran, a result rewritten after settlement, an upstream subsidy nobody +/// records. (1) fails when the reported split disagrees with the per-site bookings, which is what +/// the block's admission counter reads. Pre-REX7 specs have neither a destroyed lane nor a +/// non-compute lane, so the identity is REX7-only by construction. +fn assert_terminal_identity(spec: MegaSpecId, outcome: &Outcome) { + if !spec.is_enabled(MegaSpecId::REX7) { + return; } + assert_eq!( + outcome.compute_gas, + outcome.enforced_lane + outcome.destroyed, + "reported compute gas must split into enforced + destroyed; \ + compute={} enforced={} destroyed={} result={:?}", + outcome.compute_gas, + outcome.enforced_lane, + outcome.destroyed, + outcome.result, + ); + let accounted = i128::from(outcome.compute_gas) + outcome.non_compute_gas - + i128::from(outcome.minted_call_stipend); + assert_eq!( + accounted, + i128::from(outcome.total_gas_spent), + "the tracker lanes must account for the whole receipt envelope; \ + compute={} non_compute={} minted_stipend={} accounted={accounted} envelope={} \ + (receipt gas_used={}) result={:?}", + outcome.compute_gas, + outcome.non_compute_gas, + outcome.minted_call_stipend, + outcome.total_gas_spent, + outcome.gas_used, + outcome.result, + ); } /// Runs [`transact`] with the spec's default runtime limits. @@ -165,10 +284,24 @@ pub(crate) fn default_envs() -> TestExternalEnvs { /// so a test can read back what execution recorded into it (oracle hints, for instance). pub(crate) fn transact_tx( spec: MegaSpecId, - mut db: MemoryDatabase, + db: MemoryDatabase, limits: EvmTxRuntimeLimits, tx: TxEnv, envs: &TestExternalEnvs, +) -> Outcome { + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + transact_mega_tx(spec, db, limits, tx, envs) +} + +/// [`transact_tx`] for the shapes that need the `MegaETH` transaction itself, not just its +/// `TxEnv` — a deposit's `source_hash` and `mint` live on the outer type. +pub(crate) fn transact_mega_tx( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + tx: MegaTransaction, + envs: &TestExternalEnvs, ) -> Outcome { let mut context = MegaContext::new(&mut db, spec) .with_external_envs(envs.into()) @@ -177,8 +310,6 @@ pub(crate) fn transact_tx( chain.operator_fee_scalar = Some(U256::from(0)); chain.operator_fee_constant = Some(U256::from(0)); }); - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { @@ -192,21 +323,14 @@ pub(crate) fn transact_tx( booked_destroyed, ) }; - let gas_used = outcome.result_and_state.result.tx_gas_used(); - Outcome { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - destroyed: outcome.compute_gas_destroyed, + finish( + spec, + outcome, detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed, - state: outcome.result_and_state.state, - } + ) } /// The part of an account a transaction's state actually asserts. @@ -346,19 +470,12 @@ pub(crate) fn transact_with_bucket_capacity( booked_destroyed, ) }; - let gas_used = outcome.result_and_state.result.tx_gas_used(); - Outcome { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - destroyed: outcome.compute_gas_destroyed, + finish( + spec, + outcome, detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed, - state: outcome.result_and_state.state, - } + ) } diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index f85fa6df..e34e0b75 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -13,7 +13,9 @@ //! `MegaLimitControl.remainingComputeGas` would return after the transaction: the TX-level //! remaining, which is what the interceptor reads once the frames have been popped. -use crate::common::{transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH}; +use crate::common::{ + finish, transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, +}; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::{SolCall as _, SolError}; use mega_evm::{ @@ -117,21 +119,14 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { ) }; let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); - let gas_used = executed.result_and_state.result.tx_gas_used(); - let outcome = Outcome { - result: executed.result_and_state.result, - compute_gas: executed.compute_gas_used, - data_size: executed.data_size, - kv_updates: executed.kv_updates, - state_growth: executed.state_growth_used, - gas_used, - destroyed: executed.compute_gas_destroyed, + let outcome = finish( + MegaSpecId::REX7, + executed, detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed, - state: executed.result_and_state.state, - }; + ); GuardPassRun { outcome, accessed, remaining_compute_gas } } From 2a71acfda063f4d88be1abdd6c5c7df9efaa2806 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 18 Aug 2026 11:46:27 +0800 Subject: [PATCH 074/208] fix(rex7): account for the envelope a failed deposit's receipt reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An OP deposit is not allowed to fail, so a deposit that does fail has its result rebuilt: state rolls back to the sender's nonce bump and the mint, and the receipt reports the transaction's whole gas limit. That rebuild happens at the outermost error boundary, past every site that records or settles compute gas, so nothing on the MegaETH side saw it. A deposit rejected in validation reported only the standard-EVM share of its intrinsic gas against a receipt burning far more; a deposit stopped by a per-transaction resource limit reported the envelope its gas rescue had shrunk, while the receipt was raised back to the full limit. Settle the rewritten envelope at that boundary. The difference between what the conservation law derives for the rebuilt envelope and what the per-site bookings already hold is destroyed compute gas — the receipt burns it and nothing was executed for it — so it goes to the non-enforcing lane and the derivation is re-settled against the rebuilt envelope. Enforcement does not move: what the per-transaction limits and the block's admission counter read stays exactly the work the transaction performed, which is what keeps a deposit rejected before it ran anything from consuming block compute capacity. Skipped inside a keyless-deploy sandbox, whose own rejected transactions never settle a derivation: the law is stated over an outer transaction's final envelope, and the sandbox's gas is a charge inside its parent's. Pre-REX7 specs have no destroyed lane and are untouched. Also corrects the premise the old accounting rested on, in the spec pages, the intrinsic-gas recording site, and the test module that pinned it: a validation reject producing no receipt is true of ordinary transactions only. --- crates/mega-evm/src/evm/execution.rs | 29 +- crates/mega-evm/src/limit/limit.rs | 63 +++ .../tests/rex7/deposit_receipt_rewrite.rs | 476 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 8 +- .../rex7/pre_execution_intrinsic_reject.rs | 6 + docs/spec/evm/compute-gas.md | 12 +- docs/spec/upgrades/rex7.md | 11 +- 7 files changed, 596 insertions(+), 9 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index df17a8a1..e6582d53 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -956,9 +956,14 @@ where // Book the MegaETH share of intrinsic gas — calldata storage gas, the flat REX // intrinsic storage gas, and the callee-side / deposit-caller account-creation gas — - // as non-compute gas. Deliberately last: every contribution above is inside it, and - // the paths that return early from here are validation rejects, which never reach a - // settlement that would read the lane. + // as non-compute gas. Deliberately last: every contribution above is inside it. + // + // The paths that return early from here are validation rejects, which leave the lanes + // holding nothing but the base intrinsic already recorded as compute. For an ordinary + // transaction that is the end of it: there is no receipt, so no settlement ever reads + // the lanes. A deposit is the exception — it is not allowed to fail, so its receipt is + // rebuilt to report the whole gas limit, and the boundary that rebuilds it settles the + // difference against exactly this pair of lanes. ctx.additional_limit().borrow_mut().record_non_compute_gas(i128::from( initial_and_floor_gas.initial_regular_gas.saturating_sub(base_intrinsic_gas), )); @@ -1155,6 +1160,24 @@ where error: Self::Error, ) -> Result, Self::Error> { let result = self.op.catch_error(evm, error)?; + + // Reaching an `Ok` here means one thing: op-revm rewrote a failed deposit into a receipt + // that reports the transaction's whole gas limit. Its `output` starts as the incoming + // error and is replaced only on that branch, so every other error still propagates as + // `Err` and produces no receipt at all. The rewrite is the last thing that happens to the + // transaction's envelope, after the journal has been rolled back and after any settlement + // the transaction reached — so it is the only place the rewritten envelope can be booked. + // + // Skipped inside a keyless-deploy sandbox. The conservation law is stated over an outer + // transaction's final envelope; a sandbox transaction's gas is a charge inside its + // parent's envelope, and the parent settles once, later, over its own. A sandbox tx that + // is rewritten here is a validation reject, whose whole reservation the interceptor hands + // back and whose usage never crosses the boundary. + if !evm.ctx().is_inside_sandbox() { + let envelope_gas_spent = result.gas().total_gas_spent(); + evm.ctx().additional_limit().borrow_mut().settle_rewritten_envelope(envelope_gas_spent); + } + // Belt-and-braces: op-revm already reverts the journal to the default checkpoint before // building FailedDeposit, so logs are already empty. Clearing is idempotent. Ok(strip_logs_if_not_success(result.map_haltreason(MegaHaltReason::Base))) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 0675d41f..fa33767e 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -334,6 +334,69 @@ impl AdditionalLimit { self.checkpoint.set_settled_destroyed(settled); } + /// Settles a transaction whose reported envelope is rewritten after every `MegaETH` + /// settlement has already run (REX7+; a no-op before, where nothing is destroyed). + /// + /// One such rewrite exists. An OP deposit is not allowed to fail, so a deposit that does fail + /// has its receipt rebuilt to report the whole `gas_limit`, with the journal rolled back to + /// nothing but the nonce bump and the mint. That rebuild happens at the outermost error + /// boundary, past every site that records or settles, so neither the per-site bookings nor + /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) can see it. Two + /// shapes arrive here: + /// + /// - A validation reject, which never reached a settlement at all. Its lanes hold only what + /// `validate` recorded before returning the error, and the whole rest of the rewritten + /// envelope is unaccounted. + /// - An execution halt, which settled correctly against the envelope it really burnt and is + /// then raised back to `gas_limit`. The gap is exactly what the resource-limit rescue had + /// handed back to the sender, which the rewrite takes away again. + /// + /// Both are the same accounting event: the receipt burns an envelope that nothing was + /// executed for. So the difference between what the conservation law derives for the rewritten + /// envelope and what the per-site bookings already hold is destroyed compute gas. Booking it + /// makes the reported total cover the receipt; re-settling against the rewritten envelope + /// keeps the derived total and the bookings agreeing, which is what the cross-check in + /// `settle_destroyed_compute_gas` verifies. + /// + /// Enforcement is deliberately untouched. [`record_burned_gas`](Self::record_burned_gas) + /// raises the reported total and the destroyed lane by the same amount, so + /// [`enforced_compute_gas`](Self::enforced_compute_gas) — what every limit comparison and the + /// block's admission counter read — does not move. A deposit rejected before it executed + /// anything must not consume block compute capacity for work it never performed. + /// + /// The difference is non-negative on every shape that reaches here. The rewritten envelope is + /// the transaction's `gas_limit`. A rejected deposit's lanes hold at most the intrinsic gas + /// requirement it had already cleared against that limit when the reject fired, and nothing is + /// booked as destroyed yet. A halted deposit settled against the same limit less whatever the + /// resource-limit rescue returned, so raising the envelope back to the limit can only add. + /// + /// The one shape that would break that is a synthetic pre-frame halt which books a destroyed + /// remainder without settling — it would leave the `MegaETH` share of intrinsic gas booked as + /// non-compute *and* the whole envelope booked as destroyed, double-counting it. No spec with + /// the destroyed lane reaches one: an intrinsic overrun has been a validation reject since + /// REX5. A spec that re-opened that path would have to settle it at its own site. Debug builds + /// trip on a negative difference; release builds book nothing for it. + #[inline] + pub(crate) fn settle_rewritten_envelope(&mut self, envelope_gas_spent: u64) { + if !self.rex7_enabled() { + return; + } + let unbooked = self.derived_burned_compute_gas(envelope_gas_spent) - + i128::from(self.burned_compute_gas()); + debug_assert!( + unbooked >= 0, + "rewritten envelope destroys a negative amount: {unbooked} \ + (envelope {envelope_gas_spent}, minted stipend {}, non-compute {}, \ + enforced compute {}, booked destroyed {})", + self.minted_call_stipend(), + self.non_compute_gas(), + self.enforced_compute_gas(), + self.burned_compute_gas(), + ); + self.record_burned_gas(u64::try_from(unbooked.max(0)).unwrap_or(u64::MAX)); + self.settle_destroyed_compute_gas(envelope_gas_spent); + } + /// The transaction's destroyed compute gas, as settled by /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) — the part of /// [`get_usage`](Self::get_usage)'s `compute_gas` that is reported and accounted but never diff --git a/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs new file mode 100644 index 00000000..a55eb570 --- /dev/null +++ b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs @@ -0,0 +1,476 @@ +//! A failed OP deposit's receipt is rewritten to report the whole gas limit, after every `MegaETH` +//! settlement has already run. +//! +//! An OP deposit is not allowed to fail, so when one does, its receipt is rebuilt at the outermost +//! error boundary: the journal is rolled back to nothing but the nonce bump and the mint, and the +//! reported gas becomes the transaction's whole `gas_limit`. Two shapes arrive there, and they +//! start from opposite accounting positions: +//! +//! - a validation reject, which never reached a settlement at all, so its lanes hold only the +//! intrinsic compute gas `validate` recorded before returning the error; +//! - an execution halt, which settled correctly against the envelope it burnt and is then raised +//! back to `gas_limit`, re-taking whatever the resource-limit rescue had handed back. +//! +//! Both end at the same place: a receipt burning an envelope for which nothing was executed. The +//! boundary books the difference as destroyed compute gas, so the reported total covers the +//! receipt while the enforced total — what the per-tx limits and the block's admission counter +//! read — stays exactly the work the transaction performed. +//! +//! What is deliberately not affected: pre-REX7 specs, which have no destroyed lane and whose lane +//! values here must stay what they always were; and the keyless-deploy sandbox, whose own rejected +//! transactions never settle a derivation, because the law is stated over an outer transaction's +//! final envelope. + +use crate::common::{transact_mega_tx, transact_tx, Outcome, ONE_ETH}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::{SolCall as _, SolError as _}; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + constants::rex::TX_INTRINSIC_STORAGE_GAS, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, + MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, +}; +use revm::{ + bytecode::opcode::{INVALID, JUMP, JUMPDEST}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + inspector::NoOpInspector, +}; +use std::vec::Vec; + +/// Sender of the deposit transactions. +const DEPOSIT_CALLER: Address = address!("0000000000000000000000000000000000350000"); +/// Callee of the deposit transactions. +const TARGET: Address = address!("0000000000000000000000000000000000350001"); +/// Relayer that sends the keyless-deploy transaction. +const RELAYER: Address = address!("0000000000000000000000000000000000350002"); + +/// Standard EVM intrinsic gas for a plain call with no calldata and no access list — the whole of +/// what `validate` records as compute gas before the first frame opens. +const BASE_INTRINSIC_GAS: u64 = 21_000; + +/// What a plain deposit call must supply before a frame can open: the standard EVM intrinsic plus +/// `MegaETH`'s flat intrinsic storage gas, which is charged to the envelope but is not compute. +const INTRINSIC_REQUIREMENT: u64 = BASE_INTRINSIC_GAS + TX_INTRINSIC_STORAGE_GAS; + +/// A source hash no `MegaETH` component produces, so the deposit is an ordinary user deposit +/// rather than a system-originated one. +fn user_source_hash() -> B256 { + B256::repeat_byte(0x42) +} + +/// A deposit transaction calling [`TARGET`], with the given source hash and gas limit. +fn deposit_tx(source_hash: B256, gas_limit: u64) -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(DEPOSIT_CALLER) + .call(TARGET) + .gas_limit(gas_limit) + .gas_price(0) + .build_fill(), + ); + tx.deposit.source_hash = source_hash; + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// The same call as [`deposit_tx`], as an ordinary (non-deposit) transaction — the control that +/// shows what the receipt would have reported without the rewrite. +fn plain_tx(gas_limit: u64) -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(DEPOSIT_CALLER) + .call(TARGET) + .gas_limit(gas_limit) + .gas_price(0) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// A funded sender, so the deposit never owes caller-materialization storage gas, plus whatever +/// code [`TARGET`] needs for the shape under test. +fn db(target_code: Option) -> MemoryDatabase { + let db = MemoryDatabase::default().account_balance(DEPOSIT_CALLER, U256::from(ONE_ETH)); + match target_code { + Some(code) => db.account_code(TARGET, code), + None => db, + } +} + +fn run(spec: MegaSpecId, target_code: Option, tx: MegaTransaction) -> Outcome { + transact_mega_tx( + spec, + db(target_code), + EvmTxRuntimeLimits::from_spec(spec), + tx, + &TestExternalEnvs::default(), + ) +} + +fn run_with_compute_limit( + spec: MegaSpecId, + target_code: Option, + tx: MegaTransaction, + tx_compute_gas_limit: u64, +) -> Outcome { + transact_mega_tx( + spec, + db(target_code), + EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(tx_compute_gas_limit), + tx, + &TestExternalEnvs::default(), + ) +} + +/// Asserts the shape every rewritten receipt has: a `FailedDeposit` halt reporting the whole gas +/// limit. +fn assert_failed_deposit(outcome: &Outcome, gas_limit: u64, label: &str) { + let rendered = std::format!("{:?}", outcome.halt_reason(label)); + assert!( + rendered.contains("FailedDeposit"), + "{label}: a failed deposit must be reported as FailedDeposit, got {rendered}", + ); + assert_eq!( + outcome.gas_used, gas_limit, + "{label}: a failed deposit's receipt reports the whole gas limit", + ); +} + +/// Runtime code that loops forever, so the transaction stops on a budget rather than on its own. +fn spin_forever() -> Bytes { + BytecodeBuilder::default().append(JUMPDEST).push_number(0u8).append(JUMP).build() +} + +/// A deposit one gas short of its own intrinsic requirement never reaches execution: `validate` +/// rejects it after recording the standard EVM intrinsic as compute and before booking the +/// `MegaETH` share as non-compute. The receipt still reports the whole gas limit, so everything +/// past the intrinsic is an envelope that nothing was executed for. +#[test] +fn test_underfunded_deposit_reject_settles_the_rewritten_envelope() { + let gas_limit = INTRINSIC_REQUIREMENT - 1; + let outcome = run(MegaSpecId::REX7, None, deposit_tx(user_source_hash(), gas_limit)); + + assert_failed_deposit(&outcome, gas_limit, "underfunded deposit"); + assert_eq!( + outcome.enforced(), + BASE_INTRINSIC_GAS, + "only the intrinsic validate recorded may enforce — the transaction executed nothing", + ); + assert_eq!( + outcome.destroyed, + gas_limit - BASE_INTRINSIC_GAS, + "the rest of the rewritten envelope is destroyed", + ); + assert_eq!( + outcome.compute_gas, gas_limit, + "the reported total must cover the receipt: the reject books no MegaETH storage gas, so \ + the whole envelope is compute", + ); + assert_eq!( + outcome.non_compute_gas, 0, + "the reject returns before the MegaETH share of intrinsic gas is booked", + ); + assert_eq!( + outcome.booked_destroyed, outcome.destroyed, + "the per-site booking and the derived total must agree", + ); +} + +/// A deposit that halts inside execution has already settled correctly against the envelope it +/// burnt, and that envelope is the whole gas limit — an exceptional halt keeps everything. The +/// rewrite reports the same number, so this shape needs no correction and must not receive one. +#[test] +fn test_deposit_runtime_halt_keeps_its_settlement() { + const GAS_LIMIT: u64 = 200_000; + let code = BytecodeBuilder::default().append(INVALID).build(); + let outcome = run(MegaSpecId::REX7, Some(code), deposit_tx(user_source_hash(), GAS_LIMIT)); + + assert_failed_deposit(&outcome, GAS_LIMIT, "halting deposit"); + assert_eq!( + outcome.enforced(), + BASE_INTRINSIC_GAS, + "INVALID performs no work, so the intrinsic is the whole of what enforces", + ); + assert_eq!( + outcome.destroyed, + GAS_LIMIT - INTRINSIC_REQUIREMENT, + "the frame's whole budget is destroyed", + ); + assert_eq!( + outcome.non_compute_gas, + i128::from(TX_INTRINSIC_STORAGE_GAS), + "the MegaETH share of intrinsic gas is booked as non-compute", + ); + assert_eq!( + outcome.compute_gas, + GAS_LIMIT - TX_INTRINSIC_STORAGE_GAS, + "the reported total plus the storage gas must cover the receipt", + ); +} + +/// A deposit stopped by a per-transaction resource limit is the shape where the rewrite actually +/// takes gas back. The limit halt rescues the frame's remaining gas for the sender, which shrinks +/// the envelope settlement reads; the rewrite then raises the receipt back to the gas limit. The +/// rescued amount is exactly what the boundary has to destroy, which is asserted against an +/// identical non-deposit transaction rather than against a constant. +#[test] +fn test_deposit_resource_limit_halt_destroys_what_the_rescue_returned() { + const GAS_LIMIT: u64 = 5_000_000; + const COMPUTE_LIMIT: u64 = 100_000; + + let plain = run_with_compute_limit( + MegaSpecId::REX7, + Some(spin_forever()), + plain_tx(GAS_LIMIT), + COMPUTE_LIMIT, + ); + let rescued = GAS_LIMIT - plain.total_gas_spent; + assert!( + rescued > 0, + "the control must actually rescue gas, otherwise the shape proves nothing; \ + spent={} limit={GAS_LIMIT}", + plain.total_gas_spent, + ); + + let deposit = run_with_compute_limit( + MegaSpecId::REX7, + Some(spin_forever()), + deposit_tx(user_source_hash(), GAS_LIMIT), + COMPUTE_LIMIT, + ); + + assert_failed_deposit(&deposit, GAS_LIMIT, "resource-limited deposit"); + assert_eq!( + deposit.enforced(), + plain.enforced(), + "the rewrite must not change what enforces — the same work was performed either way", + ); + assert_eq!( + deposit.enforced(), + COMPUTE_LIMIT, + "the transaction ran until the compute limit bound it", + ); + assert_eq!( + deposit.destroyed, + plain.destroyed + rescued, + "the rewrite destroys exactly the gas the rescue had returned to the sender", + ); + assert_eq!( + deposit.compute_gas, + plain.compute_gas + rescued, + "the reported total grows by the same amount, so it covers the rewritten receipt", + ); + assert_eq!( + deposit.non_compute_gas, plain.non_compute_gas, + "the storage-gas lane is untouched by the rewrite", + ); +} + +/// Inspection is a separate entry into the handler, and the boundary settlement has to be on both. +/// The same resource-limited deposit is run with a no-op inspector attached and must report +/// exactly what the uninspected run reports — an inspector changes what is observed, never what is +/// accounted. +#[test] +fn test_inspected_deposit_failure_settles_the_same_way() { + const GAS_LIMIT: u64 = 5_000_000; + const COMPUTE_LIMIT: u64 = 100_000; + + let uninspected = run_with_compute_limit( + MegaSpecId::REX7, + Some(spin_forever()), + deposit_tx(user_source_hash(), GAS_LIMIT), + COMPUTE_LIMIT, + ); + + let mut database = db(Some(spin_forever())); + let mut context = MegaContext::new(&mut database, MegaSpecId::REX7).with_tx_runtime_limits( + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(COMPUTE_LIMIT), + ); + 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 executed = evm + .execute_transaction(deposit_tx(user_source_hash(), GAS_LIMIT)) + .expect("tx should not surface EVMError"); + + assert_eq!( + ( + executed.result_and_state.result.tx_gas_used(), + executed.compute_gas_used, + executed.compute_gas_enforced, + executed.compute_gas_destroyed, + ), + ( + uninspected.gas_used, + uninspected.compute_gas, + uninspected.enforced(), + uninspected.destroyed, + ), + "the inspected run must account for the rewritten envelope exactly as the plain run does", + ); + assert_eq!( + executed.compute_gas_destroyed, + GAS_LIMIT - TX_INTRINSIC_STORAGE_GAS - COMPUTE_LIMIT, + "the destroyed total is the envelope less the storage gas and the work performed", + ); +} + +/// A system-originated deposit is exempt from `MegaETH`'s per-transaction resource limits, which +/// is a statement about enforcement, not about recording. Its lanes must still account for the +/// rewritten envelope exactly as a user deposit's do. +#[test] +fn test_exempt_deposit_reject_still_accounts_for_the_envelope() { + let gas_limit = INTRINSIC_REQUIREMENT - 1; + let exempt = + run(MegaSpecId::REX7, None, deposit_tx(MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, gas_limit)); + let user = run(MegaSpecId::REX7, None, deposit_tx(user_source_hash(), gas_limit)); + + assert_failed_deposit(&exempt, gas_limit, "exempt deposit"); + assert_eq!( + exempt.enforced(), + BASE_INTRINSIC_GAS, + "the exempt deposit enforces only the intrinsic validate recorded", + ); + assert_eq!( + exempt.destroyed, + gas_limit - BASE_INTRINSIC_GAS, + "the rest of its rewritten envelope is destroyed, exemption or not", + ); + assert_eq!( + (exempt.compute_gas, exempt.enforced(), exempt.destroyed, exempt.non_compute_gas), + (user.compute_gas, user.enforced(), user.destroyed, user.non_compute_gas), + "an exemption suppresses limit enforcement, not accounting", + ); +} + +/// The inner keyless transaction's own gas limit, set below what `MegaETH`'s intrinsic +/// requirement for a create transaction comes to, so the sandbox transaction is rejected in +/// validation rather than running. +const SANDBOX_REJECT_INNER_GAS_LIMIT: u64 = INTRINSIC_REQUIREMENT; + +/// Builds a deterministic pre-EIP-155 keyless deployment transaction whose gas limit cannot cover +/// its own `MegaETH` intrinsic requirement. +fn underfunded_keyless_tx_bytes() -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: SANDBOX_REJECT_INNER_GAS_LIMIT, + to: TxKind::Create, + value: U256::ZERO, + input: BytecodeBuilder::default().append(INVALID).build(), + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// A keyless-deploy sandbox transaction that fails validation is rewritten into a failed deposit +/// too — inside the sandbox, where no settlement of its own belongs. Its usage is discarded and +/// the interceptor hands the whole reservation back, so the outer transaction sees only the +/// upfront charges. Pinned across both specs: the boundary settlement must not reach in here and +/// change what the outer transaction reports. +#[test] +fn test_keyless_sandbox_reject_leaves_the_outer_transaction_alone() { + const OUTER_GAS_LIMIT: u64 = 1_000_000; + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: underfunded_keyless_tx_bytes(), + gasLimitOverride: U256::from(SANDBOX_REJECT_INNER_GAS_LIMIT), + } + .abi_encode(); + + let outcomes: Vec = [MegaSpecId::REX6, MegaSpecId::REX7] + .into_iter() + .map(|spec| { + let tx = TxEnvBuilder::default() + .caller(RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(OUTER_GAS_LIMIT) + .chain_id(Some(1)) + .data(Bytes::from(call_data.clone())) + .build_fill(); + transact_tx( + spec, + MemoryDatabase::default().account_balance(RELAYER, U256::from(10 * ONE_ETH)), + EvmTxRuntimeLimits::from_spec(spec), + tx, + &TestExternalEnvs::default(), + ) + }) + .collect(); + let (rex6, rex7) = (&outcomes[0], &outcomes[1]); + + let ExecutionResult::Revert { output, .. } = &rex7.result else { + panic!("a sandbox reject must revert the outer call, got {:?}", rex7.result); + }; + IKeylessDeploy::InvalidTransaction::abi_decode(output).expect( + "the sandbox transaction must be rejected in validation — that is the shape whose inner \ + deposit is rewritten inside the sandbox", + ); + assert_eq!( + rex7.gas_used, rex6.gas_used, + "a sandbox reject must cost the outer transaction the same on both specs", + ); + assert_eq!( + rex7.compute_gas, rex6.compute_gas, + "the sandbox's own rejected envelope must not reach the outer transaction's lanes", + ); + assert_eq!( + rex7.destroyed, 0, + "nothing the outer transaction did was destroyed: the reservation came back in full", + ); +} + +/// Pre-REX7 specs have no destroyed lane, and the boundary settlement must leave them alone. Both +/// rewritten shapes are run under REX6 and pinned to the accounting they have always produced: +/// the receipt reports the whole gas limit, nothing is destroyed, and the reported compute total +/// stays exactly what REX7 now enforces. +#[test] +fn test_rex6_deposit_failures_keep_their_frozen_accounting() { + let underfunded_gas_limit = INTRINSIC_REQUIREMENT - 1; + let rex6_reject = + run(MegaSpecId::REX6, None, deposit_tx(user_source_hash(), underfunded_gas_limit)); + let rex7_reject = + run(MegaSpecId::REX7, None, deposit_tx(user_source_hash(), underfunded_gas_limit)); + + assert_failed_deposit(&rex6_reject, underfunded_gas_limit, "REX6 underfunded deposit"); + assert_eq!(rex6_reject.destroyed, 0, "REX6 has no destroyed lane"); + assert_eq!( + rex6_reject.compute_gas, BASE_INTRINSIC_GAS, + "REX6 reports only the intrinsic validate recorded before the reject", + ); + assert_eq!( + rex7_reject.enforced(), + rex6_reject.compute_gas, + "REX7 must enforce exactly what REX6 recorded — the destroyed lane is an addition to the \ + reported total, never a change to the enforced one", + ); + + const HALT_GAS_LIMIT: u64 = 200_000; + let code = BytecodeBuilder::default().append(INVALID).build(); + let rex6_halt = + run(MegaSpecId::REX6, Some(code.clone()), deposit_tx(user_source_hash(), HALT_GAS_LIMIT)); + let rex7_halt = + run(MegaSpecId::REX7, Some(code), deposit_tx(user_source_hash(), HALT_GAS_LIMIT)); + + assert_failed_deposit(&rex6_halt, HALT_GAS_LIMIT, "REX6 halting deposit"); + assert_eq!(rex6_halt.destroyed, 0, "REX6 has no destroyed lane"); + assert_eq!( + rex7_halt.enforced(), + rex6_halt.compute_gas, + "REX7 must enforce exactly what REX6 recorded for the halting shape too", + ); + assert_eq!( + rex6_halt.gas_used, rex7_halt.gas_used, + "the receipt is op-revm's, identical on both specs", + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 22b76bf5..9f064f60 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -44,8 +44,11 @@ //! same way an interpreter frame is: executed work enforces, the unused forwarded envelope does //! not. //! - `pre_execution_intrinsic_reject` — the one envelope-keeping synthetic halt REX7 cannot reach: -//! an intrinsic overrun is a validation error from REX5 on, so the destroyed lane never has to -//! account for it. +//! for an ordinary transaction an intrinsic overrun is a validation error from REX5 on, and a +//! validation error produces no receipt for any lane to account for. +//! - `deposit_receipt_rewrite` — the transactions that break that last step. A failed OP deposit +//! does get a receipt, rebuilt to report its whole gas limit after every settlement has run; the +//! boundary that rebuilds it books the difference as destroyed without moving what enforces. mod burn_split; mod charge_on_reject; @@ -55,6 +58,7 @@ mod checkpoint_static_fee_edges; mod clamp_classification; mod common; mod conservation_terms; +mod deposit_receipt_rewrite; mod detention_window; mod double_exceed_corner; mod exceptional_halt; diff --git a/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs b/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs index ff2672f9..c63ffb86 100644 --- a/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs +++ b/crates/mega-evm/tests/rex7/pre_execution_intrinsic_reject.rs @@ -11,6 +11,12 @@ //! to participate: no transaction on a spec that has the lane can reach the halt. These probes pin //! both sides of the boundary, so a future change that re-opens the halt for REX7 turns red here //! rather than silently reporting a transaction whose burnt envelope no lane accounts for. +//! +//! Every probe here is an ordinary transaction, and that is the whole of what they claim. The +//! second half of the reasoning — a validation reject produces no receipt, so there is nothing for +//! the lane to account for — holds only for ordinary transactions. A rejected deposit does produce +//! a receipt, rebuilt to report its whole gas limit, and the destroyed lane does have to account +//! for it; that shape lives in `deposit_receipt_rewrite`. use std::convert::Infallible; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 6bd0a52c..46d04bd4 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -532,6 +532,7 @@ Two of those three are recorded as they happen, so a node MUST derive the third: - `spent` — the EVM gas the transaction's envelope burnt, read once, at the moment the envelope is final: after the transaction's gas accounting has settled and any resource-limit gas rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied. Those two move the number the receipt reports without anything having been burnt, so a node MUST NOT read `spent` after them. Gas rescued for the sender, and gas the clamp was hiding, are both out of the envelope by this point and MUST NOT be added back. + A failed deposit transaction, whose result is rebuilt after that point, is the one exception; the rule for it is below. - `minted_stipends` — the sum of `CALL_STIPEND` over the transaction's value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints. The inherited EVM grants that stipend to the child's frame budget without debiting the caller's gas counter, so the frames between them record one stipend more work than the envelope funded, per such call, whatever becomes of the stipend afterwards. The mint is created when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from that point rather than from the child frame running: an invocation turned away at frame entry — for want of balance, or at the call-depth limit — hands the whole child budget back to the caller with the stipend inside it, which shrinks the envelope against recorded work by exactly as much as a child that ran and returned it would. @@ -571,9 +572,16 @@ A [system contract](../system-contracts/overview.md) invocation a node answers w It applies only when the answer is a halt that keeps the call's gas: the part the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. An answer that returns or reverts hands the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund; a node MUST NOT record either as destroyed, because that gas was not lost. -A transaction a node rejects during validation has no envelope to split. +An ordinary transaction a node rejects during validation has no envelope to split. Since [Rex5](../upgrades/rex5.md) a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied is rejected during validation — after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited — so it produces no receipt. -A node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. +A node MUST NOT record such a transaction's gas limit as a destroyed remainder. + +A deposit transaction is not allowed to fail, and that is where the exception lies. +A deposit a node would otherwise reject during validation, and a deposit that halts during execution, are both rebuilt into a receipt reporting the transaction's whole gas limit, with state rolled back to the sender's nonce bump and the deposit's mint. +The rebuild runs after every recording and settlement site, so it is the last thing that decides the envelope: a node MUST derive the law against the rebuilt envelope rather than against the one the transaction reached on its own. +The difference between the two is destroyed compute gas, because the receipt burns it and nothing was executed for it. +The two shapes arrive from opposite positions and the law covers both without distinguishing them — a rejected deposit has recorded only the standard-EVM share of its intrinsic gas and settled nothing, while a halted deposit has already settled against the smaller envelope its resource-limit gas rescue left behind. +A node MUST NOT let the rebuild change `executed_compute`: nothing was executed for the difference, so a deposit rejected before it ran anything consumes no compute capacity at transaction or block level. The split MUST be driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. That zeroing has one consequence a node MUST accept: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero when the frame exits, so the whole segment measures as executed and is enforced in full. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 79571f45..5d1fae5c 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -100,6 +100,7 @@ It defines it as a conservation law over the gas the transaction spent: `destroyed = spent + minted_stipends − storage_gas − executed_compute` `spent` is the EVM gas the envelope burnt, read once at the moment the envelope is final — after the transaction's gas accounting has settled and any resource-limit rescue has been returned to the sender, and before the EIP-3529 refund and the EIP-7623 floor are applied, since those move the receipt's number without anything having been burnt. +For a deposit transaction whose result is rebuilt into a failed-deposit receipt, that moment is the rebuild; see below. `minted_stipends` is the sum of `CALL_STIPEND` over the value-transferring `CALL` and `CALLCODE` invocations, counted once per stipend the EVM mints: the inherited EVM grants that stipend to the child's frame budget without debiting the caller, so the frames record one stipend more work than the envelope funded per such call, and a node MUST add it back. The mint happens when the invocation is handed to the EVM, before the child is entered, and a node MUST count it from there rather than from the child frame running — an invocation turned away at frame entry, for want of balance or at the call-depth limit, returns the whole child budget with the stipend inside it and shrinks the envelope by exactly as much as a child that ran and returned it would. An invocation a node halts before handing it to the EVM, which is what a compute-gas limit reached at the call site does, mints nothing and a node MUST NOT count it. @@ -138,7 +139,13 @@ Completeness is a consequence of the law: a lost envelope is gas the transaction Reading the two independently and requiring them to agree is what turns the list from an assumption into a checkable claim. One further shape burns a whole envelope having executed nothing — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — but [Rex5](rex5.md) already rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited. -It therefore produces no receipt on Rex7 and there is no envelope to split; a node MUST NOT record a rejected transaction's gas limit as a destroyed remainder. +An ordinary transaction therefore produces no receipt on Rex7 and there is no envelope to split; a node MUST NOT record such a transaction's gas limit as a destroyed remainder. + +A deposit transaction is the exception, because it is not allowed to fail. +A deposit a node would otherwise reject during validation, and a deposit that halts during execution, are both rebuilt into a receipt reporting the transaction's whole gas limit, with state rolled back to the sender's nonce bump and the deposit's mint. +That rebuild runs after every recording and settlement site, so it is the last thing that decides the envelope: a node MUST derive the law against the rebuilt envelope, which makes the difference between it and what those sites already hold destroyed compute gas. +The two shapes arrive from opposite positions and the law covers both without distinguishing them — a rejected deposit has recorded only the standard-EVM share of its intrinsic gas and settled nothing, while a halted deposit has already settled against the smaller envelope its resource-limit gas rescue left behind. +`executed_compute` MUST NOT move: nothing was executed for the difference, so a deposit rejected before it ran anything consumes no compute capacity at transaction or block level. Under per-opcode recording through Rex6, neither the failing opcode nor the destroyed remainder is attributed to compute gas. Consequently, a transaction that halts exceptionally, or that contains an inner call frame which does, MAY report a **strictly higher** compute-gas total under Rex7 than under Rex6, while EVM gas accounting and the receipt remain identical. @@ -265,7 +272,7 @@ Any node, tool, or test fixture pinned to Rex7 must expect its results to move. A deployment that needs stable semantics must select a frozen spec explicitly rather than relying on the latest one. The gas clamp is strictly tighter than Rex6's post-opcode enforcement on the overshoot axis: the crossing opcode does not run, and enforced usage does not pass the limit by that opcode's cost. -Rex7 can report more compute gas than Rex6 for the same inputs on three paths: the exceptional-halt frame carve-out, which over-reports rather than under-reports; a failing precompile whose unused forwarded envelope is now reported as destroyed; and a `disableVolatileDataAccess` rejection, which now includes the rejected opcode's static fee. +Rex7 can report more compute gas than Rex6 for the same inputs on four paths: the exceptional-halt frame carve-out, which over-reports rather than under-reports; a failing precompile whose unused forwarded envelope is now reported as destroyed; a `disableVolatileDataAccess` rejection, which now includes the rejected opcode's static fee; and a failed deposit, whose rebuilt receipt is now covered by the reported total instead of leaving part of the envelope unaccounted. The carve-out's enforcing half is never looser than Rex6's on interpreter frames, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. On a precompile that fails before performing work, Rex7 enforcement is deliberately looser than Rex6's: the unused envelope does not bind the compute limit. From 52989f7b05ef19330f1f60e77d223879824b4465 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 18 Aug 2026 11:58:10 +0800 Subject: [PATCH 075/208] fix(state-test): list Rex6 and Rex7 in the --bench-spec error message --- crates/state-test/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 0c7e0b1d..8770ffdc 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -126,6 +126,8 @@ impl Cmd { mega_evm::name::REX3, mega_evm::name::REX4, mega_evm::name::REX5, + mega_evm::name::REX6, + mega_evm::name::REX7, ] .join(", ") )), From a265a52e6dc3b84f0ad8b6689dc4e7cbc5f5d7c5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 20 Aug 2026 11:26:05 +0800 Subject: [PATCH 076/208] fix(rex7): weigh a CREATE's code-deposit compute gas before recording it The canonical code-deposit charge is one revm only takes when the frame's result is still successful at action processing. REX5 records it ahead of that point so a compute exceed can fail the frame while the deployment is still revertible, and the amount then stays in the tracker however the frame ends -- including when it ends on a data-size or state-growth exceed, where no deposit is charged at all. That leaves the transaction reporting, and the block enforcing, compute gas nothing spent, which breaks the REX7 envelope conservation law. REX7 now settles the charge at the frame's exit, after the tail segment is settled and the frame-exit accounting merged, and asks a non-mutating peek whether it fits before recording it. A frame-local exceed reverts the frame without recording or latching; a TX-level exceed latches and halts with gas rescue, carrying the detention attribution the recorded path would have had. REX4-REX6 keep their existing recording point and behavior. --- crates/mega-evm/src/evm/execution.rs | 74 ++- crates/mega-evm/src/limit/compute_gas.rs | 73 +-- crates/mega-evm/src/limit/frame_limit.rs | 21 +- crates/mega-evm/src/limit/limit.rs | 154 +++++++ crates/mega-evm/src/limit/mod.rs | 2 +- .../tests/rex7/create_code_deposit_charge.rs | 422 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + docs/spec/evm/compute-gas.md | 17 +- docs/spec/upgrades/rex7.md | 29 ++ 9 files changed, 746 insertions(+), 50 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/create_code_deposit_charge.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index e6582d53..a316743c 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -482,6 +482,7 @@ impl MegaEvm { return Ok(()); } let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); + let is_rex7 = ctx.spec.is_enabled(MegaSpecId::REX7); if let InterpreterAction::Return(interpreter_result) = action { // REX7: hand any clamp-hidden gas back to the result and latch a clamp-induced @@ -506,21 +507,14 @@ impl MegaEvm { } } - // REX5+: pre-charge canonical code-deposit compute gas before + // REX5/REX6: pre-charge canonical code-deposit compute gas before // process_next_action commits the CREATE checkpoint. Skip when // revm's return_create would not charge it; the existing // limit-side hook below owns the result-marking on exceed. - if is_rex5 && frame.data.is_create() { - let cfg = ctx.cfg(); - if will_return_create_charge_code_deposit( - interpreter_result, - cfg.max_code_size(), - cfg.spec().into_eth_spec(), - cfg.is_eip3541_disabled(), - ) { - let code_len = interpreter_result.output.len() as u64; - let canonical_code_deposit_gas = - code_len.saturating_mul(revm::interpreter::gas::CODEDEPOSIT); + if is_rex5 && !is_rex7 && frame.data.is_create() { + if let Some(canonical_code_deposit_gas) = + canonical_code_deposit_gas(ctx, interpreter_result) + { let _ = ctx .additional_limit .borrow_mut() @@ -532,14 +526,41 @@ impl MegaEvm { // Update additional limits. MiniRex is guaranteed to be enabled here. ctx.additional_limit.borrow_mut().after_frame_run_instructions(frame, action); + // REX7: settle the same canonical code-deposit charge, after the hook above has closed the + // frame's tail segment, merged this frame's non-compute usage and marked the result if any + // of that put the frame over a limit. Two things follow from settling here rather than + // ahead of the hook. The charge is weighed against the frame's complete usage instead of a + // total still missing its tail. And a frame the hook already failed is skipped, because the + // marked result fails the deposit predicate — which is the point: revm only charges the + // deposit on a result that is still successful when the action is processed, so a charge + // recorded for a frame that ends any other way is compute gas nothing ever spent. + if is_rex7 { + if let InterpreterAction::Return(interpreter_result) = action { + if frame.data.is_create() { + if let Some(canonical_code_deposit_gas) = + canonical_code_deposit_gas(ctx, interpreter_result) + { + let rewrite = ctx + .additional_limit + .borrow_mut() + .settle_create_code_deposit_compute_gas(canonical_code_deposit_gas); + if let Some((result, output)) = rewrite { + interpreter_result.result = result; + interpreter_result.output = output; + } + } + } + } + } + Ok(()) } /// Apply `MiniRex` additional limits after frame action processing. /// - /// Under REX5+ for CREATE results, the code-deposit compute gas was - /// already pre-charged in [`after_frame_run_instructions`]; pass - /// `None` here so the post-action hook does not double-record. + /// Under REX5+ for CREATE results, the code-deposit compute gas was already settled in + /// [`after_frame_run_instructions`]; pass `None` here so the post-action hook does not + /// re-record what that settlement recorded, or record what it deliberately did not. #[inline] fn after_frame_run( ctx: &MegaContext, @@ -552,8 +573,8 @@ impl MegaEvm { let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); if let ItemOrResult::Result(frame_result) = frame_output { - // REX5+: code-deposit compute gas for CREATE results was already - // pre-charged. Skip post-action recording so we don't double-count. + // REX5+: code-deposit compute gas for CREATE results was already settled at the + // frame's exit. Skip post-action recording so we don't double-count. let pass_through = if is_rex5 && matches!(frame_result, FrameResult::Create(_)) { None } else { @@ -566,6 +587,25 @@ impl MegaEvm { } } +/// The canonical code-deposit compute gas revm will charge a CREATE frame, or `None` when +/// `return_create` would not charge it at all. +#[inline] +fn canonical_code_deposit_gas( + ctx: &MegaContext, + interpreter_result: &InterpreterResult, +) -> Option { + let cfg = ctx.cfg(); + will_return_create_charge_code_deposit( + interpreter_result, + cfg.max_code_size(), + cfg.spec().into_eth_spec(), + cfg.is_eip3541_disabled(), + ) + .then(|| { + (interpreter_result.output.len() as u64).saturating_mul(revm::interpreter::gas::CODEDEPOSIT) + }) +} + /// Mirrors `revm_handler::frame::return_create`'s pre-commit predicate. /// Returns `true` iff `return_create` would charge `code_len * CODEDEPOSIT` /// from the interpreter gas and commit the checkpoint. diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index f228bb81..4d506273 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -258,6 +258,52 @@ impl ComputeGasTracker { pub(crate) fn merge_persistent_usage(&mut self, amount: u64) { self.frame_tracker.add_tx_persistent(amount); } + + /// Pushes a frame with an explicit budget, for tests that need a specific frame-local edge + /// without executing a transaction to get there. + #[cfg(test)] + pub(crate) fn push_frame_with_limit_for_test(&mut self, limit: u64) { + self.frame_tracker.push_frame_with_limit(limit, ()); + } + + /// [`check_limit`](TxRuntimeLimit::check_limit) evaluated as if `extra` gas had already been + /// recorded, without recording it. + /// + /// This is the whole verdict a caller would get by recording and then checking: the Rex4+ + /// per-frame budget first, then the TX-level (possibly detained) limit, reported exactly as + /// enforcement reports them. A caller that must decide whether to make a charge at all asks + /// here; `check_limit` is this method at `extra = 0`, so there is no second copy of the + /// predicate to drift. + #[inline] + pub(crate) fn check_limit_with_extra(&self, extra: u64) -> LimitCheck { + if self.rex4_enabled { + let frame_check = + self.frame_tracker.would_exceed_current_frame_limit(LimitKind::ComputeGas, extra); + if frame_check.exceeded_limit() { + return frame_check; + } + // Do not early-return on frame WithinLimit: + // 1) pre-frame intrinsic compute gas is recorded in `tx_entry`, outside current frame + // budget; + // 2) `detained_limit` can be lowered at runtime by volatile-data access. + // So TX-level detained check must still run even when frame check is within limit. + } + // TX-level detained check (all specs): total usage vs effective limit (min of tx/detained). + // The comparison runs on enforced usage — burned remainders are excluded — while the + // reported `used` is the full settled total, so a halt reason states the usage the + // transaction actually ends with. The two coincide on every spec before REX7. + let limit = self.tx_limit(); + if self.enforced_tx_usage().saturating_add(extra) > limit { + LimitCheck::ExceedsLimit { + kind: LimitKind::ComputeGas, + frame_local: false, + limit, + used: self.tx_usage().saturating_add(extra), + } + } else { + LimitCheck::WithinLimit + } + } } impl TxRuntimeLimit for ComputeGasTracker { @@ -297,32 +343,7 @@ impl TxRuntimeLimit for ComputeGasTracker { /// when the current frame budget is still within limit. #[inline] fn check_limit(&self) -> LimitCheck { - if self.rex4_enabled { - let frame_check = self.frame_tracker.exceeds_current_frame_limit(LimitKind::ComputeGas); - if frame_check.exceeded_limit() { - return frame_check; - } - // Do not early-return on frame WithinLimit: - // 1) pre-frame intrinsic compute gas is recorded in `tx_entry`, outside current frame - // budget; - // 2) `detained_limit` can be lowered at runtime by volatile-data access. - // So TX-level detained check must still run even when frame check is within limit. - } - // TX-level detained check (all specs): total usage vs effective limit (min of tx/detained). - // The comparison runs on enforced usage — burned remainders are excluded — while the - // reported `used` is the full settled total, so a halt reason states the usage the - // transaction actually ends with. The two coincide on every spec before REX7. - let limit = self.tx_limit(); - if self.enforced_tx_usage() > limit { - LimitCheck::ExceedsLimit { - kind: LimitKind::ComputeGas, - frame_local: false, - limit, - used: self.tx_usage(), - } - } else { - LimitCheck::WithinLimit - } + self.check_limit_with_extra(0) } #[inline] diff --git a/crates/mega-evm/src/limit/frame_limit.rs b/crates/mega-evm/src/limit/frame_limit.rs index 8d05ac6b..cca3fc57 100644 --- a/crates/mega-evm/src/limit/frame_limit.rs +++ b/crates/mega-evm/src/limit/frame_limit.rs @@ -223,12 +223,29 @@ impl FrameLimitTracker { /// Returns whether the current frame has exceeded its frame-local limit. /// If exceeded, `frame_local` is always `true` since this checks per-frame budgets. pub(crate) fn exceeds_current_frame_limit(&self, kind: LimitKind) -> LimitCheck { + self.would_exceed_current_frame_limit(kind, 0) + } + + /// [`exceeds_current_frame_limit`](Self::exceeds_current_frame_limit) evaluated as if `extra` + /// had already been added to the current frame's usage, without adding it. + /// + /// A caller that must decide whether to make a charge at all — rather than make it and react + /// to the verdict — asks here. The two share one predicate on purpose: a separate copy of + /// `used - refund > limit` would be free to drift from the one enforcement actually uses. + pub(crate) fn would_exceed_current_frame_limit( + &self, + kind: LimitKind, + extra: u64, + ) -> LimitCheck { match self.frame_stack.last() { - Some(entry) if entry.used().saturating_sub(entry.refund) > entry.limit => { + Some(entry) + if entry.used().saturating_add(extra).saturating_sub(entry.refund) > + entry.limit => + { LimitCheck::ExceedsLimit { kind, limit: entry.limit, - used: entry.used(), + used: entry.used().saturating_add(extra), frame_local: true, } } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index fa33767e..826a8e63 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -1236,6 +1236,71 @@ impl AdditionalLimit { } } + /// The verdict [`record_compute_gas`](Self::record_compute_gas) would reach for `charge`, + /// without recording it and without latching anything. + /// + /// Recording and then reacting is the right shape for work that has already happened: the gas + /// was spent whatever the verdict says. It is the wrong shape for a charge that is still + /// conditional — one the EVM only takes if the frame survives — because a charge skipped after + /// being recorded leaves compute gas in the tracker that nothing ever spent. Such a caller asks + /// here first and records only on the answer that lets the charge happen. + /// + /// The verdict is produced by the same predicate enforcement uses, evaluated at `charge` more + /// usage, so there is no gap between what this reports and what recording would produce. + #[inline] + pub(crate) fn would_exceed_compute_gas(&self, charge: u64) -> LimitCheck { + // Sticky short-circuit, mirroring `record_compute_gas`: an already-latched `ExceedsLimit` + // is what the caller would observe, and `Exempt` suppresses the decision entirely. + if !self.has_exceeded_limit.within_limit() { + return self.has_exceeded_limit; + } + self.compute_gas.check_limit_with_extra(charge) + } + + /// Records the canonical code-deposit compute gas of a CREATE frame that is about to deposit + /// its code, or reports the result rewrite that stops the deposit from happening (REX7+). + /// + /// `charge` is the gas revm charges the frame for the deposit, and it is charged only if the + /// frame's result is still successful when the action is processed. So the decision has to be + /// made here, ahead of that: recording it and marking the result afterwards would leave the + /// tracker holding compute gas for a deposit that then never happened. + /// + /// Returns `Some((result, output))` when the charge cannot be afforded, for the caller to write + /// onto the frame's result: + /// + /// - a frame-local exceed reverts the frame and is settled here — nothing is recorded and + /// nothing is latched, because with the charge not made the transaction is within its limits + /// and the frames above are free to continue; + /// - a TX-level exceed is latched, which is what the transaction halts and rescues its gas on, + /// exactly as it would have had the charge been recorded. + /// + /// Returns `None` when the charge fits, having recorded it. + pub(crate) fn settle_create_code_deposit_compute_gas( + &mut self, + charge: u64, + ) -> Option<(InstructionResult, Bytes)> { + let check = self.would_exceed_compute_gas(charge); + if !check.exceeded_limit() { + let recorded = self.record_compute_gas(charge); + debug_assert!(recorded, "the peek and the record must reach the same verdict"); + return None; + } + + let output = check.revert_data(); + if check.is_frame_local() { + return Some((InstructionResult::Revert, output)); + } + + self.has_exceeded_limit = check; + // Preserve the volatile-detention attribution the recorded path would have produced: with + // nothing recorded, usage stays at or below the detained limit, so `is_detained_exceed` + // cannot see that detention is what the charge ran into. + self.checkpoint.set_latched_detained( + self.compute_gas.detained_limit() < self.compute_gas.base_tx_limit(), + ); + Some((Self::EXCEEDING_LIMIT_INSTRUCTION_RESULT, output)) + } + /// Hook called when returning a frame result to parent frame in `frame_return_result` or /// `last_frame_result`. May modify the frame result in place if the limit is exceeded. pub(crate) fn before_frame_return_result( @@ -1914,6 +1979,95 @@ mod tests { ); } + /// The peek must reach exactly the verdict recording would have produced. Recording the + /// charge and then checking, versus asking first, are compared on the same tracker state + /// across the whole knife edge — the one place a second copy of `used > limit` could drift + /// from the copy enforcement runs. + #[test] + fn test_peek_matches_record_then_check_across_the_edge() { + const FRAME_BUDGET: u64 = 1_000; + for charge in 0..=(FRAME_BUDGET + 2) { + let mut peeked = rex7_limit(); + peeked.compute_gas.push_frame_with_limit_for_test(FRAME_BUDGET); + let mut recorded = rex7_limit(); + recorded.compute_gas.push_frame_with_limit_for_test(FRAME_BUDGET); + + let peek = peeked.would_exceed_compute_gas(charge); + let within = recorded.record_compute_gas(charge); + + assert_eq!( + peek.exceeded_limit(), + !within, + "charge {charge}: the peek and the record must agree on the verdict", + ); + assert_eq!( + peek, recorded.has_exceeded_limit, + "charge {charge}: the peek must report what the record latched", + ); + } + } + + /// The frame-local arm of the code-deposit settlement reverts the frame without touching the + /// tracker: nothing recorded, nothing latched. Both matter — a recorded charge would be + /// compute gas the deposit never spent, and a latched exceed would outlive the frame that is + /// already being reverted for it. + #[test] + fn test_create_code_deposit_frame_local_arm_records_and_latches_nothing() { + let mut limit = rex7_limit(); + limit.compute_gas.push_frame_with_limit_for_test(100); + let before = limit.get_usage().compute_gas; + + let rewrite = limit.settle_create_code_deposit_compute_gas(101); + + let (result, output) = rewrite.expect("an unaffordable charge must rewrite the result"); + assert_eq!(result, InstructionResult::Revert, "a frame-local exceed reverts the frame"); + assert!(!output.is_empty(), "the revert must carry the MegaLimitExceeded payload"); + assert_eq!(limit.get_usage().compute_gas, before, "the charge must not be recorded"); + assert_eq!(latched_kind(&limit), None, "the frame-local arm must not latch"); + } + + /// The TX-level arm latches instead, which is what the transaction halts and rescues its gas + /// on — but still records nothing. With nothing recorded, usage stays under the detained + /// limit, so the halt can only keep blaming detention through the latched flag. + #[test] + fn test_create_code_deposit_tx_level_arm_latches_detention_without_recording() { + let mut limit = rex7_limit(); + // A frame budget far above the charge, so the transaction limit is what binds. + limit.compute_gas.push_frame_with_limit_for_test(u64::MAX); + limit.set_compute_gas_limit(10); + let before = limit.get_usage().compute_gas; + + let rewrite = limit.settle_create_code_deposit_compute_gas(11); + + let (result, _) = rewrite.expect("an unaffordable charge must rewrite the result"); + assert_eq!( + result, + AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT, + "a TX-level exceed halts the transaction", + ); + assert_eq!(limit.get_usage().compute_gas, before, "the charge must not be recorded"); + assert_eq!(latched_kind(&limit), Some(LimitKind::ComputeGas), "the TX-level arm latches"); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::empty()).is_some(), + "the halt must still be attributable to detention", + ); + } + + /// An affordable charge is recorded like any other work and leaves the result alone. + #[test] + fn test_create_code_deposit_affordable_charge_is_recorded() { + let mut limit = rex7_limit(); + limit.compute_gas.push_frame_with_limit_for_test(100); + let before = limit.get_usage().compute_gas; + + assert!( + limit.settle_create_code_deposit_compute_gas(100).is_none(), + "an affordable charge must not rewrite the result", + ); + assert_eq!(limit.get_usage().compute_gas, before + 100, "an affordable charge is recorded",); + assert_eq!(latched_kind(&limit), None, "an affordable charge must not latch"); + } + /// `mark_frame_result_as_exceeding_limit` rewrites both frame-result variants in place. #[test] fn test_mark_frame_result_as_exceeding_limit_rewrites_both_variants() { diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index fe0a986e..25563af7 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -66,7 +66,7 @@ impl LimitKind { /// see [`crate::is_system_originated`]). The `Exempt` state is **sticky**: once `AdditionalLimit` /// stores it in `has_exceeded_limit`, `check_limit` short-circuits and the sub-tracker checks /// are skipped, so no later overflow can overwrite it. -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum LimitCheck { /// All limits are within their configured thresholds. #[default] diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs new file mode 100644 index 00000000..abf25b8b --- /dev/null +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -0,0 +1,422 @@ +//! REX7: the canonical code-deposit compute gas of a CREATE frame is recorded only when the +//! deposit actually happens. +//! +//! revm charges a successful CREATE `code_len * CODEDEPOSIT` when it processes the frame's action, +//! and only then — a frame whose result is no longer successful at that point pays nothing and +//! deposits nothing. `MegaETH` has to decide the charge one step earlier, before the action is +//! processed, because a compute-limit exceed discovered after the CREATE checkpoint is committed +//! would leave the frame's state changes in the journal under a reverted result. +//! +//! Deciding early is not the same as charging early. REX5 and REX6 record the charge and then let +//! the latched exceed mark the result, which leaves the tracker holding compute gas for a deposit +//! that never happened. REX7 asks first: the charge is weighed against the frame's completed usage +//! and recorded only on the answer that lets the deposit go through. The other two answers stop the +//! frame — a frame-local exceed reverts it, a TX-level exceed halts the transaction — and record +//! nothing, which is what keeps the reported compute total equal to the gas the transaction spent. +//! +//! These tests pin the four rows of that decision, the frozen REX4-REX6 shapes they must not +//! disturb, and the journal consistency that the early decision exists for in the first place. + +use crate::common::{default_envs, transact_tx, Outcome, CALLER, ONE_ETH}; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_sol_types::SolError as _; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{MSTORE, POP, RETURN, TIMESTAMP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + state::EvmState, +}; + +/// revm's per-byte code-deposit gas (`revm::interpreter::gas::CODEDEPOSIT`). +const CODEDEPOSIT: u64 = 200; + +/// Runtime code size the constructor returns. Small enough that the `MegaETH` code-deposit storage +/// charge (10,000 per byte) stays well inside the transaction gas limit. +const RUNTIME_LEN: u64 = 100; + +/// The canonical code-deposit compute gas for [`RUNTIME_LEN`] bytes — the charge under test. +const CODE_DEPOSIT_GAS: u64 = RUNTIME_LEN * CODEDEPOSIT; + +/// Transaction gas limit: covers the constructor, the 1,000,000 `MegaETH` code-deposit storage +/// charge and the canonical charge many times over. +const TX_GAS_LIMIT: u64 = 10_000_000; + +/// Init code that returns `len` zero bytes from memory, and nothing else. +fn return_zeros_initcode(len: u64) -> Bytes { + BytecodeBuilder::default().push_number(len).push_number(0u64).append(RETURN).build() +} + +/// The address the first CREATE from [`CALLER`] deploys to. +fn deployed_address() -> Address { + CALLER.create(0) +} + +/// Whether the produced state actually carries deployed code at `address`. +/// +/// Reads the state delta rather than the journal: it is what the transaction reports as committed, +/// so it answers the question a caller of the CREATE would ask. +fn has_deployed_code(state: &EvmState, address: Address) -> bool { + state + .get(&address) + .map(|account| { + account.info.code.as_ref().is_some_and(|code| !code.is_empty()) || + account.info.code_hash != revm::primitives::KECCAK_EMPTY + }) + .unwrap_or(false) +} + +/// Runs `init_code` as a creation transaction under `spec` with `limits`. +fn create(spec: MegaSpecId, limits: EvmTxRuntimeLimits, init_code: Bytes) -> Outcome { + let db = MemoryDatabase::default().account_balance(CALLER, U256::from(10 * ONE_ETH)); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .kind(TxKind::Create) + .gas_limit(TX_GAS_LIMIT) + .gas_price(0) + .data(init_code) + .build_fill(); + transact_tx(spec, db, limits, tx, &default_envs()) +} + +/// The unconstrained run of the standard fixture: a CREATE that deposits [`RUNTIME_LEN`] bytes with +/// no resource limit in the way. Used to calibrate the limits the constrained runs sit against. +fn calibrate(spec: MegaSpecId) -> Outcome { + let outcome = create(spec, EvmTxRuntimeLimits::no_limits(), return_zeros_initcode(RUNTIME_LEN)); + assert!( + outcome.is_success(), + "{spec:?}: the calibration run must deploy: {:?}", + outcome.result + ); + assert!( + has_deployed_code(&outcome.state, deployed_address()), + "{spec:?}: the calibration run must leave code behind", + ); + outcome +} + +/// The revert payload of an absorbed frame-local limit exceed. +fn revert_payload(label: &str, outcome: &Outcome) -> Bytes { + match &outcome.result { + ExecutionResult::Revert { output, .. } => output.clone(), + other => panic!("{label}: expected a revert, got {other:?}"), + } +} + +/// A frame-local compute exceed produced by the code-deposit charge stops the deposit, and REX7 +/// records nothing for it. +/// +/// The compute limit is set one gas below what the whole transaction needs, so the constructor runs +/// to a successful RETURN and only the code-deposit charge is unaffordable. REX6 is run beside it: +/// the two must produce the same failure, the same revert payload and the same receipt, and differ +/// only in the compute total they report — REX6 by exactly the charge it recorded and then did not +/// spend. +#[test] +fn test_create_frame_local_code_deposit_exceed_records_nothing() { + let deployed = calibrate(MegaSpecId::REX7); + let full_compute = deployed.compute_gas; + assert_eq!( + calibrate(MegaSpecId::REX6).compute_gas, + full_compute, + "the specs must agree on a successful CREATE's compute total, or the shared limit below \ + would not mean the same thing to both", + ); + + let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(full_compute - 1); + let rex6 = create(MegaSpecId::REX6, limits, return_zeros_initcode(RUNTIME_LEN)); + let rex7 = create(MegaSpecId::REX7, limits, return_zeros_initcode(RUNTIME_LEN)); + + // The frame fails, and its journal fails with it: no code is deployed under either spec. + for (label, outcome) in [("REX6", &rex6), ("REX7", &rex7)] { + assert!( + matches!(outcome.result, ExecutionResult::Revert { .. }), + "{label}: the frame-local exceed must be absorbed into a revert: {:?}", + outcome.result, + ); + assert!( + !has_deployed_code(&outcome.state, deployed_address()), + "{label}: a reverted CREATE must leave no code behind", + ); + } + + // The parent's view is bit-identical: same payload, same receipt. + assert_eq!( + revert_payload("REX7", &rex7), + revert_payload("REX6", &rex6), + "the revert payload must not change", + ); + assert_eq!( + MegaLimitExceeded::abi_decode(&revert_payload("REX7", &rex7)) + .expect("the payload must be a MegaLimitExceeded") + .kind, + LimitKind::ComputeGas.as_u8(), + "the revert must blame compute gas", + ); + assert_eq!(rex7.gas_used, rex6.gas_used, "the receipt's gas must not change"); + + // What does change is the compute total: REX6 holds a charge nobody spent, REX7 does not. + assert_eq!( + rex6.compute_gas, + rex7.compute_gas + CODE_DEPOSIT_GAS, + "REX6 must keep recording the unspent code-deposit charge (rex6={}, rex7={})", + rex6.compute_gas, + rex7.compute_gas, + ); + assert_eq!( + rex7.compute_gas, + full_compute - CODE_DEPOSIT_GAS, + "REX7's total must be the successful run's minus exactly the charge that never happened", + ); + + // Nothing was destroyed and nothing was latched: with the charge not made, the transaction is + // within its limits and the reverted frame is an ordinary revert. + assert_eq!(rex7.destroyed, 0, "a reverted frame keeps its gas; nothing is destroyed"); + assert_eq!(rex7.booked_destroyed, 0, "no site may book a destroyed remainder here"); + assert_eq!( + rex7.enforced(), + rex7.compute_gas, + "the whole reported total enforces when nothing is destroyed", + ); +} + +/// The knife edge of the same decision: the compute limit that exactly affords the charge deploys, +/// one gas less reverts. Both sides stay accounted for. +#[test] +fn test_create_code_deposit_charge_knife_edge() { + let full_compute = calibrate(MegaSpecId::REX7).compute_gas; + + let exact = create( + MegaSpecId::REX7, + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(full_compute), + return_zeros_initcode(RUNTIME_LEN), + ); + assert!( + exact.is_success(), + "a limit exactly equal to the transaction's compute total must deploy: {:?}", + exact.result, + ); + assert!( + has_deployed_code(&exact.state, deployed_address()), + "the exactly-affordable CREATE must leave code behind", + ); + assert_eq!( + exact.compute_gas, full_compute, + "the affordable charge is recorded like any other work", + ); + + let short = create( + MegaSpecId::REX7, + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(full_compute - 1), + return_zeros_initcode(RUNTIME_LEN), + ); + assert!( + matches!(short.result, ExecutionResult::Revert { .. }), + "one gas short must revert: {:?}", + short.result, + ); + assert!( + !has_deployed_code(&short.state, deployed_address()), + "one gas short must leave no code behind", + ); + assert_eq!( + short.compute_gas, + full_compute - CODE_DEPOSIT_GAS, + "one gas short must record the frame's work and none of the charge", + ); +} + +/// The same decision reached through a dimension that is not compute gas. +/// +/// The data-size tracker records the deployed code's size as the frame ends, one step before the +/// code-deposit charge is settled. When that record puts the frame over its data-size budget the +/// frame reverts, so revm never charges the deposit — and REX7 must not have recorded it either. +#[test] +fn test_create_data_size_exceed_at_frame_exit_records_no_code_deposit() { + let deployed = calibrate(MegaSpecId::REX7); + let full_compute = deployed.compute_gas; + let full_data_size = deployed.data_size; + + let limits = EvmTxRuntimeLimits::no_limits().with_tx_data_size_limit(full_data_size - 1); + let rex6 = create(MegaSpecId::REX6, limits, return_zeros_initcode(RUNTIME_LEN)); + let rex7 = create(MegaSpecId::REX7, limits, return_zeros_initcode(RUNTIME_LEN)); + + for (label, outcome) in [("REX6", &rex6), ("REX7", &rex7)] { + assert!( + matches!(outcome.result, ExecutionResult::Revert { .. }), + "{label}: the data-size exceed must be absorbed into a revert: {:?}", + outcome.result, + ); + assert!( + !has_deployed_code(&outcome.state, deployed_address()), + "{label}: a reverted CREATE must leave no code behind", + ); + } + assert_eq!( + MegaLimitExceeded::abi_decode(&revert_payload("REX7", &rex7)) + .expect("the payload must be a MegaLimitExceeded") + .kind, + LimitKind::DataSize.as_u8(), + "the revert must blame data size", + ); + assert_eq!(rex7.gas_used, rex6.gas_used, "the receipt's gas must not change"); + assert_eq!( + rex7.compute_gas, + full_compute - CODE_DEPOSIT_GAS, + "a frame that failed on another dimension must not be charged for a deposit it never made", + ); + assert_eq!( + rex6.compute_gas, full_compute, + "REX6 keeps recording the charge whatever the frame's fate", + ); +} + +/// The reverted CREATE's other tracked usage goes with it: the state growth of an account that was +/// never deployed is discarded when the frame pops. +#[test] +fn test_create_frame_local_exceed_discards_state_growth() { + let deployed = calibrate(MegaSpecId::REX7); + assert!( + deployed.state_growth > 0, + "the successful CREATE must record state growth for the new account", + ); + + let short = create( + MegaSpecId::REX7, + EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(deployed.compute_gas - 1), + return_zeros_initcode(RUNTIME_LEN), + ); + assert!(matches!(short.result, ExecutionResult::Revert { .. }), "{:?}", short.result); + assert_eq!( + short.state_growth, 0, + "the reverted frame's state growth must be discarded with the frame", + ); +} + +/// Init code that detains the transaction, burns most of the detained budget in one memory +/// expansion, and then returns `len` bytes of runtime code. +/// +/// `TIMESTAMP` caps the transaction's remaining compute gas relative to usage at that point; the +/// `MSTORE` then spends nearly all of that cap. The frame's own budget is untouched by detention, +/// so what the code-deposit charge runs into afterwards is the transaction limit alone. +fn detained_burn_initcode(mstore_offset: u64, len: u64) -> Bytes { + BytecodeBuilder::default() + .append(TIMESTAMP) + .append(POP) + .push_number(0u64) + .push_number(mstore_offset) + .append(MSTORE) + .push_number(len) + .push_number(0u64) + .append(RETURN) + .build() +} + +/// A TX-level exceed produced by the code-deposit charge halts the transaction and rescues its +/// remaining gas, and REX7 records nothing for the charge that caused it. +/// +/// Detention is what separates the two budgets: it lowers the transaction's compute limit without +/// touching the frame's, so the charge can be unaffordable for the transaction while the frame +/// still has room. The halt must keep blaming detention, which it can no longer read off usage — +/// the charge that crossed the limit was never recorded. +#[test] +fn test_create_tx_level_code_deposit_exceed_halts_and_blames_detention() { + // Memory offset chosen so the expansion costs ~19.9M of the 20M detention cap, leaving less + // than the 200,000-gas code-deposit charge but more than zero. + const BURN_OFFSET: u64 = 3_205_568; + const DETAINED_RUNTIME_LEN: u64 = 1_000; + const DETAINED_TX_GAS_LIMIT: u64 = 50_000_000; + + let init_code = detained_burn_initcode(BURN_OFFSET, DETAINED_RUNTIME_LEN); + let run = |spec: MegaSpecId| { + let db = MemoryDatabase::default().account_balance(CALLER, U256::from(10 * ONE_ETH)); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .kind(TxKind::Create) + .gas_limit(DETAINED_TX_GAS_LIMIT) + .gas_price(0) + .data(init_code.clone()) + .build_fill(); + transact_tx(spec, db, EvmTxRuntimeLimits::from_spec(spec), tx, &default_envs()) + }; + + let rex6 = run(MegaSpecId::REX6); + let rex7 = run(MegaSpecId::REX7); + + for (label, outcome) in [("REX6", &rex6), ("REX7", &rex7)] { + assert!( + matches!(outcome.halt_reason(label), MegaHaltReason::VolatileDataAccessOutOfGas { .. }), + "{label}: the halt must blame detention: {:?}", + outcome.result, + ); + assert!( + !has_deployed_code(&outcome.state, deployed_address()), + "{label}: a halted CREATE must leave no code behind", + ); + } + + assert_eq!( + rex7.gas_used, rex6.gas_used, + "the rescued receipt must not change (rex6={}, rex7={})", + rex6.gas_used, rex7.gas_used, + ); + + // What proves the halt came from the charge rather than from usage crossing on its own: REX7's + // recorded usage never reaches the detained limit, so the only thing left that can classify + // this halt as detention is the flag the charge's settlement set. + assert!( + rex7.enforced() <= rex7.detained_compute_gas_limit, + "REX7's usage must stay within the detained limit (usage={}, limit={})", + rex7.enforced(), + rex7.detained_compute_gas_limit, + ); + assert!( + rex6.enforced() > rex6.detained_compute_gas_limit, + "REX6's usage crosses the limit because it recorded the charge (usage={}, limit={})", + rex6.enforced(), + rex6.detained_compute_gas_limit, + ); + assert_eq!( + rex6.compute_gas, + rex7.compute_gas + DETAINED_RUNTIME_LEN * CODEDEPOSIT, + "REX6 records the charge that halted it, REX7 does not (rex6={}, rex7={})", + rex6.compute_gas, + rex7.compute_gas, + ); +} + +/// The frozen shapes this decision sits on top of. +/// +/// Deciding the charge before the action is processed is what keeps a CREATE's journal and its +/// reported result in agreement, and that has been true since REX5 — REX4 is the last spec that +/// reports a revert over a committed deployment. REX7 changes what is recorded, not this. +#[test] +fn test_frozen_specs_keep_their_create_journal_shapes() { + let full_compute = calibrate(MegaSpecId::REX5).compute_gas; + let limits = EvmTxRuntimeLimits::no_limits().with_tx_compute_gas_limit(full_compute - 1); + + let rex4 = create(MegaSpecId::REX4, limits, return_zeros_initcode(RUNTIME_LEN)); + assert!( + matches!(rex4.result, ExecutionResult::Revert { .. }), + "REX4 reports a revert: {:?}", + rex4.result, + ); + assert!( + has_deployed_code(&rex4.state, deployed_address()), + "REX4 keeps its split outcome: the deployment stands under the reverted result", + ); + + for spec in [MegaSpecId::REX5, MegaSpecId::REX6, MegaSpecId::REX7] { + let outcome = create(spec, limits, return_zeros_initcode(RUNTIME_LEN)); + assert!( + matches!(outcome.result, ExecutionResult::Revert { .. }), + "{spec:?} reports a revert: {:?}", + outcome.result, + ); + assert!( + !has_deployed_code(&outcome.state, deployed_address()), + "{spec:?} must roll the deployment back with the result", + ); + } +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 9f064f60..50436716 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -8,6 +8,9 @@ //! and the ABI payload / halt fields a clamp-induced exceed reports. //! - `checkpoint_families` — one parity case per checkpoint opcode the REX7 table wires, so the set //! is covered exhaustively rather than through representatives. +//! - `create_code_deposit_charge` — a CREATE's canonical code-deposit compute gas is weighed before +//! it is recorded, so a creation that fails at its frame exit is charged nothing for a deposit +//! the EVM never makes. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. //! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that @@ -58,6 +61,7 @@ mod checkpoint_static_fee_edges; mod clamp_classification; mod common; mod conservation_terms; +mod create_code_deposit_charge; mod deposit_receipt_rewrite; mod detention_window; mod double_exceed_corner; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 46d04bd4..7a86752d 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -334,14 +334,23 @@ These conditions apply on every spec; only the point at which the recording happ | Spec | Recording point | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Rex5+ | Atomically with the deployment commit: recorded when the deployment's pre-commit success conditions hold, at the same point the EVM charges the code-deposit gas and commits the created contract. | +| Rex7+ | Conditional on the deposit: evaluated once the frame's own accounting is complete, and recorded only if the amount fits the budgets it is weighed against. | +| Rex5–Rex6 | Atomically with the deployment commit: recorded when the deployment's pre-commit success conditions hold, at the same point the EVM charges the code-deposit gas and commits the created contract. | | MiniRex–Rex4 | During frame-return processing, in the window covering the EVM's code-deposit charge. | A node MUST NOT record this amount twice. -The recording itself can latch a compute-gas exceed, and the two recording points then produce different deployment outcomes: - -- From Rex5, the recording precedes the commit: the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing, but the recorded amount stands — recording precedes exceed evaluation, and compute gas is never reverted. +The recording interacts with the compute-gas limit, and the three recording points then produce different outcomes: + +- From Rex7, the amount is weighed before it is recorded. + A node MUST evaluate the frame-local and transaction-level compute budgets against the frame's usage plus this amount, and MUST NOT record the amount when either would be exceeded. + That evaluation MUST happen once the frame's own accounting for the exit is complete — its final segment settled and its frame-exit resource usage merged — so the amount is weighed against the frame's whole usage rather than a total still missing part of it. + A frame that failed on any dimension before this point never reaches the evaluation: the EVM does not charge a deposit such a frame will not make, so there is nothing to record. + When the amount does not fit, the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing — the same outcome Rex5 and Rex6 produce — but the transaction's compute total reports only what it spent. + A frame-local exceed on this path MUST NOT be latched: with the amount unrecorded the transaction is within every limit, and the frames above it MAY continue. + A transaction-level exceed MUST be latched and MUST halt the transaction with the usual gas rescue, and MUST carry the same detention attribution it would have carried had the amount been recorded. +- Under Rex5 and Rex6, the recording precedes the commit: the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing, but the recorded amount stands — recording precedes exceed evaluation, and compute gas is never reverted. + The amount stands on every path that fails the frame after it, not only on a compute-gas exceed. - Under Rex4, the only earlier spec with a per-frame budget, the recording happens after the EVM has already charged the deposit and committed the created contract. A frame-budget exceed latched by this recording therefore produces a split outcome: the frame's result is the frame-local revert, while the deployed code remains committed. A node MUST NOT roll the deployment back on this path. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 5d1fae5c..ba22d171 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -28,6 +28,7 @@ Rex7 also makes two guard- and detention-related choices that Rex6 does not: - A `disableVolatileDataAccess` rejection still charges the rejected opcode's static fee. - A detention mark is produced when the target account is loaded, so a frame that cannot afford the fees charged before that load produces no mark. +- A contract creation's code-deposit compute gas is weighed before it is recorded, so a creation that fails at its frame exit is no longer charged compute gas for a deposit it never made. Two deliberate accounting carve-outs remain. A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its whole EVM-gas budget as compute gas, apart from storage gas it had already been charged, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. @@ -244,6 +245,28 @@ Under Rex7, a node MUST produce a beneficiary or oracle detention mark when the A CALL-family opcode whose static fee or value-transfer fee exhausts the frame, and an `EXTCODECOPY` whose copy cost exhausts the frame, therefore halt without marking, and the rest of the transaction runs undetained unless some other access has already marked. This is specified behavior, not a replay exception. +### Conditional Code-Deposit Compute Gas Recording + +#### Previous behavior + +From [Rex5](rex5.md), a node records a contract creation's code-deposit compute gas (`code_length × CODEDEPOSIT`) before the deployment is committed, so that a compute-limit exceed caused by that amount fails the frame while the deployment is still revertible. +The amount is recorded first and the limit is evaluated afterwards, so it stays in the transaction's compute total whichever way the frame then ends. +It stays there on every path that fails the frame after the recording, not only on a compute-gas exceed: a data-size or state-growth exceed detected as the frame exits fails the frame just as effectively, and the EVM then charges no deposit at all. +The recorded amount is therefore compute gas that nothing spent, which both the transaction's reported total and the block's compute accounting carry. + +#### New behavior + +Under Rex7, a node MUST weigh the amount before recording it. + +A node MUST evaluate the frame-local and transaction-level compute budgets against the frame's usage plus the amount, and MUST record the amount only when neither budget would be exceeded. +That evaluation MUST happen once the frame's own accounting for the exit is complete — its final segment settled and its frame-exit resource usage merged — so the amount is weighed against the frame's whole usage. +A frame that has already failed at that point never reaches the evaluation, and nothing is recorded for it. + +When the amount does not fit, the outcome for the deployment is unchanged from Rex5: the frame fails as specified in [Exceed Behavior](../evm/compute-gas.md#exceed-behavior) and the deployment commits nothing. +What changes is what the transaction reports. +A frame-local exceed on this path MUST NOT be latched — with the amount unrecorded the transaction is within every limit, and the frames above it MAY continue. +A transaction-level exceed MUST be latched and MUST halt the transaction with the usual gas rescue, and MUST carry the same detention attribution it would have carried had the amount been recorded. + ## Developer Impact Rex7 is not scheduled on any network. @@ -262,6 +285,9 @@ The executed half does enforce, so a contract that calls into a failing child an A contract that calls a precompile which then fails is on the same split: work the precompile performed still binds the remaining compute budget; the unused forwarded envelope does not. Under Rex6 that unused envelope was enforcing, so the same tail work can survive under Rex7 and starve under Rex6. +A contract creation that fails at its frame exit reports `code_length × CODEDEPOSIT` less compute gas under Rex7 than under Rex6, and leaves that much more of the transaction's and the block's compute budget for the work that follows. +Whether the creation succeeds, what it deploys, and the receipt it produces are unchanged. + ## Safety and Compatibility Rex7 changes nothing about how blocks under earlier specs are executed. @@ -276,6 +302,9 @@ Rex7 can report more compute gas than Rex6 for the same inputs on four paths: th The carve-out's enforcing half is never looser than Rex6's on interpreter frames, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. On a precompile that fails before performing work, Rex7 enforcement is deliberately looser than Rex6's: the unused envelope does not bind the compute limit. +Rex7 reports less compute gas than Rex6 on one path: a contract creation that fails at its frame exit, whose code-deposit compute gas Rex6 records and Rex7 does not. +Enforcement is looser there by the same amount, and deliberately so — the EVM charges that amount only for a deposit that happens, so enforcing it against a creation that failed would bind the compute limit with gas nobody spent. + ## References - [Hardforks and Specs](../hardfork-spec.md) — how specs are versioned, frozen, and activated. From b4d8d0c23990a6d6ac2fc8612af7a47eb06e7c24 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 20 Aug 2026 12:38:23 +0800 Subject: [PATCH 077/208] fix(rex7): read the code-deposit charge off the active gas schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revm's create-return debits `gas_params().code_deposit_cost(len)`, so an embedder that installs its own gas schedule moves the amount a successful CREATE pays. REX7's settlement read revm's built-in per-byte constant instead, which under such a schedule made the recorded charge differ from the debited one on every CREATE, and made the affordability predicate answer for a charge revm was not about to take. Take all three readings — the predicate, the weighing peek and the record — from the configuration's schedule. REX5/REX6 keep the constant: their behavior is frozen, and they have no conservation law behind the charge. Under the default schedule the two readings are the same number, so nothing about a mainnet transaction changes. --- crates/mega-evm/src/evm/execution.rs | 61 ++++-- .../tests/rex7/create_code_deposit_charge.rs | 207 +++++++++++++++++- 2 files changed, 244 insertions(+), 24 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index a316743c..961ba0a5 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -512,9 +512,11 @@ impl MegaEvm { // revm's return_create would not charge it; the existing // limit-side hook below owns the result-marking on exceed. if is_rex5 && !is_rex7 && frame.data.is_create() { - if let Some(canonical_code_deposit_gas) = - canonical_code_deposit_gas(ctx, interpreter_result) - { + if let Some(canonical_code_deposit_gas) = canonical_code_deposit_gas( + ctx, + interpreter_result, + frozen_code_deposit_gas(interpreter_result.output.len()), + ) { let _ = ctx .additional_limit .borrow_mut() @@ -526,9 +528,11 @@ impl MegaEvm { // Update additional limits. MiniRex is guaranteed to be enabled here. ctx.additional_limit.borrow_mut().after_frame_run_instructions(frame, action); - // REX7: settle the same canonical code-deposit charge, after the hook above has closed the - // frame's tail segment, merged this frame's non-compute usage and marked the result if any - // of that put the frame over a limit. Two things follow from settling here rather than + // REX7: settle the canonical code-deposit charge — read off the active gas schedule, so + // the amount weighed and recorded is the amount revm will debit even under a schedule an + // embedder installed — after the hook above has closed the frame's tail segment, merged + // this frame's non-compute usage and marked the result if any of that put the frame over a + // limit. Two things follow from settling here rather than // ahead of the hook. The charge is weighed against the frame's complete usage instead of a // total still missing its tail. And a frame the hook already failed is skipped, because the // marked result fails the deposit predicate — which is the point: revm only charges the @@ -537,9 +541,11 @@ impl MegaEvm { if is_rex7 { if let InterpreterAction::Return(interpreter_result) = action { if frame.data.is_create() { - if let Some(canonical_code_deposit_gas) = - canonical_code_deposit_gas(ctx, interpreter_result) - { + if let Some(canonical_code_deposit_gas) = canonical_code_deposit_gas( + ctx, + interpreter_result, + active_code_deposit_gas(ctx, interpreter_result.output.len()), + ) { let rewrite = ctx .additional_limit .borrow_mut() @@ -587,12 +593,37 @@ impl MegaEvm { } } -/// The canonical code-deposit compute gas revm will charge a CREATE frame, or `None` when +/// The code-deposit compute gas a CREATE returning `output_len` bytes is charged under the +/// configuration's active gas schedule — the same reading `return_create` takes. +/// +/// An embedder may install its own schedule, so the per-byte rate is not necessarily revm's +/// built-in constant. Reading it here keeps the amount `MegaETH` weighs and records equal to the +/// amount revm debits, whatever schedule the configuration carries. +#[inline] +fn active_code_deposit_gas( + ctx: &MegaContext, + output_len: usize, +) -> u64 { + ctx.cfg().gas_params().code_deposit_cost(output_len) +} + +/// The frozen REX5/REX6 reading of the same charge: revm's built-in per-byte rate as a constant. +/// +/// Those specs record the charge without a conservation law behind it, so a schedule that departs +/// from the constant only shifts their reported compute total. Their behavior is frozen, which +/// makes the constant the definition rather than an approximation of one. +#[inline] +fn frozen_code_deposit_gas(output_len: usize) -> u64 { + (output_len as u64).saturating_mul(revm::interpreter::gas::CODEDEPOSIT) +} + +/// `code_deposit_gas` if revm will actually charge it to this CREATE frame, `None` when /// `return_create` would not charge it at all. #[inline] fn canonical_code_deposit_gas( ctx: &MegaContext, interpreter_result: &InterpreterResult, + code_deposit_gas: u64, ) -> Option { let cfg = ctx.cfg(); will_return_create_charge_code_deposit( @@ -600,14 +631,13 @@ fn canonical_code_deposit_gas( cfg.max_code_size(), cfg.spec().into_eth_spec(), cfg.is_eip3541_disabled(), + code_deposit_gas, ) - .then(|| { - (interpreter_result.output.len() as u64).saturating_mul(revm::interpreter::gas::CODEDEPOSIT) - }) + .then_some(code_deposit_gas) } /// Mirrors `revm_handler::frame::return_create`'s pre-commit predicate. -/// Returns `true` iff `return_create` would charge `code_len * CODEDEPOSIT` +/// Returns `true` iff `return_create` would charge `code_deposit_gas` /// from the interpreter gas and commit the checkpoint. /// /// REVIEW ON UPSTREAM BUMP: keep in lockstep with @@ -619,6 +649,7 @@ fn will_return_create_charge_code_deposit( max_code_size: usize, runtime_spec_id: revm::primitives::hardfork::SpecId, is_eip3541_disabled: bool, + code_deposit_gas: u64, ) -> bool { use revm::primitives::hardfork::SpecId; @@ -636,8 +667,6 @@ fn will_return_create_charge_code_deposit( { return false; } - let code_deposit_gas = (interpreter_result.output.len() as u64) - .saturating_mul(revm::interpreter::gas::CODEDEPOSIT); interpreter_result.gas.remaining() >= code_deposit_gas } diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs index abf25b8b..5bc9865d 100644 --- a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -1,11 +1,12 @@ //! REX7: the canonical code-deposit compute gas of a CREATE frame is recorded only when the //! deposit actually happens. //! -//! revm charges a successful CREATE `code_len * CODEDEPOSIT` when it processes the frame's action, -//! and only then — a frame whose result is no longer successful at that point pays nothing and -//! deposits nothing. `MegaETH` has to decide the charge one step earlier, before the action is -//! processed, because a compute-limit exceed discovered after the CREATE checkpoint is committed -//! would leave the frame's state changes in the journal under a reverted result. +//! revm charges a successful CREATE the active gas schedule's per-byte code-deposit rate when it +//! processes the frame's action, and only then — a frame whose result is no longer successful at +//! that point pays nothing and deposits nothing. `MegaETH` has to decide the charge one step +//! earlier, before the action is processed, because a compute-limit exceed discovered after the +//! CREATE checkpoint is committed would leave the frame's state changes in the journal under a +//! reverted result. //! //! Deciding early is not the same as charging early. REX5 and REX6 record the charge and then let //! the latched exceed mark the result, which leaves the tracker holding compute gas for a deposit @@ -17,16 +18,19 @@ //! These tests pin the four rows of that decision, the frozen REX4-REX6 shapes they must not //! disturb, and the journal consistency that the early decision exists for in the first place. -use crate::common::{default_envs, transact_tx, Outcome, CALLER, ONE_ETH}; +use crate::common::{default_envs, finish, transact_tx, Outcome, CALLER, ONE_ETH}; use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_sol_types::SolError as _; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, + EthHaltReason, EvmTxRuntimeLimits, LimitKind, MegaContext, MegaEvm, MegaHaltReason, + MegaLimitExceeded, MegaSpecId, MegaTransaction, MegaTransactionNew as _, OpHaltReason, }; use revm::{ bytecode::opcode::{MSTORE, POP, RETURN, TIMESTAMP}, - context::{result::ExecutionResult, tx::TxEnvBuilder}, + context::{result::ExecutionResult, tx::TxEnvBuilder, CfgEnv}, + context_interface::cfg::GasId, + handler::EvmTr, state::EvmState, }; @@ -420,3 +424,190 @@ fn test_frozen_specs_keep_their_create_journal_shapes() { ); } } + +/// The per-byte code-deposit rate the override cases install, one gas above revm's built-in +/// [`CODEDEPOSIT`]. Any value that differs would do; one gas apart keeps the arithmetic below +/// legible and makes the gap between the two readings exactly [`RUNTIME_LEN`]. +const OVERRIDDEN_CODEDEPOSIT: u64 = CODEDEPOSIT + 1; + +/// The code-deposit charge for [`RUNTIME_LEN`] bytes under the overridden schedule. +const OVERRIDDEN_CODE_DEPOSIT_GAS: u64 = RUNTIME_LEN * OVERRIDDEN_CODEDEPOSIT; + +/// Runs [`return_zeros_initcode`] as a REX7 creation transaction with `gas_limit`, under a +/// configuration whose gas schedule charges `rate` gas per deployed byte. +/// +/// An embedder-installed gas schedule is a supported configuration, and revm's create-return reads +/// the code-deposit rate off it. The shared helpers all run the default schedule, so this builds +/// the context itself — everything else about the transaction matches [`create`]. +fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { + let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(10 * ONE_ETH)); + let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); + cfg.gas_params.override_gas([(GasId::code_deposit_cost(), rate)]); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::no_limits()); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .kind(TxKind::Create) + .gas_limit(gas_limit) + .gas_price(0) + .data(return_zeros_initcode(RUNTIME_LEN)) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); + let (non_compute_gas, minted_call_stipend, booked_destroyed) = + additional_limit.conservation_terms_for_test(); + ( + additional_limit.detained_compute_gas_limit(), + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) + }; + finish( + MegaSpecId::REX7, + outcome, + detained_compute_gas_limit, + non_compute_gas, + minted_call_stipend, + booked_destroyed, + ) +} + +/// The charge REX7 records is the one the active gas schedule defines, not revm's built-in rate. +/// +/// revm's create-return debits `gas_params().code_deposit_cost(len)`, and an embedder may have +/// installed a schedule where that is not `len * CODEDEPOSIT`. Reading the constant instead would +/// record a charge that differs from the one debited on every successful CREATE — a standing break +/// of the conservation law the reported compute total is derived from, which is what +/// [`crate::common::finish`] checks on every transaction these helpers run. +#[test] +fn test_create_code_deposit_charge_follows_the_active_gas_schedule() { + let default_rate = create_at_rate(CODEDEPOSIT, TX_GAS_LIMIT); + assert!( + default_rate.is_success(), + "the default-schedule run must deploy: {:?}", + default_rate.result, + ); + assert_eq!( + default_rate.compute_gas, + calibrate(MegaSpecId::REX7).compute_gas, + "installing the built-in rate explicitly must not change what the shared helper measures", + ); + + let overridden = create_at_rate(OVERRIDDEN_CODEDEPOSIT, TX_GAS_LIMIT); + assert!( + overridden.is_success(), + "the overridden-schedule run must deploy too: {:?}", + overridden.result, + ); + assert!( + has_deployed_code(&overridden.state, deployed_address()), + "the overridden-schedule run must leave code behind", + ); + + // The whole difference between the two runs is the deposit charge, and it moved by exactly the + // rate difference — so the recorded amount tracks the schedule rather than the constant. + assert_eq!( + overridden.compute_gas, + default_rate.compute_gas + OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS, + "the recorded charge must follow the schedule (default={}, overridden={})", + default_rate.compute_gas, + overridden.compute_gas, + ); + assert_eq!( + overridden.gas_used, + default_rate.gas_used + OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS, + "the receipt moved by the same amount, which is what the recorded charge has to match", + ); + + // Nothing was destroyed, so the whole reported total enforces — and the terminal identity + // already checked that it accounts for the receipt envelope. + assert_eq!(overridden.destroyed, 0, "a successful CREATE destroys nothing"); + assert_eq!( + overridden.enforced(), + overridden.compute_gas, + "the whole reported total enforces when nothing is destroyed", + ); +} + +/// The knife edge the constant would fall off: a frame holding exactly the built-in rate's charge +/// under a schedule that charges more. +/// +/// `return_create` weighs the deposit against the schedule and takes the frame out of gas. A +/// predicate reading the constant would answer "affordable", record a charge nobody is ever +/// debited, and leave the deposit rejected anyway — the phantom accounting this decision exists to +/// prevent. Reading the schedule answers "unaffordable": nothing is recorded, and the remainder the +/// rejected frame never spends is settled as destroyed like any other exceptional halt's. +#[test] +fn test_create_code_deposit_knife_edge_under_an_overridden_schedule() { + // A gas limit equal to what the unconstrained run spends leaves the frame exactly the + // schedule's charge at the deposit; one deployed byte's worth less leaves it exactly the + // built-in rate's charge, which is the point being tested. + let unconstrained = create_at_rate(OVERRIDDEN_CODEDEPOSIT, TX_GAS_LIMIT); + assert!(unconstrained.is_success(), "{:?}", unconstrained.result); + let exactly_affordable_gas_limit = unconstrained.total_gas_spent; + + let exact = create_at_rate(OVERRIDDEN_CODEDEPOSIT, exactly_affordable_gas_limit); + assert!( + exact.is_success(), + "a frame holding exactly the schedule's charge must deposit: {:?}", + exact.result, + ); + assert!( + has_deployed_code(&exact.state, deployed_address()), + "the exactly-affordable CREATE must leave code behind", + ); + assert_eq!( + exact.compute_gas, unconstrained.compute_gas, + "the gas limit is not part of the work; only the room left over changed", + ); + + let knife = create_at_rate( + OVERRIDDEN_CODEDEPOSIT, + exactly_affordable_gas_limit - (OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS), + ); + assert!( + matches!( + knife.result, + ExecutionResult::Halt { + reason: MegaHaltReason::Base(OpHaltReason::Base(EthHaltReason::OutOfGas(_))), + .. + } + ), + "a frame holding only the built-in rate's charge must be taken out of gas by the \ + create-return: {:?}", + knife.result, + ); + assert!( + !has_deployed_code(&knife.state, deployed_address()), + "the rejected CREATE must leave no code behind", + ); + + // The deposit was not recorded: what the constant would have called affordable is exactly what + // the halted frame destroyed instead. + assert_eq!( + knife.destroyed, CODE_DEPOSIT_GAS, + "the frame's whole remainder — the built-in rate's charge — must settle as destroyed", + ); + assert_eq!( + knife.booked_destroyed, knife.destroyed, + "the per-site booking must agree with the derived destroyed total", + ); + assert_eq!( + knife.enforced() + OVERRIDDEN_CODE_DEPOSIT_GAS, + exact.compute_gas, + "the enforced lane must hold the frame's work and none of the deposit (enforced={}, \ + successful total={})", + knife.enforced(), + exact.compute_gas, + ); +} From 208861f32ba4702f8840fecf1ba6772c8e5591c5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 20 Aug 2026 15:56:12 +0800 Subject: [PATCH 078/208] feat: reject a gas schedule that deviates from the spec's revm 40 turned every operation's price into a `CfgEnv.gas_params` table an embedder can rewrite, but several MegaETH accounting sites carry the schedule's values as constants: the `CALL_STIPEND` a value-transferring call mints (booked for the destroyed-gas conservation law, and subtracted back out by the 98/100 forwarding cap), the pre-REX7 per-byte code-deposit rate, and the mainnet table the keyless-deploy preflight estimates intrinsic gas from. Under a rewritten table those sites book something other than what revm charged. The gas schedule is a property of the spec, so a configuration carrying anything other than `GasParams::new_spec(SpecId::from(cfg.spec))` is now rejected with a panic naming the entry that deviated and both values, rather than executed. The check runs at both `with_cfg` entry points (covering the factory, the block executor and every tool), at the deprecated `new_with_context`, and again at the point of use before every transaction, which also covers a configuration mutated in place after the context was built. It is unconditional across specs: the configuration domain has no historical block coverage to preserve. Tests that exercised a rewritten schedule become pins that it is rejected. The CREATE knife-edge case separating the active-schedule reading from the constant is removed with a note in the module doc: it needed an inadmissible configuration, so the two readings can no longer disagree on any input. --- AGENTS.md | 6 + crates/mega-evm/src/evm/context.rs | 341 ++++++++++++++++-- crates/mega-evm/src/evm/execution.rs | 12 +- crates/mega-evm/src/evm/factory.rs | 124 ++++--- crates/mega-evm/src/evm/instructions.rs | 7 +- .../tests/rex7/create_code_deposit_charge.rs | 164 ++------- 6 files changed, 431 insertions(+), 223 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 486b35ef..485ae4cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -327,6 +327,12 @@ When the agent is requested to implement a new feature or bug fix, it should con Never change what an existing stable spec does. - **System contract changes require a new spec.** Do not modify system contract Solidity sources or their Rust integration without also introducing a new spec for backward compatibility. +- **The gas schedule belongs to the spec, not to `CfgEnv`.** + revm 40 made every operation's price a `CfgEnv.gas_params` table an embedder can rewrite, but several `MegaETH` accounting sites carry the schedule's values as constants (the `CALL_STIPEND` a value-transferring call mints, the pre-`REX7` per-byte code-deposit rate, the mainnet table the keyless-deploy preflight estimates intrinsic gas from). + A configuration whose `gas_params` is not exactly `GasParams::new_spec(SpecId::from(cfg.spec))` is therefore rejected with a panic rather than executed, at both `with_cfg` entry points, at the deprecated `new_with_context`, and again at the point of use before every transaction — so a configuration mutated in place after the context was built is caught too. + The check is unconditional across specs: it governs the configuration domain, which no historical block covers. + Build configurations with `CfgEnv::new_with_spec(spec)` or `cfg.set_spec_and_mainnet_gas_params(spec)`; do not add a way to opt out, and do not add a tool-only bypass. + New code that needs one of the schedule's values may read it from `cfg().gas_params()` or restate the constant — under the pin the two are equal, and reading the table is the preferred form for unfrozen specs. - **Override `HardforkParams::validate()` for every new params type.** The default implementation accepts any value silently. Override it with field-level invariant checks (e.g., non-zero addresses) so that `with_params()` panics loudly at chain-config load time rather than allowing the error to surface at the first block where the fork activates. diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index 52c31651..cd0447a6 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -37,8 +37,12 @@ pub type MegaInnerContext = revm::Context< >; use revm::{ context::{BlockEnv, CfgEnv, ContextSetters, ContextTr, LocalContext}, - context_interface::context::ContextError, + context_interface::{ + cfg::{GasId, GasParams}, + context::ContextError, + }, database::EmptyDB, + primitives::hardfork::SpecId as EthSpecId, Journal, }; @@ -237,6 +241,13 @@ impl MegaContext { /// # Returns /// /// Returns a new `Context` instance wrapping the provided context. + /// + /// # Panics + /// + /// If the provided context's `gas_params` is not the schedule its own `cfg.spec` defines — + /// the gas schedule belongs to the spec, see [`assert_spec_owned_gas_schedule`]. A + /// configuration still on its builder's default spec is fine: the spec relabel below + /// re-derives the schedule for `spec`. #[deprecated(note = "Use `MegaContext::new` instead")] pub fn new_with_context( context: MegaInnerContext, @@ -245,6 +256,12 @@ impl MegaContext { ) -> Self { let mut inner = context; + // Checked against the spec the caller's configuration itself carries, before the relabel + // below re-derives the schedule: a configuration still on its builder's default op-spec + // (`revm::Context::op()` starts at `BEDROCK`) is a supported input here, but its schedule + // must be that spec's, not one the caller rewrote. + assert_spec_owned_gas_schedule(&inner.cfg); + // Spec in context must keep the same with parameter `spec`. // revm 40 keeps per-spec `GasParams` in `CfgEnv`, so update both together — // bare `cfg.spec = ...` would leave the caller's (e.g. BEDROCK) params in place. @@ -381,6 +398,19 @@ impl MegaContext { /// configuration that explicitly set it. An embedder that wants the gate opts in through /// [`with_cfg_unpinned`](Self::with_cfg_unpinned), where the field is taken as provided. /// + /// # The gas schedule is defined by the spec + /// + /// `cfg.gas_params` must be exactly the schedule `cfg.spec` defines — the table + /// `CfgEnv::new_with_spec(spec)` and `cfg.set_spec_and_mainnet_gas_params(spec)` install. + /// `MegaETH`'s gas schedule is a property of the spec rather than of the configuration, so + /// there is no supported way to override it, and a configuration that deviates is rejected + /// here with a panic rather than run. See [`assert_spec_owned_gas_schedule`] for why the + /// deviation cannot be tolerated and where else the same check runs. + /// + /// # Panics + /// + /// If `cfg.gas_params` is not the schedule `cfg.spec` defines. + /// /// # Arguments /// /// * `cfg` - The configuration environment @@ -401,11 +431,18 @@ impl MegaContext { /// /// Skipping that pin does not skip `MegaETH`'s own consensus pins: /// + /// - The gas schedule is defined by the spec: a `gas_params` that deviates from what `cfg.spec` + /// defines panics here exactly as it does in [`with_cfg`](Self::with_cfg) — see + /// [`assert_spec_owned_gas_schedule`]. /// - EIP-8037 (Amsterdam state gas) is forced off before every transaction runs, wherever the /// configuration came from — see [`force_amsterdam_eip8037_off`]. /// - Under `MINI_REX` and later, the contract size and initcode size limits fill in when the /// configuration leaves them unset. /// + /// # Panics + /// + /// If `cfg.gas_params` is not the schedule `cfg.spec` defines. + /// /// # Arguments /// /// * `cfg` - The configuration environment @@ -427,7 +464,13 @@ impl MegaContext { /// already set by [`with_tx_runtime_limits`](Self::with_tx_runtime_limits) are kept; they are /// not replaced by the new spec's defaults. An unchanged spec leaves the existing tracker in /// place. + /// + /// # Panics + /// + /// If `cfg.gas_params` is not the schedule `cfg.spec` defines — the gas schedule belongs to + /// the spec, see [`assert_spec_owned_gas_schedule`]. fn apply_cfg(mut self, cfg: CfgEnv, intent: CfgIntent) -> Self { + assert_spec_owned_gas_schedule(&cfg); let new_spec = cfg.spec; let spec_changed = new_spec != self.spec; self.spec = new_spec; @@ -711,6 +754,7 @@ impl MegaContext { /// /// DB-dependent pre-frame usage may still be recorded later during pre-execution. pub(crate) fn on_new_tx(&mut self) { + assert_spec_owned_gas_schedule(&self.inner.cfg); force_amsterdam_eip8037_off(&mut self.inner.cfg); self.reset_volatile_data_access(); @@ -861,6 +905,79 @@ pub(crate) fn force_amsterdam_eip8037_off(cfg: &mut CfgEnv) { cfg.enable_amsterdam_eip8037 = false; } +/// Panics unless `cfg` carries exactly the gas schedule its own `spec` defines. +/// +/// `MegaETH`'s gas schedule belongs to the spec, not to the configuration. revm 40 turned the +/// price of every operation into a `CfgEnv::gas_params` table an embedder can rewrite, and +/// `MegaETH`'s own accounting does not read that table everywhere revm does: several recording +/// sites carry the schedule's value as a constant (the `CALL_STIPEND` a value-transferring call +/// mints, the pre-`REX7` per-byte code-deposit rate, the mainnet table the keyless-deploy +/// preflight estimates intrinsic gas from). Under a rewritten table those sites would book +/// something other than what revm charged, which silently breaks the conservation law the +/// reported compute total is derived from — and, for a table that prices the call stipend below +/// revm's, takes the 98/100 forwarding cap's subtraction below zero. +/// +/// Rather than teach every such site to read the table, the schedule is pinned: a configuration +/// whose `gas_params` deviates from its spec's is rejected outright, at the loudest available +/// signal, so no transaction ever runs on one. This mirrors [`crate::HardforkParams::validate`], +/// which panics at chain-config load time instead of letting a bad value surface at the first +/// block that uses it. The check is unconditional across specs: it governs the configuration +/// domain, which no historical block covers, so gating it on a spec would only narrow the +/// guarantee without preserving anything. +/// +/// Deviation is the only thing rejected — the rest of `CfgEnv` stays the embedder's, including +/// switches that change what a transaction costs by other means (`disable_eip7623`, +/// `limit_contract_code_size`, the blob caps). +/// +/// Called from the three points a configuration can reach the EVM through: both `with_cfg` +/// entry points (via [`MegaContext::apply_cfg`]), the deprecated +/// [`MegaContext::new_with_context`], and [`MegaContext::on_new_tx`] — the last one being the +/// point of use, which also covers a configuration mutated in place after the context was built +/// (reachable through the mutable deref, e.g. `ctx.modify_cfg`). The comparison is a pointer +/// compare in the common case: both tables come from the same per-spec `OnceLock` inside revm. +pub(crate) fn assert_spec_owned_gas_schedule + Clone + core::fmt::Debug>( + cfg: &CfgEnv, +) { + let expected = GasParams::new_spec(cfg.spec.clone().into()); + if cfg.gas_params != expected { + panic_gas_schedule_mismatch(&cfg.spec, &cfg.gas_params, &expected); + } +} + +/// Reports the first entry on which a configuration's gas schedule deviates from its spec's, and +/// panics. +/// +/// Split out of [`assert_spec_owned_gas_schedule`] and taking the spec as `&dyn Debug` so the +/// formatting and the table walk stay out of the caller's inlined fast path, and are emitted once +/// rather than per instantiation. +#[cold] +#[inline(never)] +fn panic_gas_schedule_mismatch( + spec: &dyn core::fmt::Debug, + actual: &GasParams, + expected: &GasParams, +) -> ! { + let mismatch = actual + .table() + .iter() + .zip(expected.table().iter()) + .enumerate() + .find(|(_, (got, want))| got != want); + let (id, got, want) = match mismatch { + Some((index, (got, want))) => (GasId::new(index as u8), *got, *want), + // `!=` on `GasParams` compares exactly these tables, so a mismatch always has an entry. + None => unreachable!("gas params differ but no table entry does"), + }; + panic!( + "gas params differ from the spec-defined schedule for {spec:?}: `{}` is {got}, the \ + schedule defines {want}. MegaETH's gas schedule is defined by the spec and cannot be \ + overridden through `CfgEnv::gas_params`; build the configuration with \ + `CfgEnv::new_with_spec(spec)` or `cfg.set_spec_and_mainnet_gas_params(spec)` and leave \ + the schedule alone.", + id.name(), + ) +} + /// A convenient trait to convert a `CfgEnv` into a `CfgEnv`. /// /// This trait provides a conversion method for `OpStack` configuration environments @@ -891,16 +1008,17 @@ impl IntoOpCfgEnv for CfgEnv { /// This method relabels the specification type and carries every other field of the /// caller's configuration — the gas schedule included — into the `OpStack` shape. It is a /// relabel and nothing more: the fields `MegaETH` does not let a caller choose are settled - /// where they are read, not here (EIP-8037 by [`force_amsterdam_eip8037_off`]). + /// where they are read, not here (EIP-8037 by [`force_amsterdam_eip8037_off`], the gas + /// schedule by [`assert_spec_owned_gas_schedule`]). /// /// # Returns /// /// Returns a new `CfgEnv` with all fields moved from `self`. fn into_op_cfg(self) -> CfgEnv { let op_spec = OpSpecId::from(self.spec); - // Keep the caller's gas schedule instead of re-deriving it from the spec: an embedder - // may have installed its own, and a spec-derived one is unaffected either way because - // every `MegaSpecId` and its op-spec map to the same eth hardfork. + // Carry the schedule rather than re-deriving it: the relabel must not be the thing that + // silently repairs a deviating table, and re-deriving would be a no-op on a conforming + // one anyway — every `MegaSpecId` and its op-spec map to the same eth hardfork. let gas_params = self.gas_params.clone(); // `with_spec_and_gas_params` is revm's own whole-struct carrier: it moves every field // (including the ones behind revm cargo features) into the new spec type, so fields @@ -949,9 +1067,10 @@ mod tests { use crate::{test_utils::MemoryDatabase, MegaTransactionNew as _, TestExternalEnvs}; - /// A gas schedule an embedder could install: the spec table with one entry moved off its - /// mainnet value. Distinct from every `GasParams::new_spec(..)` table, so a conversion that - /// re-derives the schedule from the spec instead of carrying it shows up as a diff. + /// A gas schedule an embedder could try to install: the spec table with one entry moved off + /// its mainnet value. Distinct from every `GasParams::new_spec(..)` table, so a conversion + /// that re-derives the schedule from the spec instead of carrying it shows up as a diff — and + /// so an entry point that admits it instead of rejecting it does too. fn custom_gas_params() -> GasParams { let mut gas_params = GasParams::new_spec(SpecId::PRAGUE); gas_params.override_gas([(GasId::tx_token_cost(), 40)]); @@ -959,6 +1078,31 @@ mod tests { gas_params } + /// The schedule `spec` defines — the one and only schedule an entry point admits. + fn spec_gas_params(spec: MegaSpecId) -> GasParams { + GasParams::new_spec(SpecId::from(spec)) + } + + /// An untouched configuration for `spec`, schedule included. + fn spec_cfg(spec: MegaSpecId) -> CfgEnv { + CfgEnv::new_with_spec(spec) + } + + /// Every `MegaSpecId`, so the schedule pin is asserted across the whole progression rather + /// than on whichever spec a test happened to pick. + const ALL_SPECS: [MegaSpecId; 10] = [ + MegaSpecId::EQUIVALENCE, + MegaSpecId::MINI_REX, + MegaSpecId::REX, + MegaSpecId::REX1, + MegaSpecId::REX2, + MegaSpecId::REX3, + MegaSpecId::REX4, + MegaSpecId::REX5, + MegaSpecId::REX6, + MegaSpecId::REX7, + ]; + /// A [`CfgEnv`] with every field moved off its revm default, so any field the conversion /// drops instead of carrying collapses back to a default and fails an equality assert. fn fully_customized_cfg(spec: MegaSpecId) -> CfgEnv { @@ -985,6 +1129,16 @@ mod tests { cfg } + /// [`fully_customized_cfg`] with the one field a caller does not own put back to the schedule + /// its spec defines, so the configuration reaches an entry point instead of being rejected by + /// it. Everything else is still off its revm default, which is what the carry-every-field + /// asserts need. + fn admissible_customized_cfg(spec: MegaSpecId) -> CfgEnv { + let mut cfg = fully_customized_cfg(spec); + cfg.gas_params = spec_gas_params(spec); + cfg + } + /// The `MegaSpecId` <-> `OpSpecId` config conversions relabel the spec type and nothing else: /// every other field — the gas schedule and the revm 40 switches included — belongs to the /// caller and must survive both legs, EIP-8037 included. What `MegaETH` does not let a caller @@ -1014,20 +1168,142 @@ mod tests { /// `with_cfg` is where an embedder's `CfgEnv` lands. It must reach the inner revm config /// intact — only the `MegaETH` pins (spec, `MINI_REX` size limits) may differ. #[test] - fn test_with_cfg_carries_embedder_gas_params_and_switches() { + fn test_with_cfg_carries_embedder_switches_and_the_spec_schedule() { let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX6); cfg.chain_id = 6342; - cfg.gas_params = custom_gas_params(); cfg.disable_eip7623 = true; - let context = - MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg.clone()); + let context = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); - assert_eq!(context.inner.cfg.gas_params, cfg.gas_params); + assert_eq!(context.inner.cfg.gas_params, spec_gas_params(MegaSpecId::REX6)); assert!(context.inner.cfg.disable_eip7623); assert_eq!(context.inner.cfg.chain_id, 6342); } + /// The gas schedule is not an embedder's to set. A configuration carrying anything other than + /// the schedule its spec defines is rejected at the entry point rather than run, because + /// `MegaETH` records several of the schedule's values from constants rather than from the + /// table and would otherwise book charges revm never made. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_with_cfg_rejects_a_schedule_off_the_spec_table() { + let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX6); + cfg.gas_params = custom_gas_params(); + + let _ = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); + } + + /// The `call_stipend` entry specifically: `MegaETH` books the stipend revm mints into a + /// value-transferring call's child frame from `gas::CALL_STIPEND`, and the 98/100 forwarding + /// cap subtracts the same constant back out of the child's budget. A schedule that priced the + /// stipend differently would desync both. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_with_cfg_rejects_an_overridden_call_stipend() { + let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); + cfg.gas_params.override_gas([(GasId::call_stipend(), 0)]); + + let _ = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); + } + + /// The `code_deposit_cost` entry specifically: revm debits a successful `CREATE` the + /// schedule's per-byte rate, and pre-`REX7` specs record that charge from the constant. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_with_cfg_rejects_an_overridden_code_deposit_cost() { + let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); + cfg.gas_params.override_gas([(GasId::code_deposit_cost(), 201)]); + + let _ = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); + } + + /// The rejection reports the spec whose schedule was expected, which entry deviated and both + /// values, so an embedder that hits it can act on the message rather than bisect its + /// configuration. + #[test] + #[should_panic( + expected = "schedule for REX7: `code_deposit_cost` is 201, the schedule defines 200" + )] + fn test_schedule_rejection_names_the_spec_the_entry_and_both_values() { + let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); + cfg.gas_params.override_gas([(GasId::code_deposit_cost(), 201)]); + + let _ = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); + } + + /// Every spec's own default schedule is admissible on every entry point, and reaches the EVM + /// as written. The pin is a rejection of deviation, not a narrowing of which specs run. + #[test] + fn test_every_spec_default_schedule_is_admissible() { + for spec in ALL_SPECS { + let expected = spec_gas_params(spec); + + let pinned = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE) + .with_cfg(spec_cfg(spec)); + assert_eq!(pinned.inner.cfg.gas_params, expected, "with_cfg on {spec:?}"); + + let unpinned = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE) + .with_cfg_unpinned(spec_cfg(spec)); + assert_eq!(unpinned.inner.cfg.gas_params, expected, "with_cfg_unpinned on {spec:?}"); + } + } + + /// The constructors that build their own configuration install the spec's schedule, so the + /// contexts that never see a caller's `CfgEnv` satisfy the pin by construction. This is the + /// path the keyless-deploy sandbox takes: it builds its inner context from + /// [`MegaContext::new_with_shared_ext_envs`] rather than inheriting the outer configuration, + /// so no override could reach it even if one had been admitted outside. + #[test] + fn test_constructors_install_the_spec_schedule_on_every_spec() { + for spec in ALL_SPECS { + let expected = spec_gas_params(spec); + + let plain = MegaContext::new(EmptyDB::default(), spec); + assert_eq!(plain.inner.cfg.gas_params, expected, "MegaContext::new on {spec:?}"); + + let sandbox_shaped = MegaContext::<_, EmptyExternalEnv>::new_with_shared_ext_envs( + EmptyDB::default(), + spec, + Rc::new(EmptyExternalEnv), + Rc::new(RefCell::new(EmptyExternalEnv)), + ); + assert_eq!( + sandbox_shaped.inner.cfg.gas_params, expected, + "the sandbox's constructor on {spec:?}", + ); + } + } + + /// Migrating a live context between specs leaves it on the schedule the new spec defines, in + /// both directions — the entry point adopts the whole configuration, so the schedule cannot + /// be left behind from the spec the context previously ran. + #[test] + fn test_spec_migration_keeps_the_schedule_on_the_active_spec() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE); + + for spec in [MegaSpecId::REX5, MegaSpecId::REX7, MegaSpecId::REX5, MegaSpecId::MINI_REX] { + context = context.with_cfg(spec_cfg(spec)); + + assert_eq!(context.mega_spec(), spec); + assert_eq!(context.inner.cfg.gas_params, spec_gas_params(spec), "after {spec:?}"); + // And the migrated context is one a transaction can run on: `on_new_tx` re-checks the + // schedule at the point of use. + context.on_new_tx(); + } + } + + /// The entry points are not the only place the schedule is checked: it is re-checked at the + /// point of use, so a configuration mutated in place after the context was built — reachable + /// through the mutable deref, e.g. `ctx.modify_cfg` — cannot execute either. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_on_new_tx_rejects_a_schedule_mutated_after_construction() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7); + context.modify_cfg(|cfg| cfg.gas_params = custom_gas_params()); + + context.on_new_tx(); + } + /// The pin is unconditional: a configured cfg — here a blob schedule plus an explicitly /// enabled check — still comes out with the gate off. `with_cfg` is the compatibility entry /// point; enabling the gate requires `with_cfg_unpinned`. @@ -1045,19 +1321,6 @@ mod tests { ); } - /// The pin does not depend on the rest of the configuration: a custom gas schedule rides - /// through while the gate still comes out pinned off. - #[test] - fn test_with_cfg_pins_chain_id_check_off_with_custom_gas_params() { - let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX5); - cfg.gas_params = custom_gas_params(); - - let context = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg(cfg); - - assert!(!context.inner.cfg.tx_chain_id_check); - assert_eq!(context.inner.cfg.gas_params, custom_gas_params()); - } - /// An untouched `CfgEnv::new_with_spec` config carries revm 40's flipped default, which the /// caller never asked for: `with_cfg` re-pins revm 27's `false`. #[test] @@ -1094,7 +1357,7 @@ mod tests { /// inner revm config as written. #[test] fn test_with_cfg_unpinned_carries_every_field() { - let mut cfg = fully_customized_cfg(MegaSpecId::REX6); + let mut cfg = admissible_customized_cfg(MegaSpecId::REX6); cfg.tx_chain_id_check = true; let context = MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE) @@ -1104,6 +1367,17 @@ mod tests { assert_eq!(context.inner.cfg, cfg.into_op_cfg()); } + /// Skipping the chain-id pin does not skip the schedule check: the escape hatch is an opt-in + /// to revm 40's chain-id gate, not to owning the gas schedule. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_with_cfg_unpinned_rejects_a_schedule_off_the_spec_table() { + let cfg = fully_customized_cfg(MegaSpecId::REX6); + + let _ = + MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE).with_cfg_unpinned(cfg); + } + /// The opt-in skips the chain-id pin, not `MegaETH`'s other normalization: the spec is /// adopted and the `MINI_REX` size limits still fill in when unset. #[test] @@ -1174,24 +1448,25 @@ mod tests { assert!(!alloy_evm::Evm::cfg_env(&evm).enable_amsterdam_eip8037); } - /// The deprecated constructor re-derives the gas schedule only when it applies a different - /// op-spec. A caller already sitting on the `MegaETH` op-spec keeps its own schedule. + /// The deprecated constructor rejects a rewritten schedule like every other entry point. Its + /// input is checked against the spec the caller's own configuration carries — the relabel + /// below re-derives the schedule for a configuration on a different spec, and must not be + /// what quietly repairs a rewritten one. #[allow(deprecated)] #[test] - fn test_new_with_context_keeps_gas_params_when_spec_already_matches() { + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_new_with_context_rejects_a_schedule_off_the_spec_table() { let mut inner: MegaInnerContext = revm::Context::op() .with_tx(crate::MegaTransaction::default()) .with_db(EmptyDB::default()); inner.cfg.set_spec_and_mainnet_gas_params(MegaSpecId::EQUIVALENCE.into_op_spec()); inner.cfg.gas_params = custom_gas_params(); - let context = MegaContext::new_with_context( + let _ = MegaContext::new_with_context( inner, MegaSpecId::EQUIVALENCE, ExternalEnvs::::default(), ); - - assert_eq!(context.inner.cfg.gas_params, custom_gas_params()); } /// Same unconditional pin at the deprecated constructor: a configured context that enabled diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 961ba0a5..4e265488 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -596,9 +596,10 @@ impl MegaEvm { /// The code-deposit compute gas a CREATE returning `output_len` bytes is charged under the /// configuration's active gas schedule — the same reading `return_create` takes. /// -/// An embedder may install its own schedule, so the per-byte rate is not necessarily revm's -/// built-in constant. Reading it here keeps the amount `MegaETH` weighs and records equal to the -/// amount revm debits, whatever schedule the configuration carries. +/// The active schedule is required to be the one the spec defines, so this reads the same +/// per-byte rate as revm's built-in constant. Reading it off the schedule rather than restating +/// the constant keeps the amount `MegaETH` weighs and records tied to the amount revm debits at +/// the source, so the two cannot drift apart independently. #[inline] fn active_code_deposit_gas( ctx: &MegaContext, @@ -609,9 +610,8 @@ fn active_code_deposit_gas( /// The frozen REX5/REX6 reading of the same charge: revm's built-in per-byte rate as a constant. /// -/// Those specs record the charge without a conservation law behind it, so a schedule that departs -/// from the constant only shifts their reported compute total. Their behavior is frozen, which -/// makes the constant the definition rather than an approximation of one. +/// Those specs record the charge without a conservation law behind it, and their behavior is +/// frozen, which makes the constant the definition rather than an approximation of one. #[inline] fn frozen_code_deposit_gas(output_len: usize) -> u64 { (output_len as u64).saturating_mul(revm::interpreter::gas::CODEDEPOSIT) diff --git a/crates/mega-evm/src/evm/factory.rs b/crates/mega-evm/src/evm/factory.rs index 6afe5241..5b4c6c4e 100644 --- a/crates/mega-evm/src/evm/factory.rs +++ b/crates/mega-evm/src/evm/factory.rs @@ -197,17 +197,12 @@ mod tests { const CHAIN_ID: u64 = 6342; const SENDER: Address = address!("0000000000000000000000000000000000000f00"); const TARGET: Address = address!("0000000000000000000000000000000000000f01"); - /// revm's mainnet cost per calldata token. - const MAINNET_TX_TOKEN_COST: u64 = 4; - /// The per-token cost an embedder installs in place of the mainnet one. + /// The per-token cost an embedder might try to install in place of the mainnet one. const CUSTOM_TX_TOKEN_COST: u64 = 40; /// revm's mainnet EIP-7623 floor cost per calldata token. const MAINNET_TX_FLOOR_COST_PER_TOKEN: u64 = 10; /// revm's mainnet base cost of a transaction, the constant part of the EIP-7623 floor. const MAINNET_TX_BASE_STIPEND: u64 = 21_000; - /// Zero calldata bytes are one EIP-7623 token each. This many keeps the transaction above - /// every gas floor, so its gas used tracks the per-token cost of the schedule directly. - const UNFLOORED_CALLDATA_TOKENS: u64 = 100; /// Enough calldata tokens that the EIP-7623 floor rises above the transaction's own cost /// (`MegaETH`'s calldata storage gas included), so the floor decides the gas used. const FLOOR_BINDING_CALLDATA_TOKENS: u64 = 2_000; @@ -227,17 +222,18 @@ mod tests { } /// The mainnet `PRAGUE` schedule with the calldata token cost moved off its mainnet value — - /// the kind of override an embedder installs for its own chain. + /// the kind of override an embedder might reach for, and which the factory rejects. fn embedder_gas_params(tx_token_cost: u64) -> GasParams { let mut gas_params = GasParams::new_spec(SpecId::PRAGUE); gas_params.override_gas([(GasId::tx_token_cost(), tx_token_cost)]); gas_params } - fn embedder_cfg(tx_token_cost: u64, disable_eip7623: bool) -> CfgEnv { + /// A `REX6` configuration an embedder hands the factory. Only `disable_eip7623` is moved off + /// its default — the gas schedule is the spec's, which is the only one the factory admits. + fn embedder_cfg(disable_eip7623: bool) -> CfgEnv { let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX6); cfg.chain_id = CHAIN_ID; - cfg.gas_params = embedder_gas_params(tx_token_cost); cfg.disable_eip7623 = disable_eip7623; cfg } @@ -286,7 +282,7 @@ mod tests { /// neither leg may silently reset a field to its revm default. #[test] fn test_create_evm_round_trips_embedder_cfg() { - let cfg = embedder_cfg(40, true); + let cfg = embedder_cfg(true); let db = MemoryDatabase::default(); let evm = MegaEvmFactory::new().create_evm(db, evm_env(cfg.clone())); @@ -295,7 +291,7 @@ mod tests { let read_back = evm.cfg_env(); assert_eq!(read_back.spec, MegaSpecId::REX6); assert_eq!(read_back.chain_id, CHAIN_ID); - assert_eq!(read_back.gas_params, cfg.gas_params, "custom gas schedule must survive"); + assert_eq!(read_back.gas_params, cfg.gas_params, "the spec's gas schedule must survive"); assert!(read_back.disable_eip7623, "revm 40 switches must survive"); // And through `finish`, which hands the config back to the embedder. @@ -306,6 +302,19 @@ mod tests { assert!(evm_env.cfg_env.disable_eip7623); } + /// The gas schedule is the one `CfgEnv` field the factory does not accept from an embedder. + /// It belongs to the spec, and `create_evm` — the production entry point an embedder's + /// `EvmEnv` arrives through — rejects a rewritten one rather than building an EVM that would + /// charge one schedule and account for another. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_create_evm_rejects_a_gas_schedule_off_the_spec_table() { + let mut cfg = embedder_cfg(true); + cfg.gas_params = embedder_gas_params(CUSTOM_TX_TOKEN_COST); + + let _ = MegaEvmFactory::new().create_evm(MemoryDatabase::default(), evm_env(cfg)); + } + /// The production path — `create_evm` routing the embedder's `EvmEnv` through `with_cfg` — /// pins the chain-id gate off however the config arrives: revm 40 defaults the flag to /// `true`, and every frozen `MegaETH` spec ran without the gate. @@ -323,34 +332,13 @@ mod tests { ); } - /// Carrying the config is not enough — it must also drive execution. A custom per-token - /// calldata cost moves the same transaction's gas by exactly its per-token delta. - #[test] - fn test_embedder_gas_schedule_takes_effect_on_gas() { - let mainnet_schedule = - run_calldata_tx(embedder_cfg(MAINNET_TX_TOKEN_COST, true), UNFLOORED_CALLDATA_TOKENS); - let custom_schedule = - run_calldata_tx(embedder_cfg(CUSTOM_TX_TOKEN_COST, true), UNFLOORED_CALLDATA_TOKENS); - - assert_eq!( - custom_schedule - mainnet_schedule, - (CUSTOM_TX_TOKEN_COST - MAINNET_TX_TOKEN_COST) * UNFLOORED_CALLDATA_TOKENS, - "the embedder's per-token calldata cost must price the transaction" - ); - } - - /// Same for the `disable_eip7623` switch: with enough calldata for the floor to bind, turning + /// Carrying the config is not enough — it must also drive execution. The `disable_eip7623` + /// switch is one an embedder does own: with enough calldata for the floor to bind, turning /// EIP-7623 off removes exactly revm's floor cost from the transaction's gas. #[test] fn test_embedder_eip7623_switch_takes_effect_on_gas() { - let with_eip7623 = run_calldata_tx( - embedder_cfg(MAINNET_TX_TOKEN_COST, false), - FLOOR_BINDING_CALLDATA_TOKENS, - ); - let without_eip7623 = run_calldata_tx( - embedder_cfg(MAINNET_TX_TOKEN_COST, true), - FLOOR_BINDING_CALLDATA_TOKENS, - ); + let with_eip7623 = run_calldata_tx(embedder_cfg(false), FLOOR_BINDING_CALLDATA_TOKENS); + let without_eip7623 = run_calldata_tx(embedder_cfg(true), FLOOR_BINDING_CALLDATA_TOKENS); assert_eq!( with_eip7623 - without_eip7623, @@ -361,19 +349,29 @@ mod tests { } /// A config whose gas schedule prices state gas: the mainnet `PRAGUE` table with Amsterdam's - /// charge for setting a fresh storage slot dropped in. An embedder can install exactly this, - /// which is what leaves EIP-8037 one flag away from repricing a frozen spec. + /// charge for setting a fresh storage slot dropped in. This is the shape the schedule pin + /// exists to turn away — a schedule under which EIP-8037 would have something to move. fn state_gas_priced_cfg() -> CfgEnv { let amsterdam_sstore_set_state_gas = GasParams::new_spec(SpecId::AMSTERDAM).get(GasId::sstore_set_state_gas()); assert_ne!(amsterdam_sstore_set_state_gas, 0, "state gas must be priced for this probe"); - let mut cfg = embedder_cfg(MAINNET_TX_TOKEN_COST, true); + let mut cfg = embedder_cfg(true); cfg.gas_params .override_gas([(GasId::sstore_set_state_gas(), amsterdam_sstore_set_state_gas)]); cfg } + /// The entries EIP-8037 splits a charge into. All of them are zero on every schedule + /// `MegaSpecId` defines, which is what makes the split inert once the schedule is pinned. + const STATE_GAS_IDS: [fn() -> GasId; 5] = [ + GasId::sstore_set_state_gas, + GasId::new_account_state_gas, + GasId::code_deposit_state_gas, + GasId::create_state_gas, + GasId::tx_eip7702_state_gas_bytecode, + ]; + /// Executes one transaction into [`SSTORE_FRESH_SLOT_CODE`] planted at [`TARGET`] and returns /// its gas used. fn run_sstore_tx(cfg: CfgEnv) -> u64 { @@ -407,19 +405,18 @@ mod tests { result.result.tx_gas_used() } - /// EIP-8037 is the one `CfgEnv` field an embedder does not own. `MegaETH`'s gas accounting + /// EIP-8037 is another `CfgEnv` field an embedder does not own. `MegaETH`'s gas accounting /// assumes no state-gas split exists, so the flag is forced off before the EVM is built and /// again before every transaction, reads back off, and setting it changes nothing about what a /// transaction costs. /// - /// The probe is a fresh-slot `SSTORE` under a schedule that prices state gas — - /// `state_gas_priced_cfg` asserts the Amsterdam charge it installs is non-zero, so a live - /// split would land on this transaction. There is deliberately no "forced past the pin" - /// control any more: the force now happens inside the transaction, after any window a test - /// could write the flag in, which is the property being asserted. + /// The probe is a fresh-slot `SSTORE`, the operation the split would reprice first. It runs + /// on the spec's schedule because that is the only schedule a transaction can run on — see + /// `test_a_state_gas_priced_schedule_is_rejected` for the schedule that would have given the + /// split something to move, and why it never reaches execution. #[test] fn test_embedder_cannot_enable_amsterdam_eip8037() { - let mut cfg = state_gas_priced_cfg(); + let mut cfg = embedder_cfg(true); cfg.enable_amsterdam_eip8037 = true; let evm = MegaEvmFactory::new().create_evm(MemoryDatabase::default(), evm_env(cfg.clone())); @@ -440,20 +437,35 @@ mod tests { ); // And execution never enters state-gas accounting: a fresh-slot `SSTORE` costs the same - // whether or not the embedder asked for EIP-8037, even on a schedule that prices state - // gas. + // whether or not the embedder asked for EIP-8037. assert_eq!( run_sstore_tx(cfg), - run_sstore_tx(state_gas_priced_cfg()), + run_sstore_tx(embedder_cfg(true)), "an embedder's EIP-8037 request must not reprice a fresh-slot SSTORE" ); + } - // And the state-gas price in the schedule is inert on its own: installing it changes - // nothing either, so no part of execution reads the state-gas table. - assert_eq!( - run_sstore_tx(state_gas_priced_cfg()), - run_sstore_tx(embedder_cfg(MAINNET_TX_TOKEN_COST, true)), - "a schedule that prices state gas must not reprice a transaction while the split is off" - ); + /// The second half of the EIP-8037 guarantee, now carried by the schedule pin: the split has + /// nothing to move. Every state-gas entry is zero on every schedule `MegaSpecId` defines, and + /// a schedule that priced one is rejected before it can run a transaction — so the flag being + /// forced off is a second lock on a door the schedule already closed, not the only one. + #[test] + fn test_state_gas_is_unpriced_on_every_spec_schedule() { + for spec in [MegaSpecId::EQUIVALENCE, MegaSpecId::REX5, MegaSpecId::REX6, MegaSpecId::REX7] + { + let schedule = GasParams::new_spec(SpecId::from(spec)); + for id in STATE_GAS_IDS { + assert_eq!(schedule.get(id()), 0, "{:?} on {spec:?}", id().name()); + } + } + } + + /// A schedule that prices state gas is a schedule off the spec table, and is turned away at + /// the factory like any other. + #[test] + #[should_panic(expected = "gas params differ from the spec-defined schedule")] + fn test_a_state_gas_priced_schedule_is_rejected() { + let _ = MegaEvmFactory::new() + .create_evm(MemoryDatabase::default(), evm_env(state_gas_priced_cfg())); } } diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index eef74ee4..eb87b85a 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -1042,6 +1042,8 @@ macro_rules! record_storage_compute_gas { matches!(call_inputs.scheme, CallScheme::Call | CallScheme::CallCode) && call_inputs.transfers_value() { + // The constant is what revm minted: the active gas schedule is required to be + // the one the spec defines, so its `call_stipend` entry is this value. gas::CALL_STIPEND } else { 0 @@ -1424,7 +1426,10 @@ pub mod forward_gas_ext { // We recover the forwarded gas to the child call from the parent call. let child_gas = call_inputs.gas_limit as u128; - // There may be a call stipend if there is value to be transferred. + // There may be a call stipend if there is value to be transferred. The + // constant is what revm added to `gas_limit`: the active gas schedule is + // required to be the one the spec defines, so its `call_stipend` entry is + // this value and the subtraction below cannot go negative. let transfer_gas_stipend = if has_transfer { gas::CALL_STIPEND as u128 } else { 0 }; let forwarded_gas = child_gas - transfer_gas_stipend; // Safe from underflow diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs index 5bc9865d..8e11e3a7 100644 --- a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -17,14 +17,22 @@ //! //! These tests pin the four rows of that decision, the frozen REX4-REX6 shapes they must not //! disturb, and the journal consistency that the early decision exists for in the first place. +//! +//! One shape that used to be pinned here is gone: a knife-edge CREATE holding exactly revm's +//! built-in per-byte charge under a schedule that charged more, which separated a predicate +//! reading the active schedule from one reading the constant. That fork needed a configuration +//! carrying a gas schedule other than its spec's, and such a configuration is now rejected at +//! every entry point into the EVM rather than run, so the two readings can no longer disagree on +//! any input. What survives here is the pair that is still decidable: installing the built-in +//! rate explicitly changes nothing, and installing anything else is turned away. use crate::common::{default_envs, finish, transact_tx, Outcome, CALLER, ONE_ETH}; use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_sol_types::SolError as _; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - EthHaltReason, EvmTxRuntimeLimits, LimitKind, MegaContext, MegaEvm, MegaHaltReason, - MegaLimitExceeded, MegaSpecId, MegaTransaction, MegaTransactionNew as _, OpHaltReason, + EvmTxRuntimeLimits, LimitKind, MegaContext, MegaEvm, MegaHaltReason, MegaLimitExceeded, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, }; use revm::{ bytecode::opcode::{MSTORE, POP, RETURN, TIMESTAMP}, @@ -425,20 +433,17 @@ fn test_frozen_specs_keep_their_create_journal_shapes() { } } -/// The per-byte code-deposit rate the override cases install, one gas above revm's built-in -/// [`CODEDEPOSIT`]. Any value that differs would do; one gas apart keeps the arithmetic below -/// legible and makes the gap between the two readings exactly [`RUNTIME_LEN`]. +/// A per-byte code-deposit rate one gas above revm's built-in [`CODEDEPOSIT`] — the smallest +/// deviation from the schedule `REX7` defines, and one no configuration may carry. const OVERRIDDEN_CODEDEPOSIT: u64 = CODEDEPOSIT + 1; -/// The code-deposit charge for [`RUNTIME_LEN`] bytes under the overridden schedule. -const OVERRIDDEN_CODE_DEPOSIT_GAS: u64 = RUNTIME_LEN * OVERRIDDEN_CODEDEPOSIT; - /// Runs [`return_zeros_initcode`] as a REX7 creation transaction with `gas_limit`, under a /// configuration whose gas schedule charges `rate` gas per deployed byte. /// -/// An embedder-installed gas schedule is a supported configuration, and revm's create-return reads -/// the code-deposit rate off it. The shared helpers all run the default schedule, so this builds -/// the context itself — everything else about the transaction matches [`create`]. +/// The shared helpers all take the context's default configuration, so this builds the context +/// itself — everything else about the transaction matches [`create`]. A `rate` other than the +/// one `REX7`'s schedule defines makes the configuration inadmissible, and `with_cfg` panics +/// before any transaction runs. fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(10 * ONE_ETH)); let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); @@ -482,132 +487,37 @@ fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { ) } -/// The charge REX7 records is the one the active gas schedule defines, not revm's built-in rate. -/// -/// revm's create-return debits `gas_params().code_deposit_cost(len)`, and an embedder may have -/// installed a schedule where that is not `len * CODEDEPOSIT`. Reading the constant instead would -/// record a charge that differs from the one debited on every successful CREATE — a standing break -/// of the conservation law the reported compute total is derived from, which is what -/// [`crate::common::finish`] checks on every transaction these helpers run. +/// Installing a rate explicitly is not itself a deviation: a schedule that names revm's built-in +/// per-byte rate is the schedule `REX7` defines, is admitted, and measures exactly what the +/// shared helper measures on the default configuration. #[test] -fn test_create_code_deposit_charge_follows_the_active_gas_schedule() { - let default_rate = create_at_rate(CODEDEPOSIT, TX_GAS_LIMIT); - assert!( - default_rate.is_success(), - "the default-schedule run must deploy: {:?}", - default_rate.result, - ); - assert_eq!( - default_rate.compute_gas, - calibrate(MegaSpecId::REX7).compute_gas, - "installing the built-in rate explicitly must not change what the shared helper measures", - ); +fn test_installing_the_built_in_code_deposit_rate_changes_nothing() { + let explicit = create_at_rate(CODEDEPOSIT, TX_GAS_LIMIT); - let overridden = create_at_rate(OVERRIDDEN_CODEDEPOSIT, TX_GAS_LIMIT); - assert!( - overridden.is_success(), - "the overridden-schedule run must deploy too: {:?}", - overridden.result, - ); + assert!(explicit.is_success(), "the explicit-rate run must deploy: {:?}", explicit.result); assert!( - has_deployed_code(&overridden.state, deployed_address()), - "the overridden-schedule run must leave code behind", - ); - - // The whole difference between the two runs is the deposit charge, and it moved by exactly the - // rate difference — so the recorded amount tracks the schedule rather than the constant. - assert_eq!( - overridden.compute_gas, - default_rate.compute_gas + OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS, - "the recorded charge must follow the schedule (default={}, overridden={})", - default_rate.compute_gas, - overridden.compute_gas, + has_deployed_code(&explicit.state, deployed_address()), + "the explicit-rate run must leave code behind", ); assert_eq!( - overridden.gas_used, - default_rate.gas_used + OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS, - "the receipt moved by the same amount, which is what the recorded charge has to match", + explicit.compute_gas, + calibrate(MegaSpecId::REX7).compute_gas, + "naming the built-in rate must not change what the shared helper measures", ); - - // Nothing was destroyed, so the whole reported total enforces — and the terminal identity - // already checked that it accounts for the receipt envelope. - assert_eq!(overridden.destroyed, 0, "a successful CREATE destroys nothing"); + assert_eq!(explicit.destroyed, 0, "a successful CREATE destroys nothing"); assert_eq!( - overridden.enforced(), - overridden.compute_gas, + explicit.enforced(), + explicit.compute_gas, "the whole reported total enforces when nothing is destroyed", ); } -/// The knife edge the constant would fall off: a frame holding exactly the built-in rate's charge -/// under a schedule that charges more. -/// -/// `return_create` weighs the deposit against the schedule and takes the frame out of gas. A -/// predicate reading the constant would answer "affordable", record a charge nobody is ever -/// debited, and leave the deposit rejected anyway — the phantom accounting this decision exists to -/// prevent. Reading the schedule answers "unaffordable": nothing is recorded, and the remainder the -/// rejected frame never spends is settled as destroyed like any other exceptional halt's. +/// A schedule that charges a different per-byte rate never runs. `MegaETH`'s gas schedule is +/// defined by the spec, and a configuration carrying any other one is rejected where it enters +/// rather than executed — which is what keeps the charge revm debits at the create-return equal +/// to the one the tracker weighed and recorded a step earlier. #[test] -fn test_create_code_deposit_knife_edge_under_an_overridden_schedule() { - // A gas limit equal to what the unconstrained run spends leaves the frame exactly the - // schedule's charge at the deposit; one deployed byte's worth less leaves it exactly the - // built-in rate's charge, which is the point being tested. - let unconstrained = create_at_rate(OVERRIDDEN_CODEDEPOSIT, TX_GAS_LIMIT); - assert!(unconstrained.is_success(), "{:?}", unconstrained.result); - let exactly_affordable_gas_limit = unconstrained.total_gas_spent; - - let exact = create_at_rate(OVERRIDDEN_CODEDEPOSIT, exactly_affordable_gas_limit); - assert!( - exact.is_success(), - "a frame holding exactly the schedule's charge must deposit: {:?}", - exact.result, - ); - assert!( - has_deployed_code(&exact.state, deployed_address()), - "the exactly-affordable CREATE must leave code behind", - ); - assert_eq!( - exact.compute_gas, unconstrained.compute_gas, - "the gas limit is not part of the work; only the room left over changed", - ); - - let knife = create_at_rate( - OVERRIDDEN_CODEDEPOSIT, - exactly_affordable_gas_limit - (OVERRIDDEN_CODE_DEPOSIT_GAS - CODE_DEPOSIT_GAS), - ); - assert!( - matches!( - knife.result, - ExecutionResult::Halt { - reason: MegaHaltReason::Base(OpHaltReason::Base(EthHaltReason::OutOfGas(_))), - .. - } - ), - "a frame holding only the built-in rate's charge must be taken out of gas by the \ - create-return: {:?}", - knife.result, - ); - assert!( - !has_deployed_code(&knife.state, deployed_address()), - "the rejected CREATE must leave no code behind", - ); - - // The deposit was not recorded: what the constant would have called affordable is exactly what - // the halted frame destroyed instead. - assert_eq!( - knife.destroyed, CODE_DEPOSIT_GAS, - "the frame's whole remainder — the built-in rate's charge — must settle as destroyed", - ); - assert_eq!( - knife.booked_destroyed, knife.destroyed, - "the per-site booking must agree with the derived destroyed total", - ); - assert_eq!( - knife.enforced() + OVERRIDDEN_CODE_DEPOSIT_GAS, - exact.compute_gas, - "the enforced lane must hold the frame's work and none of the deposit (enforced={}, \ - successful total={})", - knife.enforced(), - exact.compute_gas, - ); +#[should_panic(expected = "gas params differ from the spec-defined schedule")] +fn test_a_code_deposit_rate_off_the_spec_schedule_is_rejected() { + let _ = create_at_rate(OVERRIDDEN_CODEDEPOSIT, TX_GAS_LIMIT); } From f21d825a4e38887eb4afc7c8998b02ce5889e8e0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 20 Aug 2026 16:37:38 +0800 Subject: [PATCH 079/208] feat: reject a context whose two spec fields disagree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MegaContext` carries its spec twice — the `MegaSpecId` that selects the instruction table, precompiles and resource-limit trackers when the EVM is built, and the `OpSpecId` in `CfgEnv` that revm's own gating reads while a transaction runs. Rewriting `cfg.spec` on a live context through the mutable deref leaves the two naming different forks, so one transaction executes under two specs at once. Rewriting the gas schedule along with it keeps the schedule pin satisfied, so that check alone does not catch the shape. Check the two against each other in `on_new_tx`, ahead of the schedule pin. --- crates/mega-evm/src/evm/context.rs | 132 +++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/crates/mega-evm/src/evm/context.rs b/crates/mega-evm/src/evm/context.rs index cd0447a6..792522a6 100644 --- a/crates/mega-evm/src/evm/context.rs +++ b/crates/mega-evm/src/evm/context.rs @@ -754,6 +754,7 @@ impl MegaContext { /// /// DB-dependent pre-frame usage may still be recorded later during pre-execution. pub(crate) fn on_new_tx(&mut self) { + assert_cfg_spec_matches_context_spec(self.spec, self.inner.cfg.spec); assert_spec_owned_gas_schedule(&self.inner.cfg); force_amsterdam_eip8037_off(&mut self.inner.cfg); @@ -978,6 +979,56 @@ fn panic_gas_schedule_mismatch( ) } +/// Panics unless `cfg_spec` is the op-spec `spec` maps to. +/// +/// A [`MegaContext`] carries its spec twice: as the [`MegaSpecId`] on the context, and as the +/// [`OpSpecId`] that spec maps to inside `CfgEnv`. Execution reads both, from different halves. +/// The `MegaSpecId` selects the instruction table, the precompile set and the +/// [`AdditionalLimit`](crate::AdditionalLimit) trackers, all baked when the EVM is built from the +/// context; `CfgEnv::spec` is what revm's own spec gating reads while a transaction runs. The two +/// must name the same fork, and every supported way of setting a spec writes both from a single +/// value — the constructors derive the configuration from the `MegaSpecId` they are given, and +/// [`MegaContext::apply_cfg`] takes the context's `MegaSpecId` from the configuration it adopts. +/// +/// They come apart only through the mutable deref to the inner context (`ctx.modify_cfg`, or a +/// `&mut CfgEnv` taken directly), which reaches the configuration without passing through either. +/// Rewriting `cfg.spec` there leaves the baked halves on the context's `MegaSpecId` while revm +/// prices the transaction under the written one — one transaction executing under two forks at +/// once, with `MegaETH`'s wrappers, precompiles and resource limits taken from a fork revm is not +/// pricing. Writing the schedule along with the spec (`set_spec_and_mainnet_gas_params`) leaves +/// [`assert_spec_owned_gas_schedule`] satisfied, because the schedule then does match the spec +/// that was written, so that check alone does not catch this. +/// +/// Called from [`MegaContext::on_new_tx`], and checked there rather than at each entry point for +/// the same reason [`force_amsterdam_eip8037_off`] is applied there: the point of use covers a +/// configuration mutated in place after the context was built, which a per-entry-point check +/// cannot. It runs ahead of [`assert_spec_owned_gas_schedule`] so that a bare `cfg.spec` write — +/// which both checks reject — is reported as the desync it is, rather than as a schedule that +/// could be repaired by installing the written spec's table. +pub(crate) fn assert_cfg_spec_matches_context_spec(spec: MegaSpecId, cfg_spec: OpSpecId) { + if cfg_spec != spec.into_op_spec() { + panic_context_spec_mismatch(spec, cfg_spec); + } +} + +/// Reports a context whose two spec fields name different forks, and panics. +/// +/// Split out of [`assert_cfg_spec_matches_context_spec`] and marked cold for the same reason +/// [`panic_gas_schedule_mismatch`] is: the formatting stays out of the caller's inlined fast path. +#[cold] +#[inline(never)] +fn panic_context_spec_mismatch(spec: MegaSpecId, cfg_spec: OpSpecId) -> ! { + panic!( + "the configuration's spec is {cfg_spec:?}, but this context executes {spec:?}, whose \ + op-spec is {:?}. MegaETH's spec is not a `CfgEnv` field a caller can rewrite on a live \ + context: it also selects the instruction table, the precompiles and the resource-limit \ + trackers, which are baked when the EVM is built, so a rewritten `CfgEnv::spec` would \ + run one transaction under two specs at once. Change the spec by adopting a whole \ + configuration through `MegaContext::with_cfg` (or `with_cfg_unpinned`), which sets both.", + spec.into_op_spec(), + ) +} + /// A convenient trait to convert a `CfgEnv` into a `CfgEnv`. /// /// This trait provides a conversion method for `OpStack` configuration environments @@ -1304,6 +1355,87 @@ mod tests { context.on_new_tx(); } + /// The spec is pinned the same way the schedule is, and for a shape the schedule pin cannot + /// see: `cfg.spec` and `cfg.gas_params` rewritten together on a live context are + /// self-consistent, so the schedule matches the spec that was written. What no longer matches + /// is the context — the instruction table, the precompiles and the resource-limit trackers + /// stay on the `MegaSpecId` the EVM was built from, so the transaction would run under two + /// specs at once. + #[test] + #[should_panic(expected = "the configuration's spec is BEDROCK, but this context executes")] + fn test_on_new_tx_rejects_a_spec_and_schedule_mutated_together() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7); + context.modify_cfg(|cfg| cfg.set_spec_and_mainnet_gas_params(OpSpecId::BEDROCK)); + // The pair is self-consistent: the schedule pin, on its own, admits this configuration. + assert_eq!( + context.inner.cfg.gas_params, + GasParams::new_spec(SpecId::from(OpSpecId::BEDROCK)), + ); + + context.on_new_tx(); + } + + /// A bare `cfg.spec` write desyncs the context the same way, and is rejected by the spec pin + /// rather than by the schedule pin it also trips: the schedule is a consequence here, and its + /// message would send an embedder to `set_spec_and_mainnet_gas_params`, which repairs the + /// schedule and leaves the desync — the shape the test above covers. + #[test] + #[should_panic(expected = "the configuration's spec is BEDROCK, but this context executes")] + fn test_on_new_tx_rejects_a_bare_spec_write() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7); + context.modify_cfg(|cfg| cfg.spec = OpSpecId::BEDROCK); + + context.on_new_tx(); + } + + /// The rejection names both specs and the entry point that sets them together, so an embedder + /// that hits it can act on the message. + #[test] + #[should_panic( + expected = "this context executes REX7, whose op-spec is ISTHMUS. MegaETH's spec is not" + )] + fn test_spec_rejection_names_both_specs_and_the_supported_entry_point() { + let mut context = MegaContext::new(EmptyDB::default(), MegaSpecId::REX7); + context.modify_cfg(|cfg| cfg.set_spec_and_mainnet_gas_params(OpSpecId::BEDROCK)); + + context.on_new_tx(); + } + + /// The spec pin rejects desync, not any particular spec: every spec, reached by every + /// supported path — the constructors, both `with_cfg` entry points, and the deprecated + /// `new_with_context` — passes it at the point of use. + #[allow(deprecated)] + #[test] + fn test_every_spec_passes_the_spec_pin_on_every_construction_path() { + for spec in ALL_SPECS { + MegaContext::new(EmptyDB::default(), spec).on_new_tx(); + + MegaContext::<_, EmptyExternalEnv>::new_with_shared_ext_envs( + EmptyDB::default(), + spec, + Rc::new(EmptyExternalEnv), + Rc::new(RefCell::new(EmptyExternalEnv)), + ) + .on_new_tx(); + + MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE) + .with_cfg(spec_cfg(spec)) + .on_new_tx(); + + MegaContext::new(EmptyDB::default(), MegaSpecId::EQUIVALENCE) + .with_cfg_unpinned(spec_cfg(spec)) + .on_new_tx(); + + // The deprecated constructor's input is a configuration on revm's default op-spec, + // which it relabels — so this also covers the relabel leaving the two in sync. + let inner: MegaInnerContext = revm::Context::op() + .with_tx(crate::MegaTransaction::default()) + .with_db(EmptyDB::default()); + MegaContext::new_with_context(inner, spec, ExternalEnvs::::default()) + .on_new_tx(); + } + } + /// The pin is unconditional: a configured cfg — here a blob schedule plus an explicitly /// enabled check — still comes out with the gate off. `with_cfg` is the compatibility entry /// point; enabling the gate requires `with_cfg_unpinned`. From 456a115a7668117ab329adb9c7168fed128275c4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 20 Aug 2026 16:37:44 +0800 Subject: [PATCH 080/208] docs: gate the Rex7 code-deposit recording rules behind `

` The compute-gas page declares `spec: Rex6`, and unstable-spec behavior belongs in a `
` block rather than in main prose and tables. Move the Rex7 table row and the Rex7 bullet of the code-deposit rules into one, and record the change in the page's Rex7 spec-history entry. --- docs/spec/evm/compute-gas.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 7a86752d..d19b8d79 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -334,21 +334,13 @@ These conditions apply on every spec; only the point at which the recording happ | Spec | Recording point | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Rex7+ | Conditional on the deposit: evaluated once the frame's own accounting is complete, and recorded only if the amount fits the budgets it is weighed against. | | Rex5–Rex6 | Atomically with the deployment commit: recorded when the deployment's pre-commit success conditions hold, at the same point the EVM charges the code-deposit gas and commits the created contract. | | MiniRex–Rex4 | During frame-return processing, in the window covering the EVM's code-deposit charge. | A node MUST NOT record this amount twice. -The recording interacts with the compute-gas limit, and the three recording points then produce different outcomes: +The recording interacts with the compute-gas limit, and the two recording points then produce different outcomes: -- From Rex7, the amount is weighed before it is recorded. - A node MUST evaluate the frame-local and transaction-level compute budgets against the frame's usage plus this amount, and MUST NOT record the amount when either would be exceeded. - That evaluation MUST happen once the frame's own accounting for the exit is complete — its final segment settled and its frame-exit resource usage merged — so the amount is weighed against the frame's whole usage rather than a total still missing part of it. - A frame that failed on any dimension before this point never reaches the evaluation: the EVM does not charge a deposit such a frame will not make, so there is nothing to record. - When the amount does not fit, the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing — the same outcome Rex5 and Rex6 produce — but the transaction's compute total reports only what it spent. - A frame-local exceed on this path MUST NOT be latched: with the amount unrecorded the transaction is within every limit, and the frames above it MAY continue. - A transaction-level exceed MUST be latched and MUST halt the transaction with the usual gas rescue, and MUST carry the same detention attribution it would have carried had the amount been recorded. - Under Rex5 and Rex6, the recording precedes the commit: the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing, but the recorded amount stands — recording precedes exceed evaluation, and compute gas is never reverted. The amount stands on every path that fails the frame after it, not only on a compute-gas exceed. - Under Rex4, the only earlier spec with a per-frame budget, the recording happens after the EVM has already charged the deposit and committed the created contract. @@ -357,6 +349,21 @@ The recording interacts with the compute-gas limit, and the three recording poin The code-deposit _storage_ gas is charged before this window opens and therefore falls outside it, consistent with the [storage gas exclusion](#storage-gas-exclusion). +
+Rex7 (unstable): the code-deposit amount is weighed before it is recorded + +Rex7 replaces the Rex5 recording point with a conditional one: the amount is evaluated once the frame's own accounting for the exit is complete, and recorded only if it fits the budgets it is weighed against. +The full previous/new pairing is on the [Rex7 Network Upgrade](../upgrades/rex7.md) page; the normative rules for implementers follow. + +A node MUST evaluate the frame-local and transaction-level compute budgets against the frame's usage plus this amount, and MUST NOT record the amount when either would be exceeded. +That evaluation MUST happen once the frame's own accounting for the exit is complete — its final segment settled and its frame-exit resource usage merged — so the amount is weighed against the frame's whole usage rather than a total still missing part of it. +A frame that failed on any dimension before this point never reaches the evaluation: the EVM does not charge a deposit such a frame will not make, so there is nothing to record. +When the amount does not fit, the frame fails as specified in [Exceed Behavior](#exceed-behavior) and the deployment commits nothing — the same outcome Rex5 and Rex6 produce — but the transaction's compute total reports only what it spent. +A frame-local exceed on this path MUST NOT be latched: with the amount unrecorded the transaction is within every limit, and the frames above it MAY continue. +A transaction-level exceed MUST be latched and MUST halt the transaction with the usual gas rescue, and MUST carry the same detention attribution it would have carried had the amount been recorded. + +
+ #### Keyless Deploy Sandbox From Rex3 onward, a node MUST record the [KeylessDeploy](../system-contracts/keyless-deploy.md) fixed dispatch overhead (`KEYLESS_DEPLOY_OVERHEAD_GAS`) as compute gas when that overhead is charged. @@ -749,4 +756,4 @@ System-granted gas leaks to the sender, who recovers gas that was never theirs t - [Rex4](../upgrades/rex4.md) — introduced the per-call-frame compute gas budget; made gas detention caps relative to usage at the access point; added beneficiary volatile-access guards to the `CALL` family, `SELFDESTRUCT`, and `SELFBALANCE`. - [Rex5](../upgrades/rex5.md) — excluded the `CALL_STIPEND` from the forwarded-gas deduction; moved `CREATE2` memory-expansion recording ahead of the storage-gas charge; made contract-creation code-deposit compute gas atomic with the deployment commit; refined precompile compute-gas recording and bounded it by the remaining compute budget; added the `SELFDESTRUCT` empty-beneficiary storage-gas charge; removed `CALLCODE` from the cold first-touch charge and added `SELFDESTRUCT`'s beneficiary to it; stopped following EIP-7702 delegation in the pre-execution inspection, restoring inherited warmth for delegates. - [Rex6](../upgrades/rex6.md) — unified the measurement window across all storage-affecting opcodes and folded `CREATE2` memory expansion into it, ending the two-window exception; returned forwarded gas to the failing frame on a compute-gas exceed; rescued the unused envelope on a keyless-deploy dispatch exceed; made beneficiary detection delegation-aware, returning `CALLCODE` call targets to the cold first-touch charge; exempted system-originated transactions from the compute gas limit and gas detention. -- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit; splits a failing precompile the same way at the recording site. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit; splits a failing precompile the same way at the recording site; weighs a contract creation's code-deposit compute gas against the compute budgets before recording it, rather than recording it ahead of the evaluation. From 452997cc25ed1353d3f2cbc776d8be40ea884203 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 28 Aug 2026 09:23:31 +0800 Subject: [PATCH 081/208] fix(rex7): book the envelope a refused frame init destroys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A frame init that hands back a result instead of a frame carries the whole child budget as remaining gas, and the classification decides its fate: a success or revert is erased back into the caller's counter, an exceptional halt is not. The child never runs, so the frame-exit settlement never sees the halting shapes and nothing booked them — a CREATE onto an occupied address left its swallowed budget out of the reported compute total and out of block-level compute accounting, and tripped the conservation cross-check in debug builds. Settle it in after_frame_init, on the halt classification only. Precompile results are excluded: they arrive through the same arm but have already booked both halves at their own recording site. The settlement runs after the gas rescue so it reads a refreshed latch — a latched exceed means the envelope is rescued or reverted back to the caller, not destroyed. --- AGENTS.md | 1 + crates/mega-evm/src/limit/limit.rs | 46 ++ .../tests/rex7/frame_init_reject_burn.rs | 685 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 5 + docs/spec/evm/compute-gas.md | 6 + docs/spec/upgrades/rex7.md | 5 + 6 files changed, 748 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/frame_init_reject_burn.rs diff --git a/AGENTS.md b/AGENTS.md index 485ae4cc..69bf3e35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. + A frame init that refuses to build a frame at all is settled in `after_frame_init`, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 826a8e63..ac8774ae 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -1096,6 +1096,10 @@ impl AdditionalLimit { // Rescue gas if a TX-level limit was exceeded. This covers the // before_frame_init early-return path and any other Result from frame_init. self.try_rescue_gas(result.gas()); + // Must run after the rescue: the rescue's `check_limit` is what latches a TX-level or + // frame-local exceed, and the settlement below reads that latch to decide whether the + // envelope is really destroyed or is about to be handed back. + self.settle_frame_init_reject_burn(result); } } @@ -1427,6 +1431,48 @@ impl AdditionalLimit { self.compute_gas.record_burned_gas(result.gas().remaining()); } + /// Settles the envelope a frame that never started destroys, as non-enforcing compute gas + /// (REX7+). + /// + /// A frame init can refuse to build a frame and hand back a result instead. Such a result + /// carries the whole child budget as `remaining`, and what the caller does with it is decided + /// by the classification alone: a success or a revert is erased back into the caller's + /// counter, while an exceptional halt is not — the caller simply never sees that gas again. + /// The child never runs, so the frame-exit settlement that splits an ordinary exceptional halt + /// cannot see it either, and without this the destroyed budget would be missing from the + /// transaction's reported total while the conservation law still derives it from the envelope. + /// + /// Only the halt classification books anything. The success and revert shapes reaching this + /// site — an empty-code call, a nonce overflow, a depth or balance rejection — destroy nothing + /// precisely because their gas is erased back into the caller. + /// + /// A precompile result is excluded. Precompiles are dispatched inside the same frame init and + /// come back as a result rather than a frame, but they have already booked both halves of + /// their own split at the recording site, against the forwarded envelope rather than the + /// capped budget this result carries. Booking again here would report the same gas twice. + /// + /// Nothing is booked once a limit is latched, which is also why this runs after the rescue in + /// [`after_frame_init`](Self::after_frame_init) rather than before it. A TX-level exceed + /// rescues this same remaining gas for the sender and erases it from the envelope, and a + /// frame-local exceed is absorbed in + /// [`before_frame_return_result`](Self::before_frame_return_result), which rewrites the result + /// to a revert and so returns the gas to the caller. Either way the envelope is not destroyed, + /// and booking it would report gas that was handed back. + fn settle_frame_init_reject_burn(&mut self, result: &FrameResult) { + if !self.checkpoint.rex7_enabled() || + self.limit_exceeded() || + result.instruction_result().is_ok_or_revert() + { + return; + } + if let FrameResult::Call(outcome) = result { + if outcome.was_precompile_called { + return; + } + } + self.compute_gas.record_burned_gas(result.gas().remaining()); + } + /// Merges resource usage from a sandbox execution into this tracker. /// /// Used by `KeylessDeploy` (REX5+) to propagate sandbox resource consumption diff --git a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs new file mode 100644 index 00000000..bf451a95 --- /dev/null +++ b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs @@ -0,0 +1,685 @@ +//! A frame init that refuses to build a frame still decides the fate of a whole child budget. +//! +//! `frame_init` hands back a result instead of a frame on several shapes, and the result carries +//! the entire child budget as `remaining`. What happens to that budget is decided by the result's +//! classification alone: a success or a revert is erased back into the caller's gas counter, an +//! exceptional halt is not. The child never runs, so the frame-exit settlement that splits an +//! ordinary exceptional halt never sees the halting shapes — REX7 books them here instead. +//! +//! The classification is what separates the rows, not the reason: +//! +//! | shape | result | class | destroyed | +//! | --------------------------------- | ---------------- | ------ | --------- | +//! | CREATE past the call-stack limit | `CallTooDeep` | revert | 0 | +//! | CREATE whose value exceeds balance| `OutOfFunds` | revert | 0 | +//! | CREATE from a `u64::MAX` nonce | `Return` | ok | 0 | +//! | CREATE onto an occupied address | `CreateCollision`| halt | whole | +//! | CALL into an account with no code | `Stop` | ok | 0 | +//! | CALL past the call-stack limit | `CallTooDeep` | revert | 0 | +//! | CALL that dispatches a precompile | precompile's own | either | booked at the precompile site | +//! +//! The precompile row is the one that has to be excluded rather than classified. A precompile is +//! dispatched inside the same frame init and comes back as a result too, but it has already booked +//! both halves of its own split — against the forwarded envelope rather than the capped budget the +//! result carries — so booking it again here would report the same gas twice. +//! +//! Pre-REX7 specs have no destroyed lane, so every row books nothing and the receipts are +//! unchanged. +//! +//! The halting row is then followed through the two boundaries that run after the booking: the +//! failed-deposit receipt rewrite, which settles again against a larger envelope, and the +//! `KeylessDeploy` sandbox merge, which carries a nested execution's split into its parent. + +use crate::common::{ + default_envs, transact, transact_default, transact_mega_tx, transact_tx, CALLER, CONTRACT, + ONE_ETH, +}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + constants::rex::TX_INTRINSIC_STORAGE_GAS, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{CREATE, CREATE2, POP, STOP}, + context::tx::TxEnvBuilder, + handler::{EvmTr, ItemOrResult}, + interpreter::{ + interpreter::SharedMemory, interpreter_action::FrameInit, CallInput, CallInputs, + CallScheme, CallValue, CreateInputs, CreateScheme, FrameInput, InstructionResult, + }, + primitives::CALL_STACK_LIMIT, +}; + +/// The budget every synthetic frame init below forwards to the child it asks for. +const FRAME_GAS: u64 = 100_000; + +/// An address seeded with code, so a CREATE aimed at it collides and a CALL into it is not the +/// empty-code shape. +const OCCUPIED: Address = address!("0000000000000000000000000000000000310001"); + +/// An address with no code and no nonce, so a CALL into it returns `Stop` without a frame. +const VACANT: Address = address!("0000000000000000000000000000000000310002"); + +/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything — a +/// precompile halt with nothing performed. +const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); + +/// The two specs every row is run under: the one with the destroyed lane, and the frozen one +/// directly beneath it. +const SPECS: [MegaSpecId; 2] = [MegaSpecId::REX6, MegaSpecId::REX7]; + +/* ------------------------------------------------------------------------------------------- * + * The state table, driven at the `frame_init` boundary. + * ------------------------------------------------------------------------------------------- */ + +/// A `frame_init` that asks for a CREATE child. +fn create_frame_init(value: U256, depth: usize) -> FrameInit { + FrameInit { + depth, + memory: SharedMemory::new(), + frame_input: FrameInput::Create(Box::new(CreateInputs::new( + CALLER, + CreateScheme::Create, + value, + Bytes::new(), + FRAME_GAS, + 0, + ))), + } +} + +/// A `frame_init` that asks for a CALL child. +fn call_frame_init(target: Address, input: Bytes, depth: usize) -> FrameInit { + FrameInit { + depth, + memory: SharedMemory::new(), + frame_input: FrameInput::Call(Box::new(CallInputs { + input: CallInput::Bytes(input), + return_memory_offset: 0..0, + gas_limit: FRAME_GAS, + bytecode_address: target, + target_address: target, + caller: CALLER, + // Apparent rather than Transfer: a zero-value transfer would still touch the target + // account, and these synthetic frame inits run against a journal that has loaded + // nothing. The rows under test are all decided before any value would move. + value: CallValue::Apparent(U256::ZERO), + scheme: CallScheme::Call, + is_static: false, + reservoir: 0, + known_bytecode: Default::default(), + charged_new_account_state_gas: false, + })), + } +} + +/// What one `frame_init` row produced: the classification it returned, the budget the result still +/// carries, and the destroyed total the tracker booked for it. +struct Row { + instruction_result: InstructionResult, + remaining: u64, + booked_destroyed: u64, +} + +/// Drives `frame_init` once against a fresh EVM and reads back the row. +fn run_frame_init(spec: MegaSpecId, mut db: MemoryDatabase, frame_init: FrameInit) -> Row { + let context = MegaContext::new(&mut db, spec); + let mut evm = MegaEvm::new(context); + let result = EvmTr::frame_init(&mut evm, frame_init).expect("frame_init must not error"); + let ItemOrResult::Result(frame_result) = result else { + panic!("{spec:?}: this shape must reject the frame, not build one"); + }; + let booked_destroyed = evm.ctx_ref().additional_limit.borrow().conservation_terms_for_test().2; + Row { + instruction_result: frame_result.instruction_result(), + remaining: frame_result.gas().remaining(), + booked_destroyed, + } +} + +/// The database every row starts from: a funded caller, an occupied address, and blake2f reachable +/// as a precompile. +fn row_db() -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(ONE_ETH)) + .account_code(OCCUPIED, BytecodeBuilder::default().append(STOP).build()) +} + +/// Every `frame_init` rejection classified as a success or a revert hands its budget back, so it +/// must book nothing — on both specs. +#[test] +fn test_returned_frame_init_rejections_book_nothing() { + let cases: Vec<(&str, MemoryDatabase, FrameInit, InstructionResult)> = vec![ + ( + "CREATE past the call-stack limit", + row_db(), + create_frame_init(U256::ZERO, CALL_STACK_LIMIT as usize + 1), + InstructionResult::CallTooDeep, + ), + ( + "CREATE whose value exceeds the caller's balance", + row_db(), + create_frame_init(U256::from(2 * ONE_ETH), 1), + InstructionResult::OutOfFunds, + ), + ( + "CREATE from a caller whose nonce cannot be bumped", + row_db().account_nonce(CALLER, u64::MAX), + create_frame_init(U256::ZERO, 1), + InstructionResult::Return, + ), + ( + "CALL into an account with no code", + row_db(), + call_frame_init(VACANT, Bytes::new(), 1), + InstructionResult::Stop, + ), + ( + "CALL past the call-stack limit", + row_db(), + // The REX5 depth guard covers `Call` / `StaticCall`; a `CallCode` reaches revm's own + // depth check, which is the arm under test here. + FrameInit { + depth: CALL_STACK_LIMIT as usize + 1, + memory: SharedMemory::new(), + frame_input: match call_frame_init( + OCCUPIED, + Bytes::new(), + CALL_STACK_LIMIT as usize + 1, + ) + .frame_input + { + FrameInput::Call(mut inputs) => { + inputs.scheme = CallScheme::CallCode; + FrameInput::Call(inputs) + } + other => other, + }, + }, + InstructionResult::CallTooDeep, + ), + ]; + + for (label, db, frame_init, expected) in cases { + for spec in SPECS { + let row = run_frame_init(spec, db.clone(), clone_frame_init(&frame_init)); + assert_eq!( + row.instruction_result, expected, + "{label} ({spec:?}): unexpected classification", + ); + assert!( + row.instruction_result.is_ok_or_revert(), + "{label} ({spec:?}): this row is only meaningful while the shape stays \ + non-halting", + ); + assert_eq!( + row.remaining, FRAME_GAS, + "{label} ({spec:?}): the whole budget must be handed back to the caller", + ); + assert_eq!( + row.booked_destroyed, 0, + "{label} ({spec:?}): gas that returns to the caller is not destroyed", + ); + } + } +} + +/// A CREATE onto an occupied address is the one `frame_init` rejection whose budget the caller +/// never sees again, so REX7 books the whole thing as destroyed and REX6 books nothing. +#[test] +fn test_create_collision_books_the_whole_swallowed_budget() { + for spec in SPECS { + let created = CALLER.create(0); + let db = row_db().account_code(created, BytecodeBuilder::default().append(STOP).build()); + let row = run_frame_init(spec, db, create_frame_init(U256::ZERO, 1)); + + assert_eq!( + row.instruction_result, + InstructionResult::CreateCollision, + "{spec:?}: the shape under test must be a collision", + ); + assert!( + !row.instruction_result.is_ok_or_revert(), + "{spec:?}: a collision is an exceptional halt, which is why the budget is lost", + ); + assert_eq!( + row.remaining, FRAME_GAS, + "{spec:?}: the result carries the whole child budget it is about to swallow", + ); + let expected = if spec.is_enabled(MegaSpecId::REX7) { FRAME_GAS } else { 0 }; + assert_eq!( + row.booked_destroyed, expected, + "{spec:?}: the swallowed budget must be booked exactly once on the destroyed lane", + ); + } +} + +/// A precompile comes back through the same arm, but it books its own split at the recording site. +/// Booking again here would double it, so the total must stay one forwarded envelope. +#[test] +fn test_precompile_result_is_not_booked_a_second_time() { + // 32 bytes: not blake2f's 213, so it is rejected before any work and halts. + let malformed = Bytes::from(vec![0xAAu8; 32]); + for spec in SPECS { + let row = run_frame_init(spec, row_db(), call_frame_init(BLAKE2F, malformed.clone(), 1)); + + assert_eq!( + row.instruction_result, + InstructionResult::PrecompileError, + "{spec:?}: the probe must reach the precompile and halt inside it", + ); + let expected = if spec.is_enabled(MegaSpecId::REX7) { FRAME_GAS } else { 0 }; + assert_eq!( + row.booked_destroyed, + expected, + "{spec:?}: the precompile's own recording site books the forwarded envelope once; \ + a second booking at the frame-init arm would report {} here", + 2 * FRAME_GAS, + ); + } +} + +/// `FrameInit` is not `Clone`, and each row is run once per spec. +fn clone_frame_init(frame_init: &FrameInit) -> FrameInit { + FrameInit { + depth: frame_init.depth, + memory: SharedMemory::new(), + frame_input: frame_init.frame_input.clone(), + } +} + +/* ------------------------------------------------------------------------------------------- * + * The same rejections reached through real transactions. + * ------------------------------------------------------------------------------------------- */ + +/// The transaction gas limit the end-to-end collision cases run with. +const TX_GAS_LIMIT: u64 = 1_000_000; + +/// Standard EVM intrinsic gas for a creation transaction with empty init code: 21,000 plus the +/// 32,000 creation surcharge. Empty init code adds neither calldata nor EIP-3860 word cost. +const CREATE_INTRINSIC_COMPUTE: u64 = 53_000; + +/// A creation transaction from [`CALLER`] with empty init code, aimed at whatever +/// `CALLER.create(0)` resolves to. +fn colliding_create_tx(gas_limit: u64) -> revm::context::TxEnv { + TxEnvBuilder::default() + .caller(CALLER) + .kind(TxKind::Create) + .gas_limit(gas_limit) + .gas_price(0) + .data(Bytes::new()) + .build_fill() +} + +/// A funded caller whose first creation address is already occupied. +fn colliding_db() -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(ONE_ETH)) + .account_code(CALLER.create(0), BytecodeBuilder::default().append(STOP).build()) +} + +/// A transaction that is nothing but a colliding creation destroys everything past its intrinsic +/// cost, and the receipt is unchanged from the frozen spec's. +#[test] +fn test_top_level_create_collision_destroys_the_rest_of_the_envelope() { + let run = |spec| { + transact_tx( + spec, + colliding_db(), + EvmTxRuntimeLimits::from_spec(spec), + colliding_create_tx(TX_GAS_LIMIT), + &default_envs(), + ) + }; + let r6 = run(MegaSpecId::REX6); + let r7 = run(MegaSpecId::REX7); + + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the collision halt itself must be unchanged", + ); + assert_eq!(r6.gas_used, TX_GAS_LIMIT, "an exceptional halt spends the whole envelope"); + assert_eq!(r7.gas_used, r6.gas_used, "receipt gas_used must be unchanged"); + + // Everything the transaction spent is either compute or the flat intrinsic storage gas: the + // collision creates no account, so nothing else is charged. + assert_eq!( + r7.compute_gas, + TX_GAS_LIMIT - TX_INTRINSIC_STORAGE_GAS, + "REX7 must report the whole envelope less its intrinsic storage gas as compute", + ); + assert_eq!( + r7.enforced(), + CREATE_INTRINSIC_COMPUTE, + "only the intrinsic compute was ever performed, so only it may enforce", + ); + assert_eq!( + r7.destroyed, + TX_GAS_LIMIT - TX_INTRINSIC_STORAGE_GAS - CREATE_INTRINSIC_COMPUTE, + "the rest of the envelope is what the refused frame swallowed", + ); + assert_eq!( + r7.booked_destroyed, r7.destroyed, + "the per-site booking and the conservation law must agree", + ); + + assert_eq!( + r6.compute_gas, CREATE_INTRINSIC_COMPUTE, + "REX6 attributes nothing to a frame that never ran", + ); + assert_eq!(r6.destroyed, 0, "REX6 has no destroyed lane"); + assert_eq!( + r7.enforced(), + r6.compute_gas, + "the enforcing lane is byte-identical across the two specs", + ); +} + +/// Two CREATE2s with the same salt and the same (empty) init code: the first deploys, the second +/// collides with it. +fn colliding_create2_code() -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // salt + .push_number(0u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE2) + .append(POP) + .push_number(0u64) // salt + .push_number(0u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE2) + .append(POP) + .append(STOP) + .build() +} + +/// The reported repro: two CREATE2s with the same salt and the same init code, the second of which +/// collides. The caller survives it, so this also pins that a swallowed inner budget is booked +/// without the surrounding frame noticing. +#[test] +fn test_inner_create2_collision_destroys_the_forwarded_budget() { + let code = colliding_create2_code(); + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code); + + let r6 = transact_default(MegaSpecId::REX6, db.clone()); + let r7 = transact_default(MegaSpecId::REX7, db); + + assert!(r7.is_success(), "the caller absorbs the failed CREATE2 and stops: {:?}", r7.result); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the caller's own result must be unchanged", + ); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); + + assert!( + r7.destroyed > 0, + "the colliding CREATE2's forwarded budget is swallowed and must be booked", + ); + assert_eq!( + r7.booked_destroyed, r7.destroyed, + "the per-site booking and the conservation law must agree", + ); + assert_eq!( + r7.enforced(), + r6.compute_gas, + "the enforcing lane is byte-identical across the two specs", + ); + assert_eq!( + r7.compute_gas, + r6.compute_gas + r7.destroyed, + "REX7 reports exactly what REX6 reported plus the swallowed budget", + ); +} + +/// A CREATE whose value exceeds the caller's balance is a revert: its budget comes back, so +/// nothing is destroyed and the two specs report the same compute total. +#[test] +fn test_inner_create_out_of_funds_destroys_nothing() { + let code = BytecodeBuilder::default() + .push_number(0u64) // size + .push_number(0u64) // offset + .push_number(2 * ONE_ETH as u64) // value, above the contract's balance + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)); + + let r6 = transact_default(MegaSpecId::REX6, db.clone()); + let r7 = transact_default(MegaSpecId::REX7, db); + + assert!(r7.is_success(), "the caller absorbs the failed CREATE: {:?}", r7.result); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); + assert_eq!(r7.destroyed, 0, "an OutOfFunds create hands its budget back"); + assert_eq!(r7.booked_destroyed, 0, "and so books nothing"); + assert_eq!( + r7.compute_gas, r6.compute_gas, + "with nothing destroyed the two specs report the same compute total", + ); +} + +/// A CREATE from an account whose nonce cannot be bumped reports success and hands its budget +/// back, so it books nothing either. +#[test] +fn test_inner_create_nonce_overflow_destroys_nothing() { + let code = BytecodeBuilder::default() + .push_number(0u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_nonce(CONTRACT, u64::MAX); + + let r6 = transact_default(MegaSpecId::REX6, db.clone()); + let r7 = transact_default(MegaSpecId::REX7, db); + + assert!(r7.is_success(), "the caller survives the refused CREATE: {:?}", r7.result); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); + assert_eq!(r7.destroyed, 0, "a nonce-overflow create hands its budget back"); + assert_eq!(r7.booked_destroyed, 0, "and so books nothing"); + assert_eq!( + r7.compute_gas, r6.compute_gas, + "with nothing destroyed the two specs report the same compute total", + ); +} + +/// A precompile that halts is booked once, by its own recording site. Running it alongside the +/// frame-init arm must not double the destroyed total. +#[test] +fn test_precompile_halt_stays_booked_once_end_to_end() { + let malformed = vec![0xAAu8; 32]; + let forwarded: u64 = 200_000; + let code = BytecodeBuilder::default() + .mstore(0, &malformed) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(malformed.len() as u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(BLAKE2F) + .push_number(forwarded) + .append(revm::bytecode::opcode::CALL) + .append(POP) + .append(STOP) + .build(); + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code); + + let r7 = transact(MegaSpecId::REX7, db, EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + + assert!(r7.is_success(), "the caller absorbs the precompile failure: {:?}", r7.result); + assert_eq!( + r7.destroyed, + forwarded, + "the forwarded envelope is destroyed exactly once; a second booking at the frame-init \ + arm would report about {} here", + 2 * forwarded, + ); + assert_eq!( + r7.booked_destroyed, r7.destroyed, + "the per-site booking and the conservation law must agree", + ); +} + +/* ------------------------------------------------------------------------------------------- * + * The rewritten-envelope boundary. + * ------------------------------------------------------------------------------------------- */ + +/// Sender of the deposit transaction. +const DEPOSIT_CALLER: Address = address!("0000000000000000000000000000000000310003"); + +/// A colliding creation, sent as an OP deposit. +/// +/// A failed deposit's receipt is rebuilt to report the whole gas limit after every settlement has +/// run, and the boundary that rebuilds it books the difference as destroyed. This transaction +/// books at both places, so it is where a double count between them would show up: the total must +/// still be the whole envelope less the work the transaction actually performed. +#[test] +fn test_failed_deposit_whose_create_collides_books_the_envelope_once() { + let gas_limit = TX_GAS_LIMIT; + let db = MemoryDatabase::default() + .account_balance(DEPOSIT_CALLER, U256::from(ONE_ETH)) + .account_code(DEPOSIT_CALLER.create(0), BytecodeBuilder::default().append(STOP).build()); + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(DEPOSIT_CALLER) + .kind(TxKind::Create) + .gas_limit(gas_limit) + .gas_price(0) + .data(Bytes::new()) + .build_fill(), + ); + tx.deposit.source_hash = B256::repeat_byte(0x42); + tx.enveloped_tx = Some(Bytes::new()); + + let r7 = transact_mega_tx( + MegaSpecId::REX7, + db, + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7), + tx, + &TestExternalEnvs::default(), + ); + + let rendered = format!("{:?}", r7.halt_reason("deposit")); + assert!( + rendered.contains("FailedDeposit"), + "a failed deposit must be reported as FailedDeposit, got {rendered}", + ); + assert_eq!(r7.gas_used, gas_limit, "a failed deposit's receipt reports the whole gas limit"); + assert_eq!( + r7.enforced(), + CREATE_INTRINSIC_COMPUTE, + "only the intrinsic compute was performed, and the rewrite must not change that", + ); + assert_eq!( + r7.destroyed, + gas_limit - TX_INTRINSIC_STORAGE_GAS - CREATE_INTRINSIC_COMPUTE, + "the rewritten envelope, less what was performed, is destroyed exactly once", + ); + assert_eq!( + r7.booked_destroyed, r7.destroyed, + "the per-site bookings and the conservation law must agree after the rewrite too", + ); +} + +/* ------------------------------------------------------------------------------------------- * + * The nested-execution boundary. + * ------------------------------------------------------------------------------------------- */ + +/// The inner keyless transaction's gas limit — enough for its constructor to run both creations. +const KEYLESS_INNER_GAS: u64 = 400_000; + +/// A deterministic pre-EIP-155 creation transaction, wrapped in a `keylessDeploy` call. +/// +/// Its constructor runs [`colliding_create2_code`], so the collision happens inside the +/// `KeylessDeploy` sandbox rather than in the outer transaction's own frames. +fn keyless_deploy_calldata() -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: KEYLESS_INNER_GAS, + to: TxKind::Create, + value: U256::ZERO, + input: colliding_create2_code(), + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from( + IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: Bytes::from(buf), + gasLimitOverride: U256::from(KEYLESS_INNER_GAS), + } + .abi_encode(), + ) +} + +/// A keyless deployment whose constructor collides with itself books the swallowed budget in the +/// sandbox's own tracker, and the merge has to carry it into the outer transaction: the outer +/// transaction reports it and still enforces only the work performed. +#[test] +fn test_keyless_sandbox_create_collision_crosses_the_merge_boundary() { + let run = |spec| { + let db = MemoryDatabase::default().account_balance(CALLER, U256::from(1_000 * ONE_ETH)); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(KEYLESS_DEPLOY_ADDRESS) + .gas_limit(2_000_000u64) + .chain_id(Some(1)) + .data(keyless_deploy_calldata()) + .build_fill(); + transact_tx(spec, db, EvmTxRuntimeLimits::from_spec(spec), tx, &default_envs()) + }; + let r6 = run(MegaSpecId::REX6); + let r7 = run(MegaSpecId::REX7); + + assert!(r7.is_success(), "the sandbox deployment must still succeed: {:?}", r7.result); + assert_eq!( + format!("{:?}", r6.result), + format!("{:?}", r7.result), + "the outer transaction's own result must be unchanged", + ); + assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); + assert!( + r7.destroyed > 0, + "the sandbox frame the collision refused swallowed its budget, and the merge must carry \ + that across", + ); + assert_eq!( + r7.booked_destroyed, r7.destroyed, + "the per-site booking and the conservation law must agree across the merge", + ); + assert_eq!( + r7.enforced(), + r6.compute_gas, + "the enforcing lane is byte-identical across the two specs", + ); + assert_eq!( + r7.compute_gas, + r6.compute_gas + r7.destroyed, + "the outer transaction reports exactly what REX6 reported plus the swallowed budget", + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 50436716..45fee87f 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -28,6 +28,10 @@ //! is shown to be stable rather than merely correct at one point. //! - `exceptional_halt` — every exceptional-halt classification, in both frame positions: the //! frame's whole burned budget settles as compute gas without changing the receipt. +//! - `frame_init_reject_burn` — the budget a refused frame init decides the fate of: the halting +//! rejections (a CREATE onto an occupied address) swallow it and book it as destroyed, the +//! returning ones hand it back and book nothing, and a precompile stays booked by its own site; +//! then through the deposit-receipt rewrite and the sandbox merge that run after the booking. //! - `burn_split` — which half of that budget enforces: the work the frame performed does, the //! remainder it destroyed does not, and both boundaries (a checkpoint's storage charge, revm's //! post-action create rejects) land on the right side. @@ -66,6 +70,7 @@ mod deposit_receipt_rewrite; mod detention_window; mod double_exceed_corner; mod exceptional_halt; +mod frame_init_reject_burn; mod gas_clamp; mod gas_leakage; mod guard_pass_static_gas; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index d19b8d79..14029818 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -588,6 +588,12 @@ A [system contract](../system-contracts/overview.md) invocation a node answers w It applies only when the answer is a halt that keeps the call's gas: the part the invocation performed before failing is executed, and the rest of the call's gas limit is destroyed. An answer that returns or reverts hands the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund; a node MUST NOT record either as destroyed, because that gas was not lost. +A call or creation an inherited EVM refuses before it opens a frame takes the same split, at the site that produces the refusal. +The refusal hands back a result carrying the whole child budget, and the classification decides that budget's fate. +A creation onto an address that already holds code or a nonce, and a value transfer that overflows the recipient's balance, are exceptional halts whose budget the caller never sees again; the frame never ran, so nothing was executed and a node MUST record the whole budget as destroyed. +A refusal classified as a success or a revert — a call or creation past the call-stack limit, a creation whose value exceeds the caller's balance, a creation from an account whose nonce cannot be bumped, a call into an account with no code — hands the budget straight back to the caller, and a node MUST NOT record any of it as destroyed. +A precompile invocation is answered on this same path and is covered by its own rule above; a node MUST NOT book it a second time here. + An ordinary transaction a node rejects during validation has no envelope to split. Since [Rex5](../upgrades/rex5.md) a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied is rejected during validation — after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited — so it produces no receipt. A node MUST NOT record such a transaction's gas limit as a destroyed remainder. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index ba22d171..2f849e4e 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -135,6 +135,11 @@ A system contract invocation a node answers without opening an EVM frame — the An answer that returns or reverts gives the gas back to the caller, and a halt whose remaining gas is rescued for the sender is a refund. A node MUST NOT record either as destroyed; that gas was not lost, and counting it would report it twice. +A call or creation an inherited EVM refuses before it opens a frame takes the same split at the site that produces the refusal, and the classification decides which way it goes. +A creation onto an address that already holds code or a nonce, and a value transfer that overflows the recipient's balance, are exceptional halts whose budget the caller never sees again; the frame never ran, so nothing was executed and a node MUST record the whole budget as destroyed. +A refusal classified as a success or a revert — a call or creation past the call-stack limit, a creation whose value exceeds the caller's balance, a creation from an account whose nonce cannot be bumped, a call into an account with no code — hands the budget straight back, and a node MUST NOT record any of it as destroyed. +A precompile invocation is answered on this same path and is covered by its own rule above; a node MUST NOT book it a second time here. + Those sites are where a Rex7 transaction is known to lose an envelope without executing it, and they are what fixes `executed_compute` at each one — but they are not what makes the enumeration complete. Completeness is a consequence of the law: a lost envelope is gas the transaction spent that neither the compute lanes nor the storage-gas lane accounts for, so it lands in the remainder whether or not a site above anticipated it. Reading the two independently and requiring them to agree is what turns the list from an assumption into a checkable claim. From 2ed8a459621a6f368a89e721661f953ff3b4bc08 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Fri, 28 Aug 2026 13:03:33 +0800 Subject: [PATCH 082/208] fix(rex7): keep a halting CALL body's charges in the open segment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CALL-family volatile wrapper carries a failing body to its tail so the detention cap is applied on every path out, and then ran the checkpoint epilogue there unconditionally. revm charges the value-transfer surcharge and the argument / return-range memory expansion inside the body, before the target load and the forwarding charge that can run out of gas, so a halting body leaves those charges in the open segment with no body window left to record them. Re-opening the segment at the current counter dropped them from the frame-exit settlement about to close it: they were neither enforced as work nor booked as destroyed, and the transaction's reported compute total no longer covered the envelope it burnt. Run the epilogue only when the handler returned normally. Every `Err` stops the interpreter loop — a halt ends the frame, and the suspension that publishes a child frame is re-clamped on resume — so a clamp is never applied to a frame that is already ending. All 83 EEST state-test cases that tripped the conservation assertion under Rex7 came from this one site. The amount dropped is whatever the body had charged before it failed: 3 to 24 gas of memory expansion on its own, the 9,000 value transfer surcharge on its own, and up to 18,460 gas for the two together. --- crates/mega-evm/src/evm/instructions.rs | 24 +- .../tests/rex7/call_body_halt_charges.rs | 210 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + docs/spec/evm/compute-gas.md | 1 + docs/spec/upgrades/rex7.md | 1 + 5 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/call_body_halt_charges.rs diff --git a/crates/mega-evm/src/evm/instructions.rs b/crates/mega-evm/src/evm/instructions.rs index eb87b85a..9afc6a65 100644 --- a/crates/mega-evm/src/evm/instructions.rs +++ b/crates/mega-evm/src/evm/instructions.rs @@ -2125,11 +2125,25 @@ pub mod volatile_data_ext { // not interpreter state, so it is safe in any interpreter state (including // `NewFrame` after a successful CALL). apply_compute_gas_limit!(context); - // REX7: re-clamp for a CALL that never published a child frame (an insufficient balance - // or depth rejection pushes 0 and lets the frame keep running). The epilogue is what - // keeps the following plain segment bounded, and it sits after the cap above so a CALL - // that just marked beneficiary access clamps against the detained headroom. - checkpoint_epilogue!(context); + // REX7: re-clamp only when the body left this frame executing, which for this + // wrapper means the handler returned normally. Every `Err` stops the interpreter + // loop: a halt ends the frame, and the suspension that publishes a child frame is + // re-clamped by `AdditionalLimit::before_frame_run` when the frame resumes. + // + // The guard is load-bearing on the halting path, because this is the one wrapper that + // carries a failing body to its tail instead of returning at the inner call. revm's + // CALL body charges the value-transfer fee and the memory expansion for the argument + // and return ranges before the account load and the forwarding charge that can run + // out of gas, so a halting body leaves real charges inside the open segment with no + // body window left to record them. The epilogue re-opens that segment at the current + // counter, which would drop exactly those charges from the frame-exit settlement + // about to close it — leaving them neither enforced as work nor booked as destroyed. + // Clamping a frame that is already ending is wrong independently: its final result + // hands the hidden gas back and reads the EVM's own out-of-gas as a clamp-induced + // compute exceed. + if inner_outcome.is_ok() { + checkpoint_epilogue!(context); + } inner_outcome } }; diff --git a/crates/mega-evm/tests/rex7/call_body_halt_charges.rs b/crates/mega-evm/tests/rex7/call_body_halt_charges.rs new file mode 100644 index 00000000..c565ad85 --- /dev/null +++ b/crates/mega-evm/tests/rex7/call_body_halt_charges.rs @@ -0,0 +1,210 @@ +//! REX7: what a CALL-family body already charged when it halts stays in the open segment. +//! +//! revm's CALL body charges before it can fail. It expands memory for the argument and return +//! ranges, then takes the value-transfer fee, and only afterwards loads the target account and +//! charges the gas it forwards — either of which can run out of gas. A body that halts there has +//! really spent the earlier charges, and it never reaches the recording window that would have +//! settled them. +//! +//! The CALL-family wrapper is the one that carries such a failure to its tail rather than +//! returning at the inner call, because the detention cap has to be applied on every path out. Its +//! tail must not re-open the settlement window on that path: the frame-exit settlement is what +//! records the charges, and re-opening the window at the current counter would drop them from +//! every lane at once — neither enforced as work nor booked as destroyed, so the transaction's +//! reported total would no longer cover the envelope it burnt. +//! +//! Each test is a differential: the same halt, at the same instruction, with the same gas left +//! over, reached once with the charge under test and once without it and with the child budget +//! reduced by exactly that charge. The difference between the two transactions' enforced compute +//! gas is what the charge contributed, and it must be the charge itself. + +use crate::common::{transact_default, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + MegaSpecId, +}; +use revm::bytecode::opcode::{CALL, CALLCODE, DELEGATECALL, POP, STATICCALL, STOP}; + +/// The account the inner frame's CALL targets. Never touched before that CALL, so the cold-access +/// surcharge is what the inner frame runs out of gas on. +const TARGET: Address = address!("0000000000000000000000000000000000350001"); + +/// Gas one `PUSH` costs, whatever its width. +const PUSH_GAS: u64 = 3; +/// The CALL-family static entry the interpreter charges before the handler is entered. +const CALL_STATIC_GAS: u64 = 100; +/// The EVM's value-transfer surcharge, charged first inside the body. +const VALUE_TRANSFER_GAS: u64 = 9_000; +/// Bytes of return range the memory-expansion shapes ask for: two words. +const RETURN_RANGE_BYTES: u64 = 64; +/// Memory gas two words cost from an untouched memory: `3 * 2 + 2 * 2 / 512`. +const RETURN_RANGE_MEMORY_GAS: u64 = 6; +/// Gas the inner frame still holds when it reaches the charge it cannot afford. Any value below +/// the cold-account surcharge puts the halt on that charge. +const SLACK_GAS: u64 = 100; + +/// Appends a CALL-family opcode targeting `target` with `gas` forwarded. +/// +/// `value` is `None` for the schemes that take no value operand. `ret_size` is the return range +/// the opcode asks for, which is what makes the body expand memory before it charges anything +/// else. +fn append_call( + builder: BytecodeBuilder, + opcode: u8, + target: Address, + gas: u64, + value: Option, + ret_size: u64, +) -> BytecodeBuilder { + let builder = + builder.push_number(ret_size).push_number(0_u64).push_number(0_u64).push_number(0_u64); + let builder = match value { + Some(value) => builder.push_number(value), + None => builder, + }; + builder.push_address(target).push_number(gas).append(opcode) +} + +/// How many stack operands a CALL-family opcode takes, which is what its pushes cost. +fn operand_count(opcode: u8) -> u64 { + match opcode { + CALL | CALLCODE => 7, + _ => 6, + } +} + +/// The inner frame: one CALL-family opcode into [`TARGET`] that cannot afford to finish. +fn inner_code(opcode: u8, value: Option, ret_size: u64) -> Bytes { + append_call(BytecodeBuilder::default(), opcode, TARGET, 0, value, ret_size) + .append(POP) + .append(STOP) + .build() +} + +/// The transaction's target: a plain CALL into [`CALLEE`] forwarding exactly `budget`, whose +/// result is discarded so the outer frame ends normally whatever the inner frame did. +fn outer_code(budget: u64) -> Bytes { + append_call(BytecodeBuilder::default(), CALL, CALLEE, budget, Some(0), 0) + .append(POP) + .append(STOP) + .build() +} + +fn db(opcode: u8, value: Option, ret_size: u64, budget: u64) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, outer_code(budget)) + .account_code(CALLEE, inner_code(opcode, value, ret_size)) + .account_balance(CALLEE, U256::from(ONE_ETH)) + .account_code(TARGET, BytecodeBuilder::default().append(STOP).build()) +} + +/// Runs one arm of a differential and checks the properties both arms share: the inner frame +/// halted on gas, so its whole budget was spent as work and nothing was destroyed. +fn run_arm(opcode: u8, value: Option, ret_size: u64, budget: u64) -> Outcome { + let outcome = transact_default(MegaSpecId::REX7, db(opcode, value, ret_size, budget)); + assert!(outcome.is_success(), "the outer frame absorbs the inner halt and stops normally"); + assert_eq!( + outcome.destroyed, 0, + "an out-of-gas frame's counter is zeroed by the interpreter, so it destroys nothing \ + the frame-exit delta cannot already see as work", + ); + assert_eq!(outcome.booked_destroyed, 0, "and no site books a destroyed remainder for it"); + outcome +} + +/// The budget an inner frame needs to reach the cold-account surcharge with [`SLACK_GAS`] left, +/// having paid `extra` inside the body first. +fn budget_for(opcode: u8, extra: u64) -> u64 { + operand_count(opcode) * PUSH_GAS + CALL_STATIC_GAS + extra + SLACK_GAS +} + +/// Asserts that `extra` gas charged inside a halting body reaches the enforced lane. +/// +/// Both arms halt on the same charge with the same gas left over, and differ only by `extra`: +/// the charge itself, and the child budget that funds it. +fn assert_body_charge_is_enforced( + opcode: u8, + with: (Option, u64), + without: (Option, u64), + extra: u64, +) { + let (with_value, with_ret_size) = with; + let (without_value, without_ret_size) = without; + let charged = run_arm(opcode, with_value, with_ret_size, budget_for(opcode, extra)); + let control = run_arm(opcode, without_value, without_ret_size, budget_for(opcode, 0)); + assert_eq!( + charged.total_gas_spent - control.total_gas_spent, + extra, + "the two arms must differ by the charge alone", + ); + assert_eq!( + charged.enforced() - control.enforced(), + extra, + "a charge the body took before halting is work the transaction performed, so it must \ + reach the lane every compute limit is evaluated against", + ); +} + +/// `CALLCODE` with a value transfer: the 9,000 surcharge is charged, and then the cold-account +/// load the frame cannot afford halts it. +#[test] +fn test_rex7_value_callcode_halt_enforces_the_transfer_fee() { + assert_body_charge_is_enforced(CALLCODE, (Some(1), 0), (Some(0), 0), VALUE_TRANSFER_GAS); +} + +/// The same shape through `CALL`, which resolves a different target account and so reaches the +/// surcharge by a different route. +#[test] +fn test_rex7_value_call_halt_enforces_the_transfer_fee() { + assert_body_charge_is_enforced(CALL, (Some(1), 0), (Some(0), 0), VALUE_TRANSFER_GAS); +} + +/// `STATICCALL` asking for a return range: the memory expansion is charged ahead of everything +/// else in the body, including the load that halts the frame. +#[test] +fn test_rex7_staticcall_halt_enforces_the_return_range_memory() { + assert_body_charge_is_enforced( + STATICCALL, + (None, RETURN_RANGE_BYTES), + (None, 0), + RETURN_RANGE_MEMORY_GAS, + ); +} + +/// `DELEGATECALL` covers the fourth instantiation of the shared wrapper. +#[test] +fn test_rex7_delegatecall_halt_enforces_the_return_range_memory() { + assert_body_charge_is_enforced( + DELEGATECALL, + (None, RETURN_RANGE_BYTES), + (None, 0), + RETURN_RANGE_MEMORY_GAS, + ); +} + +/// The receipt does not move. Checkpoint accounting changes how a halting frame's budget is +/// reported — REX7 settles it as compute gas, REX6 never records it at all — but not what the EVM +/// charged, so the gas the transaction burns is the same under both. +#[test] +fn test_rex6_and_rex7_burn_the_same_gas_on_a_halting_call_body() { + for (opcode, value, ret_size, extra) in [ + (CALLCODE, Some(1), 0, VALUE_TRANSFER_GAS), + (CALL, Some(1), 0, VALUE_TRANSFER_GAS), + (STATICCALL, None, RETURN_RANGE_BYTES, RETURN_RANGE_MEMORY_GAS), + (DELEGATECALL, None, RETURN_RANGE_BYTES, RETURN_RANGE_MEMORY_GAS), + ] { + let budget = budget_for(opcode, extra); + let rex6 = transact_default(MegaSpecId::REX6, db(opcode, value, ret_size, budget)); + let rex7 = transact_default(MegaSpecId::REX7, db(opcode, value, ret_size, budget)); + assert_eq!( + rex6.gas_used, rex7.gas_used, + "opcode 0x{opcode:02x}: the receipt must not depend on how compute gas is settled", + ); + assert_eq!( + rex6.total_gas_spent, rex7.total_gas_spent, + "opcode 0x{opcode:02x}: nor may the envelope the receipt is built from", + ); + } +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 45fee87f..e99e4dfc 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -32,6 +32,9 @@ //! rejections (a CREATE onto an occupied address) swallow it and book it as destroyed, the //! returning ones hand it back and book nothing, and a precompile stays booked by its own site; //! then through the deposit-receipt rewrite and the sandbox merge that run after the booking. +//! - `call_body_halt_charges` — what a CALL-family body already charged when it halts: the +//! value-transfer surcharge and the return-range memory expansion stay in the open segment and +//! settle as work, rather than being dropped by the wrapper's tail. //! - `burn_split` — which half of that budget enforces: the work the frame performed does, the //! remainder it destroyed does not, and both boundaries (a checkpoint's storage charge, revm's //! post-action create rejects) land on the right side. @@ -58,6 +61,7 @@ //! boundary that rebuilds it books the difference as destroyed without moving what enforces. mod burn_split; +mod call_body_halt_charges; mod charge_on_reject; mod checkpoint_families; mod checkpoint_settlement; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 14029818..eba6ea57 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -529,6 +529,7 @@ A node MUST settle that budget as compute gas, apart from any MegaETH storage ga A node MUST split what remains into two parts that are accounted differently: - **Executed** — the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, net of that storage charge. + A checkpoint opcode that halts inside its own body never reaches the recording that closes its measurement window, so the EVM gas the body had already charged — the value-transfer surcharge and the argument / return-range memory expansion a call-family body takes before it loads the target account — is still inside that segment when the frame exits, and belongs to this part. This is work the network performed, and a node MUST record it through the ordinary path: it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against, exactly as the same opcodes would if the frame had returned normally. - **Destroyed** — the budget the frame never spent and never handed back. A node MUST record it in the reported compute-gas total and in block-level compute accounting, and MUST NOT evaluate any resource limit against it, at transaction level or at block level (see [Resource Limits](resource-limits.md)). diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 2f849e4e..451970a4 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -87,6 +87,7 @@ The top-level frame's whole envelope is spent by the transaction's final gas acc A node MUST settle that budget as compute gas in two parts that are accounted differently, except for any MegaETH storage gas a checkpoint body charged before aborting: that charge stays storage gas and is in neither part. The **executed** part is the open plain-opcode segment, measured as the interpreter-gas delta since the previous checkpoint, net of that storage charge. +A checkpoint opcode that halts inside its own body never reaches the recording that closes its measurement window, so the EVM gas the body had already charged — the value-transfer surcharge and the argument / return-range memory expansion a call-family body takes before it loads the target account — is still inside that segment when the frame exits, and a node MUST settle it as executed work rather than dropping it or treating it as destroyed. A node MUST record it through the ordinary path, so it counts toward the transaction's reported total **and** toward the usage every resource limit is evaluated against — exactly as the same opcodes would if the frame had returned normally. A parent frame keeps executing after it absorbs a failed child; excluding the child's work from enforcement would let the code that follows spend the same compute headroom a second time. From bb6814dd56a4d6fc70ed1c72f927885e14414eae Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 11:26:33 +0800 Subject: [PATCH 083/208] feat(state-test): judge an unstable spec against the frozen spec it inherits from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A state-test fixture pins what a transaction must produce, but only for a spec someone has already computed an expectation for. Rex7 has none, so sweeping the Ethereum corpus under it could only check that nothing crashed. Rex7 states the conditions under which it may *not* differ from Rex6. Read as a contrapositive, that sentence is a classifier: a difference must come with evidence, read off the execution itself, that one of the invariant's three hypotheses does not hold. `diff.rs` executes each fixture under both specs, compares the quantities the invariant names, and demands that evidence — never a list of fixtures allowed to differ. Most of the evidence is on the transaction's own result, but not all of it: an inner frame that halted and was absorbed by its caller leaves no trace there, and there are thousands of those in the corpus. Those cases take a second pass under a read-only inspector that sees every frame the EVM finished. Also lets a corpus driver keep going per unit. A fill that aborts its whole file at the first failing unit is why sweeping the corpus meant splitting every multi-unit fixture into one file per unit first; a panic that takes down a worker is why it meant one process per unit. --- crates/mega-state-test/AGENTS.md | 9 +- crates/mega-state-test/src/diff.rs | 1223 +++++++++++++++++++ crates/mega-state-test/src/lib.rs | 4 + crates/mega-state-test/src/panic_capture.rs | 110 ++ crates/mega-state-test/src/runner.rs | 241 +++- crates/mega-state-test/tests/diff_mode.rs | 211 ++++ 6 files changed, 1744 insertions(+), 54 deletions(-) create mode 100644 crates/mega-state-test/src/diff.rs create mode 100644 crates/mega-state-test/src/panic_capture.rs create mode 100644 crates/mega-state-test/tests/diff_mode.rs diff --git a/crates/mega-state-test/AGENTS.md b/crates/mega-state-test/AGENTS.md index d5852081..9f705e49 100644 --- a/crates/mega-state-test/AGENTS.md +++ b/crates/mega-state-test/AGENTS.md @@ -6,7 +6,9 @@ Published as `mega-state-test`; the library keeps the `state_test` import name. The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. ## STRUCTURE -- `src/runner.rs`: test discovery, execution pipeline, validation, worker concurrency. +- `src/runner.rs`: test discovery, execution pipeline, validation, worker concurrency, `--fill`. +- `src/diff.rs`: differential execution — run one fixture under two specs and classify any disagreement against the target spec's precision invariant. +- `src/panic_capture.rs`: turning a panic inside one fixture unit into a recorded result instead of a lost run. - `src/types/`: forked revm statetest data model and deserializers. - `src/utils.rs`: root/hash validation helpers and utility glue. - `tests/`: replay-corpus validation, fixture benches, and dump round-trip tests (rely on `bench/replay/fixtures/`, so they are excluded from the published package). @@ -17,10 +19,13 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Known slow/problematic vectors are explicitly skipped by filename list. - Failure debugging path can re-run with tracer context for inspection. - Parallel execution uses shared queue and atomic counters with optional single-thread mode. +- Differential classification is evidence-based, never a list of fixtures allowed to differ: every `Mechanism` is a fact read off an execution, and the hypothesis it falsifies is what licenses a difference. +- Corpus drivers keep going per unit (`fill_test_suite_keep_going`, `diff_test_suite`) and record a unit's failure or panic rather than ending the file. - BaseFeeVault state changes are pruned as MegaETH-specific normalization. - The SALT bucket hasher comes from `mega_evm::AHashBucketHasher` (via the `test-utils` feature); never introduce a standalone salt/hasher dependency. ## ANTI-PATTERNS +- Do not explain a differential disagreement with a fixture allowlist; add a `Mechanism` that reads the evidence instead, and state which hypothesis it falsifies. - Do not spread exception matching logic across multiple files. - Keep it centralized to avoid drift. - Do not bypass `compute_test_roots` when changing validation outputs. @@ -31,5 +36,7 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Add/adjust skip policy: `runner.rs::skip_test`. - Change validation semantics for roots/output/exception: `runner.rs::{validate_exception,validate_output,check_evm_execution}`. - Change worker behavior or fail-fast policy: `runner.rs::{run_test_worker,run,TestRunnerConfig}`. +- Change what a differential run compares or what licenses a difference: `diff.rs::{DiffField,Mechanism,judge}`. +- Change the corpus sweep or its CI gates: `tools/eest-sweep/` and `.github/workflows/eest-nightly.yml`. - Update JSON schema mapping for test fixtures: `src/types/*` and deserializer modules. - Change CLI flags or path handling: `crates/state-test/src/main.rs`. diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs new file mode 100644 index 00000000..20ccb6b4 --- /dev/null +++ b/crates/mega-state-test/src/diff.rs @@ -0,0 +1,1223 @@ +//! Differential execution: run one fixture under two specs and judge any disagreement. +//! +//! A state-test fixture pins what a transaction must produce, but only for a spec someone has +//! already computed an expectation for. For an unstable spec there is no such expectation, so a +//! corpus sweep can only check that execution stays self-consistent — that no invariant trips. +//! This module supplies the missing half: it executes the same fixture under the unstable spec +//! and under the frozen spec it inherits from, and asks whether the two agree. +//! +//! Disagreement is not by itself a defect — the new spec is new precisely because it changes +//! something. What makes the question decidable is that Rex7 states the conditions under which it +//! may *not* differ (`docs/spec/upgrades/rex7.md`, "Precision invariant"): +//! +//! > For every transaction that stays within every runtime resource limit, in which no frame ends +//! > in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, +//! > a node MUST produce the same recorded compute-gas total, the same four-dimension resource +//! > usage, the same receipt `gas_used`, the same execution result, and the same state under Rex7 +//! > as under Rex6. +//! +//! Read as a contrapositive, that sentence is a classifier. The quantities after "MUST produce" +//! are what [`SpecOutcome`] compares; the three clauses before it are the hypotheses +//! [`Hypothesis`] enumerates. Two specs that disagree on a compared quantity must therefore show +//! that one of the three hypotheses is false — and the classifier demands positive evidence of +//! that from the execution itself, never a list of fixtures allowed to differ. +//! +//! A disagreement with no such evidence is [`DiffClass::Unexplained`]: either the implementation +//! deviates from the spec, or the spec's invariant is wrong. Both are findings. + +use crate::{ + panic_capture, + runner::{ + configure_max_blobs, execution_status, external_envs_for, find_all_json_tests, halt_reason, + inject_block_hashes, prune_base_fee_vault_changes, resolve_chain_id, + set_cfg_spec_and_mainnet_gas_params, skip_test, TestError, TestErrorKind, UnitStatus, + }, + types::{tx_env_at, SpecName, TestSuite, TestUnit, TxPartIndices}, + utils::{log_rlp_hash, state_merkle_trie_root}, +}; +use indicatif::{ProgressBar, ProgressDrawTarget}; +use mega_evm::{ + alloy_sol_types::SolError, + revm::{ + context::cfg::CfgEnv, + database, + database_interface::DatabaseCommit, + handler::FrameResult, + inspector::Inspector, + interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInput}, + primitives::{Bytes, B256}, + }, + MegaContext, MegaEvm, MegaLimitExceeded, MegaTransaction, MegaTransactionNew as _, + VOLATILE_DATA_ACCESS_DISABLED_SELECTOR, +}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +/// A hypothesis of the Rex7 precision invariant. +/// +/// The invariant holds the two specs to identical output only while all three are true, so +/// evidence that one is false is what licenses a difference. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Hypothesis { + /// "stays within every runtime resource limit". + WithinLimits, + /// "no frame ends in an exceptional halt". + NoExceptionalHalt, + /// "no `disableVolatileDataAccess` guard rejects an opcode". + NoDisabledVolatileReject, +} + +/// A `MegaETH` mechanism observed in a differential run. +/// +/// Each variant is a fact read off an execution, not an interpretation of one. A variant that +/// falsifies a hypothesis of the precision invariant reports it through +/// [`Mechanism::falsifies`]; the rest are recorded for the mechanism distribution but never +/// explain a difference on their own. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Mechanism { + /// The transaction halted on a `MegaETH` resource limit (compute gas, data size, KV + /// updates, state growth). + ResourceLimitHalt, + /// The transaction halted on the detained compute-gas limit. + DetentionHalt, + /// A resource-limit exceed rescued the transaction's remaining gas for the sender. + /// + /// The rescue is the tell for a limit exceed whose halt the outer accounting has already + /// rewritten — a failed deposit, for instance, reports its whole gas limit. + GasRescued, + /// A frame reverted with the frame-local `MegaLimitExceeded` payload. + FrameLocalLimitRevert, + /// A frame ended in an exceptional halt. + /// + /// Counted at the frame the EVM finished, so it covers an inner frame the caller absorbed, a + /// precompile that failed, and a call or creation refused before a frame opened — none of + /// which the transaction's own result shows. + ExceptionalHalt, + /// Rex7 booked a destroyed compute-gas remainder: an envelope was lost without being + /// executed. + DestroyedComputeGas, + /// A `disableVolatileDataAccess` guard rejected an opcode. + VolatileAccessDisabled, + /// The two specs recorded different volatile-data access marks. + /// + /// Rex7 moves the beneficiary / oracle mark to the point where the target account is loaded, + /// so a frame that cannot afford the fees charged before that load marks under Rex6 and not + /// under Rex7. On its own this changes nothing observable — it changes the *limit*, and only + /// crossing that limit changes an outcome — so it is recorded, not accepted as an + /// explanation. + DetentionMarkDiff, + /// A detention cap was in force at the end of the transaction. + /// + /// Informational for the same reason as [`Mechanism::DetentionMarkDiff`]: a cap nobody + /// reached explains nothing. + DetentionInForce, +} + +impl Mechanism { + /// The invariant hypothesis this mechanism falsifies, if any. + pub const fn falsifies(self) -> Option { + match self { + Self::ResourceLimitHalt | + Self::DetentionHalt | + Self::GasRescued | + Self::FrameLocalLimitRevert => Some(Hypothesis::WithinLimits), + Self::ExceptionalHalt | Self::DestroyedComputeGas => { + Some(Hypothesis::NoExceptionalHalt) + } + Self::VolatileAccessDisabled => Some(Hypothesis::NoDisabledVolatileReject), + Self::DetentionMarkDiff | Self::DetentionInForce => None, + } + } + + /// Stable lower-case label, for tallies and reports. + pub const fn label(self) -> &'static str { + match self { + Self::ResourceLimitHalt => "resource_limit_halt", + Self::DetentionHalt => "detention_halt", + Self::GasRescued => "gas_rescued", + Self::FrameLocalLimitRevert => "frame_local_limit_revert", + Self::ExceptionalHalt => "exceptional_halt", + Self::DestroyedComputeGas => "destroyed_compute_gas", + Self::VolatileAccessDisabled => "volatile_access_disabled", + Self::DetentionMarkDiff => "detention_mark_diff", + Self::DetentionInForce => "detention_in_force", + } + } +} + +/// A quantity the precision invariant requires the two specs to agree on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum DiffField { + /// Post-state trie root over the fixture's account closure. + StateRoot, + /// RLP hash of the emitted logs. + LogsRoot, + /// Receipt `gas_used`. + GasUsed, + /// `success` / `revert` / `halt`. + Status, + /// Halt reason, when the status is `halt`. + HaltReason, + /// Transaction output bytes. + Output, + /// Reported compute-gas total. + ComputeGasUsed, + /// Data-size dimension usage. + DataSize, + /// KV-update dimension usage. + KvUpdates, + /// State-growth dimension usage. + StateGrowth, +} + +impl DiffField { + /// Stable lower-case label, for reports. + pub const fn label(self) -> &'static str { + match self { + Self::StateRoot => "state_root", + Self::LogsRoot => "logs_root", + Self::GasUsed => "gas_used", + Self::Status => "status", + Self::HaltReason => "halt_reason", + Self::Output => "output", + Self::ComputeGasUsed => "compute_gas_used", + Self::DataSize => "data_size", + Self::KvUpdates => "kv_updates", + Self::StateGrowth => "state_growth", + } + } + + /// Whether an exceptional halt, on its own, may move this field. + /// + /// The exceptional-halt carve-out settles a halted frame's whole budget as compute gas, which + /// raises the *reported* compute total and nothing else: the receipt, the state and the other + /// three dimensions are explicitly unchanged by it. Every other field needs the transaction + /// to have taken a different path, which under Rex7 means a resource limit was crossed or a + /// guard rejected an opcode. + const fn movable_by_halt_alone(self) -> bool { + matches!(self, Self::ComputeGasUsed) + } +} + +/// One side of a differential run. +/// +/// The first group is what the precision invariant compares; the rest is the evidence a +/// disagreement is judged against. +#[derive(Debug, Clone)] +pub struct SpecOutcome { + /// Post-state trie root over the fixture's account closure. + pub state_root: B256, + /// RLP hash of the emitted logs. + pub logs_root: B256, + /// Receipt `gas_used`. + pub gas_used: u64, + /// `success` / `revert` / `halt`. + pub status: String, + /// Halt reason (`Debug` form) when the status is `halt`. + pub halt_reason: Option, + /// Transaction output bytes, if any. + pub output: Option, + /// Reported compute-gas total. + pub compute_gas_used: u64, + /// Data-size dimension usage. + pub data_size: u64, + /// KV-update dimension usage. + pub kv_updates: u64, + /// State-growth dimension usage. + pub state_growth: u64, + + /// The part of the reported compute total that was destroyed rather than performed (Rex7+). + pub compute_gas_destroyed: u64, + /// The part of the reported compute total that every compute-gas limit is evaluated against. + pub compute_gas_enforced: u64, + /// Gas a resource-limit exceed rescued for the sender. + pub rescued_gas: u64, + /// The detained compute-gas limit in force at the end of the transaction, if any. + pub detained_limit: Option, + /// Bitmap of the volatile data the transaction accessed. + pub volatile_access: u16, + /// Per-frame evidence, when the run collected it (see [`FrameEvidence`]). + pub frames: Option, +} + +impl SpecOutcome { + /// Mechanisms visible in this single execution. + fn mechanisms(&self) -> Vec { + let mut found = Vec::new(); + match self.halt_reason.as_deref() { + Some(r) if r.starts_with("VolatileDataAccessOutOfGas") => { + found.push(Mechanism::DetentionHalt); + } + // Every MegaETH-specific halt is a resource limit; `Base(..)` wraps the inherited + // EVM's own halts, which are not. + Some(r) if !r.starts_with("Base") => found.push(Mechanism::ResourceLimitHalt), + _ => {} + } + if self.rescued_gas > 0 { + found.push(Mechanism::GasRescued); + } + if self.compute_gas_destroyed > 0 { + found.push(Mechanism::DestroyedComputeGas); + } + if self.status == "halt" { + found.push(Mechanism::ExceptionalHalt); + } + if self.detained_limit.is_some() { + found.push(Mechanism::DetentionInForce); + } + if let Some(frames) = &self.frames { + if frames.halted > 0 { + found.push(Mechanism::ExceptionalHalt); + } + if frames.limit_exceeded_reverts > 0 { + found.push(Mechanism::FrameLocalLimitRevert); + } + if frames.volatile_disabled_reverts > 0 { + found.push(Mechanism::VolatileAccessDisabled); + } + } + found + } +} + +/// Per-frame facts collected by [`FrameEvidenceInspector`]. +/// +/// A transaction's own result hides most of what happens below it: an inner frame that halted and +/// was absorbed by its caller, a precompile that failed, a call refused before a frame opened. +/// All three falsify a hypothesis of the precision invariant and none of them is visible from the +/// outside, so the classifier collects them from the frames themselves when the cheap evidence +/// runs out. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct FrameEvidence { + /// Frames whose final result was an exceptional halt. + pub halted: u32, + /// Frames that reverted with the frame-local `MegaLimitExceeded` payload. + pub limit_exceeded_reverts: u32, + /// Frames that reverted with the `VolatileDataAccessDisabled` payload. + pub volatile_disabled_reverts: u32, +} + +/// Read-only inspector that records each frame's final result. +/// +/// It implements `frame_end` and nothing else, so it neither rewrites a frame result nor touches +/// the interpreter's gas counter — the two things the Rex7 accounting notes call out as making an +/// inspected execution diverge from an uninspected one. +#[derive(Debug, Default)] +pub struct FrameEvidenceInspector { + evidence: FrameEvidence, +} + +impl FrameEvidenceInspector { + /// The facts collected so far. + pub const fn evidence(&self) -> FrameEvidence { + self.evidence + } +} + +impl Inspector for FrameEvidenceInspector { + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + // `frame_end` is the one hook revm calls for *every* frame outcome — an interpreter frame + // that ran, a precompile answered without a frame, and a frame init the EVM refused — + // and it runs after the create-return processing that can still turn a successful + // constructor into a halt. + let result = frame_result.interpreter_result(); + if result.result.is_halt() { + self.evidence.halted += 1; + return; + } + if !result.result.is_revert() { + return; + } + match result.output.get(..4) { + Some(s) if s == MegaLimitExceeded::SELECTOR => { + self.evidence.limit_exceeded_reverts += 1; + } + Some(s) if s == VOLATILE_DATA_ACCESS_DISABLED_SELECTOR => { + self.evidence.volatile_disabled_reverts += 1; + } + _ => {} + } + } +} + +/// How a fixture unit's two executions compare. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiffClass { + /// Both specs agreed on every compared quantity. + Pass, + /// The specs disagreed, and the disagreement carries evidence that a hypothesis of the + /// precision invariant does not hold. + Explained, + /// The specs disagreed with no such evidence. + /// + /// Either the implementation deviates from the spec, or the spec's invariant is wrong. + Unexplained, + /// Neither spec executed the transaction, and both declined it the same way. + /// + /// A fixture the runner rejects before execution (an intrinsic-gas overrun, an unsupported + /// transaction shape, a fixture defect) says nothing about either spec's semantics as long as + /// both sides reject it identically — validation is spec-independent here, so an *asymmetric* + /// rejection is a difference and is classified as one. + Skipped, + /// Executing the unit panicked on at least one side. + Panic, +} + +impl DiffClass { + /// Stable upper-case label, for tallies and reports. + pub const fn label(self) -> &'static str { + match self { + Self::Pass => "PASS", + Self::Explained => "EXPLAINED", + Self::Unexplained => "UNEXPLAINED", + Self::Skipped => "SKIPPED", + Self::Panic => "PANIC", + } + } +} + +/// The verdict on one fixture unit. +#[derive(Debug, Clone)] +pub struct UnitDiff { + /// The unit's key in the fixture's test-suite map. + pub name: String, + /// The fixture file the unit came from. + pub path: String, + /// How the two executions compare. + pub class: DiffClass, + /// Quantities the two specs disagreed on. + pub fields: Vec, + /// Mechanisms observed on either side. + pub mechanisms: Vec, + /// Why the unit is [`DiffClass::Skipped`] or [`DiffClass::Panic`], or what the two sides + /// disagreed on in detail. + pub detail: Option, +} + +/// Which specs a differential run compares. +#[derive(Debug, Clone, Copy)] +pub struct DiffSpecs { + /// The spec under test, normally the unstable one. + pub target: SpecName, + /// The frozen spec the target inherits from. + pub base: SpecName, +} + +/// Runs one unit under both specs and classifies the result. +/// +/// `collect_evidence` decides whether the second, inspected pass runs when the cheap evidence does +/// not settle the case; see [`diff_unit`] for why it is staged. +pub fn diff_unit(unit: &TestUnit, specs: DiffSpecs, collect_evidence: bool) -> UnitDiffOutcome { + let target = execute_unit_outcome(unit, &specs.target, false); + let base = execute_unit_outcome(unit, &specs.base, false); + + let (target, base) = match (target, base) { + (Ok(t), Ok(b)) => (t, b), + (Err(t), Err(b)) => { + let (t, b) = (t.to_string(), b.to_string()); + return if t == b { + UnitDiffOutcome::skipped(t) + } else { + // Validation is the same code on both specs, so two different rejections are a + // difference in their own right — and one no execution evidence can explain, + // because neither side executed anything. + UnitDiffOutcome::unexplained( + vec![], + vec![], + format!("both specs rejected the transaction, differently: {t} != {b}"), + ) + }; + } + (Ok(_), Err(e)) => { + return UnitDiffOutcome::unexplained( + vec![], + vec![], + format!( + "{} executed but {} rejected the transaction: {e}", + label(specs.target), + label(specs.base) + ), + ) + } + (Err(e), Ok(_)) => { + return UnitDiffOutcome::unexplained( + vec![], + vec![], + format!( + "{} rejected the transaction but {} executed it: {e}", + label(specs.target), + label(specs.base) + ), + ) + } + }; + + let fields = compare(&target, &base); + if fields.is_empty() { + return UnitDiffOutcome::pass(collect_mechanisms(&target, &base)); + } + + let verdict = judge(&fields, &target, &base); + if verdict.class != DiffClass::Unexplained || !collect_evidence { + return verdict; + } + + // Stage two. The cheap evidence found nothing, so re-run both sides with the frame inspector, + // which sees the frames the transaction's own result hides. It costs an inspected execution + // only for the handful of units that reach here, instead of on every unit in the corpus. + let (Ok(target), Ok(base)) = ( + execute_unit_outcome(unit, &specs.target, true), + execute_unit_outcome(unit, &specs.base, true), + ) else { + return verdict; + }; + let inspected_fields = compare(&target, &base); + if inspected_fields.is_empty() { + // The inspected pair agrees where the uninspected pair did not: the inspector moved the + // execution, so its evidence does not describe the difference under judgement. + return UnitDiffOutcome { + detail: Some( + "uninspected runs disagreed but inspected runs agreed; frame evidence discarded" + .to_string(), + ), + ..verdict + }; + } + judge(&inspected_fields, &target, &base) +} + +/// The verdict body of [`UnitDiff`], before the unit's name and path are attached. +#[derive(Debug, Clone)] +pub struct UnitDiffOutcome { + /// How the two executions compare. + pub class: DiffClass, + /// Quantities the two specs disagreed on. + pub fields: Vec, + /// Mechanisms observed on either side. + pub mechanisms: Vec, + /// Supporting detail for the verdict. + pub detail: Option, +} + +impl UnitDiffOutcome { + fn pass(mechanisms: Vec) -> Self { + Self { class: DiffClass::Pass, fields: vec![], mechanisms, detail: None } + } + + fn skipped(detail: String) -> Self { + Self { class: DiffClass::Skipped, fields: vec![], mechanisms: vec![], detail: Some(detail) } + } + + fn unexplained(fields: Vec, mechanisms: Vec, detail: String) -> Self { + Self { class: DiffClass::Unexplained, fields, mechanisms, detail: Some(detail) } + } + + /// Attaches the unit's identity to the verdict. + pub fn named(self, name: String, path: String) -> UnitDiff { + UnitDiff { + name, + path, + class: self.class, + fields: self.fields, + mechanisms: self.mechanisms, + detail: self.detail, + } + } +} + +/// Human-facing name of a spec, for report text. +fn label(spec: SpecName) -> String { + format!("{spec:?}") +} + +/// Mechanisms visible on either side, deduplicated and ordered. +fn collect_mechanisms(target: &SpecOutcome, base: &SpecOutcome) -> Vec { + let mut found = target.mechanisms(); + found.extend(base.mechanisms()); + if target.volatile_access != base.volatile_access { + found.push(Mechanism::DetentionMarkDiff); + } + found.sort_unstable(); + found.dedup(); + found +} + +/// The quantities on which the two sides disagree. +fn compare(target: &SpecOutcome, base: &SpecOutcome) -> Vec { + let mut fields = Vec::new(); + let mut push = |differs: bool, field: DiffField| { + if differs { + fields.push(field); + } + }; + push(target.state_root != base.state_root, DiffField::StateRoot); + push(target.logs_root != base.logs_root, DiffField::LogsRoot); + push(target.gas_used != base.gas_used, DiffField::GasUsed); + push(target.status != base.status, DiffField::Status); + push(target.halt_reason != base.halt_reason, DiffField::HaltReason); + push(target.output != base.output, DiffField::Output); + push(target.compute_gas_used != base.compute_gas_used, DiffField::ComputeGasUsed); + push(target.data_size != base.data_size, DiffField::DataSize); + push(target.kv_updates != base.kv_updates, DiffField::KvUpdates); + push(target.state_growth != base.state_growth, DiffField::StateGrowth); + fields +} + +/// Decides whether the observed mechanisms license the observed differences. +/// +/// The two tiers come straight from what each hypothesis can move. Falsifying "within every +/// resource limit" or "no guard rejected an opcode" changes which opcodes ran, so it can move any +/// compared quantity. Falsifying "no frame ended in an exceptional halt" only re-attributes a +/// halted frame's budget, which the spec confines to the reported compute total: "The receipt +/// `gas_used`, the halt or revert reported, and the execution success or failure of the outer +/// transaction are unchanged by the destroyed half of that carve-out." An exceptional halt is +/// therefore not accepted as the explanation for a state-root or receipt difference. +fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> UnitDiffOutcome { + let mechanisms = collect_mechanisms(target, base); + let falsified: Vec = { + let mut h: Vec<_> = mechanisms.iter().filter_map(|m| m.falsifies()).collect(); + h.sort_unstable(); + h.dedup(); + h + }; + let path_changed = falsified + .iter() + .any(|h| matches!(h, Hypothesis::WithinLimits | Hypothesis::NoDisabledVolatileReject)); + let halted = falsified.contains(&Hypothesis::NoExceptionalHalt); + + let unexplained: Vec = fields + .iter() + .copied() + .filter(|f| !(path_changed || (halted && f.movable_by_halt_alone()))) + .collect(); + + if unexplained.is_empty() { + return UnitDiffOutcome { + class: DiffClass::Explained, + fields: fields.to_vec(), + mechanisms, + detail: None, + }; + } + let detail = format!( + "no evidence licenses a difference on: {}", + unexplained.iter().map(|f| f.label()).collect::>().join(", ") + ); + UnitDiffOutcome { + class: DiffClass::Unexplained, + fields: fields.to_vec(), + mechanisms, + detail: Some(detail), + } +} + +/// Executes one unit at transaction index 0 under `spec` and collects its outcome and evidence. +/// +/// Mirrors the validation path exactly — the same config, block environment, external +/// environment, block hashes and `BaseFeeVault` pruning — so the roots it computes are the roots +/// validation would check. +/// +/// `collect_evidence` runs the execution under [`FrameEvidenceInspector`], which is what makes an +/// inner frame's outcome visible; it costs an inspected interpreter loop, so the differential +/// classifier turns it on only for the units it cannot settle without it. +pub fn execute_unit_outcome( + unit: &TestUnit, + spec: &SpecName, + collect_evidence: bool, +) -> Result { + let mut cfg = CfgEnv::default(); + // See `execute_test_suite`: revm-27 chain-id gate-off (revm 40 default is true). + cfg.tx_chain_id_check = false; + cfg.chain_id = resolve_chain_id(&unit.env)?; + set_cfg_spec_and_mainnet_gas_params( + &mut cfg, + spec.to_spec_id().map_err(|e| TestErrorKind::FixtureError(format!("spec: {e}")))?, + ); + configure_max_blobs(&mut cfg); + + let block = unit.block_env(&cfg); + let tx = tx_env_at(unit, TxPartIndices { data: 0, gas: 0, value: 0 })?; + + let cache = unit.state(); + let mut state = + database::State::builder().with_cached_prestate(cache).with_bundle_update().build(); + inject_block_hashes(&mut state, unit)?; + + let evm_context = MegaContext::default() + .with_db(&mut state) + .with_cfg(cfg) + .with_block(block) + .with_external_envs(external_envs_for(unit)?.into()); + let mut megatx = MegaTransaction::new(tx); + megatx.enveloped_tx = Some(Bytes::default()); + + let (executed, frames, ctx) = if collect_evidence { + let mut evm = MegaEvm::new(evm_context).with_inspector(FrameEvidenceInspector::default()); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + let frames = Some(inner.inspector.evidence()); + (executed, frames, inner.ctx) + } else { + let mut evm = MegaEvm::new(evm_context); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + (executed, None, inner.ctx) + }; + + // Read the trackers before the context is dismantled: they carry the transaction's final + // limit and detention state, which no execution result exposes. + let rescued_gas = ctx.additional_limit.borrow().rescued_gas; + let (detained_limit, volatile_access) = { + let tracker = ctx.volatile_data_tracker.borrow(); + (tracker.get_compute_gas_limit(), tracker.get_volatile_data_accessed().bits()) + }; + let db = ctx.into_inner().journaled_state.database; + + let outcome = executed.map_err(|e| TestErrorKind::FixtureError(e.to_string()))?; + let compute_gas_used = outcome.compute_gas_used; + let compute_gas_destroyed = outcome.compute_gas_destroyed; + let compute_gas_enforced = outcome.compute_gas_enforced; + let data_size = outcome.data_size; + let kv_updates = outcome.kv_updates; + let state_growth = outcome.state_growth_used; + let result = outcome.result_and_state.result; + + // `execute_transaction` finalizes but does not commit; the roots are taken over the committed + // cache, exactly as the `transact_commit` validation path does. + db.commit(outcome.result_and_state.state); + prune_base_fee_vault_changes(db); + + Ok(SpecOutcome { + state_root: state_merkle_trie_root(db.cache.trie_account()), + logs_root: log_rlp_hash(result.logs()), + gas_used: result.tx_gas_used(), + status: execution_status(&result).to_string(), + halt_reason: halt_reason(&result), + output: result.output().cloned(), + compute_gas_used, + data_size, + kv_updates, + state_growth, + compute_gas_destroyed, + compute_gas_enforced, + rescued_gas, + detained_limit, + volatile_access, + frames, + }) +} + +/// Runs the differential comparison over every unit of one fixture file. +/// +/// A unit that panics is recorded as [`DiffClass::Panic`] and the rest of the file still runs; +/// see [`panic_capture`] for why that matters at corpus scale. +pub fn diff_test_suite( + path: &Path, + specs: DiffSpecs, + collect_evidence: bool, +) -> Result, TestError> { + let path_str = path.to_string_lossy().into_owned(); + if skip_test(path) { + return Ok(vec![]); + } + + let fixture_err = |msg: String| TestError { + name: "diff".to_string(), + path: path_str.clone(), + kind: TestErrorKind::FixtureError(msg), + }; + let s = std::fs::read_to_string(path).map_err(|e| fixture_err(format!("read: {e}")))?; + let suite: TestSuite = serde_json::from_str(&s).map_err(|e| TestError { + name: "Unknown".to_string(), + path: path_str.clone(), + kind: e.into(), + })?; + + let mut diffs = Vec::with_capacity(suite.0.len()); + for (name, unit) in suite.0 { + let outcome = match panic_capture::catch(|| diff_unit(&unit, specs, collect_evidence)) { + Ok(outcome) => outcome, + Err(report) => UnitDiffOutcome { + class: DiffClass::Panic, + fields: vec![], + mechanisms: vec![], + detail: Some(report), + }, + }; + diffs.push(outcome.named(name, path_str.clone())); + } + Ok(diffs) +} + +/// Counts of every verdict and mechanism seen over a corpus. +#[derive(Debug, Clone, Default)] +pub struct DiffTally { + /// Units per [`DiffClass`], keyed by [`DiffClass::label`]. + pub classes: BTreeMap<&'static str, usize>, + /// Units per [`Mechanism`] over the explained differences, keyed by [`Mechanism::label`]. + pub mechanisms: BTreeMap<&'static str, usize>, + /// Units per set of disagreeing quantities over the explained differences, keyed by the + /// comma-joined [`DiffField::label`]s. + /// + /// The shape of an explained difference is what a reviewer reads to see whether the corpus is + /// exercising the deviations the spec describes — one entry per distinct shape, rather than + /// one line per unit, which at corpus scale is tens of thousands of identical lines. + pub explained_fields: BTreeMap, + /// Every unit that needs a human: an unexplained difference or a panic. + pub flagged: Vec, + /// Files the runner could not read or parse at all, as rendered errors. + pub file_errors: Vec, + /// Files validation skips by filename, and which the sweep therefore judged no unit of. + /// + /// Counted rather than ignored: it is the difference between the number of units this sweep + /// reports and the number a driver that splits the corpus into one file per unit would, and + /// leaving it implicit turns every comparison against such a run into a manual subtraction. + pub skipped_files: usize, +} + +impl DiffTally { + /// Number of units in a class. + pub fn count(&self, class: DiffClass) -> usize { + self.classes.get(class.label()).copied().unwrap_or(0) + } + + /// Total number of units judged. + pub fn total(&self) -> usize { + self.classes.values().sum() + } + + /// Whether the run should fail its gate: a panic or an unexplained difference. + pub fn is_failure(&self) -> bool { + self.count(DiffClass::Panic) > 0 || + self.count(DiffClass::Unexplained) > 0 || + !self.file_errors.is_empty() + } + + /// Records one unit's verdict. + pub fn record(&mut self, diff: UnitDiff) { + *self.classes.entry(diff.class.label()).or_insert(0) += 1; + if diff.class == DiffClass::Explained { + for m in &diff.mechanisms { + *self.mechanisms.entry(m.label()).or_insert(0) += 1; + } + let shape = diff.fields.iter().map(|f| f.label()).collect::>().join(","); + *self.explained_fields.entry(shape).or_insert(0) += 1; + } + if matches!(diff.class, DiffClass::Unexplained | DiffClass::Panic) { + self.flagged.push(diff); + } + } + + /// Merges another tally into this one. + pub fn merge(&mut self, other: Self) { + for (k, v) in other.classes { + *self.classes.entry(k).or_insert(0) += v; + } + for (k, v) in other.mechanisms { + *self.mechanisms.entry(k).or_insert(0) += v; + } + for (k, v) in other.explained_fields { + *self.explained_fields.entry(k).or_insert(0) += v; + } + self.flagged.extend(other.flagged); + self.file_errors.extend(other.file_errors); + self.skipped_files += other.skipped_files; + } +} + +/// How a corpus-wide differential run behaves. +#[derive(Debug, Clone, Copy)] +pub struct DiffRunConfig { + /// The specs to compare. + pub specs: DiffSpecs, + /// Run every file on one thread. + pub single_thread: bool, + /// Re-run an otherwise unexplained difference with the frame inspector. + pub collect_evidence: bool, + /// Draw a progress bar. + pub progress: bool, +} + +/// Runs the differential comparison over every fixture file, in parallel. +/// +/// Installs the panic capture hook: a `debug_assert!` one fixture trips becomes that fixture's +/// verdict instead of taking down a worker thread, which is what makes a single-process +/// full-corpus sweep possible. +pub fn run_diff(files: Vec, config: DiffRunConfig) -> DiffTally { + panic_capture::install_capture_hook(); + + let n_files = files.len(); + let bar = Arc::new(ProgressBar::with_draw_target( + Some(n_files as u64), + if config.progress { ProgressDrawTarget::stdout() } else { ProgressDrawTarget::hidden() }, + )); + let queue = Arc::new(Mutex::new(files)); + let next = Arc::new(AtomicUsize::new(0)); + let threads = if config.single_thread { + 1 + } else { + std::thread::available_parallelism().map_or(1, |n| n.get().min(n_files.max(1))) + }; + + let mut handles = Vec::with_capacity(threads); + for i in 0..threads { + let (queue, next, bar) = (queue.clone(), next.clone(), bar.clone()); + handles.push( + std::thread::Builder::new() + .name(format!("diff-{i}")) + .spawn(move || { + let mut tally = DiffTally::default(); + loop { + let idx = next.fetch_add(1, Ordering::SeqCst); + let Some(path) = queue.lock().unwrap().get(idx).cloned() else { + return tally; + }; + if crate::runner::is_skipped_fixture(&path) { + tally.skipped_files += 1; + bar.inc(1); + continue; + } + match diff_test_suite(&path, config.specs, config.collect_evidence) { + Ok(diffs) => { + for diff in diffs { + tally.record(diff); + } + } + Err(e) => tally.file_errors.push(e.to_string()), + } + bar.inc(1); + } + }) + .expect("spawn diff worker"), + ); + } + + let mut tally = DiffTally::default(); + for handle in handles { + match handle.join() { + Ok(worker) => tally.merge(worker), + // A worker thread that unwound past `diff_test_suite` lost the files it had taken; + // surface that rather than reporting a short tally as a clean run. + Err(_) => tally + .file_errors + .push("a diff worker thread panicked; its files were not judged".to_string()), + } + } + bar.finish_and_clear(); + tally +} + +/// Collects every JSON fixture under each path, rejecting a path that does not exist. +pub fn collect_fixture_files(paths: &[PathBuf]) -> Result, TestError> { + let mut files = Vec::new(); + for path in paths { + if !path.exists() { + return Err(TestError { + name: "Path validation".to_string(), + path: path.display().to_string(), + kind: TestErrorKind::InvalidPath, + }); + } + files.extend(find_all_json_tests(path)); + } + if files.is_empty() { + return Err(TestError { + name: "Path validation".to_string(), + path: paths.iter().map(|p| p.display().to_string()).collect::>().join(", "), + kind: TestErrorKind::NoJsonFiles, + }); + } + Ok(files) +} + +/// Bridges a keep-going fill's per-unit status into the sweep's own vocabulary. +/// +/// A fill sweep and a differential sweep count the same corpus in the same three buckets, so they +/// report through one mapping rather than two that can drift. +pub const fn fill_status_class(status: &UnitStatus) -> DiffClass { + match status { + UnitStatus::Ok => DiffClass::Pass, + UnitStatus::Error(_) => DiffClass::Skipped, + UnitStatus::Panic(_) => DiffClass::Panic, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// An outcome that agrees with itself on every compared quantity and shows no mechanism. + fn quiet() -> SpecOutcome { + SpecOutcome { + state_root: B256::ZERO, + logs_root: B256::ZERO, + gas_used: 21_000, + status: "success".to_string(), + halt_reason: None, + output: None, + compute_gas_used: 1_000, + data_size: 10, + kv_updates: 1, + state_growth: 0, + compute_gas_destroyed: 0, + compute_gas_enforced: 1_000, + rescued_gas: 0, + detained_limit: None, + volatile_access: 0, + frames: None, + } + } + + // Every mechanism maps to exactly the hypothesis it observes, and the two detention labels + // stay informational. A mechanism silently gaining a hypothesis would let it explain a + // difference it is not evidence for. + #[test] + fn test_mechanism_hypothesis_table() { + use Hypothesis::{NoDisabledVolatileReject, NoExceptionalHalt, WithinLimits}; + for (mechanism, expected) in [ + (Mechanism::ResourceLimitHalt, Some(WithinLimits)), + (Mechanism::DetentionHalt, Some(WithinLimits)), + (Mechanism::GasRescued, Some(WithinLimits)), + (Mechanism::FrameLocalLimitRevert, Some(WithinLimits)), + (Mechanism::ExceptionalHalt, Some(NoExceptionalHalt)), + (Mechanism::DestroyedComputeGas, Some(NoExceptionalHalt)), + (Mechanism::VolatileAccessDisabled, Some(NoDisabledVolatileReject)), + (Mechanism::DetentionMarkDiff, None), + (Mechanism::DetentionInForce, None), + ] { + assert_eq!(mechanism.falsifies(), expected, "{}", mechanism.label()); + } + } + + /// A quantity to disturb, and how to disturb it. + type FieldProbe = (DiffField, fn(&mut SpecOutcome)); + + // `compare` covers every quantity the precision invariant names; a field left out of the + // comparison is a difference the sweep can never see. + #[test] + fn test_compare_detects_every_field() { + let base = quiet(); + let cases: [FieldProbe; 10] = [ + (DiffField::StateRoot, |o| o.state_root = B256::repeat_byte(1)), + (DiffField::LogsRoot, |o| o.logs_root = B256::repeat_byte(2)), + (DiffField::GasUsed, |o| o.gas_used += 1), + (DiffField::Status, |o| o.status = "revert".to_string()), + (DiffField::HaltReason, |o| o.halt_reason = Some("Base(OutOfGas)".to_string())), + (DiffField::Output, |o| o.output = Some(Bytes::from_static(b"\x01"))), + (DiffField::ComputeGasUsed, |o| o.compute_gas_used += 1), + (DiffField::DataSize, |o| o.data_size += 1), + (DiffField::KvUpdates, |o| o.kv_updates += 1), + (DiffField::StateGrowth, |o| o.state_growth += 1), + ]; + assert!(compare(&base, &base).is_empty(), "an outcome must agree with itself"); + for (field, mutate) in cases { + let mut target = base.clone(); + mutate(&mut target); + assert_eq!(compare(&target, &base), vec![field], "{}", field.label()); + } + } + + // The exceptional-halt carve-out raises the reported compute total and is explicitly + // forbidden from moving the receipt or the state, so it licenses one field and not the other. + #[test] + fn test_exceptional_halt_explains_only_the_reported_compute_total() { + let base = quiet(); + let mut target = base.clone(); + target.compute_gas_destroyed = 5_000; + target.compute_gas_used += 5_000; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained); + assert!(verdict.mechanisms.contains(&Mechanism::DestroyedComputeGas)); + + // Same evidence, a state-root difference: not licensed. + let mut target = base.clone(); + target.compute_gas_destroyed = 5_000; + target.state_root = B256::repeat_byte(9); + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained); + assert!( + verdict.detail.as_deref().is_some_and(|d| d.contains("state_root")), + "detail should name the unlicensed field: {:?}", + verdict.detail + ); + } + + // A crossed resource limit changes which opcodes ran, so it licenses any quantity — + // including the consensus-visible ones. + #[test] + fn test_resource_limit_evidence_explains_a_consensus_difference() { + let base = quiet(); + let mut target = base.clone(); + target.status = "halt".to_string(); + target.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + target.state_root = B256::repeat_byte(9); + target.gas_used += 5; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained); + assert!(verdict.mechanisms.contains(&Mechanism::ResourceLimitHalt)); + } + + // The evidence may sit on the *base* side: Rex7 relaxes enforcement on a failing precompile, + // so the frozen spec is the one that halts and the unstable one that survives. + #[test] + fn test_evidence_on_the_base_side_explains_the_difference() { + let target = quiet(); + let mut base = target.clone(); + base.status = "halt".to_string(); + base.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + base.state_root = B256::repeat_byte(9); + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained); + } + + // Detention labels describe the setting, not a crossing: a cap nobody reached, or a mark that + // moved without changing an outcome, must not license anything. + #[test] + fn test_detention_labels_alone_do_not_explain() { + let base = quiet(); + let mut target = base.clone(); + target.detained_limit = Some(100_000); + target.volatile_access = 0b100; + target.compute_gas_used += 1; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained); + assert!(verdict.mechanisms.contains(&Mechanism::DetentionInForce)); + assert!(verdict.mechanisms.contains(&Mechanism::DetentionMarkDiff)); + } + + // A `disableVolatileDataAccess` rejection charges the opcode's static fee under Rex7 and + // nothing under Rex6, which can change anything downstream. + #[test] + fn test_guard_rejection_explains_a_consensus_difference() { + let base = quiet(); + let mut target = base.clone(); + target.frames = Some(FrameEvidence { + halted: 0, + limit_exceeded_reverts: 0, + volatile_disabled_reverts: 1, + }); + target.gas_used += 3; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained); + assert!(verdict.mechanisms.contains(&Mechanism::VolatileAccessDisabled)); + } + + // An inner frame that halted is invisible in the transaction's own result and leaves no + // destroyed remainder when the interpreter zeroed its counter. Frame evidence is the only + // thing that sees it. + #[test] + fn test_frame_evidence_supplies_the_halt_the_result_hides() { + let base = quiet(); + let mut target = base.clone(); + target.compute_gas_used += 700; + assert_eq!( + judge(&compare(&target, &base), &target, &base).class, + DiffClass::Unexplained, + "without frame evidence there is nothing to license the difference" + ); + + target.frames = Some(FrameEvidence { + halted: 1, + limit_exceeded_reverts: 0, + volatile_disabled_reverts: 0, + }); + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained); + assert!(verdict.mechanisms.contains(&Mechanism::ExceptionalHalt)); + } + + // The halt-reason string decides which mechanism a halt is: the inherited EVM's own halts + // arrive wrapped in `Base(..)` and are not resource limits. + #[test] + fn test_halt_reason_classification() { + let mut outcome = quiet(); + outcome.status = "halt".to_string(); + + outcome.halt_reason = Some("Base(OutOfGas(Basic))".to_string()); + let m = outcome.mechanisms(); + assert!(m.contains(&Mechanism::ExceptionalHalt)); + assert!(!m.contains(&Mechanism::ResourceLimitHalt)); + + outcome.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + assert!(outcome.mechanisms().contains(&Mechanism::ResourceLimitHalt)); + + outcome.halt_reason = Some("VolatileDataAccessOutOfGas { .. }".to_string()); + let m = outcome.mechanisms(); + assert!(m.contains(&Mechanism::DetentionHalt)); + assert!(!m.contains(&Mechanism::ResourceLimitHalt)); + } + + // Rescued gas is the tell for a limit exceed whose halt the outer accounting rewrote. + #[test] + fn test_rescued_gas_is_limit_evidence() { + let mut outcome = quiet(); + outcome.rescued_gas = 1; + assert!(outcome.mechanisms().contains(&Mechanism::GasRescued)); + } + + fn diff_of(class: DiffClass, fields: Vec, mechanisms: Vec) -> UnitDiff { + UnitDiff { + name: "u".to_string(), + path: "p".to_string(), + class, + fields, + mechanisms, + detail: None, + } + } + + // The tally counts every class, keeps only what a human must look at, and fails the gate on + // exactly the two classes the sweep exists to catch. + #[test] + fn test_tally_accounting_and_gate() { + let mut tally = DiffTally::default(); + tally.record(diff_of(DiffClass::Pass, vec![], vec![])); + tally.record(diff_of(DiffClass::Skipped, vec![], vec![])); + tally.record(diff_of( + DiffClass::Explained, + vec![DiffField::ComputeGasUsed], + vec![Mechanism::ExceptionalHalt], + )); + assert_eq!(tally.total(), 3); + assert_eq!(tally.count(DiffClass::Explained), 1); + assert_eq!(tally.mechanisms.get("exceptional_halt"), Some(&1)); + assert_eq!(tally.explained_fields.get("compute_gas_used"), Some(&1)); + assert!(tally.flagged.is_empty(), "pass/skip/explained need no human"); + assert!(!tally.is_failure()); + + let mut other = DiffTally::default(); + other.record(diff_of(DiffClass::Unexplained, vec![DiffField::StateRoot], vec![])); + tally.merge(other); + assert_eq!(tally.count(DiffClass::Unexplained), 1); + assert_eq!(tally.flagged.len(), 1); + assert!(tally.is_failure()); + } + + // A file the runner could not read at all is a hole in the sweep's coverage, not a pass. + #[test] + fn test_file_error_fails_the_gate() { + let mut tally = DiffTally::default(); + tally.record(diff_of(DiffClass::Pass, vec![], vec![])); + assert!(!tally.is_failure()); + tally.file_errors.push("unreadable".to_string()); + assert!(tally.is_failure()); + } + + #[test] + fn test_fill_status_class_mapping() { + assert_eq!(fill_status_class(&UnitStatus::Ok), DiffClass::Pass); + assert_eq!(fill_status_class(&UnitStatus::Error(String::new())), DiffClass::Skipped); + assert_eq!(fill_status_class(&UnitStatus::Panic(String::new())), DiffClass::Panic); + } +} diff --git a/crates/mega-state-test/src/lib.rs b/crates/mega-state-test/src/lib.rs index 7af297a0..89cea2c1 100644 --- a/crates/mega-state-test/src/lib.rs +++ b/crates/mega-state-test/src/lib.rs @@ -3,6 +3,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +pub mod diff; + +pub mod panic_capture; + pub mod types; pub mod runner; diff --git a/crates/mega-state-test/src/panic_capture.rs b/crates/mega-state-test/src/panic_capture.rs new file mode 100644 index 00000000..a8b5d9b6 --- /dev/null +++ b/crates/mega-state-test/src/panic_capture.rs @@ -0,0 +1,110 @@ +//! Turning a panic inside one fixture unit into a recorded result. +//! +//! A sweep over tens of thousands of fixtures is only useful if a single fixture that trips a +//! `debug_assert!` costs that one fixture rather than the whole run. The alternative used before +//! this module — one process per fixture — isolates perfectly but pays a process spawn and a +//! fixture parse per case, which is what made a full-corpus sweep an overnight job. +//! +//! Two pieces are needed. [`catch`] contains the unwind, and the hook installed by +//! [`install_capture_hook`] records the panic's location and message, which the payload alone does +//! not carry. + +use std::{ + cell::RefCell, + panic::{self, AssertUnwindSafe}, + string::{String, ToString}, + sync::atomic::{AtomicBool, Ordering}, +}; + +thread_local! { + /// The report of the most recent panic on this thread, written by the capture hook and taken + /// by [`catch`]. Thread-local because worker threads panic independently. + static LAST_PANIC: RefCell> = const { RefCell::new(None) }; +} + +/// Whether [`install_capture_hook`] has run. Read by [`catch`] to decide whether a taken-empty +/// report means "no hook" or "hook installed but the panic carried no location". +static HOOK_INSTALLED: AtomicBool = AtomicBool::new(false); + +/// Replaces the process-wide panic hook with one that records each panic instead of printing it. +/// +/// This is process-wide and permanent: it silences the default hook (message, location and +/// backtrace) for every panic in the process, caught or not. Call it only from a driver that +/// reports the captured reports itself — a caught panic that nobody prints is a panic nobody +/// sees. +/// +/// Idempotent: a second call is a no-op, so several drivers in one process cannot stack hooks. +pub fn install_capture_hook() { + if HOOK_INSTALLED.swap(true, Ordering::SeqCst) { + return; + } + panic::set_hook(Box::new(|info| { + // The `Display` form is `panicked at :::\n` — the same shape the + // default hook prints, minus the backtrace. + let report = info.to_string(); + LAST_PANIC.with(|slot| *slot.borrow_mut() = Some(report)); + })); +} + +/// Runs `f`, converting a panic into `Err()`. +/// +/// The report is the one the capture hook recorded when [`install_capture_hook`] has run; +/// otherwise it falls back to the panic payload, which carries the message but not the location. +/// +/// `f` is treated as unwind-safe. Every caller in this crate builds the state it touches from +/// scratch for each fixture unit, so a half-updated value cannot outlive the panic; do not use +/// this to wrap work that mutates state shared with the next unit. +pub fn catch(f: impl FnOnce() -> T) -> Result { + // Clear first: a report left by an earlier panic on this thread must not be attributed to + // this call. + LAST_PANIC.with(|slot| *slot.borrow_mut() = None); + panic::catch_unwind(AssertUnwindSafe(f)).map_err(|payload| { + LAST_PANIC + .with(|slot| slot.borrow_mut().take()) + .unwrap_or_else(|| payload_message(&payload)) + }) +} + +/// Best-effort message from a panic payload, for the no-hook path. +fn payload_message(payload: &Box) -> String { + if let Some(s) = payload.downcast_ref::<&'static str>() { + (*s).to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "panicked with a non-string payload".to_string() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_catch_returns_the_value_when_nothing_panics() { + assert_eq!(catch(|| 41 + 1).expect("no panic"), 42); + } + + #[test] + fn test_catch_converts_a_panic_into_an_error() { + let err = catch(|| panic!("boom {}", 7)).expect_err("panic must be caught"); + assert!(err.contains("boom 7"), "report should carry the message: {err}"); + } + + #[test] + fn test_catch_reports_a_panic_after_the_hook_is_installed() { + // The hook adds the location, which the payload alone does not carry. Installing it is + // process-wide, so this test also fixes what the other tests in this module observe — + // both accept either report shape. + install_capture_hook(); + let err = catch(|| panic!("located")).expect_err("panic must be caught"); + assert!(err.contains("located"), "report should carry the message: {err}"); + assert!(err.contains("panicked at"), "hook report should carry the location: {err}"); + } + + #[test] + fn test_catch_does_not_attribute_an_earlier_panic_to_a_later_call() { + let _ = catch(|| panic!("first")); + assert_eq!(catch(|| "second").expect("no panic"), "second"); + } +} diff --git a/crates/mega-state-test/src/runner.rs b/crates/mega-state-test/src/runner.rs index ad787e20..147575d2 100644 --- a/crates/mega-state-test/src/runner.rs +++ b/crates/mega-state-test/src/runner.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] use crate::{ + panic_capture, types::{ tx_env_at, Env, SpecName, Test, TestError as TxBuildError, TestSuite, TestUnit, TxPartIndices, @@ -110,11 +111,19 @@ pub fn find_all_json_tests(path: &Path) -> Vec { } } +/// Whether validation skips this fixture file entirely, by filename. +/// +/// The skip list is a policy, not a failure: a driver that walks a corpus needs to tell a file it +/// is not meant to run from one it could not run, because only the second is a hole in coverage. +pub fn is_skipped_fixture(path: &Path) -> bool { + skip_test(path) +} + /// Check if a test should be skipped based on its filename /// Some tests are known to be problematic or take too long /// /// These tests are skipped by `revm`, so we also skip them. -fn skip_test(path: &Path) -> bool { +pub(crate) fn skip_test(path: &Path) -> bool { // A path with no file name or a non-UTF-8 name cannot match any entry on // the skip list, so it is simply not skipped (and must not panic). let Some(name) = path.file_name().and_then(|n| n.to_str()) else { @@ -401,7 +410,7 @@ fn check_evm_execution( /// revm 40 stores per-spec gas params in [`CfgEnv`]. Assigning `cfg.spec` alone /// leaves the previous params in place and drifts gas accounting. Shared by /// [`execute_test_suite`] and single-unit execution. -fn set_cfg_spec_and_mainnet_gas_params(cfg: &mut CfgEnv, spec: MegaSpecId) { +pub(crate) fn set_cfg_spec_and_mainnet_gas_params(cfg: &mut CfgEnv, spec: MegaSpecId) { cfg.set_spec_and_mainnet_gas_params(spec); } @@ -409,7 +418,7 @@ fn set_cfg_spec_and_mainnet_gas_params(cfg: &mut CfgEnv, spec: MegaS /// /// Single source of truth shared by [`execute_test_suite`] and single-unit /// execution so the validation and dump paths stay byte-identical. -fn configure_max_blobs(cfg: &mut CfgEnv) { +pub(crate) fn configure_max_blobs(cfg: &mut CfgEnv) { // OSAKA (which implies PRAGUE) caps blobs back at 6, while the PRAGUE-only // window allows 9 — so the OSAKA arm must be checked first and is distinct // from the pre-PRAGUE default of 6 despite the same value. @@ -427,7 +436,7 @@ fn configure_max_blobs(cfg: &mut CfgEnv) { /// An absent field defaults to `MegaETH`'s 6342 (intentional EEST behavior), /// but a present value that does not fit in a `u64` is a fixture error rather /// than a silent fallback to the default chain. -fn resolve_chain_id(env: &Env) -> Result { +pub(crate) fn resolve_chain_id(env: &Env) -> Result { match env.current_chain_id { None => Ok(6342), Some(id) => id.try_into().map_err(|_| { @@ -577,7 +586,7 @@ pub fn execute_test_suite( /// Uses [`AHashBucketHasher`] so that bucket IDs match those recorded during /// `mega-evme replay` — a different hasher would map keys to different buckets /// and reproduce different gas. -fn external_envs_for( +pub(crate) fn external_envs_for( unit: &TestUnit, ) -> Result, TestErrorKind> { let mega_env = unit.mega_env.clone().unwrap_or_default(); @@ -592,7 +601,10 @@ fn external_envs_for( /// A key that does not fit in a `u64` could never be requested by the EVM /// (block numbers are `u64` on the `BLOCKHASH` path), so it is a fixture error /// rather than a silently dropped entry. -fn inject_block_hashes(state: &mut State, unit: &TestUnit) -> Result<(), TestErrorKind> { +pub(crate) fn inject_block_hashes( + state: &mut State, + unit: &TestUnit, +) -> Result<(), TestErrorKind> { let Some(hashes) = &unit.env.block_hashes else { return Ok(()); }; @@ -886,6 +898,64 @@ pub fn bench_test_suite( Ok(results) } +/// What a keep-going driver observed for one fixture unit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnitStatus { + /// The unit ran to completion. + Ok, + /// The unit failed with a structured error, before or during execution. + Error(String), + /// Executing the unit panicked; the payload is the captured panic report. + /// + /// Distinct from [`UnitStatus::Error`] on purpose: an error is a fixture the runner declined + /// to execute, a panic is an internal invariant (a `debug_assert!`, an overflow check) that + /// the fixture broke. Only the second is a defect in the code under test. + Panic(String), +} + +impl UnitStatus { + /// Whether the unit ran to completion. + pub const fn is_ok(&self) -> bool { + matches!(self, Self::Ok) + } + + /// The failure report, or `None` when the unit ran to completion. + pub fn message(&self) -> Option<&str> { + match self { + Self::Ok => None, + Self::Error(m) | Self::Panic(m) => Some(m), + } + } +} + +/// What happened to one unit of a fixture file under a keep-going fill. +#[derive(Debug, Clone)] +pub struct UnitFillResult { + /// The unit's key in the fixture's test-suite map. + pub name: String, + /// Whether the unit filled, failed, or panicked. + pub status: UnitStatus, +} + +/// Per-unit outcome of filling one fixture file with `keep_going` set. +#[derive(Debug, Clone, Default)] +pub struct FillReport { + /// One entry per unit in the file, in the file's own order. + pub units: Vec, +} + +impl FillReport { + /// Number of units whose `post` was recomputed and written. + pub fn filled(&self) -> usize { + self.units.iter().filter(|u| u.status.is_ok()).count() + } + + /// Number of units that failed or panicked and kept their original `post`. + pub fn failed(&self) -> usize { + self.units.len() - self.filled() + } +} + /// Compute and write the `post` expectation for every unit in a fixture file, /// in place — the offline analog of `--dump-fixture`'s post-fill step, for a /// fixture that has no `post` yet (a hand-built or `prestateTracer`-snapshot @@ -901,11 +971,47 @@ pub fn bench_test_suite( /// Filling replaces the unit's entire `post` map (single spec, single index /// `{0,0,0}`) with circularly-derived expectations, so a unit that already has a /// non-empty `post` is refused unless `force` is set. +/// +/// The first unit that fails aborts the whole file. Use +/// [`fill_test_suite_keep_going`] to fill the rest of a file whose units fail +/// independently. pub fn fill_test_suite( path: &Path, spec_override: Option, force: bool, ) -> Result { + let report = fill_suite(path, spec_override, force, false)?; + Ok(report.filled()) +} + +/// Fill every unit of a fixture file, recording each unit's failure instead of +/// aborting the file at the first one. +/// +/// A unit that fails or panics keeps its original `post` and is reported in the +/// returned [`FillReport`]; the units that succeeded are still written. This is +/// what lets a corpus sweep run a multi-unit fixture without splitting it into +/// one file per unit first: an EEST fixture holds one unit per (test, fork) +/// pair, and under a spec override the ones that the runner declines are +/// exactly the ones a split sweep would have counted separately. +/// +/// Errors that belong to the *file* rather than to a unit — an unreadable or +/// unparseable fixture, a filename on the validation skip list, a failed write — +/// are still returned as errors: there is no per-unit result to record them on. +pub fn fill_test_suite_keep_going( + path: &Path, + spec_override: Option, + force: bool, +) -> Result { + fill_suite(path, spec_override, force, true) +} + +/// Shared body of [`fill_test_suite`] and [`fill_test_suite_keep_going`]. +fn fill_suite( + path: &Path, + spec_override: Option, + force: bool, + keep_going: bool, +) -> Result { let path_str = path.to_string_lossy().into_owned(); let fixture_err = |msg: String| TestError { name: "fill".to_string(), @@ -930,67 +1036,96 @@ pub fn fill_test_suite( kind: e.into(), })?; - let mut filled = std::collections::BTreeMap::new(); + let mut report = FillReport::default(); + let mut out = std::collections::BTreeMap::new(); + let mut any_filled = false; for (name, mut unit) in suite.0 { - if !force && unit.post.values().any(|tests| !tests.is_empty()) { - return Err(fixture_err(format!( - "unit {name} already has a post expectation; pass --force to overwrite" - ))); - } - let spec = match spec_override { - Some(s) => s, - None => { - let mut specs = unit.post.keys(); - match (specs.next(), specs.next()) { - (Some(s), None) => *s, - _ => { - return Err(fixture_err(format!( - "unit {name} has no single post spec; pass --bench-spec to fill" - ))) - } + match fill_unit(&mut unit, spec_override, force) { + Ok(()) => { + any_filled = true; + report.units.push(UnitFillResult { name: name.clone(), status: UnitStatus::Ok }); + } + Err(status) => { + if !keep_going { + let detail = status.message().unwrap_or("failed"); + return Err(fixture_err(format!("unit {name}: {detail}"))); } + report.units.push(UnitFillResult { name: name.clone(), status }); } - }; - // Reject an unmapped spec at selection time, so the error names the - // unit instead of surfacing from deep inside execution. - if spec == SpecName::Unknown { - return Err(fixture_err(format!( - "unit {name} selects an unknown spec; pass a valid --bench-spec" - ))); } - // Validation skips Constantinople (mirroring upstream revme), so a post - // recorded under it would never be checked. - if spec == SpecName::Constantinople { - return Err(fixture_err(format!( - "unit {name}: validation skips Constantinople; a post filled under it \ - would never be checked" - ))); - } - let executed = execute_unit_collect(&unit, &spec) - .map_err(|e| fixture_err(format!("execute {name}: {e}")))?; - unit.out = executed.output.clone(); - let test = Test::for_dump( - executed.state_root, - executed.logs_root, - executed.gas_used, - executed.status, - ); - unit.post = std::collections::BTreeMap::from([(spec, vec![test])]); - filled.insert(name, unit); + out.insert(name, unit); } - let count = filled.len(); - let json = serde_json::to_string_pretty(&TestSuite(filled)) + // Nothing changed: leave the file's bytes (and mtime) alone rather than + // rewriting it with a re-serialization of what it already held. + if !any_filled { + return Ok(report); + } + + let json = serde_json::to_string_pretty(&TestSuite(out)) .map_err(|e| fixture_err(format!("serialize: {e}")))?; // Write to a sibling temp file and rename so an interrupted write cannot // truncate the original fixture. let tmp = path.with_extension("json.tmp"); std::fs::write(&tmp, json).map_err(|e| fixture_err(format!("write: {e}")))?; std::fs::rename(&tmp, path).map_err(|e| fixture_err(format!("rename: {e}")))?; - Ok(count) + Ok(report) +} + +/// Recompute one unit's `post` in place, or report why it could not be filled. +/// +/// Execution runs under [`panic_capture::catch`] so that a `debug_assert!` a +/// single fixture trips is that fixture's result rather than the run's. +fn fill_unit( + unit: &mut TestUnit, + spec_override: Option, + force: bool, +) -> Result<(), UnitStatus> { + let err = |msg: String| UnitStatus::Error(msg); + + if !force && unit.post.values().any(|tests| !tests.is_empty()) { + return Err(err("already has a post expectation; pass --force to overwrite".to_string())); + } + let spec = match spec_override { + Some(s) => s, + None => { + let mut specs = unit.post.keys(); + match (specs.next(), specs.next()) { + (Some(s), None) => *s, + _ => { + return Err( + err("has no single post spec; pass --bench-spec to fill".to_string()), + ) + } + } + } + }; + // Reject an unmapped spec at selection time, so the error names the unit + // instead of surfacing from deep inside execution. + if spec == SpecName::Unknown { + return Err(err("selects an unknown spec; pass a valid --bench-spec".to_string())); + } + // Validation skips Constantinople (mirroring upstream revme), so a post + // recorded under it would never be checked. + if spec == SpecName::Constantinople { + return Err(err( + "validation skips Constantinople; a post filled under it would never be checked" + .to_string(), + )); + } + + let executed = panic_capture::catch(|| execute_unit_collect(unit, &spec)) + .map_err(UnitStatus::Panic)? + .map_err(|e| err(format!("execute: {e}")))?; + + unit.out = executed.output.clone(); + let test = + Test::for_dump(executed.state_root, executed.logs_root, executed.gas_used, executed.status); + unit.post = std::collections::BTreeMap::from([(spec, vec![test])]); + Ok(()) } -fn prune_base_fee_vault_changes(db: &mut State) { +pub(crate) fn prune_base_fee_vault_changes(db: &mut State) { let base_fee_vault = address!("0x4200000000000000000000000000000000000019"); db.cache.accounts.remove(&base_fee_vault); } diff --git a/crates/mega-state-test/tests/diff_mode.rs b/crates/mega-state-test/tests/diff_mode.rs new file mode 100644 index 00000000..48a65658 --- /dev/null +++ b/crates/mega-state-test/tests/diff_mode.rs @@ -0,0 +1,211 @@ +//! End-to-end tests for the differential runner and the keep-going fill. +//! +//! The classifier's decision table is unit-tested in `src/diff.rs`; these tests drive real +//! executions, so they cover the parts the table cannot: that the two specs are actually executed +//! and committed the way validation does, that the staged frame evidence reaches a halt no +//! transaction result exposes, and that a keep-going fill isolates one unit's failure from the +//! rest of its file. + +use state_test::{ + diff::{diff_test_suite, diff_unit, execute_unit_outcome, DiffClass, DiffSpecs, Mechanism}, + runner::{fill_test_suite, fill_test_suite_keep_going, UnitStatus}, + types::{SpecName, TestUnit}, +}; +use std::path::PathBuf; + +const SENDER: &str = "0x1000000000000000000000000000000000000001"; +const CALLEE: &str = "0x2000000000000000000000000000000000000002"; +const INNER: &str = "0x3000000000000000000000000000000000000003"; + +/// `CALL(0 gas, 0x40..04, no value, no args, no return); POP` — runs out of gas partway. +/// +/// Given a small enough allowance this frame halts, and it halts somewhere the two specs account +/// for differently: the opcode that crosses the frame's gas records nothing under Rex6, while +/// Rex7 settles the whole open segment at frame exit. +const INNER_RUNS_OUT: &str = + "0x600060006000600060007340000000000000000000000000000000000000046000f150"; + +/// `CALL( gas, INNER, no value, no args, no return); POP; STOP`. +/// +/// `CALL` pushes 0 on failure and this frame carries on to a normal `STOP`, so nothing about the +/// child's halt reaches the transaction's own result. +fn call_into_inner(gas: u16) -> String { + format!("0x6000600060006000600073{}61{gas:04x}f1500000", &INNER[2..]) +} + +/// A unit whose transaction calls `CALLEE`, with an optional third account at `INNER`. +fn unit_json(callee_code: &str, inner_code: Option<&str>) -> serde_json::Value { + let mut pre = serde_json::json!({ + SENDER: { "balance": "0xde0b6b3a7640000", "code": "0x", "nonce": "0x0", "storage": {} }, + CALLEE: { "balance": "0x0", "code": callee_code, "nonce": "0x0", "storage": {} }, + }); + if let Some(code) = inner_code { + pre[INNER] = + serde_json::json!({ "balance": "0x0", "code": code, "nonce": "0x0", "storage": {} }); + } + serde_json::json!({ + "env": { + "currentChainID": "0x18c6", + "currentCoinbase": "0x3000000000000000000000000000000000000009", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x10", + "currentTimestamp": "0x3e8", + "currentBaseFee": "0x0", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000001", + "currentExcessBlobGas": "0x0" + }, + "pre": pre, + "transaction": { + "type": 0, + "data": ["0x"], + "gasLimit": ["0x30d40"], + "gasPrice": "0x0", + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sender": SENDER, + "to": CALLEE, + "value": ["0x0"] + }, + "post": {} + }) +} + +fn parse_unit(json: &serde_json::Value) -> TestUnit { + serde_json::from_value(json.clone()).expect("valid unit json") +} + +fn rex7_over_rex6() -> DiffSpecs { + DiffSpecs { target: SpecName::Rex7, base: SpecName::Rex6 } +} + +/// Writes a suite of named units to a unique temp file and returns its path. +fn write_suite(file_name: &str, units: &[(&str, serde_json::Value)]) -> PathBuf { + let suite: serde_json::Map = + units.iter().map(|(n, u)| ((*n).to_string(), u.clone())).collect(); + let dir = std::env::temp_dir().join("mega_state_test_diff_mode"); + std::fs::create_dir_all(&dir).expect("mkdir"); + let path = dir.join(file_name); + std::fs::write(&path, serde_json::to_string_pretty(&suite).expect("serialize")) + .expect("write fixture"); + path +} + +/// A unit whose inner frame runs out of gas somewhere the two specs account for differently. +/// +/// The exact allowance that lands on such an opcode is a function of the gas schedule, so it is +/// searched for rather than hard-coded: the property under test is that *some* inner halt moves +/// the reported compute total while leaving the transaction's own result untouched, not that a +/// particular gas number does. +fn inner_halt_json() -> serde_json::Value { + for gas in 1..=64u16 { + let json = unit_json(&call_into_inner(gas), Some(INNER_RUNS_OUT)); + let unit = parse_unit(&json); + let target = execute_unit_outcome(&unit, &SpecName::Rex7, false).expect("rex7 executes"); + let base = execute_unit_outcome(&unit, &SpecName::Rex6, false).expect("rex6 executes"); + let hidden = target.status == "success" && target.compute_gas_destroyed == 0; + if hidden && target.compute_gas_used != base.compute_gas_used { + return json; + } + } + panic!("no forwarded-gas amount produced an inner halt that moves the reported compute total") +} + +fn inner_halt_unit() -> TestUnit { + parse_unit(&inner_halt_json()) +} + +// A transaction that stays inside every limit, ends no frame in an exceptional halt and trips no +// guard is bit-identical under Rex7 and Rex6 — the precision invariant's own statement, executed. +#[test] +fn test_within_limit_transaction_is_identical_under_both_specs() { + let unit = parse_unit(&unit_json("0x", None)); + let outcome = diff_unit(&unit, rex7_over_rex6(), true); + assert_eq!(outcome.class, DiffClass::Pass, "{outcome:?}"); + assert!(outcome.fields.is_empty()); +} + +// The staged evidence pass exists for exactly this shape: an inner frame runs out of gas, its +// caller absorbs the failure and returns normally, and the interpreter left nothing to destroy. +// The transaction's own result is a plain success, so only the frame the EVM finished shows the +// halt — and without that, a real and correct Rex7 deviation reads as a defect. +#[test] +fn test_inner_frame_halt_is_explained_only_with_frame_evidence() { + let unit = inner_halt_unit(); + + let without = diff_unit(&unit, rex7_over_rex6(), false); + assert_eq!( + without.class, + DiffClass::Unexplained, + "the transaction's own result hides the inner halt: {without:?}" + ); + + let with = diff_unit(&unit, rex7_over_rex6(), true); + assert_eq!(with.class, DiffClass::Explained, "{with:?}"); + assert!( + with.mechanisms.contains(&Mechanism::ExceptionalHalt), + "frame evidence should name the halt: {:?}", + with.mechanisms + ); +} + +// The whole file is judged, one verdict per unit, and a unit's verdict is attributed to its own +// name — a sweep that mislabels which fixture differed is unusable for triage. +#[test] +fn test_diff_test_suite_reports_one_verdict_per_unit() { + let path = write_suite( + "two_units.json", + &[("quiet", unit_json("0x", None)), ("inner_halt", inner_halt_json())], + ); + let diffs = diff_test_suite(&path, rex7_over_rex6(), true).expect("diff suite"); + assert_eq!(diffs.len(), 2); + let quiet = diffs.iter().find(|d| d.name == "quiet").expect("quiet unit"); + let halting = diffs.iter().find(|d| d.name == "inner_halt").expect("halting unit"); + assert_eq!(quiet.class, DiffClass::Pass); + assert_eq!(halting.class, DiffClass::Explained); +} + +// A fixture the runner declines on both sides says nothing about either spec: the gas limit here +// is below the intrinsic cost both specs charge, so neither executes anything. +#[test] +fn test_transaction_rejected_by_both_specs_is_skipped() { + let mut json = unit_json("0x", None); + json["transaction"]["gasLimit"] = serde_json::json!(["0x1"]); + let unit = parse_unit(&json); + let outcome = diff_unit(&unit, rex7_over_rex6(), true); + assert_eq!(outcome.class, DiffClass::Skipped, "{outcome:?}"); +} + +// Keep-going fill: one unit's failure must cost that unit, not the units after it in the same +// file. Without this, a corpus sweep has to split every multi-unit fixture first. +#[test] +fn test_keep_going_fill_isolates_one_unit_failure() { + // The middle unit's gas limit is below the intrinsic cost, so filling it fails. + let mut broken = unit_json("0x", None); + broken["transaction"]["gasLimit"] = serde_json::json!(["0x1"]); + let path = write_suite( + "keep_going.json", + &[("a_ok", unit_json("0x", None)), ("b_broken", broken), ("c_ok", unit_json("0x", None))], + ); + + // Without keep-going the whole file aborts at the broken unit and nothing is written. + let before = std::fs::read_to_string(&path).expect("read"); + let err = fill_test_suite(&path, Some(SpecName::Rex7), true).expect_err("must abort"); + assert!(err.to_string().contains("b_broken"), "error should name the unit: {err}"); + assert_eq!(std::fs::read_to_string(&path).expect("read"), before, "file must be untouched"); + + let report = fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill"); + assert_eq!(report.filled(), 2); + assert_eq!(report.failed(), 1); + let failed = report.units.iter().find(|u| !u.status.is_ok()).expect("one unit failed"); + assert_eq!(failed.name, "b_broken"); + assert!(matches!(failed.status, UnitStatus::Error(_)), "{:?}", failed.status); + + // The two good units carry a freshly recorded Rex7 expectation; the failed one keeps its + // original (empty) post rather than a half-written one. + let written: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read")).expect("json"); + assert!(written["a_ok"]["post"]["Rex7"].is_array()); + assert!(written["c_ok"]["post"]["Rex7"].is_array()); + assert_eq!(written["b_broken"]["post"], serde_json::json!({})); +} From 6784d213864be61565bb8c22ec19a3a5bf7bfb18 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 11:26:41 +0800 Subject: [PATCH 084/208] feat(state-test): add `--diff-spec` and keep-going `--fill` to the CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--bench-spec Rex7 --diff-spec Rex6` runs the differential sweep and prints the tally, the mechanism distribution over explained differences, and every unit that needs a human. It exits non-zero on exactly two conditions — a fixture that panicked, and a difference nothing licenses — so a corpus whose declined fixtures number in the thousands still gives CI a meaningful gate. `--diff-spec` requires `--bench-spec`: a differential run compares two named specs, and taking the target from each fixture's own `post` would make the comparison mean something different from one unit to the next. `--keep-going` now also applies to `--fill`, which reports a per-unit tally instead of stopping at the first unit it cannot fill. --- crates/state-test/README.md | 4 +- crates/state-test/src/main.rs | 287 ++++++++++++++++++++++++---- crates/state-test/tests/cli_exit.rs | 79 ++++++++ 3 files changed, 328 insertions(+), 42 deletions(-) diff --git a/crates/state-test/README.md b/crates/state-test/README.md index 58ee76f8..ae2e1c36 100644 --- a/crates/state-test/README.md +++ b/crates/state-test/README.md @@ -14,5 +14,7 @@ Every mode operates on self-contained EEST fixtures (`TestUnit { env, pre, trans - **Validate** (default) — `state-test ` executes each fixture and checks its recorded `post` (state root, logs root, gas, status). This is how the official Ethereum tests and the replay corpus (`bench/replay/fixtures/`, via `replay_corpus.rs`) are checked. - **`--bench`** — `state-test --bench [--bench-runs N] [--bench-warmup W] [--bench-spec SPEC] ` times each fixture's isolated EVM execution and prints `{ gas_used, success, bench: { min/median/mean, mgasPerSec } }` as JSON instead of validating. This is the only EVM-throughput benchmark entry point; the replay-throughput benchmark (`bench/replay/run.py`) drives it. - **`--fill`** — `state-test --fill --bench-spec SPEC ` computes each fixture's `post` and writes it back in place (atomically, via a temp file). This is the offline analog of `mega-evme replay --dump-fixture`'s post-fill step, for a fixture that has no on-chain origin (a hand-built case, or a `prestateTracer` snapshot such as `bench/replay/fixtures/attack_deploy.json`). After filling, the fixture is self-validating like any dumped one. A fixture that already has a non-empty `post` is refused unless `--force` is passed — filling replaces the whole `post` map with circularly-derived expectations, so an accidental run against real expectations (e.g. the official test suites) would destroy them. Filenames on the validation skip list and the Constantinople spec are refused outright, since validation would never check the result. +- **`--diff-spec`** — `state-test --bench-spec TARGET --diff-spec BASE ` executes each fixture under both specs and classifies how they differ. Nothing is written and no recorded `post` is consulted: the two executions are compared against each other. This is the only check available for a spec nobody has computed expectations for yet — the frozen spec it inherits from is the oracle, and that spec's precision invariant is what says when the two are allowed to disagree. See `crates/mega-state-test/src/diff.rs` for the classification and `tools/eest-sweep/` for the corpus driver built on it. +- **`--keep-going`** — with `--fill`, records each unit's failure (or panic) and carries on with the rest of its file instead of aborting at the first one, then prints a `Fill tally:` line. Without it, one bad unit ends the whole run, which is why a corpus sweep used to have to split every multi-unit fixture into one file per unit first. -`--bench-spec` selects the spec to run under; without it, the fixture's single `post` spec is used (so `--fill` needs it when the `post` is still empty). \ No newline at end of file +`--bench-spec` selects the spec to run under; without it, the fixture's single `post` spec is used (so `--fill` needs it when the `post` is still empty, and `--diff-spec` requires it outright). \ No newline at end of file diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 8770ffdc..537e1d3a 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -5,9 +5,10 @@ use clap::Parser; use state_test::{ + diff::{collect_fixture_files, run_diff, DiffClass, DiffRunConfig, DiffSpecs, DiffTally}, runner::{ - bench_test_suite, fill_test_suite, find_all_json_tests, run, TestError, TestErrorKind, - UnitBench, + bench_test_suite, fill_test_suite, fill_test_suite_keep_going, find_all_json_tests, + is_skipped_fixture, run, TestError, TestErrorKind, UnitBench, UnitStatus, }, types::SpecName, }; @@ -71,11 +72,33 @@ pub struct Cmd { /// Overwrite an existing non-empty `post` when filling with `--fill`. #[arg(long, requires = "fill")] force: bool, + /// Execute each fixture under this spec as well and report how the two differ. + /// + /// The spec under test is `--bench-spec`, which is therefore required: a differential run + /// compares two named specs, and taking the target from each fixture's own `post` would make + /// the comparison mean something different from one unit to the next. Nothing is written — + /// the comparison is between the two executions, not against a recorded expectation, which is + /// what lets an unstable spec with no expectations be checked at all. + #[arg(long, value_name = "SPEC", requires = "bench_spec", conflicts_with_all = ["bench", "fill"])] + diff_spec: Option, + /// Write the differential run's tally and every flagged unit to this file, as JSON. + #[arg(long, value_name = "FILE", requires = "diff_spec")] + diff_report: Option, + /// Skip the inspected second pass that collects per-frame evidence for a difference the + /// cheap evidence did not explain. + /// + /// Only useful for measuring the cost of that pass: without it, a difference caused by an + /// inner frame the transaction's own result hides is reported as unexplained. + #[arg(long, requires = "diff_spec")] + diff_no_frame_evidence: bool, } impl Cmd { /// Runs `statetest` command. pub fn run(&self) -> Result<(), TestError> { + if self.diff_spec.is_some() { + return self.run_diff(); + } if self.fill { return self.run_fill(); } @@ -109,47 +132,19 @@ impl Cmd { /// Parse `--bench-spec` into a [`SpecName`], if given. fn resolve_spec(&self) -> Result, TestError> { - self.bench_spec - .as_deref() - .map(|s| { - let invalid_spec = || TestError { - name: "spec".to_string(), - path: s.to_string(), - kind: TestErrorKind::FixtureError(format!( - "invalid --bench-spec {s:?}; expected one of: {}", - [ - mega_evm::name::EQUIVALENCE, - mega_evm::name::MINI_REX, - mega_evm::name::REX, - mega_evm::name::REX1, - mega_evm::name::REX2, - mega_evm::name::REX3, - mega_evm::name::REX4, - mega_evm::name::REX5, - mega_evm::name::REX6, - mega_evm::name::REX7, - ] - .join(", ") - )), - }; - let spec = MegaSpecId::from_str(s) - .map(SpecName::from_mega_spec) - .map_err(|_| invalid_spec())?; - // A spec id that parses but has no fixture-facing name (a - // future `MegaSpecId` this crate does not map yet) would - // otherwise fail much later, deep inside execution — reject it - // here with the same actionable message. - if spec == SpecName::Unknown { - return Err(invalid_spec()); - } - Ok(spec) - }) - .transpose() + self.bench_spec.as_deref().map(|s| parse_spec("--bench-spec", s)).transpose() + } + + /// Parse `--diff-spec` into a [`SpecName`], if given. + fn resolve_diff_spec(&self) -> Result, TestError> { + self.diff_spec.as_deref().map(|s| parse_spec("--diff-spec", s)).transpose() } /// Fill every fixture's `post` expectation in place (see `--fill`). fn run_fill(&self) -> Result<(), TestError> { let spec_override = self.resolve_spec()?; + let (mut filled, mut errors, mut panics) = (0usize, 0usize, 0usize); + let (mut file_errors, mut skipped_files) = (0usize, 0usize); for path in &self.paths { if !path.exists() { return Err(TestError { @@ -159,11 +154,114 @@ impl Cmd { }); } for file in find_all_json_tests(path) { - let n = fill_test_suite(&file, spec_override, self.force)?; - println!("Filled post for {n} unit(s) in {}", file.display()); + if self.keep_going { + // A file the runner declines as a whole (an unreadable fixture, a filename on + // the validation skip list) must not end the sweep either: record it and move + // on, the same way a declined unit is recorded. + if is_skipped_fixture(&file) { + println!("SKIP_FILE\t{}", file.display()); + skipped_files += 1; + continue; + } + let report = match fill_test_suite_keep_going(&file, spec_override, self.force) + { + Ok(report) => report, + Err(e) => { + println!( + "FILE_ERR\t{}\t{}", + file.display(), + e.to_string().replace('\n', " ") + ); + file_errors += 1; + continue; + } + }; + for unit in &report.units { + match &unit.status { + UnitStatus::Ok => {} + UnitStatus::Error(m) => { + println!("ERR\t{}::{}\t{m}", file.display(), unit.name); + errors += 1; + } + UnitStatus::Panic(m) => { + println!( + "PANIC\t{}::{}\t{}", + file.display(), + unit.name, + m.replace('\n', " ") + ); + panics += 1; + } + } + } + filled += report.filled(); + } else { + let n = fill_test_suite(&file, spec_override, self.force)?; + println!("Filled post for {n} unit(s) in {}", file.display()); + filled += n; + } } } - Ok(()) + if !self.keep_going { + return Ok(()); + } + let total = filled + errors + panics; + println!( + "Fill tally: OK={filled} ERR={errors} PANIC={panics} FILE_ERR={file_errors} \ + SKIP_FILE={skipped_files} TOTAL={total}" + ); + if errors + panics + file_errors == 0 { + return Ok(()); + } + // `--keep-going` changes when the run stops, not whether it failed: the CLI's exit-code + // contract still reports a unit that did not fill. + Err(TestError { + name: "fill summary".to_string(), + path: String::new(), + kind: TestErrorKind::TestsFailed { failed: errors + panics + file_errors, total }, + }) + } + + /// Execute every fixture under both specs and report how they differ (see `--diff-spec`). + fn run_diff(&self) -> Result<(), TestError> { + let base = self.resolve_diff_spec()?.expect("run_diff is only reached with --diff-spec"); + // Clap's `requires = "bench_spec"` makes the target explicit before this point. + let target = self.resolve_spec()?.expect("--diff-spec requires --bench-spec"); + let files = collect_fixture_files(&self.paths)?; + + let specs = DiffSpecs { target, base }; + let tally = run_diff( + files, + DiffRunConfig { + specs, + single_thread: self.single_thread, + collect_evidence: !self.diff_no_frame_evidence, + progress: !self.json, + }, + ); + + print_diff_tally(&tally, target, base); + if let Some(report) = &self.diff_report { + let json = serde_json::to_string_pretty(&diff_report_json(&tally, target, base)) + .expect("serialize diff report"); + std::fs::write(report, json).map_err(|e| TestError { + name: "diff report".to_string(), + path: report.display().to_string(), + kind: TestErrorKind::FixtureError(format!("write: {e}")), + })?; + } + + if !tally.is_failure() { + return Ok(()); + } + Err(TestError { + name: "diff summary".to_string(), + path: String::new(), + kind: TestErrorKind::TestsFailed { + failed: tally.count(DiffClass::Unexplained) + tally.count(DiffClass::Panic), + total: tally.total(), + }, + }) } /// Benchmark every fixture under the given paths and print the results as JSON. @@ -222,6 +320,113 @@ impl Cmd { } } +/// Parse a spec-name flag value into a [`SpecName`]. +/// +/// Rejects both an unparseable string and a `MegaSpecId` this crate has no fixture-facing name +/// for; either would otherwise fail much later, deep inside execution. +fn parse_spec(flag: &str, value: &str) -> Result { + let invalid_spec = || TestError { + name: "spec".to_string(), + path: value.to_string(), + kind: TestErrorKind::FixtureError(format!( + "invalid {flag} {value:?}; expected one of: {}", + [ + mega_evm::name::EQUIVALENCE, + mega_evm::name::MINI_REX, + mega_evm::name::REX, + mega_evm::name::REX1, + mega_evm::name::REX2, + mega_evm::name::REX3, + mega_evm::name::REX4, + mega_evm::name::REX5, + mega_evm::name::REX6, + mega_evm::name::REX7, + ] + .join(", ") + )), + }; + let spec = + MegaSpecId::from_str(value).map(SpecName::from_mega_spec).map_err(|_| invalid_spec())?; + if spec == SpecName::Unknown { + return Err(invalid_spec()); + } + Ok(spec) +} + +/// Every class a differential run can produce, in report order. +const DIFF_CLASSES: [DiffClass; 5] = [ + DiffClass::Pass, + DiffClass::Explained, + DiffClass::Unexplained, + DiffClass::Skipped, + DiffClass::Panic, +]; + +/// Prints the differential run's tally, mechanism distribution, and every flagged unit. +fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { + println!("\nDifferential run: {target:?} vs {base:?} over {} unit(s)", tally.total()); + for class in DIFF_CLASSES { + println!(" {:<12} {}", class.label(), tally.count(class)); + } + if tally.skipped_files > 0 { + println!(" ({} file(s) skipped by filename, no unit of them judged)", tally.skipped_files); + } + if !tally.mechanisms.is_empty() { + println!("Mechanisms over explained differences:"); + for (label, count) in &tally.mechanisms { + println!(" {label:<28} {count}"); + } + } + if !tally.explained_fields.is_empty() { + println!("Shapes of explained differences (disagreeing quantities):"); + for (shape, count) in &tally.explained_fields { + println!(" {shape:<48} {count}"); + } + } + for diff in &tally.flagged { + println!( + "{}\t{}::{}\t{}\t{}", + diff.class.label(), + diff.path, + diff.name, + diff.fields.iter().map(|f| f.label()).collect::>().join(","), + diff.detail.as_deref().unwrap_or("-").replace('\n', " ") + ); + } + for error in &tally.file_errors { + println!("FILE_ERROR\t{}", error.replace('\n', " ")); + } +} + +/// The machine-readable form of [`print_diff_tally`], for `--diff-report`. +fn diff_report_json(tally: &DiffTally, target: SpecName, base: SpecName) -> serde_json::Value { + json!({ + "targetSpec": format!("{target:?}"), + "baseSpec": format!("{base:?}"), + "total": tally.total(), + "classes": DIFF_CLASSES + .iter() + .map(|c| (c.label().to_string(), json!(tally.count(*c)))) + .collect::>(), + "mechanisms": tally.mechanisms, + "explainedFields": tally.explained_fields, + "fileErrors": tally.file_errors, + "skippedFiles": tally.skipped_files, + "flagged": tally + .flagged + .iter() + .map(|d| json!({ + "class": d.class.label(), + "path": d.path, + "name": d.name, + "fields": d.fields.iter().map(|f| f.label()).collect::>(), + "mechanisms": d.mechanisms.iter().map(|m| m.label()).collect::>(), + "detail": d.detail, + })) + .collect::>(), + }) +} + fn main() { let cmd = Cmd::parse(); // CI exit-code contract: any error — including `TestsFailed` when tests diff --git a/crates/state-test/tests/cli_exit.rs b/crates/state-test/tests/cli_exit.rs index e3b20cc5..a46448e4 100644 --- a/crates/state-test/tests/cli_exit.rs +++ b/crates/state-test/tests/cli_exit.rs @@ -99,3 +99,82 @@ fn passing_run_exits_with_code_0() { let out = run_cli(&[path.to_str().expect("utf8 path")]); assert_eq!(out.status.code(), Some(0), "passing run must exit 0"); } + +#[test] +fn diff_run_with_no_unexplained_difference_exits_with_code_0() { + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + // The differential run computes both sides itself; the recorded `post` is irrelevant, and an + // empty one keeps the fixture honest about that. + suite["exit_code_test"]["post"] = serde_json::json!({}); + let path = write_fixture("diff_pass.json", &serde_json::to_string(&suite).expect("serialize")); + let report = write_fixture("diff_pass_report.json", ""); + + let out = run_cli(&[ + path.to_str().expect("utf8 path"), + "--bench-spec", + "Rex7", + "--diff-spec", + "Rex6", + "--diff-report", + report.to_str().expect("utf8 path"), + ]); + assert_eq!(out.status.code(), Some(0), "a run with no unexplained difference must exit 0"); + + let written: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&report).expect("read report")) + .expect("report is json"); + assert_eq!(written["targetSpec"], "Rex7"); + assert_eq!(written["baseSpec"], "Rex6"); + assert_eq!(written["classes"]["PASS"], 1); + assert_eq!(written["classes"]["UNEXPLAINED"], 0); +} + +#[test] +fn diff_run_over_two_unrelated_specs_reports_unexplained_and_exits_1() { + // The Rex7 precision invariant relates Rex7 to Rex6 and says nothing about any other pair, so + // a Rex7-against-Equivalence difference carries no licensing evidence — the MegaETH intrinsic + // surcharge alone moves the receipt. This is the negative control for the gate: a classifier + // that explained everything would exit 0 here. + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + suite["exit_code_test"]["post"] = serde_json::json!({}); + let path = write_fixture("diff_fail.json", &serde_json::to_string(&suite).expect("serialize")); + + let out = run_cli(&[ + path.to_str().expect("utf8 path"), + "--bench-spec", + "Rex7", + "--diff-spec", + "Equivalence", + ]); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("UNEXPLAINED"), "tally should report the class: {stdout}"); + assert_eq!(out.status.code(), Some(1), "an unexplained difference must fail the gate"); +} + +#[test] +fn diff_spec_requires_an_explicit_target_spec() { + let path = write_fixture("diff_needs_target.json", FAILING_SUITE); + let out = run_cli(&[path.to_str().expect("utf8 path"), "--diff-spec", "Rex6"]); + assert_eq!(out.status.code(), Some(2), "clap rejects the incomplete flag combination"); + assert!( + String::from_utf8_lossy(&out.stderr).contains("bench-spec"), + "the error should name the missing flag" + ); +} + +#[test] +fn diff_spec_rejects_an_unknown_spec_name() { + let path = write_fixture("diff_bad_spec.json", FAILING_SUITE); + let out = run_cli(&[ + path.to_str().expect("utf8 path"), + "--bench-spec", + "Rex7", + "--diff-spec", + "FutureFork9000", + ]); + assert_eq!(out.status.code(), Some(1)); + assert!( + String::from_utf8_lossy(&out.stderr).contains("--diff-spec"), + "the error should name the offending flag" + ); +} From 1e7a27c4555d15985391d2c0ceeb44becd6c231b Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 11:26:52 +0800 Subject: [PATCH 085/208] ci: sweep the EEST corpus against the unstable spec nightly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One command fetches the hash-pinned fixture release, unpacks its state_tests subtree, and runs every fixture under Rex7 and Rex6, comparing the two. The nightly workflow runs the same command; it needs no secrets, caches the corpus on its content hash, and builds with the `hivetests` profile — optimized, but with debug assertions live, so the Rex7 gas-conservation cross-checks actually run. Two hard gates: a fixture that panicked, and a difference nothing licenses. Coverage drift against the committed baseline is a warning in the job summary, not a red run: it means the corpus or the runner changed what it reaches, which is worth seeing but is not a defect. --- .github/workflows/eest-nightly.yml | 113 ++++++++++++++++++ .gitignore | 5 + AGENTS.md | 2 + tools/eest-sweep/README.md | 74 ++++++++++++ tools/eest-sweep/baseline.json | 23 ++++ tools/eest-sweep/corpus.env | 14 +++ tools/eest-sweep/run.sh | 183 +++++++++++++++++++++++++++++ tools/eest-sweep/summarize.py | 106 +++++++++++++++++ 8 files changed, 520 insertions(+) create mode 100644 .github/workflows/eest-nightly.yml create mode 100644 tools/eest-sweep/README.md create mode 100644 tools/eest-sweep/baseline.json create mode 100644 tools/eest-sweep/corpus.env create mode 100755 tools/eest-sweep/run.sh create mode 100755 tools/eest-sweep/summarize.py diff --git a/.github/workflows/eest-nightly.yml b/.github/workflows/eest-nightly.yml new file mode 100644 index 00000000..7bc33772 --- /dev/null +++ b/.github/workflows/eest-nightly.yml @@ -0,0 +1,113 @@ +name: EEST Nightly Sweep + +# Runs the whole Ethereum execution-spec-tests state-test corpus through the mega-evm runner, +# nightly, under the unstable spec. +# +# Two questions, one pass. Does anything break — a fixture that trips a debug assertion or an +# internal invariant? And does the unstable spec still differ from the frozen spec it inherits +# from only where its own precision invariant permits? The second is what an unstable spec cannot +# get from fixtures alone: nobody has computed expected results for it, so the frozen spec is the +# only oracle available, and the invariant is what says when the two are allowed to disagree. +# +# Both are hard gates. Everything else — fixtures the runner declines before execution, +# differences the classifier accounts for — is reported and compared against a committed +# baseline, and drift there is a warning in the summary rather than a red run. +# +# Needs no secrets: it builds the repo and runs it against a public, hash-pinned corpus. + +on: + schedule: + # Nightly at 04:00 UTC, after the mutation sweep's 03:00 slot. + - cron: "0 4 * * *" + workflow_dispatch: + inputs: + target_spec: + description: "Spec under test" + type: string + default: "Rex7" + base_spec: + description: "Frozen spec to compare against" + type: string + default: "Rex6" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + sweep: + name: EEST differential sweep + runs-on: ubuntu-24.04 + # The sweep itself is minutes; the headroom is for a cold dependency build. + timeout-minutes: 90 + env: + TARGET_SPEC: ${{ inputs.target_spec || 'Rex7' }} + BASE_SPEC: ${{ inputs.base_spec || 'Rex6' }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Read pinned corpus + id: corpus + run: | + # shellcheck disable=SC1091 + . tools/eest-sweep/corpus.env + echo "release=$EEST_RELEASE" >> "$GITHUB_OUTPUT" + echo "sha256=$EEST_SHA256" >> "$GITHUB_OUTPUT" + + # Keyed on the corpus hash, not on the release name: the hash is what the sweep verifies, + # so a cache entry can never hold something the sweep would have rejected. + - name: Cache corpus + uses: actions/cache@v4 + with: + path: .eest-cache + key: eest-corpus-${{ steps.corpus.outputs.sha256 }} + + # `hivetests` is optimized *and* keeps debug assertions live, which is the point: the Rex7 + # gas-conservation cross-checks are `debug_assert!`s, and a release build would run the + # corpus without ever evaluating them. + - name: Build state-test + run: cargo build --profile hivetests -p state-test + + - name: Run sweep + id: sweep + run: | + tools/eest-sweep/run.sh \ + --no-build \ + --target-spec "$TARGET_SPEC" \ + --base-spec "$BASE_SPEC" \ + --cache-dir .eest-cache \ + --report-dir .eest-report + + - name: Write job summary + if: always() + run: | + if [ -f .eest-report/diff-report.json ]; then + python3 tools/eest-sweep/summarize.py \ + .eest-report/diff-report.json \ + --baseline tools/eest-sweep/baseline.json \ + >> "$GITHUB_STEP_SUMMARY" + else + echo "The sweep produced no report; see the job log." >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload report + if: always() + uses: actions/upload-artifact@v4 + with: + name: eest-sweep-report + path: | + .eest-report/diff-report.json + .eest-report/sweep.log + if-no-files-found: warn + retention-days: 30 diff --git a/.gitignore b/.gitignore index de945a46..914bfad3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,8 @@ # Python bytecode cache (scripts/) __pycache__/ + +# EEST sweep working directories (tools/eest-sweep/run.sh): the hash-pinned corpus +# archive and the reports a run produces. Both are regenerable. +/.eest-cache +/.eest-report diff --git a/AGENTS.md b/AGENTS.md index 69bf3e35..11100d47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,8 @@ Git submodules are required — clone with `--recursive` or run `git submodule u | `mega-evme` | `bin/mega-evme` | CLI tool for EVM execution (`run`, `tx`, `replay`) | | `mega-t8n` | `bin/mega-t8n` | Standalone state transition (t8n) tool | +The EEST corpus sweep that exercises the unstable spec against the whole Ethereum state-test suite lives in `tools/eest-sweep/` and runs nightly (`.github/workflows/eest-nightly.yml`). + ## Architecture ### Spec System (`MegaSpecId`) diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md new file mode 100644 index 00000000..dc05f18d --- /dev/null +++ b/tools/eest-sweep/README.md @@ -0,0 +1,74 @@ +# EEST corpus sweep + +Runs the whole [execution-spec-tests](https://github.com/ethereum/execution-spec-tests) state-test +corpus through the mega-evm runner, in one command: + +```bash +tools/eest-sweep/run.sh +``` + +That fetches the pinned fixture release, verifies its hash, unpacks the `state_tests` subtree, and +executes every fixture under the unstable spec **and** under the frozen spec it inherits from, +comparing the two. `.github/workflows/eest-nightly.yml` runs the same command nightly. + +## Why a differential sweep + +A state-test fixture pins what a transaction must produce — but only for a spec someone has already +computed an expectation for. The corpus records expectations for Ethereum forks, which mega-evm +maps onto `Equivalence`; for `Rex7` there is nothing to compare against, so executing the corpus +under it can only check that nothing crashes. + +The frozen spec supplies the missing oracle. `Rex7` states the conditions under which it may _not_ +differ from `Rex6` ([`docs/spec/upgrades/rex7.md`](../../docs/spec/upgrades/rex7.md), "Precision +invariant"), and read as a contrapositive that sentence classifies every disagreement: a difference +must come with evidence, read off the execution itself, that one of the invariant's three +hypotheses does not hold. A difference with no such evidence is a defect — in the implementation or +in the invariant. + +## What fails the run + +Two conditions, and only these two: + +- **`PANIC`** — a fixture tripped a debug assertion or an internal invariant. +- **`UNEXPLAINED`** — the two specs disagreed and nothing in either execution licenses it. + +Everything else is reported and does not fail: + +- **`PASS`** — the two specs agreed on every compared quantity. +- **`EXPLAINED`** — they disagreed, with evidence (a crossed resource limit, a frame that ended in + an exceptional halt, a `disableVolatileDataAccess` rejection). +- **`SKIPPED`** — neither spec executed the transaction, and both declined it identically. Most of + this class is the MegaETH intrinsic-gas surcharge putting an Ethereum fixture's gas limit below + what the transaction now costs. + +`baseline.json` records the tally at the time this sweep was written. The nightly compares against +it and warns in the job summary when the coverage numbers move — a corpus that half-unpacked, or a +change that pushed thousands of fixtures out of execution, is worth seeing even though it is not a +defect. Update it deliberately when a move is expected. + +## Options + +``` +--target-spec SPEC Spec under test (default: Rex7) +--base-spec SPEC Frozen spec to compare against (default: Rex6) +--mode diff|fill diff (default) executes both specs and classifies the differences. + fill executes the target spec only and recomputes each fixture's `post` + on a private copy — the older scan, kept because it exercises the + fixture-writing path that diff mode does not touch. +--corpus-dir DIR Use an already-unpacked `state_tests` tree instead of downloading. +--cache-dir DIR Where to keep the downloaded archive (default: .eest-cache). +--report-dir DIR Where to write the report and log (default: .eest-report). +--profile PROFILE Cargo profile (default: hivetests). +--no-build Use an already-built binary. +``` + +The default profile is `hivetests` rather than `release` or `dev` deliberately: it is optimized +_and_ keeps debug assertions live, so the Rex7 gas-conservation cross-checks actually run. A +release build would sweep the corpus without evaluating them; a `dev` build evaluates them at +roughly a tenth of the speed. + +## Bumping the corpus + +Edit `corpus.env` (release, archive name, sha256), re-run the sweep, and update `baseline.json` +from the new report. The hash is verified on every run, so a mismatch — a re-uploaded asset, a +mirror serving something else — fails loudly instead of silently changing what the sweep covers. diff --git a/tools/eest-sweep/baseline.json b/tools/eest-sweep/baseline.json new file mode 100644 index 00000000..671c29da --- /dev/null +++ b/tools/eest-sweep/baseline.json @@ -0,0 +1,23 @@ +{ + "targetSpec": "Rex7", + "baseSpec": "Rex6", + "total": 44023, + "classes": { + "PASS": 19611, + "EXPLAINED": 17363, + "UNEXPLAINED": 0, + "SKIPPED": 7049, + "PANIC": 0 + }, + "mechanisms": { + "destroyed_compute_gas": 10698, + "detention_in_force": 610, + "exceptional_halt": 11379 + }, + "explainedFields": { + "compute_gas_used": 17363 + }, + "fileErrors": [], + "skippedFiles": 5, + "flagged": [] +} diff --git a/tools/eest-sweep/corpus.env b/tools/eest-sweep/corpus.env new file mode 100644 index 00000000..c019bf23 --- /dev/null +++ b/tools/eest-sweep/corpus.env @@ -0,0 +1,14 @@ +# The EEST fixture release this sweep runs against. +# +# Pinned by version *and* content hash: a release asset that is re-uploaded, or a mirror that +# serves something else, must fail the sweep rather than silently change what it covers. Bumping +# the corpus is a deliberate edit of these three lines, and the tally it produces is the new +# baseline. +# +# `fixtures_stable` is the build that stops at the latest deployed Ethereum fork, which is what +# MegaETH's Equivalence baseline tracks; `fixtures_develop` would add unshipped forks the +# baseline does not claim to implement. +EEST_RELEASE="v5.4.0" +EEST_ARCHIVE="fixtures_stable.tar.gz" +EEST_SHA256="92cf1b47ad12fb27163261fc3c1cea5df72439cab507983d06b56c94f8741909" +EEST_URL_BASE="https://github.com/ethereum/execution-spec-tests/releases/download" diff --git a/tools/eest-sweep/run.sh b/tools/eest-sweep/run.sh new file mode 100755 index 00000000..0f140c7d --- /dev/null +++ b/tools/eest-sweep/run.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# +# Run the EEST state-test corpus through the mega-evm state-test runner. +# +# One command: fetch and verify the pinned fixture release, unpack its `state_tests` subtree, and +# execute every fixture. Two gates fail the run — a fixture that panics, and a difference between +# the spec under test and its frozen base that no MegaETH mechanism accounts for. Everything else +# (fixtures the runner declines, differences the classifier explains) is reported and does not +# fail. +# +# Usage: +# tools/eest-sweep/run.sh [options] +# +# --target-spec SPEC Spec under test (default: Rex7) +# --base-spec SPEC Frozen spec to compare against (default: Rex6) +# --mode diff|fill diff: execute under both specs and classify the differences (default). +# fill: execute under the target spec only and recompute each fixture's +# `post` in place, on a private copy. `diff` runs the target spec through +# the same execution path, so it already covers what `fill` scans for; +# `fill` remains available to exercise the fixture-writing path itself. +# --corpus-dir DIR Use an already-unpacked `state_tests` tree instead of downloading. +# --cache-dir DIR Where to keep the downloaded archive (default: .eest-cache). +# --report-dir DIR Where to write the report and log (default: .eest-report). +# --profile PROFILE Cargo profile to build with (default: hivetests — optimized, with debug +# assertions live, which is what makes the conservation cross-checks fire). +# --no-build Use an already-built binary. +# -h, --help Show this message. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=tools/eest-sweep/corpus.env +source "$REPO_ROOT/tools/eest-sweep/corpus.env" + +TARGET_SPEC="Rex7" +BASE_SPEC="Rex6" +MODE="diff" +CORPUS_DIR="" +CACHE_DIR="$REPO_ROOT/.eest-cache" +REPORT_DIR="$REPO_ROOT/.eest-report" +PROFILE="hivetests" +BUILD=1 + +while [ $# -gt 0 ]; do + case "$1" in + --target-spec) TARGET_SPEC="$2"; shift 2 ;; + --base-spec) BASE_SPEC="$2"; shift 2 ;; + --mode) MODE="$2"; shift 2 ;; + --corpus-dir) CORPUS_DIR="$2"; shift 2 ;; + --cache-dir) CACHE_DIR="$2"; shift 2 ;; + --report-dir) REPORT_DIR="$2"; shift 2 ;; + --profile) PROFILE="$2"; shift 2 ;; + --no-build) BUILD=0; shift ;; + # Print the header comment block and stop at the first line that is not one, so the help text + # can never run past it into the script body. + -h|--help) sed -n '2,${/^#/!q;s/^# \{0,1\}//p;}' "${BASH_SOURCE[0]}"; exit 0 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac +done + +case "$MODE" in + diff|fill) ;; + *) echo "--mode must be 'diff' or 'fill', got '$MODE'" >&2; exit 2 ;; +esac + +# `sha256sum` on Linux, `shasum -a 256` on macOS. +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | cut -d' ' -f1 + else + shasum -a 256 "$1" | cut -d' ' -f1 + fi +} + +mkdir -p "$REPORT_DIR" + +# --- corpus ----------------------------------------------------------------------------------- + +if [ -z "$CORPUS_DIR" ]; then + mkdir -p "$CACHE_DIR" + ARCHIVE="$CACHE_DIR/$EEST_RELEASE-$EEST_ARCHIVE" + if [ ! -f "$ARCHIVE" ]; then + echo "==> downloading EEST $EEST_RELEASE / $EEST_ARCHIVE" + # Download beside the target and rename on success, so an interrupted download can never be + # mistaken for a cached archive on the next run. + curl --fail --location --show-error --silent \ + --output "$ARCHIVE.part" \ + "$EEST_URL_BASE/$EEST_RELEASE/$EEST_ARCHIVE" + mv "$ARCHIVE.part" "$ARCHIVE" + fi + + ACTUAL="$(sha256_of "$ARCHIVE")" + if [ "$ACTUAL" != "$EEST_SHA256" ]; then + echo "corpus hash mismatch for $ARCHIVE" >&2 + echo " expected $EEST_SHA256" >&2 + echo " actual $ACTUAL" >&2 + echo "Delete the cached archive and re-run, or update tools/eest-sweep/corpus.env." >&2 + exit 1 + fi + echo "==> corpus hash verified: $EEST_SHA256" + + CORPUS_DIR="$CACHE_DIR/$EEST_RELEASE/state_tests" + if [ ! -d "$CORPUS_DIR" ]; then + echo "==> unpacking state_tests" + mkdir -p "$CACHE_DIR/$EEST_RELEASE" + # Only `state_tests` is unpacked: the runner reads the state-test format, and the archive's + # blockchain-test subtrees are several times larger. + tar -xzf "$ARCHIVE" -C "$CACHE_DIR/$EEST_RELEASE" --strip-components=1 fixtures/state_tests + fi +fi + +if [ ! -d "$CORPUS_DIR" ]; then + echo "corpus directory not found: $CORPUS_DIR" >&2 + exit 1 +fi +FIXTURE_COUNT="$(find "$CORPUS_DIR" -name '*.json' | wc -l | tr -d ' ')" +echo "==> corpus: $CORPUS_DIR ($FIXTURE_COUNT fixture files)" + +# --- binary ----------------------------------------------------------------------------------- + +BIN="$REPO_ROOT/target/$PROFILE/state-test" +if [ "$BUILD" -eq 1 ]; then + echo "==> building state-test (profile: $PROFILE)" + (cd "$REPO_ROOT" && cargo build --profile "$PROFILE" -p state-test) +fi +if [ ! -x "$BIN" ]; then + echo "state-test binary not found at $BIN" >&2 + exit 1 +fi + +# --- run -------------------------------------------------------------------------------------- + +LOG="$REPORT_DIR/sweep.log" +STATUS=0 +if [ "$MODE" = "diff" ]; then + echo "==> differential sweep: $TARGET_SPEC vs $BASE_SPEC" + "$BIN" \ + --bench-spec "$TARGET_SPEC" \ + --diff-spec "$BASE_SPEC" \ + --diff-report "$REPORT_DIR/diff-report.json" \ + "$CORPUS_DIR" >"$LOG" 2>&1 || STATUS=$? +else + # `--fill` rewrites each fixture in place, so it runs on a private copy and never touches the + # cached corpus other runs share. + WORK="$REPORT_DIR/fill-corpus" + echo "==> fill sweep under $TARGET_SPEC (private copy at $WORK)" + rm -rf "$WORK" + mkdir -p "$WORK" + cp -R "$CORPUS_DIR" "$WORK/" + "$BIN" \ + --fill --force --keep-going \ + --bench-spec "$TARGET_SPEC" \ + "$WORK" >"$LOG" 2>&1 || STATUS=$? +fi + +# The tally, plus anything that needs a human. Per-unit `ERR` lines are the expected noise floor +# (thousands of fixtures the runner declines before execution) and are left in the log only. +grep -vE '^ERR\b' "$LOG" | tail -n 60 +echo "==> full log: $LOG" + +if [ "$MODE" = "diff" ]; then + echo "==> report: $REPORT_DIR/diff-report.json" + # The CLI already fails on a panic or an unexplained difference, and on nothing else — fixtures + # it declines and differences it explains leave it at 0. Pass that verdict straight through + # rather than re-deriving it from parsed output. + exit "$STATUS" +fi + +# `--fill` has no notion of an expected failure: it exits non-zero for every unit it could not +# fill, and thousands of them are fixtures neither spec would execute. Re-derive the gate from the +# tally so `fill` mode fails on the same two conditions `diff` mode does. +TALLY="$(grep -m1 '^Fill tally:' "$LOG" || true)" +if [ -z "$TALLY" ]; then + echo "fill run produced no tally line; treating as a failure" >&2 + exit 1 +fi +field() { echo "$TALLY" | tr ' ' '\n' | grep "^$1=" | cut -d= -f2; } +PANICS="$(field PANIC)" +FILE_ERRS="$(field FILE_ERR)" +if [ "${PANICS:-0}" -gt 0 ] || [ "${FILE_ERRS:-0}" -gt 0 ]; then + echo "gate failed: PANIC=$PANICS FILE_ERR=$FILE_ERRS" >&2 + exit 1 +fi +exit 0 diff --git a/tools/eest-sweep/summarize.py b/tools/eest-sweep/summarize.py new file mode 100755 index 00000000..a221e8f1 --- /dev/null +++ b/tools/eest-sweep/summarize.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Render an EEST sweep report as a markdown summary and compare it to the committed baseline. + +The sweep's own exit code is the gate: it fails on a fixture that panicked and on a difference no +MegaETH mechanism accounts for, and on nothing else. This script never fails a run. What it adds +is drift: the counts of fixtures the runner declined and of differences it explained are the +sweep's coverage, and a silent move in either — a corpus that half-unpacked, a change that pushed +thousands of fixtures out of execution — is worth seeing even though it is not a defect. +""" + +import argparse +import json +import sys + +CLASSES = ["PASS", "EXPLAINED", "UNEXPLAINED", "SKIPPED", "PANIC"] + + +def load(path): + with open(path, encoding="utf-8") as f: + return json.load(f) + + +def delta(current, base): + d = current - base + if d == 0: + return f"{current}" + return f"{current} ({d:+d})" + + +def main(): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("report", help="diff-report.json produced by the sweep") + ap.add_argument("--baseline", help="committed baseline tally to compare against") + ap.add_argument("--out", help="write the markdown summary here (default: stdout)") + args = ap.parse_args() + + report = load(args.report) + base = load(args.baseline) if args.baseline else None + base_classes = (base or {}).get("classes", {}) + + lines = [ + f"## EEST sweep — {report['targetSpec']} vs {report['baseSpec']}", + "", + f"{report['total']} units judged" + + (f", {report['skippedFiles']} file(s) skipped by filename" if report.get("skippedFiles") else "") + + ".", + "", + "| class | units |", + "|---|--:|", + ] + for name in CLASSES: + count = report["classes"].get(name, 0) + cell = delta(count, base_classes[name]) if name in base_classes else str(count) + lines.append(f"| {name} | {cell} |") + + if report.get("mechanisms"): + lines += ["", "| mechanism (explained differences) | units |", "|---|--:|"] + for name, count in sorted(report["mechanisms"].items()): + lines.append(f"| `{name}` | {count} |") + + if report.get("explainedFields"): + lines += ["", "| disagreeing quantities | units |", "|---|--:|"] + for shape, count in sorted(report["explainedFields"].items()): + lines.append(f"| `{shape}` | {count} |") + + flagged = report.get("flagged", []) + if flagged: + lines += ["", "### Flagged units", "", "| class | fixture | quantities | detail |", "|---|---|---|---|"] + # Cap the table: an unexplained class in the thousands is one finding to investigate, not + # thousands of rows to scroll. The full list is in the uploaded report. + for item in flagged[:50]: + fixture = f"{item['path'].split('state_tests/')[-1]}::{item['name']}" + lines.append( + f"| {item['class']} | `{fixture[:160]}` | `{','.join(item['fields'])}` |" + f" {(item.get('detail') or '-')[:160]} |" + ) + if len(flagged) > 50: + lines.append(f"| … | {len(flagged) - 50} more in the uploaded report | | |") + + if report.get("fileErrors"): + lines += ["", "### Files the sweep could not judge", ""] + for err in report["fileErrors"][:20]: + lines.append(f"- `{err[:200]}`") + + if base_classes: + drifted = [n for n in CLASSES if n in base_classes and report["classes"].get(n, 0) != base_classes[n]] + lines += [""] + if drifted: + lines.append( + "> :warning: Coverage drifted from the committed baseline in: " + + ", ".join(f"`{n}`" for n in drifted) + + ". Not a failure — update `tools/eest-sweep/baseline.json` if the move is expected." + ) + else: + lines.append("> Coverage matches the committed baseline exactly.") + + text = "\n".join(lines) + "\n" + if args.out: + with open(args.out, "w", encoding="utf-8") as f: + f.write(text) + else: + sys.stdout.write(text) + + +if __name__ == "__main__": + main() From 8a86fc4cc30e1a38059cffd22887a3ac94db416a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 12:31:15 +0800 Subject: [PATCH 086/208] fix(state-test): make the differential gate's evidence unforgeable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The classifier decides whether a Rex7/Rex6 difference is licensed, so what it accepts as evidence is the whole gate. Seven ways in, closed. The fixture is the input under test, and two of the observations came out of revert-payload bytes: a contract that wrote `MegaLimitExceeded`'s four bytes claimed a crossed resource limit, which licenses a difference on *every* compared quantity. Observations now carry a `Provenance`, and only ones read off the execution — the EVM's verdict on a frame, the typed halt reason, a tracker's own counter — can falsify a hypothesis. That leaves the `disableVolatileDataAccess` hypothesis with no producer, since telling a guard rejection from a contract writing the same bytes needs a latch the runner cannot read without changing the execution; such a difference is now reported rather than licensed. The destroyed compute-gas remainder is derived, from a conservation law over the transaction's envelope, not observed. A missing term in that law yields a non-zero remainder with no halt behind it — so licensing the difference it causes made that defect its own alibi. It now needs the halted frame the inspector finds. Over the corpus this moves nothing but the evidence: all 17363 explained differences now rest on a frame the EVM actually finished, where 11379 did before. A halt was classified by whether its `Debug` rendering started with `Base`, which handed `SystemTxInvalidCallee` and every variant a later spec adds the standing of a crossed limit. It is matched variant by variant now, with no catch-all arm. The invariant that licenses a difference is Rex7's and names Rex6, so `DiffSpecs::new` accepts that pair and refuses the rest instead of judging them by a licence they were never granted; the workflow drops its spec inputs. A unit is a family of transactions, one per vector its `post` names. `TestUnit::vectors` enumerates them and diff, fill and bench each run all of them — `--fill --force` was collapsing the `post` map to a single `{0,0,0}` entry, deleting the other vectors' expectations. v5.4 has no multi-vector unit, so the corpus tally is unchanged. Finally, two ways to pass by reaching nothing. The fixture walk dropped entries it could not read, turning a permission error into a smaller corpus and a green run; they are reported and fail the gate. And an empty tally is truthful and meaningless, so every mode now fails when it judged no unit, as does `run.sh`. Corpus extraction lands via a scratch directory and a hash stamp written last, so an interrupted unpack cannot be swept as a whole tree. --- .github/workflows/eest-nightly.yml | 28 +- .gitignore | 2 + bin/mega-evme/src/replay/fixture.rs | 19 +- crates/mega-state-test/AGENTS.md | 11 +- crates/mega-state-test/src/diff.rs | 564 ++++++++++++++---- crates/mega-state-test/src/runner.rs | 215 +++++-- crates/mega-state-test/src/types/test.rs | 16 +- crates/mega-state-test/src/types/test_unit.rs | 24 +- .../mega-state-test/src/types/transaction.rs | 2 +- crates/mega-state-test/tests/diff_mode.rs | 311 +++++++++- .../mega-state-test/tests/dump_roundtrip.rs | 10 +- crates/mega-state-test/tests/hardening.rs | 19 +- crates/mega-state-test/tests/replay_corpus.rs | 9 +- crates/state-test/README.md | 14 +- crates/state-test/src/main.rs | 91 ++- crates/state-test/tests/cli_exit.rs | 95 ++- tools/eest-sweep/baseline.json | 2 +- tools/eest-sweep/run.sh | 32 +- 18 files changed, 1213 insertions(+), 251 deletions(-) diff --git a/.github/workflows/eest-nightly.yml b/.github/workflows/eest-nightly.yml index 7bc33772..538e9751 100644 --- a/.github/workflows/eest-nightly.yml +++ b/.github/workflows/eest-nightly.yml @@ -19,16 +19,11 @@ on: schedule: # Nightly at 04:00 UTC, after the mutation sweep's 03:00 slot. - cron: "0 4 * * *" + # No spec inputs. The comparison is decided by Rex7's precision invariant, which relates Rex7 to + # Rex6 and states nothing about any other pair, so there is nothing here to choose: the runner + # refuses every other pair. When a later spec becomes the unstable one, its own invariant has to + # be written into the classifier, and the pair below changes with it. workflow_dispatch: - inputs: - target_spec: - description: "Spec under test" - type: string - default: "Rex7" - base_spec: - description: "Frozen spec to compare against" - type: string - default: "Rex6" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -44,8 +39,8 @@ jobs: # The sweep itself is minutes; the headroom is for a cold dependency build. timeout-minutes: 90 env: - TARGET_SPEC: ${{ inputs.target_spec || 'Rex7' }} - BASE_SPEC: ${{ inputs.base_spec || 'Rex6' }} + TARGET_SPEC: "Rex7" + BASE_SPEC: "Rex6" steps: - uses: actions/checkout@v4 with: @@ -65,8 +60,15 @@ jobs: echo "release=$EEST_RELEASE" >> "$GITHUB_OUTPUT" echo "sha256=$EEST_SHA256" >> "$GITHUB_OUTPUT" - # Keyed on the corpus hash, not on the release name: the hash is what the sweep verifies, - # so a cache entry can never hold something the sweep would have rejected. + # Keyed on the corpus hash, not on the release name, so an entry restored here always + # belongs to the release the sweep is pinned to. + # + # The key is not what makes the entry trustworthy. The cache holds both the archive and the + # tree unpacked from it, and only the archive is hash-verified on every run; a tree saved + # from a cancelled job would be restored intact and swept as though it were whole. What + # rules that out is on the other side: `run.sh` unpacks into a scratch directory, moves it + # into place in one step, and writes a stamp naming this archive's hash only after the + # extraction finished. A tree without that stamp is discarded and unpacked again. - name: Cache corpus uses: actions/cache@v4 with: diff --git a/.gitignore b/.gitignore index 914bfad3..b30e0076 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ __pycache__/ # archive and the reports a run produces. Both are regenerable. /.eest-cache /.eest-report +# `--report-dir` names the directory, so a run that uses a different one leaves its own. +/.eest-report-* diff --git a/bin/mega-evme/src/replay/fixture.rs b/bin/mega-evme/src/replay/fixture.rs index ce203422..b8e4df40 100644 --- a/bin/mega-evme/src/replay/fixture.rs +++ b/bin/mega-evme/src/replay/fixture.rs @@ -34,7 +34,10 @@ use op_alloy_consensus::OpTxEnvelope; use op_alloy_rpc_types::Transaction; use state_test::{ runner::{execute_unit_collect, execution_status, halt_reason}, - types::{AccountInfo, Env, MegaEnv, SpecName, Test, TestSuite, TestUnit, TransactionParts}, + types::{ + AccountInfo, Env, MegaEnv, SpecName, Test, TestSuite, TestUnit, TransactionParts, + TxPartIndices, + }, }; use super::{ReplayError, Result}; @@ -225,7 +228,10 @@ where /// Re-execute the isolated unit through `state-test`, cross-check it against the /// observed replay outcome, fill the `post` expectation, and write the fixture. pub(crate) fn finalize_and_write(draft: FixtureDraft, path: &std::path::Path) -> Result<()> { - let executed = execute_unit_collect(&draft.unit, &draft.spec) + // A replayed on-chain transaction is one transaction, so the fixture it produces has exactly + // one vector and it is index zero. + let indexes = TxPartIndices { data: 0, gas: 0, value: 0 }; + let executed = execute_unit_collect(&draft.unit, indexes, &draft.spec) .map_err(|e| ReplayError::Other(format!("fixture self-execution failed: {e}")))?; // Cross-check the isolated execution against the full replay. These values @@ -272,8 +278,13 @@ pub(crate) fn finalize_and_write(draft: FixtureDraft, path: &std::path::Path) -> let mut unit = draft.unit; unit.out = executed.output.clone(); - let test = - Test::for_dump(executed.state_root, executed.logs_root, executed.gas_used, executed.status); + let test = Test::for_dump( + indexes, + executed.state_root, + executed.logs_root, + executed.gas_used, + executed.status, + ); unit.post = BTreeMap::from([(draft.spec, vec![test])]); let suite = TestSuite(BTreeMap::from([(draft.name, unit)])); diff --git a/crates/mega-state-test/AGENTS.md b/crates/mega-state-test/AGENTS.md index 9f705e49..5a4542af 100644 --- a/crates/mega-state-test/AGENTS.md +++ b/crates/mega-state-test/AGENTS.md @@ -20,12 +20,19 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Failure debugging path can re-run with tracer context for inspection. - Parallel execution uses shared queue and atomic counters with optional single-thread mode. - Differential classification is evidence-based, never a list of fixtures allowed to differ: every `Mechanism` is a fact read off an execution, and the hypothesis it falsifies is what licenses a difference. +- Only an execution-provenance observation licenses anything. The fixture is the input under test, so a `Mechanism` read out of revert-payload bytes is reported and never falsifies a hypothesis; a derived quantity (the Rex7 destroyed remainder) needs an independent witness rather than certifying itself. +- A differential run is defined for exactly one spec pair, the one whose precision invariant the classifier encodes (`DiffSpecs::new`). There is no general two-spec comparator. +- A unit is a family of transactions, one per vector its `post` names (`TestUnit::vectors`). Diff, fill and bench each enumerate them; nothing takes index `{0,0,0}` and calls it the unit. +- Every mode fails when it judged no unit at all: an empty tally is truthful and meaningless, and a corpus that never arrived must not read as a pass. - Corpus drivers keep going per unit (`fill_test_suite_keep_going`, `diff_test_suite`) and record a unit's failure or panic rather than ending the file. - BaseFeeVault state changes are pruned as MegaETH-specific normalization. - The SALT bucket hasher comes from `mega_evm::AHashBucketHasher` (via the `test-utils` feature); never introduce a standalone salt/hasher dependency. ## ANTI-PATTERNS - Do not explain a differential disagreement with a fixture allowlist; add a `Mechanism` that reads the evidence instead, and state which hypothesis it falsifies. +- Do not let a `Mechanism` inferred from bytes the fixture could have written falsify a hypothesis; `Mechanism::provenance` records where an observation came from and the licensing rule follows it. +- Do not classify a halt by matching its `Debug` rendering; match the `MegaHaltReason` variants with no catch-all arm, so a new variant has to be decided rather than defaulted. +- Do not drop an entry the fixture-discovery walk could not read; an unreadable directory is a hole in coverage, not an empty one. - Do not spread exception matching logic across multiple files. - Keep it centralized to avoid drift. - Do not bypass `compute_test_roots` when changing validation outputs. @@ -36,7 +43,9 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Add/adjust skip policy: `runner.rs::skip_test`. - Change validation semantics for roots/output/exception: `runner.rs::{validate_exception,validate_output,check_evm_execution}`. - Change worker behavior or fail-fast policy: `runner.rs::{run_test_worker,run,TestRunnerConfig}`. -- Change what a differential run compares or what licenses a difference: `diff.rs::{DiffField,Mechanism,judge}`. +- Change what a differential run compares or what licenses a difference: `diff.rs::{DiffField,Mechanism,Provenance,halt_kind,judge}`. +- Change which spec pair a differential run accepts: `diff.rs::DiffSpecs::new`. +- Change how a unit's transaction vectors are enumerated: `types/test_unit.rs::TestUnit::vectors`. - Change the corpus sweep or its CI gates: `tools/eest-sweep/` and `.github/workflows/eest-nightly.yml`. - Update JSON schema mapping for test fixtures: `src/types/*` and deserializer modules. - Change CLI flags or path handling: `crates/state-test/src/main.rs`. diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index 20ccb6b4..6bc6d3e4 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -24,13 +24,34 @@ //! //! A disagreement with no such evidence is [`DiffClass::Unexplained`]: either the implementation //! deviates from the spec, or the spec's invariant is wrong. Both are findings. +//! +//! # What counts as evidence +//! +//! The fixture is the input under test, so nothing the fixture authors may license a difference — +//! otherwise a contract that writes four chosen bytes to its revert buffer would buy itself an +//! exemption from the whole comparison. Every observation the classifier makes therefore carries +//! a [`Provenance`], and only [`Provenance::Execution`] observations — the EVM's own verdict on a +//! frame, the typed halt reason, the `MegaETH` trackers' own counters — can falsify a hypothesis. +//! Observations read out of revert-payload bytes are [`Provenance::Payload`]: they are reported, +//! because they are what a human triaging a flagged unit wants to see, and they license nothing. +//! `test_only_execution_provenance_licenses` holds that line for mechanisms added later. +//! +//! One hypothesis has no producer under that rule. A `disableVolatileDataAccess` rejection is +//! visible only as revert-payload bytes: `MegaETH` writes the guard's payload into the frame +//! result, and a contract can write the same bytes with a plain `REVERT`. Telling the two apart +//! needs a signal the runner cannot read — the trackers' latch is `pub(crate)`, and the one +//! public entry point that reports it (`AdditionalLimit::check_limit`) latches as a side effect +//! and would change the execution under observation. So a difference that only a guard rejection +//! explains is reported for a human rather than licensed, which is the safe direction: the gate +//! over-reports instead of granting an exemption on the strength of bytes the fixture chose. use crate::{ panic_capture, runner::{ configure_max_blobs, execution_status, external_envs_for, find_all_json_tests, halt_reason, inject_block_hashes, prune_base_fee_vault_changes, resolve_chain_id, - set_cfg_spec_and_mainnet_gas_params, skip_test, TestError, TestErrorKind, UnitStatus, + set_cfg_spec_and_mainnet_gas_params, skip_test, vector_label, FixtureScan, TestError, + TestErrorKind, UnitStatus, }, types::{tx_env_at, SpecName, TestSuite, TestUnit, TxPartIndices}, utils::{log_rlp_hash, state_merkle_trie_root}, @@ -39,7 +60,7 @@ use indicatif::{ProgressBar, ProgressDrawTarget}; use mega_evm::{ alloy_sol_types::SolError, revm::{ - context::cfg::CfgEnv, + context::{cfg::CfgEnv, result::ExecutionResult}, database, database_interface::DatabaseCommit, handler::FrameResult, @@ -47,8 +68,8 @@ use mega_evm::{ interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInput}, primitives::{Bytes, B256}, }, - MegaContext, MegaEvm, MegaLimitExceeded, MegaTransaction, MegaTransactionNew as _, - VOLATILE_DATA_ACCESS_DISABLED_SELECTOR, + MegaContext, MegaEvm, MegaHaltReason, MegaLimitExceeded, MegaTransaction, + MegaTransactionNew as _, VOLATILE_DATA_ACCESS_DISABLED_SELECTOR, }; use std::{ collections::BTreeMap, @@ -73,6 +94,17 @@ pub enum Hypothesis { NoDisabledVolatileReject, } +/// Where an observation came from, and therefore whether the fixture could have authored it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Provenance { + /// Read from the execution itself: the EVM's verdict on a frame, the typed halt reason, or a + /// `MegaETH` tracker's own counter. A fixture can cause such an observation — that is what + /// running it means — but it cannot fabricate one without the machinery actually firing. + Execution, + /// Inferred from revert-payload bytes, which any contract can write with a plain `REVERT`. + Payload, +} + /// A `MegaETH` mechanism observed in a differential run. /// /// Each variant is a fact read off an execution, not an interpretation of one. A variant that @@ -91,8 +123,11 @@ pub enum Mechanism { /// The rescue is the tell for a limit exceed whose halt the outer accounting has already /// rewritten — a failed deposit, for instance, reports its whole gas limit. GasRescued, - /// A frame reverted with the frame-local `MegaLimitExceeded` payload. - FrameLocalLimitRevert, + /// A frame reverted carrying the `MegaLimitExceeded` selector. + /// + /// Reported, never licensing: `MegaETH` writes that payload into a frame result, and so can + /// any contract. See the module docs on what counts as evidence. + LimitRevertPayload, /// A frame ended in an exceptional halt. /// /// Counted at the frame the EVM finished, so it covers an inner frame the caller absorbed, a @@ -101,9 +136,18 @@ pub enum Mechanism { ExceptionalHalt, /// Rex7 booked a destroyed compute-gas remainder: an envelope was lost without being /// executed. + /// + /// Recorded, never licensing. The remainder is not observed but *derived*, from a + /// conservation law over the transaction's whole envelope, so a missing term in that law + /// produces a non-zero remainder with no halt behind it. Letting it license the difference it + /// causes would make the one number a defect would move into that defect's own alibi. The + /// halt it claims has an independent witness — the frame the EVM finished — and that witness + /// is what licenses. DestroyedComputeGas, - /// A `disableVolatileDataAccess` guard rejected an opcode. - VolatileAccessDisabled, + /// A frame reverted carrying the `VolatileDataAccessDisabled` selector. + /// + /// Reported, never licensing, for the same reason as [`Mechanism::LimitRevertPayload`]. + VolatileDisabledPayload, /// The two specs recorded different volatile-data access marks. /// /// Rex7 moves the beneficiary / oracle mark to the point where the target account is loaded, @@ -121,17 +165,34 @@ pub enum Mechanism { impl Mechanism { /// The invariant hypothesis this mechanism falsifies, if any. + /// + /// Only an [`Provenance::Execution`] observation may return `Some`; see + /// [`Mechanism::provenance`] and the module docs. pub const fn falsifies(self) -> Option { + match self { + Self::ResourceLimitHalt | Self::DetentionHalt | Self::GasRescued => { + Some(Hypothesis::WithinLimits) + } + Self::ExceptionalHalt => Some(Hypothesis::NoExceptionalHalt), + Self::LimitRevertPayload | + Self::VolatileDisabledPayload | + Self::DestroyedComputeGas | + Self::DetentionMarkDiff | + Self::DetentionInForce => None, + } + } + + /// Whether this observation is read off the execution or off bytes the fixture chose. + pub const fn provenance(self) -> Provenance { match self { Self::ResourceLimitHalt | Self::DetentionHalt | Self::GasRescued | - Self::FrameLocalLimitRevert => Some(Hypothesis::WithinLimits), - Self::ExceptionalHalt | Self::DestroyedComputeGas => { - Some(Hypothesis::NoExceptionalHalt) - } - Self::VolatileAccessDisabled => Some(Hypothesis::NoDisabledVolatileReject), - Self::DetentionMarkDiff | Self::DetentionInForce => None, + Self::ExceptionalHalt | + Self::DestroyedComputeGas | + Self::DetentionMarkDiff | + Self::DetentionInForce => Provenance::Execution, + Self::LimitRevertPayload | Self::VolatileDisabledPayload => Provenance::Payload, } } @@ -141,16 +202,46 @@ impl Mechanism { Self::ResourceLimitHalt => "resource_limit_halt", Self::DetentionHalt => "detention_halt", Self::GasRescued => "gas_rescued", - Self::FrameLocalLimitRevert => "frame_local_limit_revert", + Self::LimitRevertPayload => "limit_revert_payload", Self::ExceptionalHalt => "exceptional_halt", Self::DestroyedComputeGas => "destroyed_compute_gas", - Self::VolatileAccessDisabled => "volatile_access_disabled", + Self::VolatileDisabledPayload => "volatile_disabled_payload", Self::DetentionMarkDiff => "detention_mark_diff", Self::DetentionInForce => "detention_in_force", } } } +/// What kind of halt a `MegaHaltReason` is. +/// +/// Classified by matching the reason's own variants, with no catch-all arm: a `MegaHaltReason` +/// added later fails to compile here until someone decides what it means for the invariant. +/// The rule it replaces — "a halt whose `Debug` form does not start with `Base` is a resource +/// limit" — granted every future variant, and today's `SystemTxInvalidCallee`, the standing of a +/// crossed resource limit, which licenses a difference on any quantity at all. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum HaltKind { + /// One of the four `MegaETH` per-transaction resource limits. + ResourceLimit, + /// The detained compute-gas limit. + Detention, + /// A halt that is not a resource limit: the inherited EVM's own halts, and the + /// `MegaETH`-specific halts that are not metering failures. + Other, +} + +/// Classifies a halt reason for the [`Mechanism`] it produces. +pub const fn halt_kind(reason: &MegaHaltReason) -> HaltKind { + match reason { + MegaHaltReason::DataLimitExceeded { .. } | + MegaHaltReason::KVUpdateLimitExceeded { .. } | + MegaHaltReason::ComputeGasLimitExceeded { .. } | + MegaHaltReason::StateGrowthLimitExceeded { .. } => HaltKind::ResourceLimit, + MegaHaltReason::VolatileDataAccessOutOfGas { .. } => HaltKind::Detention, + MegaHaltReason::Base(_) | MegaHaltReason::SystemTxInvalidCallee { .. } => HaltKind::Other, + } +} + /// A quantity the precision invariant requires the two specs to agree on. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum DiffField { @@ -220,7 +311,12 @@ pub struct SpecOutcome { /// `success` / `revert` / `halt`. pub status: String, /// Halt reason (`Debug` form) when the status is `halt`. + /// + /// Compared as a quantity and printed in reports. What the halt *means* to the classifier is + /// [`SpecOutcome::halt_kind`], read off the typed reason rather than off this rendering. pub halt_reason: Option, + /// What kind of halt the transaction ended in, when it halted. + pub halt_kind: Option, /// Transaction output bytes, if any. pub output: Option, /// Reported compute-gas total. @@ -250,14 +346,10 @@ impl SpecOutcome { /// Mechanisms visible in this single execution. fn mechanisms(&self) -> Vec { let mut found = Vec::new(); - match self.halt_reason.as_deref() { - Some(r) if r.starts_with("VolatileDataAccessOutOfGas") => { - found.push(Mechanism::DetentionHalt); - } - // Every MegaETH-specific halt is a resource limit; `Base(..)` wraps the inherited - // EVM's own halts, which are not. - Some(r) if !r.starts_with("Base") => found.push(Mechanism::ResourceLimitHalt), - _ => {} + match self.halt_kind { + Some(HaltKind::ResourceLimit) => found.push(Mechanism::ResourceLimitHalt), + Some(HaltKind::Detention) => found.push(Mechanism::DetentionHalt), + Some(HaltKind::Other) | None => {} } if self.rescued_gas > 0 { found.push(Mechanism::GasRescued); @@ -275,11 +367,11 @@ impl SpecOutcome { if frames.halted > 0 { found.push(Mechanism::ExceptionalHalt); } - if frames.limit_exceeded_reverts > 0 { - found.push(Mechanism::FrameLocalLimitRevert); + if frames.limit_revert_payloads > 0 { + found.push(Mechanism::LimitRevertPayload); } - if frames.volatile_disabled_reverts > 0 { - found.push(Mechanism::VolatileAccessDisabled); + if frames.volatile_disabled_payloads > 0 { + found.push(Mechanism::VolatileDisabledPayload); } } found @@ -293,14 +385,19 @@ impl SpecOutcome { /// All three falsify a hypothesis of the precision invariant and none of them is visible from the /// outside, so the classifier collects them from the frames themselves when the cheap evidence /// runs out. +/// +/// The three counters do not carry equal weight. [`FrameEvidence::halted`] is the EVM's own +/// verdict on the frame; the other two are what the frame put in its revert buffer, which a +/// contract writes as freely as `MegaETH` does. They are counted for the report and classified as +/// [`Provenance::Payload`], so they never license a difference. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct FrameEvidence { /// Frames whose final result was an exceptional halt. pub halted: u32, - /// Frames that reverted with the frame-local `MegaLimitExceeded` payload. - pub limit_exceeded_reverts: u32, - /// Frames that reverted with the `VolatileDataAccessDisabled` payload. - pub volatile_disabled_reverts: u32, + /// Frames that reverted carrying the `MegaLimitExceeded` selector, whoever wrote it. + pub limit_revert_payloads: u32, + /// Frames that reverted carrying the `VolatileDataAccessDisabled` selector, whoever wrote it. + pub volatile_disabled_payloads: u32, } /// Read-only inspector that records each frame's final result. @@ -339,12 +436,15 @@ impl Inspector for FrameEvidenceInspector { if !result.result.is_revert() { return; } + // A selector match says what the frame's revert buffer starts with and nothing more: + // `REVERT` copies whatever memory the contract points it at. Both counters are recorded + // as claims, for a human reading a flagged unit, and are never treated as evidence. match result.output.get(..4) { Some(s) if s == MegaLimitExceeded::SELECTOR => { - self.evidence.limit_exceeded_reverts += 1; + self.evidence.limit_revert_payloads += 1; } Some(s) if s == VOLATILE_DATA_ACCESS_DISABLED_SELECTOR => { - self.evidence.volatile_disabled_reverts += 1; + self.evidence.volatile_disabled_payloads += 1; } _ => {} } @@ -406,7 +506,13 @@ pub struct UnitDiff { } /// Which specs a differential run compares. -#[derive(Debug, Clone, Copy)] +/// +/// Only [`DiffSpecs::SUPPORTED`] can be constructed. The classifier is not a general-purpose +/// two-spec comparator: every rule in it is a reading of one sentence, the Rex7 precision +/// invariant, which relates Rex7 to Rex6 and says nothing about any other pair. Pointed at +/// Rex5-against-Rex4 it would apply Rex7's licence to a pair that never had one — deciding, from +/// mechanisms that are not evidence for anything there, that a difference is fine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DiffSpecs { /// The spec under test, normally the unstable one. pub target: SpecName, @@ -414,13 +520,40 @@ pub struct DiffSpecs { pub base: SpecName, } -/// Runs one unit under both specs and classifies the result. +impl DiffSpecs { + /// The one pair the classifier has an invariant for: Rex7 against Rex6. + pub const SUPPORTED: (SpecName, SpecName) = (SpecName::Rex7, SpecName::Rex6); + + /// Builds the spec pair, rejecting any pair the classifier has no invariant for. + /// + /// # Errors + /// + /// Returns a message naming the supported pair when `target` / `base` is not it. + pub fn new(target: SpecName, base: SpecName) -> Result { + let (want_target, want_base) = Self::SUPPORTED; + if (target, base) != (want_target, want_base) { + return Err(format!( + "a differential run is only defined for {want_target:?} against {want_base:?}, \ + not {target:?} against {base:?}: the precision invariant that decides when a \ + difference is licensed is {want_target:?}'s, and no other pair states one" + )); + } + Ok(Self { target, base }) + } +} + +/// Runs one unit's transaction vector under both specs and classifies the result. /// /// `collect_evidence` decides whether the second, inspected pass runs when the cheap evidence does -/// not settle the case; see [`diff_unit`] for why it is staged. -pub fn diff_unit(unit: &TestUnit, specs: DiffSpecs, collect_evidence: bool) -> UnitDiffOutcome { - let target = execute_unit_outcome(unit, &specs.target, false); - let base = execute_unit_outcome(unit, &specs.base, false); +/// not settle the case; see the body for why it is staged. +pub fn diff_unit( + unit: &TestUnit, + indexes: TxPartIndices, + specs: DiffSpecs, + collect_evidence: bool, +) -> UnitDiffOutcome { + let target = execute_unit_outcome(unit, indexes, &specs.target, false); + let base = execute_unit_outcome(unit, indexes, &specs.base, false); let (target, base) = match (target, base) { (Ok(t), Ok(b)) => (t, b), @@ -475,10 +608,10 @@ pub fn diff_unit(unit: &TestUnit, specs: DiffSpecs, collect_evidence: bool) -> U // Stage two. The cheap evidence found nothing, so re-run both sides with the frame inspector, // which sees the frames the transaction's own result hides. It costs an inspected execution - // only for the handful of units that reach here, instead of on every unit in the corpus. + // only for the units that reach here, instead of on every unit in the corpus. let (Ok(target), Ok(base)) = ( - execute_unit_outcome(unit, &specs.target, true), - execute_unit_outcome(unit, &specs.base, true), + execute_unit_outcome(unit, indexes, &specs.target, true), + execute_unit_outcome(unit, indexes, &specs.base, true), ) else { return verdict; }; @@ -554,7 +687,10 @@ fn collect_mechanisms(target: &SpecOutcome, base: &SpecOutcome) -> Vec Vec { +/// +/// Public together with [`judge`] so the classifier's verdict can be exercised against outcomes +/// taken from real executions, rather than only against hand-built ones. +pub fn compare(target: &SpecOutcome, base: &SpecOutcome) -> Vec { let mut fields = Vec::new(); let mut push = |differs: bool, field: DiffField| { if differs { @@ -583,7 +719,7 @@ fn compare(target: &SpecOutcome, base: &SpecOutcome) -> Vec { /// `gas_used`, the halt or revert reported, and the execution success or failure of the outer /// transaction are unchanged by the destroyed half of that carve-out." An exceptional halt is /// therefore not accepted as the explanation for a state-root or receipt difference. -fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> UnitDiffOutcome { +pub fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> UnitDiffOutcome { let mechanisms = collect_mechanisms(target, base); let falsified: Vec = { let mut h: Vec<_> = mechanisms.iter().filter_map(|m| m.falsifies()).collect(); @@ -622,7 +758,8 @@ fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> Unit } } -/// Executes one unit at transaction index 0 under `spec` and collects its outcome and evidence. +/// Executes one unit's given transaction vector under `spec` and collects its outcome and +/// evidence. /// /// Mirrors the validation path exactly — the same config, block environment, external /// environment, block hashes and `BaseFeeVault` pruning — so the roots it computes are the roots @@ -633,6 +770,7 @@ fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> Unit /// classifier turns it on only for the units it cannot settle without it. pub fn execute_unit_outcome( unit: &TestUnit, + indexes: TxPartIndices, spec: &SpecName, collect_evidence: bool, ) -> Result { @@ -647,7 +785,7 @@ pub fn execute_unit_outcome( configure_max_blobs(&mut cfg); let block = unit.block_env(&cfg); - let tx = tx_env_at(unit, TxPartIndices { data: 0, gas: 0, value: 0 })?; + let tx = tx_env_at(unit, indexes)?; let cache = unit.state(); let mut state = @@ -704,6 +842,10 @@ pub fn execute_unit_outcome( gas_used: result.tx_gas_used(), status: execution_status(&result).to_string(), halt_reason: halt_reason(&result), + halt_kind: match &result { + ExecutionResult::Halt { reason, .. } => Some(halt_kind(reason)), + _ => None, + }, output: result.output().cloned(), compute_gas_used, data_size, @@ -718,7 +860,8 @@ pub fn execute_unit_outcome( }) } -/// Runs the differential comparison over every unit of one fixture file. +/// Runs the differential comparison over every transaction vector of every unit of one fixture +/// file. /// /// A unit that panics is recorded as [`DiffClass::Panic`] and the rest of the file still runs; /// see [`panic_capture`] for why that matters at corpus scale. @@ -746,16 +889,25 @@ pub fn diff_test_suite( let mut diffs = Vec::with_capacity(suite.0.len()); for (name, unit) in suite.0 { - let outcome = match panic_capture::catch(|| diff_unit(&unit, specs, collect_evidence)) { - Ok(outcome) => outcome, - Err(report) => UnitDiffOutcome { - class: DiffClass::Panic, - fields: vec![], - mechanisms: vec![], - detail: Some(report), - }, - }; - diffs.push(outcome.named(name, path_str.clone())); + // One verdict per vector the unit declares: `post` entries at different `indexes` are + // different transactions over the same pre-state, and judging only index `{0,0,0}` would + // report a green unit while never running the rest. + let vectors = unit.vectors(); + let multi = vectors.len() > 1; + for indexes in vectors { + let outcome = + match panic_capture::catch(|| diff_unit(&unit, indexes, specs, collect_evidence)) { + Ok(outcome) => outcome, + Err(report) => UnitDiffOutcome { + class: DiffClass::Panic, + fields: vec![], + mechanisms: vec![], + detail: Some(report), + }, + }; + let name = if multi { vector_label(&name, indexes) } else { name.clone() }; + diffs.push(outcome.named(name, path_str.clone())); + } } Ok(diffs) } @@ -797,9 +949,16 @@ impl DiffTally { self.classes.values().sum() } - /// Whether the run should fail its gate: a panic or an unexplained difference. + /// Whether the run should fail its gate: a panic, an unexplained difference, a file the sweep + /// could not read, or a run that judged nothing at all. + /// + /// The last one is what makes the other three mean something. A sweep whose corpus never + /// arrived, or whose discovery walked into an unreadable directory, reaches the gate with an + /// empty tally — zero panics, zero unexplained differences — and every count it prints is + /// truthful. Reading that as a pass is how a broken corpus becomes a green nightly. pub fn is_failure(&self) -> bool { - self.count(DiffClass::Panic) > 0 || + self.total() == 0 || + self.count(DiffClass::Panic) > 0 || self.count(DiffClass::Unexplained) > 0 || !self.file_errors.is_empty() } @@ -854,9 +1013,14 @@ pub struct DiffRunConfig { /// Installs the panic capture hook: a `debug_assert!` one fixture trips becomes that fixture's /// verdict instead of taking down a worker thread, which is what makes a single-process /// full-corpus sweep possible. -pub fn run_diff(files: Vec, config: DiffRunConfig) -> DiffTally { +/// +/// `scan.errors` — anything the discovery walk could not read — is seeded into the tally's file +/// errors before a single fixture runs, so a corpus the sweep only partly reached fails the gate +/// however well the part it did reach behaves. +pub fn run_diff(scan: FixtureScan, config: DiffRunConfig) -> DiffTally { panic_capture::install_capture_hook(); + let FixtureScan { files, errors } = scan; let n_files = files.len(); let bar = Arc::new(ProgressBar::with_draw_target( Some(n_files as u64), @@ -903,7 +1067,7 @@ pub fn run_diff(files: Vec, config: DiffRunConfig) -> DiffTally { ); } - let mut tally = DiffTally::default(); + let mut tally = DiffTally { file_errors: errors, ..DiffTally::default() }; for handle in handles { match handle.join() { Ok(worker) => tally.merge(worker), @@ -919,8 +1083,11 @@ pub fn run_diff(files: Vec, config: DiffRunConfig) -> DiffTally { } /// Collects every JSON fixture under each path, rejecting a path that does not exist. -pub fn collect_fixture_files(paths: &[PathBuf]) -> Result, TestError> { - let mut files = Vec::new(); +/// +/// Directories the walk could not read come back in [`FixtureScan::errors`] rather than as a +/// quietly shorter file list; [`run_diff`] carries them into the tally, where they fail the gate. +pub fn collect_fixture_files(paths: &[PathBuf]) -> Result { + let mut scan = FixtureScan::default(); for path in paths { if !path.exists() { return Err(TestError { @@ -929,16 +1096,18 @@ pub fn collect_fixture_files(paths: &[PathBuf]) -> Result, TestErro kind: TestErrorKind::InvalidPath, }); } - files.extend(find_all_json_tests(path)); + let found = find_all_json_tests(path); + scan.files.extend(found.files); + scan.errors.extend(found.errors); } - if files.is_empty() { + if scan.files.is_empty() { return Err(TestError { name: "Path validation".to_string(), path: paths.iter().map(|p| p.display().to_string()).collect::>().join(", "), kind: TestErrorKind::NoJsonFiles, }); } - Ok(files) + Ok(scan) } /// Bridges a keep-going fill's per-unit status into the sweep's own vocabulary. @@ -965,6 +1134,7 @@ mod tests { gas_used: 21_000, status: "success".to_string(), halt_reason: None, + halt_kind: None, output: None, compute_gas_used: 1_000, data_size: 10, @@ -979,27 +1149,73 @@ mod tests { } } - // Every mechanism maps to exactly the hypothesis it observes, and the two detention labels - // stay informational. A mechanism silently gaining a hypothesis would let it explain a - // difference it is not evidence for. + /// Every mechanism, with the hypothesis it falsifies and where the observation comes from. + /// + /// Exhaustive by construction: `test_mechanism_table_is_exhaustive` fails if a variant is + /// added without a row here. + const MECHANISM_TABLE: [(Mechanism, Option, Provenance); 9] = [ + (Mechanism::ResourceLimitHalt, Some(Hypothesis::WithinLimits), Provenance::Execution), + (Mechanism::DetentionHalt, Some(Hypothesis::WithinLimits), Provenance::Execution), + (Mechanism::GasRescued, Some(Hypothesis::WithinLimits), Provenance::Execution), + (Mechanism::ExceptionalHalt, Some(Hypothesis::NoExceptionalHalt), Provenance::Execution), + (Mechanism::DestroyedComputeGas, None, Provenance::Execution), + (Mechanism::DetentionMarkDiff, None, Provenance::Execution), + (Mechanism::DetentionInForce, None, Provenance::Execution), + (Mechanism::LimitRevertPayload, None, Provenance::Payload), + (Mechanism::VolatileDisabledPayload, None, Provenance::Payload), + ]; + + // Every mechanism maps to exactly the hypothesis it observes. A mechanism silently gaining a + // hypothesis would let it explain a difference it is not evidence for. #[test] fn test_mechanism_hypothesis_table() { - use Hypothesis::{NoDisabledVolatileReject, NoExceptionalHalt, WithinLimits}; - for (mechanism, expected) in [ - (Mechanism::ResourceLimitHalt, Some(WithinLimits)), - (Mechanism::DetentionHalt, Some(WithinLimits)), - (Mechanism::GasRescued, Some(WithinLimits)), - (Mechanism::FrameLocalLimitRevert, Some(WithinLimits)), - (Mechanism::ExceptionalHalt, Some(NoExceptionalHalt)), - (Mechanism::DestroyedComputeGas, Some(NoExceptionalHalt)), - (Mechanism::VolatileAccessDisabled, Some(NoDisabledVolatileReject)), - (Mechanism::DetentionMarkDiff, None), - (Mechanism::DetentionInForce, None), - ] { + for (mechanism, expected, _) in MECHANISM_TABLE { assert_eq!(mechanism.falsifies(), expected, "{}", mechanism.label()); } } + // The rule that makes the classifier un-gameable by a fixture: an observation read out of + // revert-payload bytes never licenses anything, because a contract writes those bytes as + // freely as MegaETH does. A future mechanism that sniffs a payload and claims a hypothesis + // fails here rather than in a corpus sweep that quietly stops flagging. + #[test] + fn test_only_execution_provenance_licenses() { + for (mechanism, _, provenance) in MECHANISM_TABLE { + assert_eq!(mechanism.provenance(), provenance, "{}", mechanism.label()); + if provenance == Provenance::Payload { + assert_eq!( + mechanism.falsifies(), + None, + "{} is read off fixture-authored bytes and must license nothing", + mechanism.label() + ); + } + } + } + + // The table above is the test's own claim to completeness, so it has to cover every variant. + // Labels are distinct and stable, which is what makes them usable as tally keys. + #[test] + fn test_mechanism_table_is_exhaustive() { + let mut labels: Vec<&str> = MECHANISM_TABLE.iter().map(|(m, _, _)| m.label()).collect(); + labels.sort_unstable(); + assert_eq!( + labels, + [ + "destroyed_compute_gas", + "detention_halt", + "detention_in_force", + "detention_mark_diff", + "exceptional_halt", + "gas_rescued", + "limit_revert_payload", + "resource_limit_halt", + "volatile_disabled_payload", + ], + "every Mechanism variant needs a row in MECHANISM_TABLE, with a distinct label" + ); + } + /// A quantity to disturb, and how to disturb it. type FieldProbe = (DiffField, fn(&mut SpecOutcome)); @@ -1028,12 +1244,18 @@ mod tests { } } + /// Frame evidence holding `n` halted frames and no revert payloads. + fn halted_frames(n: u32) -> Option { + Some(FrameEvidence { halted: n, limit_revert_payloads: 0, volatile_disabled_payloads: 0 }) + } + // The exceptional-halt carve-out raises the reported compute total and is explicitly // forbidden from moving the receipt or the state, so it licenses one field and not the other. #[test] fn test_exceptional_halt_explains_only_the_reported_compute_total() { let base = quiet(); let mut target = base.clone(); + target.frames = halted_frames(1); target.compute_gas_destroyed = 5_000; target.compute_gas_used += 5_000; let verdict = judge(&compare(&target, &base), &target, &base); @@ -1042,6 +1264,7 @@ mod tests { // Same evidence, a state-root difference: not licensed. let mut target = base.clone(); + target.frames = halted_frames(1); target.compute_gas_destroyed = 5_000; target.state_root = B256::repeat_byte(9); let verdict = judge(&compare(&target, &base), &target, &base); @@ -1053,6 +1276,36 @@ mod tests { ); } + // A destroyed remainder is derived from a conservation law over the envelope, not observed. + // A defect in that law shows up as a non-zero remainder with no halt behind it, and if the + // remainder licensed the compute-total difference it causes, that defect would be exactly the + // shape the sweep stops reporting. The halt it claims must come from the frame the EVM + // finished; here it is booked with no frame that halted, and the difference stays a finding. + #[test] + fn test_destroyed_compute_gas_needs_an_independent_halted_frame() { + let base = quiet(); + let mut target = base.clone(); + target.compute_gas_destroyed = 5_000; + target.compute_gas_used += 5_000; + + // No frame pass at all: the remainder is the only thing on the table. + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained, "{verdict:?}"); + assert!(verdict.mechanisms.contains(&Mechanism::DestroyedComputeGas)); + + // The frame pass ran and found no halted frame: the remainder is still unexplained, and + // now it is a live contradiction — something destroyed an envelope that no frame lost. + target.frames = halted_frames(0); + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained, "{verdict:?}"); + + // With the independent witness, the same difference is licensed. + target.frames = halted_frames(1); + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Explained, "{verdict:?}"); + assert!(verdict.mechanisms.contains(&Mechanism::ExceptionalHalt)); + } + // A crossed resource limit changes which opcodes ran, so it licenses any quantity — // including the consensus-visible ones. #[test] @@ -1061,6 +1314,7 @@ mod tests { let mut target = base.clone(); target.status = "halt".to_string(); target.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + target.halt_kind = Some(HaltKind::ResourceLimit); target.state_root = B256::repeat_byte(9); target.gas_used += 5; let verdict = judge(&compare(&target, &base), &target, &base); @@ -1076,6 +1330,7 @@ mod tests { let mut base = target.clone(); base.status = "halt".to_string(); base.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + base.halt_kind = Some(HaltKind::ResourceLimit); base.state_root = B256::repeat_byte(9); let verdict = judge(&compare(&target, &base), &target, &base); assert_eq!(verdict.class, DiffClass::Explained); @@ -1096,21 +1351,42 @@ mod tests { assert!(verdict.mechanisms.contains(&Mechanism::DetentionMarkDiff)); } - // A `disableVolatileDataAccess` rejection charges the opcode's static fee under Rex7 and - // nothing under Rex6, which can change anything downstream. + // Both revert-payload claims are reported and neither licenses. A frame that reverts with + // MegaETH's selectors is indistinguishable from a contract that wrote the same four bytes, so + // treating the bytes as evidence would let any fixture buy an exemption for any difference — + // and `MegaLimitExceeded` in particular claims the hypothesis that licenses *every* quantity. #[test] - fn test_guard_rejection_explains_a_consensus_difference() { + fn test_revert_payload_claims_never_license() { let base = quiet(); - let mut target = base.clone(); - target.frames = Some(FrameEvidence { - halted: 0, - limit_exceeded_reverts: 0, - volatile_disabled_reverts: 1, - }); - target.gas_used += 3; - let verdict = judge(&compare(&target, &base), &target, &base); - assert_eq!(verdict.class, DiffClass::Explained); - assert!(verdict.mechanisms.contains(&Mechanism::VolatileAccessDisabled)); + for (payload, expected) in [ + ( + FrameEvidence { + halted: 0, + limit_revert_payloads: 1, + volatile_disabled_payloads: 0, + }, + Mechanism::LimitRevertPayload, + ), + ( + FrameEvidence { + halted: 0, + limit_revert_payloads: 0, + volatile_disabled_payloads: 1, + }, + Mechanism::VolatileDisabledPayload, + ), + ] { + let mut target = base.clone(); + target.frames = Some(payload); + target.gas_used += 3; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained, "{verdict:?}"); + assert!( + verdict.mechanisms.contains(&expected), + "the claim is still reported for a human: {:?}", + verdict.mechanisms + ); + } } // An inner frame that halted is invisible in the transaction's own result and leaves no @@ -1127,32 +1403,72 @@ mod tests { "without frame evidence there is nothing to license the difference" ); - target.frames = Some(FrameEvidence { - halted: 1, - limit_exceeded_reverts: 0, - volatile_disabled_reverts: 0, - }); + target.frames = halted_frames(1); let verdict = judge(&compare(&target, &base), &target, &base); assert_eq!(verdict.class, DiffClass::Explained); assert!(verdict.mechanisms.contains(&Mechanism::ExceptionalHalt)); } - // The halt-reason string decides which mechanism a halt is: the inherited EVM's own halts - // arrive wrapped in `Base(..)` and are not resource limits. + // Which halts count as a crossed resource limit is read off the typed reason, variant by + // variant. Every `MegaHaltReason` gets a row: the four metering halts and the detention halt + // are limits, the inherited EVM's halts and `SystemTxInvalidCallee` are not. The rule this + // replaced — "not `Base(..)` means resource limit" — put `SystemTxInvalidCallee`, and every + // variant a later spec adds, on the licensing side by default. + #[test] + fn test_halt_kind_covers_every_halt_reason() { + use mega_evm::{ + revm::{ + context::result::{HaltReason as EthHaltReason, OutOfGasError}, + primitives::Address, + }, + VolatileDataAccess, + }; + for (reason, expected) in [ + (MegaHaltReason::DataLimitExceeded { limit: 1, actual: 2 }, HaltKind::ResourceLimit), + ( + MegaHaltReason::KVUpdateLimitExceeded { limit: 1, actual: 2 }, + HaltKind::ResourceLimit, + ), + ( + MegaHaltReason::ComputeGasLimitExceeded { limit: 1, actual: 2 }, + HaltKind::ResourceLimit, + ), + ( + MegaHaltReason::StateGrowthLimitExceeded { limit: 1, actual: 2 }, + HaltKind::ResourceLimit, + ), + ( + MegaHaltReason::VolatileDataAccessOutOfGas { + access_type: VolatileDataAccess::empty(), + limit: 1, + actual: 2, + }, + HaltKind::Detention, + ), + (MegaHaltReason::from(EthHaltReason::OutOfGas(OutOfGasError::Basic)), HaltKind::Other), + (MegaHaltReason::SystemTxInvalidCallee { callee: Address::ZERO }, HaltKind::Other), + ] { + assert_eq!(halt_kind(&reason), expected, "{reason:?}"); + } + } + + // A halted transaction is always an exceptional halt; whether it is *also* a crossed resource + // limit is what the kind decides. #[test] - fn test_halt_reason_classification() { + fn test_halt_kind_drives_the_mechanism() { let mut outcome = quiet(); outcome.status = "halt".to_string(); - outcome.halt_reason = Some("Base(OutOfGas(Basic))".to_string()); + outcome.halt_kind = Some(HaltKind::Other); let m = outcome.mechanisms(); assert!(m.contains(&Mechanism::ExceptionalHalt)); assert!(!m.contains(&Mechanism::ResourceLimitHalt)); + assert!(!m.contains(&Mechanism::DetentionHalt)); - outcome.halt_reason = Some("ComputeGasLimitExceeded { limit: 1, actual: 2 }".to_string()); + outcome.halt_kind = Some(HaltKind::ResourceLimit); assert!(outcome.mechanisms().contains(&Mechanism::ResourceLimitHalt)); - outcome.halt_reason = Some("VolatileDataAccessOutOfGas { .. }".to_string()); + outcome.halt_kind = Some(HaltKind::Detention); let m = outcome.mechanisms(); assert!(m.contains(&Mechanism::DetentionHalt)); assert!(!m.contains(&Mechanism::ResourceLimitHalt)); @@ -1214,6 +1530,42 @@ mod tests { assert!(tally.is_failure()); } + // A sweep that judged nothing has nothing to say, and every count it prints is a truthful + // zero. Reading that as a pass is how a corpus that never arrived becomes a green nightly. + #[test] + fn test_a_run_that_judged_nothing_fails_the_gate() { + let mut tally = DiffTally::default(); + assert!(tally.is_failure(), "an empty tally is not a pass"); + + // Skipped-by-filename files are not units judged, so a corpus of nothing but those is + // still a run that judged nothing. + tally.skipped_files = 12; + assert!(tally.is_failure()); + + tally.record(diff_of(DiffClass::Pass, vec![], vec![])); + assert!(!tally.is_failure()); + } + + // The classifier reads one sentence — Rex7's precision invariant — and that sentence relates + // exactly one pair of specs. Any other pair would be judged by a licence it was never given. + #[test] + fn test_only_the_rex7_rex6_pair_can_be_constructed() { + let (target, base) = DiffSpecs::SUPPORTED; + assert_eq!( + DiffSpecs::new(target, base).expect("the supported pair"), + DiffSpecs { target, base } + ); + for (t, b) in [ + (SpecName::Rex7, SpecName::Equivalence), + (SpecName::Rex6, SpecName::Rex5), + (SpecName::Rex6, SpecName::Rex7), + (SpecName::Rex7, SpecName::Rex7), + ] { + let err = DiffSpecs::new(t, b).expect_err("{t:?} vs {b:?} has no invariant"); + assert!(err.contains("Rex7") && err.contains("Rex6"), "name the supported pair: {err}"); + } + } + #[test] fn test_fill_status_class_mapping() { assert_eq!(fill_status_class(&UnitStatus::Ok), DiffClass::Pass); diff --git a/crates/mega-state-test/src/runner.rs b/crates/mega-state-test/src/runner.rs index 147575d2..8ad5e92e 100644 --- a/crates/mega-state-test/src/runner.rs +++ b/crates/mega-state-test/src/runner.rs @@ -95,20 +95,46 @@ impl From for TestErrorKind { } } +/// What walking a path for JSON fixtures found — and what it could not read. +/// +/// The two halves are reported together on purpose. A directory the walk cannot descend into +/// yields no fixtures, which is indistinguishable from a directory that holds none, so a walk +/// that only returns file names turns a permission error or a broken symlink into a smaller +/// corpus and a green run. +#[derive(Debug, Clone, Default)] +pub struct FixtureScan { + /// Every `.json` file the walk reached. + pub files: Vec, + /// Entries the walk could not read, as rendered errors. + pub errors: Vec, +} + /// Find all JSON test files in the given path /// If path is a file, returns it in a vector /// If path is a directory, recursively finds all .json files -pub fn find_all_json_tests(path: &Path) -> Vec { +/// +/// Entries the walk cannot read are collected in [`FixtureScan::errors`] rather than dropped; +/// every caller has to decide what an unreadable part of the corpus means for its own verdict. +pub fn find_all_json_tests(path: &Path) -> FixtureScan { if path.is_file() { - vec![path.to_path_buf()] - } else { - WalkDir::new(path) - .into_iter() - .filter_map(Result::ok) - .filter(|e| e.path().extension() == Some("json".as_ref())) - .map(DirEntry::into_path) - .collect() + return FixtureScan { files: vec![path.to_path_buf()], errors: vec![] }; + } + let mut scan = FixtureScan::default(); + for entry in WalkDir::new(path) { + match entry { + Ok(e) if e.path().extension() == Some("json".as_ref()) => { + scan.files.push(DirEntry::into_path(e)); + } + Ok(_) => {} + // Name the entry the walk tripped on, not the root it started from: at corpus scale + // "something under state_tests/ was unreadable" is not an actionable report. + Err(e) => { + let at = e.path().unwrap_or(path).display().to_string(); + scan.errors.push(format!("walk {at}: {e}")); + } + } } + scan } /// Whether validation skips this fixture file entirely, by filename. @@ -457,9 +483,9 @@ pub fn execute_test_suite( elapsed: &Arc>, trace: bool, print_json_outcome: bool, -) -> Result<(), TestError> { +) -> Result { if skip_test(path) { - return Ok(()); + return Ok(0); } let path = path.to_string_lossy().into_owned(); @@ -474,7 +500,9 @@ pub fn execute_test_suite( kind: e.into(), })?; + let mut units = 0usize; for (name, unit) in suite.0 { + units += 1; // Prepare initial state let cache_state = unit.state(); @@ -576,7 +604,7 @@ pub fn execute_test_suite( } } } - Ok(()) + Ok(units) } /// Build the `MegaETH` external environment for a test unit, reproducing the @@ -687,8 +715,8 @@ pub struct ExecutedUnit { pub output: Option, } -/// Execute a single [`TestUnit`] at transaction index 0 for the given spec, in -/// isolation, timing only the EVM `transact` call. +/// Execute a single [`TestUnit`] at the given transaction vector for the given +/// spec, in isolation, timing only the EVM `transact` call. /// /// This runs the same `MegaEVM` pipeline as [`execute_test_suite`] — including the /// reproduced external environment and the Optimism `BaseFeeVault` pruning. When @@ -696,6 +724,7 @@ pub struct ExecutedUnit { /// timed region); otherwise they are skipped for leaner repeated benchmarking. fn run_unit_once( unit: &TestUnit, + indexes: TxPartIndices, spec: &SpecName, compute_roots: bool, ) -> Result<(Duration, ExecutionResult, Option), TestErrorKind> @@ -711,7 +740,7 @@ fn run_unit_once( configure_max_blobs(&mut cfg); let block = unit.block_env(&cfg); - let tx = tx_env_at(unit, TxPartIndices { data: 0, gas: 0, value: 0 })?; + let tx = tx_env_at(unit, indexes)?; let cache = unit.state(); let mut state = @@ -739,16 +768,17 @@ fn run_unit_once( Ok((elapsed, result, validation)) } -/// Execute a single [`TestUnit`] at transaction index 0 for the given spec and -/// collect its canonical post-execution roots, gas, status, and output. +/// Execute a single [`TestUnit`] at the given transaction vector for the given +/// spec and collect its canonical post-execution roots, gas, status, and output. /// /// Returns the computed values instead of comparing them against an expectation; /// it is the dump-time counterpart to validation. pub fn execute_unit_collect( unit: &TestUnit, + indexes: TxPartIndices, spec: &SpecName, ) -> Result { - let (_elapsed, result, validation) = run_unit_once(unit, spec, true)?; + let (_elapsed, result, validation) = run_unit_once(unit, indexes, spec, true)?; let validation = validation.expect("roots requested"); Ok(ExecutedUnit { state_root: validation.state_root, @@ -760,16 +790,17 @@ pub fn execute_unit_collect( }) } -/// Execute a single [`TestUnit`] at transaction index 0 once, returning the time -/// spent in the EVM `transact` call together with the gas used and status. +/// Execute a single [`TestUnit`] at the given transaction vector once, returning +/// the time spent in the EVM `transact` call together with the gas used and status. /// /// The primitive behind [`bench_test_suite`] / `state-test --bench`: it measures /// EVM throughput in isolation (excluding root computation). pub fn time_unit_execution( unit: &TestUnit, + indexes: TxPartIndices, spec: &SpecName, ) -> Result<(Duration, u64, String), TestErrorKind> { - let (elapsed, result, _validation) = run_unit_once(unit, spec, false)?; + let (elapsed, result, _validation) = run_unit_once(unit, indexes, spec, false)?; Ok((elapsed, result.tx_gas_used(), execution_status(&result).to_string())) } @@ -867,37 +898,54 @@ pub fn bench_test_suite( ))); } - for _ in 0..warmup { - time_unit_execution(&unit, &spec) - .map_err(|e| fixture_err(format!("warmup {name}: {e}")))?; - } - let mut durations = Vec::with_capacity(runs as usize); - let mut gas_used = 0u64; - let mut status = String::new(); - for _ in 0..runs { - let (elapsed, gas, st) = time_unit_execution(&unit, &spec) - .map_err(|e| fixture_err(format!("run {name}: {e}")))?; - durations.push(elapsed); - gas_used = gas; - status = st; + // A unit is a family of transactions, one per vector its `post` names; benchmarking only + // index `{0,0,0}` would report a multi-vector fixture as though the other vectors were + // not there. Single-vector fixtures — every replay dump, and the whole EEST state-test + // corpus — keep their bare unit name and their one result. + let vectors = unit.vectors(); + let multi = vectors.len() > 1; + for indexes in vectors { + let name = if multi { vector_label(&name, indexes) } else { name.clone() }; + for _ in 0..warmup { + time_unit_execution(&unit, indexes, &spec) + .map_err(|e| fixture_err(format!("warmup {name}: {e}")))?; + } + let mut durations = Vec::with_capacity(runs as usize); + let mut gas_used = 0u64; + let mut status = String::new(); + for _ in 0..runs { + let (elapsed, gas, st) = time_unit_execution(&unit, indexes, &spec) + .map_err(|e| fixture_err(format!("run {name}: {e}")))?; + durations.push(elapsed); + gas_used = gas; + status = st; + } + durations.sort_unstable(); + let median = durations[durations.len() / 2]; + let min = durations[0]; + let mean = durations.iter().sum::() / durations.len() as u32; + results.push(UnitBench { + name, + gas_used, + success: status == "success", + runs, + min, + median, + mean, + }); } - durations.sort_unstable(); - let median = durations[durations.len() / 2]; - let min = durations[0]; - let mean = durations.iter().sum::() / durations.len() as u32; - results.push(UnitBench { - name, - gas_used, - success: status == "success", - runs, - min, - median, - mean, - }); } Ok(results) } +/// Names one vector of a multi-vector unit, for a per-vector report line. +/// +/// Only used when a unit actually has more than one vector, so a single-vector fixture's +/// reported name stays exactly its key in the suite. +pub fn vector_label(name: &str, indexes: TxPartIndices) -> String { + format!("{name}[d={},g={},v={}]", indexes.data, indexes.gas, indexes.value) +} + /// What a keep-going driver observed for one fixture unit. #[derive(Debug, Clone, PartialEq, Eq)] pub enum UnitStatus { @@ -1114,14 +1162,41 @@ fn fill_unit( )); } - let executed = panic_capture::catch(|| execute_unit_collect(unit, &spec)) - .map_err(UnitStatus::Panic)? - .map_err(|e| err(format!("execute: {e}")))?; + // One expectation per vector the unit declares, each carrying its own `indexes`. Recording a + // single `{0,0,0}` entry would delete the other vectors' expectations along with the tests + // that used them, and `--force` — which exists to overwrite a stale expectation — would be + // the one flag that makes the loss silent. + let vectors = unit.vectors(); + let mut tests = Vec::with_capacity(vectors.len()); + let mut outputs = Vec::with_capacity(vectors.len()); + for indexes in vectors { + let executed = panic_capture::catch(|| execute_unit_collect(unit, indexes, &spec)) + .map_err(UnitStatus::Panic)? + .map_err(|e| { + err(format!( + "execute [d={},g={},v={}]: {e}", + indexes.data, indexes.gas, indexes.value + )) + })?; + outputs.push(executed.output.clone()); + tests.push(Test::for_dump( + indexes, + executed.state_root, + executed.logs_root, + executed.gas_used, + executed.status, + )); + } - unit.out = executed.output.clone(); - let test = - Test::for_dump(executed.state_root, executed.logs_root, executed.gas_used, executed.status); - unit.post = std::collections::BTreeMap::from([(spec, vec![test])]); + // `out` is one field for the whole unit, so it can only describe a unit that has one vector. + // For a multi-vector unit the per-vector outputs disagree in general; recording any one of + // them would assert it for all, so the field is cleared and the `post` entries carry the + // per-vector expectations instead. + unit.out = match outputs.as_slice() { + [single] => single.clone(), + _ => None, + }; + unit.post = std::collections::BTreeMap::from([(spec, tests)]); Ok(()) } @@ -1198,6 +1273,9 @@ impl TestRunnerConfig { #[derive(Clone)] struct TestRunnerState { n_errors: Arc, + /// Fixture units the workers reached. A run that reached none validated nothing, however + /// many files it walked. + n_units: Arc, console_bar: Arc, queue: Arc)>>, elapsed: Arc>, @@ -1208,6 +1286,7 @@ impl TestRunnerState { let n_files = test_files.len(); Self { n_errors: Arc::new(AtomicUsize::new(0)), + n_units: Arc::new(AtomicUsize::new(0)), console_bar: Arc::new(ProgressBar::with_draw_target( Some(n_files as u64), ProgressDrawTarget::stdout(), @@ -1241,10 +1320,15 @@ fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<( state.console_bar.inc(1); - if let Err(err) = result { - state.n_errors.fetch_add(1, Ordering::SeqCst); - if !config.keep_going { - return Err(err); + match result { + Ok(units) => { + state.n_units.fetch_add(units, Ordering::SeqCst); + } + Err(err) => { + state.n_errors.fetch_add(1, Ordering::SeqCst); + if !config.keep_going { + return Err(err); + } } } } @@ -1314,6 +1398,21 @@ pub fn run( let n_errors = state.n_errors.load(Ordering::SeqCst); let n_thread_errors = thread_errors.len(); + let n_units = state.n_units.load(Ordering::SeqCst); + + // A run that reached no unit at all validated nothing, and "0 errors out of 0" is a truthful + // report of that. An empty corpus, or one whose every file is on the skip list, must not + // print "All tests passed!". + if n_errors == 0 && n_thread_errors == 0 && n_units == 0 { + return Err(TestError { + name: "summary".to_string(), + path: String::new(), + kind: TestErrorKind::FixtureError(format!( + "no fixture unit was validated across {n_files} file(s); the corpus is empty or \ + entirely skipped" + )), + }); + } if n_errors == 0 && n_thread_errors == 0 { println!("All tests passed!"); diff --git a/crates/mega-state-test/src/types/test.rs b/crates/mega-state-test/src/types/test.rs index ec1e7eb7..54ed9116 100644 --- a/crates/mega-state-test/src/types/test.rs +++ b/crates/mega-state-test/src/types/test.rs @@ -62,13 +62,19 @@ impl Test { /// Construct a `post` expectation for a dumped replay fixture. /// /// Records the canonical state/logs roots plus the explicit `MegaETH` gas and - /// status expectations, at transaction index 0. `expect_exception`, - /// `post_state`, `state`, and `txbytes` are left empty/`None` — they are not - /// part of a replay-derived fixture. - pub fn for_dump(hash: B256, logs: B256, mega_gas_used: u64, mega_status: String) -> Self { + /// status expectations, for the transaction vector the caller executed. + /// `expect_exception`, `post_state`, `state`, and `txbytes` are left + /// empty/`None` — they are not part of a replay-derived fixture. + pub fn for_dump( + indexes: TxPartIndices, + hash: B256, + logs: B256, + mega_gas_used: u64, + mega_status: String, + ) -> Self { Self { expect_exception: None, - indexes: TxPartIndices { data: 0, gas: 0, value: 0 }, + indexes, hash, post_state: HashMap::default(), logs, diff --git a/crates/mega-state-test/src/types/test_unit.rs b/crates/mega-state-test/src/types/test_unit.rs index 8e1a65e6..58b1d9a8 100644 --- a/crates/mega-state-test/src/types/test_unit.rs +++ b/crates/mega-state-test/src/types/test_unit.rs @@ -2,7 +2,7 @@ use mega_evm::{revm::primitives::eip4844, MegaSpecId}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -use super::{AccountInfo, Env, MegaEnv, SpecName, Test, TransactionParts}; +use super::{AccountInfo, Env, MegaEnv, SpecName, Test, TransactionParts, TxPartIndices}; use mega_evm::revm::{ context::{block::BlockEnv, cfg::CfgEnv}, database::CacheState, @@ -74,6 +74,28 @@ pub struct TestUnit { } impl TestUnit { + /// Every transaction vector this unit defines, ascending and deduplicated. + /// + /// A state-test unit is not one transaction but a family of them: `transaction` holds arrays + /// of `data`, `gasLimit` and `value`, and each `post` entry names the combination it pins + /// through its `indexes`. Validation runs every one of those entries, so any other consumer + /// that judges or rewrites a unit has to enumerate the same set — taking index `{0,0,0}` and + /// calling it "the unit" silently drops whatever the other vectors would have shown. + /// + /// A unit with no `post` at all (a hand-built or snapshot-derived fixture) declares no + /// vector; index `{0,0,0}` is the only one such a fixture can mean, and it is what a fill + /// records. + pub fn vectors(&self) -> Vec { + let mut vectors: Vec = + self.post.values().flatten().map(|test| test.indexes).collect(); + vectors.sort_unstable(); + vectors.dedup(); + if vectors.is_empty() { + vectors.push(TxPartIndices { data: 0, gas: 0, value: 0 }); + } + vectors + } + /// Prepare the state from the test unit. /// /// This function uses [`TestUnit::pre`] to prepare the pre-state from the test unit. diff --git a/crates/mega-state-test/src/types/transaction.rs b/crates/mega-state-test/src/types/transaction.rs index 238669ff..7d0b27ee 100644 --- a/crates/mega-state-test/src/types/transaction.rs +++ b/crates/mega-state-test/src/types/transaction.rs @@ -102,7 +102,7 @@ impl TransactionParts { } /// Transaction part indices. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct TxPartIndices { /// Index into the data array diff --git a/crates/mega-state-test/tests/diff_mode.rs b/crates/mega-state-test/tests/diff_mode.rs index 48a65658..e35cb051 100644 --- a/crates/mega-state-test/tests/diff_mode.rs +++ b/crates/mega-state-test/tests/diff_mode.rs @@ -3,13 +3,17 @@ //! The classifier's decision table is unit-tested in `src/diff.rs`; these tests drive real //! executions, so they cover the parts the table cannot: that the two specs are actually executed //! and committed the way validation does, that the staged frame evidence reaches a halt no -//! transaction result exposes, and that a keep-going fill isolates one unit's failure from the -//! rest of its file. +//! transaction result exposes, that evidence a fixture authored itself buys it nothing, and that +//! a keep-going fill isolates one unit's failure from the rest of its file. +use mega_evm::{alloy_sol_types::SolError, revm::primitives::B256, MegaLimitExceeded}; use state_test::{ - diff::{diff_test_suite, diff_unit, execute_unit_outcome, DiffClass, DiffSpecs, Mechanism}, - runner::{fill_test_suite, fill_test_suite_keep_going, UnitStatus}, - types::{SpecName, TestUnit}, + diff::{ + collect_fixture_files, compare, diff_test_suite, diff_unit, execute_unit_outcome, judge, + run_diff, DiffClass, DiffRunConfig, DiffSpecs, Mechanism, SpecOutcome, + }, + runner::{fill_test_suite, fill_test_suite_keep_going, FixtureScan, UnitStatus}, + types::{SpecName, TestUnit, TxPartIndices}, }; use std::path::PathBuf; @@ -17,6 +21,9 @@ const SENDER: &str = "0x1000000000000000000000000000000000000001"; const CALLEE: &str = "0x2000000000000000000000000000000000000002"; const INNER: &str = "0x3000000000000000000000000000000000000003"; +/// The single transaction vector these hand-built fixtures declare. +const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; + /// `CALL(0 gas, 0x40..04, no value, no args, no return); POP` — runs out of gas partway. /// /// Given a small enough allowance this frame halts, and it halts somewhere the two specs account @@ -25,12 +32,36 @@ const INNER: &str = "0x3000000000000000000000000000000000000003"; const INNER_RUNS_OUT: &str = "0x600060006000600060007340000000000000000000000000000000000000046000f150"; -/// `CALL( gas, INNER, no value, no args, no return); POP; STOP`. +/// `CALL( gas, INNER, no value, no args, no return); POP` — no trailing `STOP`. +/// +/// `CALL` pushes 0 on failure and the caller carries on, so nothing about the child's halt +/// reaches the transaction's own result. +fn call_inner_frag(gas: u16) -> String { + format!("6000600060006000600073{}61{gas:04x}f150", &INNER[2..]) +} + +/// `CALL( gas, 0x08, no value, 1 byte of args, no return); POP` — no trailing `STOP`. /// -/// `CALL` pushes 0 on failure and this frame carries on to a normal `STOP`, so nothing about the -/// child's halt reaches the transaction's own result. -fn call_into_inner(gas: u16) -> String { - format!("0x6000600060006000600073{}61{gas:04x}f1500000", &INNER[2..]) +/// `0x08` is the bn128 pairing precompile, which rejects any input whose length is not a multiple +/// of 192. It never becomes an EVM frame, so the whole forwarded envelope is lost without being +/// executed: Rex7 books it as a destroyed remainder, and the caller absorbs the failure. +fn call_precompile_frag(gas: u16) -> String { + format!("60006000600160006000600861{gas:04x}f150") +} + +/// Wraps code fragments into a contract that runs them and stops. +fn contract(frags: &[String]) -> String { + format!("0x{}00", frags.concat()) +} + +/// `MSTORE(0, ); REVERT(28, 4)` — a plain revert carrying four chosen bytes. +/// +/// Nothing about this contract is a `MegaETH` mechanism. It writes the same four bytes `MegaETH` +/// writes when a frame-local resource limit is exceeded, which is all a classifier that reads +/// revert payloads as evidence would need to see. +fn revert_with_selector(selector: [u8; 4]) -> String { + let word = u32::from_be_bytes(selector); + format!("0x63{word:08x}6000526004601cfd") } /// A unit whose transaction calls `CALLEE`, with an optional third account at `INNER`. @@ -76,7 +107,16 @@ fn parse_unit(json: &serde_json::Value) -> TestUnit { } fn rex7_over_rex6() -> DiffSpecs { - DiffSpecs { target: SpecName::Rex7, base: SpecName::Rex6 } + let (target, base) = DiffSpecs::SUPPORTED; + DiffSpecs::new(target, base).expect("the supported pair") +} + +fn rex7(unit: &TestUnit, collect_evidence: bool) -> SpecOutcome { + execute_unit_outcome(unit, VECTOR_0, &SpecName::Rex7, collect_evidence).expect("rex7 executes") +} + +fn rex6(unit: &TestUnit, collect_evidence: bool) -> SpecOutcome { + execute_unit_outcome(unit, VECTOR_0, &SpecName::Rex6, collect_evidence).expect("rex6 executes") } /// Writes a suite of named units to a unique temp file and returns its path. @@ -99,10 +139,9 @@ fn write_suite(file_name: &str, units: &[(&str, serde_json::Value)]) -> PathBuf /// particular gas number does. fn inner_halt_json() -> serde_json::Value { for gas in 1..=64u16 { - let json = unit_json(&call_into_inner(gas), Some(INNER_RUNS_OUT)); + let json = unit_json(&contract(&[call_inner_frag(gas)]), Some(INNER_RUNS_OUT)); let unit = parse_unit(&json); - let target = execute_unit_outcome(&unit, &SpecName::Rex7, false).expect("rex7 executes"); - let base = execute_unit_outcome(&unit, &SpecName::Rex6, false).expect("rex6 executes"); + let (target, base) = (rex7(&unit, false), rex6(&unit, false)); let hidden = target.status == "success" && target.compute_gas_destroyed == 0; if hidden && target.compute_gas_used != base.compute_gas_used { return json; @@ -115,12 +154,38 @@ fn inner_halt_unit() -> TestUnit { parse_unit(&inner_halt_json()) } +/// A unit whose two specs disagree on the reported compute total, that books a Rex7 destroyed +/// remainder, and whose own transaction result is a plain success. +/// +/// Two independent inner calls: a failing precompile, which loses its whole envelope without +/// executing it and so books the remainder, and a gas-starved inner frame, which is what actually +/// moves the reported total. Neither is visible from the transaction's own result, so before the +/// frame pass the remainder is the only thing on the table — exactly the situation in which a +/// derived number must not be allowed to certify itself. +/// +/// Searched over the inner allowance for the same reason as [`inner_halt_json`]: the property is +/// that the shape exists, not that a particular gas number produces it. +fn destroyed_without_visible_halt_unit() -> TestUnit { + for gas in 1..=64u16 { + let code = contract(&[call_precompile_frag(2_000), call_inner_frag(gas)]); + let unit = parse_unit(&unit_json(&code, Some(INNER_RUNS_OUT))); + let (target, base) = (rex7(&unit, false), rex6(&unit, false)); + if target.status == "success" && + target.compute_gas_destroyed > 0 && + target.compute_gas_used != base.compute_gas_used + { + return unit; + } + } + panic!("no forwarded-gas amount produced a destroyed remainder under a successful transaction") +} + // A transaction that stays inside every limit, ends no frame in an exceptional halt and trips no // guard is bit-identical under Rex7 and Rex6 — the precision invariant's own statement, executed. #[test] fn test_within_limit_transaction_is_identical_under_both_specs() { let unit = parse_unit(&unit_json("0x", None)); - let outcome = diff_unit(&unit, rex7_over_rex6(), true); + let outcome = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), true); assert_eq!(outcome.class, DiffClass::Pass, "{outcome:?}"); assert!(outcome.fields.is_empty()); } @@ -133,14 +198,14 @@ fn test_within_limit_transaction_is_identical_under_both_specs() { fn test_inner_frame_halt_is_explained_only_with_frame_evidence() { let unit = inner_halt_unit(); - let without = diff_unit(&unit, rex7_over_rex6(), false); + let without = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), false); assert_eq!( without.class, DiffClass::Unexplained, "the transaction's own result hides the inner halt: {without:?}" ); - let with = diff_unit(&unit, rex7_over_rex6(), true); + let with = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), true); assert_eq!(with.class, DiffClass::Explained, "{with:?}"); assert!( with.mechanisms.contains(&Mechanism::ExceptionalHalt), @@ -149,6 +214,78 @@ fn test_inner_frame_halt_is_explained_only_with_frame_evidence() { ); } +// A destroyed remainder is derived from a conservation law over the transaction's envelope, not +// observed. A missing term in that law produces a non-zero remainder with no halt behind it, so +// letting the remainder license the compute-total difference it causes would make that defect its +// own alibi. Here a real transaction books one and ends in a plain success: the remainder alone +// leaves the difference unexplained, and only the halted frame the inspector finds licenses it. +#[test] +fn test_destroyed_remainder_is_licensed_by_the_frame_not_by_itself() { + let unit = destroyed_without_visible_halt_unit(); + + let without = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), false); + assert_eq!( + without.class, + DiffClass::Unexplained, + "a destroyed remainder must not certify the halt it claims: {without:?}" + ); + assert!( + without.mechanisms.contains(&Mechanism::DestroyedComputeGas), + "the remainder is still reported: {:?}", + without.mechanisms + ); + + let with = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), true); + assert_eq!(with.class, DiffClass::Explained, "{with:?}"); + assert!( + with.mechanisms.contains(&Mechanism::ExceptionalHalt), + "the independent witness is the frame the EVM finished: {:?}", + with.mechanisms + ); +} + +// Anti-vacuity control, and the reason evidence has to be bound to the execution. A contract that +// writes MegaETH's `MegaLimitExceeded` selector into its revert buffer is observed doing so — the +// claim is real and reported — but it claims the hypothesis that licenses *every* compared +// quantity, and it is four bytes any fixture can write. An unrelated difference laid over that +// execution stays UNEXPLAINED, so the sweep cannot be talked out of a finding by its own input. +#[test] +fn test_a_forged_limit_selector_buys_no_exemption() { + let selector: [u8; 4] = MegaLimitExceeded::SELECTOR; + let unit = parse_unit(&unit_json(&revert_with_selector(selector), None)); + + // The forgery is a plain `REVERT`, and the inspector does see the four bytes. + let observed = rex7(&unit, true); + let frames = observed.frames.expect("the inspected pass collects frame evidence"); + assert!( + frames.limit_revert_payloads > 0, + "the forged selector should be observed and reported: {frames:?}" + ); + assert_eq!(frames.halted, 0, "a plain revert is not a halt"); + + // On its own the fixture is identical under both specs — the forgery changes nothing. + let outcome = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), true); + assert_eq!(outcome.class, DiffClass::Pass, "{outcome:?}"); + + // Lay an unrelated difference over the same real execution — one on a quantity only a changed + // execution path can move. The forged claim is the only thing on the table, and it licenses + // nothing, so the difference is still a finding. + let mut target = observed.clone(); + target.state_root = B256::repeat_byte(9); + target.gas_used += 1; + let verdict = judge(&compare(&target, &observed), &target, &observed); + assert_eq!( + verdict.class, + DiffClass::Unexplained, + "bytes the fixture chose must not license a difference: {verdict:?}" + ); + assert!( + verdict.mechanisms.contains(&Mechanism::LimitRevertPayload), + "the claim is still reported for a human triaging the finding: {:?}", + verdict.mechanisms + ); +} + // The whole file is judged, one verdict per unit, and a unit's verdict is attributed to its own // name — a sweep that mislabels which fixture differed is unusable for triage. #[test] @@ -172,10 +309,73 @@ fn test_transaction_rejected_by_both_specs_is_skipped() { let mut json = unit_json("0x", None); json["transaction"]["gasLimit"] = serde_json::json!(["0x1"]); let unit = parse_unit(&json); - let outcome = diff_unit(&unit, rex7_over_rex6(), true); + let outcome = diff_unit(&unit, VECTOR_0, rex7_over_rex6(), true); assert_eq!(outcome.class, DiffClass::Skipped, "{outcome:?}"); } +/// A unit with two transaction vectors: `data[1]` is a non-empty calldata, and the two `post` +/// entries name index 0 and index 1. +fn two_vector_json() -> serde_json::Value { + let mut json = unit_json("0x", None); + json["transaction"]["data"] = serde_json::json!(["0x", "0xdeadbeef"]); + let entry = |data: usize| { + serde_json::json!({ + "indexes": { "data": data, "gas": 0, "value": 0 }, + "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "logs": "0x0000000000000000000000000000000000000000000000000000000000000000" + }) + }; + json["post"] = serde_json::json!({ "Rex6": [entry(0), entry(1)] }); + json +} + +// A unit is a family of transactions, one per vector its `post` names. Judging only index +// `{0,0,0}` would report a green unit while never running the rest of it, and every count the +// sweep prints would be short by the vectors it skipped. +#[test] +fn test_every_declared_vector_is_judged() { + let unit = parse_unit(&two_vector_json()); + assert_eq!(unit.vectors().len(), 2, "the fixture declares two vectors"); + + let path = write_suite("two_vectors.json", &[("family", two_vector_json())]); + let diffs = diff_test_suite(&path, rex7_over_rex6(), true).expect("diff suite"); + assert_eq!(diffs.len(), 2, "one verdict per vector: {diffs:?}"); + let mut names: Vec<&str> = diffs.iter().map(|d| d.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, ["family[d=0,g=0,v=0]", "family[d=1,g=0,v=0]"]); + + // The two vectors send different calldata, so they are genuinely different transactions and + // not the same one counted twice. + let gas: Vec = unit + .vectors() + .into_iter() + .map(|v| execute_unit_outcome(&unit, v, &SpecName::Rex7, false).expect("executes").gas_used) + .collect(); + assert_ne!(gas[0], gas[1], "calldata cost should differ between the vectors"); +} + +// `--fill --force` exists to overwrite a stale expectation. Collapsing the `post` map to a single +// `{0,0,0}` entry would make it delete the other vectors' expectations too, and silently: the +// file still parses, still validates, and covers less than it did. +#[test] +fn test_fill_records_one_expectation_per_vector() { + let path = write_suite("fill_two_vectors.json", &[("family", two_vector_json())]); + let report = + fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill the file"); + assert_eq!(report.filled(), 1); + + let written: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read")).expect("json"); + let post = &written["family"]["post"]["Rex7"]; + assert_eq!(post.as_array().map(Vec::len), Some(2), "both vectors kept: {post}"); + assert_eq!(post[0]["indexes"], serde_json::json!({ "data": 0, "gas": 0, "value": 0 })); + assert_eq!(post[1]["indexes"], serde_json::json!({ "data": 1, "gas": 0, "value": 0 })); + assert_ne!( + post[0]["megaGasUsed"], post[1]["megaGasUsed"], + "each entry records its own vector's execution" + ); +} + // Keep-going fill: one unit's failure must cost that unit, not the units after it in the same // file. Without this, a corpus sweep has to split every multi-unit fixture first. #[test] @@ -209,3 +409,78 @@ fn test_keep_going_fill_isolates_one_unit_failure() { assert!(written["c_ok"]["post"]["Rex7"].is_array()); assert_eq!(written["b_broken"]["post"], serde_json::json!({})); } + +// A part of the corpus the discovery walk could not read is a hole in coverage, and it reaches +// the gate as a smaller file list — every count still truthful, every count short. `run_diff` +// carries those errors into the tally so the run fails instead of grading the part it reached. +#[test] +fn test_an_unreadable_part_of_the_corpus_fails_the_run() { + let path = write_suite("scan_errors.json", &[("quiet", unit_json("0x", None))]); + let clean = FixtureScan { files: vec![path.clone()], errors: vec![] }; + let config = DiffRunConfig { + specs: rex7_over_rex6(), + single_thread: true, + collect_evidence: true, + progress: false, + }; + + let tally = run_diff(clean, config); + assert_eq!(tally.count(DiffClass::Pass), 1); + assert!(!tally.is_failure(), "a corpus the sweep read in full and passed"); + + let partial = + FixtureScan { files: vec![path], errors: vec!["walk /corpus/sub: denied".to_string()] }; + let tally = run_diff(partial, config); + assert_eq!(tally.count(DiffClass::Pass), 1, "the readable part still runs"); + assert_eq!(tally.file_errors.len(), 1, "and the unreadable part is reported"); + assert!(tally.is_failure(), "a partly-read corpus is not a pass"); +} + +// A directory whose contents cannot be listed yields no fixtures, which is indistinguishable from +// a directory that holds none. The walk has to report it. +#[test] +#[cfg(unix)] +fn test_discovery_reports_a_directory_it_cannot_read() { + use std::os::unix::fs::PermissionsExt; + + let root = std::env::temp_dir().join("mega_state_test_unreadable_scan"); + let _ = std::fs::remove_dir_all(&root); + let locked = root.join("locked"); + std::fs::create_dir_all(&locked).expect("mkdir"); + std::fs::write(locked.join("hidden.json"), "{}").expect("write"); + std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).expect("chmod"); + + let unreadable = std::fs::read_dir(&locked).is_err(); + let scan = state_test::runner::find_all_json_tests(&root); + + // Restore before asserting, so a failure does not leave an unreadable directory behind. + std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o755)).expect("chmod back"); + let _ = std::fs::remove_dir_all(&root); + + // Running as root defeats the permission bits; then there is nothing to detect and the walk + // legitimately finds the file. + if unreadable { + assert!(scan.files.is_empty(), "the fixture is behind the locked directory"); + assert!(!scan.errors.is_empty(), "the unreadable directory must be reported"); + } else { + assert_eq!(scan.files.len(), 1, "readable after all (running as root?)"); + } +} + +// The path-level guards the differential run relies on before it judges anything. +#[test] +fn test_collect_fixture_files_rejects_a_corpus_with_nothing_in_it() { + let missing = std::env::temp_dir().join("mega_state_test_no_such_corpus_4928"); + let _ = std::fs::remove_dir_all(&missing); + assert!( + collect_fixture_files(std::slice::from_ref(&missing)).is_err(), + "a path that does not exist" + ); + + std::fs::create_dir_all(&missing).expect("mkdir"); + assert!( + collect_fixture_files(std::slice::from_ref(&missing)).is_err(), + "a directory with no fixtures" + ); + let _ = std::fs::remove_dir_all(&missing); +} diff --git a/crates/mega-state-test/tests/dump_roundtrip.rs b/crates/mega-state-test/tests/dump_roundtrip.rs index 77dac481..e3dfd485 100644 --- a/crates/mega-state-test/tests/dump_roundtrip.rs +++ b/crates/mega-state-test/tests/dump_roundtrip.rs @@ -12,9 +12,12 @@ use std::{ use state_test::{ runner::{execute_test_suite, execute_unit_collect, fill_test_suite}, - types::{SpecName, Test, TestSuite, TestUnit}, + types::{SpecName, Test, TestSuite, TestUnit, TxPartIndices}, }; +/// The only transaction vector these fixtures declare. +const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; + /// A minimal `MegaETH` unit: a funded sender transfers value to a pre-existing /// recipient, under a `megaEnv` carrying a non-default SALT bucket capacity. fn sample_unit_json() -> &'static str { @@ -69,12 +72,13 @@ fn dump_fixture_json() -> (String, state_test::runner::ExecutedUnit) { let mut unit: TestUnit = serde_json::from_str(sample_unit_json()).expect("parse unit"); let spec = SpecName::Rex5; - let executed = execute_unit_collect(&unit, &spec).expect("execute unit"); + let executed = execute_unit_collect(&unit, VECTOR_0, &spec).expect("execute unit"); unit.out = executed.output.clone(); unit.post = std::collections::BTreeMap::from([( spec, vec![Test::for_dump( + VECTOR_0, executed.state_root, executed.logs_root, executed.gas_used, @@ -150,7 +154,7 @@ fn test_undersized_bucket_capacity_fails_instead_of_panicking() { assert_ne!(bad, sample_unit_json(), "capacity replacement applied"); let unit: TestUnit = serde_json::from_str(&bad).expect("parse unit"); - let err = execute_unit_collect(&unit, &SpecName::Rex5) + let err = execute_unit_collect(&unit, VECTOR_0, &SpecName::Rex5) .expect_err("undersized capacity must fail execution"); assert!(format!("{err}").contains("MIN_BUCKET_SIZE"), "unexpected error: {err}"); } diff --git a/crates/mega-state-test/tests/hardening.rs b/crates/mega-state-test/tests/hardening.rs index bf6c2b09..5c560e24 100644 --- a/crates/mega-state-test/tests/hardening.rs +++ b/crates/mega-state-test/tests/hardening.rs @@ -15,9 +15,12 @@ use state_test::{ bench_test_suite, execute_test_suite, execute_unit_collect, fill_test_suite, run, TestError, TestErrorKind, }, - types::{SpecName, TestUnit}, + types::{SpecName, TestUnit, TxPartIndices}, }; +/// The only transaction vector these fixtures declare. +const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; + /// Minimal valid unit JSON: a funded sender sends a legacy transaction to a /// recipient whose code is `code` (use `"0x"` for a plain transfer). fn unit_json(code: &str) -> serde_json::Value { @@ -83,14 +86,14 @@ fn write_suite(file_name: &str, unit: &serde_json::Value) -> PathBuf { path } -fn run_suite(path: &Path) -> Result<(), TestError> { +fn run_suite(path: &Path) -> Result { let elapsed = Arc::new(Mutex::new(Duration::ZERO)); // `print_json_outcome: true` keeps the failure path single-shot (no debug // re-run with tracing), so error assertions stay quiet and fast. execute_test_suite(path, &elapsed, false, true) } -fn expect_fixture_error(result: Result<(), TestError>, needle: &str) { +fn expect_fixture_error(result: Result, needle: &str) { let err = result.expect_err("suite must fail"); match &err.kind { TestErrorKind::FixtureError(msg) => { @@ -269,8 +272,10 @@ fn block_hashes_are_injected_into_execution() { "0xf": "0x2222222222222222222222222222222222222222222222222222222222222222" }); - let run1 = execute_unit_collect(&blockhash_unit(Some(h1)), &SpecName::Rex5).expect("run h1"); - let run2 = execute_unit_collect(&blockhash_unit(Some(h2)), &SpecName::Rex5).expect("run h2"); + let run1 = + execute_unit_collect(&blockhash_unit(Some(h1)), VECTOR_0, &SpecName::Rex5).expect("run h1"); + let run2 = + execute_unit_collect(&blockhash_unit(Some(h2)), VECTOR_0, &SpecName::Rex5).expect("run h2"); assert_eq!(run1.status, "success"); assert_eq!(run2.status, "success"); assert_ne!( @@ -279,7 +284,7 @@ fn block_hashes_are_injected_into_execution() { ); // Absent blockHashes: execution still works on the synthetic default. - let synthetic = execute_unit_collect(&blockhash_unit(None), &SpecName::Rex5) + let synthetic = execute_unit_collect(&blockhash_unit(None), VECTOR_0, &SpecName::Rex5) .expect("run without blockHashes"); assert_eq!(synthetic.status, "success"); } @@ -291,7 +296,7 @@ fn block_hashes_key_overflow_is_fixture_error() { "0x10000000000000000": "0x1111111111111111111111111111111111111111111111111111111111111111" }); - let err = execute_unit_collect(&blockhash_unit(Some(overflow)), &SpecName::Rex5) + let err = execute_unit_collect(&blockhash_unit(Some(overflow)), VECTOR_0, &SpecName::Rex5) .expect_err("overflowing blockHashes key must fail"); assert!(err.to_string().contains("blockHashes"), "unexpected error: {err}"); } diff --git a/crates/mega-state-test/tests/replay_corpus.rs b/crates/mega-state-test/tests/replay_corpus.rs index 468a83f4..26555f89 100644 --- a/crates/mega-state-test/tests/replay_corpus.rs +++ b/crates/mega-state-test/tests/replay_corpus.rs @@ -29,17 +29,18 @@ use state_test::runner::{execute_test_suite, find_all_json_tests}; #[test] fn test_replay_corpus_self_validates() { let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../bench/replay/fixtures"); - let fixtures = find_all_json_tests(std::path::Path::new(dir)); + let scan = find_all_json_tests(std::path::Path::new(dir)); - assert!(!fixtures.is_empty(), "replay corpus is empty at {dir}"); + assert!(scan.errors.is_empty(), "replay corpus is not fully readable: {:?}", scan.errors); + assert!(!scan.files.is_empty(), "replay corpus is empty at {dir}"); let elapsed = Arc::new(Mutex::new(Duration::ZERO)); let mut passed = 0usize; - for path in &fixtures { + for path in &scan.files { execute_test_suite(path, &elapsed, false, false).unwrap_or_else(|e| { panic!("replay fixture {} failed to validate: {e}", path.display()) }); passed += 1; } - assert_eq!(passed, fixtures.len(), "all corpus fixtures must validate"); + assert_eq!(passed, scan.files.len(), "all corpus fixtures must validate"); } diff --git a/crates/state-test/README.md b/crates/state-test/README.md index ae2e1c36..c6f87eb9 100644 --- a/crates/state-test/README.md +++ b/crates/state-test/README.md @@ -13,8 +13,16 @@ Every mode operates on self-contained EEST fixtures (`TestUnit { env, pre, trans - **Validate** (default) — `state-test ` executes each fixture and checks its recorded `post` (state root, logs root, gas, status). This is how the official Ethereum tests and the replay corpus (`bench/replay/fixtures/`, via `replay_corpus.rs`) are checked. - **`--bench`** — `state-test --bench [--bench-runs N] [--bench-warmup W] [--bench-spec SPEC] ` times each fixture's isolated EVM execution and prints `{ gas_used, success, bench: { min/median/mean, mgasPerSec } }` as JSON instead of validating. This is the only EVM-throughput benchmark entry point; the replay-throughput benchmark (`bench/replay/run.py`) drives it. -- **`--fill`** — `state-test --fill --bench-spec SPEC ` computes each fixture's `post` and writes it back in place (atomically, via a temp file). This is the offline analog of `mega-evme replay --dump-fixture`'s post-fill step, for a fixture that has no on-chain origin (a hand-built case, or a `prestateTracer` snapshot such as `bench/replay/fixtures/attack_deploy.json`). After filling, the fixture is self-validating like any dumped one. A fixture that already has a non-empty `post` is refused unless `--force` is passed — filling replaces the whole `post` map with circularly-derived expectations, so an accidental run against real expectations (e.g. the official test suites) would destroy them. Filenames on the validation skip list and the Constantinople spec are refused outright, since validation would never check the result. -- **`--diff-spec`** — `state-test --bench-spec TARGET --diff-spec BASE ` executes each fixture under both specs and classifies how they differ. Nothing is written and no recorded `post` is consulted: the two executions are compared against each other. This is the only check available for a spec nobody has computed expectations for yet — the frozen spec it inherits from is the oracle, and that spec's precision invariant is what says when the two are allowed to disagree. See `crates/mega-state-test/src/diff.rs` for the classification and `tools/eest-sweep/` for the corpus driver built on it. +- **`--fill`** — `state-test --fill --bench-spec SPEC ` computes each fixture's `post` and writes it back in place (atomically, via a temp file). This is the offline analog of `mega-evme replay --dump-fixture`'s post-fill step, for a fixture that has no on-chain origin (a hand-built case, or a `prestateTracer` snapshot such as `bench/replay/fixtures/attack_deploy.json`). After filling, the fixture is self-validating like any dumped one. One expectation is recorded per transaction vector the unit declares, each keeping its own `indexes`. A fixture that already has a non-empty `post` is refused unless `--force` is passed — filling replaces the whole `post` map with circularly-derived expectations, so an accidental run against real expectations (e.g. the official test suites) would destroy them. Filenames on the validation skip list and the Constantinople spec are refused outright, since validation would never check the result. +- **`--diff-spec`** — `state-test --bench-spec Rex7 --diff-spec Rex6 ` executes each fixture under both specs and classifies how they differ. Nothing is written and no recorded `post` is consulted: the two executions are compared against each other. This is the only check available for a spec nobody has computed expectations for yet — the frozen spec it inherits from is the oracle, and that spec's precision invariant is what says when the two are allowed to disagree. That invariant is Rex7's and relates Rex7 to Rex6, so Rex7-against-Rex6 is the only pair accepted; any other is refused rather than judged by a licence it was never granted. See `crates/mega-state-test/src/diff.rs` for the classification and `tools/eest-sweep/` for the corpus driver built on it. - **`--keep-going`** — with `--fill`, records each unit's failure (or panic) and carries on with the rest of its file instead of aborting at the first one, then prints a `Fill tally:` line. Without it, one bad unit ends the whole run, which is why a corpus sweep used to have to split every multi-unit fixture into one file per unit first. -`--bench-spec` selects the spec to run under; without it, the fixture's single `post` spec is used (so `--fill` needs it when the `post` is still empty, and `--diff-spec` requires it outright). \ No newline at end of file +`--bench-spec` selects the spec to run under; without it, the fixture's single `post` spec is used (so `--fill` needs it when the `post` is still empty, and `--diff-spec` requires it outright). + +## Transaction vectors + +A state-test unit is a family of transactions, not one: `transaction` holds arrays of `data`, `gasLimit` and `value`, and each `post` entry names the combination it pins through its `indexes`. Validate, `--bench`, `--fill` and `--diff-spec` all enumerate that same set, so a multi-vector unit yields one result, one benchmark, one filled expectation and one verdict per vector. A unit with no `post` declares no vector and is run at `{0,0,0}`, which is the only transaction such a fixture can mean. Per-vector results of a multi-vector unit are reported under `name[d=..,g=..,v=..]`; a single-vector unit keeps its bare name. + +## Exit codes + +Every mode exits 1 on failure and 0 otherwise, and "judged nothing" counts as a failure in all of them: a run whose corpus was empty, unreachable, or entirely unreadable reports zeroes that are truthful and meaningless, and must not read as a pass. A `--diff-spec` run additionally fails on a panic, on an unexplained difference, and on any file it could not read or parse. \ No newline at end of file diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 537e1d3a..92a961d6 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -115,9 +115,22 @@ impl Cmd { } println!("\nRunning tests in {}...", path.display()); - let test_files = find_all_json_tests(path); + let scan = find_all_json_tests(path); + // A directory the walk could not descend into contributes no fixtures, which looks + // exactly like a directory that holds none. Fail rather than run the part that was + // readable and report it as the whole. + if let Some(err) = scan.errors.first() { + return Err(TestError { + name: "Path validation".to_string(), + path: path.display().to_string(), + kind: TestErrorKind::FixtureError(format!( + "{} path(s) could not be read; first: {err}", + scan.errors.len() + )), + }); + } - if test_files.is_empty() { + if scan.files.is_empty() { return Err(TestError { name: "Path validation".to_string(), path: path.display().to_string(), @@ -125,7 +138,7 @@ impl Cmd { }); } - run(test_files, self.single_thread, self.json, self.json_outcome, self.keep_going)? + run(scan.files, self.single_thread, self.json, self.json_outcome, self.keep_going)? } Ok(()) } @@ -153,7 +166,24 @@ impl Cmd { kind: TestErrorKind::InvalidPath, }); } - for file in find_all_json_tests(path) { + let scan = find_all_json_tests(path); + // Same hole as in the other modes, reported the way this mode reports a file it could + // not read: as a FILE_ERR that the tally's gate counts. + for err in &scan.errors { + println!("FILE_ERR\t{}\t{}", path.display(), err.replace('\n', " ")); + file_errors += 1; + } + if !self.keep_going && !scan.errors.is_empty() { + return Err(TestError { + name: "Path validation".to_string(), + path: path.display().to_string(), + kind: TestErrorKind::FixtureError(format!( + "{} path(s) could not be read", + scan.errors.len() + )), + }); + } + for file in scan.files { if self.keep_going { // A file the runner declines as a whole (an unreadable fixture, a filename on // the validation skip list) must not end the sweep either: record it and move @@ -210,6 +240,18 @@ impl Cmd { "Fill tally: OK={filled} ERR={errors} PANIC={panics} FILE_ERR={file_errors} \ SKIP_FILE={skipped_files} TOTAL={total}" ); + // A sweep that filled and declined nothing reached no unit at all: an empty corpus, or one + // whose every file was unreadable. Its zeroes are truthful and meaningless, so they must + // not read as a pass. + if total == 0 { + return Err(TestError { + name: "fill summary".to_string(), + path: String::new(), + kind: TestErrorKind::FixtureError( + "no unit was judged; the corpus is empty or unreachable".to_string(), + ), + }); + } if errors + panics + file_errors == 0 { return Ok(()); } @@ -227,11 +269,18 @@ impl Cmd { let base = self.resolve_diff_spec()?.expect("run_diff is only reached with --diff-spec"); // Clap's `requires = "bench_spec"` makes the target explicit before this point. let target = self.resolve_spec()?.expect("--diff-spec requires --bench-spec"); - let files = collect_fixture_files(&self.paths)?; + // The comparison is decided by Rex7's precision invariant, which relates Rex7 to Rex6 and + // states nothing about any other pair; running it over one would apply that licence where + // none was granted. + let specs = DiffSpecs::new(target, base).map_err(|detail| TestError { + name: "spec pair".to_string(), + path: String::new(), + kind: TestErrorKind::FixtureError(detail), + })?; + let scan = collect_fixture_files(&self.paths)?; - let specs = DiffSpecs { target, base }; let tally = run_diff( - files, + scan, DiffRunConfig { specs, single_thread: self.single_thread, @@ -281,7 +330,18 @@ impl Cmd { kind: TestErrorKind::InvalidPath, }); } - for file in find_all_json_tests(path) { + let scan = find_all_json_tests(path); + if let Some(err) = scan.errors.first() { + return Err(TestError { + name: "Path validation".to_string(), + path: path.display().to_string(), + kind: TestErrorKind::FixtureError(format!( + "{} path(s) could not be read; first: {err}", + scan.errors.len() + )), + }); + } + for file in scan.files { all.extend(bench_test_suite( &file, self.bench_runs, @@ -291,6 +351,21 @@ impl Cmd { } } + if all.is_empty() { + return Err(TestError { + name: "bench".to_string(), + path: self + .paths + .iter() + .map(|p| p.display().to_string()) + .collect::>() + .join(", "), + kind: TestErrorKind::FixtureError( + "no unit was benchmarked; the corpus is empty or unreachable".to_string(), + ), + }); + } + let bench_json = |u: &UnitBench| { json!({ "runs": u.runs, diff --git a/crates/state-test/tests/cli_exit.rs b/crates/state-test/tests/cli_exit.rs index a46448e4..7fd6f657 100644 --- a/crates/state-test/tests/cli_exit.rs +++ b/crates/state-test/tests/cli_exit.rs @@ -130,25 +130,90 @@ fn diff_run_with_no_unexplained_difference_exits_with_code_0() { } #[test] -fn diff_run_over_two_unrelated_specs_reports_unexplained_and_exits_1() { - // The Rex7 precision invariant relates Rex7 to Rex6 and says nothing about any other pair, so - // a Rex7-against-Equivalence difference carries no licensing evidence — the MegaETH intrinsic - // surcharge alone moves the receipt. This is the negative control for the gate: a classifier - // that explained everything would exit 0 here. +fn diff_run_over_an_unauthorized_spec_pair_is_refused() { + // Every rule in the classifier is a reading of one sentence, the Rex7 precision invariant, + // which relates Rex7 to Rex6 and states nothing about any other pair. Pointed at another pair + // it would grant a licence that pair never had — deciding, from mechanisms that are evidence + // for nothing there, that a difference is fine. It refuses instead of judging. let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); suite["exit_code_test"]["post"] = serde_json::json!({}); - let path = write_fixture("diff_fail.json", &serde_json::to_string(&suite).expect("serialize")); + let path = write_fixture("diff_pair.json", &serde_json::to_string(&suite).expect("serialize")); + let path = path.to_str().expect("utf8 path"); - let out = run_cli(&[ - path.to_str().expect("utf8 path"), - "--bench-spec", - "Rex7", - "--diff-spec", - "Equivalence", - ]); + for (target, base) in [("Rex7", "Equivalence"), ("Rex6", "Rex5"), ("Rex6", "Rex7")] { + let out = run_cli(&[path, "--bench-spec", target, "--diff-spec", base]); + assert_eq!(out.status.code(), Some(1), "{target} vs {base} must be refused"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("Rex7") && stderr.contains("Rex6"), + "the error should name the one supported pair: {stderr}" + ); + } +} + +#[test] +fn validate_run_that_judged_nothing_exits_1() { + // Same hole as in the differential mode, one mode over: a corpus whose every file is on the + // validation skip list walks files, reaches no unit, and reports zero errors. + let dir = std::env::temp_dir().join("state_test_cli_exit_all_skipped"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + std::fs::write(dir.join("ValueOverflow.json"), FAILING_SUITE).expect("write"); + + let out = run_cli(&[dir.to_str().expect("utf8 path")]); + assert_eq!(out.status.code(), Some(1), "a run that validated no unit must fail"); + assert!( + String::from_utf8_lossy(&out.stderr).contains("no fixture unit was validated"), + "stderr should say what was missing: {}", + String::from_utf8_lossy(&out.stderr) + ); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn diff_run_that_judged_nothing_exits_1() { + // A sweep whose corpus never arrived reaches the gate with an empty tally: zero panics, zero + // unexplained differences, every count truthful and meaningless. It must not read as a pass. + let dir = std::env::temp_dir().join("state_test_cli_exit_empty_corpus"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + + let out = + run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); + assert_eq!(out.status.code(), Some(1), "a corpus with no fixture in it must fail"); + + // A directory holding only fixtures on the validation skip list reaches the runner but judges + // no unit, which is the same hole one step further in. + std::fs::write(dir.join("ValueOverflow.json"), FAILING_SUITE).expect("write"); + let out = + run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); + assert_eq!(out.status.code(), Some(1), "a sweep that judged no unit must fail"); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn diff_run_with_an_unparseable_fixture_exits_1() { + // A file the sweep cannot parse is a fixture it did not judge. Skipping it quietly is how a + // corpus shrinks without anyone noticing. + let dir = std::env::temp_dir().join("state_test_cli_exit_bad_fixture"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + suite["exit_code_test"]["post"] = serde_json::json!({}); + std::fs::write(dir.join("good.json"), serde_json::to_string(&suite).expect("serialize")) + .expect("write"); + std::fs::write(dir.join("broken.json"), "{ not json").expect("write"); + + let out = + run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); let stdout = String::from_utf8_lossy(&out.stdout); - assert!(stdout.contains("UNEXPLAINED"), "tally should report the class: {stdout}"); - assert_eq!(out.status.code(), Some(1), "an unexplained difference must fail the gate"); + assert!( + stdout.lines().any(|l| l.split_whitespace().eq(["PASS", "1"])), + "the readable fixture still runs: {stdout}" + ); + assert!(stdout.contains("FILE_ERROR"), "the unreadable one is reported: {stdout}"); + assert_eq!(out.status.code(), Some(1), "a corpus the sweep only partly read is not a pass"); + let _ = std::fs::remove_dir_all(&dir); } #[test] diff --git a/tools/eest-sweep/baseline.json b/tools/eest-sweep/baseline.json index 671c29da..a80a8968 100644 --- a/tools/eest-sweep/baseline.json +++ b/tools/eest-sweep/baseline.json @@ -12,7 +12,7 @@ "mechanisms": { "destroyed_compute_gas": 10698, "detention_in_force": 610, - "exceptional_halt": 11379 + "exceptional_halt": 17363 }, "explainedFields": { "compute_gas_used": 17363 diff --git a/tools/eest-sweep/run.sh b/tools/eest-sweep/run.sh index 0f140c7d..21018ff7 100755 --- a/tools/eest-sweep/run.sh +++ b/tools/eest-sweep/run.sh @@ -99,12 +99,27 @@ if [ -z "$CORPUS_DIR" ]; then echo "==> corpus hash verified: $EEST_SHA256" CORPUS_DIR="$CACHE_DIR/$EEST_RELEASE/state_tests" - if [ ! -d "$CORPUS_DIR" ]; then + # A tree is reusable only if the run that produced it finished. Unpacking straight into the + # final path leaves a half-extracted tree behind on any interruption — a cancelled job, a full + # disk, a cache saved mid-write — and the next run finds a directory that exists, sweeps a + # fraction of the corpus, and reports a clean tally over it. The stamp is written last and names + # the archive hash, so it is a claim only a completed extraction of *this* archive can make. + STAMP="$CORPUS_DIR/.unpacked" + if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP" 2>/dev/null)" != "$EEST_SHA256" ]; then echo "==> unpacking state_tests" - mkdir -p "$CACHE_DIR/$EEST_RELEASE" + rm -rf "$CORPUS_DIR" + # Extract into a scratch directory and move it into place in one step: the final path either + # does not exist or holds a complete tree, never a partial one. + STAGE="$CACHE_DIR/.unpack.$$" + rm -rf "$STAGE" + mkdir -p "$STAGE" # Only `state_tests` is unpacked: the runner reads the state-test format, and the archive's # blockchain-test subtrees are several times larger. - tar -xzf "$ARCHIVE" -C "$CACHE_DIR/$EEST_RELEASE" --strip-components=1 fixtures/state_tests + tar -xzf "$ARCHIVE" -C "$STAGE" --strip-components=1 fixtures/state_tests + printf '%s' "$EEST_SHA256" >"$STAGE/state_tests/.unpacked" + mkdir -p "$CACHE_DIR/$EEST_RELEASE" + mv "$STAGE/state_tests" "$CORPUS_DIR" + rm -rf "$STAGE" fi fi @@ -114,6 +129,10 @@ if [ ! -d "$CORPUS_DIR" ]; then fi FIXTURE_COUNT="$(find "$CORPUS_DIR" -name '*.json' | wc -l | tr -d ' ')" echo "==> corpus: $CORPUS_DIR ($FIXTURE_COUNT fixture files)" +if [ "$FIXTURE_COUNT" -eq 0 ]; then + echo "corpus holds no fixtures: $CORPUS_DIR" >&2 + exit 1 +fi # --- binary ----------------------------------------------------------------------------------- @@ -176,6 +195,13 @@ fi field() { echo "$TALLY" | tr ' ' '\n' | grep "^$1=" | cut -d= -f2; } PANICS="$(field PANIC)" FILE_ERRS="$(field FILE_ERR)" +TOTAL="$(field TOTAL)" +# A run that reached no unit at all reports zero panics and zero file errors, truthfully, and +# says nothing. It is the one tally that must never pass. +if [ "${TOTAL:-0}" -eq 0 ]; then + echo "gate failed: the sweep judged no unit (TOTAL=0)" >&2 + exit 1 +fi if [ "${PANICS:-0}" -gt 0 ] || [ "${FILE_ERRS:-0}" -gt 0 ]; then echo "gate failed: PANIC=$PANICS FILE_ERR=$FILE_ERRS" >&2 exit 1 From 1648bf8a200d49ea6347978d0a9c28a90ff2f1f7 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 13:10:32 +0800 Subject: [PATCH 087/208] fix(state-test): count what a sweep judged, and re-derive that its corpus is whole MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A gate is only as good as what it counts and what it reads. `DiffSpecs::new` decides which spec pair the classifier may judge, and its fields were public: the pair it refuses could be assembled directly and handed to the classifier, which cannot tell one origin from the other. The fields are private now, behind accessors, and the property that the constructor is the only way in is pinned where it is true — from outside the crate, by a `compile_fail` example and by the integration tests. Three ways to pass by counting the wrong thing. Validation counted units walked, so a unit whose `post` is empty — nothing to check, nothing checked — made a file of them a passing run; it counts expectations judged. `--fill` gated its zero tally behind `--keep-going`, so the plain form exited 0 over a corpus that never arrived; the gate now covers both. And a fill counted units where every other mode counts transaction vectors, which over a multi-vector fixture makes the two sweeps' totals incomparable — `FillReport` reports one entry per vector, named the way the differential sweep names it. On the pinned corpus units and vectors coincide, so the tally is unchanged: 44023 either way, with fill's OK=36974 and ERR=7049 landing exactly on diff's PASS+EXPLAINED and SKIPPED. `--fill --force` also cleared a multi-vector unit's `out` unconditionally, dropping an expectation the fixture was entitled to. It is kept when every vector produces the same output, and the unit is refused when they differ, since one field cannot state a per-vector expectation. A unit is filled as a whole, so a vector that fails leaves the unit untouched and every one of its vectors says so. Finally, what the sweep reads. A cached corpus tree was trusted on a stamp naming the archive, which says only that some extraction of it once finished — a tree edited, truncated by a full disk, or restored intact from a cache archived mid-write still carries the stamp and still sweeps clean over a fraction of what the tally claims. The unpack now records a manifest of every file it extracted with that file's hash, and each run re-derives it from the bytes on disk before sweeping; anything missing, added or edited discards the tree. Unpacking is serialized by an atomic `mkdir` lock, and a run that cannot get it waits, then falls back to a private tree rather than writing where another process may be. `tools/eest-sweep/tests/cache_integrity.sh` drives all of that against a synthetic archive, per-PR in CI. --- .github/workflows/build-and-test.yml | 12 ++ .github/workflows/eest-nightly.yml | 12 +- crates/mega-state-test/AGENTS.md | 9 +- crates/mega-state-test/src/diff.rs | 62 ++++-- crates/mega-state-test/src/runner.rs | 224 +++++++++++++-------- crates/mega-state-test/tests/diff_mode.rs | 130 ++++++++++++- crates/state-test/README.md | 6 +- crates/state-test/src/main.rs | 37 ++-- crates/state-test/tests/cli_exit.rs | 119 +++++++++++- tools/eest-sweep/README.md | 22 +++ tools/eest-sweep/run.sh | 150 +++++++++++--- tools/eest-sweep/tests/cache_integrity.sh | 226 ++++++++++++++++++++++ 12 files changed, 849 insertions(+), 160 deletions(-) create mode 100755 tools/eest-sweep/tests/cache_integrity.sh diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fb57502e..b7b32d99 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -48,6 +48,18 @@ jobs: - name: Run Test run: cargo test --workspace + # The EEST sweep runs nightly, but the cache guards it rests on are shell, and a change that + # breaks them would otherwise surface as a green sweep over a fraction of the corpus. The suite + # drives `run.sh` against a synthetic archive and a stub binary, so it needs no corpus and no + # build. + corpus-cache: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Run corpus cache integrity suite + run: tools/eest-sweep/tests/cache_integrity.sh + no-std: runs-on: ubuntu-24.04 timeout-minutes: 10 diff --git a/.github/workflows/eest-nightly.yml b/.github/workflows/eest-nightly.yml index 538e9751..43240354 100644 --- a/.github/workflows/eest-nightly.yml +++ b/.github/workflows/eest-nightly.yml @@ -64,11 +64,13 @@ jobs: # belongs to the release the sweep is pinned to. # # The key is not what makes the entry trustworthy. The cache holds both the archive and the - # tree unpacked from it, and only the archive is hash-verified on every run; a tree saved - # from a cancelled job would be restored intact and swept as though it were whole. What - # rules that out is on the other side: `run.sh` unpacks into a scratch directory, moves it - # into place in one step, and writes a stamp naming this archive's hash only after the - # extraction finished. A tree without that stamp is discarded and unpacked again. + # tree unpacked from it; the archive is hash-verified on every run, and the tree — which is + # what the sweep actually reads — is restored from wherever a previous run left it, whole or + # not. What rules out sweeping a fraction of the corpus is on the other side: `run.sh` + # unpacks via a scratch directory and one rename, and records a manifest of every file it + # extracted with that file's hash. Before each run the tree is re-derived from its bytes and + # compared against that manifest; anything missing, added or edited discards the tree and + # unpacks it again. - name: Cache corpus uses: actions/cache@v4 with: diff --git a/crates/mega-state-test/AGENTS.md b/crates/mega-state-test/AGENTS.md index 5a4542af..ca2764da 100644 --- a/crates/mega-state-test/AGENTS.md +++ b/crates/mega-state-test/AGENTS.md @@ -23,7 +23,9 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Only an execution-provenance observation licenses anything. The fixture is the input under test, so a `Mechanism` read out of revert-payload bytes is reported and never falsifies a hypothesis; a derived quantity (the Rex7 destroyed remainder) needs an independent witness rather than certifying itself. - A differential run is defined for exactly one spec pair, the one whose precision invariant the classifier encodes (`DiffSpecs::new`). There is no general two-spec comparator. - A unit is a family of transactions, one per vector its `post` names (`TestUnit::vectors`). Diff, fill and bench each enumerate them; nothing takes index `{0,0,0}` and calls it the unit. -- Every mode fails when it judged no unit at all: an empty tally is truthful and meaningless, and a corpus that never arrived must not read as a pass. +- The transaction vector is the unit of counting everywhere (`FillReport::vectors`, `diff_test_suite`, validation's judged count), so one corpus produces one total whichever mode swept it. +- Every mode fails when it judged nothing: an empty tally is truthful and meaningless, and neither a corpus that never arrived nor a unit that pins no expectation may read as a pass. What counts is work actually judged — an expectation checked, a vector filled — never a file walked or a unit parsed. +- A value whose constructor is the check keeps its fields private (`DiffSpecs`), so the classifier cannot be handed a pair that was assembled around `new`. - Corpus drivers keep going per unit (`fill_test_suite_keep_going`, `diff_test_suite`) and record a unit's failure or panic rather than ending the file. - BaseFeeVault state changes are pruned as MegaETH-specific normalization. - The SALT bucket hasher comes from `mega_evm::AHashBucketHasher` (via the `test-utils` feature); never introduce a standalone salt/hasher dependency. @@ -33,6 +35,8 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Do not let a `Mechanism` inferred from bytes the fixture could have written falsify a hypothesis; `Mechanism::provenance` records where an observation came from and the licensing rule follows it. - Do not classify a halt by matching its `Debug` rendering; match the `MegaHaltReason` variants with no catch-all arm, so a new variant has to be decided rather than defaulted. - Do not drop an entry the fixture-discovery walk could not read; an unreadable directory is a hole in coverage, not an empty one. +- Do not count units, files, or anything else a run merely reached in a tally that gates a sweep; count the judgements it made. +- Do not write a unit's `post` for some of its vectors, and do not record a unit-wide field (`out`) for a multi-vector unit whose vectors disagree on it; refuse the unit instead. - Do not spread exception matching logic across multiple files. - Keep it centralized to avoid drift. - Do not bypass `compute_test_roots` when changing validation outputs. @@ -46,6 +50,7 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Change what a differential run compares or what licenses a difference: `diff.rs::{DiffField,Mechanism,Provenance,halt_kind,judge}`. - Change which spec pair a differential run accepts: `diff.rs::DiffSpecs::new`. - Change how a unit's transaction vectors are enumerated: `types/test_unit.rs::TestUnit::vectors`. -- Change the corpus sweep or its CI gates: `tools/eest-sweep/` and `.github/workflows/eest-nightly.yml`. +- Change what a fill records per unit or reports per vector: `runner.rs::{fill_unit,fill_suite,FillReport}`. +- Change the corpus sweep, how it decides a cached corpus is whole, or its CI gates: `tools/eest-sweep/` (`run.sh`, `tests/cache_integrity.sh`) and `.github/workflows/eest-nightly.yml`. - Update JSON schema mapping for test fixtures: `src/types/*` and deserializer modules. - Change CLI flags or path handling: `crates/state-test/src/main.rs`. diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index 6bc6d3e4..9666c13c 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -512,12 +512,33 @@ pub struct UnitDiff { /// invariant, which relates Rex7 to Rex6 and says nothing about any other pair. Pointed at /// Rex5-against-Rex4 it would apply Rex7's licence to a pair that never had one — deciding, from /// mechanisms that are not evidence for anything there, that a difference is fine. +/// +/// [`DiffSpecs::new`] is that restriction, so the fields it validates are private: a public field +/// is a second way to build the value, and the classifier cannot tell a pair that came through the +/// check from one that was assembled around it. +/// +/// ``` +/// use state_test::{diff::DiffSpecs, types::SpecName}; +/// +/// let (target, base) = DiffSpecs::SUPPORTED; +/// let specs = DiffSpecs::new(target, base).expect("the supported pair"); +/// assert_eq!((specs.target(), specs.base()), (SpecName::Rex7, SpecName::Rex6)); +/// assert!(DiffSpecs::new(SpecName::Rex6, SpecName::Rex5).is_err()); +/// ``` +/// +/// The same pair the constructor refuses, assembled directly, does not compile: +/// +/// ```compile_fail +/// use state_test::{diff::DiffSpecs, types::SpecName}; +/// +/// let specs = DiffSpecs { target: SpecName::Rex6, base: SpecName::Rex5 }; +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DiffSpecs { /// The spec under test, normally the unstable one. - pub target: SpecName, + target: SpecName, /// The frozen spec the target inherits from. - pub base: SpecName, + base: SpecName, } impl DiffSpecs { @@ -540,6 +561,16 @@ impl DiffSpecs { } Ok(Self { target, base }) } + + /// The spec under test. + pub const fn target(&self) -> SpecName { + self.target + } + + /// The frozen spec the target is judged against. + pub const fn base(&self) -> SpecName { + self.base + } } /// Runs one unit's transaction vector under both specs and classifies the result. @@ -552,8 +583,9 @@ pub fn diff_unit( specs: DiffSpecs, collect_evidence: bool, ) -> UnitDiffOutcome { - let target = execute_unit_outcome(unit, indexes, &specs.target, false); - let base = execute_unit_outcome(unit, indexes, &specs.base, false); + let (target_spec, base_spec) = (specs.target(), specs.base()); + let target = execute_unit_outcome(unit, indexes, &target_spec, false); + let base = execute_unit_outcome(unit, indexes, &base_spec, false); let (target, base) = match (target, base) { (Ok(t), Ok(b)) => (t, b), @@ -578,8 +610,8 @@ pub fn diff_unit( vec![], format!( "{} executed but {} rejected the transaction: {e}", - label(specs.target), - label(specs.base) + label(target_spec), + label(base_spec) ), ) } @@ -589,8 +621,8 @@ pub fn diff_unit( vec![], format!( "{} rejected the transaction but {} executed it: {e}", - label(specs.target), - label(specs.base) + label(target_spec), + label(base_spec) ), ) } @@ -610,8 +642,8 @@ pub fn diff_unit( // which sees the frames the transaction's own result hides. It costs an inspected execution // only for the units that reach here, instead of on every unit in the corpus. let (Ok(target), Ok(base)) = ( - execute_unit_outcome(unit, indexes, &specs.target, true), - execute_unit_outcome(unit, indexes, &specs.base, true), + execute_unit_outcome(unit, indexes, &target_spec, true), + execute_unit_outcome(unit, indexes, &base_spec, true), ) else { return verdict; }; @@ -1548,13 +1580,15 @@ mod tests { // The classifier reads one sentence — Rex7's precision invariant — and that sentence relates // exactly one pair of specs. Any other pair would be judged by a licence it was never given. + // + // That the constructor is the *only* way in is a property of the crate's boundary, which this + // module is inside of; it is pinned by the `compile_fail` example on [`DiffSpecs`] and by + // `tests/diff_mode.rs`, which are compiled as consumers. #[test] fn test_only_the_rex7_rex6_pair_can_be_constructed() { let (target, base) = DiffSpecs::SUPPORTED; - assert_eq!( - DiffSpecs::new(target, base).expect("the supported pair"), - DiffSpecs { target, base } - ); + let specs = DiffSpecs::new(target, base).expect("the supported pair"); + assert_eq!((specs.target(), specs.base()), (target, base)); for (t, b) in [ (SpecName::Rex7, SpecName::Equivalence), (SpecName::Rex6, SpecName::Rex5), diff --git a/crates/mega-state-test/src/runner.rs b/crates/mega-state-test/src/runner.rs index 8ad5e92e..1b97d602 100644 --- a/crates/mega-state-test/src/runner.rs +++ b/crates/mega-state-test/src/runner.rs @@ -473,6 +473,12 @@ pub(crate) fn resolve_chain_id(env: &Env) -> Result { /// Execute a single test suite file containing multiple tests /// +/// Returns the number of *expectations judged*: one per `post` entry the file's units declare and +/// this run actually checked. A unit is not itself a judgement — a `post` of `{}`, a `post` whose +/// vectors are all empty, and a `post` naming only the skipped Constantinople spec each leave the +/// unit walked and nothing about it verified. Counting units instead would report such a file as +/// covered, which is the shape an empty corpus and a truncated one both have. +/// /// # Arguments /// * `path` - Path to the JSON test file /// * `elapsed` - Shared counter for total execution time @@ -500,9 +506,8 @@ pub fn execute_test_suite( kind: e.into(), })?; - let mut units = 0usize; + let mut judged = 0usize; for (name, unit) in suite.0 { - units += 1; // Prepare initial state let cache_state = unit.state(); @@ -556,7 +561,12 @@ pub fn execute_test_suite( Err( TxBuildError::InvalidTransactionType | TxBuildError::UnexpectedException { .. }, - ) if test.expect_exception.is_some() => continue, + ) if test.expect_exception.is_some() => { + // The fixture asked for this failure and got it: the expectation was + // checked, even though nothing executed. + judged += 1; + continue; + } // Propagate the real underlying cause instead of masking // every failure as an unknown private key. Err(e) => { @@ -601,10 +611,11 @@ pub fn execute_test_suite( return Err(TestError { path, name, kind: e }); } + judged += 1; } } } - Ok(units) + Ok(judged) } /// Build the `MegaETH` external environment for a test unit, reproducing the @@ -976,31 +987,38 @@ impl UnitStatus { } } -/// What happened to one unit of a fixture file under a keep-going fill. +/// What happened to one transaction vector of a fixture unit under a keep-going fill. #[derive(Debug, Clone)] pub struct UnitFillResult { - /// The unit's key in the fixture's test-suite map. + /// The unit's key in the fixture's test-suite map, suffixed with this vector's indexes when + /// the unit declares more than one (see [`vector_label`]). pub name: String, - /// Whether the unit filled, failed, or panicked. + /// The transaction vector this entry reports on. + pub indexes: TxPartIndices, + /// Whether the vector filled, failed, or panicked. pub status: UnitStatus, } -/// Per-unit outcome of filling one fixture file with `keep_going` set. +/// Per-vector outcome of filling one fixture file with `keep_going` set. #[derive(Debug, Clone, Default)] pub struct FillReport { - /// One entry per unit in the file, in the file's own order. - pub units: Vec, + /// One entry per transaction vector of every unit in the file, in the file's own order. + /// + /// A vector rather than a unit, because a unit is a family of transactions and every other + /// mode judges each of them separately; counting units here would make a fill sweep and a + /// differential sweep report different totals over the same corpus. + pub vectors: Vec, } impl FillReport { - /// Number of units whose `post` was recomputed and written. + /// Number of transaction vectors whose expectation was recomputed and written. pub fn filled(&self) -> usize { - self.units.iter().filter(|u| u.status.is_ok()).count() + self.vectors.iter().filter(|v| v.status.is_ok()).count() } - /// Number of units that failed or panicked and kept their original `post`. + /// Number of vectors that failed or panicked and kept their original expectation. pub fn failed(&self) -> usize { - self.units.len() - self.filled() + self.vectors.len() - self.filled() } } @@ -1009,16 +1027,17 @@ impl FillReport { /// fixture that has no `post` yet (a hand-built or `prestateTracer`-snapshot /// case). It re-uses the same `execute_unit_collect` + [`Test::for_dump`] the /// dump path uses, so the result is a self-validating fixture that -/// [`execute_test_suite`] checks like any other. Returns the number of units -/// filled. +/// [`execute_test_suite`] checks like any other. Returns the number of +/// transaction vectors filled. /// /// `spec_override` selects the spec to execute/record under; when `None`, the /// unit's single existing `post` spec is used (so a fixture with an empty `post` /// must pass a spec). /// -/// Filling replaces the unit's entire `post` map (single spec, single index -/// `{0,0,0}`) with circularly-derived expectations, so a unit that already has a -/// non-empty `post` is refused unless `force` is set. +/// Filling replaces the unit's entire `post` map (a single spec, one entry per +/// transaction vector the unit declares) with circularly-derived expectations, +/// so a unit that already has a non-empty `post` is refused unless `force` is +/// set. /// /// The first unit that fails aborts the whole file. Use /// [`fill_test_suite_keep_going`] to fill the rest of a file whose units fail @@ -1036,11 +1055,12 @@ pub fn fill_test_suite( /// aborting the file at the first one. /// /// A unit that fails or panics keeps its original `post` and is reported in the -/// returned [`FillReport`]; the units that succeeded are still written. This is -/// what lets a corpus sweep run a multi-unit fixture without splitting it into -/// one file per unit first: an EEST fixture holds one unit per (test, fork) -/// pair, and under a spec override the ones that the runner declines are -/// exactly the ones a split sweep would have counted separately. +/// returned [`FillReport`], one entry per transaction vector; the units that +/// succeeded are still written. This is what lets a corpus sweep run a +/// multi-unit fixture without splitting it into one file per unit first: an EEST +/// fixture holds one unit per (test, fork) pair, and under a spec override the +/// ones that the runner declines are exactly the ones a split sweep would have +/// counted separately. /// /// Errors that belong to the *file* rather than to a unit — an unreadable or /// unparseable fixture, a filename on the validation skip list, a failed write — @@ -1088,18 +1108,18 @@ fn fill_suite( let mut out = std::collections::BTreeMap::new(); let mut any_filled = false; for (name, mut unit) in suite.0 { - match fill_unit(&mut unit, spec_override, force) { - Ok(()) => { - any_filled = true; - report.units.push(UnitFillResult { name: name.clone(), status: UnitStatus::Ok }); - } - Err(status) => { - if !keep_going { - let detail = status.message().unwrap_or("failed"); - return Err(fixture_err(format!("unit {name}: {detail}"))); - } - report.units.push(UnitFillResult { name: name.clone(), status }); + let results = fill_unit(&mut unit, spec_override, force); + // A unit's `post` is rewritten as a whole, so it either filled for every vector it + // declares or for none of them. + let multi = results.len() > 1; + any_filled |= results.iter().all(|(_, status)| status.is_ok()); + for (indexes, status) in results { + if !keep_going && !status.is_ok() { + let detail = status.message().unwrap_or("failed"); + return Err(fixture_err(format!("unit {name}: {detail}"))); } + let label = if multi { vector_label(&name, indexes) } else { name.clone() }; + report.vectors.push(UnitFillResult { name: label, indexes, status }); } out.insert(name, unit); } @@ -1120,7 +1140,13 @@ fn fill_suite( Ok(report) } -/// Recompute one unit's `post` in place, or report why it could not be filled. +/// Recompute one unit's `post` in place, or report why each of its vectors could not be filled. +/// +/// Returns one entry per transaction vector the unit declares, in ascending vector order, so a +/// caller's tally counts what every other mode counts. A unit's `post` map is rewritten in one +/// step — writing only the vectors that succeeded would delete the expectations of the ones that +/// did not — so a failure anywhere in the unit leaves all of its vectors unfilled, and each of +/// them says so. /// /// Execution runs under [`panic_capture::catch`] so that a `debug_assert!` a /// single fixture trips is that fixture's result rather than the run's. @@ -1128,11 +1154,15 @@ fn fill_unit( unit: &mut TestUnit, spec_override: Option, force: bool, -) -> Result<(), UnitStatus> { - let err = |msg: String| UnitStatus::Error(msg); +) -> Vec<(TxPartIndices, UnitStatus)> { + let vectors = unit.vectors(); + // A reason the whole unit cannot be filled is a reason none of its vectors can. + let reject_all = |msg: &str| -> Vec<(TxPartIndices, UnitStatus)> { + vectors.iter().map(|&i| (i, UnitStatus::Error(msg.to_string()))).collect() + }; if !force && unit.post.values().any(|tests| !tests.is_empty()) { - return Err(err("already has a post expectation; pass --force to overwrite".to_string())); + return reject_all("already has a post expectation; pass --force to overwrite"); } let spec = match spec_override { Some(s) => s, @@ -1140,45 +1170,66 @@ fn fill_unit( let mut specs = unit.post.keys(); match (specs.next(), specs.next()) { (Some(s), None) => *s, - _ => { - return Err( - err("has no single post spec; pass --bench-spec to fill".to_string()), - ) - } + _ => return reject_all("has no single post spec; pass --bench-spec to fill"), } } }; // Reject an unmapped spec at selection time, so the error names the unit // instead of surfacing from deep inside execution. if spec == SpecName::Unknown { - return Err(err("selects an unknown spec; pass a valid --bench-spec".to_string())); + return reject_all("selects an unknown spec; pass a valid --bench-spec"); } // Validation skips Constantinople (mirroring upstream revme), so a post // recorded under it would never be checked. if spec == SpecName::Constantinople { - return Err(err( - "validation skips Constantinople; a post filled under it would never be checked" - .to_string(), - )); + return reject_all( + "validation skips Constantinople; a post filled under it would never be checked", + ); } // One expectation per vector the unit declares, each carrying its own `indexes`. Recording a // single `{0,0,0}` entry would delete the other vectors' expectations along with the tests // that used them, and `--force` — which exists to overwrite a stale expectation — would be // the one flag that makes the loss silent. - let vectors = unit.vectors(); - let mut tests = Vec::with_capacity(vectors.len()); - let mut outputs = Vec::with_capacity(vectors.len()); - for indexes in vectors { - let executed = panic_capture::catch(|| execute_unit_collect(unit, indexes, &spec)) - .map_err(UnitStatus::Panic)? - .map_err(|e| { - err(format!( - "execute [d={},g={},v={}]: {e}", - indexes.data, indexes.gas, indexes.value - )) - })?; - outputs.push(executed.output.clone()); + // + // Every vector is executed even once one has failed: the verdict is per vector, and stopping + // at the first failure would leave the rest of them with none. + let mut runs = Vec::with_capacity(vectors.len()); + for &indexes in &vectors { + let result = panic_capture::catch(|| execute_unit_collect(unit, indexes, &spec)) + .map_err(UnitStatus::Panic) + .and_then(|r| { + r.map_err(|e| { + UnitStatus::Error(format!( + "execute [d={},g={},v={}]: {e}", + indexes.data, indexes.gas, indexes.value + )) + }) + }); + runs.push((indexes, result)); + } + if runs.iter().any(|(_, r)| r.is_err()) { + return runs + .into_iter() + .map(|(indexes, result)| match result { + Err(status) => (indexes, status), + Ok(_) => ( + indexes, + UnitStatus::Error( + "not filled: another vector of this unit failed, and a unit's post is \ + written as a whole" + .to_string(), + ), + ), + }) + .collect(); + } + + let mut tests = Vec::with_capacity(runs.len()); + let mut outputs = Vec::with_capacity(runs.len()); + for (indexes, result) in runs { + let executed = result.expect("every vector succeeded"); + outputs.push(executed.output); tests.push(Test::for_dump( indexes, executed.state_root, @@ -1188,16 +1239,22 @@ fn fill_unit( )); } - // `out` is one field for the whole unit, so it can only describe a unit that has one vector. - // For a multi-vector unit the per-vector outputs disagree in general; recording any one of - // them would assert it for all, so the field is cleared and the `post` entries carry the - // per-vector expectations instead. - unit.out = match outputs.as_slice() { - [single] => single.clone(), - _ => None, - }; + // `out` is one field for the whole unit, so it can only describe an output every vector + // produced. Vectors that agree — the single-vector case, and a multi-vector unit whose + // variations do not change what the transaction returns — keep it. Vectors that disagree have + // no `out` this schema can express: recording one vector's output would assert it for all, + // and clearing the field would drop an expectation the fixture is entitled to. Per-vector + // output is a schema change, so the unit is refused instead. + let (first, rest) = outputs.split_first().expect("a unit declares at least one vector"); + if !rest.iter().all(|output| output == first) { + return reject_all( + "vectors return different output, and `out` is one field for the whole unit; this \ + fixture schema has no per-vector output to record", + ); + } + unit.out = first.clone(); unit.post = std::collections::BTreeMap::from([(spec, tests)]); - Ok(()) + vectors.iter().map(|&i| (i, UnitStatus::Ok)).collect() } pub(crate) fn prune_base_fee_vault_changes(db: &mut State) { @@ -1273,9 +1330,9 @@ impl TestRunnerConfig { #[derive(Clone)] struct TestRunnerState { n_errors: Arc, - /// Fixture units the workers reached. A run that reached none validated nothing, however - /// many files it walked. - n_units: Arc, + /// Expectations the workers actually checked, one per `post` entry judged. A run that checked + /// none validated nothing, however many files it walked and however many units they held. + n_judged: Arc, console_bar: Arc, queue: Arc)>>, elapsed: Arc>, @@ -1286,7 +1343,7 @@ impl TestRunnerState { let n_files = test_files.len(); Self { n_errors: Arc::new(AtomicUsize::new(0)), - n_units: Arc::new(AtomicUsize::new(0)), + n_judged: Arc::new(AtomicUsize::new(0)), console_bar: Arc::new(ProgressBar::with_draw_target( Some(n_files as u64), ProgressDrawTarget::stdout(), @@ -1321,8 +1378,8 @@ fn run_test_worker(state: TestRunnerState, config: TestRunnerConfig) -> Result<( state.console_bar.inc(1); match result { - Ok(units) => { - state.n_units.fetch_add(units, Ordering::SeqCst); + Ok(judged) => { + state.n_judged.fetch_add(judged, Ordering::SeqCst); } Err(err) => { state.n_errors.fetch_add(1, Ordering::SeqCst); @@ -1398,18 +1455,19 @@ pub fn run( let n_errors = state.n_errors.load(Ordering::SeqCst); let n_thread_errors = thread_errors.len(); - let n_units = state.n_units.load(Ordering::SeqCst); + let n_judged = state.n_judged.load(Ordering::SeqCst); - // A run that reached no unit at all validated nothing, and "0 errors out of 0" is a truthful - // report of that. An empty corpus, or one whose every file is on the skip list, must not + // A run that checked no expectation validated nothing, and "0 errors out of 0" is a truthful + // report of that. An empty corpus, one whose every file is on the skip list, and one whose + // units declare no `post` to check all reach this point the same way, and none of them may // print "All tests passed!". - if n_errors == 0 && n_thread_errors == 0 && n_units == 0 { + if n_errors == 0 && n_thread_errors == 0 && n_judged == 0 { return Err(TestError { name: "summary".to_string(), path: String::new(), kind: TestErrorKind::FixtureError(format!( - "no fixture unit was validated across {n_files} file(s); the corpus is empty or \ - entirely skipped" + "no fixture expectation was validated across {n_files} file(s); the corpus is \ + empty, entirely skipped, or its units declare no post expectation" )), }); } diff --git a/crates/mega-state-test/tests/diff_mode.rs b/crates/mega-state-test/tests/diff_mode.rs index e35cb051..60d96b55 100644 --- a/crates/mega-state-test/tests/diff_mode.rs +++ b/crates/mega-state-test/tests/diff_mode.rs @@ -12,10 +12,16 @@ use state_test::{ collect_fixture_files, compare, diff_test_suite, diff_unit, execute_unit_outcome, judge, run_diff, DiffClass, DiffRunConfig, DiffSpecs, Mechanism, SpecOutcome, }, - runner::{fill_test_suite, fill_test_suite_keep_going, FixtureScan, UnitStatus}, + runner::{ + execute_test_suite, fill_test_suite, fill_test_suite_keep_going, FixtureScan, UnitStatus, + }, types::{SpecName, TestUnit, TxPartIndices}, }; -use std::path::PathBuf; +use std::{ + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Duration, +}; const SENDER: &str = "0x1000000000000000000000000000000000000001"; const CALLEE: &str = "0x2000000000000000000000000000000000000002"; @@ -362,7 +368,7 @@ fn test_fill_records_one_expectation_per_vector() { let path = write_suite("fill_two_vectors.json", &[("family", two_vector_json())]); let report = fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill the file"); - assert_eq!(report.filled(), 1); + assert_eq!(report.filled(), 2, "the tally counts vectors, and this unit declares two"); let written: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&path).expect("read")).expect("json"); @@ -376,6 +382,122 @@ fn test_fill_records_one_expectation_per_vector() { ); } +/// `CALLDATACOPY(0, 0, CALLDATASIZE); RETURN(0, CALLDATASIZE)` — returns whatever it was called +/// with, so two vectors that send different calldata produce different output. +const ECHO_CALLDATA: &str = "0x366000600037366000f3"; + +/// `MSTORE(0, 42); RETURN(0, 32)` — returns the same word whatever it was called with. +const RETURN_CONSTANT: &str = "0x602a60005260206000f3"; + +/// Re-runs validation over a fixture and returns how many expectations it checked. +fn validate(path: &Path) -> usize { + let elapsed = Arc::new(Mutex::new(Duration::ZERO)); + execute_test_suite(path, &elapsed, false, false).expect("the filled fixture self-validates") +} + +/// [`two_vector_json`] with a chosen callee, so the two vectors' outputs can be made to agree or +/// to differ. +fn two_vector_json_with_callee(code: &str) -> serde_json::Value { + let mut json = two_vector_json(); + json["pre"][CALLEE]["code"] = serde_json::json!(code); + json +} + +// `out` is one field for the whole unit, and a multi-vector unit only has an output to record +// when its vectors agree on one. Clearing it unconditionally drops an expectation the fixture was +// entitled to — quietly, since the result still parses and still validates. +#[test] +fn test_fill_keeps_a_multi_vector_out_when_the_vectors_agree() { + let path = write_suite( + "fill_out_agree.json", + &[("family", two_vector_json_with_callee(RETURN_CONSTANT))], + ); + let report = + fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill the file"); + assert_eq!(report.filled(), 2); + + let written: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read")).expect("json"); + assert_eq!( + written["family"]["out"], + serde_json::json!("0x000000000000000000000000000000000000000000000000000000000000002a"), + "both vectors return that word, so the unit has one output: {written}" + ); + // And the recorded output is checked, rather than merely stored. + assert_eq!(validate(&path), 2); +} + +// The other half: vectors that return different outputs have no `out` this schema can express. +// Recording one vector's output would assert it for every vector, and per-vector output is a +// schema change, so the unit is refused with a reason instead of filled with a claim. +#[test] +fn test_fill_refuses_a_multi_vector_unit_whose_outputs_disagree() { + let unit = two_vector_json_with_callee(ECHO_CALLDATA); + let path = write_suite("fill_out_disagree.json", &[("family", unit)]); + let before = std::fs::read_to_string(&path).expect("read"); + + let report = + fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill the file"); + assert_eq!(report.filled(), 0, "{:?}", report.vectors); + assert_eq!(report.failed(), 2, "the refusal covers every vector of the unit"); + for vector in &report.vectors { + let message = vector.status.message().expect("a refused vector carries its reason"); + assert!( + message.contains("different output"), + "the reason should name what could not be recorded: {message}" + ); + } + assert_eq!(std::fs::read_to_string(&path).expect("read"), before, "file must be untouched"); +} + +// Fill and diff sweep the same corpus and print a total each. Counting units in one and vectors in +// the other makes those totals disagree over any multi-vector fixture — and a tally that cannot be +// compared against the other mode's, or against a baseline taken under the other mode, is a number +// nobody can act on. +#[test] +fn test_fill_and_diff_count_the_same_vectors() { + let units = [ + ("family", two_vector_json()), + ("single", unit_json("0x", None)), + ("another_family", two_vector_json()), + ]; + let path = write_suite("count_parity.json", &units); + + let diffs = diff_test_suite(&path, rex7_over_rex6(), true).expect("diff suite"); + let report = + fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill the file"); + + assert_eq!(report.vectors.len(), 5, "two units of two vectors and one of one"); + assert_eq!(report.vectors.len(), diffs.len(), "the two modes count the same things"); + + let mut filled: Vec<&str> = report.vectors.iter().map(|v| v.name.as_str()).collect(); + let mut judged: Vec<&str> = diffs.iter().map(|d| d.name.as_str()).collect(); + filled.sort_unstable(); + judged.sort_unstable(); + assert_eq!(filled, judged, "and name them the same way"); +} + +// Every pair but one is refused at construction, and the constructor is the only way to build the +// value: the fields it validates are private, which this file — compiled as a consumer of the +// crate — can only observe through the accessors. The compile-time half of that is pinned by the +// `compile_fail` example on `DiffSpecs`. +#[test] +fn test_diff_specs_is_only_reachable_through_its_constructor() { + let (target, base) = DiffSpecs::SUPPORTED; + let specs = DiffSpecs::new(target, base).expect("the supported pair"); + assert_eq!((specs.target(), specs.base()), (SpecName::Rex7, SpecName::Rex6)); + + for (t, b) in [ + (SpecName::Rex7, SpecName::Equivalence), + (SpecName::Rex6, SpecName::Rex5), + (SpecName::Rex6, SpecName::Rex7), + (SpecName::Rex7, SpecName::Rex7), + ] { + let err = DiffSpecs::new(t, b).expect_err("only one pair has an invariant"); + assert!(err.contains("Rex7") && err.contains("Rex6"), "name the supported pair: {err}"); + } +} + // Keep-going fill: one unit's failure must cost that unit, not the units after it in the same // file. Without this, a corpus sweep has to split every multi-unit fixture first. #[test] @@ -397,7 +519,7 @@ fn test_keep_going_fill_isolates_one_unit_failure() { let report = fill_test_suite_keep_going(&path, Some(SpecName::Rex7), true).expect("fill"); assert_eq!(report.filled(), 2); assert_eq!(report.failed(), 1); - let failed = report.units.iter().find(|u| !u.status.is_ok()).expect("one unit failed"); + let failed = report.vectors.iter().find(|v| !v.status.is_ok()).expect("one vector failed"); assert_eq!(failed.name, "b_broken"); assert!(matches!(failed.status, UnitStatus::Error(_)), "{:?}", failed.status); diff --git a/crates/state-test/README.md b/crates/state-test/README.md index c6f87eb9..bf49285b 100644 --- a/crates/state-test/README.md +++ b/crates/state-test/README.md @@ -13,9 +13,9 @@ Every mode operates on self-contained EEST fixtures (`TestUnit { env, pre, trans - **Validate** (default) — `state-test ` executes each fixture and checks its recorded `post` (state root, logs root, gas, status). This is how the official Ethereum tests and the replay corpus (`bench/replay/fixtures/`, via `replay_corpus.rs`) are checked. - **`--bench`** — `state-test --bench [--bench-runs N] [--bench-warmup W] [--bench-spec SPEC] ` times each fixture's isolated EVM execution and prints `{ gas_used, success, bench: { min/median/mean, mgasPerSec } }` as JSON instead of validating. This is the only EVM-throughput benchmark entry point; the replay-throughput benchmark (`bench/replay/run.py`) drives it. -- **`--fill`** — `state-test --fill --bench-spec SPEC ` computes each fixture's `post` and writes it back in place (atomically, via a temp file). This is the offline analog of `mega-evme replay --dump-fixture`'s post-fill step, for a fixture that has no on-chain origin (a hand-built case, or a `prestateTracer` snapshot such as `bench/replay/fixtures/attack_deploy.json`). After filling, the fixture is self-validating like any dumped one. One expectation is recorded per transaction vector the unit declares, each keeping its own `indexes`. A fixture that already has a non-empty `post` is refused unless `--force` is passed — filling replaces the whole `post` map with circularly-derived expectations, so an accidental run against real expectations (e.g. the official test suites) would destroy them. Filenames on the validation skip list and the Constantinople spec are refused outright, since validation would never check the result. +- **`--fill`** — `state-test --fill --bench-spec SPEC ` computes each fixture's `post` and writes it back in place (atomically, via a temp file). This is the offline analog of `mega-evme replay --dump-fixture`'s post-fill step, for a fixture that has no on-chain origin (a hand-built case, or a `prestateTracer` snapshot such as `bench/replay/fixtures/attack_deploy.json`). After filling, the fixture is self-validating like any dumped one. One expectation is recorded per transaction vector the unit declares, each keeping its own `indexes`, and a unit is written as a whole — a vector that cannot be executed leaves the unit's `post` untouched rather than half-rewritten. The unit-wide `out` field is recorded when every vector produces the same output and the unit is refused when they differ, since one field cannot state a per-vector expectation. A fixture that already has a non-empty `post` is refused unless `--force` is passed — filling replaces the whole `post` map with circularly-derived expectations, so an accidental run against real expectations (e.g. the official test suites) would destroy them. Filenames on the validation skip list and the Constantinople spec are refused outright, since validation would never check the result. - **`--diff-spec`** — `state-test --bench-spec Rex7 --diff-spec Rex6 ` executes each fixture under both specs and classifies how they differ. Nothing is written and no recorded `post` is consulted: the two executions are compared against each other. This is the only check available for a spec nobody has computed expectations for yet — the frozen spec it inherits from is the oracle, and that spec's precision invariant is what says when the two are allowed to disagree. That invariant is Rex7's and relates Rex7 to Rex6, so Rex7-against-Rex6 is the only pair accepted; any other is refused rather than judged by a licence it was never granted. See `crates/mega-state-test/src/diff.rs` for the classification and `tools/eest-sweep/` for the corpus driver built on it. -- **`--keep-going`** — with `--fill`, records each unit's failure (or panic) and carries on with the rest of its file instead of aborting at the first one, then prints a `Fill tally:` line. Without it, one bad unit ends the whole run, which is why a corpus sweep used to have to split every multi-unit fixture into one file per unit first. +- **`--keep-going`** — with `--fill`, records each vector's failure (or panic) and carries on with the rest of its file instead of aborting at the first one, then prints a `Fill tally:` line. Without it, one bad unit ends the whole run, which is why a corpus sweep used to have to split every multi-unit fixture into one file per unit first. The tally counts transaction vectors, the same unit of work `--diff-spec` counts, so the two modes' totals over one corpus can be compared with each other and against a baseline. `--bench-spec` selects the spec to run under; without it, the fixture's single `post` spec is used (so `--fill` needs it when the `post` is still empty, and `--diff-spec` requires it outright). @@ -25,4 +25,4 @@ A state-test unit is a family of transactions, not one: `transaction` holds arra ## Exit codes -Every mode exits 1 on failure and 0 otherwise, and "judged nothing" counts as a failure in all of them: a run whose corpus was empty, unreachable, or entirely unreadable reports zeroes that are truthful and meaningless, and must not read as a pass. A `--diff-spec` run additionally fails on a panic, on an unexplained difference, and on any file it could not read or parse. \ No newline at end of file +Every mode exits 1 on failure and 0 otherwise, and "judged nothing" counts as a failure in all of them, with or without `--keep-going`: a run whose corpus was empty, unreachable, or entirely unreadable reports zeroes that are truthful and meaningless, and must not read as a pass. Validation counts the expectations it checked rather than the units it walked, so a unit whose `post` is empty — nothing to check, nothing checked — fails the same way an empty corpus does. A `--diff-spec` run additionally fails on a panic, on an unexplained difference, and on any file it could not read or parse. \ No newline at end of file diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 92a961d6..01b6852c 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -206,18 +206,18 @@ impl Cmd { continue; } }; - for unit in &report.units { - match &unit.status { + for vector in &report.vectors { + match &vector.status { UnitStatus::Ok => {} UnitStatus::Error(m) => { - println!("ERR\t{}::{}\t{m}", file.display(), unit.name); + println!("ERR\t{}::{}\t{m}", file.display(), vector.name); errors += 1; } UnitStatus::Panic(m) => { println!( "PANIC\t{}::{}\t{}", file.display(), - unit.name, + vector.name, m.replace('\n', " ") ); panics += 1; @@ -227,31 +227,36 @@ impl Cmd { filled += report.filled(); } else { let n = fill_test_suite(&file, spec_override, self.force)?; - println!("Filled post for {n} unit(s) in {}", file.display()); + println!("Filled post for {n} transaction vector(s) in {}", file.display()); filled += n; } } } - if !self.keep_going { - return Ok(()); - } + // A sweep that filled and declined nothing reached no transaction vector at all: an empty + // corpus, or one whose every file was unreadable or skipped. Its zeroes are truthful and + // meaningless, so they must not read as a pass — in either mode. Without `--keep-going` + // the run stops at the first failure, which says nothing about the case where there was + // no work to fail at. let total = filled + errors + panics; - println!( - "Fill tally: OK={filled} ERR={errors} PANIC={panics} FILE_ERR={file_errors} \ - SKIP_FILE={skipped_files} TOTAL={total}" - ); - // A sweep that filled and declined nothing reached no unit at all: an empty corpus, or one - // whose every file was unreadable. Its zeroes are truthful and meaningless, so they must - // not read as a pass. + if self.keep_going { + println!( + "Fill tally: OK={filled} ERR={errors} PANIC={panics} FILE_ERR={file_errors} \ + SKIP_FILE={skipped_files} TOTAL={total}" + ); + } if total == 0 { return Err(TestError { name: "fill summary".to_string(), path: String::new(), kind: TestErrorKind::FixtureError( - "no unit was judged; the corpus is empty or unreachable".to_string(), + "no transaction vector was filled; the corpus is empty or unreachable" + .to_string(), ), }); } + if !self.keep_going { + return Ok(()); + } if errors + panics + file_errors == 0 { return Ok(()); } diff --git a/crates/state-test/tests/cli_exit.rs b/crates/state-test/tests/cli_exit.rs index 7fd6f657..042ed6fe 100644 --- a/crates/state-test/tests/cli_exit.rs +++ b/crates/state-test/tests/cli_exit.rs @@ -62,6 +62,10 @@ fn run_cli(args: &[&str]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_state-test")).args(args).output().expect("spawn state-test") } +fn stderr(out: &std::process::Output) -> String { + String::from_utf8_lossy(&out.stderr).into_owned() +} + #[test] fn failing_tests_exit_with_code_1() { let path = write_fixture("failing.json", FAILING_SUITE); @@ -89,15 +93,114 @@ fn invalid_path_exits_with_code_1() { #[test] fn passing_run_exits_with_code_0() { - // The same unit with an empty `post` validates trivially: the run completes - // with zero errors and must keep exiting 0. + // A fixture whose recorded roots are the ones its execution produces. `--fill` computes them, + // which is also what makes this a run with something in it to pass: the expectation exists and + // is checked. let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); suite["exit_code_test"]["post"] = serde_json::json!({}); - let passing = serde_json::to_string(&suite).expect("serialize"); + let path = write_fixture("passing.json", &serde_json::to_string(&suite).expect("serialize")); + let path = path.to_str().expect("utf8 path"); - let path = write_fixture("passing.json", &passing); + let out = run_cli(&[path, "--fill", "--bench-spec", "Rex5"]); + assert_eq!(out.status.code(), Some(0), "fill must succeed: {}", stderr(&out)); + + let out = run_cli(&[path]); + assert_eq!(out.status.code(), Some(0), "passing run must exit 0: {}", stderr(&out)); + assert!( + String::from_utf8_lossy(&out.stdout).contains("All tests passed!"), + "and say so: {}", + String::from_utf8_lossy(&out.stdout) + ); +} + +#[test] +fn validate_run_over_a_unit_with_no_expectation_exits_1() { + // A unit whose `post` is empty is walked, executed against nothing, and counted by nothing. + // Reading that as a pass makes "the runner checked this file" true of a file that pins no + // behavior at all — and `--fill --force` writing an empty `post` is one bug away. + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + suite["exit_code_test"]["post"] = serde_json::json!({}); + let path = write_fixture("no_expectation.json", &serde_json::to_string(&suite).expect("ser")); + + let out = run_cli(&[path.to_str().expect("utf8 path")]); + assert_eq!(out.status.code(), Some(1), "a unit that pins nothing is not a passing run"); + assert!( + stderr(&out).contains("no fixture expectation was validated"), + "stderr should say what was missing: {}", + stderr(&out) + ); + + // The same file with a `post` that holds an empty vector list: a unit, a spec, and still no + // expectation to check. + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + suite["exit_code_test"]["post"] = serde_json::json!({ "Rex5": [] }); + let path = write_fixture("empty_vector_list.json", &serde_json::to_string(&suite).expect("s")); let out = run_cli(&[path.to_str().expect("utf8 path")]); - assert_eq!(out.status.code(), Some(0), "passing run must exit 0"); + assert_eq!(out.status.code(), Some(1), "an empty vector list judges nothing either"); +} + +#[test] +fn fill_that_filled_nothing_exits_1() { + // `--keep-going` decides when a run stops, not whether an empty one counts. Without it the + // fill loop simply has nothing to fail at, so a corpus that never arrived walks no file, + // writes no fixture, and used to exit 0 — the one report that must never read as a pass. + let dir = std::env::temp_dir().join("state_test_cli_exit_empty_fill"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + let dir_arg = dir.to_str().expect("utf8 path"); + + for args in [ + vec![dir_arg, "--fill", "--bench-spec", "Rex7"], + vec![dir_arg, "--fill", "--keep-going", "--bench-spec", "Rex7"], + ] { + let out = run_cli(&args); + assert_eq!(out.status.code(), Some(1), "a fill that filled nothing must fail: {args:?}"); + assert!( + stderr(&out).contains("no transaction vector was filled"), + "stderr should say what was missing: {}", + stderr(&out) + ); + } + + // A corpus of nothing but files the runner skips by name reaches the fill loop and still + // fills nothing. + std::fs::write(dir.join("ValueOverflow.json"), FAILING_SUITE).expect("write"); + let out = run_cli(&[dir_arg, "--fill", "--keep-going", "--force", "--bench-spec", "Rex7"]); + assert_eq!(out.status.code(), Some(1), "a skipped-only corpus fills nothing"); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn fill_tally_counts_transaction_vectors() { + // The tally a sweep gates on has to count what the differential sweep counts, or the two + // numbers cannot be compared with each other or against a baseline recorded under the other + // mode. A unit is a family of transactions; the vector is the unit both modes agree on. + let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); + suite["exit_code_test"]["transaction"]["data"] = serde_json::json!(["0x", "0xdeadbeef"]); + let entry = |data: usize| { + serde_json::json!({ + "indexes": { "data": data, "gas": 0, "value": 0 }, + "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "logs": "0x0000000000000000000000000000000000000000000000000000000000000000" + }) + }; + suite["exit_code_test"]["post"] = serde_json::json!({ "Rex5": [entry(0), entry(1)] }); + let path = write_fixture("tally_vectors.json", &serde_json::to_string(&suite).expect("ser")); + let path = path.to_str().expect("utf8 path"); + + let out = run_cli(&[path, "--fill", "--force", "--keep-going", "--bench-spec", "Rex7"]); + assert_eq!(out.status.code(), Some(0), "{}", stderr(&out)); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!( + stdout.contains("Fill tally: OK=2 ERR=0 PANIC=0 FILE_ERR=0 SKIP_FILE=0 TOTAL=2"), + "one unit, two vectors, two filled: {stdout}" + ); + + // The differential sweep over the same fixture reports the same total. + let out = run_cli(&[path, "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); + assert_eq!(out.status.code(), Some(0), "{}", stderr(&out)); + let stdout = String::from_utf8_lossy(&out.stdout); + assert!(stdout.contains("over 2 unit(s)"), "the same two vectors: {stdout}"); } #[test] @@ -161,11 +264,11 @@ fn validate_run_that_judged_nothing_exits_1() { std::fs::write(dir.join("ValueOverflow.json"), FAILING_SUITE).expect("write"); let out = run_cli(&[dir.to_str().expect("utf8 path")]); - assert_eq!(out.status.code(), Some(1), "a run that validated no unit must fail"); + assert_eq!(out.status.code(), Some(1), "a run that validated nothing must fail"); assert!( - String::from_utf8_lossy(&out.stderr).contains("no fixture unit was validated"), + stderr(&out).contains("no fixture expectation was validated"), "stderr should say what was missing: {}", - String::from_utf8_lossy(&out.stderr) + stderr(&out) ); let _ = std::fs::remove_dir_all(&dir); } diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md index dc05f18d..e5559e6e 100644 --- a/tools/eest-sweep/README.md +++ b/tools/eest-sweep/README.md @@ -46,6 +46,28 @@ it and warns in the job summary when the coverage numbers move — a corpus that change that pushed thousands of fixtures out of execution, is worth seeing even though it is not a defect. Update it deliberately when a move is expected. +## The cached corpus + +The archive is verified against the hash in `corpus.env` on every run, and the tree unpacked from +it is verified against a manifest the unpack wrote: every file that was extracted, with that file's +hash. Before a cached tree is swept, that manifest is re-derived from the bytes on disk and +compared; a file missing, added or edited discards the tree and unpacks it again. + +The tree is what the sweep actually reads, and a cached one can be short of the corpus in ways +nothing about it announces — an extraction cut off by a cancelled job or a full disk, a CI cache +archived mid-write and restored intact, a stray edit under the cache directory. Each of those +leaves a directory that exists and sweeps clean over a fraction of what the tally claims, which is +the one failure mode a coverage number cannot show. + +Unpacking is serialized by an atomic `mkdir` lock, so two runs sharing a cache directory do not +extract into the same destination at once. A run that finds the lock held waits for it, and if the +wait runs out — the lock's owner died, or is very slow — unpacks a private tree of its own rather +than reaching into a directory another process may still be writing. A lock left behind by a dead +run is cleared by removing `/.unpack.lock`. + +`tests/cache_integrity.sh` drives all of this against a synthetic archive and a stub binary; it +runs per-PR in CI and needs neither the corpus nor a build. + ## Options ``` diff --git a/tools/eest-sweep/run.sh b/tools/eest-sweep/run.sh index 21018ff7..028c5375 100755 --- a/tools/eest-sweep/run.sh +++ b/tools/eest-sweep/run.sh @@ -63,14 +63,96 @@ case "$MODE" in esac # `sha256sum` on Linux, `shasum -a 256` on macOS. +if command -v sha256sum >/dev/null 2>&1; then + SHA256=(sha256sum) +else + SHA256=(shasum -a 256) +fi sha256_of() { - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | cut -d' ' -f1 - else - shasum -a 256 "$1" | cut -d' ' -f1 + "${SHA256[@]}" "$1" | cut -d' ' -f1 +} + +# How long to wait for another run that is already unpacking this corpus, before giving up on the +# shared tree and unpacking a private one. A full extraction is well under a minute; the override +# is for the tests, and for a machine whose lock is known to be stale. +LOCK_WAIT_SECS="${EEST_UNPACK_LOCK_WAIT_SECS:-900}" + +# Name of the manifest inside an unpacked tree. Excluded from its own listing. +MANIFEST_NAME=".manifest" + +# Every file of an unpacked tree with its hash, in a byte-stable order. +corpus_manifest() { + (cd "$1" && find . -type f ! -name "$MANIFEST_NAME" -print0 | + LC_ALL=C sort -z | + xargs -0 "${SHA256[@]}") +} + +# The manifest covers regular files, which is everything the fixture archive holds. An entry of +# any other kind is outside what it can speak for — a symlink is not hashed, so swapping its +# target would leave the manifest matching — so such a tree is rejected rather than described. +has_irregular_entry() { + [ -n "$(find "$1" ! -type d ! -type f -print -quit)" ] +} + +# Whether a tree is exactly what this archive unpacks to: the manifest names this archive, and +# every file in the tree still hashes to what the manifest recorded — with no file added, removed +# or rewritten since. This is what the sweep's coverage rests on, so it is re-derived from the +# bytes on every run rather than trusted from a marker a previous run left behind. +corpus_is_intact() { + local root="$1" manifest="$1/$MANIFEST_NAME" actual status + [ -f "$manifest" ] || return 1 + [ "$(head -n 1 "$manifest")" = "archive-sha256 $EEST_SHA256" ] || return 1 + has_irregular_entry "$root" && return 1 + actual="$(mktemp)" + if ! corpus_manifest "$root" >"$actual" 2>/dev/null; then + rm -f "$actual" + return 1 + fi + status=0 + tail -n +2 "$manifest" | cmp -s - "$actual" || status=1 + rm -f "$actual" + return "$status" +} + +# Unpack the archive into $1, manifest and all, via a scratch directory and one rename: the +# destination either does not exist or holds a tree this function extracted whole. +unpack_corpus() { + local dest="$1" stage="$CACHE_DIR/.unpack.$$" + rm -rf "$stage" + mkdir -p "$stage" + # Only `state_tests` is unpacked: the runner reads the state-test format, and the archive's + # blockchain-test subtrees are several times larger. + tar -xzf "$ARCHIVE" -C "$stage" --strip-components=1 fixtures/state_tests + if has_irregular_entry "$stage/state_tests"; then + echo "the archive unpacks to something other than a tree of regular files, which the corpus" >&2 + echo "manifest cannot describe; teach corpus_manifest about it before sweeping." >&2 + exit 1 fi + { + echo "archive-sha256 $EEST_SHA256" + corpus_manifest "$stage/state_tests" + } >"$stage/state_tests/$MANIFEST_NAME" + mkdir -p "$(dirname "$dest")" + rm -rf "$dest" + mv "$stage/state_tests" "$dest" + rm -rf "$stage" } +# The unpack lock and any private tree this run extracted, released however the run ends. +UNPACK_LOCK="" +LOCK_HELD=0 +PRIVATE_ROOT="" +cleanup() { + if [ "$LOCK_HELD" -eq 1 ]; then + rm -rf "$UNPACK_LOCK" + LOCK_HELD=0 + fi + if [ -n "$PRIVATE_ROOT" ]; then + rm -rf "$PRIVATE_ROOT" + fi +} +trap cleanup EXIT + mkdir -p "$REPORT_DIR" # --- corpus ----------------------------------------------------------------------------------- @@ -99,27 +181,45 @@ if [ -z "$CORPUS_DIR" ]; then echo "==> corpus hash verified: $EEST_SHA256" CORPUS_DIR="$CACHE_DIR/$EEST_RELEASE/state_tests" - # A tree is reusable only if the run that produced it finished. Unpacking straight into the - # final path leaves a half-extracted tree behind on any interruption — a cancelled job, a full - # disk, a cache saved mid-write — and the next run finds a directory that exists, sweeps a - # fraction of the corpus, and reports a clean tally over it. The stamp is written last and names - # the archive hash, so it is a claim only a completed extraction of *this* archive can make. - STAMP="$CORPUS_DIR/.unpacked" - if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP" 2>/dev/null)" != "$EEST_SHA256" ]; then - echo "==> unpacking state_tests" - rm -rf "$CORPUS_DIR" - # Extract into a scratch directory and move it into place in one step: the final path either - # does not exist or holds a complete tree, never a partial one. - STAGE="$CACHE_DIR/.unpack.$$" - rm -rf "$STAGE" - mkdir -p "$STAGE" - # Only `state_tests` is unpacked: the runner reads the state-test format, and the archive's - # blockchain-test subtrees are several times larger. - tar -xzf "$ARCHIVE" -C "$STAGE" --strip-components=1 fixtures/state_tests - printf '%s' "$EEST_SHA256" >"$STAGE/state_tests/.unpacked" - mkdir -p "$CACHE_DIR/$EEST_RELEASE" - mv "$STAGE/state_tests" "$CORPUS_DIR" - rm -rf "$STAGE" + # What the sweep reports is a statement about the corpus it read, so the tree it reads has to be + # the whole corpus and nothing else. A cached tree can be short of that in ways nothing about it + # announces: an extraction interrupted by a cancelled job or a full disk, a cache archived + # mid-write and restored intact, a stray edit under the cache directory. Each leaves a directory + # that exists, sweeps clean, and covers a fraction of what the tally claims. + # + # So a cached tree is re-verified against its own manifest — every file, hashed — before it is + # used, and discarded and re-extracted when it does not match. + if corpus_is_intact "$CORPUS_DIR"; then + echo "==> corpus tree verified against its manifest" + else + # Two runs sharing a cache directory would otherwise extract into the same destination at the + # same time, and the loser's rename lands inside the winner's tree. An atomic `mkdir` picks + # one producer; the other waits for it and, if the wait runs out, extracts a private tree + # rather than reaching into a directory a live process may still own. + UNPACK_LOCK="$CACHE_DIR/$EEST_RELEASE.unpack.lock" + mkdir -p "$CACHE_DIR" + if mkdir "$UNPACK_LOCK" 2>/dev/null; then + LOCK_HELD=1 + echo "$$" >"$UNPACK_LOCK/pid" 2>/dev/null || true + echo "==> unpacking state_tests" + unpack_corpus "$CORPUS_DIR" + rm -rf "$UNPACK_LOCK" + LOCK_HELD=0 + else + echo "==> another run is unpacking this corpus; waiting up to ${LOCK_WAIT_SECS}s" + WAITED=0 + while [ -d "$UNPACK_LOCK" ] && [ "$WAITED" -lt "$LOCK_WAIT_SECS" ]; do + sleep 5 + WAITED=$((WAITED + 5)) + done + if ! corpus_is_intact "$CORPUS_DIR"; then + PRIVATE_ROOT="$CACHE_DIR/.private.$$" + echo "==> shared corpus is not usable; unpacking a private tree at $PRIVATE_ROOT" + echo " (a lock left behind by a dead run is cleared by removing $UNPACK_LOCK)" >&2 + unpack_corpus "$PRIVATE_ROOT/state_tests" + CORPUS_DIR="$PRIVATE_ROOT/state_tests" + fi + fi fi fi diff --git a/tools/eest-sweep/tests/cache_integrity.sh b/tools/eest-sweep/tests/cache_integrity.sh new file mode 100755 index 00000000..b82515ce --- /dev/null +++ b/tools/eest-sweep/tests/cache_integrity.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +# +# Tests for the corpus-cache guards in `run.sh`. +# +# What a sweep reports is a statement about the corpus it read, so the tree it reads has to be the +# whole corpus and nothing else. These cases drive `run.sh` against a small synthetic archive and +# check that every way a cached tree can be wrong — truncated, edited, added to, left over from a +# different archive — is detected and re-extracted, and that two runs sharing a cache directory do +# not extract into each other. +# +# Usage: tools/eest-sweep/tests/cache_integrity.sh +set -uo pipefail + +SUITE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUN_SH="$SUITE_DIR/../run.sh" +WORK="$(mktemp -d)" +FAILURES=0 +CASE="" + +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +fail() { + echo " FAIL: $*" >&2 + FAILURES=$((FAILURES + 1)) +} + +start_case() { + CASE="$1" + echo "==> $CASE" +} + +# --- fake repository ----------------------------------------------------------------------------- + +# A repo root holding just what `run.sh` reads: its own directory next to a `corpus.env` naming a +# synthetic archive, and a binary that stands in for `state-test`. +ROOT="$WORK/repo" +CACHE="$WORK/cache" +REPORT="$WORK/report" +STUB_ARGS_LOG="$WORK/stub-args.log" +export STUB_ARGS_LOG +mkdir -p "$ROOT/tools/eest-sweep" "$ROOT/target/stubprofile" "$CACHE" +cp "$RUN_SH" "$ROOT/tools/eest-sweep/run.sh" + +cat >"$ROOT/target/stubprofile/state-test" <<'STUB' +#!/usr/bin/env bash +# Stands in for the `state-test` binary: records the path it was handed and prints the shape of +# output `run.sh` parses. The corpus guards are what is under test, not the runner. +printf '%s\n' "${@: -1}" >>"$STUB_ARGS_LOG" +case " $* " in + *" --fill "*) echo "Fill tally: OK=2 ERR=0 PANIC=0 FILE_ERR=0 SKIP_FILE=0 TOTAL=2" ;; + *) echo "Differential run: Rex7 vs Rex6 over 2 unit(s)" ;; +esac +exit 0 +STUB +chmod +x "$ROOT/target/stubprofile/state-test" + +# A two-fixture archive shaped like the real one: `fixtures/state_tests/...`. +SRC="$WORK/src" +mkdir -p "$SRC/fixtures/state_tests/a" "$SRC/fixtures/state_tests/b" +echo '{"unit_a": {}}' >"$SRC/fixtures/state_tests/a/one.json" +echo '{"unit_b": {}}' >"$SRC/fixtures/state_tests/b/two.json" +ARCHIVE_NAME="fixtures_test.tar.gz" +RELEASE="vtest" +tar -czf "$WORK/$ARCHIVE_NAME" -C "$SRC" fixtures +if command -v sha256sum >/dev/null 2>&1; then + ARCHIVE_SHA="$(sha256sum "$WORK/$ARCHIVE_NAME" | cut -d' ' -f1)" +else + ARCHIVE_SHA="$(shasum -a 256 "$WORK/$ARCHIVE_NAME" | cut -d' ' -f1)" +fi +cat >"$ROOT/tools/eest-sweep/corpus.env" <"$WORK/last.log" 2>&1 +} + +expect_ok() { + local status="$1" + [ "$status" -eq 0 ] || fail "expected exit 0, got $status: $(tail -n 3 "$WORK/last.log")" +} + +# The directory's identity, which a re-extraction replaces: the tree is moved into place, never +# written into. +tree_id() { + ls -di "$CORPUS" 2>/dev/null | awk '{print $1}' +} + +log_has() { + grep -q "$1" "$WORK/last.log" || fail "log should mention '$1': $(cat "$WORK/last.log")" +} + +log_lacks() { + grep -q "$1" "$WORK/last.log" && fail "log should not mention '$1': $(cat "$WORK/last.log")" +} + +corpus_is_whole() { + [ -f "$CORPUS/a/one.json" ] && [ -f "$CORPUS/b/two.json" ] && [ -f "$CORPUS/.manifest" ] +} + +# --- cases --------------------------------------------------------------------------------------- + +start_case "a cold cache unpacks the corpus and sweeps it" +sweep +expect_ok "$?" +log_has "unpacking state_tests" +corpus_is_whole || fail "the tree is not whole after a cold run" +grep -q "^archive-sha256 $ARCHIVE_SHA$" "$CORPUS/.manifest" || + fail "the manifest should name the archive it came from" +[ "$(grep -c . "$CORPUS/.manifest")" -eq 3 ] || fail "manifest should list both fixtures" +FIRST_ID="$(tree_id)" + +start_case "a warm cache is verified against the manifest, not re-extracted" +sweep +expect_ok "$?" +log_has "verified against its manifest" +log_lacks "unpacking state_tests" +[ "$(tree_id)" = "$FIRST_ID" ] || fail "the tree was replaced despite being intact" + +start_case "a fixture edited under the cache is detected and the tree re-extracted" +echo '{"tampered": true}' >"$CORPUS/a/one.json" +sweep +expect_ok "$?" +log_has "unpacking state_tests" +[ "$(cat "$CORPUS/a/one.json")" = '{"unit_a": {}}' ] || fail "the edit survived the re-extraction" +[ "$(tree_id)" != "$FIRST_ID" ] || fail "the tree should have been replaced" + +start_case "a fixture missing from the cache is detected" +rm "$CORPUS/b/two.json" +sweep +expect_ok "$?" +log_has "unpacking state_tests" +corpus_is_whole || fail "the missing fixture should be back" + +start_case "a file added under the cache is detected" +echo '{"stray": true}' >"$CORPUS/a/stray.json" +sweep +expect_ok "$?" +log_has "unpacking state_tests" +[ -f "$CORPUS/a/stray.json" ] && fail "the stray fixture should be gone" + +start_case "a tree left by a different archive is not reused" +# The manifest describes this tree correctly and names another archive: it is a complete corpus, +# but not the one the sweep is pinned to. +sed -i.bak "1s/.*/archive-sha256 0000000000000000000000000000000000000000000000000000000000000000/" \ + "$CORPUS/.manifest" +rm -f "$CORPUS/.manifest.bak" +sweep +expect_ok "$?" +log_has "unpacking state_tests" +grep -q "^archive-sha256 $ARCHIVE_SHA$" "$CORPUS/.manifest" || + fail "the re-extracted tree should name the pinned archive" + +start_case "a truncated tree cannot be swept as a whole one" +# What an interrupted extraction leaves behind, in the shape a cache restore would preserve. +rm -rf "${CORPUS:?}/b" +sweep +expect_ok "$?" +corpus_is_whole || fail "the truncated tree should have been replaced" + +start_case "a run that finds the lock held falls back to a private tree" +# Nobody holds this lock, but a live producer is indistinguishable from a dead one, and the +# waiter's job is the same either way: never write into a destination another process owns. +rm -rf "$CORPUS" +mkdir -p "$LOCK" +: >"$STUB_ARGS_LOG" +EEST_UNPACK_LOCK_WAIT_SECS=1 sweep +expect_ok "$?" +log_has "unpacking a private tree" +[ -d "$CORPUS" ] && fail "the waiter must not create the shared tree" +SWEPT="$(tail -n 1 "$STUB_ARGS_LOG")" +case "$SWEPT" in + *"/.private."*) ;; + *) fail "the sweep should have run against the private tree, got '$SWEPT'" ;; +esac +[ -e "$SWEPT" ] && fail "the private tree should be removed when the run ends" +rmdir "$LOCK" + +start_case "two runs sharing a cache do not extract into each other" +rm -rf "$CORPUS" +sweep & +FIRST=$! +"$ROOT/tools/eest-sweep/run.sh" --no-build --profile stubprofile \ + --cache-dir "$CACHE" --report-dir "$WORK/report2" >"$WORK/second.log" 2>&1 & +SECOND=$! +wait "$FIRST" +FIRST_STATUS=$? +wait "$SECOND" +SECOND_STATUS=$? +[ "$FIRST_STATUS" -eq 0 ] || fail "the first concurrent run failed: $(tail -n 3 "$WORK/last.log")" +[ "$SECOND_STATUS" -eq 0 ] || fail "the second concurrent run failed: $(tail -n 3 "$WORK/second.log")" +corpus_is_whole || fail "the shared tree is not whole after two concurrent runs" +[ -d "$LOCK" ] && fail "the lock should be released" +ls -d "$CACHE"/.private.* >/dev/null 2>&1 && fail "no private tree should be left behind" +ls -d "$CACHE"/.unpack.* >/dev/null 2>&1 && fail "no scratch directory should be left behind" + +start_case "fill mode runs against a private copy of the verified tree" +: >"$STUB_ARGS_LOG" +sweep --mode fill +expect_ok "$?" +log_has "verified against its manifest" +SWEPT="$(tail -n 1 "$STUB_ARGS_LOG")" +case "$SWEPT" in + "$REPORT/fill-corpus"*) ;; + *) fail "fill mode should sweep its own copy, got '$SWEPT'" ;; +esac + +# --- verdict ------------------------------------------------------------------------------------- + +if [ "$FAILURES" -ne 0 ]; then + echo "$FAILURES check(s) failed" >&2 + exit 1 +fi +echo "all corpus-cache checks passed" From c78a0951302729b0f0fe2ef0d8bd1c26877c2aaf Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 17:47:52 +0800 Subject: [PATCH 088/208] feat(evm): wrap every inspector in a measurement shim The shim is a pure passthrough for now. It exists so that there is one boundary every inspector callback has to cross, which is where a later change measures what the inspector did to the EVM's gas counters: the EVM does not execute inside a callback, so whatever changes across one is the inspector's by construction. Wrapping happens at the entry points that accept an inspector, and every accessor hands the unwrapped inspector back, so the type a caller names is unchanged. The uninspected path never calls an inspector at all. --- bin/mega-evme/src/common/trace.rs | 2 +- crates/mega-evm/src/evm/execution.rs | 12 +- crates/mega-evm/src/evm/inspector.rs | 181 ++++++++++++++++++++++++++ crates/mega-evm/src/evm/interfaces.rs | 17 ++- crates/mega-evm/src/evm/mod.rs | 27 +++- 5 files changed, 224 insertions(+), 15 deletions(-) create mode 100644 crates/mega-evm/src/evm/inspector.rs diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 1f8c27b2..feba4a70 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -235,7 +235,7 @@ impl TraceArgs { 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(&evm.inspector, &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))) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 4e265488..7204e5ca 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -43,8 +43,9 @@ use revm::{ use crate::{ constants, dispatch_system_contract_interceptors, is_deposit_like_transaction, is_mega_system_transaction_with, limit::ACCOUNT_INFO_WRITE_SIZE, sent_from_system_address, - ExternalEnvTypes, HostExt, JournalInspectTr, MegaContext, MegaEvm, MegaHaltReason, - MegaInstructions, MegaSpecId, MegaTransactionError, MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, + ExternalEnvTypes, HostExt, JournalInspectTr, MeasuredInspector, MegaContext, MegaEvm, + MegaHaltReason, MegaInstructions, MegaSpecId, MegaTransactionError, + MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, }; /// Revm handler for `MegaETH`. It internally wraps the [`op_revm::handler::OpHandler`] and inherits @@ -1634,7 +1635,12 @@ where DB: Database, INSP: Inspector>, { - type Inspector = INSP; + /// The inspector revm's inspected loops drive is the measurement shim, not the caller's own + /// inspector — every callback revm makes has to cross the shim's boundary for the shim to be + /// able to measure it. The caller's type is still what + /// [`alloy_evm::Evm::Inspector`](alloy_evm::Evm) and + /// [`InspectEvm::Inspector`](revm::InspectEvm) name. + type Inspector = MeasuredInspector; #[inline] fn all_inspector( diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs new file mode 100644 index 00000000..fead60ae --- /dev/null +++ b/crates/mega-evm/src/evm/inspector.rs @@ -0,0 +1,181 @@ +//! The measurement shim every inspector handed to `MegaETH` is wrapped in. +//! +//! # Why a shim +//! +//! An inspector is not a passive observer. Every callback that receives a live interpreter can +//! write to its gas counter, and every callback that receives a frame's inputs can change the gas +//! limit the frame is about to be built with. `MegaETH` meters compute gas by watching those exact +//! counters, and derives what a transaction destroyed from the envelope it spent, so an unmeasured +//! edit shows up as the EVM having done less work than it did, or as a transaction having spent +//! less gas than it did. +//! +//! # Why the callback boundary is enough +//! +//! The EVM does not execute inside an inspector callback. Anything that changes between the moment +//! the shim delegates to the user's inspector and the moment control comes back is therefore the +//! inspector's doing — not by attribution, but by construction. The shim snapshots the counters it +//! cares about on the way in, compares on the way out, and books the difference. +//! +//! That is why the shim lives at the `Inspector` implementation layer and not inside revm's +//! dispatch loop: wrapping the object is sufficient to sit on every boundary, and mirroring +//! `inspect_instructions` would take on a core dispatch loop for no additional reach. +//! +//! Nothing here changes what an inspector is allowed to do to the EVM, and nothing here runs on +//! the uninspected path — revm's plain interpreter loop never calls an inspector at all. + +use alloy_evm::Database; +use alloy_primitives::{Address, Log, U256}; +use revm::{ + handler::FrameResult, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, + InterpreterTypes, + }, + Inspector, +}; + +use crate::{ExternalEnvTypes, MegaContext}; + +/// Wraps a user inspector so that what it does to gas accounting can be measured and booked. +/// +/// `MegaETH` applies this itself — [`MegaEvm::with_inspector`](crate::MegaEvm::with_inspector) and +/// [`InspectEvm::set_inspector`](revm::InspectEvm::set_inspector) take the user's inspector by +/// value and store it wrapped, and the accessors hand back the unwrapped inspector — so the +/// wrapper is not something a caller opts into or can opt out of. +/// +/// Derefs to the wrapped inspector, so `evm.inspector().whatever()` reaches the user's own type. +#[derive(Clone, Copy, Debug, Default, derive_more::Deref, derive_more::DerefMut)] +pub struct MeasuredInspector { + #[deref] + #[deref_mut] + inner: I, +} + +impl MeasuredInspector { + /// Wraps `inner` in the measurement shim. + pub const fn new(inner: I) -> Self { + Self { inner } + } + + /// The wrapped inspector. + pub const fn inner(&self) -> &I { + &self.inner + } + + /// The wrapped inspector, mutably. + pub const fn inner_mut(&mut self) -> &mut I { + &mut self.inner + } + + /// Unwraps the shim, returning the inspector it was measuring. + pub fn into_inner(self) -> I { + self.inner + } +} + +impl Inspector, INTR> for MeasuredInspector +where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + I: Inspector, INTR>, +{ + #[inline] + fn initialize_interp( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, + ) { + self.inner.initialize_interp(interp, context); + } + + #[inline] + fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + self.inner.step(interp, context); + } + + #[inline] + fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + self.inner.step_end(interp, context); + } + + #[inline] + fn log(&mut self, context: &mut MegaContext, log: Log) { + self.inner.log(context, log); + } + + /// Forwards to the wrapped inspector's `log_full`, not to its `log`: the default `log_full` + /// already falls through to `log`, and short-circuiting here would silently drop an override. + #[inline] + fn log_full( + &mut self, + interpreter: &mut Interpreter, + context: &mut MegaContext, + log: Log, + ) { + self.inner.log_full(interpreter, context, log); + } + + #[inline] + fn frame_start( + &mut self, + context: &mut MegaContext, + frame_input: &mut FrameInput, + ) -> Option { + self.inner.frame_start(context, frame_input) + } + + #[inline] + fn frame_end( + &mut self, + context: &mut MegaContext, + frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + self.inner.frame_end(context, frame_input, frame_result); + } + + #[inline] + fn call( + &mut self, + context: &mut MegaContext, + inputs: &mut CallInputs, + ) -> Option { + self.inner.call(context, inputs) + } + + #[inline] + fn call_end( + &mut self, + context: &mut MegaContext, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + self.inner.call_end(context, inputs, outcome); + } + + #[inline] + fn create( + &mut self, + context: &mut MegaContext, + inputs: &mut CreateInputs, + ) -> Option { + self.inner.create(context, inputs) + } + + #[inline] + fn create_end( + &mut self, + context: &mut MegaContext, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.inner.create_end(context, inputs, outcome); + } + + /// Everything this callback receives is passed by value, so it cannot change execution state. + #[inline] + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.inner.selfdestruct(contract, target, value); + } +} diff --git a/crates/mega-evm/src/evm/interfaces.rs b/crates/mega-evm/src/evm/interfaces.rs index d00ab031..8839b25d 100644 --- a/crates/mega-evm/src/evm/interfaces.rs +++ b/crates/mega-evm/src/evm/interfaces.rs @@ -13,8 +13,8 @@ use revm::{ }; use crate::{ - constants, ExternalEnvTypes, IntoMegaethCfgEnv, MegaContext, MegaEvm, MegaHaltReason, - MegaHandler, MegaSpecId, MegaTransaction, MegaTransactionError, + constants, ExternalEnvTypes, IntoMegaethCfgEnv, MeasuredInspector, MegaContext, MegaEvm, + MegaHaltReason, MegaHandler, MegaSpecId, MegaTransaction, MegaTransactionError, }; /// Implementation of [`alloy_evm::Evm`] for `MegaETH` EVM. @@ -113,14 +113,19 @@ where self.inspect = enabled; } + /// Hands back the caller's own inspector, not the measurement shim it is executed inside. fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) { - (&self.inner.ctx.journaled_state.database, &self.inner.inspector, &self.inner.precompiles) + ( + &self.inner.ctx.journaled_state.database, + self.inner.inspector.inner(), + &self.inner.precompiles, + ) } fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) { ( &mut self.inner.ctx.journaled_state.database, - &mut self.inner.inspector, + self.inner.inspector.inner_mut(), &mut self.inner.precompiles, ) } @@ -177,8 +182,10 @@ where { type Inspector = INSP; + /// Takes the caller's inspector by value and stores it wrapped in the measurement shim, so a + /// swapped-in inspector is measured exactly like one supplied at construction. fn set_inspector(&mut self, inspector: Self::Inspector) { - self.inner.inspector = inspector; + self.inner.inspector = MeasuredInspector::new(inspector); } fn inspect_one_tx(&mut self, tx: Self::Tx) -> Result { diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 091c48d6..53930af1 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -29,6 +29,7 @@ mod context; mod execution; mod factory; mod host; +mod inspector; mod instructions; mod interfaces; mod limit; @@ -46,6 +47,7 @@ pub use context::*; pub use execution::*; pub use factory::*; pub use host::*; +pub use inspector::*; pub use instructions::*; #[allow(unused_imports, unreachable_pub)] pub use interfaces::*; @@ -89,9 +91,14 @@ use crate::{AdditionalLimit, BucketId, ExternalEnvTypes, LimitUsage, MegaTransac #[allow(missing_debug_implementations)] #[allow(clippy::type_complexity)] pub struct MegaEvm { + /// The inner EVM, holding the user's inspector wrapped in the measurement shim. + /// + /// The wrapper is `MegaETH`'s, not the caller's: every entry point that accepts an inspector + /// wraps it here, and every accessor hands the unwrapped one back, so `INSP` stays the type + /// the caller named. See [`MeasuredInspector`]. inner: revm::context::Evm< MegaContext, - INSP, + MeasuredInspector, MegaInstructions, PrecompilesMap, EthFrame, @@ -124,7 +131,7 @@ impl core::ops::Deref { type Target = revm::context::Evm< MegaContext, - INSP, + MeasuredInspector, MegaInstructions, PrecompilesMap, EthFrame, @@ -167,7 +174,7 @@ impl MegaEvm MegaEvm { let mega_cfg = self.mega_cfg; let inner = revm::context::Evm::new_with_inspector( self.inner.ctx, - inspector, + MeasuredInspector::new(inspector), self.inner.instruction, self.inner.precompiles, ); @@ -206,7 +213,7 @@ impl MegaEvm { let mega_cfg = self.mega_cfg; let inner = revm::context::Evm::new_with_inspector( self.inner.ctx, - NoOpInspector, + MeasuredInspector::new(NoOpInspector), self.inner.instruction, self.inner.precompiles, ); @@ -328,7 +335,15 @@ impl MegaEvm { PrecompilesMap, EthFrame, > { - self.inner + // The measurement shim is an implementation detail of executing through `MegaEvm`; an + // EVM taken apart is no longer executing, so it is handed back unwrapped. + revm::context::Evm { + ctx: self.inner.ctx, + inspector: self.inner.inspector.into_inner(), + instruction: self.inner.instruction, + precompiles: self.inner.precompiles, + frame_stack: self.inner.frame_stack, + } } } From 5112f5a0fda0177110dd08ca49b27a82f48029a6 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 17:49:44 +0800 Subject: [PATCH 089/208] feat(rex7): book what an inspector does to the EVM's gas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the ledger the shim writes to and the boundary measurements that feed it: interpreter gas counters around every callback that gets a live interpreter, and frame-input gas limits around every callback that can edit them. An edited frame input is only booked when the callback lets the frame be built — an intercepting callback's edits are dropped unread. The ledger is a reported quantity: no limit is compared against it. What it is for is the conservation law, which derives what a transaction destroyed from what it spent. Gas an inspector conjures is gas the transaction's envelope never funded, so without the term the derivation reads that transaction as having spent less than it did and can go negative. Both the derivation and the envelope assertion now carry it; it is zero for every transaction that ran without an inspector and for every observation-only inspector, so the law reads exactly as before on those paths. --- crates/mega-evm/src/evm/inspector.rs | 102 +++++++++++++++-- crates/mega-evm/src/evm/mod.rs | 15 ++- crates/mega-evm/src/limit/inspector_ledger.rs | 107 ++++++++++++++++++ crates/mega-evm/src/limit/limit.rs | 102 +++++++++++++++-- crates/mega-evm/src/limit/mod.rs | 2 + 5 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 crates/mega-evm/src/limit/inspector_ledger.rs diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index fead60ae..1483782e 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -5,9 +5,9 @@ //! An inspector is not a passive observer. Every callback that receives a live interpreter can //! write to its gas counter, and every callback that receives a frame's inputs can change the gas //! limit the frame is about to be built with. `MegaETH` meters compute gas by watching those exact -//! counters, and derives what a transaction destroyed from the envelope it spent, so an unmeasured -//! edit shows up as the EVM having done less work than it did, or as a transaction having spent -//! less gas than it did. +//! counters, and derives what a transaction destroyed from the envelope it spent, so an +//! unmeasured edit shows up as the EVM having done less work than it did, or as a transaction +//! having spent less gas than it did. //! //! # Why the callback boundary is enough //! @@ -20,7 +20,12 @@ //! dispatch loop: wrapping the object is sufficient to sit on every boundary, and mirroring //! `inspect_instructions` would take on a core dispatch loop for no additional reach. //! -//! Nothing here changes what an inspector is allowed to do to the EVM, and nothing here runs on +//! # What the shim does with what it measures +//! +//! - Interpreter-counter edits go to [`AdditionalLimit::record_inspector_gas_adjustment`]. +//! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. +//! +//! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on //! the uninspected path — revm's plain interpreter loop never calls an inspector at all. use alloy_evm::Database; @@ -36,12 +41,12 @@ use revm::{ use crate::{ExternalEnvTypes, MegaContext}; -/// Wraps a user inspector so that what it does to gas accounting can be measured and booked. +/// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// /// `MegaETH` applies this itself — [`MegaEvm::with_inspector`](crate::MegaEvm::with_inspector) and /// [`InspectEvm::set_inspector`](revm::InspectEvm::set_inspector) take the user's inspector by -/// value and store it wrapped, and the accessors hand back the unwrapped inspector — so the -/// wrapper is not something a caller opts into or can opt out of. +/// value and store it wrapped, and the accessors hand back the unwrapped inspector — so the wrapper +/// is not something a caller opts into or can opt out of. /// /// Derefs to the wrapped inspector, so `evm.inspector().whatever()` reaches the user's own type. #[derive(Clone, Copy, Debug, Default, derive_more::Deref, derive_more::DerefMut)] @@ -73,6 +78,44 @@ impl MeasuredInspector { } } +/// The gas limit a frame input carries, for the two variants that have one. +#[inline] +fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { + match frame_input { + FrameInput::Call(inputs) => Some(inputs.gas_limit), + FrameInput::Create(inputs) => Some(inputs.gas_limit()), + FrameInput::Empty => None, + } +} + +/// Books what a callback did to a frame's envelope, if the edited inputs will actually reach a +/// frame. +/// +/// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped +/// entirely and the inputs it edited are dropped unread, so no edit of theirs can move the +/// transaction's envelope. +#[inline] +fn book_env_adjustment( + context: &MegaContext, + before: Option, + after: Option, + intercepted: bool, +) { + if intercepted { + return; + } + let (Some(before), Some(after)) = (before, after) else { + return; + }; + if before == after { + return; + } + context + .additional_limit + .borrow_mut() + .record_inspector_env_adjustment(i128::from(after) - i128::from(before)); +} + impl Inspector, INTR> for MeasuredInspector where DB: Database, @@ -80,25 +123,44 @@ where INTR: InterpreterTypes, I: Inspector, INTR>, { + /// This runs after the frame is built but before its settlement window is opened, which is + /// why it books its adjustment without touching that window. #[inline] fn initialize_interp( &mut self, interp: &mut Interpreter, context: &mut MegaContext, ) { + let before = interp.gas.remaining(); self.inner.initialize_interp(interp, context); + context + .additional_limit + .borrow_mut() + .record_inspector_gas_adjustment::(&mut interp.gas, before); } #[inline] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + let before = interp.gas.remaining(); self.inner.step(interp, context); + context + .additional_limit + .borrow_mut() + .record_inspector_gas_adjustment::(&mut interp.gas, before); } #[inline] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + let before = interp.gas.remaining(); self.inner.step_end(interp, context); + context + .additional_limit + .borrow_mut() + .record_inspector_gas_adjustment::(&mut interp.gas, before); } + /// No interpreter and no frame inputs are reachable here, so there is nothing to measure — + /// this callback can only touch the context, which the shim does not police. #[inline] fn log(&mut self, context: &mut MegaContext, log: Log) { self.inner.log(context, log); @@ -113,7 +175,12 @@ where context: &mut MegaContext, log: Log, ) { + let before = interpreter.gas.remaining(); self.inner.log_full(interpreter, context, log); + context + .additional_limit + .borrow_mut() + .record_inspector_gas_adjustment::(&mut interpreter.gas, before); } #[inline] @@ -122,9 +189,14 @@ where context: &mut MegaContext, frame_input: &mut FrameInput, ) -> Option { - self.inner.frame_start(context, frame_input) + let before = frame_input_gas_limit(frame_input); + let outcome = self.inner.frame_start(context, frame_input); + book_env_adjustment(context, before, frame_input_gas_limit(frame_input), outcome.is_some()); + outcome } + /// The frame's result gas is deliberately not booked — see + /// [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. #[inline] fn frame_end( &mut self, @@ -141,9 +213,14 @@ where context: &mut MegaContext, inputs: &mut CallInputs, ) -> Option { - self.inner.call(context, inputs) + let before = inputs.gas_limit; + let outcome = self.inner.call(context, inputs); + book_env_adjustment(context, Some(before), Some(inputs.gas_limit), outcome.is_some()); + outcome } + /// `CallInputs` is immutable here and the frame's result gas is deliberately not booked — see + /// [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. #[inline] fn call_end( &mut self, @@ -160,9 +237,14 @@ where context: &mut MegaContext, inputs: &mut CreateInputs, ) -> Option { - self.inner.create(context, inputs) + let before = inputs.gas_limit(); + let outcome = self.inner.create(context, inputs); + book_env_adjustment(context, Some(before), Some(inputs.gas_limit()), outcome.is_some()); + outcome } + /// `CreateInputs` is immutable here and the frame's result gas is deliberately not booked — + /// see [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. #[inline] fn create_end( &mut self, diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 53930af1..b5e90231 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -448,9 +448,15 @@ where /// settlement has run they must add back up to the envelope the transaction burnt: /// /// ```text -/// compute_gas_used + non_compute_gas − minted_call_stipend == total_gas_spent +/// compute_gas_used + non_compute_gas − minted_call_stipend − inspector_conjured_gas +/// == total_gas_spent /// ``` /// +/// The inspector term is zero unless a rewriting inspector was attached: it is what the +/// measurement shim booked for gas the inspector wrote into an interpreter counter or a frame +/// envelope, which the transaction's own envelope never funded. Subtracting it is what keeps the +/// law stated over the EVM's gas rather than over the EVM's gas plus an inspector's edits. +/// /// The EIP-3529 refund and the EIP-7623 floor move the number a receipt reports without anyone /// having burnt the difference; both are carried on the result as their own fields and applied /// after the envelope is final, so the envelope this compares against is unaffected by either. @@ -474,15 +480,18 @@ fn debug_assert_envelope_accounted( if cfg!(debug_assertions) && spec.is_enabled(MegaSpecId::REX7) && !is_inside_sandbox { let envelope = outcome.result_and_state.result.gas().total_gas_spent(); let accounted = i128::from(outcome.compute_gas_used) + additional_limit.non_compute_gas() - - i128::from(additional_limit.minted_call_stipend()); + i128::from(additional_limit.minted_call_stipend()) - + additional_limit.inspector_conjured_gas(); debug_assert!( accounted == i128::from(envelope), "the tracker lanes must account for the whole receipt envelope: \ accounted {accounted} vs envelope {envelope} \ - (compute {}, non-compute {}, minted stipend {}, destroyed {}, enforced {})", + (compute {}, non-compute {}, minted stipend {}, inspector conjured {}, \ + destroyed {}, enforced {})", outcome.compute_gas_used, additional_limit.non_compute_gas(), additional_limit.minted_call_stipend(), + additional_limit.inspector_conjured_gas(), outcome.compute_gas_destroyed, outcome.compute_gas_enforced, ); diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs new file mode 100644 index 00000000..9d643727 --- /dev/null +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -0,0 +1,107 @@ +//! The ledger of what an inspector did to a transaction's gas accounting. +//! +//! `MegaETH` wraps every inspector it is handed in a measurement shim (`MeasuredInspector`), and +//! the shim books what it measures here. Nothing in this module enforces anything: the numbers it +//! holds are exactly the part of a transaction's gas movement that the EVM did not produce, kept +//! separate so that enforcement can ignore it and the conservation law can account for it. + +/// What an inspector conjured, destroyed, or refused, as measured at the callback boundaries. +/// +/// # Why the boundary is a sound place to measure +/// +/// The EVM does not execute inside an inspector callback. Every change to an interpreter's gas +/// counter, or to a frame input's gas limit, that is visible across a callback's entry and exit is +/// therefore the inspector's, by construction rather than by attribution heuristics. The shim takes +/// one snapshot before delegating and one after, and the difference lands here. +/// +/// # Sign convention +/// +/// Every field measuring gas is signed and reads *from the transaction's point of view*: a positive +/// value is gas the inspector conjured — gas that exists in the execution but that nothing debited +/// from the transaction's envelope — and a negative value is gas it destroyed. Both directions are +/// recorded, because the conservation law needs the net, not the gross. +/// +/// # What consumes it +/// +/// - [`conjured_gas`](Self::conjured_gas) is the term the destroyed-remainder derivation adds to +/// the envelope, so that a transaction run under a rewriting inspector still satisfies `destroyed +/// = spent + minted + conjured − non_compute − enforced`. Without it, gas the inspector created +/// out of nothing would show up as the transaction having spent less than it really did, and the +/// derived destroyed total would go negative. +/// - The ledger as a whole is a *reported* quantity. No resource limit is ever compared against it, +/// and enforcement never sees an inspector's adjustment: `record_inspector_gas_adjustment` shifts +/// the checkpoint baseline by the same amount it books here, so the compute-gas measurement of a +/// frame covers the work the EVM performed and nothing else. +/// +/// # Reading a frame-level aggregate +/// +/// The ledger is cumulative over the whole transaction and is deliberately not a per-frame stack: +/// aligning another stack with the EVM's frame lifecycle is exactly the machinery the frame-loop +/// rework replaces. A caller that wants what an inspector did to *one* frame reads the whole +/// ledger at the frame's entry and again at its exit and takes the difference — the type is `Copy` +/// and every field is a running total, so the difference of two readings is the aggregate over the +/// window between them, whatever happened inside it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct InspectorLedger { + /// Net gas the inspector wrote into interpreter gas counters, across every callback that is + /// handed a live [`Interpreter`](revm::interpreter::Interpreter). + /// + /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the + /// caller never forwarded and lowering it takes gas away that the caller will never get back. + pub gas: i128, + + /// Net gas the inspector wrote into frame *envelopes* — the `gas_limit` a call or create frame + /// is about to be built with. + /// + /// The caller was debited the forwarded amount by its own `CALL` / `CREATE` opcode, before any + /// inspector callback ran, so a raised limit is gas nobody paid for and a lowered one is gas + /// the caller paid for and no frame ever receives. + /// + /// Only adjustments that actually reach a frame are booked. When the callback returns a + /// synthetic outcome it has intercepted the frame entirely and the inputs it edited are + /// discarded, so nothing about them can move the envelope. + /// + /// Adjustments to a frame's *result* gas — `call_end` calling `spend_all`, say — are + /// deliberately not booked here. Whether such an edit moves the transaction's envelope at all + /// depends on the frame's final classification (a caller reclaims a returning frame's + /// remaining gas and ignores a halting one's), and that classification is not final until + /// after the last mutating callback. Booking the edit before then would be a guess. + pub env: i128, +} + +impl InspectorLedger { + /// The gas the inspector conjured, net of what it destroyed — the term the destroyed-remainder + /// derivation adds to the transaction's envelope. + #[inline] + pub const fn conjured_gas(&self) -> i128 { + self.gas + self.env + } + + /// Whether the inspector left the transaction's gas accounting exactly as the EVM produced it. + /// + /// True for every observation-only inspector, and for every transaction that ran without one. + #[inline] + pub const fn is_zero(&self) -> bool { + self.gas == 0 && self.env == 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The derivation term is the net of both lanes, so an injection into an interpreter counter + /// and a matching reduction of a frame's envelope cancel — the transaction's envelope really is + /// unmoved in that case. + #[test] + fn test_conjured_gas_is_the_net_of_both_lanes() { + let ledger = InspectorLedger { gas: 2_300, env: -2_300 }; + assert_eq!(ledger.conjured_gas(), 0); + assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); + } + + #[test] + fn test_default_ledger_is_zero() { + assert!(InspectorLedger::default().is_zero()); + } +} diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index ac8774ae..97b7641d 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -13,8 +13,8 @@ use revm::{ }; use super::{ - checkpoint, compute_gas, data_size, frame_limit::TxRuntimeLimit, kv_update, state_growth, - storage_call_stipend, + checkpoint, compute_gas, data_size, frame_limit::TxRuntimeLimit, inspector_ledger, kv_update, + state_growth, storage_call_stipend, }; use crate::{ EvmTxRuntimeLimits, JournalInspectTr, MegaHaltReason, MegaSpecId, MegaTransaction, @@ -110,6 +110,13 @@ pub struct AdditionalLimit { /// A tracker for REX7+ checkpoint settlement and gas-clamp state. pub(crate) checkpoint: checkpoint::CheckpointTracker, + + /// What the inspector — if any — did to this transaction's gas accounting. + /// + /// Written only by the measurement shim every inspector is wrapped in, and read only by the + /// conservation law and by reporting. It stays at its default for every transaction that runs + /// without an inspector and for every observation-only inspector. + inspector: inspector_ledger::InspectorLedger, } /// The usage of the additional limits. @@ -138,6 +145,7 @@ impl AdditionalLimit { compute_gas: compute_gas::ComputeGasTracker::new(spec, limits.tx_compute_gas_limit), storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), checkpoint: checkpoint::CheckpointTracker::new(spec), + inspector: inspector_ledger::InspectorLedger::default(), } } } @@ -180,6 +188,7 @@ impl AdditionalLimit { self.kv_update.reset(); self.storage_call_stipend.reset(); self.checkpoint.reset(); + self.inspector = inspector_ledger::InspectorLedger::default(); } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -250,7 +259,7 @@ impl AdditionalLimit { /// and the non-compute lane — so the third is whatever is left of `tx_gas_spent`: /// /// ```text - /// destroyed = tx_gas_spent + minted_call_stipend + /// destroyed = tx_gas_spent + minted_call_stipend + inspector_conjured_gas /// − non_compute_gas − enforced_compute_gas /// ``` /// @@ -267,13 +276,24 @@ impl AdditionalLimit { /// `MegaHandler::last_frame_result`. Gas that is rescued for the sender or hidden by the gas /// clamp is erased from the envelope before that point, so neither can reach this subtraction. /// + /// The inspector term is the same kind of correction for a different producer. An inspector + /// runs outside the EVM's own accounting and can write gas into an interpreter's counter or + /// into a frame's envelope that nobody debited — or take gas away that nobody gets back — and + /// the transaction then spends correspondingly less or more than its frames recorded. The + /// measurement shim books exactly that difference, so adding it back is again measurement + /// rather than correction. It is zero for every transaction that ran without an inspector and + /// for every observation-only inspector, which is why the law reads the same as it always did + /// on those paths. + /// /// Signed on purpose: a mismatch against the booked total is a defect to report, and clamping /// it at zero would hide the half of the mismatch space where the booking over-counts. /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) is what turns the /// signed result into the number the transaction reports. #[inline] pub(crate) fn derived_burned_compute_gas(&self, tx_gas_spent: u64) -> i128 { - i128::from(tx_gas_spent) + i128::from(self.minted_call_stipend()) - + i128::from(tx_gas_spent) + + i128::from(self.minted_call_stipend()) + + self.inspector_conjured_gas() - self.non_compute_gas() - i128::from(self.enforced_compute_gas()) } @@ -315,8 +335,10 @@ impl AdditionalLimit { debug_assert!( derived >= 0, "derived destroyed compute gas is negative: {derived} \ - (spent {tx_gas_spent}, minted stipend {}, non-compute {}, enforced compute {})", + (spent {tx_gas_spent}, minted stipend {}, inspector conjured {}, non-compute {}, \ + enforced compute {})", self.minted_call_stipend(), + self.inspector_conjured_gas(), self.non_compute_gas(), self.enforced_compute_gas(), ); @@ -324,9 +346,11 @@ impl AdditionalLimit { derived == i128::from(self.burned_compute_gas()), "destroyed compute gas disagrees with the conservation law: \ derived {derived} vs booked {} \ - (spent {tx_gas_spent}, minted stipend {}, non-compute {}, enforced compute {})", + (spent {tx_gas_spent}, minted stipend {}, inspector conjured {}, non-compute {}, \ + enforced compute {})", self.burned_compute_gas(), self.minted_call_stipend(), + self.inspector_conjured_gas(), self.non_compute_gas(), self.enforced_compute_gas(), ); @@ -386,9 +410,10 @@ impl AdditionalLimit { debug_assert!( unbooked >= 0, "rewritten envelope destroys a negative amount: {unbooked} \ - (envelope {envelope_gas_spent}, minted stipend {}, non-compute {}, \ - enforced compute {}, booked destroyed {})", + (envelope {envelope_gas_spent}, minted stipend {}, inspector conjured {}, \ + non-compute {}, enforced compute {}, booked destroyed {})", self.minted_call_stipend(), + self.inspector_conjured_gas(), self.non_compute_gas(), self.enforced_compute_gas(), self.burned_compute_gas(), @@ -425,6 +450,67 @@ impl AdditionalLimit { self.checkpoint.record_minted_call_stipend(amount); } + /// What the inspector did to this transaction's gas accounting, as measured at the callback + /// boundaries — see [`InspectorLedger`](inspector_ledger::InspectorLedger). + /// + /// Default (all-zero) for every transaction that ran without an inspector and for every + /// observation-only inspector. Cumulative over the whole transaction: a caller that wants the + /// aggregate over one frame, or over any other window, reads this at both ends of the window + /// and takes the difference. + #[inline] + pub fn inspector_ledger(&self) -> inspector_ledger::InspectorLedger { + self.inspector + } + + /// The net gas the inspector conjured — the term + /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas) adds to the envelope so + /// that gas nobody funded does not read as the transaction having spent less than it did. + #[inline] + pub(crate) fn inspector_conjured_gas(&self) -> i128 { + self.inspector.conjured_gas() + } + + /// Books an adjustment an inspector made to a live interpreter's gas counter. + /// + /// This is the single entry point for interpreter-counter adjustments. `remaining_before` is + /// the counter the shim snapshotted before delegating to the user's callback, and + /// `gas.remaining()` is what the callback left behind; the difference is the adjustment, + /// because the EVM does not execute inside a callback. + /// + /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the + /// caller never forwarded and lowering it takes gas away that the caller will never get back. + /// Either way the transaction's envelope stops matching what its frames recorded, which is + /// what the ledger's entry lets the conservation law account for. + pub(crate) fn record_inspector_gas_adjustment( + &mut self, + gas: &mut Gas, + remaining_before: u64, + ) { + let _ = IN_OPEN_SEGMENT; + let remaining_after = gas.remaining(); + if remaining_after == remaining_before { + return; + } + self.inspector.gas += i128::from(remaining_after) - i128::from(remaining_before); + } + + /// Books an adjustment an inspector made to a frame's envelope — the `gas_limit` the frame is + /// about to be built with. + /// + /// The caller's `CALL` / `CREATE` opcode debited the forwarded amount before any inspector + /// callback ran, so raising the limit hands the child gas the transaction never paid for, and + /// lowering it makes gas the caller paid for reach nobody. Either way the transaction's + /// envelope no longer matches the work its frames recorded, and the conservation law needs the + /// difference. + /// + /// Call this only when the adjusted inputs actually reach a frame. A callback that returns a + /// synthetic outcome has intercepted the frame, and the inputs it edited are dropped without + /// being read. + #[inline] + pub(crate) fn record_inspector_env_adjustment(&mut self, delta: i128) { + self.inspector.env += delta; + } + /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, /// always 0 before) — the second term of /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas). diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index 25563af7..54142556 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -5,6 +5,7 @@ mod checkpoint; mod compute_gas; mod data_size; mod frame_limit; +mod inspector_ledger; mod kv_update; #[allow(clippy::module_inception)] mod limit; @@ -13,6 +14,7 @@ mod storage_call_stipend; pub use data_size::*; pub(crate) use frame_limit::{FrameLimitTracker, TxRuntimeLimit}; +pub use inspector_ledger::*; pub use limit::*; use crate::MegaHaltReason; From 133c693001311861600e6e526e98981eba9a8dd1 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 17:50:56 +0800 Subject: [PATCH 090/208] feat(rex7): keep an inspector's gas out of enforcement and re-clamp after it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compute gas is measured as a drop in the interpreter's counter, so gas an inspector writes into that counter mid-segment reads as the frame having done less work than it did — free compute headroom. The adjustment site now closes the open segment against the counter as the EVM left it and re-opens it on the adjusted one, so the measurement covers the work and nothing else. It then re-derives the gas clamp from the freshly settled usage, exactly as a checkpoint's epilogue does. The clamp hides gas beyond the compute headroom from the interpreter; gas written in afterwards is not hidden by it, so without this an injection would be spendable straight past the limit. Recording goes through the unguarded entry for the same reason the frame-exit tail settlement does: a callback can run right after an opcode whose pre-inner recorder deliberately left a dimension unlatched. --- crates/mega-evm/src/evm/inspector.rs | 9 +++-- crates/mega-evm/src/limit/limit.rs | 59 +++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 1483782e..b042e968 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -22,7 +22,9 @@ //! //! # What the shim does with what it measures //! -//! - Interpreter-counter edits go to [`AdditionalLimit::record_inspector_gas_adjustment`]. +//! - Interpreter-counter edits go to [`AdditionalLimit::record_inspector_gas_adjustment`], which +//! books them, keeps them out of the compute-gas measurement, and re-derives the gas clamp so +//! injected gas is not spendable past the compute headroom. //! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. //! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on @@ -123,8 +125,9 @@ where INTR: InterpreterTypes, I: Inspector, INTR>, { - /// This runs after the frame is built but before its settlement window is opened, which is - /// why it books its adjustment without touching that window. + /// Measured, but without settling a segment: this runs after the frame is built and before + /// its settlement window is opened, so there is nothing open to close. The frame's own entry + /// hook opens the window on whatever counter this callback leaves behind. #[inline] fn initialize_interp( &mut self, diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 97b7641d..b94f769f 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -470,28 +470,75 @@ impl AdditionalLimit { self.inspector.conjured_gas() } - /// Books an adjustment an inspector made to a live interpreter's gas counter. + /// Books an adjustment an inspector made to a live interpreter's gas counter, and restores + /// correct accounting and enforcement around it. /// /// This is the single entry point for interpreter-counter adjustments. `remaining_before` is /// the counter the shim snapshotted before delegating to the user's callback, and /// `gas.remaining()` is what the callback left behind; the difference is the adjustment, /// because the EVM does not execute inside a callback. /// - /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the - /// caller never forwarded and lowering it takes gas away that the caller will never get back. - /// Either way the transaction's envelope stops matching what its frames recorded, which is - /// what the ledger's entry lets the conservation law account for. + /// Three things happen, in this order: + /// + /// 1. **The ledger** takes the adjustment, so the conservation law can account for gas nobody + /// funded (or gas that vanished) when it derives the destroyed remainder. + /// 2. **The open segment is settled against the pre-callback counter** (REX7+, + /// `IN_OPEN_SEGMENT`). This is what keeps the adjustment out of enforcement: compute gas is + /// measured as a drop in the interpreter's counter, so an injection made mid-segment would + /// otherwise show up as *less* work than the frame performed — the frame would have been + /// handed free compute headroom. Closing the segment at `remaining_before` and re-opening it + /// at the adjusted counter measures exactly the work, and nothing else. + /// 3. **The gas clamp is re-derived** from the freshly settled usage, exactly as a checkpoint's + /// epilogue does. Without this, an injection would be spendable past the compute headroom: + /// the clamp hides gas beyond the headroom from the interpreter, and gas written in after + /// the clamp was applied is not hidden by it. + /// + /// `IN_OPEN_SEGMENT` is false at `initialize_interp`, the one callback that runs after a frame + /// is built but before its settlement window is opened. There is no segment to settle and no + /// clamp to re-derive there; the frame's own entry hook opens the window on the adjusted + /// counter a moment later, which absorbs the adjustment for free. + /// + /// The settlement records through the unguarded entry point for the same reason the frame-exit + /// tail settlement does: a callback can run immediately after an opcode whose pre-inner + /// recorder deliberately left a non-compute dimension unlatched, and the latch-protocol guard + /// would trip on it. + /// + /// A settlement that latches an exceed does not stop the interpreter here — a callback has no + /// way to fail an instruction. The latch is sticky, so the next checkpoint or the frame's own + /// exit surfaces it as it would have anyway; the adjustment only moves *when* the halt lands, + /// never whether it does. pub(crate) fn record_inspector_gas_adjustment( &mut self, gas: &mut Gas, remaining_before: u64, ) { - let _ = IN_OPEN_SEGMENT; let remaining_after = gas.remaining(); if remaining_after == remaining_before { return; } self.inspector.gas += i128::from(remaining_after) - i128::from(remaining_before); + + if !IN_OPEN_SEGMENT || !self.rex7_enabled() { + return; + } + + // Close the open segment against the counter as the EVM left it, so the adjustment sits + // outside the measured span. Both the baseline and `remaining_before` live in the clamped + // domain, so the difference telescopes over exactly the opcodes that ran since the last + // checkpoint. + let segment = self.checkpoint_baseline().saturating_sub(remaining_before); + let hidden = self.checkpoint_restore_hidden(); + gas.erase_cost(hidden); + self.sync_checkpoint_baseline(gas.remaining()); + let _ = self.record_compute_gas_unguarded(segment); + + // Re-derive the clamp for the segment that starts now, from the usage just settled. + let hide = self.checkpoint_clamp_amount(gas.remaining()); + if hide > 0 { + let clamped = gas.record_regular_cost(hide); + debug_assert!(clamped, "clamp amount exceeds remaining gas"); + self.sync_checkpoint_baseline(gas.remaining()); + } } /// Books an adjustment an inspector made to a frame's envelope — the `gas_limit` the frame is From fc837550db2d74fecbb549fe1c05e639aee1d232 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 17:52:05 +0800 Subject: [PATCH 091/208] feat(rex7): refuse an inspector's failed-to-successful create rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By the time `create_end` runs, revm has already reverted the frame's journal checkpoint and already declined to deposit the code — the size limit, the 0xEF prefix rule and the code-deposit charge are all evaluated before the callback. A result rewritten to success therefore reports a deployment that did not happen, at an address holding no code, with the constructor's state changes rolled back. The shim detects the shape at `create_end` and at the `frame_end` after it, restores the original classification, counts the refusal in the ledger, and puts the reason in the context's error slot so the transaction fails with an error instead of a fabricated receipt. Debug builds assert: this is a detector, and a corpus that produces the shape should stop rather than take the rejection path quietly. Nothing here compensates the journal. REX7+ only: on a frozen spec the rewrite reaches no accounting lane it can make unsound, and those specs' behaviour is closed. --- crates/mega-evm/src/evm/inspector.rs | 78 ++++++++++++++++--- crates/mega-evm/src/limit/inspector_ledger.rs | 20 ++++- crates/mega-evm/src/limit/limit.rs | 7 ++ 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index b042e968..11fc6859 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -26,22 +26,32 @@ //! books them, keeps them out of the compute-gas measurement, and re-derives the gas clamp so //! injected gas is not spendable past the compute headroom. //! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. +//! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on //! the uninspected path — revm's plain interpreter loop never calls an inspector at all. +#[cfg(not(feature = "std"))] +use alloc as std; +use std::string::String; + use alloy_evm::Database; use alloy_primitives::{Address, Log, U256}; use revm::{ + context::{ContextError, ContextTr}, handler::FrameResult, interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, - InterpreterTypes, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, InstructionResult, + Interpreter, InterpreterResult, InterpreterTypes, }, Inspector, }; -use crate::{ExternalEnvTypes, MegaContext}; +use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; + +/// The message a refused `create_end` rewrite surfaces as `EVMError::Custom`. +pub(crate) const FORBIDDEN_CREATE_REVIVAL: &str = + "inspector rewrote a failed contract creation into a successful one"; /// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// @@ -118,6 +128,46 @@ fn book_env_adjustment( .record_inspector_env_adjustment(i128::from(after) - i128::from(before)); } +/// Refuses a rewrite that turns a non-successful contract creation into a successful one, and says +/// so loudly. +/// +/// The rewrite is forbidden rather than supported because there is no state behind it. By the time +/// `create_end` runs, revm has already reverted the frame's journal checkpoint and has already +/// declined to deposit the code — the size limit, the `0xEF` prefix rule and the code-deposit +/// charge are all evaluated before the callback. A result rewritten to success therefore reports a +/// deployment that did not happen, at an address holding no code, with the constructor's state +/// changes rolled back. Honouring it would hand the caller a contract that does not exist. +/// +/// Detection only; nothing here compensates the journal. The original classification is restored, +/// the ledger counts the refusal, and the context's error slot carries the reason so that the +/// transaction fails with an error rather than with a fabricated receipt. Debug builds assert: +/// this is a detector, and a test corpus that produces this shape should stop rather than quietly +/// take the rejection path. +/// +/// Gated to REX7+. On a frozen spec, an inspector's rewrite reaches no accounting lane that can be +/// made unsound by it, and the specs' behaviour — including on the inspected path — is closed. +#[inline] +fn reject_forbidden_create_rewrite( + context: &mut MegaContext, + before: InstructionResult, + result: &mut InterpreterResult, +) { + if !context.spec.is_enabled(MegaSpecId::REX7) || before.is_ok() || !result.result.is_ok() { + return; + } + result.result = before; + context.additional_limit.borrow_mut().record_inspector_rejected_rewrite(); + let slot = context.error(); + if slot.is_ok() { + *slot = Err(ContextError::Custom(String::from(FORBIDDEN_CREATE_REVIVAL))); + } + debug_assert!( + false, + "{FORBIDDEN_CREATE_REVIVAL}: {before:?} was rewritten to a success, which no journal \ + entry and no deposited code stands behind", + ); +} + impl Inspector, INTR> for MeasuredInspector where DB: Database, @@ -125,9 +175,9 @@ where INTR: InterpreterTypes, I: Inspector, INTR>, { - /// Measured, but without settling a segment: this runs after the frame is built and before - /// its settlement window is opened, so there is nothing open to close. The frame's own entry - /// hook opens the window on whatever counter this callback leaves behind. + /// Measured, but without settling a segment: this runs after the frame is built and before its + /// settlement window is opened, so there is nothing open to close. The frame's own entry hook + /// opens the window on whatever counter this callback leaves behind. #[inline] fn initialize_interp( &mut self, @@ -169,8 +219,6 @@ where self.inner.log(context, log); } - /// Forwards to the wrapped inspector's `log_full`, not to its `log`: the default `log_full` - /// already falls through to `log`, and short-circuiting here would silently drop an override. #[inline] fn log_full( &mut self, @@ -198,8 +246,6 @@ where outcome } - /// The frame's result gas is deliberately not booked — see - /// [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. #[inline] fn frame_end( &mut self, @@ -207,7 +253,13 @@ where frame_input: &FrameInput, frame_result: &mut FrameResult, ) { + let before = frame_result.instruction_result(); self.inner.frame_end(context, frame_input, frame_result); + // `frame_end` runs after `create_end` and is the last chance to rewrite a creation's + // classification, so the same refusal applies here. + if let FrameResult::Create(outcome) = frame_result { + reject_forbidden_create_rewrite(context, before, &mut outcome.result); + } } #[inline] @@ -246,8 +298,8 @@ where outcome } - /// `CreateInputs` is immutable here and the frame's result gas is deliberately not booked — - /// see [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. + /// Forwards, then refuses a failed-to-successful rewrite — see + /// [`reject_forbidden_create_rewrite`]. #[inline] fn create_end( &mut self, @@ -255,7 +307,9 @@ where inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { + let before = outcome.result.result; self.inner.create_end(context, inputs, outcome); + reject_forbidden_create_rewrite(context, before, &mut outcome.result); } /// Everything this callback receives is passed by value, so it cannot change execution state. diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 9d643727..774caafb 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -67,6 +67,14 @@ pub struct InspectorLedger { /// remaining gas and ignores a halting one's), and that classification is not final until /// after the last mutating callback. Booking the edit before then would be a guess. pub env: i128, + + /// How many rewrites the shim refused because their shape is forbidden. + /// + /// Today exactly one shape is: a `create_end` (or the `frame_end` after it) turning a + /// non-successful contract creation into a successful one. Such a rewrite runs after the + /// journal has already reverted the frame and after the deposit predicates have already + /// rejected the code, so honouring it would report a deployment that never happened. + pub rejected_rewrites: u32, } impl InspectorLedger { @@ -82,7 +90,7 @@ impl InspectorLedger { /// True for every observation-only inspector, and for every transaction that ran without one. #[inline] pub const fn is_zero(&self) -> bool { - self.gas == 0 && self.env == 0 + self.gas == 0 && self.env == 0 && self.rejected_rewrites == 0 } } @@ -95,11 +103,19 @@ mod tests { /// unmoved in that case. #[test] fn test_conjured_gas_is_the_net_of_both_lanes() { - let ledger = InspectorLedger { gas: 2_300, env: -2_300 }; + let ledger = InspectorLedger { gas: 2_300, env: -2_300, rejected_rewrites: 0 }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); } + /// A refused rewrite moves no gas but must still show the transaction was not left alone. + #[test] + fn test_a_rejected_rewrite_alone_is_not_zero() { + let ledger = InspectorLedger { gas: 0, env: 0, rejected_rewrites: 1 }; + assert_eq!(ledger.conjured_gas(), 0); + assert!(!ledger.is_zero()); + } + #[test] fn test_default_ledger_is_zero() { assert!(InspectorLedger::default().is_zero()); diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index b94f769f..53739542 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -558,6 +558,13 @@ impl AdditionalLimit { self.inspector.env += delta; } + /// Counts one rewrite the shim refused because its shape is forbidden — see + /// [`InspectorLedger::rejected_rewrites`](inspector_ledger::InspectorLedger::rejected_rewrites). + #[inline] + pub(crate) fn record_inspector_rejected_rewrite(&mut self) { + self.inspector.rejected_rewrites = self.inspector.rejected_rewrites.saturating_add(1); + } + /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, /// always 0 before) — the second term of /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas). From a94a09f48406f3616170b0128a601f40c5b17785 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 18:03:27 +0800 Subject: [PATCH 092/208] test(rex7): pin the shapes a rewriting inspector can take One test per shape the measurement shim has to handle, each written so that removing the mechanism it covers turns it red: - injecting gas into a running interpreter: the compute total is identical to the uninspected run's and usage still stops exactly at the limit (without the re-clamp the loop runs its whole 300k before anything notices); - removing gas: booked as a negative entry, not charged as work; - raising a child frame's gas limit: booked as conjured gas, and the envelope only balances because of it; - the same edit made by an intercepting callback: the inputs reach no frame, so nothing is booked; - reviving a failed creation: refused, loudly in debug and as an EVMError in release; - an observation-only inspector: an empty ledger and a transaction identical to one run with no inspector at all, down to the produced state. --- crates/mega-evm/tests/rex7/main.rs | 5 + .../mega-evm/tests/rex7/measured_inspector.rs | 645 ++++++++++++++++++ 2 files changed, 650 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/measured_inspector.rs diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index e99e4dfc..cc1b3c7a 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -17,6 +17,10 @@ //! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. +//! - `measured_inspector` — the shim every inspector is wrapped in: gas an inspector writes into an +//! interpreter counter or a frame's gas limit is measured at the callback boundary, booked, and +//! kept out of enforcement, with the clamp re-derived on the spot; reviving a failed creation is +//! refused; an observation-only inspector is bit-identical to no inspector at all. //! - `gas_leakage` — the three paths a per-frame gas mechanism can leak through (interception, //! TX-level rescue, frame return), each with a clamp outstanding. //! - `opcode_set_parity` — all 256 opcodes probed under both specs, so the REX7 table cannot gain @@ -81,6 +85,7 @@ mod guard_pass_static_gas; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; +mod measured_inspector; mod modexp_gas; mod opcode_set_parity; mod parity_shapes; diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs new file mode 100644 index 00000000..41568eb4 --- /dev/null +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -0,0 +1,645 @@ +//! The measurement shim: what an inspector does to gas is measured, booked, and kept out of +//! enforcement. +//! +//! `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector +//! callback, so anything that changes across one is the inspector's doing by construction — which +//! is what makes the callback boundary a sound place to measure from. +//! +//! Each test here is one shape a rewriting inspector can take, and each pins a different half of +//! the mechanism: +//! +//! - injecting gas into a running interpreter must not buy compute headroom, and the gas clamp must +//! tighten again immediately rather than at the next checkpoint; +//! - raising a child frame's gas limit conjures gas the transaction never funded, which the ledger +//! has to account for or the conservation law breaks; +//! - resurrecting a failed contract creation is refused outright; +//! - an observation-only inspector changes nothing at all; +//! - and removing gas is measured with the same machinery as adding it. + +use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + AdditionalLimit, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, MegaContext, MegaEvm, + MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, +}; +use revm::{ + bytecode::opcode::{ + CALL, CREATE, DUP1, JUMPDEST, JUMPI, MSTORE, POP, RETURN, STOP, SUB, SWAP1, + }, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + handler::EvmTr, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, + InterpreterResult, InterpreterTypes, + }, + state::EvmState, + Inspector, +}; + +/// Transaction gas limit used throughout: high enough that EVM gas is never what binds. +const TX_GAS_LIMIT: u64 = 100_000_000; + +/// Everything one transaction reports, plus what the shim booked for it. +struct Reading { + result: ExecutionResult, + compute_gas: u64, + enforced: u64, + destroyed: u64, + data_size: u64, + kv_updates: u64, + state_growth: u64, + gas_used: u64, + total_gas_spent: u64, + non_compute_gas: i128, + minted_call_stipend: u64, + ledger: InspectorLedger, + state: EvmState, +} + +impl Reading { + fn halt_reason(&self) -> &MegaHaltReason { + match &self.result { + ExecutionResult::Halt { reason, .. } => reason, + other => panic!("expected a halt, got {other:?}"), + } + } +} + +/// The conservation identity, stated with the term the measurement shim contributes. +/// +/// Uninspected, this is the identity `common::assert_terminal_identity` checks: the reported +/// compute total plus `MegaETH` storage gas, less the `CALL_STIPEND` the EVM minted into child +/// frames, is exactly the envelope the receipt reports. An inspector that conjures gas — by writing +/// into an interpreter's counter or into a frame's gas limit — makes the transaction spend less +/// than its frames recorded, by exactly what it conjured, so the identity only closes once the +/// ledger's term is taken out of the accounted side. +/// +/// This is what goes red when a lane the shim is supposed to book goes unbooked: the two sides +/// disagree by precisely the unbooked amount. +fn assert_identity(label: &str, r: &Reading) { + assert_eq!( + r.compute_gas, + r.enforced + r.destroyed, + "{label}: reported compute must split into enforced + destroyed", + ); + let accounted = i128::from(r.compute_gas) + r.non_compute_gas - + i128::from(r.minted_call_stipend) - + r.ledger.conjured_gas(); + assert_eq!( + accounted, + i128::from(r.total_gas_spent), + "{label}: the tracker lanes plus the inspector ledger must account for the whole envelope; \ + compute={} non_compute={} minted={} conjured={} envelope={}", + r.compute_gas, + r.non_compute_gas, + r.minted_call_stipend, + r.ledger.conjured_gas(), + r.total_gas_spent, + ); +} + +fn tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// The context every run in this module uses: REX7, no external environment, no operator fee. +fn context( + db: &mut MemoryDatabase, + limits: EvmTxRuntimeLimits, +) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX7).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context +} + +/// Runs the transaction with no inspector at all — the reference every inspected run is compared +/// against. +fn transact_plain(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> Reading { + let mut evm = MegaEvm::new(context(&mut db, limits)); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +/// Runs the transaction with `inspector` attached, borrowed so the caller can read it back +/// afterwards. +fn transact_inspected( + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + inspector: &mut I, +) -> Reading +where + I: for<'a> Inspector>, +{ + let mut evm = MegaEvm::new(context(&mut db, limits)).with_inspector(inspector); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +/// Like [`transact_inspected`], but surfaces the `EVMError` instead of panicking on it. +fn try_transact_inspected( + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + inspector: &mut I, +) -> Result<(), String> +where + I: for<'a> Inspector>, +{ + let mut evm = MegaEvm::new(context(&mut db, limits)).with_inspector(inspector); + evm.execute_transaction(tx()).map(|_| ()).map_err(|e| format!("{e:?}")) +} + +fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { + let (non_compute_gas, minted_call_stipend, _booked) = limit.conservation_terms_for_test(); + let gas_used = outcome.result_and_state.result.tx_gas_used(); + let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); + Reading { + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, + gas_used, + total_gas_spent, + non_compute_gas, + minted_call_stipend, + ledger: limit.inspector_ledger(), + state: outcome.result_and_state.state, + } +} + +fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// A countdown loop of plain opcodes with no checkpoint anywhere in the body, so the whole run is +/// one settlement segment and the gas clamp is the only thing enforcing the compute limit inside +/// it. +fn countdown_loop_code(iterations: u16) -> Bytes { + let mut code = Vec::new(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// A straight run of plain opcodes that always succeeds. +fn plain_run_code(pairs: usize) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder.append(STOP).build() +} + +fn limits_with_compute(limit: u64) -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) +} + +fn default_limits() -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) +} + +/// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. +/// +/// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact +/// number a test can assert on, and so the edit lands well inside the plain segment rather than at +/// its boundary. +#[derive(Default)] +struct GasEditor { + at: u64, + delta: i64, + steps: u64, + applied: bool, +} + +impl GasEditor { + fn new(at: u64, delta: i64) -> Self { + Self { at, delta, steps: 0, applied: false } + } +} + +impl Inspector for GasEditor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.steps != self.at || self.applied { + return; + } + self.applied = true; + if self.delta >= 0 { + interp.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + interp.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave enough gas for the removal to land", + ); + } + } +} + +/// Raises the gas limit of every call to [`CALLEE`] by a fixed amount. +#[derive(Default)] +struct CallGasLimitRaiser { + bonus: u64, + raises: u64, +} + +impl Inspector for CallGasLimitRaiser { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address == CALLEE { + inputs.gas_limit += self.bonus; + self.raises += 1; + } + None + } +} + +/// Rewrites every failed contract creation into a successful one — the shape the shim refuses. +#[derive(Default)] +struct CreateReviver; + +impl Inspector for CreateReviver { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if !outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Return; + } + } +} + +/// Counts callbacks and changes nothing. +#[derive(Default)] +struct Observer { + steps: u64, + calls: u64, + call_ends: u64, +} + +impl Inspector for Observer { + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.calls += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.call_ends += 1; + } +} + +/// (i) Gas injected into a running interpreter buys no compute headroom, is booked, and the clamp +/// tightens again on the spot. +/// +/// The fixture is a checkpoint-free loop under a compute limit far below what the loop needs, so +/// the gas clamp is the only thing that can stop it: the visible counter is pinned to the compute +/// headroom and revm's own gas check rejects the crossing opcode. An inspector then writes four +/// times that headroom into the counter, mid-loop. +/// +/// Three separate mechanisms are pinned: +/// +/// - **Enforcement does not eat the injection.** The recorded compute total is identical to the +/// uninspected run's, to the gas. Without the baseline shift, the injection reads as negative +/// work and the loop is handed free headroom. +/// - **The clamp is re-derived immediately.** Usage still stops exactly at the limit. Without the +/// re-clamp the loop runs on the injected gas until the frame ends, and the frame-exit settlement +/// then records the whole overshoot — the halt still lands, but hundreds of thousands of gas +/// late. +/// - **The ledger records it.** Exactly what was injected, no more. +#[test] +fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { + const INJECTED: u64 = 20_000; + let code = countdown_loop_code(10_000); + // Far below what the loop needs, so the clamp binds for the whole run. + let intrinsic = transact_plain(base_db(plain_run_code(0)), default_limits()).compute_gas; + let limits = limits_with_compute(intrinsic + 5_000); + + let plain = transact_plain(base_db(code.clone()), limits); + let mut inspector = GasEditor::new(20, INJECTED as i64); + let inspected = transact_inspected(base_db(code), limits, &mut inspector); + + assert!(inspector.applied, "the fixture must reach the injection point"); + assert!( + matches!(plain.halt_reason(), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "fixture check: the uninspected run must stop on the compute limit, got {:?}", + plain.halt_reason(), + ); + assert_eq!( + inspected.enforced, plain.enforced, + "the injection must be neither counted as work nor deducted from it, and the re-derived \ + clamp must stop the loop at the same opcode the uninspected run stopped at; \ + inspected result {:?}", + inspected.result, + ); + assert!( + matches!(inspected.halt_reason(), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "injected gas must not turn a compute-limit halt into something else, got {:?}", + inspected.halt_reason(), + ); + assert_eq!( + inspected.ledger.gas, + i128::from(INJECTED), + "the ledger must hold exactly what was injected", + ); + assert_eq!(inspected.ledger.env, 0, "no frame envelope was touched"); + assert_eq!( + i128::from(inspected.total_gas_spent) + i128::from(INJECTED), + i128::from(plain.total_gas_spent), + "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ + spends exactly that much less than the uninspected run", + ); + assert_identity("injected", &inspected); +} + +/// (v) The same machinery, in the other direction: gas removed from a running interpreter is +/// booked as a negative entry and is not charged as work. +/// +/// Under an active clamp the removal comes out of the hidden remainder rather than the visible +/// counter — the frame has more EVM gas than compute headroom, and destroying EVM gas does not +/// shrink the headroom — so the transaction runs to the same successful end while spending exactly +/// the removed amount more. +#[test] +fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { + const REMOVED: u64 = 1_000; + let code = plain_run_code(200); + + let plain = transact_plain(base_db(code.clone()), default_limits()); + let mut inspector = GasEditor::new(20, -(REMOVED as i64)); + let inspected = transact_inspected(base_db(code), default_limits(), &mut inspector); + + assert!(inspector.applied, "the fixture must reach the removal point"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); + assert_eq!( + inspected.ledger.gas, + -i128::from(REMOVED), + "the ledger must hold the removal as a negative entry", + ); + assert_eq!( + inspected.enforced, plain.enforced, + "gas the inspector destroyed is not work the EVM performed", + ); + assert_eq!( + inspected.total_gas_spent, + plain.total_gas_spent + REMOVED, + "the removed gas never comes back, so the envelope is exactly that much larger", + ); + assert_identity("removed", &inspected); +} + +/// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the +/// envelope only balances once the ledger accounts for it. +/// +/// The caller's `CALL` opcode debited the gas it forwards before any inspector callback ran, so the +/// bonus the inspector adds is paid for by nobody. The child hands it straight back on return, and +/// the transaction ends up spending exactly that much less than the uninspected run. +/// +/// Without the `env` lane the conservation law derives a destroyed total that is short by the +/// bonus, and the envelope assertion inside `execute_transaction` fails on the spot. +#[test] +fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { + const BONUS: u64 = 10_000; + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000u64) // gas + .append(CALL) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let plain = transact_plain(build_db(), default_limits()); + let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; + let inspected = transact_inspected(build_db(), default_limits(), &mut inspector); + + assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "the inner call must still succeed"); + assert_eq!( + inspected.ledger.env, + i128::from(BONUS), + "the ledger must hold exactly the gas the inspector added to the child's envelope", + ); + assert_eq!(inspected.ledger.gas, 0, "no interpreter counter was touched"); + assert_eq!( + inspected.total_gas_spent + BONUS, + plain.total_gas_spent, + "the child returns the conjured gas to its caller, so the transaction spends that much less", + ); + assert_eq!( + inspected.enforced, plain.enforced, + "a wider envelope is not more work: the child's compute budget comes from the compute \ + tracker, not from its gas limit", + ); + assert_identity("raised child gas limit", &inspected); +} + +/// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. +/// +/// A callback that returns a synthetic outcome has intercepted the frame: no frame is built from +/// the inputs, so no edit of theirs can widen an envelope. The gas that outcome carries is the +/// inspector's own choice and has nothing to do with the edit — here it is deliberately the +/// original forwarded amount, so the transaction really does conjure nothing and the identity has +/// to close at zero. +/// +/// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the +/// conservation law would come out over by the bonus — the same failure as not booking a real one, +/// with the sign flipped. +#[test] +fn test_an_intercepting_callback_books_no_envelope_adjustment() { + /// Raises the child's gas limit and then intercepts the call, handing back an outcome built + /// from the amount the caller actually forwarded. + #[derive(Default)] + struct Interceptor { + intercepted: u64, + } + + impl Inspector for Interceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + let forwarded = inputs.gas_limit; + inputs.gas_limit += 10_000; + self.intercepted += 1; + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), Gas::new(forwarded)), + inputs.return_memory_offset.clone(), + )) + } + } + + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = base_db(code).account_code(CALLEE, callee); + + let mut inspector = Interceptor::default(); + let inspected = transact_inspected(db, default_limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); + assert!( + inspected.ledger.is_zero(), + "an edit to inputs that never reach a frame conjures nothing; got {:?}", + inspected.ledger, + ); + assert_identity("intercepted", &inspected); +} + +/// (iii) A `create_end` that turns a failed contract creation into a successful one is refused, +/// loudly. +/// +/// By that point revm has already reverted the frame's journal checkpoint and already declined to +/// deposit any code, so the rewrite would report a deployment that did not happen. The shim +/// restores the original classification and refuses to let the transaction produce a receipt at +/// all: debug builds assert, release builds surface the refusal as an `EVMError`. +#[test] +fn test_reviving_a_failed_creation_is_refused() { + // Init code that reverts immediately: PUSH1 0, PUSH1 0, REVERT. + let init_code: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init_code.iter().enumerate() { + builder = builder + .push_number(u64::from(*byte)) + .push_number(offset as u64) + .append(revm::bytecode::opcode::MSTORE8); + } + let code = builder + .push_number(init_code.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + let db = base_db(code); + + let run = || { + let mut inspector = CreateReviver; + try_transact_inspected(db.clone(), default_limits(), &mut inspector) + }; + + if cfg!(debug_assertions) { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)); + std::panic::set_hook(previous); + let payload = panicked.expect_err("the detector must fire in debug builds"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or_default() + .to_string(); + assert!( + message.contains("inspector rewrote a failed contract creation into a successful one"), + "the assertion must name the shape it caught; got {message:?}", + ); + } else { + let error = run().expect_err("the refusal must surface as an EVMError in release builds"); + assert!( + error.contains("inspector rewrote a failed contract creation into a successful one"), + "the error must name the shape it caught; got {error:?}", + ); + } +} + +/// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. +/// +/// This is the property every tracer in production depends on. The comparison is against a run with +/// no inspector attached at all, across every number the transaction reports and the state it +/// produced — not just the ones the ledger touches. +#[test] +fn test_an_observing_inspector_changes_nothing() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(0x20), U256::from(0x99)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let plain = transact_plain(build_db(), default_limits()); + let mut inspector = Observer::default(); + let inspected = transact_inspected(build_db(), default_limits(), &mut inspector); + + assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); + assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); + assert_eq!(inspector.call_ends, 2, "every call must be paired"); + + assert!( + inspected.ledger.is_zero(), + "an observation-only inspector must leave an empty ledger; got {:?}", + inspected.ledger, + ); + assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); + assert_eq!(inspected.compute_gas, plain.compute_gas); + assert_eq!(inspected.enforced, plain.enforced); + assert_eq!(inspected.destroyed, plain.destroyed); + assert_eq!(inspected.data_size, plain.data_size); + assert_eq!(inspected.kv_updates, plain.kv_updates); + assert_eq!(inspected.state_growth, plain.state_growth); + assert_eq!(inspected.gas_used, plain.gas_used); + assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); + assert_eq!(inspected.non_compute_gas, plain.non_compute_gas); + assert_eq!(inspected.minted_call_stipend, plain.minted_call_stipend); + assert_eq!(inspected.state, plain.state, "the produced state must be identical"); + assert_identity("observed", &inspected); + assert_identity("plain", &plain); +} From 1f694481e2cb3944efe0a568be4cd71920a9dd6f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 18:31:00 +0800 Subject: [PATCH 093/208] docs: state the inspector term in the conservation law and what it does not cover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The law in AGENTS.md was missing a term, and `evm/AGENTS.md` still said an inspector that edits the interpreter gas counter moves REX7 accounting — which the shim now prevents. Both are corrected, and both now say plainly what is still open: a `call_end` / `create_end` that rewrites a frame *result* moves the split, because whether such an edit touches the envelope depends on a classification that is not final until after the last mutating callback. --- AGENTS.md | 5 ++++- crates/mega-evm/src/evm/AGENTS.md | 12 ++++++++--- crates/mega-evm/src/evm/inspector.rs | 5 +++-- crates/mega-evm/src/evm/result.rs | 12 ++++++----- crates/mega-evm/src/limit/inspector_ledger.rs | 20 ++++++++++++------- 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 11100d47..a45c13f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,9 +123,12 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. A frame init that refuses to build a frame at all is settled in `after_frame_init`, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). - The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. + The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend + inspector_conjured_gas − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call; it is booked per mint event — at the CALL-family settlement, before frame init — so a value call turned away at frame entry (insufficient balance, call depth) books one too, because its refund returns the mint into the caller's envelope. + `inspector_conjured_gas` is the same kind of correction for a producer outside the EVM: `MegaEvm` wraps every inspector it is handed in `MeasuredInspector`, which snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference into `AdditionalLimit::inspector_ledger` — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. + Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. + The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 9b5d17a0..f62112a2 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -21,9 +21,15 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - Oracle `sload` handling forces cold semantics for deterministic replay. - `MegaEvm` methods read aggregate resource usage from `additional_limit` after execution. - Keep inspector and non-inspector paths behaviorally aligned. - That alignment assumes the inspector does not rewrite a frame result and does not edit the interpreter gas counter. - A rewriting inspector — one that changes `CallOutcome` / `CreateOutcome` in `call_end` / `create_end`, or that spends or refunds interpreter gas in `step` / `step_end` — will make REX7 compute accounting diverge: the exceptional-halt burn split is settled before `frame_end`, and the plain-segment delta is measured from the interpreter counter, so either edit moves the reported `compute_gas_used` / `compute_gas_destroyed` off the uninspected path. - Observational inspectors (`NoOpInspector`, tracers that only read) stay aligned. + Observational inspectors (`NoOpInspector`, tracers that only read) are bit-identical to no inspector at all, and must stay so. +- Every inspector is wrapped in `MeasuredInspector` (`inspector.rs`) before it reaches the inner EVM; the public accessors hand the unwrapped one back, so the type a caller names is unchanged. + The shim snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. + A gas-counter edit is kept out of REX7 compute accounting (the checkpoint baseline is shifted by it) and out of the compute headroom (the gas clamp is re-derived immediately). + A raised frame `gas_limit` is booked as conjured gas so the destroyed-remainder derivation still balances. + Add the shim's counterpart when adding an `Inspector` callback: an unwrapped callback is an unmeasured hole, not a compile error. +- What the shim does **not** yet cover, because the answer depends on a frame's final classification and that is not settled until after the last mutating callback: a `call_end` / `create_end` that rewrites a frame result's gas or its classification, and the gas an intercepting callback puts into a synthetic outcome. + Those still move REX7 accounting off the uninspected path, and a large enough move trips the conservation `debug_assert`. + One shape is refused outright rather than left to diverge: a `create_end` (or the `frame_end` after it) turning a failed creation into a successful one — see `reject_forbidden_create_rewrite`. ## WHERE TO LOOK - New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` still aliases `rex5` and expresses its deltas as `is_enabled` dispatch inside the shared handlers; `rex7` is a standalone checkpoint table built from revm's base table, with the 17 storage / CALL / CREATE / SELFDESTRUCT / not-yet-activated slots inherited from `rex6`, and with 15 volatile `*_checkpoint` handlers plus `gas_checkpoint` registered as rex7-only). diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 11fc6859..1a46ec1d 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -104,8 +104,9 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { /// frame. /// /// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped -/// entirely and the inputs it edited are dropped unread, so no edit of theirs can move the -/// transaction's envelope. +/// entirely and the EVM never reads the inputs it edited, so the edit by itself moves nothing. Gas +/// the inspector then puts into that synthetic outcome travels through the result lane, which this +/// lane deliberately does not cover — see [`InspectorLedger::env`](crate::InspectorLedger::env). #[inline] fn book_env_adjustment( context: &MegaContext, diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 7a948cb9..7860caab 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -35,10 +35,12 @@ pub struct MegaTransactionOutcome { /// to accumulate into block-level compute accounting; it is not the number to compare against /// a limit — see [`compute_gas_destroyed`](Self::compute_gas_destroyed). /// - /// These two fields are the uninspected execution's split. - /// An inspector that rewrites a frame result or edits the interpreter gas counter will make - /// them diverge from that path: the burn split is settled before `frame_end`, and the - /// plain-segment delta is read from the interpreter counter. + /// These two fields are the uninspected execution's split, and an observation-only inspector + /// leaves them exactly there. An inspector's edits to interpreter gas counters and to frame + /// gas limits are measured at the callback boundary and kept out of the split — see + /// [`InspectorLedger`](crate::InspectorLedger) — but one that rewrites a frame *result*, in + /// `call_end` or `create_end`, still moves them: the burn split is settled before those + /// callbacks run. pub compute_gas_used: u64, /// The part of [`compute_gas_used`](Self::compute_gas_used) the transaction destroyed rather /// than performed (Rex7+, always 0 before). @@ -62,7 +64,7 @@ pub struct MegaTransactionOutcome { /// itself rather than out of this subtraction. /// /// Same inspector caveat as [`compute_gas_used`](Self::compute_gas_used): the field is the - /// uninspected split, and a rewriting inspector will move it. + /// uninspected split, and an inspector that rewrites a frame result will move it. pub compute_gas_destroyed: u64, /// The part of [`compute_gas_used`](Self::compute_gas_used) every compute-gas limit is /// evaluated against: the work the transaction performed, with Rex7+ destroyed remainders left diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 774caafb..5cf90377 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -58,14 +58,20 @@ pub struct InspectorLedger { /// the caller paid for and no frame ever receives. /// /// Only adjustments that actually reach a frame are booked. When the callback returns a - /// synthetic outcome it has intercepted the frame entirely and the inputs it edited are - /// discarded, so nothing about them can move the envelope. + /// synthetic outcome it has intercepted the frame entirely, and the EVM never reads the inputs + /// it edited — so the edit by itself moves nothing. (The inspector can of course read its own + /// edit back and size the synthetic outcome from it. That gas travels through the result lane + /// below, not through this one.) /// - /// Adjustments to a frame's *result* gas — `call_end` calling `spend_all`, say — are - /// deliberately not booked here. Whether such an edit moves the transaction's envelope at all - /// depends on the frame's final classification (a caller reclaims a returning frame's - /// remaining gas and ignores a halting one's), and that classification is not final until - /// after the last mutating callback. Booking the edit before then would be a guess. + /// Adjustments to a frame's *result* gas — `call_end` calling `spend_all`, an intercepting + /// callback handing back an outcome wider than what it was given — are deliberately not booked + /// here. Whether such an edit moves the transaction's envelope at all depends on the frame's + /// final classification (a caller reclaims a returning frame's remaining gas and ignores a + /// halting one's), and that classification is not final until after the last mutating + /// callback. Booking the edit before then would be a guess. Until the frame lifecycle grows a + /// settlement point past that callback, a result-rewriting inspector can still move REX7 + /// accounting off the uninspected path, and a large enough move trips the conservation + /// `debug_assert`. pub env: i128, /// How many rewrites the shim refused because their shape is forbidden. From 5b8eef9ae4fbe873c8bcefd43392a62bac637a3a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 22:22:03 +0800 Subject: [PATCH 094/208] docs(evm): state the refused create rewrite inline instead of linking a private fn `cargo doc` rejects a public item's documentation linking to a private one, and `create_end` pointed at the module-private helper. Say what the refusal is on the method itself, which is what a reader of the public API needs anyway. --- crates/mega-evm/src/evm/inspector.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 1a46ec1d..302b61cb 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -299,8 +299,10 @@ where outcome } - /// Forwards, then refuses a failed-to-successful rewrite — see - /// [`reject_forbidden_create_rewrite`]. + /// Forwards, then refuses a rewrite that turned a failed contract creation into a successful + /// one: by this point the journal is already reverted and no code was deposited, so the + /// original classification is restored and the transaction is failed with an error rather than + /// allowed to report a deployment that did not happen. #[inline] fn create_end( &mut self, From f7c7e7f11db9ed1204561899b7ce416843cb1001 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 22:59:20 +0800 Subject: [PATCH 095/208] refactor(evm): settle a frame once, after the last callback that can rewrite it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revm assembles a frame's result, decides its journal checkpoint and — for a contract creation — runs the deposit predicates and writes the code all inside one function, then runs the inspector's last mutating callback after that function returns. MegaETH's accounting needs the other order: it settles a frame on the classification the caller is actually handed, and an inspector is allowed to rewrite that classification. Split the upstream function in two. `classify_frame_action` decides what the frame's result is and records the journal decision it reached; `commit_frame_journal` carries that decision out. Between them sit the frozen post-action charge, the inspector's `frame_end`, and one settlement point, `AdditionalLimit::finalize_frame`, which now owns the destroyed-remainder booking, the frame-init refusal booking and the gas rescue for every frame outcome — a frame that ran, a frame revm's init refused, and a synthetic result that never reached that accounting at all. The rescue is taken on the gas the EVM left behind rather than the gas the result now carries, so an inspector that spends a failing result down cannot take the sender's refund with it, and what such a callback does to a returning frame's gas is booked in a new ledger lane the conservation law reads. Frozen specs keep their journal decision where revm takes it, right after the classification: what they replay includes the state a frame leaves behind when a later rewrite fails it. Both frame loops and both frame-init paths now run the same bodies, so the two copies cannot drift apart in the settlement, in the gas reading the frozen charge is measured against, or in the guards that stand in front of interceptor dispatch. --- crates/mega-evm/src/evm/execution.rs | 576 ++++++++++-------- crates/mega-evm/src/evm/frame.rs | 309 ++++++++++ crates/mega-evm/src/evm/mod.rs | 1 + crates/mega-evm/src/limit/inspector_ledger.rs | 31 +- crates/mega-evm/src/limit/limit.rs | 208 +++++-- .../src/limit/storage_call_stipend.rs | 16 +- crates/mega-evm/src/sandbox/execution.rs | 4 +- 7 files changed, 814 insertions(+), 331 deletions(-) create mode 100644 crates/mega-evm/src/evm/frame.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 7204e5ca..b61af4a0 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -40,11 +40,12 @@ use revm::{ Inspector, Journal, }; +use super::frame::{classify_frame_action, commit_frame_journal}; use crate::{ constants, dispatch_system_contract_interceptors, is_deposit_like_transaction, is_mega_system_transaction_with, limit::ACCOUNT_INFO_WRITE_SIZE, sent_from_system_address, - ExternalEnvTypes, HostExt, JournalInspectTr, MeasuredInspector, MegaContext, MegaEvm, - MegaHaltReason, MegaInstructions, MegaSpecId, MegaTransactionError, + AdditionalLimit, ExternalEnvTypes, FrameExit, HostExt, JournalInspectTr, MeasuredInspector, + MegaContext, MegaEvm, MegaHaltReason, MegaInstructions, MegaSpecId, MegaTransactionError, MEGA_SYSTEM_TRANSACTION_SOURCE_HASH, }; @@ -563,34 +564,111 @@ impl MegaEvm { Ok(()) } - /// Apply `MiniRex` additional limits after frame action processing. + /// Charges the compute gas revm's own frame-action processing spent, on the specs that read + /// it from the result rather than weighing it beforehand. /// - /// Under REX5+ for CREATE results, the code-deposit compute gas was already settled in - /// [`after_frame_run_instructions`]; pass `None` here so the post-action hook does not - /// re-record what that settlement recorded, or record what it deliberately did not. + /// The only charge this ever sees is a contract creation's code deposit, and only through + /// REX4: REX5 onwards weigh that charge at the frame's exit — against a predicate, before it + /// is taken — and pass `None` here so the same gas is not recorded twice. A call frame's + /// classification spends nothing, so its delta is structurally zero. + /// + /// This runs *before* the last mutating callback, unlike the rest of the frame's settlement. + /// The delta it measures is a difference between two readings of the result's gas, and a + /// callback that edits that gas would land inside the difference and be recorded as compute + /// work the EVM performed. The specs that reach this arm are frozen, so the reading stays + /// where their behaviour was fixed. #[inline] - fn after_frame_run( + fn settle_post_action_charge( ctx: &MegaContext, - frame_output: &mut ItemOrResult, - gas_remaining_before_process_action: Option, - ) -> Result<(), ContextDbError>> { + frame_result: &mut FrameResult, + gas_remaining_before_classification: Option, + ) { if !ctx.spec.is_enabled(MegaSpecId::MINI_REX) { - return Ok(()); + return; } - let is_rex5 = ctx.spec.is_enabled(MegaSpecId::REX5); + let pass_through = if ctx.spec.is_enabled(MegaSpecId::REX5) && + matches!(frame_result, FrameResult::Create(_)) + { + None + } else { + gas_remaining_before_classification + }; + ctx.additional_limit.borrow_mut().settle_post_action_charge(frame_result, pass_through); + } - if let ItemOrResult::Result(frame_result) = frame_output { - // REX5+: code-deposit compute gas for CREATE results was already settled at the - // frame's exit. Skip post-action recording so we don't double-count. - let pass_through = if is_rex5 && matches!(frame_result, FrameResult::Create(_)) { - None - } else { - gas_remaining_before_process_action - }; - ctx.additional_limit.borrow_mut().after_frame_run(frame_result, pass_through); + /// The single point a frame's outcome is settled: after the last callback that can rewrite it, + /// and before the journal is told what to do with it. + /// + /// `inspector_gas_delta` is what that callback did to the result's gas, and is zero on the + /// uninspected path, where no callback runs at all. + #[inline] + fn finalize_frame( + ctx: &MegaContext, + result: &mut FrameResult, + exit: FrameExit, + inspector_gas_delta: i128, + ) { + if !ctx.spec.is_enabled(MegaSpecId::MINI_REX) { + return; } + ctx.additional_limit.borrow_mut().finalize_frame(result, exit, inspector_gas_delta); + } - Ok(()) + /// The whole of a frame's exit, from its final action to the journal decision — the body both + /// frame loops run. + /// + /// The loops differ in exactly one thing, which is what `last_callback` carries: the inspected + /// loop passes the inspector's `frame_end` and the plain loop passes nothing. Everything else + /// — the gas reading the frozen post-action charge is measured against, the settlement point, + /// the journal decision — is this function, so the two loops cannot drift apart in it. + fn settle_and_commit_frame( + ctx: &mut MegaContext, + frame: &mut EthFrame, + action: InterpreterAction, + last_callback: impl FnOnce(&mut MegaContext, &FrameInput, &mut FrameResult), + ) -> FrameInitOrResult> { + let gas_remaining_before = match (&action, ctx.spec.is_enabled(MegaSpecId::MINI_REX)) { + (InterpreterAction::Return(interpreter_result), true) => { + Some(interpreter_result.gas.remaining()) + } + _ => None, + }; + + let pending = match classify_frame_action(ctx, frame, action) { + ItemOrResult::Item(frame_init) => return ItemOrResult::Item(frame_init), + ItemOrResult::Result(pending) => pending, + }; + frame.set_finished(true); + let (mut result, journal) = pending.split(); + + // Where the journal decision goes is the one thing about a frame's exit that is not the + // same on every spec. Frozen specs take it here, the moment the classification is done, + // because that is where revm takes it and because what they replay includes the state a + // frame leaves behind when a later rewrite fails it: a contract creation that commits and + // is then rewritten into a halt keeps its deployed code. REX7 withholds the decision until + // the settlement below has run, so that the state a frame leaves behind agrees with the + // result its caller is handed. + let mut deferred_journal = None; + if ctx.spec.is_enabled(MegaSpecId::REX7) { + deferred_journal = Some(journal); + } else { + commit_frame_journal(ctx, journal, &result); + } + + Self::settle_post_action_charge(ctx, &mut result, gas_remaining_before); + + let gas_before_callback = result.gas().remaining(); + last_callback(ctx, &frame.input, &mut result); + let inspector_gas_delta = + i128::from(result.gas().remaining()) - i128::from(gas_before_callback); + + Self::finalize_frame(ctx, &mut result, FrameExit::Ran, inspector_gas_delta); + + if let Some(journal) = deferred_journal { + commit_frame_journal(ctx, journal, &result); + } + + ItemOrResult::Result(result) } } @@ -627,7 +705,7 @@ fn canonical_code_deposit_gas( code_deposit_gas: u64, ) -> Option { let cfg = ctx.cfg(); - will_return_create_charge_code_deposit( + super::frame::will_return_create_charge_code_deposit( interpreter_result, cfg.max_code_size(), cfg.spec().into_eth_spec(), @@ -637,40 +715,6 @@ fn canonical_code_deposit_gas( .then_some(code_deposit_gas) } -/// Mirrors `revm_handler::frame::return_create`'s pre-commit predicate. -/// Returns `true` iff `return_create` would charge `code_deposit_gas` -/// from the interpreter gas and commit the checkpoint. -/// -/// REVIEW ON UPSTREAM BUMP: keep in lockstep with -/// `revm-handler::frame::return_create`. Any revm bump that touches the -/// predicate inputs (`is_ok`, EIP-3541 gate, EIP-170 gate, code-deposit -/// gas availability) requires re-auditing this helper. -fn will_return_create_charge_code_deposit( - interpreter_result: &InterpreterResult, - max_code_size: usize, - runtime_spec_id: revm::primitives::hardfork::SpecId, - is_eip3541_disabled: bool, - code_deposit_gas: u64, -) -> bool { - use revm::primitives::hardfork::SpecId; - - if !interpreter_result.result.is_ok() { - return false; - } - if !is_eip3541_disabled && - runtime_spec_id.is_enabled_in(SpecId::LONDON) && - interpreter_result.output.first() == Some(&0xEF) - { - return false; - } - if runtime_spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) && - interpreter_result.output.len() > max_code_size - { - return false; - } - interpreter_result.gas.remaining() >= code_deposit_gas -} - impl Handler for MegaHandler where @@ -1356,70 +1400,26 @@ where } } -impl revm::handler::EvmTr for MegaEvm +/// What [`MegaEvm::init_frame_unsettled`] hands back: revm's own frame-init outcome, plus how the +/// frame's settlement should read a refusal. +type UnsettledFrameInit<'a, DB, ExtEnvs> = Result< + (FrameInitResult<'a, EthFrame>, FrameExit), + ContextDbError>, +>; + +impl MegaEvm where DB: Database, { - type Context = MegaContext; - - type Instructions = MegaInstructions; - - type Precompiles = PrecompilesMap; - - type Frame = EthFrame; - - #[inline] - fn all( - &self, - ) -> (&Self::Context, &Self::Instructions, &Self::Precompiles, &FrameStack) { - (&self.inner.ctx, &self.inner.instruction, &self.inner.precompiles, &self.inner.frame_stack) - } - - #[inline] - fn all_mut( - &mut self, - ) -> ( - &mut Self::Context, - &mut Self::Instructions, - &mut Self::Precompiles, - &mut FrameStack, - ) { - ( - &mut self.inner.ctx, - &mut self.inner.instruction, - &mut self.inner.precompiles, - &mut self.inner.frame_stack, - ) - } - - #[inline] - fn ctx(&mut self) -> &mut Self::Context { - &mut self.inner.ctx - } - - #[inline] - fn ctx_ref(&self) -> &Self::Context { - &self.inner.ctx - } - - #[inline] - fn ctx_instructions(&mut self) -> (&mut Self::Context, &mut Self::Instructions) { - (&mut self.inner.ctx, &mut self.inner.instruction) - } - - #[inline] - fn ctx_precompiles(&mut self) -> (&mut Self::Context, &mut Self::Precompiles) { - (&mut self.inner.ctx, &mut self.inner.precompiles) - } - - fn frame_stack(&mut self) -> &mut FrameStack { - &mut self.inner.frame_stack - } - - fn frame_init( + /// Everything `frame_init` decides — whether a frame is built at all, and what result stands + /// in for it when it is not — with the refusal left unsettled. + /// + /// Both frame-init paths run this. The inspected one has a callback to insert between the + /// refusal and its settlement, so the settlement cannot live in here. + fn init_frame_unsettled( &mut self, - mut frame_init: ::FrameInit, - ) -> Result, ContextDbError> { + mut frame_init: FrameInit, + ) -> UnsettledFrameInit<'_, DB, ExtEnvs> { let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); let is_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX); let is_rex3_enabled = self.ctx().spec.is_enabled(MegaSpecId::REX3); @@ -1464,40 +1464,14 @@ where } } - // REX4+: If a TX-level limit is already exceeded (e.g., intrinsic DataSize/KVUpdate - // overflow from before_tx_start), abort before interceptor dispatch. Interceptors - // return synthetic results that skip before_frame_init(), which would otherwise - // catch the exceeded limit. - // - // Gated to REX4 only: pre-REX4 specs use TX-global check_limit() which catches - // intrinsic overflow during execution. Changing pre-REX4 behavior would break replay. - if is_rex4_enabled { - // Separate borrow scope: the RefMut must be dropped before push_empty_frame - // borrows again. - let exceeded = additional_limit - .borrow_mut() - .frame_result_if_exceeding_limit(&frame_init.frame_input); - if let Some(frame_result) = exceeded { - additional_limit.borrow_mut().push_empty_frame(); - return Ok(FrameInitResult::Result(frame_result)); - } - } - - // REX5+: enforce `CALL_STACK_LIMIT` before interceptor dispatch. Interceptors - // short-circuit before revm's `make_call_frame` runs its own depth check, so - // without this guard a system contract could be invoked at unbounded depth. - // Scope mirrors interceptor dispatch (Call/StaticCall only); other schemes still - // flow into revm where its own depth check applies. - if is_rex5_enabled { - if let FrameInput::Call(call_inputs) = &frame_init.frame_input { - if matches!(call_inputs.scheme, CallScheme::Call | CallScheme::StaticCall) && - frame_init.depth > CALL_STACK_LIMIT as usize - { - let frame_result = gen_call_too_deep_result(call_inputs); - additional_limit.borrow_mut().push_empty_frame(); - return Ok(FrameInitResult::Result(frame_result)); - } - } + if let Some((frame_result, exit)) = Self::refuse_frame_before_dispatch( + &additional_limit, + &frame_init, + is_rex4_enabled, + is_rex5_enabled, + ) { + additional_limit.borrow_mut().push_empty_frame(); + return Ok((FrameInitResult::Result(frame_result), exit)); } // System contract interception dispatch. @@ -1523,7 +1497,7 @@ where if is_mini_rex_enabled { additional_limit.borrow_mut().push_empty_frame(); } - return Ok(FrameInitResult::Result(result)); + return Ok((FrameInitResult::Result(result), FrameExit::RefusedSynthetically)); } } } @@ -1533,7 +1507,7 @@ where .borrow_mut() .before_frame_init(&mut frame_init, self.ctx().journal_mut())? { - return Ok(FrameInitResult::Result(frame_result)); + return Ok((FrameInitResult::Result(frame_result), FrameExit::Refused)); } } @@ -1545,11 +1519,146 @@ where additional_limit.borrow_mut().after_frame_init(&init_result); } + Ok((init_result, FrameExit::Refused)) + } + + /// The two guards that stand in front of system contract interceptor dispatch, and the + /// refusal each of them produces. + /// + /// The order is load-bearing: a transaction that is already over a resource limit has to + /// report that, rather than have it shadowed by a depth rejection that happens to also apply. + /// Both frame-init paths run this, so an inspector's synthetic outcome cannot be delivered + /// under conditions the plain path refuses. + fn refuse_frame_before_dispatch( + additional_limit: &core::cell::RefCell, + frame_init: &FrameInit, + is_rex4_enabled: bool, + is_rex5_enabled: bool, + ) -> Option<(FrameResult, FrameExit)> { + // REX4+: If a TX-level limit is already exceeded (e.g., intrinsic DataSize/KVUpdate + // overflow from before_tx_start), abort before interceptor dispatch. Interceptors + // return synthetic results that skip before_frame_init(), which would otherwise + // catch the exceeded limit. + // + // Gated to REX4 only: pre-REX4 specs use TX-global check_limit() which catches + // intrinsic overflow during execution. Changing pre-REX4 behavior would break replay. + if is_rex4_enabled { + let exceeded = additional_limit + .borrow_mut() + .frame_result_if_exceeding_limit(&frame_init.frame_input); + if let Some(frame_result) = exceeded { + return Some((frame_result, FrameExit::Refused)); + } + } + + // REX5+: enforce `CALL_STACK_LIMIT` before interceptor dispatch. Interceptors + // short-circuit before revm's `make_call_frame` runs its own depth check, so + // without this guard a system contract could be invoked at unbounded depth. + // Scope mirrors interceptor dispatch (Call/StaticCall only); other schemes still + // flow into revm where its own depth check applies. + if is_rex5_enabled { + if let FrameInput::Call(call_inputs) = &frame_init.frame_input { + if matches!(call_inputs.scheme, CallScheme::Call | CallScheme::StaticCall) && + frame_init.depth > CALL_STACK_LIMIT as usize + { + return Some(( + gen_call_too_deep_result(call_inputs), + FrameExit::RefusedSynthetically, + )); + } + } + } + + None + } +} + +impl revm::handler::EvmTr for MegaEvm +where + DB: Database, +{ + type Context = MegaContext; + + type Instructions = MegaInstructions; + + type Precompiles = PrecompilesMap; + + type Frame = EthFrame; + + #[inline] + fn all( + &self, + ) -> (&Self::Context, &Self::Instructions, &Self::Precompiles, &FrameStack) { + (&self.inner.ctx, &self.inner.instruction, &self.inner.precompiles, &self.inner.frame_stack) + } + + #[inline] + fn all_mut( + &mut self, + ) -> ( + &mut Self::Context, + &mut Self::Instructions, + &mut Self::Precompiles, + &mut FrameStack, + ) { + ( + &mut self.inner.ctx, + &mut self.inner.instruction, + &mut self.inner.precompiles, + &mut self.inner.frame_stack, + ) + } + + #[inline] + fn ctx(&mut self) -> &mut Self::Context { + &mut self.inner.ctx + } + + #[inline] + fn ctx_ref(&self) -> &Self::Context { + &self.inner.ctx + } + + #[inline] + fn ctx_instructions(&mut self) -> (&mut Self::Context, &mut Self::Instructions) { + (&mut self.inner.ctx, &mut self.inner.instruction) + } + + #[inline] + fn ctx_precompiles(&mut self) -> (&mut Self::Context, &mut Self::Precompiles) { + (&mut self.inner.ctx, &mut self.inner.precompiles) + } + + fn frame_stack(&mut self) -> &mut FrameStack { + &mut self.inner.frame_stack + } + + fn frame_init( + &mut self, + frame_init: ::FrameInit, + ) -> Result, ContextDbError> { + let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); + let additional_limit = self.ctx().additional_limit.clone(); + + let (mut init_result, exit) = self.init_frame_unsettled(frame_init)?; + + // There is no inspector on this path, so the callback slot the settlement leaves open is + // empty and the frame's refusal is settled with nothing having rewritten it. + if is_mini_rex_enabled { + if let ItemOrResult::Result(result) = &mut init_result { + additional_limit.borrow_mut().finalize_frame(result, exit, 0); + } + } Ok(init_result) } - /// This method copies the logic from `revm::handler::EvmTr::frame_run` to and add additional - /// logic before `process_next_action` to handle the additional limit. + /// Runs one frame, settles it and tells the journal what to do with it. + /// + /// This is `revm::handler::EvmTr::frame_run` with `MegaETH`'s frame hooks and with the journal + /// decision withheld until the frame's settlement has run — see + /// [`settle_and_commit_frame`](MegaEvm::settle_and_commit_frame), which is the whole of the + /// body this shares with the inspected loop. There is no inspector on this path, so the + /// callback slot the settlement leaves for one is empty. #[inline] fn frame_run( &mut self, @@ -1572,28 +1681,7 @@ where // After frame_run instructions Hook Self::after_frame_run_instructions(context, frame, &mut action)?; - // Record gas remaining before frame action processing - let gas_remaining_before = match (&action, context.spec.is_enabled(MegaSpecId::MINI_REX)) { - (InterpreterAction::Return(interpreter_result), true) => { - Some(interpreter_result.gas.remaining()) - } - _ => None, - }; - - // Process the frame action, it may need to create a new frame or return the current frame - // result. - let mut frame_output = frame - .process_next_action::<_, ContextDbError>(context, action) - .inspect(|i| { - if i.is_result() { - frame.set_finished(true); - } - })?; - - // After frame_run Hook - Self::after_frame_run(context, &mut frame_output, gas_remaining_before)?; - - Ok(frame_output) + Ok(Self::settle_and_commit_frame(context, frame, action, |_, _, _| {})) } fn frame_return_result( @@ -1722,63 +1810,62 @@ where let is_mini_rex_enabled = ctx.spec.is_enabled(MegaSpecId::MINI_REX); let is_rex4_enabled = ctx.spec.is_enabled(MegaSpecId::REX4); let is_rex5_enabled = ctx.spec.is_enabled(MegaSpecId::REX5); + let additional_limit = ctx.additional_limit.clone(); // Check if inspector wants to skip this call/create - if let Some(mut output) = frame_start(ctx, inspector, &mut frame_init.frame_input) { + if let Some(output) = frame_start(ctx, inspector, &mut frame_init.frame_input) { // Inspector intercepted — `frame_init()` is skipped entirely, so neither - // `frame_result_if_exceeding_limit` nor `before_frame_init` would run. - // - // The priority order below mirrors `frame_init`'s exact order so that a - // TX-level additional-limit exceed is reported instead of being shadowed by - // a CallTooDeep guard: - // 1. TX-level limit exceed (REX4+) - // 2. CALL_STACK_LIMIT depth guard (REX5+) - // 3. Deliver the inspector's synthetic output - // Each early-return path calls `frame_end` to keep inspector callbacks paired. - - // (1) REX4+: if a TX-level limit is already exceeded (e.g., intrinsic - // overflow), abort to ensure correct gas rescue before inspector callbacks. - // Gated to REX4 to avoid changing stable spec behavior. - if is_rex4_enabled { - let exceeded = ctx - .additional_limit - .borrow_mut() - .frame_result_if_exceeding_limit(&frame_init.frame_input); - if let Some(mut frame_result) = exceeded { - ctx.additional_limit.borrow_mut().push_empty_frame(); - frame_end(ctx, inspector, &frame_init.frame_input, &mut frame_result); - return Ok(ItemOrResult::Result(frame_result)); - } - } - // (2) REX5+: enforce CALL_STACK_LIMIT for Call/StaticCall so an inspector - // cannot deliver a synthetic call result at unbounded depth, mirroring the - // protection added to `frame_init` before interceptor dispatch. - if is_rex5_enabled { - if let FrameInput::Call(call_inputs) = &frame_init.frame_input { - if matches!(call_inputs.scheme, CallScheme::Call | CallScheme::StaticCall) && - frame_init.depth > CALL_STACK_LIMIT as usize - { - let mut frame_result = gen_call_too_deep_result(call_inputs); - ctx.additional_limit.borrow_mut().push_empty_frame(); - frame_end(ctx, inspector, &frame_init.frame_input, &mut frame_result); - return Ok(ItemOrResult::Result(frame_result)); - } - } - } - // (3) MINI_REX+: push empty frame to keep the limit tracker stack balanced + // `frame_result_if_exceeding_limit` nor `before_frame_init` would run. The two + // guards that stand in front of interceptor dispatch are the same ones the plain + // path runs, in the same order, so that a TX-level additional-limit exceed is + // reported instead of being shadowed by a depth rejection. + let (mut output, exit) = Self::refuse_frame_before_dispatch( + &additional_limit, + &frame_init, + is_rex4_enabled, + is_rex5_enabled, + ) + .unwrap_or((output, FrameExit::RefusedSynthetically)); + + // MINI_REX+: push empty frame to keep the limit tracker stack balanced // (`before_frame_return_result` will pop). if is_mini_rex_enabled { - ctx.additional_limit.borrow_mut().push_empty_frame(); + additional_limit.borrow_mut().push_empty_frame(); } + + let gas_before_callback = output.gas().remaining(); frame_end(ctx, inspector, &frame_init.frame_input, &mut output); + let inspector_gas_delta = + i128::from(output.gas().remaining()) - i128::from(gas_before_callback); + + if is_mini_rex_enabled { + additional_limit.borrow_mut().finalize_frame( + &mut output, + exit, + inspector_gas_delta, + ); + } return Ok(ItemOrResult::Result(output)); } - // Normal path - delegate to frame_init (which pushes a real frame) + // Normal path - delegate to the shared frame-init body (which pushes a real frame). let frame_input = frame_init.frame_input.clone(); - if let ItemOrResult::Result(mut output) = self.frame_init(frame_init)? { + let (init_result, exit) = self.init_frame_unsettled(frame_init)?; + if let ItemOrResult::Result(mut output) = init_result { let (ctx, inspector) = self.ctx_inspector(); + + let gas_before_callback = output.gas().remaining(); frame_end(ctx, inspector, &frame_input, &mut output); + let inspector_gas_delta = + i128::from(output.gas().remaining()) - i128::from(gas_before_callback); + + if is_mini_rex_enabled { + additional_limit.borrow_mut().finalize_frame( + &mut output, + exit, + inspector_gas_delta, + ); + } return Ok(ItemOrResult::Result(output)); } @@ -1788,9 +1875,17 @@ where Ok(ItemOrResult::Item(frame)) } - /// This method copies the logic from `MegaEvm::frame_run` with inspector support. - /// It adds the same additional limit checks while using `inspect_instructions` instead of - /// `run_plain`. + /// The inspected twin of [`frame_run`](revm::handler::EvmTr::frame_run). + /// + /// It differs from the plain loop in exactly two places: the instruction loop is the inspected + /// one, and the callback slot the shared settlement leaves open is filled with the inspector's + /// `frame_end`. Everything between the frame's final action and the journal decision is the + /// same function for both loops. + /// + /// `frame_end` runs *before* the journal decision, which is where it differs from revm's own + /// inspected loop. It is the last callback that can rewrite a frame's classification, and both + /// `MegaETH`'s settlement and the state the frame leaves behind follow the classification it + /// hands back. #[inline] fn inspect_frame_run( &mut self, @@ -1803,7 +1898,7 @@ where inspect_instructions( ctx, &mut frame.interpreter, - inspector, + &mut *inspector, instructions.instruction_table(), instructions.gas_table(), ) @@ -1812,34 +1907,9 @@ where // Apply additional limits and storage gas cost Self::after_frame_run_instructions(ctx, frame, &mut action)?; - // Record gas remaining before frame action processing - let gas_remaining_before = match (&action, ctx.spec.is_enabled(MegaSpecId::MINI_REX)) { - (InterpreterAction::Return(interpreter_result), true) => { - Some(interpreter_result.gas.remaining()) - } - _ => None, - }; - - // Process the frame action, it may need to create a new frame or return the current frame - // result. - let mut frame_output = frame - .process_next_action::<_, ContextDbError>(ctx, action) - .inspect(|i| { - if i.is_result() { - frame.set_finished(true); - } - })?; - - // After frame_run Hook - Self::after_frame_run(ctx, &mut frame_output, gas_remaining_before)?; - - // Call frame_end for inspector callback - if let ItemOrResult::Result(frame_result) = &mut frame_output { - let (ctx, inspector, frame) = self.ctx_inspector_frame(); - frame_end(ctx, inspector, &frame.input, frame_result); - } - - Ok(frame_output) + Ok(Self::settle_and_commit_frame(ctx, frame, action, |ctx, frame_input, frame_result| { + frame_end(ctx, inspector, frame_input, frame_result); + })) } } diff --git a/crates/mega-evm/src/evm/frame.rs b/crates/mega-evm/src/evm/frame.rs new file mode 100644 index 00000000..4309d50b --- /dev/null +++ b/crates/mega-evm/src/evm/frame.rs @@ -0,0 +1,309 @@ +//! Turning a frame's final action into a frame result, with the journal decision withheld. +//! +//! # Why `MegaETH` owns this +//! +//! revm assembles a frame's result, decides whether the frame's journal checkpoint commits or +//! reverts, and — for a contract creation — runs the deposit predicates and writes the code, all +//! inside one function, and it runs the inspector's last mutating callback *after* that function +//! returns. So the classification an inspector is handed is already carved into state. +//! +//! `MegaETH` needs the opposite order. Its resource accounting settles a frame once, on the +//! frame's final classification, and an inspector is allowed to rewrite that classification; a +//! settlement taken before the rewrite would book a result that never reaches the caller, and a +//! journal committed before the rewrite would leave state behind that the reported result denies. +//! +//! This module therefore splits the upstream function in two. [`classify_frame_action`] does +//! everything that decides *what the frame's result is* — the create-return predicates, the +//! code-deposit charge, assembling the outcome — and records what the journal will have to be told +//! as a [`FrameJournalVerdict`]. [`commit_frame_journal`] carries that verdict out, once the +//! result is final. Between the two sit the inspector's last callback and `MegaETH`'s single +//! frame settlement point. +//! +//! # Upstream lockstep +//! +//! REVIEW ON UPSTREAM BUMP: [`classify_frame_action`] and [`commit_frame_journal`] together must +//! stay a faithful re-ordering of `revm_handler::EthFrame::process_next_action` and +//! `revm_handler::frame::return_create`. A revm bump that changes what those do — a new predicate, +//! a different charge, a changed journal decision — has to be mirrored here, because nothing in +//! the type system ties the two together. The debug assertion in [`classify_create_return`] +//! catches one specific class of drift (the deposit predicate `MegaETH` weighs against) and +//! nothing else. + +#[cfg(not(feature = "std"))] +use alloc as std; + +use alloy_primitives::{Address, Bytes}; +use revm::{ + context::{Cfg, ContextTr, JournalTr}, + context_interface::journaled_state::JournalCheckpoint, + handler::{EthFrame, FrameData, FrameResult, ItemOrResult}, + interpreter::{ + interpreter::EthInterpreter, interpreter_action::FrameInit, CallOutcome, CreateOutcome, + FrameInput, InstructionResult, InterpreterAction, InterpreterResult, + }, + primitives::hardfork::SpecId, + state::Bytecode, +}; + +/// What the journal has to be told about a frame, once that frame's result is final. +/// +/// The variants carry the decision the *classification* reached, not the decision that will be +/// carried out: a creation whose predicates all passed still reverts if the final result is no +/// longer successful, and the code it would have deposited is dropped. +#[derive(Clone, Debug)] +pub(crate) enum FrameJournalVerdict { + /// A call frame: commit if the final result is successful, revert otherwise. + Call, + /// A contract creation the deposit predicates turned away — an oversized runtime code, an + /// `0xEF` prefix, or a code-deposit charge the frame could not afford. Reverts + /// unconditionally: the classification already failed the frame, and nothing a later + /// rewrite says brings the rejected code back. + CreateRejected, + /// A contract creation that passed every deposit predicate and whose code is ready to be + /// written, if the final result is still successful. + /// + /// Holding the code here rather than re-reading the result's output at commit time is what + /// makes the deposit structurally unable to follow a rewrite: the bytes written are the bytes + /// the predicates approved, and they are written only on the branch this verdict allows. + CreateAccepted { address: Address, code: Bytes }, +} + +/// A frame's result, with the journal not yet told what to do with it. +#[derive(Debug)] +pub(crate) struct PendingFrame { + /// The frame's result as classified. + result: FrameResult, + /// The journal decision the classification reached but did not carry out. + journal: PendingJournal, +} + +impl PendingFrame { + /// Hands out the result and the journal decision still owed on it, so that the caller can put + /// its own work between the two — or not. + pub(crate) fn split(self) -> (FrameResult, PendingJournal) { + (self.result, self.journal) + } +} + +/// A journal decision a frame's classification reached, waiting to be carried out. +#[derive(Debug)] +pub(crate) struct PendingJournal { + verdict: FrameJournalVerdict, + /// The frame's own journal checkpoint, to revert to. + checkpoint: JournalCheckpoint, +} + +/// The classification half of revm's `process_next_action`: everything that decides a frame's +/// result, and nothing that writes state. +/// +/// Returns the child frame to build when the action is a new frame — that path settles nothing, +/// because the frame is suspended rather than finished. +pub(crate) fn classify_frame_action( + ctx: &CTX, + frame: &mut EthFrame, + action: InterpreterAction, +) -> ItemOrResult { + let mut interpreter_result = match action { + InterpreterAction::NewFrame(frame_input) => { + return ItemOrResult::Item(FrameInit { + frame_input, + depth: frame.depth + 1, + memory: frame.interpreter.memory.new_child_context(), + }) + } + InterpreterAction::Return(result) => result, + }; + + let (result, verdict) = match &frame.data { + FrameData::Call(call_frame) => { + // Propagate the EIP-8037 new-account state-gas flag from the frame input so the parent + // can refund the upfront charge if the call ends in revert or halt. + let charged_new_account_state_gas = match &frame.input { + FrameInput::Call(inputs) => inputs.charged_new_account_state_gas, + _ => false, + }; + let mut outcome = + CallOutcome::new(interpreter_result, call_frame.return_memory_range.clone()); + outcome.charged_new_account_state_gas = charged_new_account_state_gas; + (FrameResult::Call(outcome), FrameJournalVerdict::Call) + } + FrameData::Create(create_frame) => { + let address = create_frame.created_address; + let verdict = classify_create_return(ctx, &mut interpreter_result, address); + (FrameResult::Create(CreateOutcome::new(interpreter_result, Some(address))), verdict) + } + }; + + ItemOrResult::Result(PendingFrame { + result, + journal: PendingJournal { verdict, checkpoint: frame.checkpoint }, + }) +} + +/// The state-writing half: the journal decision the classification recorded, carried out against +/// the frame's *final* result. +/// +/// Two rewrites are possible between the two halves, and this is where each of them lands: +/// +/// - a successful frame rewritten into a failure reverts, and a creation deposits no code — the +/// caller is told the frame failed, and the state agrees; +/// - a failed frame rewritten into a success cannot commit a creation, because the verdict a +/// rejected creation carries has no code and no commit branch. (The measurement shim refuses that +/// rewrite outright and restores the original classification, so this is the second of two +/// independent stops rather than the only one.) +pub(crate) fn commit_frame_journal( + ctx: &mut CTX, + pending: PendingJournal, + result: &FrameResult, +) { + let PendingJournal { verdict, checkpoint } = pending; + let is_ok = result.instruction_result().is_ok(); + let journal = ctx.journal_mut(); + match verdict { + FrameJournalVerdict::Call => { + if is_ok { + journal.checkpoint_commit(); + } else { + journal.checkpoint_revert(checkpoint); + } + } + FrameJournalVerdict::CreateRejected => journal.checkpoint_revert(checkpoint), + FrameJournalVerdict::CreateAccepted { address, code } => { + if is_ok { + journal.checkpoint_commit(); + journal.set_code(address, Bytecode::new_legacy(code)); + } else { + journal.checkpoint_revert(checkpoint); + } + } + } +} + +/// The classification half of revm's `return_create`: the deposit predicates and the code-deposit +/// charge, with every journal write deferred to the verdict. +/// +/// The predicates run in upstream's order, because they do not commute: the code-size limit is +/// checked before the deposit is charged so that oversized code is not billed for storage it never +/// gets, and the `0xEF` rejection is checked before that same charge for the same reason. +fn classify_create_return( + ctx: &CTX, + interpreter_result: &mut InterpreterResult, + address: Address, +) -> FrameJournalVerdict { + let cfg = ctx.cfg(); + let max_code_size = cfg.max_code_size(); + let is_eip3541_disabled = cfg.is_eip3541_disabled(); + let spec_id: SpecId = cfg.spec().into(); + let is_amsterdam_eip8037 = cfg.is_amsterdam_eip8037_enabled(); + let gas_params = cfg.gas_params(); + let gas_for_code = gas_params.code_deposit_cost(interpreter_result.output.len()); + + // What `MegaETH`'s own code-deposit accounting predicted this classification would do, weighed + // a moment ago against this same result. The two are the same decision read twice, so a bump + // that changes one and not the other turns into a failing assertion rather than into compute + // gas recorded for a deposit that never happened, or a deposit charged with nothing recorded. + #[cfg(debug_assertions)] + let predicted_charge = will_return_create_charge_code_deposit( + interpreter_result, + max_code_size, + spec_id, + is_eip3541_disabled, + gas_for_code, + ); + + let verdict = 'classify: { + if !interpreter_result.result.is_ok() { + break 'classify FrameJournalVerdict::CreateRejected; + } + + // EIP-170 / EIP-7954: runtime code size limit, checked before any deposit charge. + if spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) && + interpreter_result.output.len() > max_code_size + { + interpreter_result.result = InstructionResult::CreateContractSizeLimit; + break 'classify FrameJournalVerdict::CreateRejected; + } + + // EIP-3541: reject new contract code starting with the 0xEF byte. + if !is_eip3541_disabled && + spec_id.is_enabled_in(SpecId::LONDON) && + interpreter_result.output.first() == Some(&0xEF) + { + interpreter_result.result = InstructionResult::CreateContractStartingWithEF; + break 'classify FrameJournalVerdict::CreateRejected; + } + + if !interpreter_result.gas.record_regular_cost(gas_for_code) { + // EIP-2 point 3: a creation that cannot pay for its own code deposit fails out of gas + // rather than leaving an empty contract behind. Before Homestead it left one. + if spec_id.is_enabled_in(SpecId::HOMESTEAD) { + interpreter_result.result = InstructionResult::OutOfGas; + break 'classify FrameJournalVerdict::CreateRejected; + } + interpreter_result.output = Bytes::new(); + } + + // EIP-8037 splits the deposit into a hash charge and a state-gas charge. Every `MegaEVM` + // configuration pins the EIP off, so this is mirrored for lockstep rather than for reach. + if is_amsterdam_eip8037 { + let hash_cost = gas_params.keccak256_cost(interpreter_result.output.len()); + if !interpreter_result.gas.record_regular_cost(hash_cost) { + interpreter_result.result = InstructionResult::OutOfGas; + break 'classify FrameJournalVerdict::CreateRejected; + } + let state_gas_for_code = + gas_params.code_deposit_state_gas(interpreter_result.output.len()); + if state_gas_for_code > 0 && + !interpreter_result.gas.record_state_cost(state_gas_for_code) + { + interpreter_result.result = InstructionResult::OutOfGas; + break 'classify FrameJournalVerdict::CreateRejected; + } + } + + interpreter_result.result = InstructionResult::Return; + FrameJournalVerdict::CreateAccepted { address, code: interpreter_result.output.clone() } + }; + + // EIP-8037 adds two more charges past the predicate's last one, so the two only have to + // agree while it is off — which every `MegaEVM` configuration pins it to be. + #[cfg(debug_assertions)] + debug_assert!( + is_amsterdam_eip8037 || + predicted_charge == matches!(verdict, FrameJournalVerdict::CreateAccepted { .. }), + "the code-deposit predicate and the create classification disagreed" + ); + + verdict +} + +/// Whether [`classify_create_return`] will charge `code_deposit_gas` and accept the deposit. +/// +/// The charge is conditional — the classification only takes it from a creation that clears every +/// deposit predicate — and `MegaETH` has to record the matching compute gas *before* the charge +/// happens, at the frame's exit settlement, while it can still rewrite the result to stop the +/// charge. So the decision is read twice: once here, ahead of time, and once by the classification +/// itself. [`classify_create_return`] asserts in debug builds that the two agreed. +pub(crate) fn will_return_create_charge_code_deposit( + interpreter_result: &InterpreterResult, + max_code_size: usize, + runtime_spec_id: SpecId, + is_eip3541_disabled: bool, + code_deposit_gas: u64, +) -> bool { + if !interpreter_result.result.is_ok() { + return false; + } + if !is_eip3541_disabled && + runtime_spec_id.is_enabled_in(SpecId::LONDON) && + interpreter_result.output.first() == Some(&0xEF) + { + return false; + } + if runtime_spec_id.is_enabled_in(SpecId::SPURIOUS_DRAGON) && + interpreter_result.output.len() > max_code_size + { + return false; + } + interpreter_result.gas.remaining() >= code_deposit_gas +} diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index b5e90231..3894d0cd 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -28,6 +28,7 @@ mod context; mod execution; mod factory; +mod frame; mod host; mod inspector; mod instructions; diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 5cf90377..77da565c 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -63,17 +63,22 @@ pub struct InspectorLedger { /// edit back and size the synthetic outcome from it. That gas travels through the result lane /// below, not through this one.) /// - /// Adjustments to a frame's *result* gas — `call_end` calling `spend_all`, an intercepting - /// callback handing back an outcome wider than what it was given — are deliberately not booked - /// here. Whether such an edit moves the transaction's envelope at all depends on the frame's - /// final classification (a caller reclaims a returning frame's remaining gas and ignores a - /// halting one's), and that classification is not final until after the last mutating - /// callback. Booking the edit before then would be a guess. Until the frame lifecycle grows a - /// settlement point past that callback, a result-rewriting inspector can still move REX7 - /// accounting off the uninspected path, and a large enough move trips the conservation - /// `debug_assert`. + /// Adjustments to a frame's *result* gas belong to [`result`](Self::result), which is booked + /// from the frame's own settlement point rather than from a callback boundary. pub env: i128, + /// Net gas the inspector wrote into a frame *result* — what the frame hands back to its + /// caller — across the last callback that can rewrite that result. + /// + /// Unlike the other two lanes this one cannot be booked at the callback boundary, because + /// whether the edit moves anything depends on how the frame ends: a returning or reverting + /// frame's remaining gas is reclaimed by its caller, so an edit to it changes what the + /// transaction spends, while a halting frame's is not handed back at all and an edit to it + /// changes nothing. The frame's settlement point knows the final classification and books + /// this lane only in the first case; in the second it reconstructs the EVM's own number and + /// settles the destroyed remainder against that instead. + pub result: i128, + /// How many rewrites the shim refused because their shape is forbidden. /// /// Today exactly one shape is: a `create_end` (or the `frame_end` after it) turning a @@ -88,7 +93,7 @@ impl InspectorLedger { /// derivation adds to the transaction's envelope. #[inline] pub const fn conjured_gas(&self) -> i128 { - self.gas + self.env + self.gas + self.env + self.result } /// Whether the inspector left the transaction's gas accounting exactly as the EVM produced it. @@ -96,7 +101,7 @@ impl InspectorLedger { /// True for every observation-only inspector, and for every transaction that ran without one. #[inline] pub const fn is_zero(&self) -> bool { - self.gas == 0 && self.env == 0 && self.rejected_rewrites == 0 + self.gas == 0 && self.env == 0 && self.result == 0 && self.rejected_rewrites == 0 } } @@ -109,7 +114,7 @@ mod tests { /// unmoved in that case. #[test] fn test_conjured_gas_is_the_net_of_both_lanes() { - let ledger = InspectorLedger { gas: 2_300, env: -2_300, rejected_rewrites: 0 }; + let ledger = InspectorLedger { gas: 2_300, env: -2_300, result: 0, rejected_rewrites: 0 }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); } @@ -117,7 +122,7 @@ mod tests { /// A refused rewrite moves no gas but must still show the transaction was not left alone. #[test] fn test_a_rejected_rewrite_alone_is_not_zero() { - let ledger = InspectorLedger { gas: 0, env: 0, rejected_rewrites: 1 }; + let ledger = InspectorLedger { gas: 0, env: 0, result: 0, rejected_rewrites: 1 }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero()); } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 53739542..cb4fcdc4 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -23,6 +23,29 @@ use crate::{ use super::LimitCheck; +/// How a frame reached the outcome [`AdditionalLimit::finalize_frame`] is settling. +/// +/// The three shapes differ in what the frame's remaining gas means and in which of them the +/// settlement is allowed to reach at all, so the caller names the shape rather than the settlement +/// guessing it from the result. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FrameExit { + /// The frame ran and produced its own result. + Ran, + /// The frame was refused before it could run, by a path that went through the limit tracker's + /// frame-init accounting: a resource limit already over its budget, or one of revm's own + /// frame-init rejections. + Refused, + /// The frame was refused by a synthetic result that never reached that accounting — a system + /// contract interceptor's, or an inspector's. + /// + /// Frozen specs leave such a refusal's envelope entirely alone, which is the gap REX4's + /// pre-dispatch limit check was added to narrow and which REX7 closes: without a settlement + /// here, an envelope that is neither handed back nor booked as destroyed leaves the + /// conservation law short by exactly that amount. + RefusedSynthetically, +} + /// Additional limits for the `MegaETH` EVM beyond standard EVM limits. /// /// This struct coordinates four independent resource limits: compute gas, data size, @@ -1057,8 +1080,9 @@ impl AdditionalLimit { /// refunded to the sender. The storage-stipend tracker decides how `gas.remaining()` /// maps to the refundable balance — see /// `StorageCallStipendTracker::effective_remaining_for_rescue`. - pub(crate) fn rescue_gas(&mut self, gas: &Gas) { - self.rescued_gas += self.storage_call_stipend.effective_remaining_for_rescue(gas); + pub(crate) fn rescue_gas(&mut self, gas: &Gas, remaining: u64) { + self.rescued_gas += + self.storage_call_stipend.effective_remaining_for_rescue(gas, remaining); } /// Drains up to `amount` from the current frame's storage stipend allowance and @@ -1069,16 +1093,19 @@ impl AdditionalLimit { self.storage_call_stipend.try_consume(amount) } - /// Rescue remaining gas from a frame result if a TX-level additional limit has been - /// exceeded. + /// Rescues a frame's remaining gas for the sender if a TX-level additional limit has been + /// exceeded, and refunds it in `last_frame_result`. /// - /// This must be called before any inspector callback (`frame_end`) that might modify the - /// gas via `spend_all()`, so the correct `gas.remaining()` value is captured. - /// The rescued gas is later refunded to the transaction sender in `last_frame_result`. - pub(crate) fn try_rescue_gas(&mut self, gas: &Gas) { + /// `remaining` is the gas the EVM left in the result, which is not always the number the + /// result now carries: an inspector callback runs between the two, and a callback that spends + /// the result down — the shape `GasInspector` takes on an error — must not be able to take + /// the sender's refund with it. Every frame the transaction unwinds through rescues the part + /// of the envelope it was still holding, and those parts are disjoint, so the sum is the whole + /// of what the halted transaction never spent. + pub(crate) fn try_rescue_gas(&mut self, gas: &Gas, remaining: u64) { let limit_check = self.check_limit(); if limit_check.exceeded_limit() && !limit_check.is_frame_local() { - self.rescue_gas(gas); + self.rescue_gas(gas, remaining); } } @@ -1189,7 +1216,7 @@ impl AdditionalLimit { /// /// Returns `Some(FrameResult)` if a TX-level limit is already exceeded. pub(crate) fn frame_result_if_exceeding_limit( - &mut self, + &self, frame_input: &FrameInput, ) -> Option { if !self.limit_exceeded() { @@ -1202,7 +1229,7 @@ impl AdditionalLimit { /// /// Shared by `before_frame_init` (limit exceeded after pushing sub-tracker frames) /// and `frame_result_if_exceeding_limit` (intrinsic overflow before frame push). - fn create_exceeded_limit_result(&mut self, frame_input: &FrameInput) -> Option { + fn create_exceeded_limit_result(&self, frame_input: &FrameInput) -> Option { let (gas_limit, return_memory_offset) = match frame_input { FrameInput::Call(inputs) => { (inputs.gas_limit, Some(inputs.return_memory_offset.clone())) @@ -1211,14 +1238,14 @@ impl AdditionalLimit { FrameInput::Empty => unreachable!(), }; let output = self.has_exceeded_limit.revert_data(); - let result = create_exceeding_limit_frame_result( + // The gas this result carries is rescued in `finalize_frame`, along with every other + // refused frame's, once the last callback that can rewrite it has run. + Some(create_exceeding_limit_frame_result( self.exceeding_instruction_result(), Gas::new(gas_limit), return_memory_offset, output, - ); - self.try_rescue_gas(result.gas()); - Some(result) + )) } /// Hook called when a new execution frame is successfully initialized in `frame_init` and needs @@ -1232,15 +1259,10 @@ impl AdditionalLimit { self.data_size.after_frame_init_on_frame(frame); self.kv_update.after_frame_init_on_frame(frame); self.compute_gas.after_frame_init_on_frame(frame); - } else if let ItemOrResult::Result(result) = init_result { - // Rescue gas if a TX-level limit was exceeded. This covers the - // before_frame_init early-return path and any other Result from frame_init. - self.try_rescue_gas(result.gas()); - // Must run after the rescue: the rescue's `check_limit` is what latches a TX-level or - // frame-local exceed, and the settlement below reads that latch to decide whether the - // envelope is really destroyed or is about to be handed back. - self.settle_frame_init_reject_burn(result); } + // A `Result` needs no work here. A frame init that refuses to build a frame settles in + // `finalize_frame`, like every other frame outcome, so that whatever an inspector's + // callback does to the refusal is already in it. } /// Hook called before a frame run. If the limit is exceeded, return an interpreter result @@ -1282,32 +1304,107 @@ impl AdditionalLimit { None } - /// Hook called after frame action processing in `frame_run`. + /// Records the compute gas a frame's own classification spent — the code deposit of a + /// contract creation, on the specs that read the charge back off the result instead of + /// weighing it beforehand. /// - /// Records compute gas cost induced in frame action processing (e.g., code deposit cost), - /// marks the frame result as exceeding limit if needed, settles an exceptionally halted - /// frame's destroyed remainder (REX7+), and rescues gas if a TX-level limit was exceeded - /// (before any inspector callback that might modify gas). - pub(crate) fn after_frame_run( + /// Frozen: REX5 onwards weigh the same charge at the frame's exit and pass `None` here, so + /// the live callers are the specs through REX4. Their reading is a difference between the + /// result's gas before and after classification, which is why this stays ahead of the last + /// mutating callback rather than joining [`finalize_frame`](Self::finalize_frame): a callback + /// editing that gas would otherwise land inside the difference and be recorded as work. + pub(crate) fn settle_post_action_charge( &mut self, result: &mut FrameResult, - gas_remaining_before_process_action: Option, + gas_remaining_before_classification: Option, ) { - if let Some(gas_remaining_before) = gas_remaining_before_process_action { - let compute_gas_cost = gas_remaining_before.saturating_sub(result.gas().remaining()); - if !self.record_compute_gas(compute_gas_cost) { - mark_frame_result_as_exceeding_limit( - result, - self.exceeding_instruction_result(), - Default::default(), - ); + let Some(gas_remaining_before) = gas_remaining_before_classification else { + return; + }; + let compute_gas_cost = gas_remaining_before.saturating_sub(result.gas().remaining()); + if !self.record_compute_gas(compute_gas_cost) { + mark_frame_result_as_exceeding_limit( + result, + self.exceeding_instruction_result(), + Default::default(), + ); + } + } + + /// Settles a frame's outcome, once and for all. + /// + /// # Where this sits + /// + /// After the last callback that can rewrite the frame's classification, and before the journal + /// is told what to do with the frame. Everything here reads the classification the caller will + /// actually see, and everything the journal does follows from what this leaves behind. That + /// ordering is the whole point: a settlement taken earlier books a result that may still + /// change, and a journal decision taken earlier leaves state behind that the reported result + /// denies. + /// + /// # What it does not cover + /// + /// The gas clamp's restore and its out-of-gas latch stay ahead of this point, in + /// [`settle_frame_final_result`](Self::settle_frame_final_result). The latch's input is the + /// interpreter's own exit classification, which the create-return classification overwrites — + /// a creation that cannot afford its code deposit ends `OutOfGas` for a reason that has + /// nothing to do with the clamp — and the code-deposit settlement that runs between the two + /// reads the latch. Both halves therefore stay where their inputs are still intact. + /// + /// The frame's tracker entries are popped later still, when the frame is handed back to its + /// caller. Popping here would double-pop the paths that reach the caller without running a + /// frame at all. + pub(crate) fn finalize_frame( + &mut self, + result: &mut FrameResult, + exit: FrameExit, + inspector_gas_delta: i128, + ) { + let evm_remaining = self.settle_inspector_result_gas(result, inspector_gas_delta); + + match exit { + FrameExit::Ran => { + // The burn before the rescue: the rescue's `check_limit` is what latches a + // TX-level exceed, and a latched exceed is exactly the case whose remainder is + // handed back rather than destroyed. + self.settle_exceptional_halt_burn(result, evm_remaining); + self.try_rescue_gas(result.gas(), evm_remaining); + } + FrameExit::Refused | FrameExit::RefusedSynthetically => { + // The rescue before the burn, for the mirror-image reason: here the latch the + // rescue produces is what tells the burn the envelope is being handed back. + if exit == FrameExit::Refused || self.rex7_enabled() { + self.try_rescue_gas(result.gas(), evm_remaining); + } + self.settle_frame_init_reject_burn(result, evm_remaining); } } - self.settle_exceptional_halt_burn(result); - // Rescue gas if a TX-level additional limit has been exceeded. - // This must happen before any inspector callback (`frame_end`) that might modify - // the gas via `spend_all()`, so the correct `gas.remaining()` value is captured. - self.try_rescue_gas(result.gas()); + } + + /// Books what the last mutating callback did to a frame result's gas, and reports the gas the + /// EVM itself left in that result. + /// + /// Whether such an edit moves anything depends on the frame's final classification, which is + /// why this can only run here: + /// + /// - a returning or reverting frame hands its remaining gas back to its caller, so an edit to + /// that number really does change what the transaction spends. It goes to the ledger, and the + /// conservation law reads it back out of the envelope; + /// - a halting frame hands nothing back, so the edit changes nothing the transaction spends. + /// The destroyed remainder and the rescue below are then taken on the EVM's own number, + /// reconstructed by undoing the edit — an inspector does not perform work, and gas it removed + /// from a doomed result was never the inspector's to destroy. + fn settle_inspector_result_gas(&mut self, result: &FrameResult, delta: i128) -> u64 { + let remaining = result.gas().remaining(); + if delta == 0 { + return remaining; + } + if result.instruction_result().is_ok_or_revert() { + self.inspector.result += delta; + remaining + } else { + (i128::from(remaining) - delta).clamp(0, i128::from(u64::MAX)) as u64 + } } /// Hook called when a frame finishes running in `frame_run`. If the limit is exceeded, mark @@ -1477,21 +1574,18 @@ impl AdditionalLimit { // commit-or-revert from the frame's original instruction result, before the // `FrameResult` ever reaches this hook, so a frame that ran to a successful exit is // already committed and stays committed under the rewritten Revert. + // + // Under REX7 an exceed the frame latched while it ran was already absorbed at the frame's + // settlement point, ahead of the journal decision, so what reaches here is a first + // detection by this hook's own `check_limit` — the frame's usage weighed against its + // caller's budget, after the merge. That question is only answerable here, so the absorb + // for it stays here on every spec. let limit_check = self.check_limit(); if limit_check.exceeded_limit() && !duplicate_return_frame_result { if limit_check.is_frame_local() { let output = limit_check.revert_data(); self.has_exceeded_limit = LimitCheck::WithinLimit; - match result { - FrameResult::Call(o) => { - o.result.result = InstructionResult::Revert; - o.result.output = output; - } - FrameResult::Create(o) => { - o.result.result = InstructionResult::Revert; - o.result.output = output; - } - } + mark_frame_result_as_exceeding_limit(result, InstructionResult::Revert, output); } else { // Gas should already have been rescued at the point where the limit was // exceeded (frame_result_if_exceeding_limit, before_frame_init, @@ -1561,14 +1655,14 @@ impl AdditionalLimit { /// rescued (TX-level) — including a clamp-induced out-of-gas, which /// [`settle_frame_final_result`](Self::settle_frame_final_result) latches earlier in this /// frame exit. - fn settle_exceptional_halt_burn(&mut self, result: &FrameResult) { + fn settle_exceptional_halt_burn(&mut self, result: &FrameResult, evm_remaining: u64) { if !self.checkpoint.rex7_enabled() || self.limit_exceeded() || result.instruction_result().is_ok_or_revert() { return; } - self.compute_gas.record_burned_gas(result.gas().remaining()); + self.compute_gas.record_burned_gas(evm_remaining); } /// Settles the envelope a frame that never started destroys, as non-enforcing compute gas @@ -1598,7 +1692,7 @@ impl AdditionalLimit { /// [`before_frame_return_result`](Self::before_frame_return_result), which rewrites the result /// to a revert and so returns the gas to the caller. Either way the envelope is not destroyed, /// and booking it would report gas that was handed back. - fn settle_frame_init_reject_burn(&mut self, result: &FrameResult) { + fn settle_frame_init_reject_burn(&mut self, result: &FrameResult, evm_remaining: u64) { if !self.checkpoint.rex7_enabled() || self.limit_exceeded() || result.instruction_result().is_ok_or_revert() @@ -1610,7 +1704,7 @@ impl AdditionalLimit { return; } } - self.compute_gas.record_burned_gas(result.gas().remaining()); + self.compute_gas.record_burned_gas(evm_remaining); } /// Merges resource usage from a sandbox execution into this tracker. diff --git a/crates/mega-evm/src/limit/storage_call_stipend.rs b/crates/mega-evm/src/limit/storage_call_stipend.rs index 36ced6ae..84d9b1c4 100644 --- a/crates/mega-evm/src/limit/storage_call_stipend.rs +++ b/crates/mega-evm/src/limit/storage_call_stipend.rs @@ -177,20 +177,24 @@ impl StorageCallStipendTracker { self.stack.last().map(|frame| frame.remaining).unwrap_or(0) } - /// Portion of `gas.remaining()` to add to `rescued_gas` on a TX-level limit exceed. - /// REX5 returns `gas.remaining()` directly (allowance never entered `gas.limit()`). + /// Portion of `remaining` to add to `rescued_gas` on a TX-level limit exceed. + /// REX5 returns `remaining` directly (allowance never entered `gas.limit()`). /// REX4 excludes the current frame's stipend so system-granted gas is not refunded /// to the sender. - pub(crate) fn effective_remaining_for_rescue(&self, gas: &Gas) -> u64 { + /// + /// `remaining` is passed rather than read off `gas` because the caller settles a frame after + /// an inspector callback has had a chance to edit the result, and the sender's refund is owed + /// on what the EVM left behind. `gas` is still read for its limit, which no callback moves. + pub(crate) fn effective_remaining_for_rescue(&self, gas: &Gas, remaining: u64) -> u64 { if self.rex5_enabled { - return gas.remaining(); + return remaining; } let stipend = self.current_frame_stipend(); if stipend > 0 { let original_limit = gas.limit().saturating_sub(stipend); - gas.remaining().min(original_limit) + remaining.min(original_limit) } else { - gas.remaining() + remaining } } diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index f439dff6..cb70606d 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -205,7 +205,7 @@ pub fn execute_keyless_deploy_call // the overhead already recorded as compute above. Booking the rescued remainder // as destroyed as well would report gas that was refunded. if ctx.spec.is_enabled(MegaSpecId::REX6) { - additional_limit.try_rescue_gas(&gas); + additional_limit.try_rescue_gas(&gas, gas.remaining()); } let mut result = make_halt!(); mark_frame_result_as_exceeding_limit( @@ -1109,7 +1109,7 @@ fn reject_if_tx_limit_overflow( if !limit_check.exceeded_limit() || limit_check.is_frame_local() { return None; } - limit.rescue_gas(gas); + limit.rescue_gas(gas, gas.remaining()); let mut result = oog_frame_result(gas.limit(), return_memory_offset); mark_frame_result_as_exceeding_limit( &mut result, From 2bd7293d93e7870335adae790c7c05c0ed5d9caf Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:02:37 +0800 Subject: [PATCH 096/208] fix(evm): show an inspector the logs a precompile emitted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A precompile is dispatched inside the frame init and comes back as a result rather than a frame, so nothing it emits passes through the instruction loop's `log` callback. revm's own inspected frame init forwards those logs explicitly, from the two places they can live — still on the journal for a precompile that succeeded, carried on the outcome for one whose frame was rolled back. MegaETH's override dropped that step, so an inspector's view of a transaction depended on which of the two assembled the frame init around it. No precompile MegaETH registers emits a log, so nothing is forwarded today; the tests drive the forwarding directly, because there is no way to reach it through a transaction. --- crates/mega-evm/src/evm/execution.rs | 124 ++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index b61af4a0..14e02d45 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1850,10 +1850,13 @@ where // Normal path - delegate to the shared frame-init body (which pushes a real frame). let frame_input = frame_init.frame_input.clone(); + let logs_before_init = ctx.journal().logs().len(); let (init_result, exit) = self.init_frame_unsettled(frame_init)?; if let ItemOrResult::Result(mut output) = init_result { let (ctx, inspector) = self.ctx_inspector(); + forward_precompile_logs(ctx, inspector, logs_before_init, &output); + let gas_before_callback = output.gas().remaining(); frame_end(ctx, inspector, &frame_input, &mut output); let inspector_gas_delta = @@ -1913,6 +1916,43 @@ where } } +/// Hands an inspector the logs a precompile emitted, which no other callback would show it. +/// +/// A precompile is dispatched inside the frame init and comes back as a result rather than a +/// frame, so nothing it emits passes through the instruction loop's `log` callback. Its logs reach +/// an inspector through here or not at all — which is how revm's own inspected frame init treats +/// them, and matching that is the point: an inspector's view of a transaction should not depend on +/// whether `MegaETH` or revm assembled the frame init around it. +/// +/// A rejected precompile's logs are the ones the journal has already rolled back, so the outcome +/// carries them separately; a successful one's are still on the journal, past the mark taken +/// before the frame init ran. +/// +/// No precompile `MegaETH` registers emits a log, so this forwards nothing today. It is here for +/// the same reason the log strip in `execution_result` is: one that does would otherwise change +/// what an inspector sees, silently and in the direction of showing less. +#[inline] +fn forward_precompile_logs( + ctx: &mut MegaContext, + inspector: &mut INSP, + logs_before_init: usize, + output: &FrameResult, +) where + INSP: Inspector>, +{ + let FrameResult::Call(CallOutcome { was_precompile_called, precompile_call_logs, .. }) = output + else { + return; + }; + if !*was_precompile_called { + return; + } + let journalled = ctx.journal_mut().logs()[logs_before_init..].to_vec(); + for log in journalled.into_iter().chain(precompile_call_logs.iter().cloned()) { + inspector.log(ctx, log); + } +} + /// Builds a `FrameResult` matching revm's `make_call_frame` `CallTooDeep` return: /// `Gas::new(gas_limit)` (no spend, fully refundable to caller via `erase_cost`), /// empty output, and the caller's `return_memory_offset`. @@ -1978,7 +2018,7 @@ mod mutation_tests { test_utils::MemoryDatabase, AdditionalLimit, EmptyExternalEnv, EvmTxRuntimeLimits, LimitCheck, LimitKind, }; - use alloy_primitives::Address; + use alloy_primitives::{Address, Log}; use revm::{ context::ContextTr, inspector::InspectorEvmTr, @@ -2097,6 +2137,88 @@ mod mutation_tests { consume_synthetic_limit_frame(evm.ctx_ref(), result); } + /// Counts every log the inspector is handed, and nothing else. + #[derive(Default)] + struct LogCountingInspector { + logs: Vec, + } + + impl Inspector for LogCountingInspector { + fn log(&mut self, _context: &mut CTX, log: Log) { + self.logs.push(log); + } + } + + fn precompile_outcome(journalled: bool) -> FrameResult { + let log = Log::new_unchecked(Address::ZERO, Vec::new(), Bytes::from_static(b"emitted")); + FrameResult::Call(CallOutcome { + result: InterpreterResult::new( + InstructionResult::Return, + Bytes::new(), + Gas::new(TEST_GAS_LIMIT), + ), + memory_offset: 0..0, + was_precompile_called: true, + precompile_call_logs: if journalled { Vec::new() } else { vec![log] }, + charged_new_account_state_gas: false, + }) + } + + /// A precompile's logs never reach the instruction loop, so unless the frame init forwards + /// them an inspector simply does not see them. Both of the places they can be — still on the + /// journal for a precompile that succeeded, carried on the outcome for one whose frame was + /// rolled back — have to be forwarded, and in that order. + #[test] + fn test_precompile_logs_reach_the_inspector_from_both_places_they_live() { + for journalled in [false, true] { + let mut context = MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX7); + let logs_before = context.journal_mut().logs().len(); + if journalled { + context.journal_mut().log(Log::new_unchecked( + Address::ZERO, + Vec::new(), + Bytes::from_static(b"emitted"), + )); + } + let mut inspector = LogCountingInspector::default(); + + forward_precompile_logs( + &mut context, + &mut inspector, + logs_before, + &precompile_outcome(journalled), + ); + + assert_eq!( + inspector.logs.len(), + 1, + "the precompile's log must reach the inspector (journalled: {journalled})", + ); + assert_eq!(inspector.logs[0].data.data, Bytes::from_static(b"emitted")); + } + } + + /// Nothing else in a frame init is a precompile, and forwarding a call frame's journal tail + /// would replay logs the instruction loop already showed. + #[test] + fn test_a_non_precompile_frame_init_result_forwards_nothing() { + let mut context = MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX7); + context.journal_mut().log(Log::new_unchecked( + Address::ZERO, + Vec::new(), + Bytes::from_static(b"emitted"), + )); + let mut inspector = LogCountingInspector::default(); + + let mut outcome = precompile_outcome(true); + let FrameResult::Call(call) = &mut outcome else { unreachable!() }; + call.was_precompile_called = false; + + forward_precompile_logs(&mut context, &mut inspector, 0, &outcome); + + assert!(inspector.logs.is_empty(), "a plain frame-init result forwards nothing"); + } + /// Drives [`MegaHandler::before_execution`] straight at its short-circuit: a transaction whose /// gas limit is one below the initial gas it is handed, with `recorded_intrinsic` already on /// the compute-gas tracker the way `validate` leaves it. From 9224115dd5387ac836dfbf59d6b035a6b0df6a4f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:09:03 +0800 Subject: [PATCH 097/208] feat(rex7): roll a frame's state back with the frame-local revert it reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A frame that overran a per-frame budget reports a revert to its caller, and until now its state stayed committed underneath: the journal decision was taken from the frame's original instruction result, before the rewrite reached it. For a contract creation that split leaves deployed code and emitted constructor logs behind an otherwise-failed frame. REX7 absorbs at the frame's settlement point, ahead of the journal decision, so the two agree. Only an exceed the frame itself latched is absorbed there. A first detection on the way out to the caller weighs the frame's usage against the caller's budget, after the merge — a different question, whose answer decides which transactions fail — so it stays where it is, on every spec. With REX7's gas clamp stopping a compute exceed at the crossing opcode and the code-deposit charge weighed before it is taken, no path in the test suite reaches the settlement with a latched, unsurfaced frame-local exceed. The absorb is the guarantee that if one appears, its state follows its result. --- crates/mega-evm/src/limit/limit.rs | 113 +++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index cb4fcdc4..d293fb7b 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -1360,6 +1360,10 @@ impl AdditionalLimit { exit: FrameExit, inspector_gas_delta: i128, ) { + // First, because everything below reads the classification: a frame-local exceed rewrites + // it to a revert. + self.absorb_frame_local_exceed(result); + let evm_remaining = self.settle_inspector_result_gas(result, inspector_gas_delta); match exit { @@ -1407,6 +1411,44 @@ impl AdditionalLimit { } } + /// Absorbs a frame-local resource exceed the frame itself latched, into the frame's own + /// result (REX7+). + /// + /// A frame that overran a per-frame budget reverts: the exceed is the frame's, its caller is + /// free to carry on, and the gas the frame still held goes back to that caller. Running the + /// rewrite here rather than on the way out to the caller is what makes the frame's state + /// follow it — the journal decision is still ahead, and it reads this same result — so a + /// frame that reports a revert has reverted, rather than reporting one over state that stayed + /// committed. That split is what a contract creation needs closed most: a constructor that + /// ran to a successful exit and is then rewritten leaves deployed code and emitted logs + /// behind an otherwise-failed frame. + /// + /// Only an exceed that is *already latched* is absorbed here — one the frame recorded against + /// its own budget while it ran, or one its exit settlement stamped. This deliberately does not + /// run a fresh [`check_limit`](Self::check_limit): a fresh pass at this point would weigh the + /// frame's usage against its own budget, whereas the pass that runs on the way out to the + /// caller weighs it after the frame's usage has been merged into the caller's. Those are + /// different questions with different answers, and the second one is the one the per-frame + /// budgets are defined by. So a late first detection stays where it is, and this settles the + /// one that produces the split. + /// + /// Frozen specs absorb everything later, on the way out to the caller, and leave a + /// successfully-exited frame's state committed under the revert they report. + fn absorb_frame_local_exceed(&mut self, result: &mut FrameResult) { + if !self.checkpoint.rex7_enabled() { + return; + } + let limit_check = self.has_exceeded_limit; + if limit_check.exceeded_limit() && limit_check.is_frame_local() { + self.has_exceeded_limit = LimitCheck::WithinLimit; + mark_frame_result_as_exceeding_limit( + result, + InstructionResult::Revert, + limit_check.revert_data(), + ); + } + } + /// Hook called when a frame finishes running in `frame_run`. If the limit is exceeded, mark /// in place the interpreter result as exceeding the limit. pub(crate) fn after_frame_run_instructions<'a>( @@ -2144,6 +2186,77 @@ mod tests { ); } + /// A frame-local exceed the frame latched while it ran is absorbed at the frame's settlement + /// point under REX7, which is ahead of the journal decision — so the state the frame leaves + /// behind is rolled back with the revert it reports, instead of staying committed under it. + /// + /// Frozen specs must not absorb here: they take the journal decision first, and absorbing + /// ahead of it would revert state their replay keeps. + #[test] + fn test_rex7_absorbs_a_latched_frame_local_exceed_before_the_journal_decision() { + for spec in [MegaSpecId::REX6, MegaSpecId::REX7] { + let mut limit = AdditionalLimit::new(spec, EvmTxRuntimeLimits::from_spec(spec)); + limit.set_has_exceeded_limit_for_test(LimitCheck::ExceedsLimit { + kind: LimitKind::ComputeGas, + limit: 10, + used: 11, + frame_local: true, + }); + + let mut result = stopped_call_result(50_000); + limit.finalize_frame(&mut result, FrameExit::Ran, 0); + + if spec.is_enabled(MegaSpecId::REX7) { + assert_eq!( + result.instruction_result(), + InstructionResult::Revert, + "REX7 must absorb here, so the journal decision that follows reverts too", + ); + assert!( + !limit.limit_exceeded(), + "an absorbed frame-local exceed must not stop the frames above it", + ); + } else { + assert_eq!( + result.instruction_result(), + InstructionResult::Stop, + "a frozen spec absorbs on the way out to the caller, after the journal", + ); + assert!(limit.limit_exceeded(), "and so it still has the exceed to absorb"); + } + } + } + + /// The settlement must not go looking for an exceed of its own. A fresh check here would + /// weigh a frame's usage against its own budget, while the check that decides a per-frame + /// exceed weighs it against the caller's, after the frame's usage has been merged in — so a + /// fresh check here fails frames the per-frame budgets do not. + #[test] + fn test_the_settlement_does_not_detect_a_frame_local_exceed_of_its_own() { + let mut limit = AdditionalLimit::new( + MegaSpecId::REX7, + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(10), + ); + limit.push_empty_frame(); + // An order of magnitude past the frame's budget, and unlatched: only a fresh check + // inside the settlement could find it. + let _ = limit.record_compute_gas_unguarded(100); + assert!( + limit.check_limit().is_frame_local(), + "the fixture must be a frame-local exceed a fresh check would find", + ); + limit.set_has_exceeded_limit_for_test(LimitCheck::WithinLimit); + + let mut result = stopped_call_result(50_000); + limit.finalize_frame(&mut result, FrameExit::Ran, 0); + + assert_eq!( + result.instruction_result(), + InstructionResult::Stop, + "the settlement absorbs what the frame latched, and nothing else", + ); + } + fn oog_result() -> InterpreterResult { InterpreterResult::new(InstructionResult::OutOfGas, Bytes::new(), Gas::new(100_000)) } From 0eff221d9cce6ceb75b617ebbb5bf2cd1ca41e7e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:17:57 +0800 Subject: [PATCH 098/208] test(rex7): pin the frame lifecycle end to end, on both loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three layers, because the rework spans three. The classification arms of a contract creation are driven directly: each rejecting predicate rejects, names itself, and hands the journal a verdict with no code — which is what makes the deposit structurally unable to follow a rewrite in either direction. A creation the predicates accepted and a callback then failed deposits nothing; a creation they rejected and a callback then revived has nothing to deposit. At transaction level, one case per branch of the frame lifecycle that can end a frame runs through both frame loops with an observation-only inspector, compared on everything the transaction produces — the receipt, the four dimensions, the enforced / destroyed split, and the state. The state is what covers the journal decision, which the loops now take themselves. And end to end: an inspector that fails a successful deployment leaves no code and none of the constructor's storage writes behind. --- crates/mega-evm/src/evm/frame.rs | 175 +++++++ .../mega-evm/tests/rex7/frame_loop_parity.rs | 446 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + .../mega-evm/tests/rex7/measured_inspector.rs | 91 ++++ 4 files changed, 716 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/frame_loop_parity.rs diff --git a/crates/mega-evm/src/evm/frame.rs b/crates/mega-evm/src/evm/frame.rs index 4309d50b..f998abb4 100644 --- a/crates/mega-evm/src/evm/frame.rs +++ b/crates/mega-evm/src/evm/frame.rs @@ -307,3 +307,178 @@ pub(crate) fn will_return_create_charge_code_deposit( } interpreter_result.gas.remaining() >= code_deposit_gas } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::MemoryDatabase, EmptyExternalEnv, MegaContext, MegaSpecId}; + use alloy_primitives::{address, Address as Addr, U256}; + use revm::{ + context::JournalTr, + interpreter::{Gas, InstructionResult, InterpreterResult}, + }; + use std::{vec, vec::Vec}; + + const DEPLOYED: Addr = address!("00000000000000000000000000000000000c0de0"); + /// Ample: the deposit charge for the runtime codes below is a few hundred gas. + const FRAME_GAS: u64 = 1_000_000; + + fn context() -> MegaContext { + MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX7) + } + + fn returned(output: Vec, gas: u64) -> InterpreterResult { + InterpreterResult::new(InstructionResult::Return, Bytes::from(output), Gas::new(gas)) + } + + /// Runs the classification and reports what it decided: the rewritten instruction result, and + /// whether the verdict allows a deposit. + fn classify(result: &mut InterpreterResult) -> FrameJournalVerdict { + classify_create_return(&context(), result, DEPLOYED) + } + + fn accepts(verdict: &FrameJournalVerdict) -> bool { + matches!(verdict, FrameJournalVerdict::CreateAccepted { .. }) + } + + /// A creation that clears every deposit predicate is accepted, is charged for its code, and + /// carries the exact bytes the predicates approved. + #[test] + fn test_a_clean_creation_is_accepted_and_charged_for_its_code() { + let mut result = returned(vec![0x00; 32], FRAME_GAS); + let verdict = classify(&mut result); + + let FrameJournalVerdict::CreateAccepted { address, code } = verdict else { + panic!("a clean creation must be accepted, got {verdict:?}") + }; + assert_eq!(address, DEPLOYED); + assert_eq!(code, Bytes::from(vec![0x00; 32]), "the approved bytes travel with the verdict"); + assert_eq!(result.result, InstructionResult::Return); + assert_eq!( + FRAME_GAS - result.gas.remaining(), + 32 * revm::interpreter::gas::CODEDEPOSIT, + "the deposit is charged during classification, not at the journal decision", + ); + } + + /// Each rejecting predicate rejects, names itself on the result, and — this is the part that + /// matters for the deposit — leaves no code on the verdict for anything to write. + #[test] + fn test_every_rejecting_predicate_rejects_without_code() { + // (name, runtime code, gas the frame has left, the classification it must produce) + let cases: Vec<(&str, Vec, u64, InstructionResult)> = vec![ + ( + "0xEF prefix", + vec![0xEF, 0x00], + FRAME_GAS, + InstructionResult::CreateContractStartingWithEF, + ), + ( + "cannot pay the deposit", + vec![0x00; 32], + 32 * revm::interpreter::gas::CODEDEPOSIT - 1, + InstructionResult::OutOfGas, + ), + ]; + + for (name, code, gas, expected) in cases { + let mut result = returned(code, gas); + let verdict = classify(&mut result); + + assert!(!accepts(&verdict), "{name}: must not be accepted, got {verdict:?}"); + assert_eq!(result.result, expected, "{name}: classification"); + } + } + + /// A creation whose frame never succeeded is rejected untouched: the classification does not + /// charge it, does not rename its failure, and hands the journal a verdict with no code. + #[test] + fn test_a_failed_frame_is_rejected_without_being_charged() { + let mut result = InterpreterResult::new( + InstructionResult::Revert, + Bytes::from_static(b"reason"), + Gas::new(FRAME_GAS), + ); + let verdict = classify(&mut result); + + assert!(!accepts(&verdict)); + assert_eq!(result.result, InstructionResult::Revert, "the failure keeps its own name"); + assert_eq!(result.gas.remaining(), FRAME_GAS, "and pays nothing for a deposit"); + } + + /// The journal decision follows the *final* result. A creation the predicates accepted, whose + /// result is then rewritten into a failure, must not leave its code behind. + #[test] + fn test_a_creation_rewritten_into_a_failure_deposits_nothing() { + let mut ctx = context(); + let checkpoint = ctx.journal_mut().checkpoint(); + let mut result = returned(vec![0x60; 32], FRAME_GAS); + let verdict = classify_create_return(&ctx, &mut result, DEPLOYED); + assert!(accepts(&verdict), "the fixture must be a creation the predicates accepted"); + + // What a `create_end` rewrite does, after the classification and before the journal. + result.result = InstructionResult::Revert; + let frame_result = FrameResult::Create(CreateOutcome::new(result, Some(DEPLOYED))); + commit_frame_journal(&mut ctx, PendingJournal { verdict, checkpoint }, &frame_result); + + let account = ctx.journal_mut().load_account(DEPLOYED).unwrap(); + assert!(account.info.is_empty_code_hash(), "no code may be deposited for a failed frame"); + } + + /// And the other direction: a creation the predicates *rejected*, whose result is then + /// rewritten into a success, has no code and no commit branch to reach. The rewrite cannot + /// deposit code that never passed the predicates, whatever the result says. + #[test] + fn test_a_rejected_creation_rewritten_into_a_success_still_deposits_nothing() { + let mut ctx = context(); + let checkpoint = ctx.journal_mut().checkpoint(); + // Runtime code the frame cannot pay to deposit. + let mut result = returned(vec![0x60; 32], 32 * revm::interpreter::gas::CODEDEPOSIT - 1); + let verdict = classify_create_return(&ctx, &mut result, DEPLOYED); + assert!(!accepts(&verdict), "the fixture must be a creation the predicates rejected"); + + result.result = InstructionResult::Return; + let frame_result = FrameResult::Create(CreateOutcome::new(result, Some(DEPLOYED))); + commit_frame_journal(&mut ctx, PendingJournal { verdict, checkpoint }, &frame_result); + + let account = ctx.journal_mut().load_account(DEPLOYED).unwrap(); + assert!( + account.info.is_empty_code_hash(), + "a rejected creation carries no code, so a rewrite has nothing to deposit", + ); + } + + /// A call frame's journal decision reads the final result and nothing else. + #[test] + fn test_a_call_frame_commits_or_reverts_on_its_final_result() { + for (label, instruction_result, expect_committed) in [ + ("success", InstructionResult::Stop, true), + ("revert", InstructionResult::Revert, false), + ("halt", InstructionResult::OutOfGas, false), + ] { + let mut ctx = context(); + ctx.journal_mut().load_account(DEPLOYED).expect("the account must load"); + let checkpoint = ctx.journal_mut().checkpoint(); + ctx.journal_mut() + .sstore(DEPLOYED, U256::from(1), U256::from(7)) + .expect("sstore must reach the in-memory database"); + + let frame_result = FrameResult::Call(CallOutcome::new( + InterpreterResult::new(instruction_result, Bytes::new(), Gas::new(FRAME_GAS)), + 0..0, + )); + commit_frame_journal( + &mut ctx, + PendingJournal { verdict: FrameJournalVerdict::Call, checkpoint }, + &frame_result, + ); + + let stored = ctx.journal_mut().sload(DEPLOYED, U256::from(1)).unwrap().data; + assert_eq!( + stored == U256::from(7), + expect_committed, + "{label}: the frame's write must follow its final result", + ); + } + } +} diff --git a/crates/mega-evm/tests/rex7/frame_loop_parity.rs b/crates/mega-evm/tests/rex7/frame_loop_parity.rs new file mode 100644 index 00000000..c847a435 --- /dev/null +++ b/crates/mega-evm/tests/rex7/frame_loop_parity.rs @@ -0,0 +1,446 @@ +//! The two frame loops must agree, on every shape a frame can end in. +//! +//! `frame_run` and `inspect_frame_run` are separate functions, and so are `frame_init` and +//! `inspect_frame_init`. What they share is a body: the frame's settlement point, the reading the +//! frozen post-action charge is measured against, the guards in front of interceptor dispatch, and +//! the journal decision. The inspected copies add exactly one thing to it — the callback that can +//! rewrite a frame's classification — and an observation-only inspector rewrites nothing. +//! +//! So with such an inspector attached, every quantity a transaction produces has to be identical +//! to the uninspected run: the receipt, the four resource dimensions, the enforced / destroyed +//! split, and the state. A difference means one loop reached a settlement the other did not. +//! +//! The cases below are not representative samples. They are one per branch of the frame lifecycle +//! that can end a frame: the classification arms of a contract creation (accepted, oversized, +//! `0xEF`-prefixed, unaffordable deposit, reverted constructor, occupied address), a call frame's +//! three outcomes, the frame inits that refuse to build a frame at all, a precompile, and the two +//! suspension shapes that must settle nothing. + +use crate::common::{CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}; +use alloy_primitives::{address, Address, Bytes, B256, U256}; +use mega_evm::{ + constants::mini_rex::MAX_CONTRACT_SIZE, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, INVALID, MSTORE8, PUSH0, RETURN, REVERT, STATICCALL, STOP}, + context::{tx::TxEnvBuilder, CfgEnv}, + inspector::NoOpInspector, + primitives::TxKind, + state::EvmState, +}; +use std::{collections::BTreeMap, string::String}; + +/// High enough that EVM gas is never what binds, except where a case says otherwise. +const TX_GAS_LIMIT: u64 = 30_000_000; +/// `ecrecover`, the cheapest precompile to reach with junk input. +const ECRECOVER: Address = address!("0000000000000000000000000000000000000001"); + +/// Everything one transaction produced, in a form two runs can be compared field by field. +#[derive(Debug, PartialEq, Eq)] +struct Reading { + result: String, + compute_gas: u64, + enforced: u64, + destroyed: u64, + data_size: u64, + kv_updates: u64, + state_growth: u64, + gas_used: u64, + total_gas_spent: u64, + state: String, +} + +/// Renders the produced state in a canonical, order-independent form. +/// +/// Comparing the state is what makes these cases cover the journal decision rather than only the +/// accounting: a frame committed on one loop and reverted on the other shows up here and nowhere +/// else. +fn render_state(state: &EvmState) -> String { + let canonical: BTreeMap)> = state + .iter() + .map(|(address, account)| { + let storage = account + .storage + .iter() + .map(|(slot, value)| (*slot, value.present_value())) + .collect(); + (*address, (account.info.balance, account.info.nonce, account.info.code_hash, storage)) + }) + .collect(); + std::format!("{canonical:?}") +} + +/// Runs `case` once under `spec`, with the inspector either driving the inspected loops or +/// switched off. +/// +/// Both arms build the same `MegaEvm` type and toggle the flag, so the only thing that changes is +/// which pair of loops runs — not the inspector, not the context, not the transaction. +fn run_under(case: &Case, spec: MegaSpecId, inspected: bool) -> Reading { + let mut db = (case.db)(); + let mut cfg = CfgEnv::default(); + cfg.spec = spec; + cfg.limit_contract_code_size = Some(case.code_size_limit.unwrap_or(MAX_CONTRACT_SIZE)); + let mut context = MegaContext::new(&mut db, spec) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(CALLER) + .kind(case.kind) + .data(case.data.clone()) + .value(case.value) + .gas_limit(case.gas_limit) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + + let mut evm: MegaEvm<_, NoOpInspector, EmptyExternalEnv> = + MegaEvm::new(context).with_inspector(NoOpInspector); + if !inspected { + alloy_evm::Evm::set_inspector_enabled(&mut evm, false); + } + let outcome: MegaTransactionOutcome = + evm.execute_transaction(tx).expect("tx should not surface EVMError"); + + Reading { + result: std::format!("{:?}", outcome.result_and_state.result), + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, + gas_used: outcome.result_and_state.result.tx_gas_used(), + total_gas_spent: outcome.result_and_state.result.gas().total_gas_spent(), + state: render_state(&outcome.result_and_state.state), + } +} + +/// One frame-lifecycle shape, and how to reach it. +struct Case { + /// What the case pins, used as the assertion label. + name: &'static str, + db: fn() -> MemoryDatabase, + kind: TxKind, + data: Bytes, + value: U256, + gas_limit: u64, + /// A lowered contract-size limit, for the case that needs revm's size reject. `MegaETH`'s own + /// 512 KiB limit is far past what a constructor can afford to return under the per-byte + /// storage gas. + code_size_limit: Option, + /// Asserted against the plain run, so a case that stops reaching its shape fails loudly + /// instead of comparing two runs of something else. + expect: fn(&Reading), +} + +fn base_db() -> MemoryDatabase { + MemoryDatabase::default().account_balance(CALLER, U256::from(ONE_ETH)) +} + +fn caller_db(code: Bytes) -> MemoryDatabase { + base_db().account_code(CONTRACT, code) +} + +/// Init code returning `len` bytes of runtime code, the first of them `first_byte`. +fn init_code_returning(len: u64, first_byte: u8) -> Bytes { + BytecodeBuilder::default() + .push_number(u128::from(first_byte)) + .push_number(0u64) + .append(MSTORE8) + .push_number(u128::from(len)) + .push_number(0u64) + .append(RETURN) + .build() +} + +/// A contract whose body issues one `CALL` to `target`, forwarding `gas`, then stops. +fn calls(target: Address, gas: u64, value: u128) -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(value) + .push_address(target) + .push_number(u128::from(gas)) + .append(CALL) + .append(STOP) + .build() +} + +fn assert_success(r: &Reading) { + assert!(r.result.starts_with("Success"), "expected a success, got {}", r.result); +} + +fn assert_revert(r: &Reading) { + assert!(r.result.starts_with("Revert"), "expected a revert, got {}", r.result); +} + +fn assert_halt(r: &Reading) { + assert!(r.result.starts_with("Halt"), "expected a halt, got {}", r.result); +} + +/// A halt with a named reason, so a case that stops reaching its classification arm fails rather +/// than comparing two runs of a different failure. +fn assert_halt_reason(r: &Reading, reason: &str) { + assert_halt(r); + assert!(r.result.contains(reason), "expected a {reason} halt, got {}", r.result); +} + +/// The frame's remainder was destroyed rather than handed back — the shape whose booking site sits +/// on one side of the callback and whose derivation sits on the other. +fn assert_destroyed(r: &Reading) { + assert!(r.destroyed > 0, "expected a destroyed remainder, got {r:?}"); +} + +fn cases() -> Vec { + std::vec![ + Case { + name: "CALL succeeds and commits its storage write", + db: || { + caller_db(calls(CALLEE, 200_000, 0)).account_code( + CALLEE, + BytecodeBuilder::default().sstore(U256::from(1), U256::from(7)).stop().build(), + ) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "CALL reverts and its storage write is rolled back", + db: || { + caller_db(calls(CALLEE, 200_000, 0)).account_code( + CALLEE, + BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(7)) + .revert() + .build(), + ) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "CALL halts on INVALID and destroys its forwarded budget", + db: || { + caller_db(calls(CALLEE, 200_000, 0)) + .account_code(CALLEE, Bytes::from_static(&[INVALID])) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: |r| { + assert_success(r); + assert_destroyed(r); + }, + }, + Case { + name: "CALL into empty code stops without a frame", + db: || caller_db(calls(EMPTY_TARGET, 200_000, 0)), + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "CALL is refused for want of balance", + db: || caller_db(calls(CALLEE, 200_000, 1)) + .account_code(CALLEE, init_code_returning(1, 0x00)), + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "STATICCALL reaches a precompile, which returns without a frame", + db: || { + caller_db( + BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(32u64) + .push_number(0u64) + .push_address(ECRECOVER) + .push_number(100_000u64) + .append(STATICCALL) + .append(STOP) + .build(), + ) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "CREATE deposits its code", + db: base_db, + kind: TxKind::Create, + data: init_code_returning(64, 0x00), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "CREATE is rejected for an oversized runtime code", + db: base_db, + kind: TxKind::Create, + data: init_code_returning(64, 0x00), + value: U256::ZERO, + code_size_limit: Some(32), + gas_limit: TX_GAS_LIMIT, + expect: |r| { + assert_halt_reason(r, "CreateContractSizeLimit"); + assert_destroyed(r); + }, + }, + Case { + name: "CREATE is rejected for an 0xEF-prefixed runtime code", + db: base_db, + kind: TxKind::Create, + data: init_code_returning(4, 0xEF), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: |r| { + assert_halt_reason(r, "CreateContractStartingWithEF"); + assert_destroyed(r); + }, + }, + Case { + name: "CREATE runs out of gas paying for its own code", + db: base_db, + // 1000 bytes of runtime code cost ten million gas to store; the limit below lets the + // constructor run and leaves it unable to pay for what it returned. + data: init_code_returning(1_000, 0x00), + kind: TxKind::Create, + value: U256::ZERO, + code_size_limit: None, + gas_limit: 150_000, + expect: |r| assert_halt_reason(r, "OutOfGas"), + }, + Case { + name: "CREATE's constructor reverts", + db: base_db, + kind: TxKind::Create, + data: BytecodeBuilder::default().revert().build(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_revert, + }, + Case { + name: "CREATE onto an occupied address collides", + db: || { + caller_db( + BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .append(STOP) + .build(), + ) + // The address CONTRACT's first CREATE derives, pre-occupied with code. + .account_code(CONTRACT.create(1), Bytes::from_static(&[STOP])) + .account_nonce(CONTRACT, 1) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: |r| { + assert_success(r); + assert_destroyed(r); + }, + }, + Case { + name: "a nested CALL suspends its caller without settling it", + db: || { + caller_db(calls(CALLEE, 500_000, 0)) + .account_code(CALLEE, calls(EMPTY_TARGET, 100_000, 0)) + }, + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_success, + }, + Case { + name: "the top-level frame itself halts", + db: || caller_db(Bytes::from_static(&[PUSH0, PUSH0, REVERT])), + kind: TxKind::Call(CONTRACT), + data: Bytes::new(), + value: U256::ZERO, + code_size_limit: None, + gas_limit: TX_GAS_LIMIT, + expect: assert_revert, + }, + ] +} + +/// Every frame-lifecycle shape, run through both loops, compared on everything a transaction +/// produces. +/// +/// The state comparison is what covers the journal decision: the loops now decide it themselves, +/// after the callback, and a frame committed on one and reverted on the other is invisible in the +/// receipt of a transaction whose caller absorbed the difference. +#[test] +fn test_both_frame_loops_agree_on_every_frame_outcome() { + for case in cases() { + let plain = run_under(&case, MegaSpecId::REX7, false); + (case.expect)(&plain); + let inspected = run_under(&case, MegaSpecId::REX7, true); + assert_eq!( + plain, inspected, + "{}: an observation-only inspector must change nothing", + case.name, + ); + } +} + +/// The same matrix under the frozen spec the REX7 loops share their body with. +/// +/// The loops are not spec-gated — only where they take the journal decision is — so a settlement +/// that reaches one loop and not the other would show up here too, on a spec whose behaviour is +/// closed. +#[test] +fn test_both_frame_loops_agree_on_every_frame_outcome_under_rex6() { + for case in cases() { + let plain = run_under(&case, MegaSpecId::REX6, false); + let inspected = run_under(&case, MegaSpecId::REX6, true); + assert_eq!( + plain, inspected, + "{}: an observation-only inspector must change nothing under REX6", + case.name, + ); + } +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index cc1b3c7a..2759ca9f 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -2,6 +2,9 @@ //! //! - `checkpoint_settlement` — checkpoint compute-gas settlement: per-transaction totals stay //! bit-identical to per-opcode recording, and the two places where the models diverge. +//! - `frame_loop_parity` — one case per branch of the frame lifecycle that can end a frame, each +//! run through both frame loops and compared on everything a transaction produces, state +//! included: the loops share one body, and an observation-only inspector adds nothing to it. //! - `gas_clamp` — gas-clamp enforcement: a crossing opcode is stopped before it executes, and the //! resulting out-of-gas is restored and reclassified by the constraint that bound the clamp. //! - `clamp_classification` — which constraint a clamp binds to, including the exact-value case, @@ -79,6 +82,7 @@ mod detention_window; mod double_exceed_corner; mod exceptional_halt; mod frame_init_reject_burn; +mod frame_loop_parity; mod gas_clamp; mod gas_leakage; mod guard_pass_static_gas; diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 41568eb4..0668146a 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -276,6 +276,27 @@ impl Inspector for CallGasLimitRaiser { } } +/// Rewrites every successful contract creation into a revert — the shape the frame loop has to +/// carry through to the journal. +#[derive(Default)] +struct CreateKiller { + killed: u64, +} + +impl Inspector for CreateKiller { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Revert; + self.killed += 1; + } + } +} + /// Rewrites every failed contract creation into a successful one — the shape the shim refuses. #[derive(Default)] struct CreateReviver; @@ -592,6 +613,76 @@ fn test_reviving_a_failed_creation_is_refused() { } } +/// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the +/// state has to follow it. +/// +/// This is the rewrite direction there is something behind: the constructor ran, the deposit +/// predicates passed, and the inspector is telling the caller the frame failed. If the journal +/// decision were taken before the callback, the caller would be handed a failure over a deployed +/// contract, with the constructor's storage writes committed underneath it. +#[test] +fn test_killing_a_successful_creation_rolls_its_state_back() { + // Init code that stores to slot 1 and returns a two-byte runtime code. + let init_code: Vec = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(7)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset: the last two bytes of the word just stored + .append(RETURN) + .build() + .to_vec(); + + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init_code.iter().enumerate() { + builder = builder + .push_number(u64::from(*byte)) + .push_number(offset as u64) + .append(revm::bytecode::opcode::MSTORE8); + } + let code = builder + .push_number(init_code.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + + let deployed = CONTRACT.create(0); + + // The uninspected run deploys, so the rewrite has something to undo. + let mut observer = Observer::default(); + let plain = transact_inspected(base_db(code.clone()), default_limits(), &mut observer); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); + assert!( + !deployed_account.info.is_empty_code_hash(), + "the fixture must deploy code for the rewrite to have something to undo", + ); + + let mut killer = CreateKiller::default(); + let killed = transact_inspected(base_db(code), default_limits(), &mut killer); + + assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); + assert!( + killed.state.get(&deployed).is_none_or(|account| account.info.is_empty_code_hash()), + "a creation the inspector failed must leave no code at {deployed}", + ); + assert_eq!( + killed + .state + .get(&deployed) + .and_then(|account| account.storage.get(&U256::from(1))) + .map(|slot| slot.present_value()) + .unwrap_or_default(), + U256::ZERO, + "and none of the constructor's storage writes", + ); + assert_identity("killed creation", &killed); +} + /// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. /// /// This is the property every tracer in production depends on. The comparison is against a run with From 50a51487826917554609d4f0d6402740c7580092 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:18:53 +0800 Subject: [PATCH 099/208] test(rex7): pin the envelope an intercepted frame destroys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A callback that returns a synthetic outcome skips the frame init entirely, so the settlement that books what a refused frame init destroys never used to run on that path. A halting synthetic outcome hands nothing back to its caller, and the transaction then spends an envelope with no compute total to show for it — the shape the conservation law exists to catch. The settlement now covers it, and the test goes red the moment it stops. --- .../mega-evm/tests/rex7/measured_inspector.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 0668146a..63326832 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -613,6 +613,71 @@ fn test_reviving_a_failed_creation_is_refused() { } } +/// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. +/// +/// A callback that returns a synthetic outcome skips the frame init entirely: no frame is built, +/// and the settlement that books what a refused frame init destroys never used to run on this +/// path. A halting outcome hands nothing back to the caller, so the transaction spends that +/// envelope with no compute total to show for it — which is exactly what the conservation law is +/// stated over, and what it goes red on. +#[test] +fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { + /// Intercepts the call to [`CALLEE`] with an exceptional halt, keeping the forwarded gas. + #[derive(Default)] + struct HaltingInterceptor { + intercepted: u64, + forwarded: u64, + } + + impl Inspector for HaltingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.forwarded = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::OutOfGas, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = base_db(code).account_code(CALLEE, plain_run_code(20)); + + let mut inspector = HaltingInterceptor::default(); + let inspected = transact_inspected(db, default_limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); + assert_eq!( + inspected.destroyed, inspector.forwarded, + "the whole intercepted envelope is destroyed — nothing hands it back", + ); + assert_eq!( + inspected.compute_gas, + inspected.enforced + inspected.destroyed, + "and it is reported without being enforced", + ); + assert_identity("intercepted halt", &inspected); +} + /// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the /// state has to follow it. /// From 3a30b75ee0965bda9d1c3c68f542056706c28ee3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:22:29 +0800 Subject: [PATCH 100/208] docs: state that a frame's state follows the result its caller is handed Rex7 decides a frame's journal outcome from the frame's final result, so a frame that reports a revert has reverted and a creation that reports anything but success deposits no code. Frozen specs decide it from the instruction result before the resource-limit rewrites reach it, which is the split Rex4 already documents for the code-deposit path; the spec pages now say so for the general case, and say what the new rule does not reach. The agent guides gain the frame lifecycle the rework introduced: where the single settlement point is, which of the two upstream functions `evm/frame.rs` mirrors and therefore has to be re-audited on a revm bump, and why a fresh limit check must not run inside the settlement. --- AGENTS.md | 18 +++++++++++++++--- crates/mega-evm/src/evm/AGENTS.md | 11 +++++++---- crates/mega-evm/src/limit/AGENTS.md | 4 ++++ docs/spec/evm/compute-gas.md | 13 +++++++++++++ docs/spec/upgrades/rex7.md | 26 ++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a45c13f9..e616683d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,9 +119,9 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Through REX6 every opcode's gas consumption is recorded via wrapped instructions in `evm/instructions.rs` — `compute_gas_ext::*` for plain opcodes and `storage_gas_ext::*` for storage-affecting opcodes (SSTORE, LOG, CALL-family, CREATE/CREATE2, SELFDESTRUCT) — both invoking the shared `record_storage_compute_gas!` primitive after the opcode body completes. REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a gas clamp. A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. - The destroyed half is read from the frame's final result after action processing, so revm's post-action create rejects are covered; storage gas a checkpoint body charged before aborting belongs to neither half. + The destroyed half is read from the frame's final result at `AdditionalLimit::finalize_frame`, so revm's create-return rejects and any rewrite an inspector's last callback made are both covered; storage gas a checkpoint body charged before aborting belongs to neither half. A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. - A frame init that refuses to build a frame at all is settled in `after_frame_init`, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. + A frame init that refuses to build a frame at all is settled at the same point, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend + inspector_conjured_gas − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. @@ -129,6 +129,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi `inspector_conjured_gas` is the same kind of correction for a producer outside the EVM: `MegaEvm` wraps every inspector it is handed in `MeasuredInspector`, which snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference into `AdditionalLimit::inspector_ledger` — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. + An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). @@ -137,6 +138,15 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Both dimensions are enforced independently. A transaction can be halted by exceeding either limit. +#### Frame Lifecycle and the Single Settlement Point + +revm assembles a frame's result, decides its journal checkpoint and — for a contract creation — runs the deposit predicates and writes the code all inside `EthFrame::process_next_action`, and runs the inspector's last mutating callback after that function returns. +`evm/frame.rs` splits it: `classify_frame_action` decides what the frame's result is and records the journal decision it reached as a `FrameJournalVerdict`; `commit_frame_journal` carries that decision out. +Between the two run the frozen post-action charge, the inspector's `frame_end`, and `AdditionalLimit::finalize_frame` — the single point a frame's outcome is settled (final classification, executed/destroyed split, frame-init refusal booking, gas rescue, and the REX7 frame-local absorb). +Under REX7 the journal decision is taken after that settlement, so a frame's state agrees with the result its caller is handed; frozen specs take it where revm does, right after the classification, because what they replay includes the state a frame leaves behind when a later rewrite fails it. +Both frame loops (`frame_run` / `inspect_frame_run`) and both frame-init paths (`frame_init` / `inspect_frame_init`) run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. +`classify_frame_action` and `commit_frame_journal` together are a re-ordering of upstream code with no type-level tie to it, so a revm bump has to re-audit them; the debug assertion in `classify_create_return` catches only the one drift class it names. + #### Multidimensional Resource Limits Beyond the dual gas model, mega-evm enforces **four independent per-transaction resource limits** via `AdditionalLimit` (`limit/limit.rs`): @@ -223,6 +233,7 @@ The following paths are common sources of leakage: Any per-frame gas adjustment applied in `before_frame_init` is skipped on this path. The `push_empty_frame()` call maintains stack alignment but does not apply adjustments. Synthetic results must not assume any per-frame gas mechanism was applied. + Such a result does reach `finalize_frame`, as `FrameExit::RefusedSynthetically`: under REX7 its envelope is settled like any other refusal's, while frozen specs leave it alone. 2. **Gas rescue on TX-level limit exceed** (`limit/limit.rs`): When a transaction-level resource limit is exceeded, `rescue_gas` captures remaining gas for sender refund. If a frame's gas was inflated by a per-frame mechanism, the rescued amount must exclude the inflated portion — otherwise the sender recovers system-granted gas that should have been burned. 3. **Frame return** (`limit/limit.rs`): `before_frame_return_result` is the final hook before gas is returned to the parent. @@ -322,7 +333,8 @@ When the agent is requested to implement a new feature or bug fix, it should con For instruction-count deltas across a PR, use the CodSpeed report posted on the PR rather than local wall-clock numbers. - **Re-run the destroyed-gas conservation scan after a revm / alloy-evm upgrade.** The REX7 destroyed total is derived from the envelope, so any upstream change that moves gas without a MegaETH site recording it — a new minted subsidy like `CALL_STIPEND`, a changed refund or floor ordering, a new component of `total_gas_spent` — becomes a missing term in the law rather than a compile error. - After bumping revm or alloy-evm, run `cargo test -p mega-evm` and `cargo test -p mega-state-test -p state-test` (the `debug_assert` cross-check is live in debug builds) plus the replay fixtures under the latest spec (`cargo run -p state-test -- --bench --bench-spec bench/replay/fixtures`), whose own `post` expectations pin an older spec and would otherwise give the derivation no coverage. + The frame-lifecycle mirror in `evm/frame.rs` is the same kind of exposure in the other direction: it is a re-ordering of `EthFrame::process_next_action` and `return_create`, so an upstream change to either becomes a silent divergence rather than a compile error. + After bumping revm or alloy-evm, diff those two upstream functions against `evm/frame.rs`, then run `cargo test -p mega-evm` and `cargo test -p mega-state-test -p state-test` (the `debug_assert` cross-check is live in debug builds) plus the replay fixtures under the latest spec (`cargo run -p state-test -- --bench --bench-spec bench/replay/fixtures`), whose own `post` expectations pin an older spec and would otherwise give the derivation no coverage. - **Use `test_` prefix for Rust test function names.** New `#[test]` functions should be named with a `test_` prefix for consistency with this repository and upstream revm style. If editing nearby tests in the same module, align names to the same `test_` style when reasonable. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index f62112a2..5bc6f12c 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -6,7 +6,8 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, ## STRUCTURE - `mod.rs`: `MegaEvm` wrapper, inspector toggling, execution convenience APIs. - `context.rs`: execution context composition and state wiring. -- `execution.rs`: transaction execution flow and result shaping. +- `execution.rs`: transaction execution flow, the two frame loops and the two frame-init paths, and result shaping. +- `frame.rs`: revm's frame-action processing, split so the journal decision can be withheld until the frame's settlement has run — `classify_frame_action` decides the result, `commit_frame_journal` carries the decision out. - `factory.rs`: `MegaEvmFactory` builder for context and external env wiring. - `instructions.rs`: spec-layered opcode table and extension wrappers. - `host.rs`: host overrides for volatile tracking, oracle reads, SALT gas hooks. @@ -27,9 +28,11 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, A gas-counter edit is kept out of REX7 compute accounting (the checkpoint baseline is shifted by it) and out of the compute headroom (the gas clamp is re-derived immediately). A raised frame `gas_limit` is booked as conjured gas so the destroyed-remainder derivation still balances. Add the shim's counterpart when adding an `Inspector` callback: an unwrapped callback is an unmeasured hole, not a compile error. -- What the shim does **not** yet cover, because the answer depends on a frame's final classification and that is not settled until after the last mutating callback: a `call_end` / `create_end` that rewrites a frame result's gas or its classification, and the gas an intercepting callback puts into a synthetic outcome. - Those still move REX7 accounting off the uninspected path, and a large enough move trips the conservation `debug_assert`. - One shape is refused outright rather than left to diverge: a `create_end` (or the `frame_end` after it) turning a failed creation into a successful one — see `reject_forbidden_create_rewrite`. +- A rewrite the last mutating callback makes to a frame result's gas is booked from the frame's settlement point rather than from the callback boundary, because whether it moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. + The gas an intercepting callback puts into a synthetic outcome travels through that same lane. + One shape is refused outright rather than measured: a `create_end` (or the `frame_end` after it) turning a failed creation into a successful one — see `reject_forbidden_create_rewrite`, and the verdict in `frame.rs` that gives such a rewrite no code to deposit even if the refusal were removed. +- Both frame loops and both frame-init paths run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. + Add to the shared body, not to one copy: `tests/rex7/frame_loop_parity.rs` compares the two on every frame outcome, state included, and is what a one-sided edit fails. ## WHERE TO LOOK - New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` still aliases `rex5` and expresses its deltas as `is_enabled` dispatch inside the shared handlers; `rex7` is a standalone checkpoint table built from revm's base table, with the 17 storage / CALL / CREATE / SELFDESTRUCT / not-yet-activated slots inherited from `rex6`, and with 15 volatile `*_checkpoint` handlers plus `gas_checkpoint` registered as rex7-only). diff --git a/crates/mega-evm/src/limit/AGENTS.md b/crates/mega-evm/src/limit/AGENTS.md index c4799024..b8403d5b 100644 --- a/crates/mega-evm/src/limit/AGENTS.md +++ b/crates/mega-evm/src/limit/AGENTS.md @@ -17,6 +17,9 @@ Resource metering subsystem for transaction and frame limits across compute gas, - Limit-check order is deterministic and shared by all opcode paths. - Distinguish TX-level exceed (halt/OutOfGas) from frame-local exceed (revert). - All trackers push/pop per-frame in lockstep with EVM frame lifecycle hooks. +- `AdditionalLimit::finalize_frame` is the single point a frame's outcome is settled — the destroyed-remainder booking, the frame-init refusal booking, the gas rescue, and the REX7 frame-local absorb — and it runs after the last callback that can rewrite the frame's classification and before the journal decision. + Put a new frame-exit settlement there, not in a lifecycle hook on either side of it. + The pops stay in `before_frame_return_result`: the paths that reach a caller without ever running a frame would double-pop. - Synthetic frame results still require empty-frame pushes for stack alignment. - Gas rescue must exclude any system-granted stipend gas. - Revert paths must roll back discardable usage for data/KV/state growth trackers. @@ -26,6 +29,7 @@ Resource metering subsystem for transaction and frame limits across compute gas, - Do not encode frame-local exceeds as halts. - They must be reverts with bounded payload. - Do not read tracker totals after an exceeded-limit revert path unless using tracker-owned finalized APIs. +- Do not run a fresh `check_limit()` inside `finalize_frame`: a per-frame exceed is defined by the frame's usage weighed against its *caller's* budget after the merge, which is only answerable once the frame is back with its caller. - Avoid duplicating limit checks inside opcode handlers when the tracker already enforces the same dimension. ## WHERE TO LOOK diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index eba6ea57..7f5629c3 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -450,6 +450,19 @@ The rule admits no exception: the keyless-deploy dispatch path rescues on the sa Rescue is specific to a transaction-level exceed. A frame-local exceed needs none: the frame reverts and its unspent gas returns to the parent through ordinary frame accounting. +Through Rex6, the frame's state does not follow that revert. +A node commits or reverts a frame's journal checkpoint from the frame's instruction result when the frame's action is processed, which is before the frame-local rewrite reaches the result; a frame that ran to a successful exit therefore reports the revert over state that stays committed. + +
+Rex7 (unstable): the frame's state follows its final result + +Under Rex7, a node MUST decide a frame's journal outcome from the frame's final result — the result after every settlement and every rewrite the node applies at that frame's exit — so a frame that reports a revert has reverted. + +The rule reaches every exceed the frame itself latched while it ran. +It does not reach one first detected on the way out to the caller, which weighs the frame's usage against the caller's budget after the merge and is therefore only answerable once the frame is back with its caller; that one is absorbed there, as on every spec. + +
+ When a `CALL`-family or `CREATE` / `CREATE2` opcode fails on a compute-gas exceed — the frame-local revert and the transaction-level halt alike — its pending child frame is discarded before the child runs. A node MUST return the gas already forwarded to that discarded child to the frame before it terminates, so that gas is not charged as consumed: on a frame-local revert it returns to the parent frame, and on a transaction-level halt it is excluded from the transaction's `gas_used`. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 451970a4..39cdc712 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -273,6 +273,26 @@ What changes is what the transaction reports. A frame-local exceed on this path MUST NOT be latched — with the amount unrecorded the transaction is within every limit, and the frames above it MAY continue. A transaction-level exceed MUST be latched and MUST halt the transaction with the usual gas rescue, and MUST carry the same detention attribution it would have carried had the amount been recorded. +### Frame Result and Frame State Agree + +#### Previous behavior + +A frame's journal checkpoint is committed or reverted from the frame's instruction result at the moment the frame's action is processed, which is before the node applies the resource-limit rewrites that decide what the frame finally reports. +A frame that ran to a successful exit and is then rewritten into a frame-local revert therefore reports failure over state that stays committed: a contract creation on that path leaves its deployed code and its constructor's storage writes behind, and the caller is told the frame failed. +[Rex4](rex4.md) documents that split for the code-deposit path, where a node MUST NOT roll the deployment back. + +#### New behavior + +Under Rex7, a node MUST decide a frame's journal outcome from the frame's **final** result — the result after every settlement and every rewrite the node applies at that frame's exit. +A frame that reports a revert MUST have its state reverted; a contract creation that reports anything other than success MUST deposit no code. + +This closes the split for every exceed the frame itself latched while it ran. +It does not reach an exceed first detected on the way out to the caller: that check weighs the frame's usage against the caller's budget, after the frame's usage has been merged into it, so it is answerable only once the frame is already back with its caller. +Such an exceed is absorbed there, on every spec, and the frozen split remains for it. + +A node MUST NOT deposit code that its create-return predicates rejected, whatever a later rewrite says the result is. +The bytes a deployment writes MUST be the bytes those predicates approved. + ## Developer Impact Rex7 is not scheduled on any network. @@ -294,6 +314,9 @@ Under Rex6 that unused envelope was enforcing, so the same tail work can survive A contract creation that fails at its frame exit reports `code_length × CODEDEPOSIT` less compute gas under Rex7 than under Rex6, and leaves that much more of the transaction's and the block's compute budget for the work that follows. Whether the creation succeeds, what it deploys, and the receipt it produces are unchanged. +A frame that reports a frame-local revert leaves no state behind under Rex7, where under Rex6 a frame that had already exited successfully kept its writes. +A caller that read state written by such a frame after absorbing its revert sees nothing under Rex7. + ## Safety and Compatibility Rex7 changes nothing about how blocks under earlier specs are executed. @@ -308,6 +331,9 @@ Rex7 can report more compute gas than Rex6 for the same inputs on four paths: th The carve-out's enforcing half is never looser than Rex6's on interpreter frames, and is stricter in exactly one shape: an ordinary out-of-gas taken with no clamp in force, whose zeroed counter leaves the whole segment measuring as executed. On a precompile that fails before performing work, Rex7 enforcement is deliberately looser than Rex6's: the unused envelope does not bind the compute limit. +Rex7 rolls back state Rex6 keeps on one path: a frame that exited successfully and was then rewritten into a frame-local revert. +The rewrite itself is unchanged; what changes is that the journal follows it. + Rex7 reports less compute gas than Rex6 on one path: a contract creation that fails at its frame exit, whose code-deposit compute gas Rex6 records and Rex7 does not. Enforcement is looser there by the same amount, and deliberately so — the EVM charges that amount only for a deposit that happens, so enforcing it against a creation that failed would bind the compute limit with gas nobody spent. From 0c78af46c41e0c2167ac7e4b5b97038bb7c133fa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Mon, 31 Aug 2026 23:46:55 +0800 Subject: [PATCH 101/208] chore(evm): drop the unused alloc alias from the frame module --- crates/mega-evm/src/evm/frame.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/mega-evm/src/evm/frame.rs b/crates/mega-evm/src/evm/frame.rs index f998abb4..53f24ce3 100644 --- a/crates/mega-evm/src/evm/frame.rs +++ b/crates/mega-evm/src/evm/frame.rs @@ -29,9 +29,6 @@ //! catches one specific class of drift (the deposit predicate `MegaETH` weighs against) and //! nothing else. -#[cfg(not(feature = "std"))] -use alloc as std; - use alloy_primitives::{Address, Bytes}; use revm::{ context::{Cfg, ContextTr, JournalTr}, From 67131041807b097edc6346007b2e3bb0d2a2e50a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 10:46:41 +0800 Subject: [PATCH 102/208] refactor(limit): read every limit check off an explicit view of its tracker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four resource-limit checks read two things out of a FrameLimitTracker: the current frame's budget, when there is one, and the transaction's net usage. Both are now gathered into a FrameLimitView, and every check body takes one as a parameter rather than reaching into the tracker itself. Nothing about what the checks decide changes — the frame-local predicate moves to one shared function and the bodies are otherwise as they were. What changes is that a check can now be asked over a reading other than the tracker's current one, which is what a frame return needs: the question it has to answer is whether the returning frame overran its caller's budget, and that is only readable after the merge, one step past the point where the answer could still change anything. --- crates/mega-evm/src/limit/compute_gas.rs | 22 +++- crates/mega-evm/src/limit/data_size.rs | 73 ++++++++------ crates/mega-evm/src/limit/frame_limit.rs | 117 ++++++++++++++++------ crates/mega-evm/src/limit/kv_update.rs | 61 ++++++----- crates/mega-evm/src/limit/mod.rs | 2 +- crates/mega-evm/src/limit/state_growth.rs | 51 ++++++---- 6 files changed, 211 insertions(+), 115 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 4d506273..75a88d54 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -276,9 +276,23 @@ impl ComputeGasTracker { /// predicate to drift. #[inline] pub(crate) fn check_limit_with_extra(&self, extra: u64) -> LimitCheck { + self.check_limit_with_extra_on(&self.frame_tracker.view(), extra) + } + + /// [`check_limit_with_extra`](Self::check_limit_with_extra) against an explicit reading of the + /// tracker. + /// + /// The reading is a parameter so that one body can answer both questions asked of this check: + /// what it says now, and what it will say once a returning frame has been merged into its + /// caller. A frame return needs the second answer before the merge happens, and a second copy + /// of the predicates would be free to drift from the first. + pub(crate) fn check_limit_with_extra_on( + &self, + view: &super::FrameLimitView, + extra: u64, + ) -> LimitCheck { if self.rex4_enabled { - let frame_check = - self.frame_tracker.would_exceed_current_frame_limit(LimitKind::ComputeGas, extra); + let frame_check = view.would_exceed_frame_limit(LimitKind::ComputeGas, extra); if frame_check.exceeded_limit() { return frame_check; } @@ -293,12 +307,12 @@ impl ComputeGasTracker { // reported `used` is the full settled total, so a halt reason states the usage the // transaction actually ends with. The two coincide on every spec before REX7. let limit = self.tx_limit(); - if self.enforced_tx_usage().saturating_add(extra) > limit { + if view.net_usage().saturating_sub(self.burned).saturating_add(extra) > limit { LimitCheck::ExceedsLimit { kind: LimitKind::ComputeGas, frame_local: false, limit, - used: self.tx_usage().saturating_add(extra), + used: view.net_usage().saturating_add(extra), } } else { LimitCheck::WithinLimit diff --git a/crates/mega-evm/src/limit/data_size.rs b/crates/mega-evm/src/limit/data_size.rs index 20bff89c..78648dfb 100644 --- a/crates/mega-evm/src/limit/data_size.rs +++ b/crates/mega-evm/src/limit/data_size.rs @@ -133,6 +133,46 @@ impl DataSizeTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. + /// + /// The reading is a parameter so that one body can answer both questions asked of this check: + /// what it says now, and what it will say once a returning frame has been merged into its + /// caller. A frame return needs the second answer before the merge happens, and a second copy + /// of the predicates would be free to drift from the first. + pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + if self.rex4_enabled { + let frame_check = view.exceeds_frame_limit(super::LimitKind::DataSize); + if frame_check.exceeded_limit() { + return frame_check; + } + // TX-level fallthrough: defense-in-depth safety net. + // In Rex4+ during execution, per-frame budgets are derived from remaining TX + // budget, so this should only exceed when no frame exists (intrinsic overflow). + } + let used = view.net_usage(); + let limit = self.frame_tracker.tx_limit(); + if used > limit { + // Defense-in-depth: pre-REX5, the only mid-execution writer to `tx_entry` is + // `before_tx_start` (which runs before any frame is pushed), so a TX-level + // exceed with an active frame indicates a budget-accounting bug. REX5+ adds + // `record_oracle_hint_bytes` which legitimately writes to `tx_entry` mid- + // execution to meter oracle-hint payloads as TX-scoped side-channel cost, so + // the invariant is only asserted on pre-REX5 specs. + debug_assert!( + !self.rex4_enabled || self.rex5_enabled || !view.has_frame(), + "DataSize TX-level exceeded with active frame — budget invariant violated" + ); + super::LimitCheck::ExceedsLimit { + kind: super::LimitKind::DataSize, + limit, + used, + frame_local: false, + } + } else { + super::LimitCheck::WithinLimit + } + } + /// Returns the remaining data size budget for the current call frame, capped by /// the TX-level remaining. pub(crate) fn current_call_remaining(&self) -> u64 { @@ -170,38 +210,7 @@ impl TxRuntimeLimit for DataSizeTracker { /// (intrinsic usage is recorded in `tx_entry` before the first frame is pushed). /// In pre-Rex4, checks total data size across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - if self.rex4_enabled { - let frame_check = - self.frame_tracker.exceeds_current_frame_limit(super::LimitKind::DataSize); - if frame_check.exceeded_limit() { - return frame_check; - } - // TX-level fallthrough: defense-in-depth safety net. - // In Rex4+ during execution, per-frame budgets are derived from remaining TX - // budget, so this should only exceed when no frame exists (intrinsic overflow). - } - let used = self.tx_usage(); - let limit = self.frame_tracker.tx_limit(); - if used > limit { - // Defense-in-depth: pre-REX5, the only mid-execution writer to `tx_entry` is - // `before_tx_start` (which runs before any frame is pushed), so a TX-level - // exceed with an active frame indicates a budget-accounting bug. REX5+ adds - // `record_oracle_hint_bytes` which legitimately writes to `tx_entry` mid- - // execution to meter oracle-hint payloads as TX-scoped side-channel cost, so - // the invariant is only asserted on pre-REX5 specs. - debug_assert!( - !self.rex4_enabled || self.rex5_enabled || !self.frame_tracker.has_active_frame(), - "DataSize TX-level exceeded with active frame — budget invariant violated" - ); - super::LimitCheck::ExceedsLimit { - kind: super::LimitKind::DataSize, - limit, - used, - frame_local: false, - } - } else { - super::LimitCheck::WithinLimit - } + self.check_limit_on(&self.frame_tracker.view()) } /// Records the data size of a transaction at the start of execution. diff --git a/crates/mega-evm/src/limit/frame_limit.rs b/crates/mega-evm/src/limit/frame_limit.rs index cca3fc57..f713ce33 100644 --- a/crates/mega-evm/src/limit/frame_limit.rs +++ b/crates/mega-evm/src/limit/frame_limit.rs @@ -32,6 +32,78 @@ pub(crate) struct CallFrameInfo { charged_parent_update: bool, } +/// The numbers a resource-limit check reads out of a [`FrameLimitTracker`]: the current frame's +/// budget, when there is one, and the transaction's net usage. +/// +/// Two of these exist at a frame return — the tracker as it stands, and the tracker as it will +/// stand once the returning frame has been popped and merged into its caller. Handing the check +/// bodies a view rather than the tracker is what lets the second reading be taken *before* the +/// merge without a second copy of the predicates to drift from the first: the same body runs, only +/// its input differs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct FrameLimitView { + /// The current frame's budget, or `None` when no frame is on the stack. + frame: Option, + /// `Σ(persistent + discardable) − Σ refund` across the TX entry and every frame on the stack. + net_usage: u64, +} + +/// One frame's budget, as a limit check reads it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct FrameBudget { + limit: u64, + used: u64, + refund: u64, +} + +impl FrameLimitView { + /// Whether a frame is on the stack — the predicate the TX-level budget invariants assert on. + #[inline] + pub(crate) fn has_frame(&self) -> bool { + self.frame.is_some() + } + + /// The transaction's net usage. + #[inline] + pub(crate) fn net_usage(&self) -> u64 { + self.net_usage + } + + /// Whether the current frame has exceeded its frame-local budget, evaluated as if `extra` more + /// usage had already been recorded against it. + #[inline] + pub(crate) fn would_exceed_frame_limit(&self, kind: LimitKind, extra: u64) -> LimitCheck { + frame_budget_check(self.frame, kind, extra) + } + + /// [`would_exceed_frame_limit`](Self::would_exceed_frame_limit) with nothing extra. + #[inline] + pub(crate) fn exceeds_frame_limit(&self, kind: LimitKind) -> LimitCheck { + self.would_exceed_frame_limit(kind, 0) + } +} + +/// The one frame-local exceed predicate, shared by every reading of it. +/// +/// A separate copy per call site — or per view — would be free to drift from the one enforcement +/// actually uses, which is exactly what a pre-merge reading must not do. +#[inline] +fn frame_budget_check(budget: Option, kind: LimitKind, extra: u64) -> LimitCheck { + match budget { + Some(entry) + if entry.used.saturating_add(extra).saturating_sub(entry.refund) > entry.limit => + { + LimitCheck::ExceedsLimit { + kind, + limit: entry.limit, + used: entry.used.saturating_add(extra), + frame_local: true, + } + } + _ => LimitCheck::WithinLimit, + } +} + #[derive(Debug, Clone)] pub(crate) struct FrameLimitTracker { /// Top-level (TX-scope) entry. Holds the TX limit and accumulates usage @@ -98,7 +170,7 @@ impl FrameLimitEntry { /// /// Computed as `limit - (used - refund)`, clamped to `[0, limit]`. /// The net usage (`used - refund`) is computed first to stay consistent with - /// the exceed check in `exceeds_current_frame_limit`. + /// the exceed check in `frame_budget_check`. #[inline] pub(crate) fn remaining(&self) -> u64 { self.limit.saturating_sub(self.used().saturating_sub(self.refund)) @@ -220,41 +292,24 @@ impl FrameLimitTracker { child } - /// Returns whether the current frame has exceeded its frame-local limit. - /// If exceeded, `frame_local` is always `true` since this checks per-frame budgets. - pub(crate) fn exceeds_current_frame_limit(&self, kind: LimitKind) -> LimitCheck { - self.would_exceed_current_frame_limit(kind, 0) + /// The current frame's budget, as a limit check reads it. + #[inline] + fn frame_budget(&self) -> Option { + self.frame_stack.last().map(|entry| FrameBudget { + limit: entry.limit, + used: entry.used(), + refund: entry.refund, + }) } - /// [`exceeds_current_frame_limit`](Self::exceeds_current_frame_limit) evaluated as if `extra` - /// had already been added to the current frame's usage, without adding it. - /// - /// A caller that must decide whether to make a charge at all — rather than make it and react - /// to the verdict — asks here. The two share one predicate on purpose: a separate copy of - /// `used - refund > limit` would be free to drift from the one enforcement actually uses. - pub(crate) fn would_exceed_current_frame_limit( - &self, - kind: LimitKind, - extra: u64, - ) -> LimitCheck { - match self.frame_stack.last() { - Some(entry) - if entry.used().saturating_add(extra).saturating_sub(entry.refund) > - entry.limit => - { - LimitCheck::ExceedsLimit { - kind, - limit: entry.limit, - used: entry.used().saturating_add(extra), - frame_local: true, - } - } - _ => LimitCheck::WithinLimit, - } + /// What the limit checks read out of this tracker as it stands. + #[inline] + pub(crate) fn view(&self) -> FrameLimitView { + FrameLimitView { frame: self.frame_budget(), net_usage: self.net_usage() } } /// Returns the budget of the current frame, in the same form - /// [`exceeds_current_frame_limit`](Self::exceeds_current_frame_limit) reports it on an exceed. + /// [`frame_budget_check`] reports it on an exceed. /// /// If the frame stack is empty (before the first frame is pushed), returns the TX-level limit. pub(crate) fn current_frame_limit(&self) -> u64 { diff --git a/crates/mega-evm/src/limit/kv_update.rs b/crates/mega-evm/src/limit/kv_update.rs index a1339b68..ca92f91c 100644 --- a/crates/mega-evm/src/limit/kv_update.rs +++ b/crates/mega-evm/src/limit/kv_update.rs @@ -88,6 +88,40 @@ impl KVUpdateTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. + /// + /// The reading is a parameter so that one body can answer both questions asked of this check: + /// what it says now, and what it will say once a returning frame has been merged into its + /// caller. A frame return needs the second answer before the merge happens, and a second copy + /// of the predicates would be free to drift from the first. + pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + if self.rex4_enabled { + let frame_check = view.exceeds_frame_limit(super::LimitKind::KVUpdate); + if frame_check.exceeded_limit() { + return frame_check; + } + // TX-level fallthrough: defense-in-depth safety net. + // In Rex4+ during execution, per-frame budgets are derived from remaining TX + // budget, so this should only exceed when no frame exists (intrinsic overflow). + } + let used = view.net_usage(); + let limit = self.frame_tracker.tx_limit(); + if used > limit { + debug_assert!( + !self.rex4_enabled || !view.has_frame(), + "KVUpdate TX-level exceeded with active frame — budget invariant violated" + ); + super::LimitCheck::ExceedsLimit { + kind: super::LimitKind::KVUpdate, + limit, + used, + frame_local: false, + } + } else { + super::LimitCheck::WithinLimit + } + } + /// Returns the remaining KV update budget for the current call frame, capped by /// the TX-level remaining. pub(crate) fn current_call_remaining(&self) -> u64 { @@ -126,32 +160,7 @@ impl TxRuntimeLimit for KVUpdateTracker { /// (intrinsic usage is recorded in `tx_entry` before the first frame is pushed). /// In pre-Rex4, checks total KV updates across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - if self.rex4_enabled { - let frame_check = - self.frame_tracker.exceeds_current_frame_limit(super::LimitKind::KVUpdate); - if frame_check.exceeded_limit() { - return frame_check; - } - // TX-level fallthrough: defense-in-depth safety net. - // In Rex4+ during execution, per-frame budgets are derived from remaining TX - // budget, so this should only exceed when no frame exists (intrinsic overflow). - } - let used = self.tx_usage(); - let limit = self.frame_tracker.tx_limit(); - if used > limit { - debug_assert!( - !self.rex4_enabled || !self.frame_tracker.has_active_frame(), - "KVUpdate TX-level exceeded with active frame — budget invariant violated" - ); - super::LimitCheck::ExceedsLimit { - kind: super::LimitKind::KVUpdate, - limit, - used, - frame_local: false, - } - } else { - super::LimitCheck::WithinLimit - } + self.check_limit_on(&self.frame_tracker.view()) } /// Records the KV updates at the start of a transaction. diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index 54142556..923e996a 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -13,7 +13,7 @@ mod state_growth; mod storage_call_stipend; pub use data_size::*; -pub(crate) use frame_limit::{FrameLimitTracker, TxRuntimeLimit}; +pub(crate) use frame_limit::{FrameLimitTracker, FrameLimitView, TxRuntimeLimit}; pub use inspector_ledger::*; pub use limit::*; diff --git a/crates/mega-evm/src/limit/state_growth.rs b/crates/mega-evm/src/limit/state_growth.rs index 55baa9dd..d2fde879 100644 --- a/crates/mega-evm/src/limit/state_growth.rs +++ b/crates/mega-evm/src/limit/state_growth.rs @@ -161,6 +161,35 @@ impl StateGrowthTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. + /// + /// The reading is a parameter so that one body can answer both questions asked of this check: + /// what it says now, and what it will say once a returning frame has been merged into its + /// caller. A frame return needs the second answer before the merge happens, and a second copy + /// of the predicates would be free to drift from the first. + pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + if self.spec.is_enabled(MegaSpecId::REX4) { + let frame_check = view.exceeds_frame_limit(super::LimitKind::StateGrowth); + if frame_check.exceeded_limit() { + return frame_check; + } + // TX-level fallthrough: catches Rex5 pre-frame authority usage and any + // future TX-level state-growth contribution. + } + let used = view.net_usage(); + let limit = self.frame_tracker.tx_limit(); + if used > limit { + super::LimitCheck::ExceedsLimit { + kind: super::LimitKind::StateGrowth, + limit, + used, + frame_local: false, + } + } else { + super::LimitCheck::WithinLimit + } + } + /// Returns the remaining state growth budget for the current call frame, capped by /// the TX-level remaining. pub(crate) fn current_call_remaining(&self) -> u64 { @@ -203,27 +232,7 @@ impl TxRuntimeLimit for StateGrowthTracker { /// usage — and any frame-level overflow that has already been popped into `tx_entry`. /// For pre-Rex4, checks total net growth across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - if self.spec.is_enabled(MegaSpecId::REX4) { - let frame_check = - self.frame_tracker.exceeds_current_frame_limit(super::LimitKind::StateGrowth); - if frame_check.exceeded_limit() { - return frame_check; - } - // TX-level fallthrough: catches Rex5 pre-frame authority usage and any - // future TX-level state-growth contribution. - } - let used = self.tx_usage(); - let limit = self.frame_tracker.tx_limit(); - if used > limit { - super::LimitCheck::ExceedsLimit { - kind: super::LimitKind::StateGrowth, - limit, - used, - frame_local: false, - } - } else { - super::LimitCheck::WithinLimit - } + self.check_limit_on(&self.frame_tracker.view()) } /// No-op. From 0ff7c72d5e2b95a31e415247c6dbf5866917eb13 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 10:48:00 +0800 Subject: [PATCH 103/208] feat(rex7): settle a late frame-local exceed before the pop that hides it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-frame budget is the frame's usage weighed against its caller's budget after the merge, so a frame can overrun one with nothing having noticed while it ran. The frame return is where that is first detectable, and through REX6 it is detected one step too late to act on: the pop has already merged the frame's usage on its original classification. What the caller is handed is a revert over usage that was kept. REX7 asks the same question one step earlier. FrameLimitTracker::view_after_pop computes what the trackers will read once the frame has been popped and merged, and each dimension runs its own check body over that reading, in check_limit's order — so the answer is the post-merge answer, taken before the merge, with no second copy of the predicates to drift from the first. A frame-local exceed is written onto the frame's result there, and the pop that follows reads a revert and discards the frame's usage the way it discards any reverting frame's. The two readings are cross-checked against each other on every frame return in debug builds, on every spec: whenever the merge the peek was asked about is the merge that happened, they must be identical down to the reported limit and used. Frozen specs compute the peek for that assertion alone, and the cfg! is a constant, so their release builds skip it. --- crates/mega-evm/src/limit/compute_gas.rs | 7 ++ crates/mega-evm/src/limit/data_size.rs | 7 ++ crates/mega-evm/src/limit/frame_limit.rs | 95 +++++++++++++++++ crates/mega-evm/src/limit/kv_update.rs | 7 ++ crates/mega-evm/src/limit/limit.rs | 123 +++++++++++++++++++--- crates/mega-evm/src/limit/state_growth.rs | 7 ++ 6 files changed, 229 insertions(+), 17 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index 75a88d54..ca566ff9 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -279,6 +279,13 @@ impl ComputeGasTracker { self.check_limit_with_extra_on(&self.frame_tracker.view(), extra) } + /// [`check_limit`](TxRuntimeLimit::check_limit) as it will read once the current frame has + /// been popped and merged into its caller, computed without popping it. + #[inline] + pub(crate) fn check_limit_after_pop(&self, success: bool) -> LimitCheck { + self.check_limit_with_extra_on(&self.frame_tracker.view_after_pop(success), 0) + } + /// [`check_limit_with_extra`](Self::check_limit_with_extra) against an explicit reading of the /// tracker. /// diff --git a/crates/mega-evm/src/limit/data_size.rs b/crates/mega-evm/src/limit/data_size.rs index 78648dfb..1b705112 100644 --- a/crates/mega-evm/src/limit/data_size.rs +++ b/crates/mega-evm/src/limit/data_size.rs @@ -133,6 +133,13 @@ impl DataSizeTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) as it will read once the current frame has + /// been popped and merged into its caller, computed without popping it. + #[inline] + pub(crate) fn check_limit_after_pop(&self, success: bool) -> super::LimitCheck { + self.check_limit_on(&self.frame_tracker.view_after_pop(success)) + } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. /// /// The reading is a parameter so that one body can answer both questions asked of this check: diff --git a/crates/mega-evm/src/limit/frame_limit.rs b/crates/mega-evm/src/limit/frame_limit.rs index f713ce33..a7521947 100644 --- a/crates/mega-evm/src/limit/frame_limit.rs +++ b/crates/mega-evm/src/limit/frame_limit.rs @@ -308,6 +308,41 @@ impl FrameLimitTracker { FrameLimitView { frame: self.frame_budget(), net_usage: self.net_usage() } } + /// What the limit checks will read out of this tracker once the current frame has been popped + /// and merged into its caller — computed without popping it. + /// + /// This mirrors [`pop_frame`](Self::pop_frame) term for term, and is the *only* place that + /// mirrors it: the child's persistent usage always moves up, its discardable usage and refund + /// move up on success and vanish otherwise, and the cached totals lose exactly what vanished. + /// A frame return cross-checks the two against each other in debug builds. + pub(crate) fn view_after_pop(&self, success: bool) -> FrameLimitView { + let Some(child) = self.frame_stack.last() else { + // Nothing to pop: `pop_frame` on an empty stack changes nothing. + return self.view(); + }; + let frame = self.frame_stack.len().checked_sub(2).map(|parent_index| { + let parent = &self.frame_stack[parent_index]; + let persistent = parent.persistent_usage + child.persistent_usage; + let (discardable, refund) = if success { + (parent.discardable_usage + child.discardable_usage, parent.refund + child.refund) + } else { + (parent.discardable_usage, parent.refund) + }; + FrameBudget { + limit: parent.limit, + used: persistent.checked_add(discardable).expect("overflow"), + refund, + } + }); + let net_usage = if success { + self.net_usage() + } else { + (self.cached_total_used - child.discardable_usage) + .saturating_sub(self.cached_total_refund - child.refund) + }; + FrameLimitView { frame, net_usage } + } + /// Returns the budget of the current frame, in the same form /// [`frame_budget_check`] reports it on an exceed. /// @@ -614,6 +649,66 @@ mod tests { const ADDR: Address = address!("0000000000000000000000000000000000001234"); + /// The pre-pop reading must be the post-pop reading, exactly. + /// + /// A frame return decides whether the returning frame overran its caller's budget from + /// [`FrameLimitTracker::view_after_pop`], one step before the pop that would produce that + /// reading naturally. The whole ordering rests on the two being the same numbers, so drive a + /// tracker into every shape the merge distinguishes — persistent-only, discardable, refunds, + /// a refund larger than the discardable usage it accompanies, and both depths at which the + /// merge target differs (a parent frame, and the TX entry) — and compare the predicted + /// reading against the one a real pop produces, field for field. + #[test] + fn test_view_after_pop_matches_the_view_a_real_pop_produces() { + // (label, child persistent, child discardable, child refund, extra frames beneath) + let shapes: [(&str, u64, u64, u64, usize); 6] = [ + ("empty child", 0, 0, 0, 1), + ("persistent only", 30, 0, 0, 1), + ("discardable only", 0, 40, 0, 1), + ("mixed", 7, 40, 11, 1), + ("refund above discardable", 7, 5, 40, 1), + ("child of the tx entry", 7, 40, 11, 0), + ]; + + for (label, persistent, discardable, refund, parents) in shapes { + for success in [true, false] { + let mut tracker = FrameLimitTracker::<()>::new(MegaSpecId::REX7, 10_000); + tracker.add_tx_persistent(90); + for _ in 0..parents { + tracker.push_frame(()); + // Give the parent a reading of its own on every lane, so a merge that + // dropped a term would show up rather than cancel. + tracker.add_frame_persistent(13); + tracker.add_frame_discardable(21); + tracker.add_frame_refund(5); + } + tracker.push_frame(()); + tracker.add_frame_persistent(persistent); + tracker.add_frame_discardable(discardable); + tracker.add_frame_refund(refund); + + let predicted = tracker.view_after_pop(success); + tracker.pop_frame(success); + assert_eq!( + predicted, + tracker.view(), + "{label} (success={success}): the pre-pop reading must equal the post-pop one", + ); + } + } + } + + /// The pre-pop reading of an empty stack is the reading itself: `pop_frame` on an empty stack + /// changes nothing, and the top-level frame return reaches this with the stack already popped. + #[test] + fn test_view_after_pop_on_an_empty_stack_is_the_current_view() { + let mut tracker = FrameLimitTracker::<()>::new(MegaSpecId::REX7, 10_000); + tracker.add_tx_persistent(90); + for success in [true, false] { + assert_eq!(tracker.view_after_pop(success), tracker.view()); + } + } + /// `set_created_address` on an empty frame stack must be a no-op. #[test] fn test_set_created_address_empty_stack_is_noop() { diff --git a/crates/mega-evm/src/limit/kv_update.rs b/crates/mega-evm/src/limit/kv_update.rs index ca92f91c..404bdae6 100644 --- a/crates/mega-evm/src/limit/kv_update.rs +++ b/crates/mega-evm/src/limit/kv_update.rs @@ -88,6 +88,13 @@ impl KVUpdateTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) as it will read once the current frame has + /// been popped and merged into its caller, computed without popping it. + #[inline] + pub(crate) fn check_limit_after_pop(&self, success: bool) -> super::LimitCheck { + self.check_limit_on(&self.frame_tracker.view_after_pop(success)) + } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. /// /// The reading is a parameter so that one body can answer both questions asked of this check: diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index d293fb7b..b446692b 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -957,6 +957,54 @@ impl AdditionalLimit { self.has_exceeded_limit } + /// [`check_limit`](Self::check_limit) as it will read once the returning frame has been + /// popped and merged into its caller — asked before the merge, and latching nothing. + /// + /// A per-frame budget is defined by the frame's usage weighed against its *caller's* budget + /// after the merge, so that is the only question worth asking at a frame return. Today it can + /// only be answered after the merge has already happened, which is too late for the answer to + /// change what the merge does with the frame's usage or what the journal does with its state. + /// This asks the same question one step earlier. + /// + /// "The same question" is meant literally: every dimension runs its own `check_limit` body, + /// in `check_limit`'s order, over a reading of its tracker taken as if the pop had happened. + /// The only thing that differs is where the numbers come from, and + /// [`FrameLimitTracker::view_after_pop`](super::FrameLimitTracker::view_after_pop) is the + /// single place that computes them. `before_frame_return_result` cross-checks the two readings + /// against each other on every frame return in debug builds. + /// + /// `success` is the merge the pop would perform — the returning frame's classification as it + /// stands when this is asked, before anything this answer causes rewrites it. + pub(crate) fn peek_check_limit_after_pop(&self, success: bool) -> LimitCheck { + // Sticky short-circuit, mirroring `check_limit`: a latched exceed or an exemption is what + // that pass would return, whatever the sub-trackers hold. + if !self.has_exceeded_limit.within_limit() { + return self.has_exceeded_limit; + } + + let data_size_check = self.data_size.check_limit_after_pop(success); + if data_size_check.exceeded_limit() { + return data_size_check; + } + + let kv_update_check = self.kv_update.check_limit_after_pop(success); + if kv_update_check.exceeded_limit() { + return kv_update_check; + } + + let compute_gas_check = self.compute_gas.check_limit_after_pop(success); + if compute_gas_check.exceeded_limit() { + return compute_gas_check; + } + + let state_growth_check = self.state_growth.check_limit_after_pop(success); + if state_growth_check.exceeded_limit() { + return state_growth_check; + } + + self.has_exceeded_limit + } + /// `true` when a per-tx resource limit has already been latched as exceeded — the exact /// condition [`frame_result_if_exceeding_limit`](Self::frame_result_if_exceeding_limit) halts /// the transaction on. `WithinLimit` and `Exempt` both return `false`. Reads the latched @@ -1586,6 +1634,26 @@ impl AdditionalLimit { /// Hook called when returning a frame result to parent frame in `frame_return_result` or /// `last_frame_result`. May modify the frame result in place if the limit is exceeded. + /// + /// # The late frame-local exceed + /// + /// A per-frame budget is defined by the frame's usage weighed against its *caller's* budget + /// after the merge, so a frame can overrun one without anything having noticed while it ran. + /// This hook is where that is first detectable. + /// + /// REX7 asks the question ahead of the pop, through + /// [`peek_check_limit_after_pop`](Self::peek_check_limit_after_pop), and writes the answer onto + /// the frame's result before anything acts on it. One classification then drives all three + /// things that follow from it: the caller is told the frame reverted, the pop discards the + /// frame's usage the way it discards any reverting frame's, and the journal — whose decision + /// waits until this hook has run — rolls the frame's state back. Weighing the usage before the + /// merge would be a different question with a different answer, which is why the *reading* is + /// taken as of after the merge even though the *decision* is taken before it. + /// + /// Frozen specs keep the split: the check runs after the pop, the merge has already happened + /// on the frame's original classification, and revm decided commit-or-revert from that same + /// classification before the result ever reached this hook — so a frame that ran to a + /// successful exit is already committed and stays committed under the rewritten `Revert`. pub(crate) fn before_frame_return_result( &mut self, result: &mut FrameResult, @@ -1596,6 +1664,27 @@ impl AdditionalLimit { // used to distinguish these two cases. let duplicate_return_frame_result = LAST_FRAME && !self.data_size.has_active_frame(); + // The merge the pop below is about to perform, read before anything can rewrite it. + let merges_usage = result.instruction_result().is_ok(); + // Frozen specs need this only for the debug cross-check under the pop, and the `cfg!` is a + // constant, so their release builds skip it entirely. + let peeked = (!duplicate_return_frame_result && + (self.rex7_enabled() || cfg!(debug_assertions))) + .then(|| self.peek_check_limit_after_pop(merges_usage)); + + if self.rex7_enabled() { + if let Some(check) = peeked { + if check.is_frame_local() { + // Nothing is latched and nothing needs clearing: the peek only read. + mark_frame_result_as_exceeding_limit( + result, + InstructionResult::Revert, + check.revert_data(), + ); + } + } + } + // Pop frame from the frame limit trackers. self.state_growth.before_frame_return_result::(result); self.data_size.before_frame_return_result::(result); @@ -1605,24 +1694,24 @@ impl AdditionalLimit { // Pop stipend from stack and burn unused stipend (Rex4+). self.storage_call_stipend.before_frame_return_result::(result); - // Frame-level limit handling (Rex4+): check if the child frame exceeded its - // frame-local budget. The detection may not have happened during execution, so - // we call check_limit() here to ensure it's caught. - // If frame-local, absorb it — clear the exceed flag and change to Revert so - // remaining gas returns to the caller. This works at any depth including the - // top-level frame. - // - // The rewrite changes the reported result, not the journal. revm decides - // commit-or-revert from the frame's original instruction result, before the - // `FrameResult` ever reaches this hook, so a frame that ran to a successful exit is - // already committed and stays committed under the rewritten Revert. - // - // Under REX7 an exceed the frame latched while it ran was already absorbed at the frame's - // settlement point, ahead of the journal decision, so what reaches here is a first - // detection by this hook's own `check_limit` — the frame's usage weighed against its - // caller's budget, after the merge. That question is only answerable here, so the absorb - // for it stays here on every spec. let limit_check = self.check_limit(); + + // The peek and this check are one question asked on either side of the merge. Whenever the + // merge the peek was asked about is the merge that happened — which is every frame return + // on a frozen spec, and every REX7 one the peek did not itself rewrite — the two readings + // must be identical, down to the reported `limit` and `used`. This is what stands between + // the pre-pop decision and a drift in what counts as a frame-local exceed. + debug_assert!( + peeked.is_none_or(|peeked| result.instruction_result().is_ok() != merges_usage || + peeked == limit_check), + "the pre-pop peek and the post-pop check disagreed: {peeked:?} vs {limit_check:?}" + ); + + // Frame-level limit handling (Rex4+): if frame-local, absorb it — clear the exceed flag + // and change to Revert so remaining gas returns to the caller. This works at any depth + // including the top-level frame. Under REX7 the settlement above has already taken the + // frame-local case, and what reaches here is a second reading of a caller that the discard + // could not bring back within its budget. if limit_check.exceeded_limit() && !duplicate_return_frame_result { if limit_check.is_frame_local() { let output = limit_check.revert_data(); diff --git a/crates/mega-evm/src/limit/state_growth.rs b/crates/mega-evm/src/limit/state_growth.rs index d2fde879..ea1fab8a 100644 --- a/crates/mega-evm/src/limit/state_growth.rs +++ b/crates/mega-evm/src/limit/state_growth.rs @@ -161,6 +161,13 @@ impl StateGrowthTracker { self.frame_tracker.add_tx_persistent(amount); } + /// [`check_limit`](TxRuntimeLimit::check_limit) as it will read once the current frame has + /// been popped and merged into its caller, computed without popping it. + #[inline] + pub(crate) fn check_limit_after_pop(&self, success: bool) -> super::LimitCheck { + self.check_limit_on(&self.frame_tracker.view_after_pop(success)) + } + /// [`check_limit`](TxRuntimeLimit::check_limit) against an explicit reading of the tracker. /// /// The reading is a parameter so that one body can answer both questions asked of this check: From 428f7134144717bd9c3455f70b6b2074c281936d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 10:49:05 +0800 Subject: [PATCH 104/208] feat(rex7): hold a frame's journal decision until its result is final MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The last thing that can rewrite a frame's result is not the frame loop. A frame-local resource exceed the frame's own budget could not see — because it is defined against the caller's budget after the merge — lands one step further on, in before_frame_return_result. Committing the journal at the end of the frame loop therefore still left the earlier split in place for that one shape: state committed under a result that reports a revert. So the decision travels. settle_and_commit_frame hands it back instead of carrying it out, the frame loops park it on the EVM, and frame_return_result carries it out after the last rewrite and before the caller resumes — which keeps a creation's set_code inside the same window, with no point at which the caller could observe a contract the frame's result denies. There is never more than one decision outstanding and it never survives the step it was parked for, which the parking site asserts in debug builds. Frozen specs are unchanged: they still tell the journal at classification time, where revm does. --- crates/mega-evm/src/evm/execution.rs | 66 +++++++++++++++++++++------- crates/mega-evm/src/evm/mod.rs | 33 ++++++++++++-- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 14e02d45..8af1e587 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -40,7 +40,7 @@ use revm::{ Inspector, Journal, }; -use super::frame::{classify_frame_action, commit_frame_journal}; +use super::frame::{classify_frame_action, commit_frame_journal, PendingJournal}; use crate::{ constants, dispatch_system_contract_interceptors, is_deposit_like_transaction, is_mega_system_transaction_with, limit::ACCOUNT_INFO_WRITE_SIZE, sent_from_system_address, @@ -626,7 +626,7 @@ impl MegaEvm { frame: &mut EthFrame, action: InterpreterAction, last_callback: impl FnOnce(&mut MegaContext, &FrameInput, &mut FrameResult), - ) -> FrameInitOrResult> { + ) -> (FrameInitOrResult>, Option) { let gas_remaining_before = match (&action, ctx.spec.is_enabled(MegaSpecId::MINI_REX)) { (InterpreterAction::Return(interpreter_result), true) => { Some(interpreter_result.gas.remaining()) @@ -635,7 +635,7 @@ impl MegaEvm { }; let pending = match classify_frame_action(ctx, frame, action) { - ItemOrResult::Item(frame_init) => return ItemOrResult::Item(frame_init), + ItemOrResult::Item(frame_init) => return (ItemOrResult::Item(frame_init), None), ItemOrResult::Result(pending) => pending, }; frame.set_finished(true); @@ -645,9 +645,14 @@ impl MegaEvm { // same on every spec. Frozen specs take it here, the moment the classification is done, // because that is where revm takes it and because what they replay includes the state a // frame leaves behind when a later rewrite fails it: a contract creation that commits and - // is then rewritten into a halt keeps its deployed code. REX7 withholds the decision until - // the settlement below has run, so that the state a frame leaves behind agrees with the - // result its caller is handed. + // is then rewritten into a halt keeps its deployed code. + // + // REX7 withholds the decision, so that the state a frame leaves behind agrees with the + // result its caller is handed. It is withheld past the end of this function, because the + // last thing that can rewrite the result is not here: a frame-local resource exceed + // detected only once the frame's usage has been weighed against its caller's budget lands + // in `before_frame_return_result`, one step further on. The caller hands the decision back + // to `commit_frame_journal` there, still ahead of the caller resuming. let mut deferred_journal = None; if ctx.spec.is_enabled(MegaSpecId::REX7) { deferred_journal = Some(journal); @@ -664,11 +669,7 @@ impl MegaEvm { Self::finalize_frame(ctx, &mut result, FrameExit::Ran, inspector_gas_delta); - if let Some(journal) = deferred_journal { - commit_frame_journal(ctx, journal, &result); - } - - ItemOrResult::Result(result) + (ItemOrResult::Result(result), deferred_journal) } } @@ -1411,6 +1412,26 @@ impl MegaEvm where DB: Database, { + /// Parks a frame's journal decision until `frame_return_result`, one step of revm's execution + /// loop later. + /// + /// That step is the only thing that runs in between, and it is where the last rewrite of the + /// frame's result happens — a frame-local resource exceed the frame's own budget could not see, + /// because it is defined against the caller's budget after the merge. Holding the decision + /// across it is what lets a frame that reports a revert have reverted. + /// + /// A frame that suspended into a child frame parks nothing and clears whatever a previous + /// frame left, which is the invariant the assertion states: there is never more than one + /// decision outstanding, and it never survives the step it was parked for. + #[inline] + fn hold_deferred_journal(&mut self, pending: Option) { + debug_assert!( + self.deferred_journal.is_none(), + "a frame's journal decision outlived the step it was parked for" + ); + self.deferred_journal = pending; + } + /// Everything `frame_init` decides — whether a frame is built at all, and what result stands /// in for it when it is not — with the refusal left unsettled. /// @@ -1681,7 +1702,10 @@ where // After frame_run instructions Hook Self::after_frame_run_instructions(context, frame, &mut action)?; - Ok(Self::settle_and_commit_frame(context, frame, action, |_, _, _| {})) + let (outcome, deferred_journal) = + Self::settle_and_commit_frame(context, frame, action, |_, _, _| {}); + self.hold_deferred_journal(deferred_journal); + Ok(outcome) } fn frame_return_result( @@ -1700,6 +1724,15 @@ where ctx.additional_limit.borrow_mut().before_frame_return_result::(&mut result); } + // REX7: the journal decision the frame's classification reached, carried out now — after + // the last thing that can rewrite the result, and before the caller resumes. A creation's + // `set_code` therefore still lands inside this window, with no point at which the caller + // could observe a deployed contract the frame's result denies. Frozen specs carry nothing + // here; they told the journal at classification time. + if let Some(pending) = self.deferred_journal.take() { + commit_frame_journal(&mut self.inner.ctx, pending, &result); + } + // Call the inner frame_return_result function to return the frame result. let ret = self.inner.frame_return_result(result)?; @@ -1910,9 +1943,12 @@ where // Apply additional limits and storage gas cost Self::after_frame_run_instructions(ctx, frame, &mut action)?; - Ok(Self::settle_and_commit_frame(ctx, frame, action, |ctx, frame_input, frame_result| { - frame_end(ctx, inspector, frame_input, frame_result); - })) + let (outcome, deferred_journal) = + Self::settle_and_commit_frame(ctx, frame, action, |ctx, frame_input, frame_result| { + frame_end(ctx, inspector, frame_input, frame_result); + }); + self.hold_deferred_journal(deferred_journal); + Ok(outcome) } } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 3894d0cd..dec2311b 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -117,6 +117,20 @@ pub struct MegaEvm { /// this view from what execution actually uses. The supported way to change configuration /// is to rebuild the EVM from a reconfigured context. mega_cfg: CfgEnv, + /// The journal decision a frame's classification reached and has not yet carried out (REX7). + /// + /// A frame's result can still be rewritten after the frame loop has produced it — by a late + /// frame-local resource exceed, which is only detectable once the frame's usage has been + /// weighed against its caller's budget. So under REX7 the decision travels from the frame + /// loop that took it to `frame_return_result`, which is past the last rewrite and still ahead + /// of the caller resuming. Frozen specs carry nothing here: they tell the journal at the + /// moment of classification, where revm does. + /// + /// Set by exactly one producer (the frame loops, both of which route through + /// `settle_and_commit_frame`) and taken by exactly one consumer, on the very next step of + /// revm's execution loop. It is `None` outside that one-step window, which + /// `settle_and_commit_frame` asserts in debug builds. + deferred_journal: Option, } impl core::fmt::Debug @@ -173,6 +187,7 @@ impl MegaEvm MegaEvm { self.inner.instruction, self.inner.precompiles, ); - MegaEvm { inner, inspect: true, mega_cfg } + MegaEvm { inner, inspect: true, mega_cfg, deferred_journal: None } } /// Creates a new `MegaETH` EVM instance with the inspector disabled at runtime. @@ -218,7 +233,7 @@ impl MegaEvm { self.inner.instruction, self.inner.precompiles, ); - MegaEvm { inner, inspect: false, mega_cfg } + MegaEvm { inner, inspect: false, mega_cfg, deferred_journal: None } } /// Sets the transaction runtime limits for the EVM. @@ -230,7 +245,12 @@ impl MegaEvm { precompiles: self.inner.precompiles, frame_stack: self.inner.frame_stack, }; - Self { inner, inspect: self.inspect, mega_cfg: self.mega_cfg } + Self { + inner, + inspect: self.inspect, + mega_cfg: self.mega_cfg, + deferred_journal: self.deferred_journal, + } } /// Adds or overrides dynamic precompiles in the EVM. @@ -257,7 +277,12 @@ impl MegaEvm { precompiles, frame_stack: self.inner.frame_stack, }; - Self { inner, inspect: self.inspect, mega_cfg: self.mega_cfg } + Self { + inner, + inspect: self.inspect, + mega_cfg: self.mega_cfg, + deferred_journal: self.deferred_journal, + } } } From e7535e24b986961dfcec35252b29614fc2e2f2a0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 10:49:14 +0800 Subject: [PATCH 105/208] test(rex7): pin the late frame-local exceed end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Natural traffic produces no instance of the shape: a child frame is pushed with 98% of its caller's remaining budget, so merging a child that stayed inside its own budget cannot push the caller past its own. The one charge that breaks that arithmetic is REX6's creator nonce bump, which lands on the caller's lane after the child's budget has already been computed — one account-info write, so it can only tip the balance when 2% of the caller's remaining data-size budget is under 40 bytes. The fixture puts a caller there with an explicit runtime limit and has it CREATE a contract whose deployed code fills the child's budget almost exactly. Four tests: the fixture's own control, with room to spare, which pins the usage every budget in the construction is derived from; the settlement itself — the frame reverts, its usage is discarded rather than merged, its code is not deposited, and its caller carries on; the receipt of that same *successful* transaction carrying no log from the reverted frame, which strip_logs_if_not_ success cannot be responsible for because it returns a success untouched; and REX6 taking the frozen path, where the caller is failed by its child's exceed rather than told about it. --- .../mega-evm/tests/rex7/late_frame_local.rs | 230 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 231 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/late_frame_local.rs diff --git a/crates/mega-evm/tests/rex7/late_frame_local.rs b/crates/mega-evm/tests/rex7/late_frame_local.rs new file mode 100644 index 00000000..8879e079 --- /dev/null +++ b/crates/mega-evm/tests/rex7/late_frame_local.rs @@ -0,0 +1,230 @@ +//! REX7: a frame-local exceed that only becomes visible once the frame has been merged. +//! +//! A per-frame budget is the frame's usage weighed against its *caller's* budget after the merge, +//! so a frame can overrun one with nothing having noticed while it ran. The frame return is where +//! that is first detectable, and through REX6 it is detected one step too late to act on: the +//! merge has already happened on the frame's original classification, and so has the journal +//! decision. What the caller is handed is a revert over usage that was kept and state that was +//! committed. +//! +//! REX7 asks the question before the pop and writes the answer onto the frame's result first, so +//! all three follow one classification — the caller is told the frame reverted, the pop discards +//! the frame's usage the way it discards any reverting frame's, and the journal rolls the frame's +//! state back. +//! +//! # The construction +//! +//! Natural traffic produces no instance of this: a child frame is pushed with 98% of its caller's +//! remaining budget, so merging a child that stayed inside its own budget cannot push the caller +//! past its own. The one charge that breaks that arithmetic is REX6's creator nonce bump, which is +//! charged to the *caller's* lane after the child's budget has already been computed from the +//! caller's remaining. It costs one account-info write, so it can only tip the balance when 2% of +//! the caller's remaining data-size budget is under 40 bytes — a caller with under two kilobytes +//! left. The transaction below puts one there with an explicit runtime limit, and has it CREATE a +//! contract whose deployed code fills the child's budget almost exactly. +//! +//! Frame budgets, with `tx_data_size_limit` at 1171 and 150 bytes of intrinsic usage: +//! +//! | frame | budget | usage | +//! | ---------------- | ------ | ---------------------------------------------- | +//! | `CONTRACT` | 1 021 | 0 while the call is out | +//! | `CALLEE` | 1 000 | 40 — the creator nonce bump, charged after push | +//! | the constructor | 980 | 970 = 40 account + 32 log + 898 deployed code | +//! +//! 40 + 970 = 1 010 > 1 000: the constructor overran `CALLEE`'s budget, and nothing could have +//! seen it before the merge. + +use crate::common::{transact, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::bytecode::opcode::{CALL, CREATE, ISZERO, LOG0, MSTORE, PUSH0, RETURN, SSTORE, STOP}; + +/// `CALLEE` stores 1 here when the CREATE it ran came back as a failure. +const CREATE_FAILED_SLOT: u64 = 0x11; +/// `CONTRACT` stores the CALL's own success flag here. +const CALL_RESULT_SLOT: u64 = 0x12; + +/// Deployed code length, chosen so the constructor's frame lands just under its own budget and +/// just over its caller's — see the table in the module docs. +const DEPLOYED_CODE_LEN: u16 = 898; +/// Leaves `CONTRACT`'s frame 1 021 bytes of data-size budget, after 150 bytes of intrinsic usage. +const TX_DATA_SIZE_LIMIT: u64 = 1171; + +/// Ample: the deposit alone is nearly 180 000 gas, and `MegaETH` charges storage gas on top. +const FORWARDED_GAS: u64 = 50_000_000; + +/// Emits a log and returns [`DEPLOYED_CODE_LEN`] zero bytes of runtime code. +/// +/// The log is what makes this a statement about receipts: it is emitted by a frame that runs to a +/// successful exit and is then failed by the merge. +fn constructor_code() -> Vec { + let mut code = vec![PUSH0, PUSH0, LOG0, 0x61]; + code.extend_from_slice(&DEPLOYED_CODE_LEN.to_be_bytes()); + code.extend_from_slice(&[PUSH0, RETURN]); + code +} + +/// Runs the CREATE, records whether it failed, and emits a log of its own. +fn callee_code() -> Bytes { + let constructor = constructor_code(); + let size = constructor.len(); + // `MSTORE` writes the pushed word right-aligned, so the constructor sits at the tail of the + // first memory word. + let offset = 32 - size; + BytecodeBuilder::default() + .push_bytes(&constructor) + .append(PUSH0) + .append(MSTORE) + .push_number(size as u64) + .push_number(offset as u64) + .append(PUSH0) + .append(CREATE) + .append(ISZERO) + .push_number(CREATE_FAILED_SLOT) + .append(SSTORE) + .append_many([PUSH0, PUSH0, LOG0]) + .append(STOP) + .build() +} + +/// Calls [`CALLEE`] and records whether that call survived. +fn contract_code() -> Bytes { + BytecodeBuilder::default() + .append(PUSH0) // retSize + .append(PUSH0) // retOffset + .append(PUSH0) // argsSize + .append(PUSH0) // argsOffset + .append(PUSH0) // value + .push_address(CALLEE) + .push_number(FORWARDED_GAS) + .append(CALL) + .push_number(CALL_RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build() +} + +/// The address the CREATE would deploy to: `CALLEE`'s first creation. +fn created_address() -> Address { + CALLEE.create(0) +} + +fn run(spec: MegaSpecId, data_size_limit: u64) -> Outcome { + let db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, contract_code()) + .account_code(CALLEE, callee_code()); + transact(spec, db, EvmTxRuntimeLimits::from_spec(spec).with_tx_data_size_limit(data_size_limit)) +} + +fn deployed_code_len(outcome: &Outcome) -> usize { + outcome + .state + .get(&created_address()) + .and_then(|account| account.info.code.as_ref()) + .map(|code| code.original_bytes().len()) + .unwrap_or(0) +} + +/// With room to spare, the same transaction deploys: this is the fixture's own control, and it +/// pins the usage the budgets in the module docs are derived from. +#[test] +fn test_the_same_transaction_deploys_when_the_caller_has_room() { + let outcome = run(MegaSpecId::REX7, u64::MAX); + + assert!(outcome.is_success(), "control: {:?}", outcome.result); + assert_eq!( + outcome.storage_value(CALLEE, U256::from(CREATE_FAILED_SLOT)), + U256::ZERO, + "control: the CREATE must succeed when nothing is binding", + ); + assert_eq!( + deployed_code_len(&outcome), + usize::from(DEPLOYED_CODE_LEN), + "control: the constructor's code must be deployed", + ); + assert_eq!( + outcome.data_size, 1232, + "control: 150 intrinsic + 40 nonce bump + 970 constructor + 32 CALLEE log \ + + 40 CALLEE store" + ); + assert_eq!(outcome.result.logs().len(), 2, "control: both logs must reach the receipt"); +} + +/// The whole of the settlement, on one transaction: the frame reverts, its usage is discarded, its +/// state is rolled back, and its caller carries on. +#[test] +fn test_a_late_frame_local_exceed_reverts_the_frame_and_discards_its_usage() { + let outcome = run(MegaSpecId::REX7, TX_DATA_SIZE_LIMIT); + + assert!(outcome.is_success(), "the transaction itself must survive: {:?}", outcome.result); + assert_eq!( + outcome.storage_value(CALLEE, U256::from(CREATE_FAILED_SLOT)), + U256::from(1), + "the constructor's frame must come back to its caller as a failure", + ); + assert_eq!( + outcome.storage_value(CONTRACT, U256::from(CALL_RESULT_SLOT)), + U256::from(1), + "and its caller must be free to carry on — the exceed was the constructor's", + ); + assert_eq!( + deployed_code_len(&outcome), + 0, + "a frame that reports a revert has reverted: no code may be deposited", + ); + assert_eq!( + outcome.state.get(&CALLEE).map(|account| account.info.nonce), + Some(1), + "the creator's nonce bump survives the child's revert, which is why it is charged to the \ + creator's own lane and why this shape exists at all", + ); + assert_eq!( + outcome.data_size, 302, + "150 intrinsic + 40 nonce bump + 32 CALLEE log + 40 CALLEE store + 40 CONTRACT store: \ + the reverted frame's 970 bytes are discarded, not merged", + ); +} + +/// The receipt of a *successful* transaction carries no log from the frame the merge failed. +/// +/// `strip_logs_if_not_success` cannot be what removed it: that function returns a `Success` result +/// untouched. What removed it is the journal decision, which now follows the frame's final result +/// — the same rollback that left no deployed code behind. This is the pin that the strip is a +/// no-op under REX7 rather than the thing holding the receipt together. +#[test] +fn test_a_reverted_frames_log_never_reaches_a_successful_receipt() { + let outcome = run(MegaSpecId::REX7, TX_DATA_SIZE_LIMIT); + + assert!(outcome.is_success(), "the strip does nothing to a success: {:?}", outcome.result); + let logs = outcome.result.logs(); + assert_eq!(logs.len(), 1, "exactly one log survives: {logs:?}"); + assert_eq!(logs[0].address, CALLEE, "and it is the one the surviving frame emitted"); +} + +/// Frozen specs keep the split, so the same transaction ends differently there. +/// +/// REX6 merges the constructor's usage on its original classification, then rewrites the result; +/// the caller resumes over its own budget and is failed on the spot. The caller's call fails, its +/// store never runs, and its log never reaches the receipt — none of which happens under REX7. +#[test] +fn test_frozen_specs_fail_the_caller_instead() { + let outcome = run(MegaSpecId::REX6, TX_DATA_SIZE_LIMIT); + + assert!(outcome.is_success(), "the top-level frame still returns: {:?}", outcome.result); + assert_eq!( + outcome.storage_value(CONTRACT, U256::from(CALL_RESULT_SLOT)), + U256::ZERO, + "REX6: the caller is failed by its child's exceed, not just told about it", + ); + assert_eq!( + outcome.storage_value(CALLEE, U256::from(CREATE_FAILED_SLOT)), + U256::ZERO, + "REX6: the caller never gets to record the failure", + ); + assert_eq!(outcome.result.logs().len(), 0, "REX6: nothing survives to the receipt"); + assert_eq!(outcome.data_size, 150, "REX6: the whole call frame's usage is discarded with it"); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 2759ca9f..974c444d 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -89,6 +89,7 @@ mod guard_pass_static_gas; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; +mod late_frame_local; mod measured_inspector; mod modexp_gas; mod opcode_set_parity; From c4c8b640f77e729c45a61b1611ff477896b946f0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 11:01:16 +0800 Subject: [PATCH 106/208] docs: state that a late frame-local exceed is decided before the merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rex7 page's frame-state rule carried a carve-out for the one exceed a frame cannot latch — the one defined against its caller's budget after the merge — and that carve-out is now gone. Its own section says what replaced it: the exceed is determined before the merge, over the reading the merge would produce, and the frame's result is rewritten first, so the merge discards its usage, the journal rolls its state back, and the caller is free to continue. The developer-impact and compatibility sections gain the caller that Rex7 keeps alive and Rex6 fails, with the arithmetic that says how narrow the shape is. Per-Call-Frame Runtime Budgets gains the fact underneath all of it: a frame's own budget is not the only one it can overrun. The agent guides gain the extra station the journal decision travels to and the window anything new that rewrites a frame's result has to land inside, and the limit guide gains where the post-merge question is asked and what to extend when adding a dimension or a lane. --- AGENTS.md | 6 ++++- crates/mega-evm/src/evm/AGENTS.md | 5 +++- crates/mega-evm/src/limit/AGENTS.md | 7 +++++- crates/mega-evm/src/limit/limit.rs | 8 +++--- docs/spec/evm/compute-gas.md | 2 +- docs/spec/evm/resource-limits.md | 11 +++++++++ docs/spec/upgrades/rex7.md | 38 ++++++++++++++++++++++++++--- 7 files changed, 66 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e616683d..7e0e9e17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,7 +143,11 @@ A transaction can be halted by exceeding either limit. revm assembles a frame's result, decides its journal checkpoint and — for a contract creation — runs the deposit predicates and writes the code all inside `EthFrame::process_next_action`, and runs the inspector's last mutating callback after that function returns. `evm/frame.rs` splits it: `classify_frame_action` decides what the frame's result is and records the journal decision it reached as a `FrameJournalVerdict`; `commit_frame_journal` carries that decision out. Between the two run the frozen post-action charge, the inspector's `frame_end`, and `AdditionalLimit::finalize_frame` — the single point a frame's outcome is settled (final classification, executed/destroyed split, frame-init refusal booking, gas rescue, and the REX7 frame-local absorb). -Under REX7 the journal decision is taken after that settlement, so a frame's state agrees with the result its caller is handed; frozen specs take it where revm does, right after the classification, because what they replay includes the state a frame leaves behind when a later rewrite fails it. +Under REX7 the journal decision is taken later still — the frame loops park it on `MegaEvm::deferred_journal` and `frame_return_result` carries it out, after `AdditionalLimit::before_frame_return_result` (the last thing that can rewrite a frame's result) and before the caller resumes — so a frame's state agrees with the result its caller is handed and a creation's `set_code` still lands with no observation window. +Frozen specs take it where revm does, right after the classification, because what they replay includes the state a frame leaves behind when a later rewrite fails it. +The rewrite that made the extra station necessary is the late frame-local exceed: a per-frame budget is the frame's usage weighed against its *caller's* budget after the merge, so a frame can overrun one with nothing having latched it. +REX7 asks that question before the pop, through `AdditionalLimit::peek_check_limit_after_pop` over `FrameLimitTracker::view_after_pop`, and rewrites the frame to a revert first; the pop then discards the frame's usage the way it discards any reverting frame's, and the caller carries on. +The pre-pop reading and the post-pop `check_limit()` are cross-checked against each other on every frame return in debug builds, on every spec — that assertion is what stands between the early decision and a drift in what counts as a frame-local exceed. Both frame loops (`frame_run` / `inspect_frame_run`) and both frame-init paths (`frame_init` / `inspect_frame_init`) run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. `classify_frame_action` and `commit_frame_journal` together are a re-ordering of upstream code with no type-level tie to it, so a revm bump has to re-audit them; the debug assertion in `classify_create_return` catches only the one drift class it names. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 5bc6f12c..df4f1e33 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -7,7 +7,7 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - `mod.rs`: `MegaEvm` wrapper, inspector toggling, execution convenience APIs. - `context.rs`: execution context composition and state wiring. - `execution.rs`: transaction execution flow, the two frame loops and the two frame-init paths, and result shaping. -- `frame.rs`: revm's frame-action processing, split so the journal decision can be withheld until the frame's settlement has run — `classify_frame_action` decides the result, `commit_frame_journal` carries the decision out. +- `frame.rs`: revm's frame-action processing, split so the journal decision can be withheld until the frame's result is final — `classify_frame_action` decides the result, `commit_frame_journal` carries the decision out. - `factory.rs`: `MegaEvmFactory` builder for context and external env wiring. - `instructions.rs`: spec-layered opcode table and extension wrappers. - `host.rs`: host overrides for volatile tracking, oracle reads, SALT gas hooks. @@ -31,6 +31,9 @@ MegaEVM execution core that wraps revm/op-revm with MegaETH instruction tables, - A rewrite the last mutating callback makes to a frame result's gas is booked from the frame's settlement point rather than from the callback boundary, because whether it moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. The gas an intercepting callback puts into a synthetic outcome travels through that same lane. One shape is refused outright rather than measured: a `create_end` (or the `frame_end` after it) turning a failed creation into a successful one — see `reject_forbidden_create_rewrite`, and the verdict in `frame.rs` that gives such a rewrite no code to deposit even if the refusal were removed. +- Under REX7 a frame's journal decision travels: the frame loops park it on `MegaEvm::deferred_journal` and `frame_return_result` carries it out, after `AdditionalLimit::before_frame_return_result` — the last thing that can rewrite a frame's result — and before the caller resumes. + There is never more than one decision outstanding and it never survives the step it was parked for; `hold_deferred_journal` asserts that. + Anything new that can rewrite a frame's result has to land inside that window, or it reopens the split the deferral closed. - Both frame loops and both frame-init paths run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. Add to the shared body, not to one copy: `tests/rex7/frame_loop_parity.rs` compares the two on every frame outcome, state included, and is what a one-sided edit fails. diff --git a/crates/mega-evm/src/limit/AGENTS.md b/crates/mega-evm/src/limit/AGENTS.md index b8403d5b..1b7fa316 100644 --- a/crates/mega-evm/src/limit/AGENTS.md +++ b/crates/mega-evm/src/limit/AGENTS.md @@ -20,6 +20,10 @@ Resource metering subsystem for transaction and frame limits across compute gas, - `AdditionalLimit::finalize_frame` is the single point a frame's outcome is settled — the destroyed-remainder booking, the frame-init refusal booking, the gas rescue, and the REX7 frame-local absorb — and it runs after the last callback that can rewrite the frame's classification and before the journal decision. Put a new frame-exit settlement there, not in a lifecycle hook on either side of it. The pops stay in `before_frame_return_result`: the paths that reach a caller without ever running a frame would double-pop. +- A frame-local exceed a frame could not latch — the one defined against its *caller's* budget after the merge — is settled in `before_frame_return_result` instead, and under REX7 before the pops rather than after them. + `peek_check_limit_after_pop` answers the post-merge question over `FrameLimitTracker::view_after_pop`, so the reading is the merged one and only the timing moves; the pop that follows reads a revert and discards the frame's usage. + Every dimension answers it with its own `check_limit` body over a `FrameLimitView`, and the two readings are cross-checked against each other on every frame return in debug builds. + Add a dimension's `check_limit_after_pop` when adding a dimension, and extend `view_after_pop` when adding a lane the pop moves. - Synthetic frame results still require empty-frame pushes for stack alignment. - Gas rescue must exclude any system-granted stipend gas. - Revert paths must roll back discardable usage for data/KV/state growth trackers. @@ -29,7 +33,8 @@ Resource metering subsystem for transaction and frame limits across compute gas, - Do not encode frame-local exceeds as halts. - They must be reverts with bounded payload. - Do not read tracker totals after an exceeded-limit revert path unless using tracker-owned finalized APIs. -- Do not run a fresh `check_limit()` inside `finalize_frame`: a per-frame exceed is defined by the frame's usage weighed against its *caller's* budget after the merge, which is only answerable once the frame is back with its caller. +- Do not run a fresh `check_limit()` inside `finalize_frame`: a per-frame exceed is defined by the frame's usage weighed against its *caller's* budget after the merge, which nothing at that point can read. + The pre-pop settlement in `before_frame_return_result` is where that question belongs, and it reads the merged numbers rather than the current ones. - Avoid duplicating limit checks inside opcode handlers when the tracker already enforces the same dimension. ## WHERE TO LOOK diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index b446692b..dd14b4b2 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -961,10 +961,10 @@ impl AdditionalLimit { /// popped and merged into its caller — asked before the merge, and latching nothing. /// /// A per-frame budget is defined by the frame's usage weighed against its *caller's* budget - /// after the merge, so that is the only question worth asking at a frame return. Today it can - /// only be answered after the merge has already happened, which is too late for the answer to - /// change what the merge does with the frame's usage or what the journal does with its state. - /// This asks the same question one step earlier. + /// after the merge, so that is the only question worth asking at a frame return. Asked where + /// its numbers naturally appear — after the pop — it comes too late for the answer to change + /// what the merge did with the frame's usage or what the journal did with its state. This asks + /// the same question one step earlier, which is the whole of why it exists. /// /// "The same question" is meant literally: every dimension runs its own `check_limit` body, /// in `check_limit`'s order, over a reading of its tracker taken as if the pop had happened. diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 7f5629c3..a10be043 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -459,7 +459,7 @@ A node commits or reverts a frame's journal checkpoint from the frame's instruct Under Rex7, a node MUST decide a frame's journal outcome from the frame's final result — the result after every settlement and every rewrite the node applies at that frame's exit — so a frame that reports a revert has reverted. The rule reaches every exceed the frame itself latched while it ran. -It does not reach one first detected on the way out to the caller, which weighs the frame's usage against the caller's budget after the merge and is therefore only answerable once the frame is back with its caller; that one is absorbed there, as on every spec. +It reaches one first detected on the way out to the caller too — that one weighs the frame's usage against the caller's budget after the merge, so under Rex7 a node determines it before the merge and rewrites the frame's result first; see [Per-Call-Frame Runtime Budgets](resource-limits.md#per-call-frame-runtime-budgets).
diff --git a/docs/spec/evm/resource-limits.md b/docs/spec/evm/resource-limits.md index 87bc0a4f..ae04d29e 100644 --- a/docs/spec/evm/resource-limits.md +++ b/docs/spec/evm/resource-limits.md @@ -173,6 +173,17 @@ Only total gas (the standard EVM gas parameter in CALL-like opcodes) remains und If a child call frame exceeds its local budget, it MUST revert with `MegaLimitExceeded(uint8 kind, uint64 limit)`. The parent call frame MAY continue execution. +A frame's own budget is not the only one it can overrun: its usage is merged into its caller's when it returns, and that merge can put the caller past its budget even though the frame stayed inside its own. +Through [Rex6](../upgrades/rex6.md), a node detects that after the merge — the frame is told to revert, its usage is carried up as a successful frame's is, and the caller is failed by it at the caller's next resource check. + +
+Rex7 (unstable): the exceed is determined before the merge + +Under Rex7, a node MUST determine such an exceed before merging, over the reading the merge would produce, and MUST rewrite the frame's result to the same frame-local revert before merging. +The merge then discards the frame's usage as it discards any reverting frame's, the frame's state is rolled back with it, and the caller MUST be free to continue. + +
+ The top-level call frame's budget MUST equal the transaction limit minus any resource usage already recorded before the first frame begins. These deductions include transaction-only intrinsic usage and any DB-dependent pre-execution usage that is resolved before the first frame starts. Each resource dimension deducts only the pre-frame items relevant to it: diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index 39cdc712..f446eed0 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -286,13 +286,37 @@ A frame that ran to a successful exit and is then rewritten into a frame-local r Under Rex7, a node MUST decide a frame's journal outcome from the frame's **final** result — the result after every settlement and every rewrite the node applies at that frame's exit. A frame that reports a revert MUST have its state reverted; a contract creation that reports anything other than success MUST deposit no code. -This closes the split for every exceed the frame itself latched while it ran. -It does not reach an exceed first detected on the way out to the caller: that check weighs the frame's usage against the caller's budget, after the frame's usage has been merged into it, so it is answerable only once the frame is already back with its caller. -Such an exceed is absorbed there, on every spec, and the frozen split remains for it. +This closes the split for every exceed the frame itself latched while it ran, and — with the rule below — for the one it cannot latch. A node MUST NOT deposit code that its create-return predicates rejected, whatever a later rewrite says the result is. The bytes a deployment writes MUST be the bytes those predicates approved. +### A Frame-Local Exceed Detected on the Way Out + +#### Previous behavior + +A frame-local budget is the frame's usage weighed against its **caller's** budget once the frame's usage has been merged into it, so a frame can overrun one with nothing having observed it while the frame ran. +Such an exceed is first detectable at the frame return, and a node detects it there — after the merge, and after the journal decision of the previous section. +The frame is told to revert, but its usage was merged into its caller as a successful frame's is, and its state was committed. +The caller then carries the frame's usage against its own budget and is failed by it at its next resource check. + +#### New behavior + +Under Rex7, a node MUST determine a frame-local exceed of this kind **before** it merges the frame's usage into the caller, evaluating it over the state the merge would produce. + +The reading a node evaluates MUST be the post-merge reading: the caller's budget, its usage with the frame's merged into it, and the transaction's usage as the merge would leave it. +What changes is when the answer is taken, not what is asked. + +A node that finds such an exceed MUST rewrite the frame's result to a frame-local revert before merging. +The merge, the journal decision and the result then follow one classification: + +- the caller is told the frame reverted, with the same `MegaLimitExceeded(kind, limit)` payload any frame-local exceed carries; +- the merge MUST discard the frame's usage exactly as it discards a reverting frame's — the reverted lanes of every dimension are dropped, and only usage that survives a revert is carried up; +- the frame's state MUST be rolled back, by the rule of the previous section. + +The caller MUST then be free to continue. +A node MUST NOT fail the caller on the frame's discarded usage. + ## Developer Impact Rex7 is not scheduled on any network. @@ -317,6 +341,10 @@ Whether the creation succeeds, what it deploys, and the receipt it produces are A frame that reports a frame-local revert leaves no state behind under Rex7, where under Rex6 a frame that had already exited successfully kept its writes. A caller that read state written by such a frame after absorbing its revert sees nothing under Rex7. +A caller whose child overran the caller's own budget survives under Rex7 and is failed under Rex6. +The child reverts either way; what changes is that its usage no longer follows it up. +Reaching this at all needs the caller's remaining budget in one dimension to be small enough that 2% of it is under one unit of that dimension's charge, so a caller with room to spare cannot be put there by a child that stayed inside its own budget. + ## Safety and Compatibility Rex7 changes nothing about how blocks under earlier specs are executed. @@ -334,6 +362,10 @@ On a precompile that fails before performing work, Rex7 enforcement is deliberat Rex7 rolls back state Rex6 keeps on one path: a frame that exited successfully and was then rewritten into a frame-local revert. The rewrite itself is unchanged; what changes is that the journal follows it. +Rex7 keeps a caller alive that Rex6 fails, on one path: a frame whose merge would overrun its caller's budget. +Rex6 merges the usage first and fails the caller on it; Rex7 rewrites the frame first, so the merge discards that usage the way it discards any reverting frame's. +The frame's own outcome — a frame-local revert with the same payload — is the same on both. + Rex7 reports less compute gas than Rex6 on one path: a contract creation that fails at its frame exit, whose code-deposit compute gas Rex6 records and Rex7 does not. Enforcement is looser there by the same amount, and deliberately so — the EVM charges that amount only for a deposit that happens, so enforcing it against a creation that failed would bind the compute limit with gas nobody spent. From a51fc70524801796451d5d40b8d7e2202c5553df Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 12:40:01 +0800 Subject: [PATCH 107/208] perf(limit): hand the check bodies a reading to ask, not one to carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routing the live limit checks through a materialized view cost the per-opcode hot path: the frame's budget was copied into a struct and pushed through a reference, and the transaction's net usage was computed eagerly whether the body reached it or not. Pre-REX4 specs paid the most, because their bodies skip the frame check entirely and so never used the half they were now paying to build — `interpreter_hotloop/mini_rex` measured +6.10% against base, and the three specs whose bodies do use it measured −7.7%, which is the same effect read from the other side. So the reading becomes a trait rather than a value. Each dimension's body is still written once and still specialized over both readings — the tracker as it stands and the reading a pending pop would produce — but the live specialization now reads the tracker where the body asks, which is the code that was there before any of this. Six paired rounds, base↔head interleaved on prebuilt binaries: `interpreter_hotloop` is flat on every spec (mini_rex +0.01%, rex4 −0.17%, rex5 −0.03%, rex6 −0.17%, rex7 +0.91%), inside a pinned-upstream noise floor of −1.71%..+0.73%. The unit test that pins the two readings against each other now compares them through the trait rather than field for field, which is what the bodies consume. --- crates/mega-evm/src/limit/compute_gas.rs | 12 +-- crates/mega-evm/src/limit/data_size.rs | 10 +-- crates/mega-evm/src/limit/frame_limit.rs | 97 ++++++++++++++++------- crates/mega-evm/src/limit/kv_update.rs | 10 +-- crates/mega-evm/src/limit/mod.rs | 2 +- crates/mega-evm/src/limit/state_growth.rs | 8 +- 6 files changed, 89 insertions(+), 50 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index ca566ff9..ee138860 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -276,7 +276,7 @@ impl ComputeGasTracker { /// predicate to drift. #[inline] pub(crate) fn check_limit_with_extra(&self, extra: u64) -> LimitCheck { - self.check_limit_with_extra_on(&self.frame_tracker.view(), extra) + self.check_limit_with_extra_on(&self.frame_tracker, extra) } /// [`check_limit`](TxRuntimeLimit::check_limit) as it will read once the current frame has @@ -293,13 +293,13 @@ impl ComputeGasTracker { /// what it says now, and what it will say once a returning frame has been merged into its /// caller. A frame return needs the second answer before the merge happens, and a second copy /// of the predicates would be free to drift from the first. - pub(crate) fn check_limit_with_extra_on( + pub(crate) fn check_limit_with_extra_on( &self, - view: &super::FrameLimitView, + r: &R, extra: u64, ) -> LimitCheck { if self.rex4_enabled { - let frame_check = view.would_exceed_frame_limit(LimitKind::ComputeGas, extra); + let frame_check = r.frame_check(LimitKind::ComputeGas, extra); if frame_check.exceeded_limit() { return frame_check; } @@ -314,12 +314,12 @@ impl ComputeGasTracker { // reported `used` is the full settled total, so a halt reason states the usage the // transaction actually ends with. The two coincide on every spec before REX7. let limit = self.tx_limit(); - if view.net_usage().saturating_sub(self.burned).saturating_add(extra) > limit { + if r.net_usage().saturating_sub(self.burned).saturating_add(extra) > limit { LimitCheck::ExceedsLimit { kind: LimitKind::ComputeGas, frame_local: false, limit, - used: view.net_usage().saturating_add(extra), + used: r.net_usage().saturating_add(extra), } } else { LimitCheck::WithinLimit diff --git a/crates/mega-evm/src/limit/data_size.rs b/crates/mega-evm/src/limit/data_size.rs index 1b705112..6559cfce 100644 --- a/crates/mega-evm/src/limit/data_size.rs +++ b/crates/mega-evm/src/limit/data_size.rs @@ -146,9 +146,9 @@ impl DataSizeTracker { /// what it says now, and what it will say once a returning frame has been merged into its /// caller. A frame return needs the second answer before the merge happens, and a second copy /// of the predicates would be free to drift from the first. - pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + pub(crate) fn check_limit_on(&self, r: &R) -> super::LimitCheck { if self.rex4_enabled { - let frame_check = view.exceeds_frame_limit(super::LimitKind::DataSize); + let frame_check = r.frame_check(super::LimitKind::DataSize, 0); if frame_check.exceeded_limit() { return frame_check; } @@ -156,7 +156,7 @@ impl DataSizeTracker { // In Rex4+ during execution, per-frame budgets are derived from remaining TX // budget, so this should only exceed when no frame exists (intrinsic overflow). } - let used = view.net_usage(); + let used = r.net_usage(); let limit = self.frame_tracker.tx_limit(); if used > limit { // Defense-in-depth: pre-REX5, the only mid-execution writer to `tx_entry` is @@ -166,7 +166,7 @@ impl DataSizeTracker { // execution to meter oracle-hint payloads as TX-scoped side-channel cost, so // the invariant is only asserted on pre-REX5 specs. debug_assert!( - !self.rex4_enabled || self.rex5_enabled || !view.has_frame(), + !self.rex4_enabled || self.rex5_enabled || !r.has_frame(), "DataSize TX-level exceeded with active frame — budget invariant violated" ); super::LimitCheck::ExceedsLimit { @@ -217,7 +217,7 @@ impl TxRuntimeLimit for DataSizeTracker { /// (intrinsic usage is recorded in `tx_entry` before the first frame is pushed). /// In pre-Rex4, checks total data size across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - self.check_limit_on(&self.frame_tracker.view()) + self.check_limit_on(&self.frame_tracker) } /// Records the data size of a transaction at the start of execution. diff --git a/crates/mega-evm/src/limit/frame_limit.rs b/crates/mega-evm/src/limit/frame_limit.rs index a7521947..96e24d35 100644 --- a/crates/mega-evm/src/limit/frame_limit.rs +++ b/crates/mega-evm/src/limit/frame_limit.rs @@ -32,14 +32,30 @@ pub(crate) struct CallFrameInfo { charged_parent_update: bool, } -/// The numbers a resource-limit check reads out of a [`FrameLimitTracker`]: the current frame's +/// The two things a resource-limit check reads out of a [`FrameLimitTracker`]: the current frame's /// budget, when there is one, and the transaction's net usage. /// -/// Two of these exist at a frame return — the tracker as it stands, and the tracker as it will -/// stand once the returning frame has been popped and merged into its caller. Handing the check -/// bodies a view rather than the tracker is what lets the second reading be taken *before* the -/// merge without a second copy of the predicates to drift from the first: the same body runs, only -/// its input differs. +/// Two readings exist at a frame return — the tracker as it stands, and the tracker as it will +/// stand once the returning frame has been popped and merged into its caller. Each dimension's +/// check body is written once against this trait and specialized over both, so the pre-merge +/// question can be asked with no second copy of the predicates to drift from the first. +/// +/// A trait rather than a value on purpose: the live reading is the per-opcode hot path, and +/// materializing a struct for it — eagerly computing a net usage the body may not reach, and +/// pushing the frame's budget through a reference — costs measurably more than reading the tracker +/// where the body asks. Monomorphized, the live specialization is the code that was there before +/// the pre-merge reading existed. +pub(crate) trait LimitReading { + /// Whether the current frame has exceeded its frame-local budget, evaluated as if `extra` more + /// usage had already been recorded against it. + fn frame_check(&self, kind: LimitKind, extra: u64) -> LimitCheck; + /// `Σ(persistent + discardable) − Σ refund` across the TX entry and every frame on the stack. + fn net_usage(&self) -> u64; + /// Whether a frame is on the stack — the predicate the TX-level budget invariants assert on. + fn has_frame(&self) -> bool; +} + +/// The reading a pending pop would produce, computed without popping. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct FrameLimitView { /// The current frame's budget, or `None` when no frame is on the stack. @@ -56,30 +72,37 @@ struct FrameBudget { refund: u64, } -impl FrameLimitView { - /// Whether a frame is on the stack — the predicate the TX-level budget invariants assert on. +impl LimitReading for FrameLimitView { #[inline] - pub(crate) fn has_frame(&self) -> bool { - self.frame.is_some() + fn frame_check(&self, kind: LimitKind, extra: u64) -> LimitCheck { + frame_budget_check(self.frame, kind, extra) } - /// The transaction's net usage. #[inline] - pub(crate) fn net_usage(&self) -> u64 { + fn net_usage(&self) -> u64 { self.net_usage } - /// Whether the current frame has exceeded its frame-local budget, evaluated as if `extra` more - /// usage had already been recorded against it. #[inline] - pub(crate) fn would_exceed_frame_limit(&self, kind: LimitKind, extra: u64) -> LimitCheck { - frame_budget_check(self.frame, kind, extra) + fn has_frame(&self) -> bool { + self.frame.is_some() + } +} + +impl LimitReading for FrameLimitTracker { + #[inline] + fn frame_check(&self, kind: LimitKind, extra: u64) -> LimitCheck { + frame_budget_check(self.frame_budget(), kind, extra) + } + + #[inline] + fn net_usage(&self) -> u64 { + Self::net_usage(self) } - /// [`would_exceed_frame_limit`](Self::would_exceed_frame_limit) with nothing extra. #[inline] - pub(crate) fn exceeds_frame_limit(&self, kind: LimitKind) -> LimitCheck { - self.would_exceed_frame_limit(kind, 0) + fn has_frame(&self) -> bool { + self.has_active_frame() } } @@ -302,12 +325,6 @@ impl FrameLimitTracker { }) } - /// What the limit checks read out of this tracker as it stands. - #[inline] - pub(crate) fn view(&self) -> FrameLimitView { - FrameLimitView { frame: self.frame_budget(), net_usage: self.net_usage() } - } - /// What the limit checks will read out of this tracker once the current frame has been popped /// and merged into its caller — computed without popping it. /// @@ -318,7 +335,7 @@ impl FrameLimitTracker { pub(crate) fn view_after_pop(&self, success: bool) -> FrameLimitView { let Some(child) = self.frame_stack.last() else { // Nothing to pop: `pop_frame` on an empty stack changes nothing. - return self.view(); + return FrameLimitView { frame: self.frame_budget(), net_usage: self.net_usage() }; }; let frame = self.frame_stack.len().checked_sub(2).map(|parent_index| { let parent = &self.frame_stack[parent_index]; @@ -649,6 +666,25 @@ mod tests { const ADDR: Address = address!("0000000000000000000000000000000000001234"); + /// Every question a dimension's `check_limit` body can put to a reading. + /// + /// Comparing two readings through this rather than field for field is deliberate: it is what + /// the bodies consume, so a reading that agrees here cannot make a body decide differently. + fn everything_a_check_body_reads(reading: &impl LimitReading) -> (Vec, u64, bool) { + let kinds = [ + LimitKind::DataSize, + LimitKind::KVUpdate, + LimitKind::ComputeGas, + LimitKind::StateGrowth, + ]; + let checks = kinds + .into_iter() + .flat_map(|kind| [0u64, 1, 7].map(move |extra| (kind, extra))) + .map(|(kind, extra)| reading.frame_check(kind, extra)) + .collect(); + (checks, reading.net_usage(), reading.has_frame()) + } + /// The pre-pop reading must be the post-pop reading, exactly. /// /// A frame return decides whether the returning frame overran its caller's budget from @@ -690,8 +726,8 @@ mod tests { let predicted = tracker.view_after_pop(success); tracker.pop_frame(success); assert_eq!( - predicted, - tracker.view(), + everything_a_check_body_reads(&predicted), + everything_a_check_body_reads(&tracker), "{label} (success={success}): the pre-pop reading must equal the post-pop one", ); } @@ -705,7 +741,10 @@ mod tests { let mut tracker = FrameLimitTracker::<()>::new(MegaSpecId::REX7, 10_000); tracker.add_tx_persistent(90); for success in [true, false] { - assert_eq!(tracker.view_after_pop(success), tracker.view()); + assert_eq!( + everything_a_check_body_reads(&tracker.view_after_pop(success)), + everything_a_check_body_reads(&tracker), + ); } } diff --git a/crates/mega-evm/src/limit/kv_update.rs b/crates/mega-evm/src/limit/kv_update.rs index 404bdae6..fd4368ca 100644 --- a/crates/mega-evm/src/limit/kv_update.rs +++ b/crates/mega-evm/src/limit/kv_update.rs @@ -101,9 +101,9 @@ impl KVUpdateTracker { /// what it says now, and what it will say once a returning frame has been merged into its /// caller. A frame return needs the second answer before the merge happens, and a second copy /// of the predicates would be free to drift from the first. - pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + pub(crate) fn check_limit_on(&self, r: &R) -> super::LimitCheck { if self.rex4_enabled { - let frame_check = view.exceeds_frame_limit(super::LimitKind::KVUpdate); + let frame_check = r.frame_check(super::LimitKind::KVUpdate, 0); if frame_check.exceeded_limit() { return frame_check; } @@ -111,11 +111,11 @@ impl KVUpdateTracker { // In Rex4+ during execution, per-frame budgets are derived from remaining TX // budget, so this should only exceed when no frame exists (intrinsic overflow). } - let used = view.net_usage(); + let used = r.net_usage(); let limit = self.frame_tracker.tx_limit(); if used > limit { debug_assert!( - !self.rex4_enabled || !view.has_frame(), + !self.rex4_enabled || !r.has_frame(), "KVUpdate TX-level exceeded with active frame — budget invariant violated" ); super::LimitCheck::ExceedsLimit { @@ -167,7 +167,7 @@ impl TxRuntimeLimit for KVUpdateTracker { /// (intrinsic usage is recorded in `tx_entry` before the first frame is pushed). /// In pre-Rex4, checks total KV updates across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - self.check_limit_on(&self.frame_tracker.view()) + self.check_limit_on(&self.frame_tracker) } /// Records the KV updates at the start of a transaction. diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index 923e996a..072f6b29 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -13,7 +13,7 @@ mod state_growth; mod storage_call_stipend; pub use data_size::*; -pub(crate) use frame_limit::{FrameLimitTracker, FrameLimitView, TxRuntimeLimit}; +pub(crate) use frame_limit::{FrameLimitTracker, LimitReading, TxRuntimeLimit}; pub use inspector_ledger::*; pub use limit::*; diff --git a/crates/mega-evm/src/limit/state_growth.rs b/crates/mega-evm/src/limit/state_growth.rs index ea1fab8a..61b59093 100644 --- a/crates/mega-evm/src/limit/state_growth.rs +++ b/crates/mega-evm/src/limit/state_growth.rs @@ -174,16 +174,16 @@ impl StateGrowthTracker { /// what it says now, and what it will say once a returning frame has been merged into its /// caller. A frame return needs the second answer before the merge happens, and a second copy /// of the predicates would be free to drift from the first. - pub(crate) fn check_limit_on(&self, view: &super::FrameLimitView) -> super::LimitCheck { + pub(crate) fn check_limit_on(&self, r: &R) -> super::LimitCheck { if self.spec.is_enabled(MegaSpecId::REX4) { - let frame_check = view.exceeds_frame_limit(super::LimitKind::StateGrowth); + let frame_check = r.frame_check(super::LimitKind::StateGrowth, 0); if frame_check.exceeded_limit() { return frame_check; } // TX-level fallthrough: catches Rex5 pre-frame authority usage and any // future TX-level state-growth contribution. } - let used = view.net_usage(); + let used = r.net_usage(); let limit = self.frame_tracker.tx_limit(); if used > limit { super::LimitCheck::ExceedsLimit { @@ -239,7 +239,7 @@ impl TxRuntimeLimit for StateGrowthTracker { /// usage — and any frame-level overflow that has already been popped into `tx_entry`. /// For pre-Rex4, checks total net growth across all frames against the TX limit. fn check_limit(&self) -> super::LimitCheck { - self.check_limit_on(&self.frame_tracker.view()) + self.check_limit_on(&self.frame_tracker) } /// No-op. From 2fa1b5bb15e98d3fd36ba867e1a8f7fe0992ea92 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 13:32:19 +0800 Subject: [PATCH 108/208] refactor(limit): state the gas conservation law once, as one set of terms The law that ties a transaction's compute total, storage gas, destroyed remainder, minted stipend and inspector adjustments to the envelope it burnt was restated at three sites, each with its own rearrangement and its own hand-written assertion message. `ConservationTerms` holds it once and solves it in both directions; `AdditionalLimit::conservation_terms()` is where every site reads its terms from, so they cannot drift apart. The terminal envelope check gains a reading it did not have: that the reported compute total really is the sum of the enforced and destroyed lanes it is documented to split into. --- crates/mega-evm/src/evm/mod.rs | 47 ++-- crates/mega-evm/src/limit/checkpoint.rs | 4 +- crates/mega-evm/src/limit/conservation.rs | 236 ++++++++++++++++++ crates/mega-evm/src/limit/limit.rs | 128 +++------- crates/mega-evm/src/limit/mod.rs | 2 + crates/mega-evm/tests/rex7/burn_split.rs | 3 +- crates/mega-evm/tests/rex7/common.rs | 9 +- .../tests/rex7/create_code_deposit_charge.rs | 3 +- .../tests/rex7/frame_init_reject_burn.rs | 3 +- .../tests/rex7/guard_pass_static_gas.rs | 3 +- .../mega-evm/tests/rex7/measured_inspector.rs | 34 ++- 11 files changed, 337 insertions(+), 135 deletions(-) create mode 100644 crates/mega-evm/src/limit/conservation.rs diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index dec2311b..baef4b41 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -469,19 +469,27 @@ where /// reports (REX7+; before REX7 there is no destroyed lane and no non-compute lane, so there is /// nothing to reconcile). /// -/// The reported compute total, the `MegaETH` storage gas, and the `CALL_STIPEND` the EVM minted -/// into child frames are the three terms the destroyed remainder is derived from, so once -/// settlement has run they must add back up to the envelope the transaction burnt: +/// This is the conservation law solved for the envelope — +/// [`ConservationTerms::envelope_for`](crate::ConservationTerms::envelope_for) — evaluated against +/// the destroyed remainder the *outcome* reports rather than the one the settlement derived: /// /// ```text -/// compute_gas_used + non_compute_gas − minted_call_stipend − inspector_conjured_gas -/// == total_gas_spent +/// C + S + D − K − I == total_gas_spent /// ``` /// -/// The inspector term is zero unless a rewriting inspector was attached: it is what the -/// measurement shim booked for gas the inspector wrote into an interpreter counter or a frame -/// envelope, which the transaction's own envelope never funded. Subtracting it is what keeps the -/// law stated over the EVM's gas rather than over the EVM's gas plus an inspector's edits. +/// The settlement site solves the same law for `D`, so reading it back in this direction is only +/// a tautology when both sides read the same terms *and* the same envelope. Neither holds here, +/// which is what gives this check its reach: it re-reads the terms after settlement finished and +/// compares against the envelope the receipt ended up carrying. +/// +/// The inspector term `I` is zero unless a rewriting inspector was attached: it is what the +/// measurement shim booked for gas the inspector wrote into an interpreter counter, a frame +/// envelope, or a returning frame's result, none of which the transaction's own envelope funded. +/// Subtracting it is what keeps the law stated over the EVM's gas rather than over the EVM's gas +/// plus an inspector's edits. +/// +/// The reported compute total is checked to be the sum of the two lanes it is supposed to split +/// into, so a consumer reading either lane and a consumer reading the total cannot disagree. /// /// The EIP-3529 refund and the EIP-7623 floor move the number a receipt reports without anyone /// having burnt the difference; both are carried on the result as their own fields and applied @@ -505,21 +513,24 @@ fn debug_assert_envelope_accounted( ) { if cfg!(debug_assertions) && spec.is_enabled(MegaSpecId::REX7) && !is_inside_sandbox { let envelope = outcome.result_and_state.result.gas().total_gas_spent(); - let accounted = i128::from(outcome.compute_gas_used) + additional_limit.non_compute_gas() - - i128::from(additional_limit.minted_call_stipend()) - - additional_limit.inspector_conjured_gas(); + let terms = additional_limit.conservation_terms(); + debug_assert!( + outcome.compute_gas_used == + outcome.compute_gas_enforced + outcome.compute_gas_destroyed, + "the reported compute total must be the sum of the lanes it splits into: \ + reported {} vs enforced {} + destroyed {}", + outcome.compute_gas_used, + outcome.compute_gas_enforced, + outcome.compute_gas_destroyed, + ); + let accounted = terms.envelope_for(outcome.compute_gas_destroyed); debug_assert!( accounted == i128::from(envelope), "the tracker lanes must account for the whole receipt envelope: \ accounted {accounted} vs envelope {envelope} \ - (compute {}, non-compute {}, minted stipend {}, inspector conjured {}, \ - destroyed {}, enforced {})", + (reported compute {}, reported destroyed {}, {terms})", outcome.compute_gas_used, - additional_limit.non_compute_gas(), - additional_limit.minted_call_stipend(), - additional_limit.inspector_conjured_gas(), outcome.compute_gas_destroyed, - outcome.compute_gas_enforced, ); } } diff --git a/crates/mega-evm/src/limit/checkpoint.rs b/crates/mega-evm/src/limit/checkpoint.rs index a520df6a..40da78b7 100644 --- a/crates/mega-evm/src/limit/checkpoint.rs +++ b/crates/mega-evm/src/limit/checkpoint.rs @@ -53,8 +53,8 @@ pub(crate) struct CheckpointTracker { /// record as compute gas, `MegaETH` storage gas, or budget an exceptional halt threw away. /// This field is the running total of the second kind, which makes the third derivable at the /// transaction's settlement point instead of having to be booked at each site that destroys - /// one — see [`AdditionalLimit::derived_burned_compute_gas`]( - /// super::AdditionalLimit::derived_burned_compute_gas). + /// one — see [`AdditionalLimit::conservation_terms`]( + /// super::AdditionalLimit::conservation_terms). /// /// Signed because one contributor is a difference rather than a charge: the `KeylessDeploy` /// sandbox boundary hands the parent one number for what the sandbox cost and another for what diff --git a/crates/mega-evm/src/limit/conservation.rs b/crates/mega-evm/src/limit/conservation.rs new file mode 100644 index 00000000..35e7fd17 --- /dev/null +++ b/crates/mega-evm/src/limit/conservation.rs @@ -0,0 +1,236 @@ +//! The transaction-level gas conservation law, as one set of terms. +//! +//! Three places state the same law: the settlement that derives what a transaction destroyed, the +//! re-settlement that follows a rewritten envelope, and the terminal check that the tracker lanes +//! account for the whole receipt. They used to restate it three times, each with its own +//! rearrangement and its own hand-written assertion message. This module holds it once. + +use core::fmt; + +/// The terms of the transaction-level gas conservation law. +/// +/// # The law +/// +/// Every unit of EVM gas a transaction burns is one of three things: compute work the trackers +/// enforced, `MegaETH` storage gas, or a budget something threw away without executing anything +/// for it. Two producers sit outside that partition and have to be corrected for — the +/// `CALL_STIPEND` revm mints into a value-transferring call's child frame without debiting the +/// caller, and whatever an inspector wrote into the execution from outside it. What is left is an +/// identity: +/// +/// ```text +/// spent = C + S + D − K − I +/// ``` +/// +/// | term | meaning | +/// | ---- | ---------------------------------------------------------------------------- | +/// | `spent` | the envelope the transaction burnt, read where it is final | +/// | `C` | [`enforced_compute_gas`](Self::enforced_compute_gas) — the work performed | +/// | `S` | [`non_compute_gas`](Self::non_compute_gas) — `MegaETH` storage gas | +/// | `D` | the destroyed remainder — budget thrown away without work | +/// | `K` | [`minted_call_stipend`](Self::minted_call_stipend) — gas minted, never debited | +/// | `I` | [`inspector_conjured_gas`](Self::inspector_conjured_gas) — gas from outside the EVM | +/// +/// `D` is deliberately not a field: it is the one term nothing measures directly, and each caller +/// supplies the reading it holds. [`destroyed_for`](Self::destroyed_for) solves the law for it, +/// [`envelope_for`](Self::envelope_for) solves the law for `spent` given one, and neither is a +/// second law — they are the same identity rearranged. +/// +/// # Sign conventions +/// +/// `S` and `I` are signed. `S` because the sandbox boundary can return gas to the lane, and `I` +/// because an inspector destroys gas as readily as it conjures it: positive is gas that exists in +/// the execution but that nothing debited from the transaction's envelope, negative is gas the +/// envelope funded that no frame ever received. +/// +/// # Before REX7 +/// +/// Every term but `C` is structurally zero: no lane records non-compute gas, no site mints a +/// stipend into the law, nothing is destroyed, and the outcome's enforced total is its reported +/// total. The law holds there too, but trivially, which is why the assertions that read it are +/// gated to REX7+. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ConservationTerms { + /// `C` — the compute gas the transaction performed, off the lane every compute-gas limit is + /// evaluated against. + pub enforced_compute_gas: u64, + + /// `S` — the EVM gas the transaction spent that is neither compute work nor a destroyed + /// remainder: the `MegaETH` storage-gas surcharges, the `MegaETH` share of intrinsic gas, the + /// code-deposit charge, and the sandbox boundary's residue. + pub non_compute_gas: i128, + + /// `K` — the `CALL_STIPEND` total this transaction's value-transferring calls minted into + /// their child frames. + /// + /// revm mints it without debiting the caller, so recorded work exceeds the envelope by one + /// stipend per such call and the law needs it added back. + pub minted_call_stipend: u64, + + /// `I` — the net gas an inspector conjured, across every lane the measurement shim books. + /// + /// Zero for every transaction that ran without an inspector and for every observation-only + /// inspector, which is why the law reads the same as it always did on those paths. + pub inspector_conjured_gas: i128, + + /// What the sites that destroyed a budget booked as they destroyed it — *not* a term of the + /// law, and never read by [`destroyed_for`](Self::destroyed_for). + /// + /// The derivation and this total are two independent measurements of the same quantity. They + /// agree, and [`unbooked_for`](Self::unbooked_for) is the gap a caller checks or settles; + /// deriving one from the other would collapse the cross-check into a tautology. + pub booked_destroyed_compute_gas: u64, +} + +impl ConservationTerms { + /// Solves the law for `D`: `D = spent + K + I − S − C`. + /// + /// Signed on purpose. A negative result means the recorded lanes together claim more gas than + /// the transaction spent, which is a defect to report rather than a value to clamp — clamping + /// inside the law would hide the half of the mismatch space where the bookings over-count. + #[inline] + pub const fn destroyed_for(&self, tx_gas_spent: u64) -> i128 { + (tx_gas_spent as i128) + (self.minted_call_stipend as i128) + self.inspector_conjured_gas - + self.non_compute_gas - + (self.enforced_compute_gas as i128) + } + + /// Solves the law for `spent`: `spent = C + S + D − K − I`. + /// + /// The reading to pass for `D` is the transaction's *reported* destroyed total — what the + /// receipt's compute total carries — because this direction is what checks that the lanes + /// account for the envelope that receipt reports. + #[inline] + pub const fn envelope_for(&self, destroyed_compute_gas: u64) -> i128 { + (self.enforced_compute_gas as i128) + self.non_compute_gas + (destroyed_compute_gas as i128) - + (self.minted_call_stipend as i128) - + self.inspector_conjured_gas + } + + /// The gap between what the law derives for `D` and what the per-site bookings hold. + /// + /// Zero whenever the two measurements agree. A caller either asserts that (the cross-check at + /// settlement) or books the difference (the rewritten-envelope re-settlement, where the + /// receipt's envelope grows past every site that could have booked it). + #[inline] + pub const fn unbooked_for(&self, tx_gas_spent: u64) -> i128 { + self.destroyed_for(tx_gas_spent) - (self.booked_destroyed_compute_gas as i128) + } +} + +impl fmt::Display for ConservationTerms { + /// The whole term set, in the order the law states it, for assertion messages. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "enforced compute {}, non-compute {}, minted stipend {}, inspector conjured {}, \ + booked destroyed {}", + self.enforced_compute_gas, + self.non_compute_gas, + self.minted_call_stipend, + self.inspector_conjured_gas, + self.booked_destroyed_compute_gas, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn terms() -> ConservationTerms { + ConservationTerms { + enforced_compute_gas: 21_000, + non_compute_gas: 5_000, + minted_call_stipend: 2_300, + inspector_conjured_gas: -400, + booked_destroyed_compute_gas: 0, + } + } + + /// The two directions are one identity, so solving for either term and substituting it back + /// must return the reading it started from — for any term set, including one whose signed + /// lanes point in opposite directions. + #[test] + fn test_the_two_directions_are_the_same_law() { + let terms = terms(); + // Above the point where this term set's derivation turns non-negative, so the round-trip + // stays inside the domain a real transaction produces. + for spent in [24_100_u64, 100_000, 1_000_000] { + let destroyed = terms.destroyed_for(spent); + assert!(destroyed >= 0, "fixture check: {destroyed} must be a real remainder"); + assert_eq!( + terms.envelope_for(destroyed as u64), + i128::from(spent), + "solving for the destroyed remainder and substituting it back must close", + ); + } + for destroyed in [0_u64, 1, 5_000] { + let spent = terms.envelope_for(destroyed); + assert!(spent >= 0, "fixture check: {spent} must be a real envelope"); + assert_eq!( + terms.destroyed_for(spent as u64), + i128::from(destroyed), + "and so must solving for the envelope and substituting that back", + ); + } + } + + /// Each term enters the law with the sign the doc claims, and the two directions carry + /// opposite signs for the same term. + #[test] + fn test_each_term_moves_the_law_in_its_documented_direction() { + let base = terms(); + let spent = 100_000; + let destroyed = base.destroyed_for(spent); + + let more_compute = + ConservationTerms { enforced_compute_gas: base.enforced_compute_gas + 1, ..base }; + assert_eq!(more_compute.destroyed_for(spent), destroyed - 1, "C reduces D"); + assert_eq!( + more_compute.envelope_for(0), + base.envelope_for(0) + 1, + "and raises the envelope", + ); + + let more_storage = ConservationTerms { non_compute_gas: base.non_compute_gas + 1, ..base }; + assert_eq!(more_storage.destroyed_for(spent), destroyed - 1, "S reduces D"); + + let more_stipend = + ConservationTerms { minted_call_stipend: base.minted_call_stipend + 1, ..base }; + assert_eq!(more_stipend.destroyed_for(spent), destroyed + 1, "K raises D"); + + let more_conjured = + ConservationTerms { inspector_conjured_gas: base.inspector_conjured_gas + 1, ..base }; + assert_eq!(more_conjured.destroyed_for(spent), destroyed + 1, "I raises D"); + assert_eq!( + more_conjured.envelope_for(0), + base.envelope_for(0) - 1, + "and lowers the envelope", + ); + } + + /// The booked total is the cross-check operand, not a term: it moves the gap and nothing else. + #[test] + fn test_the_booked_total_is_not_a_term_of_the_law() { + let base = terms(); + let booked = ConservationTerms { booked_destroyed_compute_gas: 777, ..base }; + + assert_eq!( + booked.destroyed_for(100_000), + base.destroyed_for(100_000), + "the derivation must not read the bookings it is checked against", + ); + assert_eq!(booked.envelope_for(0), base.envelope_for(0)); + assert_eq!(booked.unbooked_for(100_000), base.unbooked_for(100_000) - 777); + } + + /// An all-zero term set is the pre-REX7 shape: the law degenerates to `spent == 0`. + #[test] + fn test_the_default_term_set_is_the_trivial_law() { + let terms = ConservationTerms::default(); + assert_eq!(terms.destroyed_for(0), 0); + assert_eq!(terms.envelope_for(0), 0); + assert_eq!(terms.unbooked_for(0), 0); + } +} diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index dd14b4b2..8c9a9a13 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -13,8 +13,8 @@ use revm::{ }; use super::{ - checkpoint, compute_gas, data_size, frame_limit::TxRuntimeLimit, inspector_ledger, kv_update, - state_growth, storage_call_stipend, + checkpoint, compute_gas, conservation, data_size, frame_limit::TxRuntimeLimit, + inspector_ledger, kv_update, state_growth, storage_call_stipend, }; use crate::{ EvmTxRuntimeLimits, JournalInspectTr, MegaHaltReason, MegaSpecId, MegaTransaction, @@ -272,53 +272,29 @@ impl AdditionalLimit { self.checkpoint.record_non_compute_gas(amount); } - /// Re-derives the transaction's destroyed compute gas from what it spent, rather than from the - /// sites that booked it (REX7+). - /// - /// Every unit of EVM gas a transaction burns is *almost* exactly one of three things: compute - /// work the trackers enforce, `MegaETH` storage gas, or budget an exceptional halt threw away - /// without executing anything for it. Two of those three are counted as they happen — - /// [`ComputeGasTracker::enforced_tx_usage`](compute_gas::ComputeGasTracker::enforced_tx_usage) - /// and the non-compute lane — so the third is whatever is left of `tx_gas_spent`: - /// - /// ```text - /// destroyed = tx_gas_spent + minted_call_stipend + inspector_conjured_gas - /// − non_compute_gas − enforced_compute_gas - /// ``` - /// - /// The stipend term is what makes "almost" necessary: recorded compute gas is deliberately not - /// a partition of the gas the transaction spent, because a value-transferring call's - /// `CALL_STIPEND` is minted into the child frame rather than debited from the caller — see - /// [`CheckpointTracker::minted_call_stipend`]( - /// checkpoint::CheckpointTracker::minted_call_stipend). Adding it back is measurement, not - /// correction: the amount is booked from the single site that already computes it, and without - /// it the two sides disagree by one stipend per such call. - /// - /// `tx_gas_spent` must be read once the transaction's envelope is final and before any - /// post-execution adjustment that moves gas without anyone having burnt it — see the caller in - /// `MegaHandler::last_frame_result`. Gas that is rescued for the sender or hidden by the gas - /// clamp is erased from the envelope before that point, so neither can reach this subtraction. - /// - /// The inspector term is the same kind of correction for a different producer. An inspector - /// runs outside the EVM's own accounting and can write gas into an interpreter's counter or - /// into a frame's envelope that nobody debited — or take gas away that nobody gets back — and - /// the transaction then spends correspondingly less or more than its frames recorded. The - /// measurement shim books exactly that difference, so adding it back is again measurement - /// rather than correction. It is zero for every transaction that ran without an inspector and - /// for every observation-only inspector, which is why the law reads the same as it always did - /// on those paths. - /// - /// Signed on purpose: a mismatch against the booked total is a defect to report, and clamping - /// it at zero would hide the half of the mismatch space where the booking over-counts. - /// [`settle_destroyed_compute_gas`](Self::settle_destroyed_compute_gas) is what turns the - /// signed result into the number the transaction reports. + /// The terms of the transaction's gas conservation law, as they stand right now — see + /// [`ConservationTerms`](conservation::ConservationTerms), which states the law and both of + /// its rearrangements. + /// + /// Every site that derives, re-settles or checks a transaction's gas accounting reads the law + /// from here, so the law exists once and the terms cannot drift apart between the places that + /// use them. + /// + /// Meaningful once the transaction's envelope is final. Read earlier, the terms are simply + /// the partial totals recorded so far — and the envelope a caller solves the law against must + /// be read at the one moment it is final too, after the resource-limit rescue has been handed + /// back and before `post_execution` applies the EIP-3529 refund and the EIP-7623 floor. Gas + /// that is rescued for the sender and gas the clamp was hiding are both erased from the + /// envelope before that point, so neither can reach the subtraction. #[inline] - pub(crate) fn derived_burned_compute_gas(&self, tx_gas_spent: u64) -> i128 { - i128::from(tx_gas_spent) + - i128::from(self.minted_call_stipend()) + - self.inspector_conjured_gas() - - self.non_compute_gas() - - i128::from(self.enforced_compute_gas()) + pub fn conservation_terms(&self) -> conservation::ConservationTerms { + conservation::ConservationTerms { + enforced_compute_gas: self.enforced_compute_gas(), + non_compute_gas: self.non_compute_gas(), + minted_call_stipend: self.minted_call_stipend(), + inspector_conjured_gas: self.inspector_conjured_gas(), + booked_destroyed_compute_gas: self.burned_compute_gas(), + } } /// The `CALL_STIPEND` total this transaction's value-transferring calls minted into their @@ -354,28 +330,16 @@ impl AdditionalLimit { if !self.rex7_enabled() { return; } - let derived = self.derived_burned_compute_gas(tx_gas_spent); + let terms = self.conservation_terms(); + let derived = terms.destroyed_for(tx_gas_spent); debug_assert!( derived >= 0, - "derived destroyed compute gas is negative: {derived} \ - (spent {tx_gas_spent}, minted stipend {}, inspector conjured {}, non-compute {}, \ - enforced compute {})", - self.minted_call_stipend(), - self.inspector_conjured_gas(), - self.non_compute_gas(), - self.enforced_compute_gas(), + "derived destroyed compute gas is negative: {derived} (spent {tx_gas_spent}, {terms})", ); debug_assert!( - derived == i128::from(self.burned_compute_gas()), - "destroyed compute gas disagrees with the conservation law: \ - derived {derived} vs booked {} \ - (spent {tx_gas_spent}, minted stipend {}, inspector conjured {}, non-compute {}, \ - enforced compute {})", - self.burned_compute_gas(), - self.minted_call_stipend(), - self.inspector_conjured_gas(), - self.non_compute_gas(), - self.enforced_compute_gas(), + terms.unbooked_for(tx_gas_spent) == 0, + "destroyed compute gas disagrees with the conservation law: derived {derived} \ + (spent {tx_gas_spent}, {terms})", ); let settled = u64::try_from(derived.max(0)).unwrap_or(u64::MAX); self.checkpoint.set_settled_destroyed(settled); @@ -428,18 +392,12 @@ impl AdditionalLimit { if !self.rex7_enabled() { return; } - let unbooked = self.derived_burned_compute_gas(envelope_gas_spent) - - i128::from(self.burned_compute_gas()); + let terms = self.conservation_terms(); + let unbooked = terms.unbooked_for(envelope_gas_spent); debug_assert!( unbooked >= 0, "rewritten envelope destroys a negative amount: {unbooked} \ - (envelope {envelope_gas_spent}, minted stipend {}, inspector conjured {}, \ - non-compute {}, enforced compute {}, booked destroyed {})", - self.minted_call_stipend(), - self.inspector_conjured_gas(), - self.non_compute_gas(), - self.enforced_compute_gas(), - self.burned_compute_gas(), + (envelope {envelope_gas_spent}, {terms})", ); self.record_burned_gas(u64::try_from(unbooked.max(0)).unwrap_or(u64::MAX)); self.settle_destroyed_compute_gas(envelope_gas_spent); @@ -486,8 +444,8 @@ impl AdditionalLimit { } /// The net gas the inspector conjured — the term - /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas) adds to the envelope so - /// that gas nobody funded does not read as the transaction having spent less than it did. + /// [`ConservationTerms`](conservation::ConservationTerms) adds to the envelope so that gas + /// nobody funded does not read as the transaction having spent less than it did. #[inline] pub(crate) fn inspector_conjured_gas(&self) -> i128 { self.inspector.conjured_gas() @@ -590,7 +548,7 @@ impl AdditionalLimit { /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, /// always 0 before) — the second term of - /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas). + /// [`ConservationTerms`](conservation::ConservationTerms). #[inline] pub(crate) fn non_compute_gas(&self) -> i128 { self.checkpoint.non_compute_gas() @@ -598,23 +556,13 @@ impl AdditionalLimit { /// The compute gas the transaction claims to have performed: the reported total less the /// destroyed remainders — the third term of - /// [`derived_burned_compute_gas`](Self::derived_burned_compute_gas), and the number every - /// compute-gas limit comparison runs against. + /// [`ConservationTerms`](conservation::ConservationTerms), and the number every compute-gas + /// limit comparison runs against. #[inline] pub(crate) fn enforced_compute_gas(&self) -> u64 { self.compute_gas.enforced_tx_usage() } - /// Test-only reads of the conservation law's terms, so a test can assert which term a fixture - /// actually moved instead of inferring it from the destroyed total the terms combine into. - /// - /// Returns `(non-compute gas, minted call stipend, per-site destroyed bookings)`. - #[cfg(any(test, feature = "test-utils"))] - #[doc(hidden)] - pub fn conservation_terms_for_test(&self) -> (i128, u64, u64) { - (self.non_compute_gas(), self.minted_call_stipend(), self.burned_compute_gas()) - } - /// Takes the outstanding clamp so the caller can hand its hidden gas back to the interpreter, /// returning that amount. /// diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index 072f6b29..b5991ae5 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -3,6 +3,7 @@ use alloy_sol_types::SolError; mod checkpoint; mod compute_gas; +mod conservation; mod data_size; mod frame_limit; mod inspector_ledger; @@ -12,6 +13,7 @@ mod limit; mod state_growth; mod storage_call_stipend; +pub use conservation::*; pub use data_size::*; pub(crate) use frame_limit::{FrameLimitTracker, LimitReading, TxRuntimeLimit}; pub use inspector_ledger::*; diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index ee0ad9d9..5216f59b 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -428,8 +428,9 @@ fn transact_create_reject( let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), non_compute_gas, diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index bcb3b782..d284552d 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -129,8 +129,9 @@ pub(crate) fn transact_with_gas_limit( let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), non_compute_gas, @@ -314,8 +315,9 @@ pub(crate) fn transact_mega_tx( let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), non_compute_gas, @@ -461,8 +463,9 @@ pub(crate) fn transact_with_bucket_capacity( let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), non_compute_gas, diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs index 8e11e3a7..7bc24fd4 100644 --- a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -468,8 +468,9 @@ fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), non_compute_gas, diff --git a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs index bf451a95..3419d0e2 100644 --- a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs +++ b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs @@ -133,7 +133,8 @@ fn run_frame_init(spec: MegaSpecId, mut db: MemoryDatabase, frame_init: FrameIni let ItemOrResult::Result(frame_result) = result else { panic!("{spec:?}: this shape must reject the frame, not build one"); }; - let booked_destroyed = evm.ctx_ref().additional_limit.borrow().conservation_terms_for_test().2; + let booked_destroyed = + evm.ctx_ref().additional_limit.borrow().conservation_terms().booked_destroyed_compute_gas; Row { instruction_result: frame_result.instruction_result(), remaining: frame_result.gas().remaining(), diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index e34e0b75..e24efbc4 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -108,8 +108,9 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { booked_destroyed, ) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); + let terms = additional_limit.conservation_terms(); let (non_compute_gas, minted_call_stipend, booked_destroyed) = - additional_limit.conservation_terms_for_test(); + (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), additional_limit.current_call_remaining_compute_gas(), diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 63326832..0bee9726 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -20,8 +20,9 @@ use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - AdditionalLimit, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, MegaContext, MegaEvm, - MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, + AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, + MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, + MegaTransactionOutcome, }; use revm::{ bytecode::opcode::{ @@ -51,8 +52,7 @@ struct Reading { state_growth: u64, gas_used: u64, total_gas_spent: u64, - non_compute_gas: i128, - minted_call_stipend: u64, + terms: ConservationTerms, ledger: InspectorLedger, state: EvmState, } @@ -83,19 +83,20 @@ fn assert_identity(label: &str, r: &Reading) { r.enforced + r.destroyed, "{label}: reported compute must split into enforced + destroyed", ); - let accounted = i128::from(r.compute_gas) + r.non_compute_gas - - i128::from(r.minted_call_stipend) - - r.ledger.conjured_gas(); assert_eq!( - accounted, + r.terms.inspector_conjured_gas, + r.ledger.conjured_gas(), + "{label}: the law's `I` term is the ledger's net, and nothing else", + ); + assert_eq!( + r.terms.envelope_for(r.destroyed), i128::from(r.total_gas_spent), - "{label}: the tracker lanes plus the inspector ledger must account for the whole envelope; \ - compute={} non_compute={} minted={} conjured={} envelope={}", + "{label}: the law must close against the envelope the receipt reports; \ + reported compute={} destroyed={} envelope={} ({})", r.compute_gas, - r.non_compute_gas, - r.minted_call_stipend, - r.ledger.conjured_gas(), + r.destroyed, r.total_gas_spent, + r.terms, ); } @@ -159,7 +160,6 @@ where } fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { - let (non_compute_gas, minted_call_stipend, _booked) = limit.conservation_terms_for_test(); let gas_used = outcome.result_and_state.result.tx_gas_used(); let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); Reading { @@ -172,8 +172,7 @@ fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { state_growth: outcome.state_growth_used, gas_used, total_gas_spent, - non_compute_gas, - minted_call_stipend, + terms: limit.conservation_terms(), ledger: limit.inspector_ledger(), state: outcome.result_and_state.state, } @@ -793,8 +792,7 @@ fn test_an_observing_inspector_changes_nothing() { assert_eq!(inspected.state_growth, plain.state_growth); assert_eq!(inspected.gas_used, plain.gas_used); assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); - assert_eq!(inspected.non_compute_gas, plain.non_compute_gas); - assert_eq!(inspected.minted_call_stipend, plain.minted_call_stipend); + assert_eq!(inspected.terms, plain.terms); assert_eq!(inspected.state, plain.state, "the produced state must be identical"); assert_identity("observed", &inspected); assert_identity("plain", &plain); From 520d623879fb3f0c24fa6ce180ceb71bd8f13499 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 13:33:54 +0800 Subject: [PATCH 109/208] feat(evm): report the inspector ledger on the transaction outcome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An outcome carrying gas numbers was not fully described without saying whether an inspector took part in producing them. `MegaTransactionOutcome` now carries the whole ledger the measurement shim booked, so a consumer reads the answer off the outcome instead of reaching into the tracker the outcome came from — or, worse, assuming it. Additive: an all-zero ledger is what every uninspected transaction and every observation-only inspector reports, which is every consumer today. The test harness now checks the outcome's ledger against the tracker's on every shape it runs, so the API is pinned by the whole module rather than by the tests that look at it on purpose. --- crates/mega-evm/src/block/executor.rs | 1 + crates/mega-evm/src/block/result.rs | 1 + crates/mega-evm/src/evm/mod.rs | 2 + crates/mega-evm/src/evm/result.rs | 43 +++++++++++++-- .../mega-evm/tests/rex7/measured_inspector.rs | 54 ++++++++++++++++++- 5 files changed, 95 insertions(+), 6 deletions(-) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 423d5666..4834e91c 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -605,6 +605,7 @@ where compute_gas_destroyed: _, compute_gas_enforced, state_growth_used, + inspector_ledger: _, }, } = result; diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 5e0113cb..8d1afea9 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -287,6 +287,7 @@ mod tests { compute_gas_destroyed: 1, compute_gas_enforced: 2, state_growth_used: 4, + inspector_ledger: crate::InspectorLedger::default(), }; // One hop: MegaTransactionOutcome -> ResultAndState. diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index baef4b41..6003e6b8 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -413,6 +413,7 @@ where compute_gas_destroyed: additional_limit.destroyed_compute_gas(), compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, + inspector_ledger: additional_limit.inspector_ledger(), }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); Ok(outcome) @@ -450,6 +451,7 @@ where compute_gas_destroyed: additional_limit.destroyed_compute_gas(), compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, + inspector_ledger: additional_limit.inspector_ledger(), }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); Ok(outcome) diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 7860caab..c21c30fc 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -37,10 +37,10 @@ pub struct MegaTransactionOutcome { /// /// These two fields are the uninspected execution's split, and an observation-only inspector /// leaves them exactly there. An inspector's edits to interpreter gas counters and to frame - /// gas limits are measured at the callback boundary and kept out of the split — see - /// [`InspectorLedger`](crate::InspectorLedger) — but one that rewrites a frame *result*, in - /// `call_end` or `create_end`, still moves them: the burn split is settled before those - /// callbacks run. + /// gas limits are measured at the callback boundary and kept out of the split, and an edit to + /// a returning frame's result is booked at that frame's settlement — all three are reported on + /// [`inspector_ledger`](Self::inspector_ledger), which is how a consumer tells an execution + /// the EVM produced alone from one an inspector took part in. pub compute_gas_used: u64, /// The part of [`compute_gas_used`](Self::compute_gas_used) the transaction destroyed rather /// than performed (Rex7+, always 0 before). @@ -64,7 +64,7 @@ pub struct MegaTransactionOutcome { /// itself rather than out of this subtraction. /// /// Same inspector caveat as [`compute_gas_used`](Self::compute_gas_used): the field is the - /// uninspected split, and an inspector that rewrites a frame result will move it. + /// uninspected split unless [`inspector_ledger`](Self::inspector_ledger) says otherwise. pub compute_gas_destroyed: u64, /// The part of [`compute_gas_used`](Self::compute_gas_used) every compute-gas limit is /// evaluated against: the work the transaction performed, with Rex7+ destroyed remainders left @@ -80,6 +80,39 @@ pub struct MegaTransactionOutcome { pub compute_gas_enforced: u64, /// The state growth used. pub state_growth_used: u64, + /// What an inspector did to this transaction's gas accounting, measured rather than inferred. + /// + /// `MegaETH` wraps every inspector it is handed in a measurement shim. The EVM does not + /// execute inside an inspector callback, so anything that moves across one is the inspector's + /// doing by construction, and this is what the shim booked: gas written into a live + /// interpreter's counter ([`gas`](crate::InspectorLedger::gas)), into the envelope a frame is + /// about to be built with ([`env`](crate::InspectorLedger::env)), into a returning frame's + /// result ([`result`](crate::InspectorLedger::result)), and how many rewrites the shim refused + /// outright ([`rejected_rewrites`](crate::InspectorLedger::rejected_rewrites)). + /// + /// # Sign convention + /// + /// Every gas lane is signed and reads from the transaction's point of view: **positive is gas + /// conjured** — gas that exists in the execution but that nothing debited from the + /// transaction's envelope — and **negative is gas destroyed**, which the envelope funded and + /// no frame ever received. The lanes are net, so an injection and a matching removal cancel. + /// + /// # When it is zero + /// + /// [`InspectorLedger::is_zero`](crate::InspectorLedger::is_zero) holds for every transaction + /// that ran without an inspector and for every observation-only inspector — which is every + /// tracer. A non-zero ledger means this outcome's gas numbers describe an execution an + /// inspector took part in, and the block-execution path refuses to admit one into a block for + /// exactly that reason. + /// + /// # What it is for + /// + /// Reporting, and that refusal. No resource limit is ever evaluated against it: enforcement + /// never sees an inspector's adjustment, because the shim shifts the compute measurement's + /// baseline by the same amount it books here. It is also the `I` term of the conservation law + /// — see [`ConservationTerms`](crate::ConservationTerms) — which is why an outcome carrying + /// gas numbers is not fully described without it. + pub inspector_ledger: crate::InspectorLedger, } /// Identifies which stage of block execution produced a state change. diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 0bee9726..1c5e4e38 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -159,7 +159,18 @@ where evm.execute_transaction(tx()).map(|_| ()).map_err(|e| format!("{e:?}")) } +/// Reads one transaction's outcome, and pins the outcome's own ledger field against the tracker's +/// on every shape this module runs. +/// +/// The outcome is what a consumer sees; the tracker is where the shim booked. Checking them here +/// means every test below asserts the outcome API carries the measurement, not just the two that +/// look at it on purpose. fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { + assert_eq!( + outcome.inspector_ledger, + limit.inspector_ledger(), + "the outcome must report the ledger the shim booked, unchanged", + ); let gas_used = outcome.result_and_state.result.tx_gas_used(); let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); Reading { @@ -173,7 +184,7 @@ fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { gas_used, total_gas_spent, terms: limit.conservation_terms(), - ledger: limit.inspector_ledger(), + ledger: outcome.inspector_ledger, state: outcome.result_and_state.state, } } @@ -797,3 +808,44 @@ fn test_an_observing_inspector_changes_nothing() { assert_identity("observed", &inspected); assert_identity("plain", &plain); } + +/// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term +/// is zero — the shape every consumer of this API sees in practice. +/// +/// The stronger property is what the field is *for*: an all-zero ledger is a consumer's guarantee +/// that the gas numbers next to it are the EVM's own, so it has to be exactly zero rather than +/// merely small. A fixture that makes an inner call and writes storage is used, so the assertion +/// covers a transaction with something for a lane to have picked up. +#[test] +fn test_an_uninspected_transaction_reports_an_empty_ledger() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(9)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = base_db(code).account_code(CALLEE, callee); + + let plain = transact_plain(db, default_limits()); + + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!( + plain.terms.non_compute_gas > 0, + "fixture check: the transaction must have moved a lane other than compute", + ); + assert_eq!( + plain.ledger, + InspectorLedger::default(), + "no inspector ran, so every lane must be untouched", + ); + assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); + assert_identity("uninspected", &plain); +} From 68a10fbe947ca1fe40d04b5355e2d8093efaa51b Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 13:35:57 +0800 Subject: [PATCH 110/208] feat(block): refuse an inspector-adjusted transaction on the canonical path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Block production and block validation must produce the same numbers for the same block on every node, and an inspector is one node's configuration: what it writes into a gas counter, a frame's envelope or a returning frame's result reaches the receipt, the transaction's reported compute total, and through it the block's cumulative counters. So the two entries that run a transaction and the one funnel every commit routes through refuse a non-zero ledger. The refusal is an error rather than an assertion, and holds in release builds: it is a boundary the canonical path holds against its embedder, so it has to hold in the binaries that build and validate blocks, and it fails the block rather than the process. Observation is untouched — every inspector on this path today is a tracer, and a tracer's ledger is empty. Pre- and post-block system calls run uninspected and the keyless-deploy sandbox builds its own uninspected EVM, so neither is an entry the guard has to cover. --- AGENTS.md | 5 +- crates/mega-evm/src/block/executor.rs | 51 ++- crates/mega-evm/src/block/result.rs | 36 ++ .../tests/block_executor/inspector_guard.rs | 373 ++++++++++++++++++ crates/mega-evm/tests/block_executor/main.rs | 1 + 5 files changed, 464 insertions(+), 2 deletions(-) create mode 100644 crates/mega-evm/tests/block_executor/inspector_guard.rs diff --git a/AGENTS.md b/AGENTS.md index 7e0e9e17..eb7f130b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,13 +123,16 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. A frame init that refuses to build a frame at all is settled at the same point, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). - The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `destroyed = spent + minted_call_stipend + inspector_conjured_gas − non_compute_gas − enforced_compute_gas` — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. + The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `spent = C + S + D − K − I`, stated once as `ConservationTerms` (`limit/conservation.rs`) and read from `AdditionalLimit::conservation_terms()` by every site that derives, re-settles or checks it — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. `minted_call_stipend` is the correction the law needs because revm mints `CALL_STIPEND` into a value-transferring call's child frame without debiting the caller, so recorded work exceeds the envelope by one stipend per such call; it is booked per mint event — at the CALL-family settlement, before frame init — so a value call turned away at frame entry (insufficient balance, call depth) books one too, because its refund returns the mint into the caller's envelope. `inspector_conjured_gas` is the same kind of correction for a producer outside the EVM: `MegaEvm` wraps every inspector it is handed in `MeasuredInspector`, which snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference into `AdditionalLimit::inspector_ledger` — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. + The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. + Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full. + Pre- and post-block system calls run uninspected (`Handler::run_system_call` takes the plain frame path) and the keyless-deploy sandbox builds its own uninspected EVM, so neither is an entry the guard has to cover. Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 4834e91c..cc670450 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -78,6 +78,34 @@ impl core::fmt::Debug for MegaBlockExecutor } } +/// Refuses a transaction whose gas accounting an inspector adjusted, on the canonical path. +/// +/// Block production and block validation are the two places where what the executor reports has to +/// be what the EVM did, reproducibly, on every node. An inspector's edits reach the receipt and the +/// block's counters but live in one node's configuration, so a transaction carrying any is not +/// something this executor may run or admit — see +/// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]. +/// +/// Enforced in release builds, deliberately. This is a boundary the canonical path holds against +/// its embedder rather than an invariant `MegaETH` maintains internally, so it has to hold in the +/// binaries that build and validate blocks, and it fails the block rather than the process. +/// +/// The check is free on every path that passes it: the ledger is a `Copy` struct already on the +/// outcome, and this reads four fields of it once per transaction. +#[inline] +fn reject_inspector_adjusted_accounting( + tx_hash: B256, + ledger: crate::InspectorLedger, +) -> Result<(), BlockExecutionError> { + if ledger.is_zero() { + return Ok(()); + } + Err(BlockExecutionError::other(crate::MegaBlockExecutionError::InspectorAdjustedAccounting { + tx_hash, + ledger, + })) +} + impl MegaBlockExecutor, R> where DB: StateDB, @@ -470,6 +498,14 @@ where /// (e.g. the `alloy_evm` `ExecutableTx`-constrained path): they resolve the sizes themselves /// and pass them in. Prefer [`MegaBlockExecutor::run_transaction`] otherwise, which resolves /// the sizes for you and cross-checks any cached values. + /// + /// # Contract + /// + /// A transaction whose gas accounting an inspector adjusted is refused with + /// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]( + /// crate::MegaBlockExecutionError::InspectorAdjustedAccounting) rather than returned. An + /// observation-only inspector — which is every tracer — is unaffected; an embedder that wants + /// a rewriting one drives [`crate::MegaEvm::execute_transaction`] directly. pub fn run_transaction_with_sizes( &mut self, tx: Tx, @@ -507,6 +543,7 @@ where .evm .execute_transaction(tx.into_tx_env()) .map_err(move |err| BlockExecutionError::evm(alloy_op_evm::map_op_err(err), hash))?; + reject_inspector_adjusted_accounting(tx.tx().tx_hash(), outcome.inspector_ledger)?; Ok(BlockMegaTransactionOutcome { tx, tx_size, da_size, depositor, inner: outcome }) } @@ -547,6 +584,7 @@ where .evm .execute_transaction(tx_env) .map_err(move |err| BlockExecutionError::evm(alloy_op_evm::map_op_err(err), hash))?; + reject_inspector_adjusted_accounting(recovered.tx().tx_hash(), outcome.inspector_ledger)?; Ok((depositor, outcome)) } @@ -578,6 +616,13 @@ where /// /// Rejection is not a block-level failure — a block that ends without the rejected /// transaction is perfectly valid — which is why the error is returned rather than latched. + /// + /// This is also where a result an inspector took part in is refused, ahead of admission and + /// of any other reading: the producers guard their own outputs, but a result reaching this + /// funnel may have been produced by another executor instance or built by hand, and the + /// outcome's own ledger is the only thing here that knows. See + /// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]( + /// crate::MegaBlockExecutionError::InspectorAdjustedAccounting). pub fn commit_tx_result( &mut self, result: crate::MegaBlockTxResult<::TxType>, @@ -605,10 +650,14 @@ where compute_gas_destroyed: _, compute_gas_enforced, state_growth_used, - inspector_ledger: _, + inspector_ledger, }, } = result; + // Before anything else, including admission: a result an inspector took part in is not + // one this block may contain at all, whether or not it would still fit. + reject_inspector_adjusted_accounting(tx_hash, inspector_ledger)?; + // Re-validate limits at commit time to handle parallel execution race conditions. // Between execution and commit, other transactions may have been committed, potentially // exhausting the block's remaining capacity. diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 8d1afea9..c4a4ee16 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -249,6 +249,42 @@ impl InvalidTxError for MegaBlockLimitExceededError { } } +/// A `MegaETH` block executor's own refusals — the ones that are neither a resource limit nor a +/// transaction the EVM rejected. +/// +/// These say the executor was asked to do something it must not do, so they are reported as +/// [`BlockExecutionError::Internal`](alloy_evm::block::BlockExecutionError::Internal) rather than +/// as a verdict about the transaction: the block is not built, and there is nothing about the +/// transaction itself for a caller to fix. +#[derive(Debug, Clone, thiserror::Error)] +pub enum MegaBlockExecutionError { + /// A transaction whose gas accounting an inspector adjusted reached the canonical + /// block-execution path. + /// + /// Block production and block validation must produce the same numbers for the same block, on + /// every node, so what the executor reports has to be what the EVM did — and only that. An + /// inspector that writes gas into an interpreter's counter, into a frame's envelope, or into a + /// returning frame's result is a second producer of gas movement, present on one node's + /// configuration and not on another's, whose effect reaches the receipt, the transaction's + /// reported compute total, and through it the block's cumulative counters. + /// + /// Observation is untouched: a tracer leaves an all-zero ledger, which is what every inspector + /// on this path today does. An embedder that genuinely wants a rewriting inspector still has + /// one — [`MegaEvm::execute_transaction`](crate::MegaEvm::execute_transaction) supports it in + /// full, with the ledger reported on the outcome — it just does not get to call the result a + /// block. + #[error( + "transaction {tx_hash} reached the canonical block-execution path with its gas accounting \ + adjusted by an inspector: {ledger:?}" + )] + InspectorAdjustedAccounting { + /// The transaction the adjusted accounting belongs to. + tx_hash: TxHash, + /// What the measurement shim booked for that transaction. + ledger: crate::InspectorLedger, + }, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs new file mode 100644 index 00000000..7a12e7c9 --- /dev/null +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -0,0 +1,373 @@ +//! The canonical block-execution path admits no transaction an inspector took part in. +//! +//! `MegaETH` supports rewriting inspectors in full — the measurement shim books what they do and +//! the conservation law accounts for it — but supporting a rewrite is not the same as letting it +//! into a block. Block production and block validation have to produce the same numbers for the +//! same block on every node, and an inspector is one node's configuration: what it writes into a +//! gas counter reaches the receipt, the transaction's reported compute total, and through it the +//! block's cumulative counters. +//! +//! So every entry on the canonical path — the two that run a transaction and the one funnel that +//! admits a result — refuses a non-zero ledger. The refusal is an error rather than an assertion, +//! because it is a boundary held against an embedder and has to hold in the binaries that build +//! and validate blocks; the tests here therefore pass identically in debug and release builds. +//! +//! The green half matters as much as the red: every inspector on this path today is a tracer, and +//! a tracer must keep working. That is what the observation tests pin. + +use std::convert::Infallible; + +use alloy_evm::{block::BlockExecutor, EvmEnv}; +use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; +use alloy_primitives::{address, Address, Bytes, Signature, TxHash, TxKind, B256, U256}; +use mega_evm::{ + alloy_consensus::{transaction::Recovered, Signed, TxLegacy}, + alloy_evm::block::BlockExecutionError, + test_utils::{BytecodeBuilder, MemoryDatabase}, + BlockLimits, InspectorLedger, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, + MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, TestExternalEnvs, +}; +use revm::{ + bytecode::opcode::{POP, STOP}, + context::BlockEnv, + database::State, + interpreter::{Interpreter, InterpreterTypes}, + Inspector, +}; + +/// Sends every transaction in these tests. +const CALLER: Address = address!("2000000000000000000000000000000000000002"); +/// A callee with enough plain opcodes for an inspector to land an edit mid-run. +const CONTRACT: Address = address!("1000000000000000000000000000000000000001"); + +/// Gas the injecting inspector writes into the interpreter's counter. +const INJECTED: u64 = 7_000; + +/// Writes gas into the running interpreter's counter, once — the smallest rewrite that moves the +/// ledger's [`gas`](InspectorLedger::gas) lane. +#[derive(Default)] +struct GasInjector { + applied: bool, +} + +impl Inspector for GasInjector { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.applied { + return; + } + self.applied = true; + interp.gas.erase_cost(INJECTED); + } +} + +/// Counts callbacks and changes nothing — the shape every tracer in production has. +#[derive(Default)] +struct Observer { + steps: u64, +} + +impl Inspector for Observer { + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + } +} + +fn envelope(nonce: u64) -> MegaTxEnvelope { + let tx = TxLegacy { + chain_id: Some(8453), + nonce, + gas_price: 1_000_000, + gas_limit: 1_000_000, + to: TxKind::Call(CONTRACT), + value: U256::ZERO, + input: Bytes::new(), + }; + MegaTxEnvelope::Legacy(Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)) +} + +fn build_db() -> MemoryDatabase { + let mut code = BytecodeBuilder::default(); + for _ in 0..16 { + code = code.push_number(1u64).append(POP); + } + let mut db = MemoryDatabase::default(); + db.set_account_code(CONTRACT, code.append(STOP).build()); + db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000_000u64)); + db +} + +fn evm_env(spec: MegaSpecId) -> EvmEnv { + let mut cfg_env = revm::context::CfgEnv::default(); + cfg_env.set_spec_and_mainnet_gas_params(spec); + EvmEnv::new( + cfg_env, + BlockEnv { + number: U256::from(1000), + timestamp: U256::from(1_800_000_000), + gas_limit: 30_000_000, + ..Default::default() + }, + ) +} + +fn executor_factory( + spec: MegaSpecId, +) -> MegaBlockExecutorFactory< + MegaHardforkConfig, + MegaEvmFactory>, + OpAlloyReceiptBuilder, +> { + MegaBlockExecutorFactory::new( + MegaHardforkConfig::default().with_all_activated_through(spec), + MegaEvmFactory::new().with_external_env_factory(TestExternalEnvs::::new()), + OpAlloyReceiptBuilder::default(), + ) +} + +fn block_ctx() -> MegaBlockExecutionCtx { + MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits()) +} + +/// Unwraps the refusal, checking it is the one this module is about and that it names the +/// transaction and the measurement it refused over. +/// +/// Reached by downcast rather than by matching the message: the error crosses the `alloy_evm` +/// boundary as a boxed `dyn Error`, and a consumer that wants to react to it — a sequencer that +/// would rather drop the transaction than fail the block — has to get the typed value back. +#[track_caller] +fn expect_refusal(err: &BlockExecutionError, expected_hash: TxHash) -> InspectorLedger { + let internal = err.as_internal().unwrap_or_else(|| { + panic!("the refusal must be an internal error, not a verdict on the transaction: {err:?}") + }); + let other = internal + .as_other() + .unwrap_or_else(|| panic!("the refusal must carry MegaETH's own error: {internal:?}")); + let mega = other + .downcast_ref::() + .unwrap_or_else(|| panic!("the refusal must survive the boxing as a typed value: {other}")); + let mega_evm::MegaBlockExecutionError::InspectorAdjustedAccounting { tx_hash, ledger } = mega; + assert_eq!(*tx_hash, expected_hash, "the refusal must name the transaction it refused"); + assert!(!ledger.is_zero(), "a refusal over an empty ledger is a refusal of nothing"); + *ledger +} + +/// The producer entry: a transaction an inspector adjusted never becomes an outcome the block +/// path will hand back. +/// +/// The rewrite is booked, the transaction itself executes fine, and the refusal comes from the +/// executor rather than from the EVM — which is the whole point, since the EVM is required to keep +/// supporting the rewrite. +#[test] +fn test_run_transaction_refuses_an_inspector_adjusted_transaction() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + GasInjector::default(), + ); + + let tx = envelope(0); + let err = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect_err("the canonical path must refuse an inspector-adjusted transaction"); + + assert!(executor.evm().inspector.applied, "the fixture must reach the injection point"); + let ledger = expect_refusal(&err, *tx.hash()); + assert_eq!( + ledger.gas, + i128::from(INJECTED), + "the refusal must carry what was actually injected, so a caller can see the size of it", + ); + assert_eq!(ledger.env, 0, "no frame envelope was touched"); + assert_eq!( + executor.block_limiter.block_compute_gas_used, 0, + "a refused transaction must leave the block's counters where they were", + ); +} + +/// The other producer entry, reached through the `alloy_evm` trait rather than the inherent +/// method: the two resolve their transaction sizes differently and share no body, so a guard on +/// one says nothing about the other. +#[test] +fn test_execute_transaction_without_commit_refuses_it_too() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + GasInjector::default(), + ); + + let tx = envelope(0); + let err = executor + .execute_transaction_without_commit(&Recovered::new_unchecked(&tx, CALLER)) + .expect_err("the trait entry must refuse it as well"); + + assert_eq!(expect_refusal(&err, *tx.hash()).gas, i128::from(INJECTED)); + assert!(executor.receipts.is_empty(), "nothing may have been recorded"); +} + +/// The consumer entry: a result whose adjustment was not made by *this* executor is refused at +/// the commit funnel, before it can touch anything. +/// +/// This is the entry that has to hold. Execution and commit are separate steps — the parallel +/// executor speculatively runs many transactions and commits the survivors one by one — so a +/// result arriving here may have been produced by a different executor instance, or built by +/// hand. The producer-side guards cannot see any of those; the outcome's own ledger can. +#[test] +fn test_commit_refuses_a_result_an_inspector_took_part_in() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + ); + + let tx = envelope(0); + let mut outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("fixture check: an uninspected run must be admitted"); + assert!( + outcome.inner.inspector_ledger.is_zero(), + "fixture check: an uninspected run reports an empty ledger", + ); + + // The shape a result produced elsewhere arrives in: the numbers are execution's, the ledger + // says an inspector moved some of them. + outcome.inner.inspector_ledger = InspectorLedger { gas: 1, ..Default::default() }; + + let err = + executor.commit_transaction_outcome(outcome).expect_err("the commit funnel must refuse it"); + + assert_eq!(expect_refusal(&err, *tx.hash()).gas, 1); + assert!(executor.receipts.is_empty(), "no receipt may have been pushed"); + assert_eq!( + executor.block_limiter.block_gas_used, 0, + "and no limiter counter may have been advanced", + ); + assert!( + executor.take_pending_commit_error().is_none(), + "the fallible entry reports rather than latches, so the executor stays usable", + ); +} + +/// The infallible commit hook has no way to report the refusal, so it latches it and the block +/// fails at `finish` — the same contract it already holds for a late block-limit rejection. +#[test] +fn test_the_infallible_commit_hook_latches_the_refusal() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + ); + + let tx = envelope(0); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("fixture check: an uninspected run must be admitted"); + let mut result = mega_evm::MegaBlockTxResult { + tx_type: tx.tx_type(), + tx_hash: *tx.hash(), + gas_limit: 1_000_000, + tx_size: outcome.tx_size, + da_size: outcome.da_size, + depositor: outcome.depositor, + inner: outcome.inner, + }; + result.inner.inspector_ledger = InspectorLedger { env: -5, ..Default::default() }; + + let gas = executor.commit_transaction(result); + assert_eq!(gas.tx_gas_used(), 0, "a transaction that contributed nothing must report zero gas",); + let latched = executor + .pending_commit_error() + .expect("the refusal must be latched where `finish` will find it"); + assert_eq!(expect_refusal(latched, *tx.hash()).env, -5); + + let err = executor.finish().expect_err("the block must not finish over a latched refusal"); + expect_refusal(&err, *tx.hash()); +} + +/// The guard governs the configuration a block is built with, which no historical block covers, so +/// it is not gated on a spec — the same rewrite is refused on a frozen one. +/// +/// The measurement it reads is spec-independent for the same reason: the shim books what an +/// inspector writes into a gas counter whether or not the spec has a lane that the write could +/// make unsound. +#[test] +fn test_the_guard_is_not_spec_gated() { + for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX4, MegaSpecId::REX6, MegaSpecId::REX7] { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(spec).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(spec), + GasInjector::default(), + ); + + let tx = envelope(0); + let err = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .err() + .unwrap_or_else(|| panic!("{spec:?}: the rewrite must be refused on every spec")); + assert_eq!( + expect_refusal(&err, *tx.hash()).gas, + i128::from(INJECTED), + "{spec:?}: and the measurement it is refused over must be the same one", + ); + } +} + +/// The green half: an observation-only inspector is left alone, and the block it helps build is +/// bit-identical to the one built without it. +/// +/// Every inspector on this path today is a tracer. If the guard could not tell one from a +/// rewriting inspector, it would take tracing off block production entirely. +#[test] +fn test_an_observing_inspector_still_builds_a_block() { + let build = |observe: bool| { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let tx = envelope(0); + let factory = executor_factory(MegaSpecId::REX7); + let (gas_used, steps) = if observe { + let mut executor = factory.create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + Observer::default(), + ); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("an observing inspector must not be refused"); + assert!(outcome.inner.inspector_ledger.is_zero(), "and must leave an empty ledger"); + let gas = executor.commit_transaction_outcome(outcome).expect("nor at commit"); + let steps = executor.evm().inspector.steps; + let (_, result) = executor.finish().expect("the block must finish"); + assert_eq!(result.receipts.len(), 1, "the observed block still has its receipt"); + (gas, steps) + } else { + let mut executor = + factory.create_executor(&mut state, block_ctx(), evm_env(MegaSpecId::REX7)); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("the reference run must be admitted"); + let gas = executor.commit_transaction_outcome(outcome).expect("and committed"); + let (_, result) = executor.finish().expect("the block must finish"); + assert_eq!(result.receipts.len(), 1); + (gas, 0) + }; + (gas_used, steps) + }; + + let (observed_gas, steps) = build(true); + let (plain_gas, _) = build(false); + assert!(steps > 0, "the fixture must actually have observed something"); + assert_eq!(observed_gas, plain_gas, "observation must not move a single unit of gas"); +} diff --git a/crates/mega-evm/tests/block_executor/main.rs b/crates/mega-evm/tests/block_executor/main.rs index 5538bff0..8619a57b 100644 --- a/crates/mega-evm/tests/block_executor/main.rs +++ b/crates/mega-evm/tests/block_executor/main.rs @@ -5,5 +5,6 @@ mod block_limits; mod compute_gas_lanes; mod deposit_da_exemption; mod inspector; +mod inspector_guard; mod sequencer_registry; mod trait_factory_runtime_limits; From 831db5218011ad84aef0fe97fae61f78afdb23b5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 13:46:20 +0800 Subject: [PATCH 111/208] test(block): run the two claims the guard's coverage rests on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The report that pre- and post-block system calls need no guard rested on reading revm's `run_system_call`, and the claim that the guard costs today's consumers nothing rested on reading revm-inspectors. Both are now executed: a system call with a gas-injecting inspector attached never reaches it, and a real `TracingInspector` over a transaction with a nested frame and a storage write is admitted with an empty ledger. Also corrects three doc claims that were stronger than what the code does: the terminal envelope check is the settlement identity restated on the ordinary path (its reach is the paths where the envelope moved after settlement), the ledger's negative direction covers gas destroyed inside a frame as well as gas that never reached one, and only the interpreter-counter lane is kept out of the compute measurement by a baseline shift — the other two never enter it. --- AGENTS.md | 2 +- crates/mega-evm/src/evm/mod.rs | 8 +- crates/mega-evm/src/evm/result.rs | 17 ++-- .../tests/block_executor/inspector_guard.rs | 99 ++++++++++++++++++- 4 files changed, 109 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eb7f130b..561950b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,7 +132,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full. - Pre- and post-block system calls run uninspected (`Handler::run_system_call` takes the plain frame path) and the keyless-deploy sandbox builds its own uninspected EVM, so neither is an entry the guard has to cover. + Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). These costs scale dynamically with SALT bucket capacity (see External Environment Dependencies below). diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 6003e6b8..0cd0b6a3 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -479,10 +479,10 @@ where /// C + S + D − K − I == total_gas_spent /// ``` /// -/// The settlement site solves the same law for `D`, so reading it back in this direction is only -/// a tautology when both sides read the same terms *and* the same envelope. Neither holds here, -/// which is what gives this check its reach: it re-reads the terms after settlement finished and -/// compares against the envelope the receipt ended up carrying. +/// The settlement site solved the same law for `D`, so on a transaction that settled against this +/// same envelope the check is that identity restated — and that is the point. Its reach is the +/// paths where the two are *not* the same, named below: the terms are re-read after settlement +/// finished, and the envelope is the one the receipt ended up carrying. /// /// The inspector term `I` is zero unless a rewriting inspector was attached: it is what the /// measurement shim booked for gas the inspector wrote into an interpreter counter, a frame diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index c21c30fc..6bf5eac4 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -94,8 +94,9 @@ pub struct MegaTransactionOutcome { /// /// Every gas lane is signed and reads from the transaction's point of view: **positive is gas /// conjured** — gas that exists in the execution but that nothing debited from the - /// transaction's envelope — and **negative is gas destroyed**, which the envelope funded and - /// no frame ever received. The lanes are net, so an injection and a matching removal cancel. + /// transaction's envelope — and **negative is gas destroyed** — gas the envelope funded that + /// the execution never got the benefit of. The lanes are net, so an injection and a matching + /// removal cancel. /// /// # When it is zero /// @@ -107,11 +108,13 @@ pub struct MegaTransactionOutcome { /// /// # What it is for /// - /// Reporting, and that refusal. No resource limit is ever evaluated against it: enforcement - /// never sees an inspector's adjustment, because the shim shifts the compute measurement's - /// baseline by the same amount it books here. It is also the `I` term of the conservation law - /// — see [`ConservationTerms`](crate::ConservationTerms) — which is why an outcome carrying - /// gas numbers is not fully described without it. + /// Reporting, and that refusal. No resource limit is ever evaluated against it, and no + /// adjustment recorded here is ever counted as work: the interpreter-counter lane shifts the + /// compute measurement's baseline as it books, so the edit settles outside the measured span + /// and the gas clamp is re-derived on the spot, while the other two lanes move a gas budget + /// rather than a recording and so never enter the measurement at all. It is also the `I` term + /// of the conservation law — see [`ConservationTerms`](crate::ConservationTerms) — which is + /// why an outcome carrying gas numbers is not fully described without it. pub inspector_ledger: crate::InspectorLedger, } diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 7a12e7c9..8a6f169a 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -28,7 +28,7 @@ use mega_evm::{ MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, TestExternalEnvs, }; use revm::{ - bytecode::opcode::{POP, STOP}, + bytecode::opcode::{CALL, POP, STOP}, context::BlockEnv, database::State, interpreter::{Interpreter, InterpreterTypes}, @@ -39,6 +39,8 @@ use revm::{ const CALLER: Address = address!("2000000000000000000000000000000000000002"); /// A callee with enough plain opcodes for an inspector to land an edit mid-run. const CONTRACT: Address = address!("1000000000000000000000000000000000000001"); +/// A second callee, so a tracer has a nested frame to record. +const CALLEE: Address = address!("1000000000000000000000000000000000000002"); /// Gas the injecting inspector writes into the interpreter's counter. const INJECTED: u64 = 7_000; @@ -90,8 +92,22 @@ fn build_db() -> MemoryDatabase { for _ in 0..16 { code = code.push_number(1u64).append(POP); } + let code = code + .sstore(U256::from(1), U256::from(9)) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000u64) // gas + .append(CALL) + .append(POP) + .append(STOP) + .build(); let mut db = MemoryDatabase::default(); - db.set_account_code(CONTRACT, code.append(STOP).build()); + db.set_account_code(CONTRACT, code); + db.set_account_code(CALLEE, BytecodeBuilder::default().stop().build()); db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000_000u64)); db } @@ -296,9 +312,9 @@ fn test_the_infallible_commit_hook_latches_the_refusal() { /// The guard governs the configuration a block is built with, which no historical block covers, so /// it is not gated on a spec — the same rewrite is refused on a frozen one. /// -/// The measurement it reads is spec-independent for the same reason: the shim books what an -/// inspector writes into a gas counter whether or not the spec has a lane that the write could -/// make unsound. +/// The measurement it reads is spec-independent too, for its own reason: the shim books what an +/// inspector writes into a gas counter whether or not the spec has a lane the write could make +/// unsound. #[test] fn test_the_guard_is_not_spec_gated() { for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX4, MegaSpecId::REX6, MegaSpecId::REX7] { @@ -371,3 +387,76 @@ fn test_an_observing_inspector_still_builds_a_block() { assert!(steps > 0, "the fixture must actually have observed something"); assert_eq!(observed_gas, plain_gas, "observation must not move a single unit of gas"); } + +/// A pre- or post-block system call never runs the inspector, so it is not an entry the guard has +/// to cover. +/// +/// Two independent reasons, and this pins the one that is not visible from the block executor's +/// own signatures. Structurally, a system call produces a `ResultAndState` rather than a +/// `MegaTransactionOutcome`, and the ledger is reset at the start of every transaction, so nothing +/// a system call booked could reach a transaction's outcome anyway. Underneath that, the system +/// call path takes revm's plain frame loop rather than the inspecting one — which is what this +/// runs to find out, rather than reading it off upstream's source. +#[test] +fn test_a_system_call_does_not_run_the_inspector() { + use revm::SystemCallEvm as _; + + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + GasInjector::default(), + ); + + let result = executor + .evm_mut() + .system_call(CONTRACT, Bytes::new()) + .expect("the system call must not surface an EVMError"); + + assert!(result.result.is_success(), "fixture check: the callee must have run, got {result:?}",); + assert!(!executor.evm().inspector.applied, "a system call must not reach the inspector at all",); +} + +/// The real tracer that `mega-evme replay` attaches to this exact path is admitted, and the block +/// it observes is the one built without it. +/// +/// The `Observer` above is a fixture; this is the production shape. `TracingInspector` receives +/// every callback the shim measures — including the ones handed a live interpreter and the ones +/// handed a frame's inputs — so if observation could move a lane by accident, it would move one +/// here. Run rather than reasoned about: the guard's blast radius is only acceptable if the +/// inspectors that exist today pass it. +#[test] +fn test_the_production_tracer_is_admitted() { + use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; + + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + TracingInspector::new(TracingInspectorConfig::all()), + ); + + let tx = envelope(0); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("the tracer every replay uses must not be refused"); + + assert!(outcome.result.is_success(), "fixture check: {:?}", outcome.result); + assert!( + outcome.inner.inspector_ledger.is_zero(), + "a tracer must leave every lane untouched; got {:?}", + outcome.inner.inspector_ledger, + ); + executor.commit_transaction_outcome(outcome).expect("nor at commit"); + + assert!( + executor.evm().inspector.traces().nodes().len() >= 2, + "fixture check: the tracer must have recorded the nested frame it was given", + ); + let (_, result) = executor.finish().expect("the block must finish"); + assert_eq!(result.receipts.len(), 1); +} From 69cec4bb8837c928d3c4cef8f49f3045b3e94c62 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 14:16:13 +0800 Subject: [PATCH 112/208] test(rex7): sweep the inspector trait surface, callback by rewrite shape --- .../tests/rex7/inspector_cheat_matrix.rs | 1289 +++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + 2 files changed, 1290 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs new file mode 100644 index 00000000..1056b971 --- /dev/null +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -0,0 +1,1289 @@ +//! Every rewrite shape, at every callback that can carry it. +//! +//! `tests/rex7/measured_inspector.rs` pins one mechanism per test, chosen because each is a +//! different half of the measurement shim. This module asks the complementary question: not "does +//! each mechanism work" but "is there a callback on the `Inspector` trait, or a rewrite shape a +//! callback admits, that nothing measures". So the cases here are laid out as a matrix over the +//! trait's own surface — one row per callback, one column per rewrite shape — rather than over the +//! shapes any particular tool is known to use. A callback added upstream, or a shape a callback +//! newly admits, shows up as an empty cell. +//! +//! The matrix is machine-checked, not documented: [`test_the_matrix_leaves_no_cell_unaccounted`] +//! enumerates every row × column pair and requires each to be either covered by a case below or +//! named in [`inapplicable`] with the reason it cannot exist. A doc table would go stale the first +//! time a callback grew a mutable argument. +//! +//! # What every cell asserts +//! +//! - **The ledger recorded what the cheat did**, on the lane it belongs to and to the gas — an +//! under-booked lane is what makes a transaction's reported numbers a fiction, and an over-booked +//! one is the same failure with the sign flipped. +//! - **The conservation law closes** against the envelope the receipt reports. This is the +//! assertion the whole ledger exists to keep true, and it is the one that goes red when a lane is +//! missed. +//! - **The state agrees with the result the caller was handed.** A cheat that fails a frame must +//! leave that frame's writes rolled back, and one that revives a reverted frame must leave them +//! committed — the journal decision is taken after the last rewrite, so it has to follow it. +//! +//! # Which loop runs +//! +//! The matrix runs on the inspected loops, because that is where a callback exists at all. +//! [`test_the_matrix_is_inert_with_the_inspected_loops_switched_off`] samples it against the plain +//! loop through `set_inspector_enabled`: the same inspector, the same context, the same +//! transaction, and no callback — every sampled cell must then be bit-identical to a run with no +//! inspector attached, which is what says the shim itself contributes nothing. + +use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Address, Bytes, Log, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, + MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, + MegaTransactionOutcome, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, SSTORE, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr, JournalTr}, + handler::{EvmTr, FrameResult}, + interpreter::{ + interpreter_types::{Jumps, MemoryTr, StackTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + Interpreter, InterpreterResult, InterpreterTypes, + }, + state::EvmState, + Inspector, +}; +use std::{collections::BTreeMap, string::String, vec::Vec}; + +/// High enough that EVM gas is never what binds. +const TX_GAS_LIMIT: u64 = 100_000_000; +/// Gas the fixture's inner `CALL` forwards. +const INNER_CALL_GAS: u64 = 2_000_000; + +/// Gas an injecting cheat writes into a live interpreter's counter. +const INJECT: u64 = 3_000; +/// Gas a draining cheat takes out of one. +const DRAIN: u64 = 1_000; +/// Gas an envelope cheat adds to, or removes from, a frame input's `gas_limit`. +const ENVELOPE: u64 = 5_000; +/// Gas a result cheat adds to, or removes from, a frame result's remaining gas. +const RESULT: u64 = 2_000; + +/// Slot the top frame writes, last of all, so a cheat that fails the top frame is visible. +const TOP_SLOT: u64 = 0x10; +/// Slot the inner `CALL`'s callee writes. +const CALLEE_SLOT: u64 = 0x20; +/// Slot the fixture's constructor writes. +const INIT_SLOT: u64 = 0x30; +/// Value every fixture write stores, so a stack cheat that bumps it is visible as `2`. +const STORED: u64 = 1; + +/// The address the fixture's `CREATE` deploys to. +fn deployed_address() -> Address { + CONTRACT.create(0) +} + +// --- rows and columns ----------------------------------------------------------------------- + +/// One row of the matrix: a callback on the `Inspector` trait. +/// +/// Every method of the trait is here. `log` is the one that never fires in these fixtures — it is +/// reached only when a precompile's logs are forwarded, which no wired `MegaETH` precompile +/// produces — and [`inapplicable`] carries that, together with the reason its every column is +/// empty anyway. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum At { + InitializeInterp, + Step, + StepEnd, + Log, + LogFull, + FrameStart, + FrameEnd, + Call, + CallEnd, + Create, + CreateEnd, + Selfdestruct, +} + +impl At { + const ALL: [Self; 12] = [ + Self::InitializeInterp, + Self::Step, + Self::StepEnd, + Self::Log, + Self::LogFull, + Self::FrameStart, + Self::FrameEnd, + Self::Call, + Self::CallEnd, + Self::Create, + Self::CreateEnd, + Self::Selfdestruct, + ]; +} + +/// One column of the matrix: a shape a rewrite can take. +/// +/// The columns are the rewrite's *mechanism*, not its purpose: what argument it reaches through +/// and in which direction it moves it. Two shapes that move the same argument in opposite +/// directions are separate columns because the ledger's sign convention is exactly that +/// distinction, and a lane that books one direction and drops the other is a real failure mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum Shape { + /// Write gas into a live interpreter's counter. + InjectGas, + /// Take gas out of one. + DrainGas, + /// Raise the `gas_limit` a frame is about to be built with. + RaiseEnvelope, + /// Lower it. + LowerEnvelope, + /// Edit a semantic field of a frame's inputs — what the frame will do, not what it costs. + EditInput, + /// Return a synthetic outcome, so no frame is built at all. + Intercept, + /// Raise the gas a finished frame hands back to its caller. + RaiseResultGas, + /// Lower it. + LowerResultGas, + /// Rewrite a successful frame result into a failure. + FailResult, + /// Rewrite a failed frame result into a success. + ReviveResult, + /// Edit the interpreter's stack or memory — the frame's working state, which the EVM reads + /// back as operands and as data. + EditStackOrMemory, + /// Write to the journal directly, behind the EVM's back. + JournalWrite, +} + +impl Shape { + const ALL: [Self; 12] = [ + Self::InjectGas, + Self::DrainGas, + Self::RaiseEnvelope, + Self::LowerEnvelope, + Self::EditInput, + Self::Intercept, + Self::RaiseResultGas, + Self::LowerResultGas, + Self::FailResult, + Self::ReviveResult, + Self::EditStackOrMemory, + Self::JournalWrite, + ]; +} + +/// Why a row × column pair cannot be covered, for every pair the matrix leaves out. +/// +/// A cell is left out only when the callback's signature makes the shape unreachable, or when +/// reaching it is a different mechanism that its own test already pins. Anything else is a hole. +fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { + use At::*; + use Shape::*; + + // Callbacks that receive nothing mutable but the context. + if at == Log { + return match shape { + JournalWrite => Some( + "`log` is reached only by the precompile-log forwarding, which no wired MegaETH \ + precompile produces; the forwarding itself is pinned by \ + `execution.rs::test_precompile_logs_reach_the_inspector_from_both_places_they_live`", + ), + _ => Some("`log` takes the log by value and no interpreter or frame input at all"), + }; + } + if at == Selfdestruct { + return Some( + "`selfdestruct` takes every argument by value and is handed no context, so it has no \ + mutable surface; `test_a_selfdestruct_only_inspector_moves_nothing` pins that the \ + shim still forwards it", + ); + } + + let interpreter_facing = matches!(at, InitializeInterp | Step | StepEnd | LogFull); + let input_facing = matches!(at, FrameStart | Call | Create); + let result_facing = matches!(at, FrameEnd | CallEnd | CreateEnd); + + match shape { + InjectGas | DrainGas | EditStackOrMemory if !interpreter_facing => { + Some("no live interpreter is reachable from this callback") + } + RaiseEnvelope | LowerEnvelope | EditInput | Intercept if !input_facing => Some( + "this callback receives no frame input it can build a frame from: the `*_end` \ + callbacks take theirs by shared reference, after the frame has already run", + ), + RaiseResultGas | LowerResultGas | FailResult | ReviveResult if !result_facing => { + Some("no frame result exists yet at this callback") + } + ReviveResult if at == CreateEnd => Some( + "refused outright rather than measured — `test_reviving_a_failed_creation_is_refused` \ + in `measured_inspector.rs` pins the refusal at `create_end`, and \ + `test_reviving_a_failed_creation_is_refused_at_frame_end` pins it one callback later", + ), + _ => None, + } +} + +// --- the cheating inspector ----------------------------------------------------------------- + +/// Applies one cell's rewrite, once, and records what it actually did. +/// +/// Firing once rather than on every callback is what makes the ledger assertions exact: the cell +/// says "this many gas, on this lane", and a trickle would only support an inequality. +#[derive(Debug)] +struct Cheat { + at: At, + shape: Shape, + /// How many times the cheat fired. Every cell asserts this is 1, so a fixture that stops + /// reaching a callback fails loudly instead of passing as a run that cheated nothing. + fired: u32, + /// Ordinal of the `step` / `step_end` callback the interpreter-facing rows fire at. + step_at: u64, + steps: u64, + /// Gas actually moved on the interpreter lane, as the cheat measured it. + moved_gas: i128, +} + +impl Cheat { + fn new(at: At, shape: Shape) -> Self { + Self { at, shape, fired: 0, step_at: 8, steps: 0, moved_gas: 0 } + } + + /// Whether this callback is the cheat's row, and the cheat has not fired yet. + fn arm(&self, at: At) -> bool { + at == self.at && self.fired == 0 + } + + /// Applies an interpreter-facing shape. + fn hit_interpreter(&mut self, interp: &mut Interpreter) { + match self.shape { + Shape::InjectGas => { + interp.gas.erase_cost(INJECT); + self.moved_gas += i128::from(INJECT); + self.fired += 1; + } + Shape::DrainGas => { + assert!( + interp.gas.record_regular_cost(DRAIN), + "the fixture must leave enough gas for a {DRAIN} gas removal to land", + ); + self.moved_gas -= i128::from(DRAIN); + self.fired += 1; + } + Shape::EditStackOrMemory => { + // Bump the value an `SSTORE` is about to write, so the edit is visible in the + // produced state rather than only in the absence of an accounting change. + let [key, value] = + interp.stack.popn::<2>().expect("an SSTORE has both its operands on the stack"); + assert!(interp.stack.push(value.wrapping_add(U256::from(1)))); + assert!(interp.stack.push(key)); + self.fired += 1; + } + _ => unreachable!("{:?} is not an interpreter-facing shape", self.shape), + } + } + + /// Overwrites the frame's first memory word — the edit the three interpreter-facing rows + /// that cannot safely touch the stack use instead. + /// + /// `initialize_interp` runs before the frame has any memory and before any operand exists, so + /// there it leaves a word at the bottom of the stack, under everything the frame will push; + /// `step_end` and `log_full` run between opcodes, where a pushed word would be consumed as the + /// next opcode's operand and would change the fixture rather than cheat inside it. + fn hit_frame_state(&mut self, interp: &mut Interpreter) { + if interp.memory.size() >= 32 { + interp.memory.set(0, &[0xAB; 32]); + } else { + assert!(interp.stack.push(U256::from(0xDEADu64))); + } + self.fired += 1; + } + + /// Applies the one shape that reaches past the EVM entirely. + fn hit_journal(&mut self, context: &mut CTX) { + context.journal_mut().tstore(CONTRACT, U256::from(0xF00Du64), U256::from(1)); + self.fired += 1; + } + + /// Applies an input-facing shape to a call's inputs, or intercepts the frame. + fn hit_call_inputs(&mut self, inputs: &mut CallInputs) -> Option { + match self.shape { + Shape::RaiseEnvelope => { + inputs.gas_limit += ENVELOPE; + self.fired += 1; + None + } + Shape::LowerEnvelope => { + inputs.gas_limit -= ENVELOPE; + self.fired += 1; + None + } + Shape::EditInput => { + // A static call: the callee's `SSTORE` now fails, which is a change to what the + // frame does rather than to what it is allowed to spend. + inputs.is_static = true; + self.fired += 1; + None + } + Shape::Intercept => { + self.fired += 1; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + _ => unreachable!("{:?} is not an input-facing shape", self.shape), + } + } + + /// Applies an input-facing shape to a creation's inputs, or intercepts the frame. + fn hit_create_inputs(&mut self, inputs: &mut CreateInputs) -> Option { + match self.shape { + Shape::RaiseEnvelope => { + inputs.set_gas_limit(inputs.gas_limit() + ENVELOPE); + self.fired += 1; + None + } + Shape::LowerEnvelope => { + inputs.set_gas_limit(inputs.gas_limit() - ENVELOPE); + self.fired += 1; + None + } + Shape::EditInput => { + // Init code that reverts immediately: PUSH1 0, PUSH1 0, REVERT. + inputs.set_init_code(Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd])); + self.fired += 1; + None + } + Shape::Intercept => { + self.fired += 1; + Some(CreateOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit()), + ), + None, + )) + } + _ => unreachable!("{:?} is not an input-facing shape", self.shape), + } + } + + /// Applies a result-facing shape to a finished frame's result. + fn hit_result(&mut self, result: &mut InterpreterResult) { + match self.shape { + Shape::RaiseResultGas => { + result.gas.erase_cost(RESULT); + self.fired += 1; + } + Shape::LowerResultGas => { + assert!( + result.gas.record_regular_cost(RESULT), + "the fixture must leave the frame enough gas for a {RESULT} gas removal", + ); + self.fired += 1; + } + Shape::FailResult => { + assert!( + result.result.is_ok(), + "the fixture must hand this cell a successful frame" + ); + result.result = InstructionResult::Revert; + self.fired += 1; + } + Shape::ReviveResult => { + assert!( + result.result.is_revert(), + "the fixture must hand this cell a reverted frame, got {:?}", + result.result, + ); + result.result = InstructionResult::Stop; + self.fired += 1; + } + _ => unreachable!("{:?} is not a result-facing shape", self.shape), + } + } +} + +/// Whether a frame input is the fixture's inner call — the one frame every input-facing and +/// result-facing cell targets, so the top-level frame is never the one rewritten. +fn is_inner_call(input: &FrameInput) -> bool { + matches!(input, FrameInput::Call(inputs) if inputs.target_address == CALLEE) +} + +impl Inspector for Cheat { + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if !self.arm(At::InitializeInterp) { + return; + } + match self.shape { + Shape::JournalWrite => self.hit_journal(context), + Shape::EditStackOrMemory => self.hit_frame_state(interp), + _ => self.hit_interpreter(interp), + } + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.steps += 1; + if !self.arm(At::Step) { + return; + } + match self.shape { + Shape::JournalWrite if self.steps == self.step_at => self.hit_journal(context), + // Fire on the first `SSTORE` the transaction reaches, whose operands are on the stack + // and about to be consumed. + Shape::EditStackOrMemory if interp.bytecode.opcode() == SSTORE => { + self.hit_interpreter(interp) + } + Shape::EditStackOrMemory | Shape::JournalWrite => {} + _ if self.steps == self.step_at => self.hit_interpreter(interp), + _ => {} + } + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if !self.arm(At::StepEnd) || self.steps != self.step_at { + return; + } + match self.shape { + Shape::JournalWrite => self.hit_journal(context), + Shape::EditStackOrMemory => self.hit_frame_state(interp), + _ => self.hit_interpreter(interp), + } + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, _log: Log) { + if !self.arm(At::LogFull) { + return; + } + match self.shape { + Shape::JournalWrite => self.hit_journal(context), + Shape::EditStackOrMemory => self.hit_frame_state(interp), + _ => self.hit_interpreter(interp), + } + } + + fn frame_start( + &mut self, + context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + if !self.arm(At::FrameStart) || !is_inner_call(frame_input) { + return None; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return None; + } + let FrameInput::Call(inputs) = frame_input else { unreachable!() }; + self.hit_call_inputs(inputs).map(FrameResult::Call) + } + + fn frame_end(&mut self, context: &mut CTX, frame_input: &FrameInput, result: &mut FrameResult) { + if !self.arm(At::FrameEnd) || !is_inner_call(frame_input) { + return; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return; + } + self.hit_result(result.interpreter_result_mut()); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + if !self.arm(At::Call) || inputs.target_address != CALLEE { + return None; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return None; + } + self.hit_call_inputs(inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if !self.arm(At::CallEnd) || inputs.target_address != CALLEE { + return; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return; + } + self.hit_result(&mut outcome.result); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + if !self.arm(At::Create) { + return None; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return None; + } + self.hit_create_inputs(inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if !self.arm(At::CreateEnd) { + return; + } + if self.shape == Shape::JournalWrite { + self.hit_journal(context); + return; + } + self.hit_result(&mut outcome.result); + } +} + +// --- fixtures ------------------------------------------------------------------------------- + +/// Which contract the fixture's inner `CALL` reaches. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum Fixture { + /// The callee writes storage and returns. Every cell that rewrites a *successful* frame uses + /// this one. + ReturningCallee, + /// The callee writes storage and reverts. The two `ReviveResult` cells need a failed frame to + /// revive, and need the write behind it to be one the journal has already decided to roll + /// back — so that a revival that commits it is visible. + RevertingCallee, +} + +/// Init code that writes [`INIT_SLOT`] and returns two bytes of runtime code. +fn init_code() -> Vec { + BytecodeBuilder::default() + .sstore(U256::from(INIT_SLOT), U256::from(STORED)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset: the last two bytes of the word just stored + .append(RETURN) + .build() + .to_vec() +} + +/// The transaction's entry contract: one `LOG1`, one inner `CALL`, one `CREATE`, one `SSTORE`. +/// +/// One fixture rather than one per row, so that every callback fires in the same transaction and +/// a cell's assertions are about the cheat rather than about which fixture it got. +fn caller_code() -> Bytes { + let init = init_code(); + let mut builder = BytecodeBuilder::default() + // A word in memory for the LOG to read. + .push_number(0xAAu64) + .push_number(0u64) + .append(MSTORE) + // LOG1(offset=0, size=32, topic=1) + .push_number(1u64) + .push_number(32u64) + .push_number(0u64) + .append(LOG1) + // CALL(gas, CALLEE, value=0, argsOffset=0, argsSize=0, retOffset=0, retSize=0) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(u128::from(INNER_CALL_GAS)) + .append(CALL) + .append(POP); + // The init code, byte by byte, into memory at offset 0. + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + builder + .push_number(init.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .sstore(U256::from(TOP_SLOT), U256::from(STORED)) + .append(STOP) + .build() +} + +fn callee_code(fixture: Fixture) -> Bytes { + let builder = BytecodeBuilder::default().sstore(U256::from(CALLEE_SLOT), U256::from(STORED)); + match fixture { + Fixture::ReturningCallee => builder.append(STOP).build(), + Fixture::RevertingCallee => builder.revert().build(), + } +} + +fn db_for(fixture: Fixture) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, caller_code()) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, callee_code(fixture)) +} + +// --- running one cell ----------------------------------------------------------------------- + +/// Everything one transaction reports, plus what the shim booked for it. +struct Reading { + result: ExecutionResult, + compute_gas: u64, + enforced: u64, + destroyed: u64, + data_size: u64, + kv_updates: u64, + state_growth: u64, + gas_used: u64, + total_gas_spent: u64, + terms: ConservationTerms, + ledger: InspectorLedger, + state: EvmState, +} + +impl Reading { + /// A storage slot as the produced state has it, zero when the transaction never wrote it. + fn slot(&self, address: Address, slot: u64) -> U256 { + self.state + .get(&address) + .and_then(|account| account.storage.get(&U256::from(slot))) + .map(|value| value.present_value()) + .unwrap_or_default() + } + + /// Whether the fixture's `CREATE` left code behind. + fn deployed(&self) -> bool { + self.state + .get(&deployed_address()) + .is_some_and(|account| !account.info.is_empty_code_hash()) + } + + /// The produced state, rendered order-independently for a bit-for-bit comparison. + fn render_state(&self) -> String { + let canonical: BTreeMap = self + .state + .iter() + .map(|(address, account)| { + let storage: BTreeMap = account + .storage + .iter() + .map(|(slot, value)| (*slot, value.present_value())) + .collect(); + ( + *address, + std::format!( + "{:?}/{}/{:?}/{storage:?}", + account.info.balance, + account.info.nonce, + account.info.code_hash + ), + ) + }) + .collect(); + std::format!("{canonical:?}") + } +} + +/// The conservation identity, stated with the term the measurement shim contributes. +/// +/// This is the assertion that goes red when a lane the shim should have booked went unbooked: the +/// two sides then disagree by precisely the unbooked amount. +fn assert_identity(label: &str, r: &Reading) { + assert_eq!( + r.compute_gas, + r.enforced + r.destroyed, + "{label}: reported compute must split into enforced + destroyed", + ); + assert_eq!( + r.terms.inspector_conjured_gas, + r.ledger.conjured_gas(), + "{label}: the law's `I` term is the ledger's net, and nothing else", + ); + assert_eq!( + r.terms.envelope_for(r.destroyed), + i128::from(r.total_gas_spent), + "{label}: the law must close against the envelope the receipt reports; \ + reported compute={} destroyed={} envelope={} ({})", + r.compute_gas, + r.destroyed, + r.total_gas_spent, + r.terms, + ); +} + +fn tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX7) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context +} + +fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { + assert_eq!( + outcome.inspector_ledger, + limit.inspector_ledger(), + "the outcome must report the ledger the shim booked, unchanged", + ); + let gas_used = outcome.result_and_state.result.tx_gas_used(); + let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); + Reading { + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, + gas_used, + total_gas_spent, + terms: limit.conservation_terms(), + ledger: outcome.inspector_ledger, + state: outcome.result_and_state.state, + } +} + +/// Runs the fixture with no inspector at all. +fn transact_plain(fixture: Fixture) -> Reading { + let mut db = db_for(fixture); + let mut evm = MegaEvm::new(context(&mut db)); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +/// Runs the fixture with `cheat` attached, on the inspected loops or with them switched off. +fn transact_cheating(fixture: Fixture, cheat: &mut Cheat, inspected: bool) -> Reading { + let mut db = db_for(fixture); + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(cheat); + if !inspected { + alloy_evm::Evm::set_inspector_enabled(&mut evm, false); + } + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +// --- the matrix ----------------------------------------------------------------------------- + +/// One cell: a callback, a shape, and what the two must produce together. +struct Cell { + at: At, + shape: Shape, + fixture: Fixture, + /// The ledger the shim must have booked, exactly. + ledger: InspectorLedger, + /// What the produced state must show, given that the cheat landed. + state: fn(&Reading, &str), +} + +fn ledger_gas(gas: i128) -> InspectorLedger { + InspectorLedger { gas, env: 0, result: 0, rejected_rewrites: 0 } +} + +fn ledger_env(env: i128) -> InspectorLedger { + InspectorLedger { gas: 0, env, result: 0, rejected_rewrites: 0 } +} + +fn ledger_result(result: i128) -> InspectorLedger { + InspectorLedger { gas: 0, env: 0, result, rejected_rewrites: 0 } +} + +/// The fixture ran to its end and every frame committed: the callee's write, the deployment, and +/// the top frame's own write are all there. +fn state_all_committed(r: &Reading, label: &str) { + assert!( + r.result.is_success(), + "{label}: expected a successful transaction, got {:?}", + r.result + ); + assert_eq!(r.slot(CALLEE, CALLEE_SLOT), U256::from(STORED), "{label}: the callee's write"); + assert!(r.deployed(), "{label}: the fixture's CREATE must have deployed code"); + assert_eq!(r.slot(CONTRACT, TOP_SLOT), U256::from(STORED), "{label}: the top frame's write"); +} + +/// The callee's frame did not commit: whatever ended it, its write is gone. +fn state_callee_write_rolled_back(r: &Reading, label: &str) { + assert!( + r.result.is_success(), + "{label}: the caller absorbs the inner failure, got {:?}", + r.result + ); + assert_eq!( + r.slot(CALLEE, CALLEE_SLOT), + U256::ZERO, + "{label}: a frame the caller was told failed must leave no write behind", + ); + assert!(r.deployed(), "{label}: the rest of the transaction must be unaffected"); +} + +/// The callee's frame committed a write the EVM had decided to roll back — the journal followed +/// the rewritten result rather than the classification. +fn state_callee_write_revived(r: &Reading, label: &str) { + assert!(r.result.is_success(), "{label}: {:?}", r.result); + assert_eq!( + r.slot(CALLEE, CALLEE_SLOT), + U256::from(STORED), + "{label}: a reverted frame rewritten into a success must have its state committed with it", + ); +} + +/// The stack edit landed on the callee's `SSTORE` operand. +fn state_callee_write_bumped(r: &Reading, label: &str) { + assert!(r.result.is_success(), "{label}: {:?}", r.result); + assert_eq!( + r.slot(CALLEE, CALLEE_SLOT), + U256::from(STORED + 1), + "{label}: the value the inspector put on the stack is the value the EVM wrote", + ); +} + +/// The creation did not happen. +fn state_no_deployment(r: &Reading, label: &str) { + assert!(r.result.is_success(), "{label}: the caller absorbs it, got {:?}", r.result); + assert!(!r.deployed(), "{label}: no code may be deployed"); + assert_eq!(r.slot(deployed_address(), INIT_SLOT), U256::ZERO, "{label}: nor its storage write"); +} + +/// Every cell the matrix covers. +fn matrix() -> Vec { + use At::*; + use Shape::*; + + let mut cells = Vec::new(); + let mut push = |at, shape, fixture, ledger, state: fn(&Reading, &str)| { + cells.push(Cell { at, shape, fixture, ledger, state }); + }; + + // The four callbacks that are handed a live interpreter. + for at in [InitializeInterp, Step, StepEnd, LogFull] { + push( + at, + InjectGas, + Fixture::ReturningCallee, + ledger_gas(i128::from(INJECT)), + state_all_committed, + ); + push( + at, + DrainGas, + Fixture::ReturningCallee, + ledger_gas(-i128::from(DRAIN)), + state_all_committed, + ); + push( + at, + EditStackOrMemory, + Fixture::ReturningCallee, + InspectorLedger::default(), + if at == Step { state_callee_write_bumped } else { state_all_committed }, + ); + push( + at, + JournalWrite, + Fixture::ReturningCallee, + InspectorLedger::default(), + state_all_committed, + ); + } + + // The three callbacks that are handed a frame's inputs before the frame is built. `create` + // reaches the fixture's `CREATE`; the other two reach its inner `CALL`. + for at in [FrameStart, Call, Create] { + let deployment_side = at == Create; + push( + at, + RaiseEnvelope, + Fixture::ReturningCallee, + ledger_env(i128::from(ENVELOPE)), + state_all_committed, + ); + push( + at, + LowerEnvelope, + Fixture::ReturningCallee, + ledger_env(-i128::from(ENVELOPE)), + state_all_committed, + ); + push( + at, + EditInput, + Fixture::ReturningCallee, + InspectorLedger::default(), + if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, + ); + push( + at, + Intercept, + Fixture::ReturningCallee, + InspectorLedger::default(), + if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, + ); + push( + at, + JournalWrite, + Fixture::ReturningCallee, + InspectorLedger::default(), + state_all_committed, + ); + } + + // The three callbacks that are handed a finished frame's result. + for at in [FrameEnd, CallEnd, CreateEnd] { + let creation = at == CreateEnd; + push( + at, + RaiseResultGas, + Fixture::ReturningCallee, + ledger_result(i128::from(RESULT)), + state_all_committed, + ); + push( + at, + LowerResultGas, + Fixture::ReturningCallee, + ledger_result(-i128::from(RESULT)), + state_all_committed, + ); + push( + at, + FailResult, + Fixture::ReturningCallee, + InspectorLedger::default(), + if creation { state_no_deployment } else { state_callee_write_rolled_back }, + ); + if !creation { + // Reviving a reverted *call* is honoured; the creation form is refused, and the two + // tests that pin the refusal are named in `inapplicable`. + push( + at, + ReviveResult, + Fixture::RevertingCallee, + InspectorLedger::default(), + state_callee_write_revived, + ); + } + push( + at, + JournalWrite, + Fixture::ReturningCallee, + InspectorLedger::default(), + state_all_committed, + ); + } + + cells +} + +fn label(at: At, shape: Shape) -> String { + std::format!("{at:?} × {shape:?}") +} + +// --- the tests ------------------------------------------------------------------------------ + +/// Every cell of the matrix, run: the ledger books exactly what the cheat did, the conservation +/// law closes against the receipt, and the state agrees with the result the caller was handed. +#[test] +fn test_every_cheat_shape_is_booked_and_the_law_still_closes() { + for cell in matrix() { + let label = label(cell.at, cell.shape); + let mut cheat = Cheat::new(cell.at, cell.shape); + let reading = transact_cheating(cell.fixture, &mut cheat, true); + + assert_eq!(cheat.fired, 1, "{label}: the fixture must reach this callback exactly once"); + assert_eq!( + reading.ledger, cell.ledger, + "{label}: the shim must book exactly what the cheat did, and nothing else", + ); + // The interpreter lane the cheat measured for itself and the lane the shim booked are two + // independent readings of the same edit. + if cheat.moved_gas != 0 { + assert_eq!( + reading.ledger.gas, cheat.moved_gas, + "{label}: the shim's reading of the counter edit must match the cheat's own", + ); + } + assert_identity(&label, &reading); + (cell.state)(&reading, &label); + } +} + +/// The matrix has an entry, or a stated reason not to, for every callback × shape pair. +/// +/// This is what keeps the coverage honest as the trait moves: a callback revm adds, or a mutable +/// argument a callback grows, produces a pair that is neither covered nor explained, and this test +/// names it. A table in a doc comment could not. +#[test] +fn test_the_matrix_leaves_no_cell_unaccounted() { + let covered: Vec<(At, Shape)> = matrix().iter().map(|c| (c.at, c.shape)).collect(); + let mut holes = Vec::new(); + let (mut tested, mut excused) = (0usize, 0usize); + + for at in At::ALL { + for shape in Shape::ALL { + match (covered.contains(&(at, shape)), inapplicable(at, shape)) { + (true, None) => tested += 1, + (false, Some(_)) => excused += 1, + (true, Some(reason)) => holes.push(std::format!( + "{} is both covered and excused ({reason})", + label(at, shape) + )), + (false, None) => holes.push(std::format!( + "{} has no case and no stated reason it cannot have one", + label(at, shape) + )), + } + } + } + + assert!(holes.is_empty(), "the matrix has holes:\n {}", holes.join("\n ")); + assert_eq!( + tested + excused, + At::ALL.len() * Shape::ALL.len(), + "every pair must fall into exactly one of the two buckets", + ); + assert_eq!(tested, matrix().len(), "no cell may be listed twice"); +} + +/// With the inspected loops switched off, the same inspector is inert — and inertness is +/// bit-for-bit, against a run with no inspector attached at all. +/// +/// This is the two-loop half of the matrix. The sample is one cell per callback family, chosen so +/// that every kind of cheat is represented: a counter edit, an envelope edit, a result edit, a +/// classification rewrite, and a journal write. +#[test] +fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { + let sample = [ + (At::Step, Shape::InjectGas, Fixture::ReturningCallee), + (At::StepEnd, Shape::DrainGas, Fixture::ReturningCallee), + (At::Call, Shape::RaiseEnvelope, Fixture::ReturningCallee), + (At::FrameStart, Shape::Intercept, Fixture::ReturningCallee), + (At::Create, Shape::EditInput, Fixture::ReturningCallee), + (At::CallEnd, Shape::LowerResultGas, Fixture::ReturningCallee), + (At::CreateEnd, Shape::FailResult, Fixture::ReturningCallee), + (At::FrameEnd, Shape::ReviveResult, Fixture::RevertingCallee), + (At::Step, Shape::JournalWrite, Fixture::ReturningCallee), + ]; + + let mut plain: BTreeMap = BTreeMap::new(); + for fixture in [Fixture::ReturningCallee, Fixture::RevertingCallee] { + plain.insert(fixture, transact_plain(fixture)); + } + + for (at, shape, fixture) in sample { + let label = label(at, shape); + let mut cheat = Cheat::new(at, shape); + let off = transact_cheating(fixture, &mut cheat, false); + let reference = &plain[&fixture]; + + assert_eq!(cheat.fired, 0, "{label}: no callback may run with the inspected loops off"); + assert!( + off.ledger.is_zero(), + "{label}: an inert inspector books nothing: {:?}", + off.ledger + ); + assert_eq!( + std::format!("{:?}", off.result), + std::format!("{:?}", reference.result), + "{label}" + ); + assert_eq!(off.compute_gas, reference.compute_gas, "{label}"); + assert_eq!(off.enforced, reference.enforced, "{label}"); + assert_eq!(off.destroyed, reference.destroyed, "{label}"); + assert_eq!(off.data_size, reference.data_size, "{label}"); + assert_eq!(off.kv_updates, reference.kv_updates, "{label}"); + assert_eq!(off.state_growth, reference.state_growth, "{label}"); + assert_eq!(off.gas_used, reference.gas_used, "{label}"); + assert_eq!(off.total_gas_spent, reference.total_gas_spent, "{label}"); + assert_eq!(off.terms, reference.terms, "{label}"); + assert_eq!(off.render_state(), reference.render_state(), "{label}: the produced state"); + assert_identity(&label, &off); + } +} + +/// The two fixtures are what the cells assume they are, checked without an inspector in the way. +/// +/// Every "the cheat moved this" assertion is a comparison against this baseline; if the returning +/// callee ever stopped committing its write, or the reverting one started, half the matrix would +/// assert the fixture rather than the mechanism and would still be green. +#[test] +fn test_the_fixtures_behave_as_the_cells_assume() { + let returning = transact_plain(Fixture::ReturningCallee); + state_all_committed(&returning, "returning callee"); + assert_identity("returning callee", &returning); + + let reverting = transact_plain(Fixture::RevertingCallee); + assert!(reverting.result.is_success(), "the caller absorbs the revert: {:?}", reverting.result); + assert_eq!( + reverting.slot(CALLEE, CALLEE_SLOT), + U256::ZERO, + "the reverting callee's write must be rolled back without an inspector", + ); + assert_identity("reverting callee", &reverting); +} + +/// `frame_end` is the last callback that can rewrite a creation's classification, and the refusal +/// covers it too. +/// +/// `measured_inspector.rs` pins the `create_end` form. This is the one callback later: revm calls +/// `create_end` first and `frame_end` after it, so an inspector that leaves `create_end` alone and +/// rewrites in `frame_end` would slip past a refusal wired only to the earlier one. +#[test] +fn test_reviving_a_failed_creation_is_refused_at_frame_end() { + /// Rewrites a failed creation into a success, from `frame_end` only. + struct LateReviver; + + impl Inspector for LateReviver { + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + let FrameResult::Create(outcome) = frame_result else { return }; + if !outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Stop; + } + } + } + + // Init code that reverts immediately: PUSH1 0, PUSH1 0, REVERT. + let init: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + + let run = move || { + let mut db = MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code.clone()) + .account_balance(CONTRACT, U256::from(ONE_ETH)); + let mut inspector = LateReviver; + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut inspector); + evm.execute_transaction(tx()).map(|_| ()).map_err(|e| std::format!("{e:?}")) + }; + + assert_refused(run); +} + +/// Drives `run` and asserts the shim refused the rewrite, however this build surfaces a refusal: +/// a debug build asserts (the shape is a detector, and a corpus that produces it should stop), a +/// release build fails the transaction with the same message. +fn assert_refused(run: impl Fn() -> Result<(), String>) { + const MESSAGE: &str = "inspector rewrote a failed contract creation into a successful one"; + if cfg!(debug_assertions) { + let previous = std::panic::take_hook(); + std::panic::set_hook(std::boxed::Box::new(|_| {})); + let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)); + std::panic::set_hook(previous); + let payload = panicked.expect_err("the detector must fire in debug builds"); + let message = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or_default(); + assert!(message.contains(MESSAGE), "the assertion must name the shape; got {message:?}"); + } else { + let error = run().expect_err("the refusal must surface as an EVMError in release builds"); + assert!(error.contains(MESSAGE), "the error must name the shape; got {error:?}"); + } +} + +/// An inspector that implements only `selfdestruct` moves nothing. +/// +/// The callback takes every argument by value and is handed no context, so there is nothing for +/// the shim to measure — which is exactly why it is worth pinning that the shim still *forwards* +/// it. A wrapper that dropped the callback would be invisible to every accounting assertion in +/// this file. +#[test] +fn test_a_selfdestruct_only_inspector_moves_nothing() { + use revm::bytecode::opcode::SELFDESTRUCT; + + #[derive(Default)] + struct Watcher { + seen: Vec<(Address, Address, U256)>, + } + + impl Inspector for Watcher { + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.seen.push((contract, target, value)); + } + } + + let callee = BytecodeBuilder::default().push_address(CALLER).append(SELFDESTRUCT).build(); + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(u128::from(INNER_CALL_GAS)) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code.clone()) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, callee.clone()) + .account_balance(CALLEE, U256::from(ONE_ETH)) + }; + + let plain = { + let mut db = build_db(); + let mut evm = MegaEvm::new(context(&mut db)); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading + }; + + let mut watcher = Watcher::default(); + let watched = { + let mut db = build_db(); + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut watcher); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading + }; + + assert_eq!(watcher.seen.len(), 1, "the shim must forward the callback: {:?}", watcher.seen); + assert_eq!(watcher.seen[0].0, CALLEE, "the self-destructing contract"); + assert_eq!(watcher.seen[0].1, CALLER, "the beneficiary"); + assert!(watched.ledger.is_zero(), "nothing to measure: {:?}", watched.ledger); + assert_eq!(watched.compute_gas, plain.compute_gas); + assert_eq!(watched.total_gas_spent, plain.total_gas_spent); + assert_eq!(watched.render_state(), plain.render_state()); + assert_identity("selfdestruct", &watched); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 974c444d..3949ffce 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -86,6 +86,7 @@ mod frame_loop_parity; mod gas_clamp; mod gas_leakage; mod guard_pass_static_gas; +mod inspector_cheat_matrix; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; From 875c9f932c6edb4ad59c246b52e673df8fa833e4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 14:25:12 +0800 Subject: [PATCH 113/208] feat(limit): route destroyed booking through an exhaustive InstructionResult table --- AGENTS.md | 1 + crates/mega-evm/src/limit/AGENTS.md | 6 + crates/mega-evm/src/limit/destroyed.rs | 125 ++++++ crates/mega-evm/src/limit/limit.rs | 30 +- crates/mega-evm/src/limit/mod.rs | 2 + crates/mega-evm/tests/rex7/main.rs | 5 + .../tests/rex7/result_space_tripwire.rs | 369 ++++++++++++++++++ docs/spec/evm/compute-gas.md | 15 + docs/spec/upgrades/rex7.md | 14 +- 9 files changed, 554 insertions(+), 13 deletions(-) create mode 100644 crates/mega-evm/src/limit/destroyed.rs create mode 100644 crates/mega-evm/tests/rex7/result_space_tripwire.rs diff --git a/AGENTS.md b/AGENTS.md index 561950b7..618f729f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -341,6 +341,7 @@ When the agent is requested to implement a new feature or bug fix, it should con - **Re-run the destroyed-gas conservation scan after a revm / alloy-evm upgrade.** The REX7 destroyed total is derived from the envelope, so any upstream change that moves gas without a MegaETH site recording it — a new minted subsidy like `CALL_STIPEND`, a changed refund or floor ordering, a new component of `total_gas_spent` — becomes a missing term in the law rather than a compile error. The frame-lifecycle mirror in `evm/frame.rs` is the same kind of exposure in the other direction: it is a re-ordering of `EthFrame::process_next_action` and `return_create`, so an upstream change to either becomes a silent divergence rather than a compile error. + Diff `InstructionResult` and the early-fail arms of `make_call_frame` / `make_create_frame` / `return_create` against `destroyed_disposition` (`limit/destroyed.rs`) and the arm list in `tests/rex7/result_space_tripwire.rs`: a new variant is a compile error until it is classified swallow / return / unreachable; a new early-fail arm has no type-level tie and must be assigned by hand, which is the CreateCollision-shaped gap the tripwire exists to catch. After bumping revm or alloy-evm, diff those two upstream functions against `evm/frame.rs`, then run `cargo test -p mega-evm` and `cargo test -p mega-state-test -p state-test` (the `debug_assert` cross-check is live in debug builds) plus the replay fixtures under the latest spec (`cargo run -p state-test -- --bench --bench-spec bench/replay/fixtures`), whose own `post` expectations pin an older spec and would otherwise give the derivation no coverage. - **Use `test_` prefix for Rust test function names.** New `#[test]` functions should be named with a `test_` prefix for consistency with this repository and upstream revm style. diff --git a/crates/mega-evm/src/limit/AGENTS.md b/crates/mega-evm/src/limit/AGENTS.md index 1b7fa316..59ff3fb7 100644 --- a/crates/mega-evm/src/limit/AGENTS.md +++ b/crates/mega-evm/src/limit/AGENTS.md @@ -5,6 +5,7 @@ Resource metering subsystem for transaction and frame limits across compute gas, ## STRUCTURE - `limit.rs`: `AdditionalLimit` coordinator and frame/tx lifecycle hooks. +- `destroyed.rs`: closed `InstructionResult` classification for the destroyed-remainder protocol (swallow / return / unreachable, no catch-all) and the producer × accounting-site table a revm bump diffs against. - `compute_gas.rs`: compute gas tracking, detention limits, frame budgets. - `data_size.rs`: tx/frame data accounting with revert-aware discard paths. - `kv_update.rs`: tx/frame KV accounting with revert-aware discard paths. @@ -20,6 +21,10 @@ Resource metering subsystem for transaction and frame limits across compute gas, - `AdditionalLimit::finalize_frame` is the single point a frame's outcome is settled — the destroyed-remainder booking, the frame-init refusal booking, the gas rescue, and the REX7 frame-local absorb — and it runs after the last callback that can rewrite the frame's classification and before the journal decision. Put a new frame-exit settlement there, not in a lifecycle hook on either side of it. The pops stay in `before_frame_return_result`: the paths that reach a caller without ever running a frame would double-pop. +- A frame result's remaining gas is swallowed or returned by `destroyed_disposition`, not by `is_ok_or_revert()`. + Every `InstructionResult` variant has an arm; a new variant is a compile error until it is classified. + A new destroyed-remainder *producer* belongs on the table in `destroyed.rs`, with its own accounting site. + The early-fail arms of `make_call_frame` / `make_create_frame` / `classify_create_return` are a second closed set with no type-level tie — diff them by hand on a revm bump against `tests/rex7/result_space_tripwire.rs`. - A frame-local exceed a frame could not latch — the one defined against its *caller's* budget after the merge — is settled in `before_frame_return_result` instead, and under REX7 before the pops rather than after them. `peek_check_limit_after_pop` answers the post-merge question over `FrameLimitTracker::view_after_pop`, so the reading is the merged one and only the timing moves; the pop that follows reads a revert and discards the frame's usage. Every dimension answers it with its own `check_limit` body over a `FrameLimitView`, and the two readings are cross-checked against each other on every frame return in debug builds. @@ -43,3 +48,4 @@ Resource metering subsystem for transaction and frame limits across compute gas, - Change compute detention behavior: `compute_gas.rs` and detention callers in `evm` module. - Change frame budget forwarding logic: `frame_limit.rs` and each tracker’s frame hooks. - Change storage call stipend semantics: `storage_call_stipend.rs` and `limit.rs` integration points. +- Classify a new `InstructionResult` variant or add a destroyed-remainder producer: `destroyed.rs` plus `tests/rex7/result_space_tripwire.rs`. diff --git a/crates/mega-evm/src/limit/destroyed.rs b/crates/mega-evm/src/limit/destroyed.rs new file mode 100644 index 00000000..c8bbc0ee --- /dev/null +++ b/crates/mega-evm/src/limit/destroyed.rs @@ -0,0 +1,125 @@ +//! Destroyed-remainder classification for a frame result's [`InstructionResult`]. +//! +//! The conservation law defines a transaction's destroyed total from the envelope. The per-site +//! bookings that cross-check it still have to decide, for each result, whether the remaining gas +//! was swallowed (book it) or handed back (book nothing). That decision used to be +//! `is_ok_or_revert()` — a catch-all on the halt side, so a variant revm added later would be +//! swallowed without anyone classifying it. +//! +//! [`destroyed_disposition`] is the closed table: every [`InstructionResult`] variant has an +//! explicit arm, and there is no `_`. A revm bump that adds a variant fails to compile here until +//! a human assigns it. +//! +//! # Producers × accounting sites +//! +//! After the frame-settlement single point, every producer that can destroy an envelope books at +//! exactly one of the sites below. Completeness of the *reported* total is still the conservation +//! law; this table is what the per-site bookings — and a revm-upgrade diff — are checked against. +//! +//! | Producer | Accounting site | Notes | +//! | --- | --- | --- | +//! | Frame-run exceptional halt, including create-return rejects (`CreateContractSizeLimit`, `CreateContractStartingWithEF`, deposit `OutOfGas`) | [`AdditionalLimit::finalize_frame`](super::AdditionalLimit::finalize_frame) on `FrameExit::Ran` → `settle_exceptional_halt_burn` | [`DestroyedDisposition::Swallow`] | +//! | Frame-init refusal from revm (`make_call_frame` / `make_create_frame` early-fail arms) | `finalize_frame` on `FrameExit::Refused` → `settle_frame_init_reject_burn` | per variant: collision / overflow-payment swallow; depth / funds / empty-code / nonce-overflow return | +//! | Synthetic frame-init refusal (system-contract interceptor, inspector intercept, REX5 depth guard) | `finalize_frame` on `FrameExit::RefusedSynthetically` → the same burn | same classification; a `KeylessDeploy` destroying halt is the row below, not this one | +//! | Precompile halt | the precompile recording site in `evm/precompiles.rs` | swallow of the unused forwarded envelope; KZG splits executed / destroyed by halt reason; `was_precompile_called` excludes this result from the frame-init burn | +//! | `KeylessDeploy` synthetic halt that keeps the call's gas (cannot pay dispatch overhead; cannot pay signer-materialization storage gas) | `sandbox/execution.rs::destroying_oog_frame_result`, which books remaining *before* the result spends the envelope | swallow; `finalize_frame` then sees remaining 0 and books nothing | +//! | Failed-deposit receipt rewrite | [`AdditionalLimit::settle_rewritten_envelope`](super::AdditionalLimit::settle_rewritten_envelope) | not an `InstructionResult` decision: the gap between the rebuilt envelope and the per-site bookings | +//! | Intrinsic pre-frame out-of-gas (`before_execution`) | `MegaHandler::before_execution` | swallow of `gas_limit − performed`; unreachable on REX7 (REX5+ rejects that transaction in validation) | +//! +//! A new producer belongs on this table with its own site, not as a silent extra call to +//! `record_burned_gas`. A new [`InstructionResult`] variant belongs in [`destroyed_disposition`]. +//! +//! # Early-fail arms are a second closed set +//! +//! `make_call_frame`, `make_create_frame`, and `return_create` / `classify_create_return` each +//! return a result without running a child body on a fixed list of arms. Those arms are not an +//! enum: a revm bump that adds one does not fail this match. The upgrade checklist diffs them by +//! hand against the list in `tests/rex7/result_space_tripwire.rs`. +//! +//! One live mismatch is load-bearing: a CREATE whose nonce cannot be bumped returns +//! [`InstructionResult::Return`], not [`InstructionResult::NonceOverflow`]. The variant is still +//! classified (swallow, because it is a halt), so that if an arm ever starts producing it the +//! booking is defined. + +use revm::interpreter::InstructionResult; + +/// What the destroyed-remainder protocol does with a frame result's remaining gas. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DestroyedDisposition { + /// Remaining gas is erased back into the caller. Book nothing. + Return, + /// Remaining gas is never handed back. Book it as destroyed. + Swallow, + /// Cannot appear as a frame result. Settlement must not observe it. + Unreachable, +} + +impl DestroyedDisposition { + /// Whether the protocol books this result's remaining gas as destroyed. + pub const fn swallows(self) -> bool { + matches!(self, Self::Swallow) + } +} + +/// Classifies `result` for the destroyed-remainder protocol. +/// +/// Every [`InstructionResult`] variant has an arm. A new variant is a compile error until it is +/// assigned [`Return`](DestroyedDisposition::Return), [`Swallow`](DestroyedDisposition::Swallow), +/// or [`Unreachable`](DestroyedDisposition::Unreachable). +pub const fn destroyed_disposition(result: InstructionResult) -> DestroyedDisposition { + match result { + // Success / revert: the caller gets the remaining gas back. + InstructionResult::Stop | + InstructionResult::Return | + InstructionResult::SelfDestruct | + InstructionResult::Revert | + InstructionResult::CallTooDeep | + InstructionResult::OutOfFunds | + InstructionResult::CreateInitCodeStartingEF00 | + InstructionResult::InvalidEOFInitCode | + InstructionResult::InvalidExtDelegateCallTarget => DestroyedDisposition::Return, + + // Internal interpreter state. Never a `FrameResult`. + InstructionResult::Suspend => DestroyedDisposition::Unreachable, + + // Exceptional halt: the remaining gas is gone. + InstructionResult::OutOfGas | + InstructionResult::MemoryOOG | + InstructionResult::MemoryLimitOOG | + InstructionResult::PrecompileOOG | + InstructionResult::InvalidOperandOOG | + InstructionResult::ReentrancySentryOOG | + InstructionResult::OpcodeNotFound | + InstructionResult::CallNotAllowedInsideStatic | + InstructionResult::StateChangeDuringStaticCall | + InstructionResult::InvalidFEOpcode | + InstructionResult::InvalidJump | + InstructionResult::NotActivated | + InstructionResult::StackUnderflow | + InstructionResult::StackOverflow | + InstructionResult::OutOfOffset | + InstructionResult::CreateCollision | + InstructionResult::OverflowPayment | + InstructionResult::PrecompileError | + InstructionResult::NonceOverflow | + InstructionResult::CreateContractSizeLimit | + InstructionResult::CreateContractStartingWithEF | + InstructionResult::CreateInitCodeSizeLimit | + InstructionResult::FatalExternalError | + InstructionResult::InvalidImmediateEncoding => DestroyedDisposition::Swallow, + } +} + +/// Whether a frame result with this instruction result has remaining gas the protocol books as +/// destroyed. +/// +/// Debug builds panic if [`DestroyedDisposition::Unreachable`] appears: that variant is not a +/// frame result, and reaching settlement with it means the classification table is stale. +pub(crate) fn remaining_is_destroyed(result: InstructionResult) -> bool { + let class = destroyed_disposition(result); + debug_assert!( + !matches!(class, DestroyedDisposition::Unreachable), + "unreachable InstructionResult at a destroyed-remainder settlement: {result:?}" + ); + class.swallows() +} diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 8c9a9a13..4eb2ce96 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -13,7 +13,7 @@ use revm::{ }; use super::{ - checkpoint, compute_gas, conservation, data_size, frame_limit::TxRuntimeLimit, + checkpoint, compute_gas, conservation, data_size, destroyed, frame_limit::TxRuntimeLimit, inspector_ledger, kv_update, state_growth, storage_call_stipend, }; use crate::{ @@ -1390,20 +1390,24 @@ impl AdditionalLimit { /// - a returning or reverting frame hands its remaining gas back to its caller, so an edit to /// that number really does change what the transaction spends. It goes to the ledger, and the /// conservation law reads it back out of the envelope; - /// - a halting frame hands nothing back, so the edit changes nothing the transaction spends. - /// The destroyed remainder and the rescue below are then taken on the EVM's own number, - /// reconstructed by undoing the edit — an inspector does not perform work, and gas it removed - /// from a doomed result was never the inspector's to destroy. + /// - a swallowed (halting) frame hands nothing back, so the edit changes nothing the + /// transaction spends. The destroyed remainder and the rescue below are then taken on the + /// EVM's own number, reconstructed by undoing the edit — an inspector does not perform work, + /// and gas it removed from a doomed result was never the inspector's to destroy. + /// + /// The split is [`destroyed_disposition`](super::destroyed_disposition), not revm's + /// `is_ok_or_revert` catch-all: a new [`InstructionResult`] variant is a compile error until + /// it is classified. fn settle_inspector_result_gas(&mut self, result: &FrameResult, delta: i128) -> u64 { let remaining = result.gas().remaining(); if delta == 0 { return remaining; } - if result.instruction_result().is_ok_or_revert() { + if destroyed::remaining_is_destroyed(result.instruction_result()) { + (i128::from(remaining) - delta).clamp(0, i128::from(u64::MAX)) as u64 + } else { self.inspector.result += delta; remaining - } else { - (i128::from(remaining) - delta).clamp(0, i128::from(u64::MAX)) as u64 } } @@ -1737,7 +1741,7 @@ impl AdditionalLimit { fn settle_exceptional_halt_burn(&mut self, result: &FrameResult, evm_remaining: u64) { if !self.checkpoint.rex7_enabled() || self.limit_exceeded() || - result.instruction_result().is_ok_or_revert() + !destroyed::remaining_is_destroyed(result.instruction_result()) { return; } @@ -1755,9 +1759,11 @@ impl AdditionalLimit { /// cannot see it either, and without this the destroyed budget would be missing from the /// transaction's reported total while the conservation law still derives it from the envelope. /// - /// Only the halt classification books anything. The success and revert shapes reaching this + /// Only a swallowed classification books anything. The success and revert shapes reaching this /// site — an empty-code call, a nonce overflow, a depth or balance rejection — destroy nothing - /// precisely because their gas is erased back into the caller. + /// precisely because their gas is erased back into the caller. The split is + /// [`destroyed_disposition`](super::destroyed_disposition): a new [`InstructionResult`] + /// variant is a compile error until it is classified. /// /// A precompile result is excluded. Precompiles are dispatched inside the same frame init and /// come back as a result rather than a frame, but they have already booked both halves of @@ -1774,7 +1780,7 @@ impl AdditionalLimit { fn settle_frame_init_reject_burn(&mut self, result: &FrameResult, evm_remaining: u64) { if !self.checkpoint.rex7_enabled() || self.limit_exceeded() || - result.instruction_result().is_ok_or_revert() + !destroyed::remaining_is_destroyed(result.instruction_result()) { return; } diff --git a/crates/mega-evm/src/limit/mod.rs b/crates/mega-evm/src/limit/mod.rs index b5991ae5..fb74e59d 100644 --- a/crates/mega-evm/src/limit/mod.rs +++ b/crates/mega-evm/src/limit/mod.rs @@ -5,6 +5,7 @@ mod checkpoint; mod compute_gas; mod conservation; mod data_size; +mod destroyed; mod frame_limit; mod inspector_ledger; mod kv_update; @@ -15,6 +16,7 @@ mod storage_call_stipend; pub use conservation::*; pub use data_size::*; +pub use destroyed::*; pub(crate) use frame_limit::{FrameLimitTracker, LimitReading, TxRuntimeLimit}; pub use inspector_ledger::*; pub use limit::*; diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 974c444d..0c4c30ba 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -66,6 +66,10 @@ //! - `deposit_receipt_rewrite` — the transactions that break that last step. A failed OP deposit //! does get a receipt, rebuilt to report its whole gas limit after every settlement has run; the //! boundary that rebuilds it books the difference as destroyed without moving what enforces. +//! - `result_space_tripwire` — every `InstructionResult` variant has an explicit destroyed- +//! remainder class (swallow / return / unreachable), with no catch-all, so a revm bump that adds +//! a variant fails to compile until a human assigns it; the early-fail arms of frame init are +//! listed beside it for the upgrade diff that those arms have no type-level tie to. mod burn_split; mod call_body_halt_charges; @@ -96,3 +100,4 @@ mod opcode_set_parity; mod parity_shapes; mod pre_execution_intrinsic_reject; mod precompile_halt; +mod result_space_tripwire; diff --git a/crates/mega-evm/tests/rex7/result_space_tripwire.rs b/crates/mega-evm/tests/rex7/result_space_tripwire.rs new file mode 100644 index 00000000..f7b6f7fd --- /dev/null +++ b/crates/mega-evm/tests/rex7/result_space_tripwire.rs @@ -0,0 +1,369 @@ +//! Closed classification of every [`InstructionResult`] variant for the destroyed-remainder +//! protocol, and the early-fail arm list a revm bump has to diff by hand. +//! +//! revm's `InstructionResult` is a closed enumeration of envelope endings, but until this file the +//! destroyed-remainder protocol classified them through `is_ok_or_revert()` — a catch-all on the +//! halt side. A variant revm added later would be swallowed without anyone assigning it. The +//! `CreateCollision` booking was that gap: the halt class happened to be right, and nothing forced +//! a human to say so. +//! +//! [`destroyed_disposition`] is the assignment table: every variant has an arm, and there is no +//! `_`. Commenting one out, or a revm bump that adds a variant, fails to compile. This file is the +//! readable copy of that table, plus the second closed set that *is* a catch-all: the early-fail +//! arms of `make_call_frame` / `make_create_frame` / `classify_create_return`, which are not an +//! enum and have to be read by hand on an upgrade. +//! +//! Complementary to the EEST corpus sweep, which collides with whatever the fixtures reach. This +//! file is the enumeration seal. + +use mega_evm::{destroyed_disposition, DestroyedDisposition}; +use revm::interpreter::InstructionResult; + +/// One row of the destroyed-remainder assignment table. +struct VariantRow { + result: InstructionResult, + disposition: DestroyedDisposition, +} + +/// Every [`InstructionResult`] variant, with the disposition [`destroyed_disposition`] assigns. +/// +/// A new variant is a new row here *and* a new arm in [`destroyed_disposition`]. Omitting the arm +/// is a compile error; omitting the row is what +/// [`test_every_instruction_result_has_an_explicit_destroyed_disposition`] catches. +const VARIANTS: &[VariantRow] = &[ + // Return: remaining gas is erased back into the caller. + VariantRow { result: InstructionResult::Stop, disposition: DestroyedDisposition::Return }, + VariantRow { result: InstructionResult::Return, disposition: DestroyedDisposition::Return }, + VariantRow { + result: InstructionResult::SelfDestruct, + disposition: DestroyedDisposition::Return, + }, + VariantRow { result: InstructionResult::Revert, disposition: DestroyedDisposition::Return }, + VariantRow { + result: InstructionResult::CallTooDeep, + disposition: DestroyedDisposition::Return, + }, + VariantRow { result: InstructionResult::OutOfFunds, disposition: DestroyedDisposition::Return }, + VariantRow { + result: InstructionResult::CreateInitCodeStartingEF00, + disposition: DestroyedDisposition::Return, + }, + VariantRow { + result: InstructionResult::InvalidEOFInitCode, + disposition: DestroyedDisposition::Return, + }, + VariantRow { + result: InstructionResult::InvalidExtDelegateCallTarget, + disposition: DestroyedDisposition::Return, + }, + // Unreachable: never a frame result. + VariantRow { + result: InstructionResult::Suspend, + disposition: DestroyedDisposition::Unreachable, + }, + // Swallow: remaining gas is never handed back. + VariantRow { result: InstructionResult::OutOfGas, disposition: DestroyedDisposition::Swallow }, + VariantRow { result: InstructionResult::MemoryOOG, disposition: DestroyedDisposition::Swallow }, + VariantRow { + result: InstructionResult::MemoryLimitOOG, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::PrecompileOOG, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::InvalidOperandOOG, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::ReentrancySentryOOG, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::OpcodeNotFound, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::CallNotAllowedInsideStatic, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::StateChangeDuringStaticCall, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::InvalidFEOpcode, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::InvalidJump, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::NotActivated, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::StackUnderflow, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::StackOverflow, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::OutOfOffset, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::CreateCollision, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::OverflowPayment, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::PrecompileError, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::NonceOverflow, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::CreateContractSizeLimit, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::CreateContractStartingWithEF, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::CreateInitCodeSizeLimit, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::FatalExternalError, + disposition: DestroyedDisposition::Swallow, + }, + VariantRow { + result: InstructionResult::InvalidImmediateEncoding, + disposition: DestroyedDisposition::Swallow, + }, +]; + +/// The early-fail arms of revm's frame-init / create-return as of revm-handler 20.0.3. +/// +/// This list is not type-tied to upstream. A revm bump that adds an arm does not fail to compile. +/// Diff `EthFrame::make_call_frame`, `EthFrame::make_create_frame`, and `return_create` / +/// `classify_create_return` against it, then add the row and assign the produced +/// [`InstructionResult`] in [`destroyed_disposition`]. +/// +/// The nonce-overflow arm is the live mismatch the `CreateCollision` gap was a sibling of: the +/// arm returns `Return`, not `NonceOverflow`. The variant is still classified (swallow) so a +/// future arm that starts producing it has a defined booking. +struct EarlyFailArm { + /// Upstream function and the condition that returns without a child body. + site: &'static str, + result: InstructionResult, +} + +const EARLY_FAIL_ARMS: &[EarlyFailArm] = &[ + EarlyFailArm { + site: "make_call_frame: depth > CALL_STACK_LIMIT", + result: InstructionResult::CallTooDeep, + }, + EarlyFailArm { + site: "make_call_frame: transfer_loaded → TransferError::OutOfFunds", + result: InstructionResult::OutOfFunds, + }, + EarlyFailArm { + site: "make_call_frame: transfer_loaded → TransferError::OverflowPayment", + result: InstructionResult::OverflowPayment, + }, + EarlyFailArm { + site: "make_call_frame: transfer_loaded → TransferError::CreateCollision", + result: InstructionResult::CreateCollision, + }, + EarlyFailArm { site: "make_call_frame: empty bytecode", result: InstructionResult::Stop }, + EarlyFailArm { + site: "make_create_frame: depth > CALL_STACK_LIMIT", + result: InstructionResult::CallTooDeep, + }, + EarlyFailArm { + site: "make_create_frame: caller balance < value", + result: InstructionResult::OutOfFunds, + }, + EarlyFailArm { + site: "make_create_frame: nonce bump fails (NOT NonceOverflow)", + result: InstructionResult::Return, + }, + EarlyFailArm { + site: "make_create_frame: create_account_checkpoint → TransferError::CreateCollision", + result: InstructionResult::CreateCollision, + }, + EarlyFailArm { + site: "make_create_frame: create_account_checkpoint → TransferError::OverflowPayment", + result: InstructionResult::OverflowPayment, + }, + EarlyFailArm { + site: "make_create_frame: create_account_checkpoint → TransferError::OutOfFunds", + result: InstructionResult::OutOfFunds, + }, + EarlyFailArm { + site: "classify_create_return: runtime code size", + result: InstructionResult::CreateContractSizeLimit, + }, + EarlyFailArm { + site: "classify_create_return: 0xEF prefix", + result: InstructionResult::CreateContractStartingWithEF, + }, + EarlyFailArm { + site: "classify_create_return: code-deposit charge", + result: InstructionResult::OutOfGas, + }, +]; + +/// Naming every variant, with no `_`, is the compile-time tripwire in this file. +/// +/// Commenting one out is a non-exhaustive match. A revm bump that adds a variant is the same +/// error, and the next step is an arm in [`destroyed_disposition`] plus a row in [`VARIANTS`]. +#[test] +fn test_instruction_result_space_has_no_catchall() { + match InstructionResult::Stop { + InstructionResult::Stop | + InstructionResult::Return | + InstructionResult::SelfDestruct | + InstructionResult::Suspend | + InstructionResult::Revert | + InstructionResult::CallTooDeep | + InstructionResult::OutOfFunds | + InstructionResult::CreateInitCodeStartingEF00 | + InstructionResult::InvalidEOFInitCode | + InstructionResult::InvalidExtDelegateCallTarget | + InstructionResult::OutOfGas | + InstructionResult::MemoryOOG | + InstructionResult::MemoryLimitOOG | + InstructionResult::PrecompileOOG | + InstructionResult::InvalidOperandOOG | + InstructionResult::ReentrancySentryOOG | + InstructionResult::OpcodeNotFound | + InstructionResult::CallNotAllowedInsideStatic | + InstructionResult::StateChangeDuringStaticCall | + InstructionResult::InvalidFEOpcode | + InstructionResult::InvalidJump | + InstructionResult::NotActivated | + InstructionResult::StackUnderflow | + InstructionResult::StackOverflow | + InstructionResult::OutOfOffset | + InstructionResult::CreateCollision | + InstructionResult::OverflowPayment | + InstructionResult::PrecompileError | + InstructionResult::NonceOverflow | + InstructionResult::CreateContractSizeLimit | + InstructionResult::CreateContractStartingWithEF | + InstructionResult::CreateInitCodeSizeLimit | + InstructionResult::FatalExternalError | + InstructionResult::InvalidImmediateEncoding => {} + } +} + +/// Every variant has a row, and the row matches [`destroyed_disposition`]. +/// +/// Commenting out a match arm in `destroyed_disposition` fails this crate at compile time — that +/// is the tripwire. This test is the readable copy: a missing row, or a row that disagrees with +/// the match, is a runtime failure naming the variant. +#[test] +fn test_every_instruction_result_has_an_explicit_destroyed_disposition() { + for row in VARIANTS { + assert_eq!( + destroyed_disposition(row.result), + row.disposition, + "{:?}: the tripwire table and destroyed_disposition must assign the same class", + row.result, + ); + } +} + +/// Return / Swallow follow revm's ok-or-revert / halt macros; Unreachable is only `Suspend`. +/// +/// The protocol owns the assignment, so this is a snapshot of today's agreement rather than a +/// requirement that they stay coupled. A variant reclassified away from the macros is a deliberate +/// row change here. +#[test] +fn test_return_and_swallow_agree_with_revm_ok_or_revert() { + for row in VARIANTS { + match row.disposition { + DestroyedDisposition::Return => { + assert!( + row.result.is_ok_or_revert(), + "{:?} is Return, so is_ok_or_revert must hold", + row.result, + ); + assert!(!row.result.is_halt(), "{:?} is Return, so it is not a halt", row.result); + } + DestroyedDisposition::Swallow => { + assert!(row.result.is_halt(), "{:?} is Swallow, so it must be a halt", row.result,); + assert!( + !row.result.is_ok_or_revert(), + "{:?} is Swallow, so is_ok_or_revert must not hold", + row.result, + ); + } + DestroyedDisposition::Unreachable => { + assert_eq!( + row.result, + InstructionResult::Suspend, + "the only unreachable variant is Suspend (internal interpreter state); \ + got {:?}", + row.result, + ); + } + } + } +} + +/// Each documented early-fail arm produces a variant the disposition table already classifies. +/// +/// Adding an arm in revm without a row here is what the upgrade checklist is for; this test only +/// pins that the arms we already know about have a defined booking. +#[test] +fn test_every_documented_early_fail_arm_has_a_classified_result() { + for arm in EARLY_FAIL_ARMS { + let class = destroyed_disposition(arm.result); + assert!( + VARIANTS.iter().any(|row| row.result == arm.result && row.disposition == class), + "{} produces {:?}, which must have a tripwire row", + arm.site, + arm.result, + ); + assert_ne!( + class, + DestroyedDisposition::Unreachable, + "{} produces {:?}, which cannot be classified unreachable: it is a live frame result", + arm.site, + arm.result, + ); + } +} + +/// The CREATE nonce-overflow arm returns `Return`, not `NonceOverflow`. +/// +/// That is the shape the `CreateCollision` gap was a sibling of: the variant exists, the live arm +/// produces a different one, and both must stay classified. +#[test] +fn test_create_nonce_overflow_arm_returns_return_not_nonce_overflow() { + let arm = EARLY_FAIL_ARMS + .iter() + .find(|arm| arm.site.contains("nonce bump fails")) + .expect("the nonce-overflow arm is part of the early-fail list"); + assert_eq!(arm.result, InstructionResult::Return); + assert_eq!(destroyed_disposition(InstructionResult::Return), DestroyedDisposition::Return); + assert_eq!( + destroyed_disposition(InstructionResult::NonceOverflow), + DestroyedDisposition::Swallow, + "NonceOverflow is a halt; if an arm starts producing it, the booking is swallow", + ); +} diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index a10be043..b126a2e7 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -582,6 +582,21 @@ The two readings agree by construction of the law; keeping enforcement on the re The rules that follow fix `executed_compute` at each site that can leave budget unspent, which is what makes the law's remainder well defined; they are not themselves the definition of the destroyed total. +A node MUST record each producer at the site the table names, and MUST NOT record it at any other site. + +| Producer | Recording site | +| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| A frame that ends in an exceptional halt, including a creation rejected at code deposit | The frame's final-result settlement | +| A call or creation the inherited EVM refuses before it opens a frame | The same settlement, classified by whether the refusal swallows the child budget or hands it back | +| A precompile invocation that fails | The precompile recording site; a node MUST NOT also record it at the frame-init refusal site | +| A system-contract invocation answered without an EVM frame, when the answer is a halt that keeps the call's gas | The site that produces the answer | +| A failed-deposit receipt rebuild | The rebuild of the envelope, as the gap between that envelope and every earlier recording | +| An ordinary transaction rejected during validation because intrinsic gas outgrew the sender's gas limit | Nowhere: the transaction produces no receipt | + +The classification that decides whether a refusal swallows its budget or hands it back MUST be exhaustive over the inherited instruction-result space. +Every result the inherited EVM can produce MUST be assigned swallowed, returned, or unreachable. +A newly introduced result MUST NOT be assigned by a default arm. + A precompile invocation that fails is the same split, taken at the precompile recording site. A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index f446eed0..b2d042a6 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -142,7 +142,19 @@ A refusal classified as a success or a revert — a call or creation past the ca A precompile invocation is answered on this same path and is covered by its own rule above; a node MUST NOT book it a second time here. Those sites are where a Rex7 transaction is known to lose an envelope without executing it, and they are what fixes `executed_compute` at each one — but they are not what makes the enumeration complete. -Completeness is a consequence of the law: a lost envelope is gas the transaction spent that neither the compute lanes nor the storage-gas lane accounts for, so it lands in the remainder whether or not a site above anticipated it. + +| Producer | Recording site | +| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| A frame that ends in an exceptional halt, including a creation rejected at code deposit | The frame's final-result settlement | +| A call or creation the inherited EVM refuses before it opens a frame | The same settlement, classified by whether the refusal swallows the child budget or hands it back | +| A precompile invocation that fails | The precompile recording site; not also at the frame-init refusal site | +| A system-contract invocation answered without an EVM frame, when the answer is a halt that keeps the call's gas | The site that produces the answer | +| A failed-deposit receipt rebuild | The rebuild of the envelope | + +The classification that decides whether a result swallows its remaining budget or hands it back MUST be exhaustive over the inherited instruction-result space. +Every result MUST be assigned swallowed, returned, or unreachable. +A newly introduced result MUST NOT be assigned by a default arm. +Completeness of the _reported_ total is a consequence of the law: a lost envelope is gas the transaction spent that neither the compute lanes nor the storage-gas lane accounts for, so it lands in the remainder whether or not a site above anticipated it. Reading the two independently and requiring them to agree is what turns the list from an assumption into a checkable claim. One further shape burns a whole envelope having executed nothing — a transaction whose intrinsic gas requirement outgrows the gas limit its sender supplied — but [Rex5](rex5.md) already rejects that transaction during validation, after every MegaETH storage-gas contribution has been folded into the intrinsic total and before the sender is debited. From ab6e4cc2b9036406d05437a8dde8ab31b0cab553 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 14:51:42 +0800 Subject: [PATCH 114/208] feat(state-test): sweep the corpus under a deterministic rewriting inspector --- crates/mega-state-test/src/chaos.rs | 1158 ++++++++++++++++++++ crates/mega-state-test/src/diff.rs | 115 +- crates/mega-state-test/src/lib.rs | 2 + crates/mega-state-test/tests/chaos_mode.rs | 268 +++++ crates/state-test/src/main.rs | 212 ++++ tools/eest-sweep/README.md | 39 +- tools/eest-sweep/run.sh | 42 +- 7 files changed, 1809 insertions(+), 27 deletions(-) create mode 100644 crates/mega-state-test/src/chaos.rs create mode 100644 crates/mega-state-test/tests/chaos_mode.rs diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs new file mode 100644 index 00000000..f8f28132 --- /dev/null +++ b/crates/mega-state-test/src/chaos.rs @@ -0,0 +1,1158 @@ +//! A deterministic rewriting inspector, and the corpus sweep that runs it. +//! +//! # What this is for +//! +//! `MegaETH` supports rewriting inspectors in full: the measurement shim books what one does to a +//! transaction's gas, and the conservation law accounts for it. `tests/rex7/measured_inspector.rs` +//! and `tests/rex7/inspector_cheat_matrix.rs` pin that mechanism shape by shape, on fixtures built +//! to reach each shape. What neither can do is put a rewriting inspector on top of *arbitrary* +//! execution — the corner of the state space where a rewrite meets a detained frame, a latched +//! resource exceed, a precompile, a `SELFDESTRUCT`, an EIP-7702 delegation, a nested revert. +//! +//! The EEST corpus is that state space, already written down. This module drives it: every vector +//! is executed three times — with no inspector, with a read-only one, and with a rewriting one — +//! and asks two questions. +//! +//! - **Does anything break?** Every gas-accounting cross-check `MegaETH` has is a `debug_assert`, +//! so a build with debug assertions live turns a broken conservation law into a panic, which +//! [`panic_capture`](crate::panic_capture) turns into that vector's verdict rather than a lost +//! worker thread. Zero panics over the corpus is the gate. +//! - **Is observation still free?** The read-only run must be bit-identical to the run with no +//! inspector at all, on every quantity the differential classifier compares. That is the property +//! every tracer in production depends on, and it is checked here against 44,000 transactions +//! rather than against a handful of fixtures. +//! +//! # Why the randomness is not random +//! +//! A sweep whose failures cannot be reproduced is a sweep whose failures cannot be fixed. Every +//! decision the chaos inspector makes comes from a hash of two things: a global seed the caller +//! chooses, and the vector's own identity (its fixture path, its unit name, its transaction +//! indexes). No clock, no address, no iteration order, no thread id. The same seed and the same +//! corpus produce the same mutations on any machine, in any thread count, in any order — so a +//! flagged vector comes with everything needed to re-run exactly it. +//! +//! # What the pool leaves out +//! +//! One rewrite shape is missing on purpose: turning a *failed contract creation* into a successful +//! one. The shim refuses that shape and asserts on it, deliberately — by the time `create_end` +//! runs, the journal has been reverted and no code was deposited, so a success there reports a +//! deployment that did not happen, and a corpus that produces it should stop rather than quietly +//! take the rejection path. Including it here would make the detector's own firing the sweep's +//! dominant result. The refusal is pinned end-to-end by the two tests named in +//! `tests/rex7/inspector_cheat_matrix.rs`'s `inapplicable` table instead. + +use crate::{ + diff::{compare, execute_unit_in_mode, RunMode}, + panic_capture, + runner::{is_skipped_fixture, skip_test, vector_label, FixtureScan, TestError, TestErrorKind}, + types::{SpecName, TestSuite, TestUnit, TxPartIndices}, +}; +use indicatif::{ProgressBar, ProgressDrawTarget}; +use mega_evm::revm::{ + context::{ContextTr, JournalTr}, + handler::FrameResult, + inspector::Inspector, + interpreter::{ + interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + Interpreter, InterpreterResult, InterpreterTypes, + }, + primitives::{Address, Bytes, Log, U256}, +}; +use std::{ + collections::BTreeMap, + path::Path, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +/// How many mutations one transaction may receive. +/// +/// Bounded for two reasons. A budget keeps the sweep's running time bounded — every gas injection +/// buys the transaction more opcodes to execute, and an unbounded trickle into a loop is an +/// unbounded sweep. And a budget keeps a flagged vector legible: a dozen mutations can be listed +/// in a report, a hundred thousand cannot. +const MUTATION_BUDGET: u32 = 12; + +/// One in this many callbacks carries a mutation, until the budget runs out. +const FIRE_IN: u64 = 8; + +/// The largest gas amount a single mutation moves. +/// +/// Small on purpose: the whole budget can move at most `MUTATION_BUDGET * GAS_DELTA_MAX` gas, +/// which is far less than a fixture's gas limit. The shapes are being tested, not the magnitudes — +/// a lane that drops an adjustment drops it whatever its size. +const GAS_DELTA_MAX: u64 = 512; + +/// Transient-storage slot the journal-write shape writes to. +const CHAOS_SLOT: u64 = 0xC4A05; + +/// Account the journal-write shape writes that slot on. +/// +/// An address no fixture uses, so the write cannot collide with one the transaction makes and be +/// mistaken for it. Transient storage is discarded at the end of the transaction either way, so +/// the write reaches no post-state — the point is that it goes through the journal, which is the +/// surface an inspector can reach without any `MegaETH` lane metering it. +const CHAOS_ADDRESS: Address = + mega_evm::revm::primitives::address!("00000000000000000000000000000000c4a05c4a"); + +// --- the deterministic stream ----------------------------------------------------------------- + +/// `splitmix64`: a full-period, well-distributed mixing function with no state but its input. +/// +/// Written out rather than taken from a crate so that the stream is fixed by this file: a +/// dependency bump that changed a generator's algorithm would silently change what every seed +/// means, and a seed that no longer reproduces its own failure is worse than no seed at all. +const fn mix(seed: u64) -> u64 { + let mut x = seed.wrapping_add(0x9E37_79B9_7F4A_7C15); + x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + x ^ (x >> 31) +} + +/// FNV-1a over bytes — the hash a vector's identity is folded through. +/// +/// Also written out rather than taken from the standard library: `DefaultHasher`'s output is +/// explicitly not guaranteed stable across Rust releases, and a seed whose meaning depends on the +/// toolchain does not reproduce anything. +fn fnv1a(bytes: &[u8], mut hash: u64) -> u64 { + for byte in bytes { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01B3); + } + hash +} + +/// The seed one vector's chaos run uses, derived from the global seed and the vector's identity. +/// +/// The identity is everything that distinguishes this transaction from every other in the corpus: +/// which file it came from, which unit of that file, and which of that unit's transaction vectors. +/// Two runs of the same corpus with the same global seed therefore mutate the same vectors the +/// same way, whatever order the files are swept in and however many threads sweep them. +pub fn vector_seed(global: u64, path: &str, name: &str, indexes: TxPartIndices) -> u64 { + let mut hash = fnv1a(path.as_bytes(), 0xCBF2_9CE4_8422_2325); + hash = fnv1a(&[0], hash); + hash = fnv1a(name.as_bytes(), hash); + hash = fnv1a(&[0], hash); + hash = fnv1a(&(indexes.data as u64).to_le_bytes(), hash); + hash = fnv1a(&(indexes.gas as u64).to_le_bytes(), hash); + hash = fnv1a(&(indexes.value as u64).to_le_bytes(), hash); + mix(hash ^ mix(global)) +} + +// --- the shapes --------------------------------------------------------------------------------- + +/// A rewrite shape the chaos pool draws from — one legal column of the cheat-shape matrix. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ChaosShape { + /// Gas written into a live interpreter's counter. + InjectGas, + /// Gas taken out of one. + DrainGas, + /// The interpreter's own working state — a memory word, or the operand an `SSTORE` is about + /// to consume. + EditFrameState, + /// A transient-storage write made behind the EVM's back. + JournalWrite, + /// A raised `gas_limit` on a frame about to be built. + RaiseEnvelope, + /// A lowered one. + LowerEnvelope, + /// A call turned static, so what the frame is allowed to do changes rather than what it costs. + MakeStatic, + /// A synthetic outcome, so no frame is built at all. + Intercept, + /// A raised remaining-gas figure on a finished frame's result. + RaiseResultGas, + /// A lowered one. + LowerResultGas, + /// A successful frame result rewritten into a revert or an exceptional halt. + FailFrame, + /// A failed *call* frame rewritten into a success. The creation form of this shape is refused + /// by the shim and is deliberately not in the pool — see the module docs. + ReviveCall, +} + +impl ChaosShape { + /// Every shape, in the order the labels are listed by `--chaos-shapes`. + pub const ALL: [Self; 12] = [ + Self::InjectGas, + Self::DrainGas, + Self::EditFrameState, + Self::JournalWrite, + Self::RaiseEnvelope, + Self::LowerEnvelope, + Self::MakeStatic, + Self::Intercept, + Self::RaiseResultGas, + Self::LowerResultGas, + Self::FailFrame, + Self::ReviveCall, + ]; + + /// The shape a label names. + /// + /// # Errors + /// + /// Returns a message listing every label when `label` is not one. + pub fn parse(label: &str) -> Result { + Self::ALL.into_iter().find(|shape| shape.label() == label).ok_or_else(|| { + format!( + "unknown chaos shape {label:?}; known shapes are {}", + Self::ALL.map(Self::label).join(", ") + ) + }) + } + + /// Whether this shape writes to a live interpreter's gas counter. + const fn is_counter_edit(self) -> bool { + matches!(self, Self::InjectGas | Self::DrainGas) + } + + /// Whether this shape rewrites a frame result's classification. + const fn is_reclassification(self) -> bool { + matches!(self, Self::FailFrame | Self::ReviveCall) + } + + /// Stable label, for reports. + pub const fn label(self) -> &'static str { + match self { + Self::InjectGas => "inject_gas", + Self::DrainGas => "drain_gas", + Self::EditFrameState => "edit_frame_state", + Self::JournalWrite => "journal_write", + Self::RaiseEnvelope => "raise_envelope", + Self::LowerEnvelope => "lower_envelope", + Self::MakeStatic => "make_static", + Self::Intercept => "intercept", + Self::RaiseResultGas => "raise_result_gas", + Self::LowerResultGas => "lower_result_gas", + Self::FailFrame => "fail_frame", + Self::ReviveCall => "revive_call", + } + } +} + +/// Shapes reachable from a callback that holds a live interpreter. +const INTERPRETER_SHAPES: [ChaosShape; 4] = [ + ChaosShape::InjectGas, + ChaosShape::DrainGas, + ChaosShape::EditFrameState, + ChaosShape::JournalWrite, +]; + +/// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. +const INPUT_SHAPES: [ChaosShape; 5] = [ + ChaosShape::RaiseEnvelope, + ChaosShape::LowerEnvelope, + ChaosShape::MakeStatic, + ChaosShape::Intercept, + ChaosShape::JournalWrite, +]; + +/// Shapes reachable from a callback that holds a finished frame's result. +const RESULT_SHAPES: [ChaosShape; 5] = [ + ChaosShape::RaiseResultGas, + ChaosShape::LowerResultGas, + ChaosShape::FailFrame, + ChaosShape::ReviveCall, + ChaosShape::JournalWrite, +]; + +/// Which mutations a chaos run is allowed to make. +/// +/// Both knobs exist for triage rather than for the sweep's normal operation: a flagged vector is +/// re-run with the filter narrowed until the smallest set of shapes that still reproduces it is +/// found, which is the difference between "chaos broke something" and a defect report. See +/// [`ChaosInspector::new`] for what narrowing does and does not preserve. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ShapeFilter { + /// Bitmask over [`ChaosShape::ALL`], by index. + allowed: u16, + /// Whether a gas-counter edit may land at the `step_end` of a frame's *terminating* opcode. + /// + /// That callback is the one place where the interpreter's counter has already been copied into + /// the frame's action — revm's loop runs `step_end` after the instruction that set the action, + /// and the action carries its own snapshot of the gas — so an edit there changes the counter + /// `MegaETH`'s tail settlement reads, and nothing the caller ever sees. Turning it off narrows + /// the sweep to edits that actually move a frame's budget. + terminal_counter_edits: bool, + /// Whether a classification rewrite may land on a *precompile's* result. + /// + /// A precompile is answered inside the frame init and never becomes a child frame, so the + /// executed / destroyed split of its forwarded envelope is booked at its own recording site — + /// before any callback sees the synthetic result it produced. Turning it off narrows the sweep + /// to results whose split is decided after the last rewrite. + precompile_reclassification: bool, +} + +impl Default for ShapeFilter { + fn default() -> Self { + Self { allowed: u16::MAX, terminal_counter_edits: true, precompile_reclassification: true } + } +} + +impl ShapeFilter { + /// A filter allowing exactly the listed shapes. + pub fn only(shapes: &[ChaosShape]) -> Self { + let mut allowed = 0; + for shape in shapes { + allowed |= 1 << Self::index(*shape); + } + Self { allowed, ..Self::default() } + } + + /// This filter, with gas-counter edits at a terminating `step_end` turned off. + pub const fn without_terminal_counter_edits(mut self) -> Self { + self.terminal_counter_edits = false; + self + } + + /// This filter, with classification rewrites of a precompile's result turned off. + pub const fn without_precompile_reclassification(mut self) -> Self { + self.precompile_reclassification = false; + self + } + + /// Whether gas-counter edits at a terminating `step_end` are allowed. + pub const fn allows_terminal_counter_edits(&self) -> bool { + self.terminal_counter_edits + } + + /// Whether classification rewrites of a precompile's result are allowed. + pub const fn allows_precompile_reclassification(&self) -> bool { + self.precompile_reclassification + } + + /// Whether `shape` may be drawn. + pub const fn allows(&self, shape: ChaosShape) -> bool { + self.allowed & (1 << Self::index(shape)) != 0 + } + + /// Whether this filter allows every shape. + pub fn is_complete(&self) -> bool { + self.terminal_counter_edits && + self.precompile_reclassification && + ChaosShape::ALL.into_iter().all(|s| self.allows(s)) + } + + const fn index(shape: ChaosShape) -> u16 { + shape as u16 + } +} + +/// How many mutations of each shape one run applied. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ChaosTally { + /// Mutations applied, per shape label. + pub applied: BTreeMap<&'static str, u32>, + /// Callbacks the inspector was handed. + pub callbacks: u64, +} + +impl ChaosTally { + /// Total mutations applied. + pub fn total(&self) -> u32 { + self.applied.values().sum() + } + + /// Folds another run's tally into this one. + pub fn merge(&mut self, other: &Self) { + for (shape, count) in &other.applied { + *self.applied.entry(shape).or_insert(0) += count; + } + self.callbacks += other.callbacks; + } +} + +// --- the inspector ------------------------------------------------------------------------------ + +/// A read-only inspector that counts every callback it is handed and changes nothing. +/// +/// The control the chaos run is judged against. It implements every callback, on purpose: an +/// inspector that implemented only one would exercise only one of the shim's wrappers, and the +/// claim under test is that *observation* costs nothing, not that one callback does. +#[derive(Debug, Default)] +pub struct CallbackCounter { + callbacks: u64, +} + +impl CallbackCounter { + /// How many callbacks this inspector was handed. + pub const fn callbacks(&self) -> u64 { + self.callbacks + } +} + +impl Inspector for CallbackCounter { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.callbacks += 1; + } + + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.callbacks += 1; + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.callbacks += 1; + } + + fn log(&mut self, _context: &mut CTX, _log: Log) { + self.callbacks += 1; + } + + fn frame_start( + &mut self, + _context: &mut CTX, + _frame_input: &mut FrameInput, + ) -> Option { + self.callbacks += 1; + None + } + + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + _frame_result: &mut FrameResult, + ) { + self.callbacks += 1; + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.callbacks += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.callbacks += 1; + } + + fn create(&mut self, _context: &mut CTX, _inputs: &mut CreateInputs) -> Option { + self.callbacks += 1; + None + } + + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.callbacks += 1; + } + + fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) { + self.callbacks += 1; + } +} + +/// Rewrites what it is handed, deterministically, from a seed. +/// +/// At every callback it draws one value from the stream; that value decides whether this callback +/// carries a mutation and, if so, which shape and how large. The stream advances on every callback +/// whether or not a mutation lands, so the decision sequence is a function of the seed and the +/// execution — never of what the inspector chose earlier. +#[derive(Debug)] +pub struct ChaosInspector { + seed: u64, + filter: ShapeFilter, + /// Position in the stream: how many callbacks have been seen. + tick: u64, + /// Mutations left in this transaction's budget. + budget: u32, + tally: ChaosTally, +} + +impl ChaosInspector { + /// A chaos inspector driven by `seed`, restricted to what `filter` allows. + /// + /// The decision stream does not depend on the filter: every callback draws the same value + /// whatever is allowed, so narrowing a filter keeps each remaining mutation exactly where the + /// full run put it. What narrowing does change is how far the budget reaches — a rejected draw + /// spends none of it — so a narrowed run can carry mutations further into a transaction than + /// the full run did. Narrowing therefore reproduces a flagged mutation; it does not reproduce + /// a flagged run. + pub fn new(seed: u64, filter: ShapeFilter) -> Self { + Self { seed, filter, tick: 0, budget: MUTATION_BUDGET, tally: ChaosTally::default() } + } + + /// What this run mutated. + pub fn tally(&self) -> ChaosTally { + self.tally.clone() + } + + /// Draws the next value from the stream, advancing it by one callback. + fn draw(&mut self) -> u64 { + self.tick += 1; + self.tally.callbacks += 1; + mix(self.seed ^ mix(self.tick)) + } + + /// Picks a shape from `pool` for this callback, or `None` when this callback carries no + /// mutation or the budget is spent. + fn pick(&mut self, pool: &[ChaosShape]) -> Option<(ChaosShape, u64)> { + let draw = self.draw(); + if self.budget == 0 || !draw.is_multiple_of(FIRE_IN) { + return None; + } + let shape = pool[(draw / FIRE_IN) as usize % pool.len()]; + if !self.filter.allows(shape) { + return None; + } + Some((shape, mix(draw))) + } + + /// Books one applied mutation against the budget. + fn applied(&mut self, shape: ChaosShape) { + self.budget = self.budget.saturating_sub(1); + *self.tally.applied.entry(shape.label()).or_insert(0) += 1; + } + + /// A gas amount in `1..=GAS_DELTA_MAX`, drawn from `entropy`. + fn amount(entropy: u64) -> u64 { + entropy % GAS_DELTA_MAX + 1 + } + + /// Applies an interpreter-facing shape. + fn hit_interpreter( + &mut self, + interp: &mut Interpreter, + context: &mut CTX, + shape: ChaosShape, + entropy: u64, + ) { + match shape { + ChaosShape::InjectGas => interp.gas.erase_cost(Self::amount(entropy)), + ChaosShape::DrainGas => { + if !interp.gas.record_regular_cost(Self::amount(entropy)) { + // The frame cannot afford the removal; leave the counter alone rather than + // manufacture an out-of-gas the EVM did not reach. + return; + } + } + ChaosShape::EditFrameState => { + if !edit_frame_state(interp, entropy) { + return; + } + } + ChaosShape::JournalWrite => write_journal(context, entropy), + _ => return, + } + self.applied(shape); + } + + /// Applies an input-facing shape to a call's inputs, or intercepts the frame. + fn hit_call_inputs( + &mut self, + context: &mut CTX, + inputs: &mut CallInputs, + shape: ChaosShape, + entropy: u64, + ) -> Option { + let mut outcome = None; + match shape { + ChaosShape::RaiseEnvelope => { + inputs.gas_limit = inputs.gas_limit.saturating_add(Self::amount(entropy)); + } + ChaosShape::LowerEnvelope => { + inputs.gas_limit = inputs.gas_limit.saturating_sub(Self::amount(entropy)); + } + ChaosShape::MakeStatic => inputs.is_static = true, + ChaosShape::Intercept => { + outcome = Some(CallOutcome::new( + InterpreterResult::new( + synthetic_result(entropy), + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )); + } + ChaosShape::JournalWrite => write_journal(context, entropy), + _ => return None, + } + self.applied(shape); + outcome + } + + /// Applies an input-facing shape to a creation's inputs, or intercepts the frame. + fn hit_create_inputs( + &mut self, + context: &mut CTX, + inputs: &mut CreateInputs, + shape: ChaosShape, + entropy: u64, + ) -> Option { + let mut outcome = None; + match shape { + ChaosShape::RaiseEnvelope => { + inputs.set_gas_limit(inputs.gas_limit().saturating_add(Self::amount(entropy))); + } + ChaosShape::LowerEnvelope => { + inputs.set_gas_limit(inputs.gas_limit().saturating_sub(Self::amount(entropy))); + } + ChaosShape::Intercept => { + outcome = Some(CreateOutcome::new( + InterpreterResult::new( + synthetic_result(entropy), + Bytes::new(), + Gas::new(inputs.gas_limit()), + ), + None, + )); + } + ChaosShape::JournalWrite => write_journal(context, entropy), + // `MakeStatic` has no counterpart here — a creation carries no static flag — and the + // rest are not input-facing at all. Both leave the inputs alone and spend no budget. + _ => return None, + } + self.applied(shape); + outcome + } + + /// Whether the filter withholds this shape from a precompile's result. + fn refuses_reclassification(&self, shape: ChaosShape, is_precompile: bool) -> bool { + is_precompile && + shape.is_reclassification() && + !self.filter.allows_precompile_reclassification() + } + + /// Applies a result-facing shape to a finished frame's result. + /// + /// `is_creation` withholds the one shape the shim refuses: a failed contract creation rewritten + /// into a success. The pool never offers it, so a creation drawing `ReviveCall` leaves the + /// result alone and spends no budget. + fn hit_result( + &mut self, + result: &mut InterpreterResult, + is_creation: bool, + shape: ChaosShape, + entropy: u64, + ) { + match shape { + ChaosShape::RaiseResultGas => result.gas.erase_cost(Self::amount(entropy)), + ChaosShape::LowerResultGas => { + if !result.gas.record_regular_cost(Self::amount(entropy)) { + return; + } + } + ChaosShape::FailFrame => { + if !result.result.is_ok() { + return; + } + result.result = if entropy.is_multiple_of(2) { + InstructionResult::Revert + } else { + InstructionResult::OutOfGas + }; + } + ChaosShape::ReviveCall => { + if is_creation || result.result.is_ok() { + return; + } + result.result = InstructionResult::Stop; + } + _ => return, + } + self.applied(shape); + } +} + +/// The classification a synthetic outcome carries — one of the three a real frame can end in. +fn synthetic_result(entropy: u64) -> InstructionResult { + match entropy % 3 { + 0 => InstructionResult::Stop, + 1 => InstructionResult::Revert, + _ => InstructionResult::OutOfGas, + } +} + +/// Edits the interpreter's working state, returning whether anything was edited. +/// +/// Two edits, chosen by what the frame is doing rather than at random: the operand an `SSTORE` is +/// about to consume, when that is what the interpreter is on, and a memory word otherwise. Neither +/// can fail the frame by itself — a pushed word would be read as the next opcode's operand, which +/// changes the fixture rather than cheating inside it. +fn edit_frame_state(interp: &mut Interpreter, entropy: u64) -> bool { + const SSTORE: u8 = 0x55; + if interp.bytecode.opcode() == SSTORE { + if let Some([key, value]) = interp.stack.popn::<2>() { + let pushed = interp.stack.push(value.wrapping_add(U256::from(entropy % 8 + 1))) && + interp.stack.push(key); + return pushed; + } + return false; + } + if interp.memory.size() >= 32 { + interp.memory.set(0, &[(entropy % 256) as u8; 32]); + return true; + } + false +} + +/// Writes one transient-storage slot on the frame's own account, behind the EVM's back. +/// +/// Transient storage is journalled, so the write follows the frame's checkpoint like any other +/// state change — which is the point: this is the unmetered surface an inspector reaches through, +/// and it must leave the accounting lanes alone without leaving the journal inconsistent. +fn write_journal(context: &mut CTX, entropy: u64) { + context.journal_mut().tstore(CHAOS_ADDRESS, U256::from(CHAOS_SLOT), U256::from(entropy)); +} + +impl Inspector for ChaosInspector { + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { + self.hit_interpreter(interp, context, shape, entropy); + } + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { + self.hit_interpreter(interp, context, shape, entropy); + } + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) else { return }; + if shape.is_counter_edit() && + !self.filter.allows_terminal_counter_edits() && + interp.bytecode.is_end() + { + return; + } + self.hit_interpreter(interp, context, shape, entropy); + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, _log: Log) { + if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { + self.hit_interpreter(interp, context, shape, entropy); + } + } + + fn frame_start( + &mut self, + context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + let (shape, entropy) = self.pick(&INPUT_SHAPES)?; + match frame_input { + FrameInput::Call(inputs) => { + self.hit_call_inputs(context, inputs, shape, entropy).map(FrameResult::Call) + } + FrameInput::Create(inputs) => { + self.hit_create_inputs(context, inputs, shape, entropy).map(FrameResult::Create) + } + FrameInput::Empty => None, + } + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + let (shape, entropy) = self.pick(&INPUT_SHAPES)?; + self.hit_call_inputs(context, inputs, shape, entropy) + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + let (shape, entropy) = self.pick(&INPUT_SHAPES)?; + self.hit_create_inputs(context, inputs, shape, entropy) + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { + let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; + if self.refuses_reclassification(shape, outcome.was_precompile_called) { + return; + } + self.hit_result(&mut outcome.result, false, shape, entropy); + } + + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) { + self.hit_result(&mut outcome.result, true, shape, entropy); + } + } + + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; + let precompile = matches!(frame_result, FrameResult::Call(o) if o.was_precompile_called); + if self.refuses_reclassification(shape, precompile) { + return; + } + let is_creation = matches!(frame_result, FrameResult::Create(_)); + self.hit_result(frame_result.interpreter_result_mut(), is_creation, shape, entropy); + } +} + +// --- the sweep ------------------------------------------------------------------------------ + +/// How one vector's three runs came out. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum ChaosClass { + /// The read-only run was identical to the run with no inspector, and the rewriting run + /// completed without tripping anything. + #[default] + Pass, + /// The read-only run differed from the run with no inspector. Observation is not free, which + /// breaks every tracer in production. + ControlDrift, + /// The rewriting run and the reference disagreed about whether the transaction executes at + /// all: one produced a receipt and the other an `EVMError`. No inspector callback runs before + /// validation, so the two cannot legitimately differ here. + ChaosRejected, + /// Neither run executed the transaction, and the runner declined it identically. + Skipped, + /// A run panicked — which, in a build with debug assertions live, is how a broken conservation + /// law surfaces. + Panic, +} + +impl ChaosClass { + /// Stable upper-case label, for tallies and reports. + pub const fn label(self) -> &'static str { + match self { + Self::Pass => "PASS", + Self::ControlDrift => "CONTROL_DRIFT", + Self::ChaosRejected => "CHAOS_REJECTED", + Self::Skipped => "SKIPPED", + Self::Panic => "PANIC", + } + } + + /// Whether this verdict fails the gate. + pub const fn is_failure(self) -> bool { + matches!(self, Self::ControlDrift | Self::ChaosRejected | Self::Panic) + } +} + +/// The verdict on one vector. +#[derive(Debug, Clone)] +pub struct UnitChaos { + /// The unit's key in the fixture's test-suite map, with the vector's indexes when the unit + /// declares more than one. + pub name: String, + /// The fixture file the unit came from. + pub path: String, + /// The seed this vector's rewriting run was driven by — everything needed to re-run exactly + /// it. + pub seed: u64, + /// How the runs came out. + pub class: ChaosClass, + /// Mutations the rewriting run applied. + pub mutations: u32, + /// What went wrong, for a verdict that needs a human. + pub detail: Option, +} + +/// Runs one vector three times — no inspector, a read-only one, a rewriting one — and judges. +/// +/// The reference and the control settle the "observation is free" half. The rewriting run is +/// judged by what it does *not* do: it must not panic (every gas-accounting cross-check is a debug +/// assertion, so a broken law is a panic) and it must not change whether the transaction executes +/// at all. +/// +/// Nothing compares the rewriting run's *numbers* to the reference's. A rewriting inspector is +/// supposed to change them — that is what "supported" means — and the property that they still add +/// up is stated by the conservation law, which the execution checks itself. +pub fn chaos_unit( + unit: &TestUnit, + indexes: TxPartIndices, + spec: &SpecName, + seed: u64, + filter: ShapeFilter, +) -> ChaosVerdict { + let reference = execute_unit_in_mode(unit, indexes, spec, RunMode::Plain); + let control = execute_unit_in_mode(unit, indexes, spec, RunMode::Observe); + + match (&reference, &control) { + (Ok(reference), Ok(control)) => { + let fields = compare(&control.outcome, &reference.outcome); + if !fields.is_empty() { + return ChaosVerdict::failed( + ChaosClass::ControlDrift, + format!( + "an observation-only inspector moved: {}", + fields.iter().map(|f| f.label()).collect::>().join(", ") + ), + ); + } + if !control.ledger.is_zero() { + return ChaosVerdict::failed( + ChaosClass::ControlDrift, + format!( + "an observation-only inspector booked a ledger entry: {:?}", + control.ledger + ), + ); + } + } + (Err(reference), Err(control)) => { + let (reference, control) = (reference.to_string(), control.to_string()); + if reference != control { + return ChaosVerdict::failed( + ChaosClass::ControlDrift, + format!("the runs were declined differently: {reference} != {control}"), + ); + } + } + (Ok(_), Err(e)) | (Err(e), Ok(_)) => { + return ChaosVerdict::failed( + ChaosClass::ControlDrift, + format!("only one of the two read-only runs executed: {e}"), + ) + } + } + + let chaos = execute_unit_in_mode(unit, indexes, spec, RunMode::Chaos { seed, filter }); + let applied = chaos.as_ref().ok().and_then(|run| run.chaos.clone()).unwrap_or_default(); + + let (class, detail) = match (reference.is_ok(), &chaos) { + (true, Ok(_)) => (ChaosClass::Pass, None), + // The runner declined this vector before execution — an intrinsic-gas overrun, an + // unsupported transaction shape — and declined it the same way with the inspector + // attached. Nothing executed, so nothing was tested; counted rather than passed. + (false, Err(_)) => (ChaosClass::Skipped, None), + (true, Err(e)) => ( + ChaosClass::ChaosRejected, + Some(format!("the rewriting run was declined where the reference executed: {e}")), + ), + (false, Ok(_)) => ( + ChaosClass::ChaosRejected, + Some("the rewriting run executed where the reference was declined".to_string()), + ), + }; + ChaosVerdict { class, applied, detail } +} + +/// What one vector's three runs produced. +#[derive(Debug, Clone, Default)] +pub struct ChaosVerdict { + /// How the runs came out. + pub class: ChaosClass, + /// What the rewriting run mutated. + pub applied: ChaosTally, + /// What went wrong, for a verdict that needs a human. + pub detail: Option, +} + +impl ChaosVerdict { + /// A verdict that failed before the rewriting run was reached, so it mutated nothing. + fn failed(class: ChaosClass, detail: String) -> Self { + Self { class, applied: ChaosTally::default(), detail: Some(detail) } + } +} + +/// The per-shape aggregate a whole sweep applied, plus the verdict counts. +#[derive(Debug, Clone, Default)] +pub struct ChaosSweepTally { + /// Vectors per [`ChaosClass`], keyed by [`ChaosClass::label`]. + pub classes: BTreeMap<&'static str, usize>, + /// Mutations applied over the whole sweep, per shape. + pub shapes: ChaosTally, + /// Every vector that needs a human. + pub flagged: Vec, + /// Files the runner could not read or parse at all, as rendered errors. + pub file_errors: Vec, + /// Files validation skips by filename, and which the sweep therefore judged no vector of. + pub skipped_files: usize, +} + +impl ChaosSweepTally { + /// Number of vectors in a class. + pub fn count(&self, class: ChaosClass) -> usize { + self.classes.get(class.label()).copied().unwrap_or(0) + } + + /// Total number of vectors judged. + pub fn total(&self) -> usize { + self.classes.values().sum() + } + + /// Whether the run should fail its gate. + /// + /// The two content conditions are a failing verdict and a file the sweep could not read. The + /// other two are what make those mean something: a sweep that judged no vector reaches the + /// gate with every count truthfully zero, and so does one whose inspector never mutated + /// anything — a corpus that never arrived and a chaos run that was not chaotic both look + /// exactly like a clean sweep from the counts alone. + pub fn is_failure(&self) -> bool { + self.total() == 0 || + self.shapes.total() == 0 || + !self.flagged.is_empty() || + !self.file_errors.is_empty() + } + + /// Records one vector's verdict. + pub fn record(&mut self, verdict: UnitChaos) { + *self.classes.entry(verdict.class.label()).or_insert(0) += 1; + if verdict.class.is_failure() { + self.flagged.push(verdict); + } + } + + /// Merges another tally into this one. + pub fn merge(&mut self, other: Self) { + for (label, count) in other.classes { + *self.classes.entry(label).or_insert(0) += count; + } + self.shapes.merge(&other.shapes); + self.flagged.extend(other.flagged); + self.file_errors.extend(other.file_errors); + self.skipped_files += other.skipped_files; + } +} + +/// Runs the chaos comparison over every transaction vector of every unit of one fixture file. +pub fn chaos_test_suite( + path: &Path, + spec: &SpecName, + global_seed: u64, + filter: ShapeFilter, +) -> Result<(Vec, ChaosTally), TestError> { + let path_str = path.to_string_lossy().into_owned(); + if skip_test(path) { + return Ok((vec![], ChaosTally::default())); + } + + let fixture_err = |msg: String| TestError { + name: "chaos".to_string(), + path: path_str.clone(), + kind: TestErrorKind::FixtureError(msg), + }; + let source = std::fs::read_to_string(path).map_err(|e| fixture_err(format!("read: {e}")))?; + let suite: TestSuite = serde_json::from_str(&source).map_err(|e| TestError { + name: "Unknown".to_string(), + path: path_str.clone(), + kind: e.into(), + })?; + + let mut verdicts = Vec::with_capacity(suite.0.len()); + let mut shapes = ChaosTally::default(); + for (name, unit) in suite.0 { + let vectors = unit.vectors(); + let multi = vectors.len() > 1; + for indexes in vectors { + let label = if multi { vector_label(&name, indexes) } else { name.clone() }; + let seed = vector_seed(global_seed, &path_str, &label, indexes); + let verdict = + match panic_capture::catch(|| chaos_unit(&unit, indexes, spec, seed, filter)) { + Ok(verdict) => { + shapes.merge(&verdict.applied); + UnitChaos { + name: label, + path: path_str.clone(), + seed, + class: verdict.class, + mutations: verdict.applied.total(), + detail: verdict.detail, + } + } + // A vector that panicked has no tally to report and still has to be counted. + Err(report) => UnitChaos { + name: label, + path: path_str.clone(), + seed, + class: ChaosClass::Panic, + mutations: 0, + detail: Some(report), + }, + }; + verdicts.push(verdict); + } + } + Ok((verdicts, shapes)) +} + +/// How a corpus-wide chaos run behaves. +#[derive(Debug, Clone, Copy)] +pub struct ChaosRunConfig { + /// The spec every run executes under. + pub spec: SpecName, + /// The global seed every vector's own seed is derived from. + pub seed: u64, + /// Which mutations the rewriting run is allowed to make. + pub filter: ShapeFilter, + /// Run every file on one thread. + pub single_thread: bool, + /// Draw a progress bar. + pub progress: bool, +} + +/// Runs the chaos comparison over every fixture file, in parallel. +/// +/// Installs the panic capture hook, for the same reason the differential sweep does: a +/// `debug_assert!` one vector trips becomes that vector's verdict instead of taking down a worker +/// thread, which is what makes a single-process full-corpus sweep possible at all. +pub fn run_chaos(scan: FixtureScan, config: ChaosRunConfig) -> ChaosSweepTally { + panic_capture::install_capture_hook(); + + let FixtureScan { files, errors } = scan; + let n_files = files.len(); + let bar = Arc::new(ProgressBar::with_draw_target( + Some(n_files as u64), + if config.progress { ProgressDrawTarget::stdout() } else { ProgressDrawTarget::hidden() }, + )); + let queue = Arc::new(Mutex::new(files)); + let next = Arc::new(AtomicUsize::new(0)); + let threads = if config.single_thread { + 1 + } else { + std::thread::available_parallelism().map_or(1, |n| n.get().min(n_files.max(1))) + }; + + let mut handles = Vec::with_capacity(threads); + for i in 0..threads { + let (queue, next, bar) = (queue.clone(), next.clone(), bar.clone()); + handles.push( + std::thread::Builder::new() + .name(format!("chaos-{i}")) + .spawn(move || { + let mut tally = ChaosSweepTally::default(); + loop { + let index = next.fetch_add(1, Ordering::SeqCst); + let Some(path) = queue.lock().unwrap().get(index).cloned() else { + return tally; + }; + if is_skipped_fixture(&path) { + tally.skipped_files += 1; + bar.inc(1); + continue; + } + match chaos_test_suite(&path, &config.spec, config.seed, config.filter) { + Ok((verdicts, shapes)) => { + tally.shapes.merge(&shapes); + for verdict in verdicts { + tally.record(verdict); + } + } + Err(e) => tally.file_errors.push(e.to_string()), + } + bar.inc(1); + } + }) + .expect("spawn chaos worker"), + ); + } + + let mut tally = ChaosSweepTally { file_errors: errors, ..ChaosSweepTally::default() }; + for handle in handles { + match handle.join() { + Ok(worker) => tally.merge(worker), + Err(_) => tally + .file_errors + .push("a chaos worker thread panicked; its files were not judged".to_string()), + } + } + bar.finish_and_clear(); + tally +} diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index 9666c13c..a2fbbaa1 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -46,6 +46,7 @@ //! over-reports instead of granting an exemption on the strength of bytes the fixture chose. use crate::{ + chaos::{CallbackCounter, ChaosInspector, ShapeFilter}, panic_capture, runner::{ configure_max_blobs, execution_status, external_envs_for, find_all_json_tests, halt_reason, @@ -68,7 +69,7 @@ use mega_evm::{ interpreter::{interpreter::EthInterpreter, interpreter_action::FrameInput}, primitives::{Bytes, B256}, }, - MegaContext, MegaEvm, MegaHaltReason, MegaLimitExceeded, MegaTransaction, + InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaLimitExceeded, MegaTransaction, MegaTransactionNew as _, VOLATILE_DATA_ACCESS_DISABLED_SELECTOR, }; use std::{ @@ -791,11 +792,8 @@ pub fn judge(fields: &[DiffField], target: &SpecOutcome, base: &SpecOutcome) -> } /// Executes one unit's given transaction vector under `spec` and collects its outcome and -/// evidence. -/// -/// Mirrors the validation path exactly — the same config, block environment, external -/// environment, block hashes and `BaseFeeVault` pruning — so the roots it computes are the roots -/// validation would check. +/// evidence — the differential classifier's entry point into +/// [`execute_unit_in_mode`](execute_unit_in_mode). /// /// `collect_evidence` runs the execution under [`FrameEvidenceInspector`], which is what makes an /// inner frame's outcome visible; it costs an inspected interpreter loop, so the differential @@ -806,6 +804,62 @@ pub fn execute_unit_outcome( spec: &SpecName, collect_evidence: bool, ) -> Result { + let mode = if collect_evidence { RunMode::Evidence } else { RunMode::Plain }; + execute_unit_in_mode(unit, indexes, spec, mode).map(|run| run.outcome) +} + +/// Which inspector, if any, drives a unit's execution. +/// +/// Every mode runs the same setup — the same config, block environment, external environment, +/// block hashes and `BaseFeeVault` pruning — so that what a mode changes is the inspector and +/// nothing else. That is what lets a rewriting run be compared against a plain one and the +/// difference be attributed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RunMode { + /// No inspector at all: revm's plain frame loops. + Plain, + /// The read-only [`FrameEvidenceInspector`]. + Evidence, + /// A read-only inspector that counts the callbacks it is handed and changes nothing — the + /// control a rewriting run is judged against. + Observe, + /// [`ChaosInspector`](crate::chaos::ChaosInspector), seeded with `seed` and restricted to + /// what `filter` allows. + Chaos { + /// The stream this run's decisions come from. + seed: u64, + /// Which mutations the run may make. + filter: ShapeFilter, + }, +} + +/// One unit's execution, with whatever the mode's inspector collected alongside it. +#[derive(Debug, Clone)] +pub struct UnitExecution { + /// The quantities the differential classifier compares. + pub outcome: SpecOutcome, + /// What the chaos inspector did, in [`RunMode::Chaos`]. + pub chaos: Option, + /// How many callbacks the observing inspector was handed, in [`RunMode::Observe`]. + pub observed: u64, + /// What the measurement shim booked for the transaction. + /// + /// Empty for every mode but [`RunMode::Chaos`] — which is itself an assertion the chaos sweep + /// makes, since a read-only inspector that moved a lane would not be read-only. + pub ledger: InspectorLedger, +} + +/// Executes one unit's given transaction vector under `spec`, with `mode`'s inspector attached. +/// +/// Mirrors the validation path exactly — the same config, block environment, external +/// environment, block hashes and `BaseFeeVault` pruning — so the roots it computes are the roots +/// validation would check. +pub fn execute_unit_in_mode( + unit: &TestUnit, + indexes: TxPartIndices, + spec: &SpecName, + mode: RunMode, +) -> Result { let mut cfg = CfgEnv::default(); // See `execute_test_suite`: revm-27 chain-id gate-off (revm 40 default is true). cfg.tx_chain_id_check = false; @@ -832,17 +886,38 @@ pub fn execute_unit_outcome( let mut megatx = MegaTransaction::new(tx); megatx.enveloped_tx = Some(Bytes::default()); - let (executed, frames, ctx) = if collect_evidence { - let mut evm = MegaEvm::new(evm_context).with_inspector(FrameEvidenceInspector::default()); - let executed = evm.execute_transaction(megatx); - let inner = evm.into_inner(); - let frames = Some(inner.inspector.evidence()); - (executed, frames, inner.ctx) - } else { - let mut evm = MegaEvm::new(evm_context); - let executed = evm.execute_transaction(megatx); - let inner = evm.into_inner(); - (executed, None, inner.ctx) + let mut chaos_tally = None; + let mut observed = 0; + let (executed, frames, ctx) = match mode { + RunMode::Evidence => { + let mut evm = + MegaEvm::new(evm_context).with_inspector(FrameEvidenceInspector::default()); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + let frames = Some(inner.inspector.evidence()); + (executed, frames, inner.ctx) + } + RunMode::Observe => { + let mut evm = MegaEvm::new(evm_context).with_inspector(CallbackCounter::default()); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + observed = inner.inspector.callbacks(); + (executed, None, inner.ctx) + } + RunMode::Chaos { seed, filter } => { + let mut evm = + MegaEvm::new(evm_context).with_inspector(ChaosInspector::new(seed, filter)); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + chaos_tally = Some(inner.inspector.tally()); + (executed, None, inner.ctx) + } + RunMode::Plain => { + let mut evm = MegaEvm::new(evm_context); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + (executed, None, inner.ctx) + } }; // Read the trackers before the context is dismantled: they carry the transaction's final @@ -855,6 +930,7 @@ pub fn execute_unit_outcome( let db = ctx.into_inner().journaled_state.database; let outcome = executed.map_err(|e| TestErrorKind::FixtureError(e.to_string()))?; + let ledger = outcome.inspector_ledger; let compute_gas_used = outcome.compute_gas_used; let compute_gas_destroyed = outcome.compute_gas_destroyed; let compute_gas_enforced = outcome.compute_gas_enforced; @@ -868,7 +944,7 @@ pub fn execute_unit_outcome( db.commit(outcome.result_and_state.state); prune_base_fee_vault_changes(db); - Ok(SpecOutcome { + let outcome = SpecOutcome { state_root: state_merkle_trie_root(db.cache.trie_account()), logs_root: log_rlp_hash(result.logs()), gas_used: result.tx_gas_used(), @@ -889,7 +965,8 @@ pub fn execute_unit_outcome( detained_limit, volatile_access, frames, - }) + }; + Ok(UnitExecution { outcome, chaos: chaos_tally, observed, ledger }) } /// Runs the differential comparison over every transaction vector of every unit of one fixture diff --git a/crates/mega-state-test/src/lib.rs b/crates/mega-state-test/src/lib.rs index 89cea2c1..5b5865a0 100644 --- a/crates/mega-state-test/src/lib.rs +++ b/crates/mega-state-test/src/lib.rs @@ -3,6 +3,8 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +pub mod chaos; + pub mod diff; pub mod panic_capture; diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs new file mode 100644 index 00000000..a3c0b6b6 --- /dev/null +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -0,0 +1,268 @@ +//! End-to-end tests for the chaos sweep. +//! +//! The corpus run is what the mode is for, and it cannot be a unit test: it needs the EEST corpus +//! and takes seconds. What can be pinned here is everything the corpus run's conclusions rest on — +//! that the same seed reproduces the same run, that different seeds are actually different, that +//! narrowing the shape filter narrows and nothing else, that the read-only control is read-only, +//! and that a sweep which mutated nothing fails its own gate rather than reporting a clean corpus. + +use state_test::{ + chaos::{ + chaos_unit, run_chaos, vector_seed, ChaosClass, ChaosRunConfig, ChaosShape, ShapeFilter, + }, + diff::{execute_unit_in_mode, RunMode}, + runner::FixtureScan, + types::{SpecName, TestUnit, TxPartIndices}, +}; +use std::path::PathBuf; + +const SENDER: &str = "0x1000000000000000000000000000000000000001"; +const CALLEE: &str = "0x2000000000000000000000000000000000000002"; +const INNER: &str = "0x3000000000000000000000000000000000000003"; + +/// The single transaction vector these hand-built fixtures declare. +const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; + +/// The filter every test here runs with: everything except the two windows the corpus sweep +/// currently reports as open findings. +/// +/// Those two windows are a statement about `MegaETH`'s accounting, not about this mode, and the +/// sweep is where they are measured. A tool test that tripped them would be testing the product, +/// and would go red for a reason that has nothing to do with what it is asserting. When they are +/// closed, this becomes `ShapeFilter::default()`. +fn tool_filter() -> ShapeFilter { + ShapeFilter::default().without_terminal_counter_edits().without_precompile_reclassification() +} + +/// `SSTORE(1, 1); CALL(0x2710 gas, INNER, no value, no args, no return); POP; LOG0(0, 0); STOP`. +/// +/// One of everything a callback can be handed: a storage write, a child frame, a log, and enough +/// plain opcodes between them for the stream to reach every callback family. +fn callee_code() -> String { + format!("0x600160015560006000600060006000 73{} 612710 f1 50 60006000a000", &INNER[2..]) + .replace(' ', "") +} + +/// `SSTORE(2, 2); CREATE(0, 0, 0); POP; STOP` — a child frame the create callbacks see. +const INNER_CODE: &str = "0x60026002556000600060006000f05000"; + +fn unit_json() -> serde_json::Value { + serde_json::json!({ + "env": { + "currentChainID": "0x18c6", + "currentCoinbase": "0x3000000000000000000000000000000000000009", + "currentDifficulty": "0x0", + "currentGasLimit": "0x1c9c380", + "currentNumber": "0x10", + "currentTimestamp": "0x3e8", + "currentBaseFee": "0x0", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000001", + "currentExcessBlobGas": "0x0" + }, + "pre": { + SENDER: { "balance": "0xde0b6b3a7640000", "code": "0x", "nonce": "0x0", "storage": {} }, + CALLEE: { "balance": "0x0", "code": callee_code(), "nonce": "0x0", "storage": {} }, + INNER: { "balance": "0x0", "code": INNER_CODE, "nonce": "0x0", "storage": {} }, + }, + "transaction": { + "type": 0, + "data": ["0x"], + "gasLimit": ["0x1e8480"], + "gasPrice": "0x0", + "nonce": "0x0", + "secretKey": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sender": SENDER, + "to": CALLEE, + "value": ["0x0"] + }, + "post": {} + }) +} + +fn unit() -> TestUnit { + serde_json::from_value(unit_json()).expect("valid unit json") +} + +/// The chaos run's tally for `unit` under `seed` and `filter`. +fn mutations(seed: u64, filter: ShapeFilter) -> Vec<(String, u32)> { + let run = + execute_unit_in_mode(&unit(), VECTOR_0, &SpecName::Rex7, RunMode::Chaos { seed, filter }) + .expect("the fixture executes"); + let tally = run.chaos.expect("a chaos run reports its tally"); + let mut applied: Vec<(String, u32)> = + tally.applied.iter().map(|(k, v)| ((*k).to_string(), *v)).collect(); + applied.sort(); + applied +} + +/// The same seed produces the same run, mutation for mutation. +/// +/// Everything else this mode claims rests on this: a flagged vector's report line is only a +/// reproduction if re-running it reproduces. +#[test] +fn test_a_seed_reproduces_its_own_run() { + let first = mutations(0xC0FFEE, tool_filter()); + let second = mutations(0xC0FFEE, tool_filter()); + assert!(!first.is_empty(), "the fixture must reach enough callbacks to mutate something"); + assert_eq!(first, second, "the same seed must produce the same mutations"); +} + +/// Different seeds produce different runs. +/// +/// The mirror of the test above, and the one that fails if the seed stops reaching the decision +/// stream at all — a generator wired to ignore its seed would pass reproducibility perfectly. +#[test] +fn test_different_seeds_produce_different_runs() { + let seeds = [1u64, 2, 3, 4, 5, 6, 7, 8]; + let runs: Vec<_> = seeds.iter().map(|s| mutations(*s, tool_filter())).collect(); + assert!( + runs.iter().any(|run| *run != runs[0]), + "eight seeds that all mutate identically means the seed reaches nothing: {runs:?}", + ); +} + +/// A vector's seed depends on every part of its identity, and on the global seed. +#[test] +fn test_a_vector_seed_separates_every_part_of_the_identity() { + let base = vector_seed(7, "a.json", "unit", VECTOR_0); + let others = [ + vector_seed(8, "a.json", "unit", VECTOR_0), + vector_seed(7, "b.json", "unit", VECTOR_0), + vector_seed(7, "a.json", "other", VECTOR_0), + vector_seed(7, "a.json", "unit", TxPartIndices { data: 1, gas: 0, value: 0 }), + vector_seed(7, "a.json", "unit", TxPartIndices { data: 0, gas: 1, value: 0 }), + vector_seed(7, "a.json", "unit", TxPartIndices { data: 0, gas: 0, value: 1 }), + ]; + for (i, other) in others.iter().enumerate() { + assert_ne!(base, *other, "identity component {i} does not reach the seed"); + } + assert_eq!(base, vector_seed(7, "a.json", "unit", VECTOR_0), "and the seed is a function"); +} + +/// Narrowing the filter keeps every surviving mutation where the full run put it. +/// +/// This is what makes narrowing a triage tool rather than a different experiment: the shapes that +/// remain are applied at the same callbacks, so a flagged mutation is still there to be found. +#[test] +fn test_narrowing_the_filter_keeps_the_surviving_mutations() { + let full = mutations(0xC0FFEE, tool_filter()); + let only = [ChaosShape::InjectGas, ChaosShape::DrainGas]; + let narrowed = mutations(0xC0FFEE, ShapeFilter::only(&only).without_terminal_counter_edits()); + let kept: Vec<_> = only.iter().map(|s| s.label()).collect(); + + assert!(!narrowed.is_empty(), "the narrowed run must still mutate something"); + for (shape, _) in &narrowed { + assert!(kept.contains(&shape.as_str()), "{shape} is not in the filter"); + } + for (shape, count) in &full { + if !kept.contains(&shape.as_str()) { + continue; + } + let narrowed_count = narrowed.iter().find(|(s, _)| s == shape).map_or(0, |(_, c)| *c); + assert!( + narrowed_count >= *count, + "{shape}: narrowing dropped a mutation the full run made ({count} -> {narrowed_count})", + ); + } +} + +/// Every shape label round-trips, and an unknown one is refused with a message that lists them. +#[test] +fn test_every_shape_label_parses_and_an_unknown_one_does_not() { + for shape in ChaosShape::ALL { + assert_eq!(ChaosShape::parse(shape.label()), Ok(shape)); + } + let error = ChaosShape::parse("not_a_shape").expect_err("an unknown label must be refused"); + assert!(error.contains("inject_gas"), "the message must list the known shapes: {error}"); +} + +/// The read-only control leaves the execution exactly as it found it. +/// +/// Checked here on one fixture and over the whole corpus by the sweep itself; this is the version +/// that fails in a unit test run rather than only in a five-second corpus sweep. +#[test] +fn test_the_control_inspector_changes_nothing() { + let unit = unit(); + let plain = execute_unit_in_mode(&unit, VECTOR_0, &SpecName::Rex7, RunMode::Plain) + .expect("the fixture executes"); + let observed = execute_unit_in_mode(&unit, VECTOR_0, &SpecName::Rex7, RunMode::Observe) + .expect("the fixture executes"); + + assert!(observed.observed > 0, "the control must actually be handed callbacks"); + assert!(observed.ledger.is_zero(), "and must book nothing: {:?}", observed.ledger); + assert!( + state_test::diff::compare(&observed.outcome, &plain.outcome).is_empty(), + "an observation-only inspector moved something", + ); +} + +/// A vector the rewriting run leaves executable comes back `Pass`, with mutations to show for it. +#[test] +fn test_a_mutated_vector_passes_with_mutations_recorded() { + let verdict = chaos_unit(&unit(), VECTOR_0, &SpecName::Rex7, 0xC0FFEE, tool_filter()); + assert_eq!(verdict.class, ChaosClass::Pass, "{:?}", verdict.detail); + assert!(verdict.applied.total() > 0, "the fixture must be mutated: {:?}", verdict.applied); + assert!(verdict.applied.callbacks > 0, "and the callbacks must be counted"); +} + +/// A sweep whose inspector mutated nothing fails its own gate. +/// +/// Every count such a run prints is truthful and every one of them is zero, which is exactly what +/// a clean sweep looks like. Reading it as a pass is how a chaos mode that stopped being chaotic +/// becomes a green nightly. +#[test] +fn test_a_sweep_that_mutated_nothing_is_a_failure() { + let path = write_suite("chaos_mode_no_mutations.json"); + let scan = FixtureScan { files: vec![path], errors: vec![] }; + // An empty allow-list: every draw is rejected, so the run executes the whole corpus and + // changes nothing. + let tally = run_chaos( + scan, + ChaosRunConfig { + spec: SpecName::Rex7, + seed: 1, + filter: ShapeFilter::only(&[]), + single_thread: true, + progress: false, + }, + ); + + assert_eq!(tally.count(ChaosClass::Pass), 1, "the vector must still be judged"); + assert_eq!(tally.count(ChaosClass::Panic), 0); + assert_eq!(tally.shapes.total(), 0, "and must have been mutated in no way at all"); + assert!(tally.is_failure(), "a sweep that tested nothing must not report success"); +} + +/// A sweep that did mutate, over the same fixture, passes. +#[test] +fn test_a_sweep_that_mutated_passes() { + let path = write_suite("chaos_mode_mutations.json"); + let scan = FixtureScan { files: vec![path], errors: vec![] }; + let tally = run_chaos( + scan, + ChaosRunConfig { + spec: SpecName::Rex7, + seed: 1, + filter: tool_filter(), + single_thread: true, + progress: false, + }, + ); + + assert_eq!(tally.count(ChaosClass::Pass), 1); + assert!(tally.shapes.total() > 0, "the fixture must be mutated"); + assert!(tally.flagged.is_empty(), "{:?}", tally.flagged); + assert!(!tally.is_failure(), "the sweep must pass"); +} + +/// Writes the fixture to a unique temp file and returns its path. +fn write_suite(file_name: &str) -> PathBuf { + let suite: serde_json::Map = + std::iter::once(("chaos_unit".to_string(), unit_json())).collect(); + let dir = std::env::temp_dir().join("mega_state_test_chaos_mode"); + std::fs::create_dir_all(&dir).expect("mkdir"); + let path = dir.join(file_name); + std::fs::write(&path, serde_json::to_string_pretty(&suite).expect("serialize")) + .expect("write fixture"); + path +} diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 01b6852c..50e0ac75 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; use state_test::{ + chaos::{run_chaos, ChaosClass, ChaosRunConfig, ChaosShape, ChaosSweepTally, ShapeFilter}, diff::{collect_fixture_files, run_diff, DiffClass, DiffRunConfig, DiffSpecs, DiffTally}, runner::{ bench_test_suite, fill_test_suite, fill_test_suite_keep_going, find_all_json_tests, @@ -91,6 +92,45 @@ pub struct Cmd { /// inner frame the transaction's own result hides is reported as unexplained. #[arg(long, requires = "diff_spec")] diff_no_frame_evidence: bool, + /// Execute each fixture three times — with no inspector, with a read-only one, and with a + /// deterministic rewriting one seeded from this value and the vector's own identity — and + /// report how the three came out. + /// + /// The spec every run executes under is `--bench-spec`, which is therefore required. Nothing + /// is written and nothing is compared against a recorded expectation: the read-only run is + /// judged against the run with no inspector, and the rewriting run is judged by whether the + /// execution's own gas-accounting cross-checks survive it. Those cross-checks are debug + /// assertions, so this mode is only meaningful in a build that keeps them. + #[arg(long, value_name = "SEED", requires = "bench_spec", conflicts_with_all = ["bench", "fill", "diff_spec"])] + chaos_seed: Option, + /// Write the chaos run's tally and every flagged vector to this file, as JSON. + #[arg(long, value_name = "FILE", requires = "chaos_seed")] + chaos_report: Option, + /// Restrict the rewriting run to these shapes (comma-separated labels). + /// + /// For triage: a flagged vector is re-run with the list narrowed until the smallest set that + /// still reproduces it is found. Narrowing does not reshuffle the decision stream, so each + /// surviving mutation stays where the full run put it; it does leave the mutation budget + /// unspent on rejected draws, so a narrowed run can reach further into a transaction. + #[arg(long, value_name = "SHAPES", value_delimiter = ',', requires = "chaos_seed")] + chaos_shapes: Vec, + /// Make no gas-counter edit at the `step_end` of a frame's terminating opcode. + /// + /// That is the one callback where the interpreter's counter has already been copied into the + /// frame's action — revm runs `step_end` after the instruction that set the action, and the + /// action carries its own snapshot — so an edit there reaches the counter `MegaETH`'s tail + /// settlement reads and nothing the caller ever sees. The other half of the same triage knob + /// as `--chaos-shapes`. + #[arg(long, requires = "chaos_seed")] + chaos_skip_terminal_counter_edits: bool, + /// Rewrite no precompile result's classification. + /// + /// A precompile is answered inside the frame init and never becomes a child frame, so the + /// executed / destroyed split of its forwarded envelope is booked at its own recording site, + /// before any callback sees the synthetic result. The third triage knob, alongside + /// `--chaos-shapes` and `--chaos-skip-terminal-counter-edits`. + #[arg(long, requires = "chaos_seed")] + chaos_skip_precompile_reclassification: bool, } impl Cmd { @@ -99,6 +139,9 @@ impl Cmd { if self.diff_spec.is_some() { return self.run_diff(); } + if self.chaos_seed.is_some() { + return self.run_chaos(); + } if self.fill { return self.run_fill(); } @@ -318,6 +361,76 @@ impl Cmd { }) } + /// Build the chaos run's shape filter from `--chaos-shapes` and + /// `--chaos-skip-terminal-counter-edits`. + fn resolve_chaos_filter(&self) -> Result { + let mut filter = if self.chaos_shapes.is_empty() { + ShapeFilter::default() + } else { + let shapes = self + .chaos_shapes + .iter() + .map(|label| ChaosShape::parse(label)) + .collect::, _>>() + .map_err(|detail| TestError { + name: "--chaos-shapes".to_string(), + path: String::new(), + kind: TestErrorKind::FixtureError(detail), + })?; + ShapeFilter::only(&shapes) + }; + if self.chaos_skip_terminal_counter_edits { + filter = filter.without_terminal_counter_edits(); + } + if self.chaos_skip_precompile_reclassification { + filter = filter.without_precompile_reclassification(); + } + Ok(filter) + } + + /// Sweep the corpus under a deterministic rewriting inspector (see `--chaos-seed`). + fn run_chaos(&self) -> Result<(), TestError> { + let seed = self.chaos_seed.expect("run_chaos is only reached with --chaos-seed"); + // Clap's `requires = "bench_spec"` makes the spec explicit before this point. + let spec = self.resolve_spec()?.expect("--chaos-seed requires --bench-spec"); + let filter = self.resolve_chaos_filter()?; + let scan = collect_fixture_files(&self.paths)?; + + let tally = run_chaos( + scan, + ChaosRunConfig { + spec, + seed, + filter, + single_thread: self.single_thread, + progress: !self.json, + }, + ); + + print_chaos_tally(&tally, spec, seed, filter); + if let Some(report) = &self.chaos_report { + let json = serde_json::to_string_pretty(&chaos_report_json(&tally, spec, seed, filter)) + .expect("serialize chaos report"); + std::fs::write(report, json).map_err(|e| TestError { + name: "chaos report".to_string(), + path: report.display().to_string(), + kind: TestErrorKind::FixtureError(format!("write: {e}")), + })?; + } + + if !tally.is_failure() { + return Ok(()); + } + Err(TestError { + name: "chaos summary".to_string(), + path: String::new(), + kind: TestErrorKind::TestsFailed { + failed: tally.flagged.len().max(1), + total: tally.total(), + }, + }) + } + /// Benchmark every fixture under the given paths and print the results as JSON. /// /// A single benchmarked unit prints one object `{ gas_used, success, bench }`; @@ -478,6 +591,105 @@ fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { } } +/// Every chaos verdict, in the order a reader wants them. +const CHAOS_CLASSES: [ChaosClass; 5] = [ + ChaosClass::Pass, + ChaosClass::ControlDrift, + ChaosClass::ChaosRejected, + ChaosClass::Skipped, + ChaosClass::Panic, +]; + +/// Prints the chaos run's tally, plus every vector that needs a human. +fn print_chaos_tally(tally: &ChaosSweepTally, spec: SpecName, seed: u64, filter: ShapeFilter) { + println!("\nChaos run: {spec:?} under seed {seed} over {} vector(s)", tally.total()); + if !filter.is_complete() { + println!(" (shape filter: {})", chaos_filter_label(filter)); + } + for class in CHAOS_CLASSES { + println!(" {:<16} {}", class.label(), tally.count(class)); + } + if tally.skipped_files > 0 { + println!( + " ({} file(s) skipped by filename, no vector of them judged)", + tally.skipped_files + ); + } + println!( + "Mutations applied: {} over {} callback(s)", + tally.shapes.total(), + tally.shapes.callbacks + ); + for (shape, count) in &tally.shapes.applied { + println!(" {shape:<20} {count}"); + } + for verdict in &tally.flagged { + println!( + "{}\t{}::{}\tseed={}\t{}", + verdict.class.label(), + verdict.path, + verdict.name, + verdict.seed, + verdict.detail.as_deref().unwrap_or("-").replace('\n', " ") + ); + } + for error in &tally.file_errors { + println!("FILE_ERROR\t{}", error.replace('\n', " ")); + } +} + +/// The machine-readable form of [`print_chaos_tally`], for `--chaos-report`. +fn chaos_report_json( + tally: &ChaosSweepTally, + spec: SpecName, + seed: u64, + filter: ShapeFilter, +) -> serde_json::Value { + json!({ + "spec": format!("{spec:?}"), + "seed": seed, + "shapeFilter": chaos_filter_label(filter), + "total": tally.total(), + "classes": CHAOS_CLASSES + .iter() + .map(|c| (c.label().to_string(), json!(tally.count(*c)))) + .collect::>(), + "callbacks": tally.shapes.callbacks, + "mutations": tally.shapes.total(), + "mutationsByShape": tally.shapes.applied, + "fileErrors": tally.file_errors, + "skippedFiles": tally.skipped_files, + "flagged": tally + .flagged + .iter() + .map(|v| json!({ + "class": v.class.label(), + "path": v.path, + "name": v.name, + "seed": v.seed, + "mutations": v.mutations, + "detail": v.detail, + })) + .collect::>(), + }) +} + +/// What a chaos run's shape filter allows, as one line. +fn chaos_filter_label(filter: ShapeFilter) -> String { + let mut parts: Vec = ChaosShape::ALL + .into_iter() + .filter(|shape| filter.allows(*shape)) + .map(|shape| shape.label().to_string()) + .collect(); + if !filter.allows_terminal_counter_edits() { + parts.push("no-terminal-counter-edits".to_string()); + } + if !filter.allows_precompile_reclassification() { + parts.push("no-precompile-reclassification".to_string()); + } + parts.join(",") +} + /// The machine-readable form of [`print_diff_tally`], for `--diff-report`. fn diff_report_json(tally: &DiffTally, target: SpecName, base: SpecName) -> serde_json::Value { json!({ diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md index e5559e6e..a5135aa8 100644 --- a/tools/eest-sweep/README.md +++ b/tools/eest-sweep/README.md @@ -68,15 +68,52 @@ run is cleared by removing `/.unpack.lock`. `tests/cache_integrity.sh` drives all of this against a synthetic archive and a stub binary; it runs per-PR in CI and needs neither the corpus nor a build. +## Chaos mode + +`--mode chaos` asks a different question of the same corpus: not whether two specs agree, but whether the accounting survives an inspector that rewrites what it is handed. + +Every vector is executed three times under the target spec — with no inspector, with a read-only one, and with a deterministic rewriting one — and two things are checked. + +- **Observation is free.** The read-only run must be identical to the run with no inspector on every quantity the differential classifier compares, and must leave an empty inspector ledger. That is the property every tracer in production depends on, checked against the whole corpus rather than a handful of fixtures. +- **Rewriting does not break the books.** Every gas-accounting cross-check MegaETH has is a debug assertion, so under the default `hivetests` profile a broken conservation law is a panic, and a panic is that vector's verdict rather than a lost worker thread. + +The rewriting inspector's decisions come from a hash of the global seed and the vector's own identity — its fixture path, unit name and transaction indexes. +No clock, no address, no iteration order. +The same seed and the same corpus produce the same mutations on any machine, in any thread count, so a flagged vector's report line carries everything needed to re-run exactly it. + +What fails the run: a `PANIC`, a `CONTROL_DRIFT` (the read-only run moved something), a `CHAOS_REJECTED` (the rewriting run changed whether the transaction executes at all), a file the sweep could not read, a run that judged no vector — and a run whose inspector mutated nothing, which would report every count truthfully zero while testing nothing. + +One rewrite shape is deliberately absent from the pool: turning a failed contract creation into a successful one. +The shim refuses that shape and asserts on it, so including it would make the detector's own firing the sweep's dominant result. +`crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs` pins the refusal instead. + +### Narrowing a flagged vector + +Three knobs, passed through with `--chaos-arg`: + +```bash +tools/eest-sweep/run.sh --mode chaos \ + --chaos-arg --chaos-shapes --chaos-arg inject_gas,drain_gas +``` + +- `--chaos-shapes LIST` restricts the pool to the named shapes. Narrowing does not reshuffle the decision stream, so each surviving mutation stays where the full run put it; it does leave the mutation budget unspent on rejected draws, so a narrowed run can reach further into a transaction. +- `--chaos-skip-terminal-counter-edits` makes no gas-counter edit at the `step_end` of a frame's terminating opcode, where the interpreter's counter has already been copied into the frame's action. +- `--chaos-skip-precompile-reclassification` rewrites no precompile result's classification, whose executed / destroyed split is booked at the precompile's own recording site before any callback sees it. + ## Options ``` --target-spec SPEC Spec under test (default: Rex7) --base-spec SPEC Frozen spec to compare against (default: Rex6) ---mode diff|fill diff (default) executes both specs and classifies the differences. +--mode diff|fill|chaos + diff (default) executes both specs and classifies the differences. fill executes the target spec only and recomputes each fixture's `post` on a private copy — the older scan, kept because it exercises the fixture-writing path that diff mode does not touch. + chaos executes the target spec three times per vector under three + inspectors; see "Chaos mode" above. +--chaos-seed SEED Global seed for chaos mode (default: 1). +--chaos-arg ARG Extra argument passed through to the chaos run; repeatable. --corpus-dir DIR Use an already-unpacked `state_tests` tree instead of downloading. --cache-dir DIR Where to keep the downloaded archive (default: .eest-cache). --report-dir DIR Where to write the report and log (default: .eest-report). diff --git a/tools/eest-sweep/run.sh b/tools/eest-sweep/run.sh index 028c5375..a20a37e0 100755 --- a/tools/eest-sweep/run.sh +++ b/tools/eest-sweep/run.sh @@ -3,21 +3,31 @@ # Run the EEST state-test corpus through the mega-evm state-test runner. # # One command: fetch and verify the pinned fixture release, unpack its `state_tests` subtree, and -# execute every fixture. Two gates fail the run — a fixture that panics, and a difference between -# the spec under test and its frozen base that no MegaETH mechanism accounts for. Everything else -# (fixtures the runner declines, differences the classifier explains) is reported and does not -# fail. +# execute every fixture. In the default `diff` mode two gates fail the run — a fixture that panics, +# and a difference between the spec under test and its frozen base that no MegaETH mechanism +# accounts for. Everything else (fixtures the runner declines, differences the classifier explains) +# is reported and does not fail. `chaos` mode has its own gates; see `--mode`. # # Usage: # tools/eest-sweep/run.sh [options] # # --target-spec SPEC Spec under test (default: Rex7) # --base-spec SPEC Frozen spec to compare against (default: Rex6) -# --mode diff|fill diff: execute under both specs and classify the differences (default). +# --mode diff|fill|chaos +# diff: execute under both specs and classify the differences (default). # fill: execute under the target spec only and recompute each fixture's # `post` in place, on a private copy. `diff` runs the target spec through # the same execution path, so it already covers what `fill` scans for; # `fill` remains available to exercise the fixture-writing path itself. +# chaos: execute under the target spec three times per vector — with no +# inspector, with a read-only one, and with a deterministic rewriting one — +# and check that observation stays free and that nothing the rewriting run +# does breaks the gas-accounting cross-checks. +# --chaos-seed SEED Global seed for `--mode chaos` (default: 1). Each vector's own seed is +# derived from this and the vector's identity, so a flagged vector +# reproduces exactly. +# --chaos-arg ARG Extra argument passed through to the chaos run; repeatable. Used to +# narrow a flagged vector (`--chaos-shapes`, `--chaos-skip-*`). # --corpus-dir DIR Use an already-unpacked `state_tests` tree instead of downloading. # --cache-dir DIR Where to keep the downloaded archive (default: .eest-cache). # --report-dir DIR Where to write the report and log (default: .eest-report). @@ -34,6 +44,8 @@ source "$REPO_ROOT/tools/eest-sweep/corpus.env" TARGET_SPEC="Rex7" BASE_SPEC="Rex6" MODE="diff" +CHAOS_SEED="1" +CHAOS_ARGS=() CORPUS_DIR="" CACHE_DIR="$REPO_ROOT/.eest-cache" REPORT_DIR="$REPO_ROOT/.eest-report" @@ -45,6 +57,8 @@ while [ $# -gt 0 ]; do --target-spec) TARGET_SPEC="$2"; shift 2 ;; --base-spec) BASE_SPEC="$2"; shift 2 ;; --mode) MODE="$2"; shift 2 ;; + --chaos-seed) CHAOS_SEED="$2"; shift 2 ;; + --chaos-arg) CHAOS_ARGS+=("$2"); shift 2 ;; --corpus-dir) CORPUS_DIR="$2"; shift 2 ;; --cache-dir) CACHE_DIR="$2"; shift 2 ;; --report-dir) REPORT_DIR="$2"; shift 2 ;; @@ -58,8 +72,8 @@ while [ $# -gt 0 ]; do done case "$MODE" in - diff|fill) ;; - *) echo "--mode must be 'diff' or 'fill', got '$MODE'" >&2; exit 2 ;; + diff|fill|chaos) ;; + *) echo "--mode must be 'diff', 'fill' or 'chaos', got '$MODE'" >&2; exit 2 ;; esac # `sha256sum` on Linux, `shasum -a 256` on macOS. @@ -257,6 +271,14 @@ if [ "$MODE" = "diff" ]; then --diff-spec "$BASE_SPEC" \ --diff-report "$REPORT_DIR/diff-report.json" \ "$CORPUS_DIR" >"$LOG" 2>&1 || STATUS=$? +elif [ "$MODE" = "chaos" ]; then + echo "==> chaos sweep under $TARGET_SPEC, seed $CHAOS_SEED" + "$BIN" \ + --bench-spec "$TARGET_SPEC" \ + --chaos-seed "$CHAOS_SEED" \ + --chaos-report "$REPORT_DIR/chaos-report.json" \ + "${CHAOS_ARGS[@]+"${CHAOS_ARGS[@]}"}" \ + "$CORPUS_DIR" >"$LOG" 2>&1 || STATUS=$? else # `--fill` rewrites each fixture in place, so it runs on a private copy and never touches the # cached corpus other runs share. @@ -284,6 +306,12 @@ if [ "$MODE" = "diff" ]; then exit "$STATUS" fi +if [ "$MODE" = "chaos" ]; then + echo "==> report: $REPORT_DIR/chaos-report.json" + # Same reasoning as diff mode: the CLI's own gate is the verdict. + exit "$STATUS" +fi + # `--fill` has no notion of an expected failure: it exits non-zero for every unit it could not # fill, and thousands of them are fixtures neither spec would execute. Re-derive the gate from the # tally so `fill` mode fails on the same two conditions `diff` mode does. From 6891cddc24088490983ced3acf8c221e5d87fd3c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 14:51:54 +0800 Subject: [PATCH 115/208] docs(evm): state the inspector contract as a per-shape table --- crates/mega-evm/src/evm/AGENTS.md | 59 ++++++++++++++++--- crates/mega-evm/src/evm/result.rs | 5 ++ crates/mega-evm/src/limit/inspector_ledger.rs | 10 ++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index df4f1e33..2087757e 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -23,20 +23,63 @@ 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. Observational inspectors (`NoOpInspector`, tracers that only read) are bit-identical to no inspector at all, and must stay so. -- Every inspector is wrapped in `MeasuredInspector` (`inspector.rs`) before it reaches the inner EVM; the public accessors hand the unwrapped one back, so the type a caller names is unchanged. - The shim snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. - A gas-counter edit is kept out of REX7 compute accounting (the checkpoint baseline is shifted by it) and out of the compute headroom (the gas clamp is re-derived immediately). - A raised frame `gas_limit` is booked as conjured gas so the destroyed-remainder derivation still balances. - Add the shim's counterpart when adding an `Inspector` callback: an unwrapped callback is an unmeasured hole, not a compile error. -- A rewrite the last mutating callback makes to a frame result's gas is booked from the frame's settlement point rather than from the callback boundary, because whether it moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. - The gas an intercepting callback puts into a synthetic outcome travels through that same lane. - One shape is refused outright rather than measured: a `create_end` (or the `frame_end` after it) turning a failed creation into a successful one — see `reject_forbidden_create_rewrite`, and the verdict in `frame.rs` that gives such a rewrite no code to deposit even if the refusal were removed. +- Rewriting inspectors are supported in full, and what they do to gas is measured rather than assumed — see `## INSPECTOR CONTRACT` below for the per-shape table and the two shapes that are refused. - Under REX7 a frame's journal decision travels: the frame loops park it on `MegaEvm::deferred_journal` and `frame_return_result` carries it out, after `AdditionalLimit::before_frame_return_result` — the last thing that can rewrite a frame's result — and before the caller resumes. There is never more than one decision outstanding and it never survives the step it was parked for; `hold_deferred_journal` asserts that. Anything new that can rewrite a frame's result has to land inside that window, or it reopens the split the deferral closed. - Both frame loops and both frame-init paths run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. Add to the shared body, not to one copy: `tests/rex7/frame_loop_parity.rs` compares the two on every frame outcome, state included, and is what a one-sided edit fails. +## INSPECTOR CONTRACT + +Every inspector `MegaEvm` is handed is wrapped in `MeasuredInspector` (`inspector.rs`) before it reaches the inner EVM. +The public accessors hand the unwrapped inspector back, so the type a caller names is unchanged and the wrapper is not something a caller opts into or can opt out of. + +The shim's soundness rests on one fact: the EVM does not execute inside an inspector callback. +Anything that changes between the moment the shim delegates to the user's inspector and the moment control comes back is therefore the inspector's doing by construction, not by attribution — which is what makes the callback boundary a place a measurement can be taken at all. +The shim snapshots what it cares about on the way in, compares on the way out, and books the difference on `InspectorLedger` (`../limit/inspector_ledger.rs`), which travels out on `MegaTransactionOutcome::inspector_ledger`. + +### What each rewrite shape costs + +Read the table by the *argument the rewrite reaches through*, not by the tool that makes it: two tools editing the same argument are one row. + +| Rewrite shape | Support | Where it is booked | What enforcement sees | +| --- | --- | --- | --- | +| Read-only observation | Supported, free | nothing | an empty ledger, and numbers identical to an uninspected run | +| Gas written into a live interpreter's counter (`initialize_interp`, `step`, `step_end`, `log_full`) | Supported | `InspectorLedger::gas`, at the callback boundary | nothing: the checkpoint baseline shifts by the same amount, and the gas clamp is re-derived on the spot so injected gas buys no compute headroom | +| A frame input's `gas_limit`, raised or lowered (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::env`, at the callback boundary | nothing: a frame's compute budget comes from the tracker, not from its gas limit | +| A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | nothing on the `env` lane — the edited inputs never reach a frame — and the outcome's own gas on the `result` lane | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | +| A finished frame result's remaining gas (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::result`, at the frame's settlement point rather than at the callback boundary | nothing | +| A successful frame result rewritten into a revert or a halt | Supported | nothing — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it | +| A failed **call** frame rewritten into a success | Supported | nothing | the journal commits, so the frame's state follows the result its caller was handed | +| A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | +| The interpreter's stack or memory | Supported, unmeasured | nothing | the EVM executes on the edited state and meters it as its own work, because it is | +| A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | +| `CfgEnv` or the active gas schedule | **Refused** | — | the gas-schedule pin panics; the schedule belongs to the spec, and a rewritten one has no accounting lane that could rescue it | + +Two independent stops back the creation refusal: the shim restores the classification, and `frame.rs`'s `FrameJournalVerdict::CreateRejected` carries no code and no commit branch, so even with the refusal removed such a rewrite deposits nothing. + +Booking is a *reported* quantity throughout. No resource limit is ever compared against the ledger, and `MegaTransactionOutcome::compute_gas_enforced` comes off the enforcement lane rather than out of the reported total — so an inspector cannot buy a transaction headroom on any dimension. + +### Known gap in the counter lane + +One window books an adjustment that reaches nothing: a gas-counter edit made at the `step_end` of a frame's *terminating* opcode. +revm's inspected loop runs `step_end` after the instruction that set the frame's action, and the action carries its own snapshot of the gas, so an edit there moves the interpreter's counter — which `MegaETH`'s tail settlement reads — and never the result the caller is handed. +The shim books it as conjured gas anyway, and the conservation law then over-counts by exactly that amount; `tools/eest-sweep`'s chaos mode reproduces it, and `--chaos-skip-terminal-counter-edits` is the switch that excludes it. +The same window exists at the top-of-loop `step` when the bytecode has already ended, which is reachable only when an action was set before the loop began. + +### Rules for changing this + +- **Add the shim's counterpart when adding an `Inspector` callback.** + An unwrapped callback is an unmeasured hole, not a compile error. + `tests/rex7/inspector_cheat_matrix.rs` enumerates every callback × shape pair and fails on one that is neither covered nor excused, which is what turns a new callback into a red test. +- **Book a result rewrite from the frame's settlement point, not from the callback boundary.** + Whether such an edit moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. + The gas an intercepting callback puts into a synthetic outcome travels through that same lane. +- **Keep every rewrite out of a block.** + Supporting a rewrite is not the same as admitting one: the canonical block-execution path refuses a transaction whose ledger is non-zero, in release builds as well as debug, because an inspector is one node's configuration and its edits reach the receipt. + See `tests/block_executor/inspector_guard.rs`. + ## WHERE TO LOOK - New spec opcode delta: `instructions.rs` (`mini_rex`, `rex`, `rex2`, `rex3`, `rex4`, `rex5`, `rex6`, `rex7` tables; `rex6` still aliases `rex5` and expresses its deltas as `is_enabled` dispatch inside the shared handlers; `rex7` is a standalone checkpoint table built from revm's base table, with the 17 storage / CALL / CREATE / SELFDESTRUCT / not-yet-activated slots inherited from `rex6`, and with 15 volatile `*_checkpoint` handlers plus `gas_checkpoint` registered as rex7-only). - Volatile access detention trigger changes: `host.rs` and volatile wrappers in `instructions.rs`. diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 6bf5eac4..7b1465c4 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -106,6 +106,11 @@ pub struct MegaTransactionOutcome { /// inspector took part in, and the block-execution path refuses to admit one into a block for /// exactly that reason. /// + /// The converse does not hold, and reading it that way is the mistake this field invites. What + /// is measured is gas movement: an inspector that only rewrites a frame result's + /// classification, edits the interpreter's stack or memory, or writes the journal directly + /// moves no gas and leaves this empty, while changing the state the transaction produces. + /// /// # What it is for /// /// Reporting, and that refusal. No resource limit is ever evaluated against it, and no diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 77da565c..abf21d85 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -21,6 +21,14 @@ /// from the transaction's envelope — and a negative value is gas it destroyed. Both directions are /// recorded, because the conservation law needs the net, not the gross. /// +/// # What it does not measure +/// +/// Gas movement, and only that. An inspector can change an execution in ways that move no gas at +/// all — rewriting a frame result's classification, editing the interpreter's stack or memory, +/// writing the journal directly — and every one of those leaves this all-zero. So an empty ledger +/// says the transaction's *gas numbers* are the EVM's own; it does not say the transaction is the +/// one the EVM would have produced alone. +/// /// # What consumes it /// /// - [`conjured_gas`](Self::conjured_gas) is the term the destroyed-remainder derivation adds to @@ -99,6 +107,8 @@ impl InspectorLedger { /// Whether the inspector left the transaction's gas accounting exactly as the EVM produced it. /// /// True for every observation-only inspector, and for every transaction that ran without one. + /// Not the converse of "an inspector changed something": see the type's own documentation for + /// the rewrites that move no gas and so leave this true. #[inline] pub const fn is_zero(&self) -> bool { self.gas == 0 && self.env == 0 && self.result == 0 && self.rejected_rewrites == 0 From 9782cba6f709d6867245da7cbe8b2a2182fe1ce2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 15:44:02 +0800 Subject: [PATCH 116/208] test(rex7): pin the two windows a rewrite lands in after its accounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are red. A counter edit at a terminating opcode's `step_end` is booked but reaches nobody — the action already holds its own copy of the gas — so the law derives 1,000 of destroyed gas that nothing booked. A precompile's executed/destroyed split is decided at its recording site, before any callback sees the synthetic result, so rewriting the classification afterwards leaves the split describing a call that did not happen: 999,985 unbooked one way, 1,000,000 over-booked the other, and a KZG failure priced as work drives the derivation to -100,000 once the caller reclaims the fee. The two neighbouring `step_end` windows pass already and stay: a mid-frame edit and the one after a CALL has set a `NewFrame` action both reach the envelope, so a coarser "the loop is ending" rule would break them. The REX7 harness now carries the inspector's own term, so the identity it checks on every transaction holds on the inspected path too. --- crates/mega-evm/tests/rex7/burn_split.rs | 21 +- crates/mega-evm/tests/rex7/common.rs | 140 ++++--- .../tests/rex7/create_code_deposit_charge.rs | 21 +- .../tests/rex7/guard_pass_static_gas.rs | 24 +- .../tests/rex7/inspector_settlement_window.rs | 393 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 5 + 6 files changed, 479 insertions(+), 125 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/inspector_settlement_window.rs diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 5216f59b..28411850 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -426,26 +426,11 @@ fn transact_create_reject( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let (detained_compute_gas_limit, terms) = { let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); - ( - additional_limit.detained_compute_gas_limit(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) }; - finish( - MegaSpecId::REX7, - outcome, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + finish(MegaSpecId::REX7, outcome, detained_compute_gas_limit, terms) } /// Runtime length the CREATE cases deploy — small enough that the per-byte code-deposit storage diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index d284552d..78b99096 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -2,13 +2,15 @@ use alloy_primitives::{address, Address, Bytes, B256, U256}; use mega_evm::{ - test_utils::MemoryDatabase, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaHaltReason, - MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, + test_utils::MemoryDatabase, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, + InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, }; use revm::{ context::{result::ExecutionResult, tx::TxEnvBuilder, TxEnv}, handler::EvmTr, state::EvmState, + Inspector, }; use std::collections::BTreeMap; @@ -57,6 +59,11 @@ pub(crate) struct Outcome { /// Post-tx sum of the per-site destroyed bookings — the second opinion the derived /// [`destroyed`](Self::destroyed) is cross-checked against, never the reported number. pub(crate) booked_destroyed: u64, + /// Post-tx net gas an inspector conjured — zero for every transaction that ran without one, + /// and for every observation-only inspector. + pub(crate) inspector_conjured_gas: i128, + /// What the measurement shim booked for this transaction, as the outcome reports it. + pub(crate) inspector_ledger: InspectorLedger, /// The state the transaction produced. pub(crate) state: EvmState, } @@ -127,26 +134,11 @@ pub(crate) fn transact_with_gas_limit( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let (detained_compute_gas_limit, terms) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); - ( - additional_limit.detained_compute_gas_limit(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) }; - finish( - spec, - outcome, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + finish(spec, outcome, detained_compute_gas_limit, terms) } /// Assembles an [`Outcome`] from what a transaction reported and checks the terminal identity @@ -159,9 +151,7 @@ pub(crate) fn finish( spec: MegaSpecId, outcome: MegaTransactionOutcome, detained_compute_gas_limit: u64, - non_compute_gas: i128, - minted_call_stipend: u64, - booked_destroyed: u64, + terms: ConservationTerms, ) -> Outcome { let gas_used = outcome.result_and_state.result.tx_gas_used(); let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); @@ -176,9 +166,11 @@ pub(crate) fn finish( enforced_lane: outcome.compute_gas_enforced, total_gas_spent, detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, + non_compute_gas: terms.non_compute_gas, + minted_call_stipend: terms.minted_call_stipend, + booked_destroyed: terms.booked_destroyed_compute_gas, + inspector_conjured_gas: terms.inspector_conjured_gas, + inspector_ledger: outcome.inspector_ledger, state: outcome.result_and_state.state, }; assert_terminal_identity(spec, &outcome); @@ -198,6 +190,7 @@ pub(crate) fn finish( /// D = destroyed the part that is reported and accounted but never enforced /// N = non_compute_gas MegaETH storage gas plus the sandbox boundary residue (signed) /// M = minted_call_stipend CALL_STIPEND minted into child frames and never debited from a caller +/// I = inspector_conjured_gas gas an inspector wrote into the execution that nothing debited /// S = total_gas_spent the receipt envelope, before the refund and the floor /// R = the receipt's raw refund /// F = the receipt's EIP-7623 floor gas @@ -207,13 +200,17 @@ pub(crate) fn finish( /// /// ```text /// (1) C = E + D -/// (2) C + N − M = S +/// (2) C + N − M − I = S /// (3) receipt gas_used = max(S − R, F) /// ``` /// /// (1) is the split of the reported total. (2) is the conservation law rearranged: settlement -/// defines `D = S + M − N − E`, so `S = D + E + N − M`, and substituting (1) gives `S = C + N − M`. -/// (3) is how a receipt's gas number is built from its envelope. +/// defines `D = S + M + I − N − E`, so `S = D + E + N − M − I`, and substituting (1) gives +/// `S = C + N − M − I`. (3) is how a receipt's gas number is built from its envelope. +/// +/// `I` is zero for every transaction that runs without an inspector and for every +/// observation-only one, so (2) is the plain two-term identity on all but the handful of runs +/// that attach a rewriting inspector — which is exactly where it earns its keep. /// /// # Why (2) needs no refund or floor correction /// @@ -249,22 +246,59 @@ fn assert_terminal_identity(spec: MegaSpecId, outcome: &Outcome) { outcome.result, ); let accounted = i128::from(outcome.compute_gas) + outcome.non_compute_gas - - i128::from(outcome.minted_call_stipend); + i128::from(outcome.minted_call_stipend) - + outcome.inspector_conjured_gas; assert_eq!( accounted, i128::from(outcome.total_gas_spent), "the tracker lanes must account for the whole receipt envelope; \ - compute={} non_compute={} minted_stipend={} accounted={accounted} envelope={} \ - (receipt gas_used={}) result={:?}", + compute={} non_compute={} minted_stipend={} conjured={} accounted={accounted} \ + envelope={} (receipt gas_used={}) result={:?}", outcome.compute_gas, outcome.non_compute_gas, outcome.minted_call_stipend, + outcome.inspector_conjured_gas, outcome.total_gas_spent, outcome.gas_used, outcome.result, ); } +/// [`transact`] with an inspector attached, borrowed so the caller can read it back afterwards. +/// +/// Runs the same fixture through the inspected frame loops. The identity every other helper here +/// checks holds on this path too, with the inspector's own term in it — which is the point: a +/// rewriting inspector must leave the transaction's numbers accountable, not merely plausible. +pub(crate) fn transact_inspected( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + inspector: &mut I, +) -> Outcome +where + I: for<'a> Inspector>, +{ + let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context).with_inspector(inspector); + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let (detained_compute_gas_limit, terms) = { + let additional_limit = evm.ctx_ref().additional_limit.borrow(); + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) + }; + finish(spec, outcome, detained_compute_gas_limit, terms) +} + /// Runs [`transact`] with the spec's default runtime limits. pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome { transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) @@ -313,26 +347,11 @@ pub(crate) fn transact_mega_tx( }); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let (detained_compute_gas_limit, terms) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); - ( - additional_limit.detained_compute_gas_limit(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) }; - finish( - spec, - outcome, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + finish(spec, outcome, detained_compute_gas_limit, terms) } /// The part of an account a transaction's state actually asserts. @@ -461,24 +480,9 @@ pub(crate) fn transact_with_bucket_capacity( tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let (detained_compute_gas_limit, terms) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); - ( - additional_limit.detained_compute_gas_limit(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) }; - finish( - spec, - outcome, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + finish(spec, outcome, detained_compute_gas_limit, terms) } diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs index 7bc24fd4..1a50b78c 100644 --- a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -466,26 +466,11 @@ fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, non_compute_gas, minted_call_stipend, booked_destroyed) = { + let (detained_compute_gas_limit, terms) = { let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); - ( - additional_limit.detained_compute_gas_limit(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) }; - finish( - MegaSpecId::REX7, - outcome, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) + finish(MegaSpecId::REX7, outcome, detained_compute_gas_limit, terms) } /// Installing a rate explicitly is not itself a deviation: a schedule that names revm's built-in diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index e24efbc4..6e84f513 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -100,34 +100,16 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); let executed = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let ( - detained_compute_gas_limit, - remaining_compute_gas, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ) = { + let (detained_compute_gas_limit, remaining_compute_gas, terms) = { let additional_limit = evm.ctx_ref().additional_limit.borrow(); - let terms = additional_limit.conservation_terms(); - let (non_compute_gas, minted_call_stipend, booked_destroyed) = - (terms.non_compute_gas, terms.minted_call_stipend, terms.booked_destroyed_compute_gas); ( additional_limit.detained_compute_gas_limit(), additional_limit.current_call_remaining_compute_gas(), - non_compute_gas, - minted_call_stipend, - booked_destroyed, + additional_limit.conservation_terms(), ) }; let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); - let outcome = finish( - MegaSpecId::REX7, - executed, - detained_compute_gas_limit, - non_compute_gas, - minted_call_stipend, - booked_destroyed, - ); + let outcome = finish(MegaSpecId::REX7, executed, detained_compute_gas_limit, terms); GuardPassRun { outcome, accessed, remaining_compute_gas } } diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs new file mode 100644 index 00000000..0597e8c4 --- /dev/null +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -0,0 +1,393 @@ +//! The two windows in which a rewrite lands after the accounting that should have read it. +//! +//! Both halves of the measurement shim rest on the same claim: what the shim books is what the +//! transaction's envelope actually moved by. There are two places where the number the shim reads +//! and the number the envelope carries are not the same object, and each of them is a fixture +//! here. +//! +//! - **A terminating opcode's `step_end`.** revm's inspected loop runs `step_end` *after* the +//! instruction that produced the frame's action, and that action carries its own copy of the gas +//! counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail +//! settlement measures work against and nothing the caller will ever see, so it must move the +//! settlement baseline and must not move the ledger. The two neighbouring windows — a step_end in +//! mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of that +//! rule: the frame resumes on the edited counter in both, so both are booked. +//! +//! - **A precompile's classification.** A precompile is answered inside the frame init and never +//! becomes a child frame, so its recording site is the only place that knows the forwarded +//! envelope and the work performed. It is not, however, the place that knows how the call ends: +//! `call_end` runs afterwards and can rewrite the classification, and the classification is what +//! decides whether the caller reclaims the remainder. So the split has to be settled at the +//! frame's settlement point, from what the recording site staged, exactly as an ordinary frame's +//! is. +//! +//! Every case here is checked by the identity `common::finish` runs on every transaction: the +//! tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. + +use crate::common::{ + transact, transact_inspected, Outcome, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + kzg_point_evaluation, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, InspectorLedger, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CALL, POP, STOP}, + context::ContextTr, + interpreter::{ + interpreter_types::LoopControl, CallInputs, CallOutcome, InstructionResult, Interpreter, + InterpreterAction, InterpreterTypes, + }, + Inspector, +}; +use sha2::{Digest, Sha256}; + +/// Gas the edit-once inspector writes into a live interpreter's counter. +const INJECT: u64 = 1_000; + +/// Gas every probed CALL forwards. Well inside the 63/64 rule at the default transaction gas +/// limit and well inside the default compute budget, so the forwarded envelope is exactly this. +const FORWARDED: u64 = 1_000_000; + +/// The identity precompile. +const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); +/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. +const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); +/// KZG point evaluation. +const KZG: Address = address!("000000000000000000000000000000000000000a"); + +/// What the identity precompile charges for an empty input: its base cost, with no words to copy. +const IDENTITY_GAS: u64 = 15; + +fn limits() -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) +} + +fn db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +// --- A: the window a terminating opcode's `step_end` sits in --------------------------------- + +/// Which of the three `step_end` windows an edit is aimed at, told apart by the action the +/// instruction that just ran left behind. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Window { + /// No action yet: the frame carries on, and the edited counter is what it carries on with. + MidFrame, + /// A `NewFrame` action: the frame suspends into a child and then resumes on this counter. + Suspending, + /// A `Return` action: the frame is over, and the gas it hands back was copied into the action + /// before this callback ran. + Terminating, +} + +impl Window { + fn of(interp: &mut Interpreter) -> Self { + match interp.bytecode.action() { + None => Self::MidFrame, + Some(InterpreterAction::NewFrame(_)) => Self::Suspending, + Some(InterpreterAction::Return(_)) => Self::Terminating, + } + } +} + +/// Writes [`INJECT`] into the interpreter's counter once, at the first `step_end` that sits in +/// `window`. +#[derive(Debug)] +struct CounterEditor { + window: Window, + fired: u32, +} + +impl CounterEditor { + fn new(window: Window) -> Self { + Self { window, fired: 0 } + } +} + +impl Inspector for CounterEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + self.fired += 1; + interp.gas.erase_cost(INJECT); + } +} + +/// `PUSH1 1; POP; STOP` — three opcodes, so a mid-frame `step_end` and a terminating one are both +/// reached, and nothing else happens in between. +fn straight_line_code() -> Bytes { + BytecodeBuilder::default().push_number(1u64).append(POP).append(STOP).build() +} + +/// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame +/// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. +fn suspending_code() -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(IDENTITY) + .push_number(FORWARDED) + .append(CALL) + .append(POP) + .append(STOP) + .build() +} + +fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { + let plain = transact(MegaSpecId::REX7, db(code.clone()), limits()); + let mut inspector = CounterEditor::new(window); + let edited = transact_inspected(MegaSpecId::REX7, db(code), limits(), &mut inspector); + (plain, edited, inspector.fired) +} + +/// An edit made in the terminating window reaches nobody, so nothing is booked for it — and the +/// transaction is the one the EVM would have produced alone. +/// +/// The action the terminating instruction set already holds its own copy of the counter, so the +/// caller is handed a number this edit never touched. Booking it would tell the conservation law +/// that the transaction spent [`INJECT`] less than it did. +/// +/// `compute_gas` being unmoved is the other half of the rule, and the one that would break if the +/// fix were written as "leave the counter alone" rather than "book nothing for it": the tail +/// settlement measures work as a drop in this very counter, so without the baseline shift the +/// injection would read as [`INJECT`] gas of work the frame never performed. +#[test] +fn test_an_edit_in_the_terminating_window_is_not_booked() { + let (plain, edited, fired) = run_counter_edit(straight_line_code(), Window::Terminating); + + assert_eq!(fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger::default(), + "an edit that cannot reach the envelope must leave the ledger untouched", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the settlement baseline must absorb the edit, so it counts as no work at all", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope must be the one the uninspected run produced", + ); +} + +/// The near boundary: a mid-frame edit is booked, because the frame carries on spending the +/// counter the callback left behind. +#[test] +fn test_an_edit_in_mid_frame_is_still_booked() { + let (_, edited, fired) = run_counter_edit(straight_line_code(), Window::MidFrame); + + assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + i128::from(INJECT), + "gas written into a counter the frame will keep spending is conjured gas", + ); +} + +/// The far boundary, and the one a coarser rule would get wrong: a `CALL` has set an action too, +/// but it is a `NewFrame` action — the frame suspends, the child runs, and then the frame resumes +/// on exactly this counter. So the edit reaches the envelope and must be booked, even though the +/// interpreter is "at the end of its loop" in precisely the same sense as the terminating case. +#[test] +fn test_an_edit_in_the_suspending_window_is_still_booked() { + let (_, edited, fired) = run_counter_edit(suspending_code(), Window::Suspending); + + assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + i128::from(INJECT), + "a suspended frame resumes on the edited counter, so the edit reaches the envelope", + ); +} + +// --- B: a precompile's classification, rewritten after its recording site --------------------- + +/// Rewrites the result of the call to `target` into `to`, once. +#[derive(Debug)] +struct Reclassifier { + target: Address, + to: InstructionResult, + fired: u32, +} + +impl Reclassifier { + fn new(target: Address, to: InstructionResult) -> Self { + Self { target, to, fired: 0 } + } +} + +impl Inspector for Reclassifier { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != self.target { + return; + } + self.fired += 1; + outcome.result.result = self.to; + } +} + +/// A `CALL` forwarding [`FORWARDED`] gas to `target` with `calldata` at `mem[0..]`, its success +/// flag popped so the caller survives either classification. +fn call_precompile(target: Address, calldata: &[u8]) -> Bytes { + BytecodeBuilder::default() + .mstore(0, calldata) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(calldata.len() as u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(FORWARDED) + .append(CALL) + .append(POP) + .append(STOP) + .build() +} + +/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped: 192 bytes +/// with a matching versioned hash, so KZG clears the length doorway and fails inside verification +/// — the one halt shape `MegaETH` prices as work performed. +fn kzg_verification_failure() -> Vec { + let commitment = hex::decode( + "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ + 5f26936857bc3a7c2539ea8ec3a952b7", + ) + .unwrap(); + let mut versioned_hash = Sha256::digest(&commitment).to_vec(); + versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG + let z = + hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); + let y = + hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); + let proof = hex::decode( + "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ + 4faf0070725e00b60ad9a026a15b1a8c", + ) + .unwrap(); + + let mut input = Vec::new(); + input.extend_from_slice(&versioned_hash); + input.extend_from_slice(&z); + input.extend_from_slice(&y); + input.extend_from_slice(&commitment); + input.extend_from_slice(&proof); + assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); + let last = input.len() - 1; + input[last] ^= 0x01; + input +} + +fn run_reclassified( + target: Address, + calldata: &[u8], + to: InstructionResult, +) -> (Outcome, Outcome, u32) { + let code = call_precompile(target, calldata); + let plain = transact(MegaSpecId::REX7, db(code.clone()), limits()); + let mut inspector = Reclassifier::new(target, to); + let rewritten = transact_inspected(MegaSpecId::REX7, db(code), limits(), &mut inspector); + (plain, rewritten, inspector.fired) +} + +/// A successful precompile rewritten into a halt destroys the rest of its forwarded envelope, and +/// the transaction has to report it. +/// +/// The caller reclaims nothing from a halted call, so everything the identity precompile did not +/// charge for is gone. Its recording site booked a destroyed remainder of zero, because at that +/// moment the call had succeeded. +#[test] +fn test_a_precompile_rewritten_into_a_halt_destroys_its_remainder() { + let (plain, rewritten, fired) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); + + assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); + assert_eq!( + rewritten.destroyed, + FORWARDED - IDENTITY_GAS, + "everything the precompile did not spend is destroyed once the call halts", + ); + assert_eq!( + rewritten.enforced(), + plain.enforced(), + "the same work ran either way, so the enforcing lane must not move", + ); + assert_eq!( + rewritten.compute_gas, + plain.compute_gas + rewritten.destroyed, + "the destroyed remainder is reported on top of the work performed", + ); +} + +/// A halted precompile rewritten into a success destroys nothing, because the caller reclaims the +/// envelope its recording site had already written off. +#[test] +fn test_a_precompile_rewritten_into_a_success_destroys_nothing() { + let (plain, rewritten, fired) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); + + assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!( + plain.destroyed, FORWARDED, + "blake2f rejects the input before any work, so the uninspected run destroys all of it", + ); + assert_eq!(rewritten.destroyed, 0, "a reclaimed envelope is not a destroyed one"); + assert_eq!( + rewritten.compute_gas, + rewritten.enforced(), + "with nothing destroyed the reported total is the work performed", + ); +} + +/// The corner where the two halves of the split move in opposite directions: a KZG failure that +/// `MegaETH` prices as work, rewritten into a success. +/// +/// The fixed fee really was performed and stays on the enforcing lane. But the halt's gas object +/// carries the whole forwarded envelope as remaining — a halting precompile's gas is reset rather +/// than spent down — so a caller told the call succeeded reclaims all of it, including the fee. +/// That fee is then gas the execution priced and the envelope never paid: conjured gas, which the +/// ledger has to carry or the law reads the transaction as having spent less than it did. +#[test] +fn test_a_priced_precompile_failure_rewritten_into_a_success_conjures_its_fee() { + let calldata = kzg_verification_failure(); + let (plain, rewritten, fired) = run_reclassified(KZG, &calldata, InstructionResult::Stop); + + assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!( + plain.destroyed, + FORWARDED - kzg_point_evaluation::GAS_COST, + "verification ran, so the uninspected run destroys the envelope less the fixed fee", + ); + assert_eq!(rewritten.destroyed, 0, "a reclaimed envelope is not a destroyed one"); + assert_eq!( + rewritten.enforced(), + plain.enforced(), + "the verification work is the same on both runs", + ); + assert_eq!( + rewritten.inspector_conjured_gas, + i128::from(kzg_point_evaluation::GAS_COST), + "the fee the caller reclaimed is gas the transaction was never charged for", + ); +} + +/// The transaction gas limit is not what binds any of these fixtures — stated once, so a future +/// change to the shared limit cannot silently turn a destroyed-remainder case into an +/// out-of-gas one. +#[test] +fn test_the_fixtures_are_not_bound_by_the_transaction_gas_limit() { + assert!( + DEFAULT_TX_GAS_LIMIT > 10 * FORWARDED, + "the forwarded envelope must be a small part of the transaction's own", + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 3949ffce..8cf5d440 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -14,6 +14,10 @@ //! - `create_code_deposit_charge` — a CREATE's canonical code-deposit compute gas is weighed before //! it is recorded, so a creation that fails at its frame exit is charged nothing for a deposit //! the EVM never makes. +//! - `inspector_settlement_window` — the two windows where a rewrite lands after the accounting +//! that should have read it: a terminating opcode's `step_end`, whose counter edit reaches +//! nobody, and a precompile's classification, whose split has to follow the callback rather than +//! the recording site. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. //! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that @@ -87,6 +91,7 @@ mod gas_clamp; mod gas_leakage; mod guard_pass_static_gas; mod inspector_cheat_matrix; +mod inspector_settlement_window; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; From b632ff4391a64d66c0c2466b4c4b40aa9bbdf7b3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 15:45:38 +0800 Subject: [PATCH 117/208] fix(evm): stop booking a counter edit that cannot reach the envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit revm's inspected loop runs a terminating instruction first and `step_end` after it, and that instruction has already copied the gas counter into the action it set. The action is what becomes the frame's result and what the caller reclaims from, so a counter edit made afterwards is written into an object nobody reads again — booking it told the conservation law the transaction spent that much less than it did. The predicate is "is the interpreter holding a `Return` action", not "is the loop about to break": revm breaks out of the instruction loop for a suspending `CALL` too, and that frame resumes on exactly this counter. It is read after the callback returns, because the question is what the EVM will do with the counter the callback left behind. Only the ledger is gated on it. The settlement baseline still shifts, because MegaETH's own tail settlement measures work as a drop in this counter and does read it after the action is set — without the shift the injection would read as work the frame performed, which is the opposite error. --- crates/mega-evm/src/evm/inspector.rs | 88 ++++++++++++++++++++++------ crates/mega-evm/src/limit/limit.rs | 17 +++++- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 302b61cb..7c8a6325 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -41,8 +41,9 @@ use revm::{ context::{ContextError, ContextTr}, handler::FrameResult, interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, InstructionResult, - Interpreter, InterpreterResult, InterpreterTypes, + interpreter_types::LoopControl, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + FrameInput, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, + InterpreterTypes, }, Inspector, }; @@ -90,6 +91,46 @@ impl MeasuredInspector { } } +/// Whether an edit to this interpreter's gas counter can still reach the transaction's envelope. +/// +/// It cannot exactly when the interpreter is holding a `Return` action. revm's inspected loop runs +/// the terminating instruction first and the callback after it, and that instruction has already +/// copied the counter into the action it set — the action is what becomes the frame's result and +/// what the caller reclaims from. Whatever the callback writes into the counter afterwards is +/// written into an object nobody will read again. +/// +/// The two neighbouring shapes are live and must stay booked. With no action pending the frame +/// carries straight on; with a `NewFrame` action pending it suspends into a child and then resumes +/// on this very counter. "The loop is about to break" is therefore not the question — revm breaks +/// out of the instruction loop in both the suspending and the terminating case — and a rule phrased +/// that way would stop booking edits that really do move a frame's budget. +/// +/// Read *after* the callback returns, because the question is about the counter the callback left +/// behind: an inspector that sets or clears an action has changed what the EVM does next, and the +/// answer has to follow it. +/// +/// # Why this decides booking and not measurement +/// +/// Only the ledger is gated on it. `MegaETH`'s own tail settlement measures a frame's work as a +/// drop in this same counter and does read it after the action is set, so the settlement baseline +/// has to shift for a dead-window edit exactly as it does for a live one — otherwise gas the +/// inspector wrote in would read as work the frame performed, which is the opposite error. +/// +/// # What it does not cover +/// +/// The pending action itself. A callback holding a live interpreter can reach the action through +/// `LoopControl` and edit the gas inside it, which does move the envelope. Measuring that at the +/// callback boundary would be unsound rather than merely incomplete: whether such an edit moves +/// anything depends on the frame's *final* classification, which no callback here knows — a +/// returning frame hands its remainder back and a halting one does not — so it belongs to the +/// frame's settlement point, which books edits made at the one callback it can see +/// ([`InspectorLedger::result`](crate::InspectorLedger::result)). Extending that lane to cover +/// this one is a per-frame snapshot this shim deliberately does not carry. +#[inline] +fn counter_reaches_envelope(interp: &mut Interpreter) -> bool { + !matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) +} + /// The gas limit a frame input carries, for the two variants that have one. #[inline] fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { @@ -187,30 +228,39 @@ where ) { let before = interp.gas.remaining(); self.inner.initialize_interp(interp, context); - context - .additional_limit - .borrow_mut() - .record_inspector_gas_adjustment::(&mut interp.gas, before); + let reaches_envelope = counter_reaches_envelope(interp); + context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( + &mut interp.gas, + before, + reaches_envelope, + ); } #[inline] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { let before = interp.gas.remaining(); self.inner.step(interp, context); - context - .additional_limit - .borrow_mut() - .record_inspector_gas_adjustment::(&mut interp.gas, before); + let reaches_envelope = counter_reaches_envelope(interp); + context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( + &mut interp.gas, + before, + reaches_envelope, + ); } + /// The callback that sits in the dead window: revm runs it after the instruction that set the + /// frame's action, so on a terminating opcode the counter it hands out has already been copied + /// into the result the caller will be given — see [`counter_reaches_envelope`]. #[inline] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { let before = interp.gas.remaining(); self.inner.step_end(interp, context); - context - .additional_limit - .borrow_mut() - .record_inspector_gas_adjustment::(&mut interp.gas, before); + let reaches_envelope = counter_reaches_envelope(interp); + context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( + &mut interp.gas, + before, + reaches_envelope, + ); } /// No interpreter and no frame inputs are reachable here, so there is nothing to measure — @@ -229,10 +279,12 @@ where ) { let before = interpreter.gas.remaining(); self.inner.log_full(interpreter, context, log); - context - .additional_limit - .borrow_mut() - .record_inspector_gas_adjustment::(&mut interpreter.gas, before); + let reaches_envelope = counter_reaches_envelope(interpreter); + context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( + &mut interpreter.gas, + before, + reaches_envelope, + ); } #[inline] diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 8c9a9a13..dd02b997 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -459,10 +459,18 @@ impl AdditionalLimit { /// `gas.remaining()` is what the callback left behind; the difference is the adjustment, /// because the EVM does not execute inside a callback. /// + /// `reaches_envelope` is whether the counter the callback left behind is one the EVM will read + /// again — false exactly when the interpreter is already holding a terminating action, whose + /// own copy of the counter is what the caller reclaims from. It gates the ledger and + /// nothing else: an edit nobody will read moves no gas and must not be booked, but + /// `MegaETH`'s own tail settlement does read this counter after the action is set, so the + /// baseline still has to shift or the edit would be measured as work the frame performed. + /// /// Three things happen, in this order: /// - /// 1. **The ledger** takes the adjustment, so the conservation law can account for gas nobody - /// funded (or gas that vanished) when it derives the destroyed remainder. + /// 1. **The ledger** takes the adjustment, if it can reach the envelope at all, so the + /// conservation law can account for gas nobody funded (or gas that vanished) when it derives + /// the destroyed remainder. /// 2. **The open segment is settled against the pre-callback counter** (REX7+, /// `IN_OPEN_SEGMENT`). This is what keeps the adjustment out of enforcement: compute gas is /// measured as a drop in the interpreter's counter, so an injection made mid-segment would @@ -492,12 +500,15 @@ impl AdditionalLimit { &mut self, gas: &mut Gas, remaining_before: u64, + reaches_envelope: bool, ) { let remaining_after = gas.remaining(); if remaining_after == remaining_before { return; } - self.inspector.gas += i128::from(remaining_after) - i128::from(remaining_before); + if reaches_envelope { + self.inspector.gas += i128::from(remaining_after) - i128::from(remaining_before); + } if !IN_OPEN_SEGMENT || !self.rex7_enabled() { return; From 4fd8f6389024d7de63a5310e9bebffb629201a00 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 15:53:28 +0800 Subject: [PATCH 118/208] fix(limit): settle a precompile's envelope against its final classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A precompile is answered inside a frame init and never becomes a child frame, so its executed/destroyed split was taken at its own recording site — before any callback saw the synthetic result it produced. The classification is what decides whether the caller reclaims the remainder, and `call_end` runs afterwards and can rewrite it, so the split described a call that did not happen: a success rewritten into a halt destroyed a remainder nobody booked, and a halt rewritten into a success booked a whole envelope the caller got back. The recording site now stages the two numbers only it knows — the uncapped forwarded envelope, and MegaETH's price for the work performed, which a halting precompile's `Gas` does not carry — and `finalize_frame` takes the difference against the classification the caller will see, the same point and the same question as every other frame outcome. For an uninspected transaction the classification never moves, so every existing split is unchanged; the `run`-level probes now drive the settlement so they still pin the split rather than where it is computed. One direction needs the ledger. A halting precompile's gas is reset rather than spent down, so a caller told a KZG verification failure succeeded reclaims the fixed fee too. The work stays enforcing because it was performed, and the fee nobody paid for is booked as conjured gas. --- AGENTS.md | 5 +- crates/mega-evm/src/evm/precompiles.rs | 60 ++++-- crates/mega-evm/src/limit/limit.rs | 182 +++++++++++++++--- .../tests/rex7/frame_init_reject_burn.rs | 3 +- crates/mega-evm/tests/rex7/main.rs | 11 +- crates/mega-evm/tests/rex7/precompile_halt.rs | 10 +- docs/spec/evm/compute-gas.md | 6 +- docs/spec/upgrades/rex7.md | 5 +- 8 files changed, 221 insertions(+), 61 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 561950b7..ee768cfb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,8 +120,9 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi REX7 settles compute gas at checkpoints (storage-gas opcodes, CALL/CREATE family, volatile opcodes, `GAS`, frame entry/resume/exit) rather than after every plain opcode, and enforces limits inside plain segments with a gas clamp. A REX7 frame that ends in an exceptional halt splits its remaining budget: the work it performed before failing settles through the ordinary enforcing path, while the remainder it destroyed goes into a lane of `ComputeGasTracker` that the reported total and block accounting include but no limit comparison sees — destroyed gas is not work performed, and enforcing it would turn an EVM halt into a resource-limit failure with the gas rescued. The destroyed half is read from the frame's final result at `AdditionalLimit::finalize_frame`, so revm's create-return rejects and any rewrite an inspector's last callback made are both covered; storage gas a checkpoint body charged before aborting belongs to neither half. - A precompile that fails never becomes a child EVM frame, so the same split is taken at the precompile recording site: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. - A frame init that refuses to build a frame at all is settled at the same point, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, a returning or reverting one books nothing because the caller gets the budget back, and a precompile result is excluded there because its own recording site already booked both halves. + A precompile that fails never becomes a child EVM frame, so the same split is taken at `AdditionalLimit::finalize_frame` from the classification the call returns to its caller: executed work (the KZG fixed fee when verification ran; zero when the input was rejected before any work) enforces, and the unused caller-supplied envelope — including any REX5 forwarded-gas cap gap — is destroyed. + The recording site stages the two numbers only it knows (the uncapped forwarded envelope, and `MegaETH`'s price for the work performed, which a halting precompile's `Gas` does not carry) and the settlement point takes the difference, so a classification an inspector rewrote after the dispatch is the one the split follows. + A frame init that refuses to build a frame at all is settled at the same point, driven by the same classification: a halting refusal (a CREATE onto an occupied address) has its whole child budget destroyed, and a returning or reverting one books nothing because the caller gets the budget back. The split crosses the transaction boundary: `MegaTransactionOutcome` carries the destroyed part and the enforced part alongside the reported total, and `BlockLimiter` keeps `block_compute_gas_used` (reported) separate from `block_compute_gas_enforced` (the counter block admission compares). The destroyed part a transaction _reports_ is not the sum of those per-site bookings: `MegaHandler::last_frame_result` derives it once, from a conservation law over the envelope — `spent = C + S + D − K − I`, stated once as `ConservationTerms` (`limit/conservation.rs`) and read from `AdditionalLimit::conservation_terms()` by every site that derives, re-settles or checks it — and the per-site bookings stay as the enforcement split and as the `debug_assert` cross-check that the two agree. The derived number is reported and nothing else: the block's enforced counter accumulates `MegaTransactionOutcome::compute_gas_enforced`, read from `AdditionalLimit::enforced_compute_gas` (the per-site lane), rather than subtracting the reported destroyed total, so a missing term in the law misreports a statistic instead of repacking blocks. diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index 7ede0220..bcfa6d0b 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -403,10 +403,12 @@ impl PrecompileProvider PrecompileProvider PrecompileProvider PrecompileProvider PrecompileProvider, output: InterpreterResult) { + let mut outcome = CallOutcome::new(output, 0..0); + outcome.was_precompile_called = true; + limit.borrow_mut().finalize_frame(&mut FrameResult::Call(outcome), FrameExit::Refused, 0); + } + /// Wraps precompile call data into the [`CallInputs`] shape `PrecompilesMap::run` takes, /// carrying the bytecode address, static flag and gas limit that used to be separate /// arguments. @@ -1157,6 +1177,7 @@ mod tests { output.result ); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert_eq!( additional.get_usage().compute_gas, @@ -1198,6 +1219,7 @@ mod tests { output.result ); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert_eq!( additional.get_usage().compute_gas, @@ -1237,6 +1259,7 @@ mod tests { // Halt Gas stays on the capped effective limit. assert_eq!(output.gas.limit(), GAS_COST + 1_000); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert_eq!( additional.get_usage().compute_gas - additional.burned_compute_gas(), @@ -1285,6 +1308,7 @@ mod tests { "{spec:?}: verification must fail past the gas gate; got {:?}", output.result, ); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert_eq!( additional.get_usage().compute_gas - additional.burned_compute_gas(), @@ -1317,6 +1341,7 @@ mod tests { output.gas.limit() < GAS_COST, "{spec:?}: the capped effective limit is what excludes the fixed-cost arm", ); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert!( !additional.limit_exceeded(), @@ -1347,6 +1372,7 @@ mod tests { output.result ); + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); assert_eq!( additional.get_usage().compute_gas, @@ -1383,9 +1409,11 @@ mod tests { precompiles_map.run(&mut context, &call_inputs(inputs, address, true, forwarded_gas)); let output = result.expect("run ok").expect("Some output"); assert!(!output.result.is_ok_or_revert(), "the probe must fail; got {:?}", output.result,); + let result = output.result; + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); - (output.result, additional.get_usage().compute_gas, additional.burned_compute_gas()) + (result, additional.get_usage().compute_gas, additional.burned_compute_gas()) } /// The premise the REX7 split rests on: upstream checks the input length before it reads @@ -1565,9 +1593,11 @@ mod tests { .expect("run ok") .expect("Some output"); assert!(!output.result.is_ok_or_revert(), "the probe must halt; got {:?}", output.result,); + let result = output.result; + settle_frame_init(&context.additional_limit, output); let additional = context.additional_limit.borrow(); - (output.result, additional.get_usage().compute_gas, additional.burned_compute_gas()) + (result, additional.get_usage().compute_gas, additional.burned_compute_gas()) } /// REX7: a Custom override of the KZG address is not the wired KZG implementation, so diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index dd02b997..d77223a5 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -46,6 +46,24 @@ pub enum FrameExit { RefusedSynthetically, } +/// What a precompile's recording site knows about its call, held until the frame's settlement +/// point can decide the split (REX7+). +/// +/// A precompile is answered inside the frame init and never becomes a child frame, so its +/// recording site is the only place that knows both of these numbers: the envelope is the +/// caller-supplied forwarded amount rather than the REX5-capped effective limit, and the work is +/// `MegaETH`'s own price for what the call performed, which a halting precompile's gas object does +/// not carry. What that site cannot know is how the call ends — an inspector's `call_end` runs +/// afterwards and can rewrite the classification, and the classification is what decides whether +/// the caller reclaims the remainder. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct PrecompileEnvelope { + /// The gas the caller forwarded, uncapped. + forwarded: u64, + /// The work the precompile performed, already recorded on the enforcing lane. + executed: u64, +} + /// Additional limits for the `MegaETH` EVM beyond standard EVM limits. /// /// This struct coordinates four independent resource limits: compute gas, data size, @@ -140,6 +158,14 @@ pub struct AdditionalLimit { /// conservation law and by reporting. It stays at its default for every transaction that runs /// without an inspector and for every observation-only inspector. inspector: inspector_ledger::InspectorLedger, + + /// The precompile call whose split is waiting for its frame's settlement point (REX7+). + /// + /// At most one can ever be outstanding: a precompile is answered inside a frame init and the + /// same frame init settles the result a few statements later, with no room for another frame + /// to start in between. [`finalize_frame`](Self::finalize_frame) takes it unconditionally, so + /// it cannot outlive the frame that staged it. + staged_precompile: Option, } /// The usage of the additional limits. @@ -169,6 +195,7 @@ impl AdditionalLimit { storage_call_stipend: storage_call_stipend::StorageCallStipendTracker::new(spec), checkpoint: checkpoint::CheckpointTracker::new(spec), inspector: inspector_ledger::InspectorLedger::default(), + staged_precompile: None, } } } @@ -212,6 +239,7 @@ impl AdditionalLimit { self.storage_call_stipend.reset(); self.checkpoint.reset(); self.inspector = inspector_ledger::InspectorLedger::default(); + self.staged_precompile = None; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -726,15 +754,31 @@ impl AdditionalLimit { /// /// Raises the reported total and leaves every limit comparison unchanged — the same /// [`ComputeGasTracker::record_burned_gas`](compute_gas::ComputeGasTracker::record_burned_gas) - /// the interpreter-frame halt path uses. Precompile halt accounting calls this at the - /// recording site rather than through frame-exit settlement: a halt `Gas` is reset to - /// `Gas::new(limit)`, so the frame formula `remaining()` would double-count or miss the - /// forwarded-cap gap. + /// the interpreter-frame halt path uses. #[inline] pub(crate) fn record_burned_gas(&mut self, amount: u64) { self.compute_gas.record_burned_gas(amount); } + /// Hands the precompile recording site's two numbers to the frame's settlement point (REX7+; + /// a no-op before, where nothing is destroyed). + /// + /// `executed` must already have been recorded on the enforcing lane — it is the work the call + /// performed, which does not depend on how the call is classified afterwards. Only the + /// destroyed half does, and that is what the settlement point derives from these two numbers + /// and the final classification. See [`PrecompileEnvelope`]. + #[inline] + pub(crate) fn stage_precompile_envelope(&mut self, forwarded: u64, executed: u64) { + if !self.rex7_enabled() { + return; + } + debug_assert!( + self.staged_precompile.is_none(), + "a precompile's envelope outlived the frame init that staged it", + ); + self.staged_precompile = Some(PrecompileEnvelope { forwarded, executed }); + } + /// Gets the usage of the additional limits. #[inline] pub fn get_usage(&self) -> LimitUsage { @@ -1371,23 +1415,34 @@ impl AdditionalLimit { // it to a revert. self.absorb_frame_local_exceed(result); - let evm_remaining = self.settle_inspector_result_gas(result, inspector_gas_delta); + // Taken unconditionally, so a staged envelope can never outlive the frame that staged it. + let staged_precompile = self.staged_precompile.take(); + // The gas the EVM itself left in this result. Every settlement below is defined against + // it: the last callback's edit to the number is the inspector's, and the two are only the + // same object on a frame no callback touched. + let evm_remaining = evm_own_remaining(result.gas().remaining(), inspector_gas_delta); + let rescuable = + self.settle_inspector_result_gas(result, inspector_gas_delta, evm_remaining); match exit { FrameExit::Ran => { + debug_assert!( + staged_precompile.is_none(), + "a precompile never becomes a frame, so it cannot reach a Ran settlement", + ); // The burn before the rescue: the rescue's `check_limit` is what latches a // TX-level exceed, and a latched exceed is exactly the case whose remainder is // handed back rather than destroyed. self.settle_exceptional_halt_burn(result, evm_remaining); - self.try_rescue_gas(result.gas(), evm_remaining); + self.try_rescue_gas(result.gas(), rescuable); } FrameExit::Refused | FrameExit::RefusedSynthetically => { // The rescue before the burn, for the mirror-image reason: here the latch the // rescue produces is what tells the burn the envelope is being handed back. if exit == FrameExit::Refused || self.rex7_enabled() { - self.try_rescue_gas(result.gas(), evm_remaining); + self.try_rescue_gas(result.gas(), rescuable); } - self.settle_frame_init_reject_burn(result, evm_remaining); + self.settle_frame_init_reject_burn(result, evm_remaining, staged_precompile); } } } @@ -1402,19 +1457,28 @@ impl AdditionalLimit { /// that number really does change what the transaction spends. It goes to the ledger, and the /// conservation law reads it back out of the envelope; /// - a halting frame hands nothing back, so the edit changes nothing the transaction spends. - /// The destroyed remainder and the rescue below are then taken on the EVM's own number, - /// reconstructed by undoing the edit — an inspector does not perform work, and gas it removed - /// from a doomed result was never the inspector's to destroy. - fn settle_inspector_result_gas(&mut self, result: &FrameResult, delta: i128) -> u64 { - let remaining = result.gas().remaining(); + /// The rescue is then taken on `evm_remaining`, the EVM's own number — an inspector does not + /// perform work, and gas it removed from a doomed result was never the inspector's to + /// destroy. + /// + /// Returns the number the resource-limit rescue may hand back to the sender, which is the + /// result as it now stands on the first case and the EVM's own on the second. The destroyed + /// settlements always take `evm_remaining`, because a booked edit is already accounted for on + /// the ledger and booking it a second time as a destroyed remainder would double it. + fn settle_inspector_result_gas( + &mut self, + result: &FrameResult, + delta: i128, + evm_remaining: u64, + ) -> u64 { if delta == 0 { - return remaining; + return evm_remaining; } if result.instruction_result().is_ok_or_revert() { self.inspector.result += delta; - remaining + result.gas().remaining() } else { - (i128::from(remaining) - delta).clamp(0, i128::from(u64::MAX)) as u64 + evm_remaining } } @@ -1770,10 +1834,12 @@ impl AdditionalLimit { /// site — an empty-code call, a nonce overflow, a depth or balance rejection — destroy nothing /// precisely because their gas is erased back into the caller. /// - /// A precompile result is excluded. Precompiles are dispatched inside the same frame init and - /// come back as a result rather than a frame, but they have already booked both halves of - /// their own split at the recording site, against the forwarded envelope rather than the - /// capped budget this result carries. Booking again here would report the same gas twice. + /// A precompile result takes [`settle_precompile_envelope`](Self::settle_precompile_envelope) + /// instead. It reaches this point the same way and is settled against the same classification, + /// but neither of the two numbers the formula above uses is the right one for it: the envelope + /// it destroys is the caller's forwarded amount rather than the REX5-capped budget its result + /// carries, and a precompile that failed after doing work has that work priced by `MegaETH` + /// rather than spent down in its gas object. /// /// Nothing is booked once a limit is latched, which is also why this runs after the rescue in /// [`after_frame_init`](Self::after_frame_init) rather than before it. A TX-level exceed @@ -1782,21 +1848,68 @@ impl AdditionalLimit { /// [`before_frame_return_result`](Self::before_frame_return_result), which rewrites the result /// to a revert and so returns the gas to the caller. Either way the envelope is not destroyed, /// and booking it would report gas that was handed back. - fn settle_frame_init_reject_burn(&mut self, result: &FrameResult, evm_remaining: u64) { - if !self.checkpoint.rex7_enabled() || - self.limit_exceeded() || - result.instruction_result().is_ok_or_revert() - { + fn settle_frame_init_reject_burn( + &mut self, + result: &FrameResult, + evm_remaining: u64, + staged_precompile: Option, + ) { + debug_assert_eq!( + staged_precompile.is_some(), + self.checkpoint.rex7_enabled() && + matches!(result, FrameResult::Call(outcome) if outcome.was_precompile_called), + "every REX7 precompile call stages an envelope, and nothing else does", + ); + if !self.checkpoint.rex7_enabled() || self.limit_exceeded() { return; } - if let FrameResult::Call(outcome) = result { - if outcome.was_precompile_called { - return; - } + if let Some(staged) = staged_precompile { + self.settle_precompile_envelope(staged, result, evm_remaining); + return; + } + if result.instruction_result().is_ok_or_revert() { + return; } self.compute_gas.record_burned_gas(evm_remaining); } + /// Settles what a precompile call destroyed, against the classification its caller will + /// actually see (REX7+). + /// + /// The recording site staged the two numbers only it knows — the forwarded envelope and the + /// work performed — and the classification decides the rest, exactly as it does for an + /// ordinary frame: a success or a revert hands the remainder back to the caller, an + /// exceptional halt does not. So the envelope this call consumed is + /// + /// ```text + /// consumed = forwarded − (returned to the caller) + /// ``` + /// + /// and everything in it that was not the work performed is destroyed. + /// + /// # Why the difference can go the other way + /// + /// A halting precompile's `Gas` is reset rather than spent down, so it reports the whole + /// budget as remaining even when `MegaETH` priced the call as having done work — the KZG fixed + /// fee for a failure raised inside verification is the one case that exists today. Told that + /// such a call succeeded, the caller reclaims all of it, fee included. The work stays on the + /// enforcing lane, because it really was performed; the fee nobody paid for is gas the rewrite + /// conjured, and goes to the ledger so the conservation law still closes. That direction is + /// unreachable without an inspector: no classification the EVM itself produces both prices + /// work and hands the budget back. + fn settle_precompile_envelope( + &mut self, + staged: PrecompileEnvelope, + result: &FrameResult, + evm_remaining: u64, + ) { + let returned = + if result.instruction_result().is_ok_or_revert() { evm_remaining } else { 0 }; + let consumed = staged.forwarded.saturating_sub(returned); + self.compute_gas.record_burned_gas(consumed.saturating_sub(staged.executed)); + self.inspector.result += i128::from(staged.executed.saturating_sub(consumed)); + } + /// Merges resource usage from a sandbox execution into this tracker. /// /// Used by `KeylessDeploy` (REX5+) to propagate sandbox resource consumption @@ -1930,6 +2043,17 @@ impl AdditionalLimit { /// # Returns /// /// A `FrameResult` indicating that the limit is exceeded with the given instruction result. +/// Undoes the last mutating callback's edit to a frame result's gas, reporting the number the EVM +/// itself left there. +/// +/// The EVM does not execute inside an inspector callback, so the difference across one is the +/// inspector's by construction, and every settlement that asks what the *transaction* spent has to +/// ask it of the EVM's number rather than of the rewritten one. +#[inline] +fn evm_own_remaining(remaining: u64, inspector_gas_delta: i128) -> u64 { + (i128::from(remaining) - inspector_gas_delta).clamp(0, i128::from(u64::MAX)) as u64 +} + fn create_exceeding_limit_frame_result( instruction_result: InstructionResult, gas: Gas, diff --git a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs index 3419d0e2..d5cdf95b 100644 --- a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs +++ b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs @@ -259,7 +259,8 @@ fn test_create_collision_books_the_whole_swallowed_budget() { } } -/// A precompile comes back through the same arm, but it books its own split at the recording site. +/// A precompile comes back through the same arm on its own terms: the envelope it destroys is the +/// caller's uncapped forwarded amount rather than the budget its result carries. /// Booking again here would double it, so the total must stay one forwarded envelope. #[test] fn test_precompile_result_is_not_booked_a_second_time() { diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 8cf5d440..2128e7e5 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -41,8 +41,9 @@ //! frame's whole burned budget settles as compute gas without changing the receipt. //! - `frame_init_reject_burn` — the budget a refused frame init decides the fate of: the halting //! rejections (a CREATE onto an occupied address) swallow it and book it as destroyed, the -//! returning ones hand it back and book nothing, and a precompile stays booked by its own site; -//! then through the deposit-receipt rewrite and the sandbox merge that run after the booking. +//! returning ones hand it back and book nothing, and a precompile is settled from what its own +//! recording site staged; then through the deposit-receipt rewrite and the sandbox merge that run +//! after the booking. //! - `call_body_halt_charges` — what a CALL-family body already charged when it halts: the //! value-transfer surcharge and the return-range memory expansion stay in the open segment and //! settle as work, rather than being dropped by the wrapper's tail. @@ -61,9 +62,9 @@ //! - `checkpoint_static_fee_edges` — a table-prepaid checkpoint (`GAS`, `LOG1`) whose static fee //! exceeds the clamp headroom is a plain-segment crossing; `CREATE`'s 32,000 is charged inside //! the body, so the same headroom runs the body and then reverts. -//! - `precompile_halt` — a precompile that halts exceptionally is split at the recording site the -//! same way an interpreter frame is: executed work enforces, the unused forwarded envelope does -//! not. +//! - `precompile_halt` — a precompile that halts exceptionally is split the same way an interpreter +//! frame is, and at the same settlement point: executed work enforces, the unused forwarded +//! envelope does not. //! - `pre_execution_intrinsic_reject` — the one envelope-keeping synthetic halt REX7 cannot reach: //! for an ordinary transaction an intrinsic overrun is a validation error from REX5 on, and a //! validation error produces no receipt for any lane to account for. diff --git a/crates/mega-evm/tests/rex7/precompile_halt.rs b/crates/mega-evm/tests/rex7/precompile_halt.rs index ee0d12a5..0debcc74 100644 --- a/crates/mega-evm/tests/rex7/precompile_halt.rs +++ b/crates/mega-evm/tests/rex7/precompile_halt.rs @@ -1,7 +1,9 @@ //! A precompile that halts exceptionally is split the same way as an interpreter frame. //! //! A precompile runs inside `frame_init` and comes back as a result, so it never reaches the -//! interpreter-frame halt settlement. REX7 therefore splits it at the precompile recording site: +//! interpreter-frame halt settlement. REX7 splits it at the same settlement point every other +//! frame outcome goes through, from the classification the caller is handed — the recording site +//! stages what only it knows, and the split is taken from that: //! //! - **Executed** — the work the precompile actually performed (the KZG fixed fee when the call //! reached verification; zero when the input was rejected before any work). This is enforcing. @@ -13,9 +15,9 @@ //! any failure past that point means verification was under way and is priced at the whole fixed //! fee. //! -//! Through REX6 the same recording site stays single-lane: success / revert still charge spent, -//! every KZG failure past the wrapper's gas gate charges the fixed fee — doorway rejects -//! included — and every other error still charges the (capped) limit as enforcing usage. +//! Through REX6 the recording site stays single-lane and stages nothing: success / revert still +//! charge spent, every KZG failure past the wrapper's gas gate charges the fixed fee — doorway +//! rejects included — and every other error still charges the (capped) limit as enforcing usage. use crate::common::{transact, transact_default, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH}; use alloy_primitives::{address, Address, Bytes}; diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index a10be043..ce3b53ff 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -582,8 +582,8 @@ The two readings agree by construction of the law; keeping enforcement on the re The rules that follow fix `executed_compute` at each site that can leave budget unspent, which is what makes the law's remainder well defined; they are not themselves the definition of the destroyed total. -A precompile invocation that fails is the same split, taken at the precompile recording site. -A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. +A precompile invocation that fails is the same split. +A precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it; a node MUST take the split from the classification the call returns to its caller, which is what decides whether the caller reclaims the remainder. - **Executed** — the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work. For KZG the dividing line is its own input-length check, which runs before the commitment is read: an input whose length is not `KZG_POINT_EVALUATION_INPUT_LENGTH` is turned away before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost regardless of how far it got. @@ -776,4 +776,4 @@ System-granted gas leaks to the sender, who recovers gas that was never theirs t - [Rex4](../upgrades/rex4.md) — introduced the per-call-frame compute gas budget; made gas detention caps relative to usage at the access point; added beneficiary volatile-access guards to the `CALL` family, `SELFDESTRUCT`, and `SELFBALANCE`. - [Rex5](../upgrades/rex5.md) — excluded the `CALL_STIPEND` from the forwarded-gas deduction; moved `CREATE2` memory-expansion recording ahead of the storage-gas charge; made contract-creation code-deposit compute gas atomic with the deployment commit; refined precompile compute-gas recording and bounded it by the remaining compute budget; added the `SELFDESTRUCT` empty-beneficiary storage-gas charge; removed `CALLCODE` from the cold first-touch charge and added `SELFDESTRUCT`'s beneficiary to it; stopped following EIP-7702 delegation in the pre-execution inspection, restoring inherited warmth for delegates. - [Rex6](../upgrades/rex6.md) — unified the measurement window across all storage-affecting opcodes and folded `CREATE2` memory expansion into it, ending the two-window exception; returned forwarded gas to the failing frame on a compute-gas exceed; rescued the unused envelope on a keyless-deploy dispatch exceed; made beneficiary detection delegation-aware, returning `CALLCODE` call targets to the cold first-touch charge; exempted system-originated transactions from the compute gas limit and gas detention. -- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit; splits a failing precompile the same way at the recording site; weighs a contract creation's code-deposit compute gas against the compute budgets before recording it, rather than recording it ahead of the evaluation. +- [Rex7](../upgrades/rex7.md) _(unstable)_ — settles compute gas at checkpoints rather than after every plain opcode; enforces compute and detention limits inside plain segments by clamping interpreter-visible gas so a crossing opcode does not execute; records an exceptional-halt frame's burned remainder as compute gas at frame exit; splits a failing precompile the same way, from the classification its caller is handed; weighs a contract creation's code-deposit compute gas against the compute budgets before recording it, rather than recording it ahead of the evaluation. diff --git a/docs/spec/upgrades/rex7.md b/docs/spec/upgrades/rex7.md index f446eed0..ff78e239 100644 --- a/docs/spec/upgrades/rex7.md +++ b/docs/spec/upgrades/rex7.md @@ -34,7 +34,7 @@ Two deliberate accounting carve-outs remain. A frame that ends in an exceptional halt (including ordinary out-of-gas) settles its whole EVM-gas budget as compute gas, apart from storage gas it had already been charged, so a transaction that contains an inner out-of-gas call can report higher compute usage under Rex7 than under Rex6 even though EVM gas and the receipt are unchanged. That budget is split — the work the frame performed enforces like any other work, while the remainder it destroyed is reported but never enforced. The reported destroyed total is derived from a conservation law over what the transaction spent rather than summed from the sites that destroyed it, so an envelope lost anywhere lands in it whether or not a site was written to book it. -A precompile that fails is split the same way at its recording site: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own input-length check included) enforces, and the unused caller-supplied envelope is destroyed. +A precompile that fails is split the same way, from the classification its caller is handed: executed work (the KZG fixed fee when the call reached verification; zero when the input was rejected before any work, KZG's own input-length check included) enforces, and the unused caller-supplied envelope is destroyed. The generic error arm therefore stops enforcing the whole forwarded amount, which is an intentional enforcement difference from Rex6; the Rex5 forwarded-gas cap still prevents the precompile from performing more work than the remaining compute budget. ## What Changed @@ -120,7 +120,8 @@ This is the one shape where Rex7 enforcement is stricter than Rex6's, which attr A node MUST take the split from the frame's **final** result, after the create-return processing that can still turn a successful constructor into a canonical code-deposit out-of-gas, an EIP-3541 reject or a runtime code-size reject — each of which destroys the frame's remainder just as a halt from the interpreter loop does. When a nested execution merges its usage into an outer one, which today is only the `KeylessDeploy` sandbox boundary, a node MUST carry the split across that boundary: the outer transaction reports the inner total in full and enforces only its executed part. -A precompile invocation that fails is the same split, taken at the precompile recording site rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. +A precompile invocation that fails is the same split, taken from the classification the call returns to its caller rather than at interpreter-frame exit — a precompile never becomes a child EVM frame, so the frame-exit settlement cannot see it. +The classification is what decides whether the caller reclaims the remainder, so it is what the split has to be taken from; a node MUST NOT fix the split earlier, while the call's outcome can still change. The **executed** part is the work the precompile performed: the KZG point-evaluation fixed cost when that precompile reached verification and returned a non-out-of-gas error, and zero when the invocation was rejected before any work (malformed input, or a wrapper out-of-gas that never reached verification). For KZG the boundary is its own input-length check, which runs before the commitment is read: an input whose length is not `KZG_POINT_EVALUATION_INPUT_LENGTH` is a rejection before any work, while every other non-out-of-gas failure is raised once verification is under way and is priced at the whole fixed cost, however far it got. A node MUST price an unrecognised non-out-of-gas KZG failure as verification under way, so that an unfamiliar failure can only over-charge. From 6423162a934e23f1411b5f3c68754c1d1d66b116 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 16:06:01 +0800 Subject: [PATCH 119/208] feat(evm): book the rewrites an inspector makes that move no gas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The canonical block guard's only criterion was a zero gas ledger, and a rewrite that costs nothing passes it. A frame result whose classification is changed at `call_end` moves no gas anywhere — the remainder, the envelope and every interpreter counter are the EVM's own — and yet the callee's write is rolled back and the caller is handed a failure. Same for a rewritten output, an edited frame input, and a frame the inspector answers itself. The cheat matrix already carried the evidence: those cells asserted an empty ledger and a changed state in the same breath. So the shim now counts them, on a lane of its own, and the guard's criterion becomes the whole ledger: no gas the EVM did not move, and nothing the shim was handed coming back changed. Gas edits stay off this lane — they are what the three gas lanes are for. The line is what a callback boundary can see, and the docs say so rather than claiming more: the interpreter's stack and memory, direct journal writes and the pending action leave no trace in the arguments the shim holds, and telling whether they changed needs a snapshot of unbounded state. An all-zero ledger still does not mean the transaction is the one the EVM would have produced alone. Observation is untouched, and pinned: the production tracer still builds a block. The two block-executor interception tests now assert the refusal — reaching it is what says the frame stacks stayed aligned, which is what they were about. An EVM driven off the canonical path runs the same rewrite to completion, which is what leaves an off-band simulation free to use one. --- AGENTS.md | 4 +- crates/mega-evm/src/block/executor.rs | 14 +- crates/mega-evm/src/block/result.rs | 27 ++-- crates/mega-evm/src/evm/inspector.rs | 128 ++++++++++++++++-- crates/mega-evm/src/evm/result.rs | 24 ++-- crates/mega-evm/src/limit/inspector_ledger.rs | 75 ++++++++-- crates/mega-evm/src/limit/limit.rs | 7 + .../tests/block_executor/inspector.rs | 41 +++--- .../tests/block_executor/inspector_guard.rs | 110 ++++++++++++++- .../tests/rex7/inspector_cheat_matrix.rs | 23 +++- .../mega-evm/tests/rex7/measured_inspector.rs | 11 +- 11 files changed, 382 insertions(+), 82 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ee768cfb..6dd040b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,8 +131,10 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. + The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome — because a rewrite that costs nothing still produces different state and a different receipt. + What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes, the pending action), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. - Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full. + Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index cc670450..77fe067b 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -78,20 +78,24 @@ impl core::fmt::Debug for MegaBlockExecutor } } -/// Refuses a transaction whose gas accounting an inspector adjusted, on the canonical path. +/// Refuses a transaction an inspector took part in, on the canonical path. /// /// Block production and block validation are the two places where what the executor reports has to -/// be what the EVM did, reproducibly, on every node. An inspector's edits reach the receipt and the -/// block's counters but live in one node's configuration, so a transaction carrying any is not -/// something this executor may run or admit — see +/// be what the EVM did, reproducibly, on every node. An inspector's edits reach the receipt, the +/// block's counters and the transaction's state, but live in one node's configuration, so a +/// transaction carrying any is not something this executor may run or admit — see /// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]. /// +/// The criterion is the whole ledger, not its gas lanes. A rewrite of a frame's classification or +/// output, or a frame the inspector answered itself, moves no gas anywhere and would pass a +/// gas-only check while producing different state and a different receipt. +/// /// Enforced in release builds, deliberately. This is a boundary the canonical path holds against /// its embedder rather than an invariant `MegaETH` maintains internally, so it has to hold in the /// binaries that build and validate blocks, and it fails the block rather than the process. /// /// The check is free on every path that passes it: the ledger is a `Copy` struct already on the -/// outcome, and this reads four fields of it once per transaction. +/// outcome, and this reads its fields once per transaction. #[inline] fn reject_inspector_adjusted_accounting( tx_hash: B256, diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index c4a4ee16..8a51da96 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -258,24 +258,33 @@ impl InvalidTxError for MegaBlockLimitExceededError { /// transaction itself for a caller to fix. #[derive(Debug, Clone, thiserror::Error)] pub enum MegaBlockExecutionError { - /// A transaction whose gas accounting an inspector adjusted reached the canonical - /// block-execution path. + /// A transaction an inspector took part in reached the canonical block-execution path. /// /// Block production and block validation must produce the same numbers for the same block, on /// every node, so what the executor reports has to be what the EVM did — and only that. An - /// inspector that writes gas into an interpreter's counter, into a frame's envelope, or into a - /// returning frame's result is a second producer of gas movement, present on one node's - /// configuration and not on another's, whose effect reaches the receipt, the transaction's - /// reported compute total, and through it the block's cumulative counters. + /// inspector is present on one node's configuration and not on another's, and it can break + /// that in two ways: + /// + /// - by writing gas into an interpreter's counter, into a frame's envelope, or into a + /// returning frame's result, which reaches the receipt, the transaction's reported compute + /// total, and through it the block's cumulative counters; + /// - by rewriting what a frame *did* — its classification, its output, or the frame itself + /// through a synthetic outcome — which moves no gas at all and reaches the transaction's + /// state and its receipt directly. + /// + /// Both are refused. The second is why the criterion is the whole ledger rather than its gas + /// lanes: a rewrite that costs nothing is not a rewrite that changes nothing. /// /// Observation is untouched: a tracer leaves an all-zero ledger, which is what every inspector /// on this path today does. An embedder that genuinely wants a rewriting inspector still has /// one — [`MegaEvm::execute_transaction`](crate::MegaEvm::execute_transaction) supports it in /// full, with the ledger reported on the outcome — it just does not get to call the result a - /// block. + /// block. That is also what leaves a simulation EVM an embedder drives off the canonical path + /// alone, however much its inspector rewrites: this guard sits on the block executor's + /// entries, not on the EVM. #[error( - "transaction {tx_hash} reached the canonical block-execution path with its gas accounting \ - adjusted by an inspector: {ledger:?}" + "transaction {tx_hash} reached the canonical block-execution path after an inspector took \ + part in it: {ledger:?}" )] InspectorAdjustedAccounting { /// The transaction the adjusted accounting belongs to. diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 7c8a6325..e11821b7 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -36,7 +36,7 @@ use alloc as std; use std::string::String; use alloy_evm::Database; -use alloy_primitives::{Address, Log, U256}; +use alloy_primitives::{Address, Bytes, Log, U256}; use revm::{ context::{ContextError, ContextTr}, handler::FrameResult, @@ -170,6 +170,85 @@ fn book_env_adjustment( .record_inspector_env_adjustment(i128::from(after) - i128::from(before)); } +/// Books one rewrite that changes what the execution *did* rather than what it cost — see +/// [`InspectorLedger::interventions`](crate::InspectorLedger::interventions). +/// +/// Every caller answers the same question about the argument it was handed: did it come back +/// describing something other than what the EVM was about to do? Two things are deliberately not +/// part of that question: +/// +/// - **Gas.** A frame input's gas limit and a frame result's remaining gas are booked as gas, on +/// the ledger's own lanes; counting them here as well would report one rewrite twice. +/// - **Anything the argument does not describe.** The interpreter's stack and memory, the journal, +/// the pending action. Telling whether those came back changed needs a snapshot of unbounded +/// state, which no callback boundary can take at a cost the inspected path can carry. +#[inline] +fn book_intervention( + context: &MegaContext, + changed: bool, +) { + if changed { + context.additional_limit.borrow_mut().record_inspector_intervention(); + } +} + +/// Whether two output buffers are the same buffer. +/// +/// Compared by address and length rather than by content. `Bytes` is immutable, so a callback can +/// only change an output by putting a different buffer there, and the caller holds its snapshot +/// across the comparison — which keeps the original alive, so its address cannot be reused +/// underneath. A replacement that copies the same bytes reads as unchanged, which is what it is. +#[inline] +fn same_buffer(before: &Bytes, after: &Bytes) -> bool { + before.as_ptr() == after.as_ptr() && before.len() == after.len() +} + +/// Whether a callback rewrote what a finished frame *did*: the classification its caller will see, +/// or the output it will read. +/// +/// The classification is the one that carries the most: it decides whether the caller sees a +/// success, and — through the frame's settlement point — whether the frame's state is committed +/// and whether its remainder is handed back or destroyed. None of that moves gas by itself, so +/// none of it leaves a trace in any gas lane. +#[inline] +fn result_rewritten(before: (InstructionResult, &Bytes), after: &InterpreterResult) -> bool { + before.0 != after.result || !same_buffer(before.1, &after.output) +} + +/// Whether a callback edited a call frame's inputs anywhere but in their gas limit. +/// +/// Everything else a call input carries — who is called, with what value, under which scheme, with +/// what calldata, in a static context or not — describes what the frame will do. +#[inline] +fn call_inputs_rewritten(mut before: CallInputs, after: &CallInputs) -> bool { + before.gas_limit = after.gas_limit; + before != *after +} + +/// Whether a callback edited a creation's inputs anywhere but in their gas limit. +#[inline] +fn create_inputs_rewritten(mut before: CreateInputs, after: &CreateInputs) -> bool { + before.set_gas_limit(after.gas_limit()); + before != *after +} + +/// [`call_inputs_rewritten`] / [`create_inputs_rewritten`] for the generic callback, which is +/// handed the variant rather than the inputs. A callback that swapped the variant itself has +/// rewritten the frame as thoroughly as it is possible to. +#[inline] +fn frame_input_rewritten(before: FrameInput, after: &FrameInput) -> bool { + match (before, after) { + (FrameInput::Call(before), FrameInput::Call(after)) => { + call_inputs_rewritten(*before, after) + } + (FrameInput::Create(before), FrameInput::Create(after)) => { + create_inputs_rewritten(*before, after) + } + (FrameInput::Empty, FrameInput::Empty) => false, + _ => true, + } +} + /// Refuses a rewrite that turns a non-successful contract creation into a successful one, and says /// so loudly. /// @@ -293,9 +372,15 @@ where context: &mut MegaContext, frame_input: &mut FrameInput, ) -> Option { - let before = frame_input_gas_limit(frame_input); + let before = frame_input.clone(); let outcome = self.inner.frame_start(context, frame_input); - book_env_adjustment(context, before, frame_input_gas_limit(frame_input), outcome.is_some()); + book_env_adjustment( + context, + frame_input_gas_limit(&before), + frame_input_gas_limit(frame_input), + outcome.is_some(), + ); + book_intervention(context, outcome.is_some() || frame_input_rewritten(before, frame_input)); outcome } @@ -307,7 +392,12 @@ where frame_result: &mut FrameResult, ) { let before = frame_result.instruction_result(); + let output = frame_result.interpreter_result().output.clone(); self.inner.frame_end(context, frame_input, frame_result); + book_intervention( + context, + result_rewritten((before, &output), frame_result.interpreter_result()), + ); // `frame_end` runs after `create_end` and is the last chance to rewrite a creation's // classification, so the same refusal applies here. if let FrameResult::Create(outcome) = frame_result { @@ -321,14 +411,21 @@ where context: &mut MegaContext, inputs: &mut CallInputs, ) -> Option { - let before = inputs.gas_limit; + let before = inputs.clone(); let outcome = self.inner.call(context, inputs); - book_env_adjustment(context, Some(before), Some(inputs.gas_limit), outcome.is_some()); + book_env_adjustment( + context, + Some(before.gas_limit), + Some(inputs.gas_limit), + outcome.is_some(), + ); + book_intervention(context, outcome.is_some() || call_inputs_rewritten(before, inputs)); outcome } - /// `CallInputs` is immutable here and the frame's result gas is deliberately not booked — see - /// [`InspectorLedger::env`](crate::InspectorLedger::env) — so this is a plain forward. + /// `CallInputs` is immutable here and the frame's result gas is deliberately not booked at this + /// boundary — see [`InspectorLedger::env`](crate::InspectorLedger::env) — so the only thing to + /// measure is what the callback did to the result's classification and output. #[inline] fn call_end( &mut self, @@ -336,7 +433,9 @@ where inputs: &CallInputs, outcome: &mut CallOutcome, ) { + let before = (outcome.result.result, outcome.result.output.clone()); self.inner.call_end(context, inputs, outcome); + book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); } #[inline] @@ -345,9 +444,15 @@ where context: &mut MegaContext, inputs: &mut CreateInputs, ) -> Option { - let before = inputs.gas_limit(); + let before = inputs.clone(); let outcome = self.inner.create(context, inputs); - book_env_adjustment(context, Some(before), Some(inputs.gas_limit()), outcome.is_some()); + book_env_adjustment( + context, + Some(before.gas_limit()), + Some(inputs.gas_limit()), + outcome.is_some(), + ); + book_intervention(context, outcome.is_some() || create_inputs_rewritten(before, inputs)); outcome } @@ -362,9 +467,10 @@ where inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { - let before = outcome.result.result; + let before = (outcome.result.result, outcome.result.output.clone()); self.inner.create_end(context, inputs, outcome); - reject_forbidden_create_rewrite(context, before, &mut outcome.result); + book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); + reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); } /// Everything this callback receives is passed by value, so it cannot change execution state. diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 7b1465c4..50024b53 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -80,15 +80,18 @@ pub struct MegaTransactionOutcome { pub compute_gas_enforced: u64, /// The state growth used. pub state_growth_used: u64, - /// What an inspector did to this transaction's gas accounting, measured rather than inferred. + /// What an inspector did to this transaction, measured rather than inferred. /// /// `MegaETH` wraps every inspector it is handed in a measurement shim. The EVM does not - /// execute inside an inspector callback, so anything that moves across one is the inspector's - /// doing by construction, and this is what the shim booked: gas written into a live - /// interpreter's counter ([`gas`](crate::InspectorLedger::gas)), into the envelope a frame is - /// about to be built with ([`env`](crate::InspectorLedger::env)), into a returning frame's - /// result ([`result`](crate::InspectorLedger::result)), and how many rewrites the shim refused - /// outright ([`rejected_rewrites`](crate::InspectorLedger::rejected_rewrites)). + /// execute inside an inspector callback, so anything that changes across one is the + /// inspector's doing by construction, and this is what the shim booked: gas written into a + /// live interpreter's counter ([`gas`](crate::InspectorLedger::gas)), into the envelope a + /// frame is about to be built with ([`env`](crate::InspectorLedger::env)), into a + /// returning frame's result ([`result`](crate::InspectorLedger::result)), how many + /// rewrites the shim refused + /// outright ([`rejected_rewrites`](crate::InspectorLedger::rejected_rewrites)), and how many + /// rewrote what the execution *did* without moving any gas at all + /// ([`interventions`](crate::InspectorLedger::interventions)). /// /// # Sign convention /// @@ -107,9 +110,10 @@ pub struct MegaTransactionOutcome { /// exactly that reason. /// /// The converse does not hold, and reading it that way is the mistake this field invites. What - /// is measured is gas movement: an inspector that only rewrites a frame result's - /// classification, edits the interpreter's stack or memory, or writes the journal directly - /// moves no gas and leaves this empty, while changing the state the transaction produces. + /// is measured is what the shim can see at a callback boundary: gas that moved, and arguments + /// that came back changed. An inspector that reaches past those — editing the interpreter's + /// stack or memory, writing the journal directly, or editing the pending action — leaves this + /// empty while changing the state the transaction produces. /// /// # What it is for /// diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index abf21d85..e285beea 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -1,11 +1,13 @@ -//! The ledger of what an inspector did to a transaction's gas accounting. +//! The ledger of what an inspector did to a transaction. //! //! `MegaETH` wraps every inspector it is handed in a measurement shim (`MeasuredInspector`), and -//! the shim books what it measures here. Nothing in this module enforces anything: the numbers it -//! holds are exactly the part of a transaction's gas movement that the EVM did not produce, kept -//! separate so that enforcement can ignore it and the conservation law can account for it. +//! the shim books what it measures here. Nothing in this module enforces anything: the gas lanes +//! are exactly the part of a transaction's gas movement that the EVM did not produce, kept separate +//! so that enforcement can ignore it and the conservation law can account for it, and the two +//! counters record rewrites that move no gas at all. -/// What an inspector conjured, destroyed, or refused, as measured at the callback boundaries. +/// What an inspector conjured, destroyed, rewrote, or had refused, as measured at the callback +/// boundaries. /// /// # Why the boundary is a sound place to measure /// @@ -23,11 +25,15 @@ /// /// # What it does not measure /// -/// Gas movement, and only that. An inspector can change an execution in ways that move no gas at -/// all — rewriting a frame result's classification, editing the interpreter's stack or memory, -/// writing the journal directly — and every one of those leaves this all-zero. So an empty ledger -/// says the transaction's *gas numbers* are the EVM's own; it does not say the transaction is the -/// one the EVM would have produced alone. +/// What a callback does behind the shim's back. An inspector reaches state that no argument it is +/// handed describes — the interpreter's stack and memory, the journal, the pending action — and +/// telling whether any of those came back changed needs a snapshot of unbounded state that no +/// callback boundary can take at a cost the inspected path can carry. Those rewrites leave this +/// all-zero. +/// +/// So an empty ledger says two things: no gas moved that the EVM did not move, and nothing the +/// shim was handed came back different. It does not say the transaction is the one the EVM would +/// have produced alone. /// /// # What consumes it /// @@ -94,6 +100,30 @@ pub struct InspectorLedger { /// journal has already reverted the frame and after the deposit predicates have already /// rejected the code, so honouring it would report a deployment that never happened. pub rejected_rewrites: u32, + + /// How many rewrites the shim saw that change what the execution *did* rather than what it + /// cost. + /// + /// The three gas lanes above answer "did the transaction's numbers move". This answers the + /// other half — "was the transaction left alone" — for the part of it a callback boundary can + /// see, which is the arguments the shim itself is handed: + /// + /// - a frame result whose classification or returned output came back changed, at each of the + /// three callbacks that can change one (`call_end`, `create_end`, `frame_end` — revm runs + /// the variant-specific one and then the generic one over the same result, so each is + /// counted where it happens rather than once at the end); + /// - a frame's inputs edited anywhere but in their gas limit, at each of the three callbacks + /// that can edit them (`frame_start`, `call`, `create`); + /// - a frame the inspector answered itself, with a synthetic outcome instead of letting the + /// EVM build it. + /// + /// Gas edits are deliberately excluded — a gas limit or a result's remaining gas moving is + /// what the lanes above are for, and counting it here would say the same thing twice. + /// + /// A classification rewrite is the shape that made this lane necessary: it moves no gas + /// anywhere, so every gas lane stays zero while the transaction produces different state and + /// a different receipt. + pub interventions: u32, } impl InspectorLedger { @@ -111,7 +141,11 @@ impl InspectorLedger { /// the rewrites that move no gas and so leave this true. #[inline] pub const fn is_zero(&self) -> bool { - self.gas == 0 && self.env == 0 && self.result == 0 && self.rejected_rewrites == 0 + self.gas == 0 && + self.env == 0 && + self.result == 0 && + self.rejected_rewrites == 0 && + self.interventions == 0 } } @@ -124,7 +158,13 @@ mod tests { /// unmoved in that case. #[test] fn test_conjured_gas_is_the_net_of_both_lanes() { - let ledger = InspectorLedger { gas: 2_300, env: -2_300, result: 0, rejected_rewrites: 0 }; + let ledger = InspectorLedger { + gas: 2_300, + env: -2_300, + result: 0, + rejected_rewrites: 0, + interventions: 0, + }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); } @@ -132,7 +172,16 @@ mod tests { /// A refused rewrite moves no gas but must still show the transaction was not left alone. #[test] fn test_a_rejected_rewrite_alone_is_not_zero() { - let ledger = InspectorLedger { gas: 0, env: 0, result: 0, rejected_rewrites: 1 }; + let ledger = InspectorLedger { rejected_rewrites: 1, ..InspectorLedger::default() }; + assert_eq!(ledger.conjured_gas(), 0); + assert!(!ledger.is_zero()); + } + + /// A classification rewrite is the shape the gas lanes cannot see: it moves nothing, so the + /// only thing standing between it and an all-zero ledger is this counter. + #[test] + fn test_an_intervention_alone_is_not_zero() { + let ledger = InspectorLedger { interventions: 1, ..InspectorLedger::default() }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero()); } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index d77223a5..a765271f 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -585,6 +585,13 @@ impl AdditionalLimit { self.inspector.rejected_rewrites = self.inspector.rejected_rewrites.saturating_add(1); } + /// Counts one rewrite that changes what the execution did rather than what it cost — see + /// [`InspectorLedger::interventions`](inspector_ledger::InspectorLedger::interventions). + #[inline] + pub(crate) fn record_inspector_intervention(&mut self) { + self.inspector.interventions = self.inspector.interventions.saturating_add(1); + } + /// The EVM gas the transaction has spent that is neither compute work nor destroyed (REX7+, /// always 0 before) — the second term of /// [`ConservationTerms`](conservation::ConservationTerms). diff --git a/crates/mega-evm/tests/block_executor/inspector.rs b/crates/mega-evm/tests/block_executor/inspector.rs index cac0eb10..0a811b93 100644 --- a/crates/mega-evm/tests/block_executor/inspector.rs +++ b/crates/mega-evm/tests/block_executor/inspector.rs @@ -286,9 +286,17 @@ fn test_inspector_early_return_with_additional_limits() { // Execute transaction - this triggers a nested CALL that the inspector intercepts let tx = create_transaction(0, 1_000_000); - // Before the fix, this would panic with "frame stack is empty" - let result = executor.execute_transaction(&tx); - assert!(result.is_ok(), "Transaction should succeed: {:?}", result.err()); + // Before the fix, this would panic with "frame stack is empty". It runs to completion now, + // and the canonical path then declines to admit it — an inspector that answers a frame itself + // is a rewriting inspector, which `inspector_guard` covers. That refusal is the assertion the + // alignment rests on: reaching it at all means every push found its pop. + let err = executor + .execute_transaction(&tx) + .expect_err("the canonical path refuses an intercepting inspector"); + assert!( + format!("{err:?}").contains("interventions: 1"), + "the refusal must name the interception it saw: {err:?}", + ); // Verify the inspector intercepted the nested call assert_eq!( @@ -303,13 +311,6 @@ fn test_inspector_early_return_with_additional_limits() { 2, "call_end should be invoked for both the main call and the intercepted nested call" ); - - // Finish the block - let block_result = executor.finish(); - assert!(block_result.is_ok(), "Block should finish successfully"); - - let (_, receipts) = block_result.unwrap(); - assert_eq!(receipts.receipts.len(), 1, "Should have 1 receipt"); } /// An inspector that returns early for create operations, skipping frame execution. @@ -397,9 +398,16 @@ fn test_inspector_early_return_create_with_additional_limits() { let init_code = Bytes::from(vec![0x00]); let tx = create_deploy_transaction(0, 10_000_000, init_code); - // Before the fix, this would panic with "frame stack is empty" - let result = executor.execute_transaction(&tx); - assert!(result.is_ok(), "Transaction should succeed: {:?}", result.err()); + // Before the fix, this would panic with "frame stack is empty". As above, the transaction now + // runs to completion and the canonical path declines to admit it; getting as far as the + // refusal is what says the frame stacks stayed aligned. + let err = executor + .execute_transaction(&tx) + .expect_err("the canonical path refuses an intercepting inspector"); + assert!( + format!("{err:?}").contains("interventions: 1"), + "the refusal must name the interception it saw: {err:?}", + ); // Verify the inspector intercepted the create operation assert_eq!( @@ -414,11 +422,4 @@ fn test_inspector_early_return_create_with_additional_limits() { 1, "create_end should be invoked for the intercepted create" ); - - // Finish the block - let block_result = executor.finish(); - assert!(block_result.is_ok(), "Block should finish successfully"); - - let (_, receipts) = block_result.unwrap(); - assert_eq!(receipts.receipts.len(), 1, "Should have 1 receipt"); } diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 8a6f169a..7d398e83 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -7,6 +7,12 @@ //! gas counter reaches the receipt, the transaction's reported compute total, and through it the //! block's cumulative counters. //! +//! It can also rewrite what a frame *did* — its classification, its output, or the frame itself, +//! answered with a synthetic outcome — which moves no gas anywhere and reaches the transaction's +//! state and its receipt directly. A gas-only criterion would admit every one of those, so the +//! criterion is the whole ledger: no gas the EVM did not move, and nothing the shim was handed +//! coming back changed. +//! //! So every entry on the canonical path — the two that run a transaction and the one funnel that //! admits a result — refuses a non-zero ledger. The refusal is an error rather than an assertion, //! because it is a boundary held against an embedder and has to hold in the binaries that build @@ -25,13 +31,13 @@ use mega_evm::{ alloy_evm::block::BlockExecutionError, test_utils::{BytecodeBuilder, MemoryDatabase}, BlockLimits, InspectorLedger, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, - MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, TestExternalEnvs, + MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, MegaTxEnvelope, TestExternalEnvs, }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, - context::BlockEnv, + context::{BlockEnv, ContextTr}, database::State, - interpreter::{Interpreter, InterpreterTypes}, + interpreter::{CallInputs, CallOutcome, InstructionResult, Interpreter, InterpreterTypes}, Inspector, }; @@ -62,6 +68,28 @@ impl Inspector for GasInjector { } } +/// Rewrites the classification of the fixture's inner call, once — the smallest rewrite that moves +/// no gas at all. +/// +/// Every one of the ledger's gas lanes stays at zero under this inspector: the call's remaining +/// gas, its envelope and every interpreter counter are exactly what the EVM left. What changes is +/// what the transaction did — the callee's storage write is rolled back and the caller reads a +/// failure — which is why the guard cannot be a gas-only check. +#[derive(Default)] +struct CallFailer { + applied: bool, +} + +impl Inspector for CallFailer { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.applied || inputs.target_address != CALLEE { + return; + } + self.applied = true; + outcome.result.result = InstructionResult::Revert; + } +} + /// Counts callbacks and changes nothing — the shape every tracer in production has. #[derive(Default)] struct Observer { @@ -460,3 +488,79 @@ fn test_the_production_tracer_is_admitted() { let (_, result) = executor.finish().expect("the block must finish"); assert_eq!(result.receipts.len(), 1); } + +/// The rewrite a gas-only guard could not see: a frame's classification, changed at `call_end`. +/// +/// Nothing moves. The call's remaining gas, its envelope and every interpreter counter are the +/// EVM's own, so all three gas lanes read zero — and yet the callee's write is rolled back and the +/// caller is handed a failure, which is a different transaction with a different state root. The +/// intervention counter is the only thing standing between this and admission. +#[test] +fn test_a_rewrite_that_moves_no_gas_is_refused_too() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + CallFailer::default(), + ); + + let tx = envelope(0); + let err = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect_err("a classification rewrite must be refused like any other"); + + assert!(executor.evm().inspector.applied, "the fixture must reach the rewrite point"); + let ledger = expect_refusal(&err, *tx.hash()); + assert_eq!( + (ledger.gas, ledger.env, ledger.result), + (0, 0, 0), + "the point of this shape is that no gas lane moves; got {ledger:?}", + ); + assert_eq!(ledger.interventions, 1, "the rewrite must be the thing the refusal names"); + assert_eq!( + executor.block_limiter.block_compute_gas_used, 0, + "a refused transaction must leave the block's counters where they were", + ); +} + +/// An EVM driven off the canonical path is not covered by the guard, however much its inspector +/// rewrites. +/// +/// The guard sits on the block executor's entries, not on `MegaEvm`, and this is what makes that a +/// property rather than an accident of the current call graph. It is what leaves a simulation EVM +/// — the oracle set-slot preflight the node runs before it publishes a value, and anything else an +/// embedder drives itself — free to attach a rewriting inspector: such a run never produces a +/// block, so there is nothing for two nodes to disagree about. +/// +/// The same transaction is refused by the executor in +/// [`test_a_rewrite_that_moves_no_gas_is_refused_too`], so the two together say the boundary is +/// where it is claimed to be rather than nowhere. +#[test] +fn test_an_off_path_evm_runs_the_same_rewrite_to_completion() { + let mut db = build_db(); + let mut inspector = CallFailer::default(); + let mut evm = mega_evm::MegaEvm::new( + mega_evm::MegaContext::new(&mut db, MegaSpecId::REX7) + .with_tx_runtime_limits(mega_evm::EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ) + .with_inspector(&mut inspector); + + let mut tx = mega_evm::MegaTransaction::new( + revm::context::tx::TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(1_000_000) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + let outcome = evm.execute_transaction(tx).expect("the EVM supports the rewrite in full"); + + assert!(inspector.applied, "the fixture must reach the rewrite point"); + assert_eq!( + outcome.inspector_ledger.interventions, 1, + "the rewrite is still measured and reported — it is simply not refused here", + ); + assert!(outcome.result_and_state.result.is_success(), "and the transaction still completes"); +} diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 1056b971..081b9d67 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -798,15 +798,24 @@ struct Cell { } fn ledger_gas(gas: i128) -> InspectorLedger { - InspectorLedger { gas, env: 0, result: 0, rejected_rewrites: 0 } + InspectorLedger { gas, ..InspectorLedger::default() } } fn ledger_env(env: i128) -> InspectorLedger { - InspectorLedger { gas: 0, env, result: 0, rejected_rewrites: 0 } + InspectorLedger { env, ..InspectorLedger::default() } } fn ledger_result(result: i128) -> InspectorLedger { - InspectorLedger { gas: 0, env: 0, result, rejected_rewrites: 0 } + InspectorLedger { result, ..InspectorLedger::default() } +} + +/// The ledger of a rewrite that moves no gas: the shim saw the argument it was handed come back +/// changed, and that is the whole of what it books. +/// +/// These are the cells that would otherwise be indistinguishable from an observation-only run, and +/// the reason the canonical block path could not tell them apart before this lane existed. +fn ledger_intervention() -> InspectorLedger { + InspectorLedger { interventions: 1, ..InspectorLedger::default() } } /// The fixture ran to its end and every frame committed: the callee's write, the deployment, and @@ -929,14 +938,14 @@ fn matrix() -> Vec { at, EditInput, Fixture::ReturningCallee, - InspectorLedger::default(), + ledger_intervention(), if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, ); push( at, Intercept, Fixture::ReturningCallee, - InspectorLedger::default(), + ledger_intervention(), if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, ); push( @@ -969,7 +978,7 @@ fn matrix() -> Vec { at, FailResult, Fixture::ReturningCallee, - InspectorLedger::default(), + ledger_intervention(), if creation { state_no_deployment } else { state_callee_write_rolled_back }, ); if !creation { @@ -979,7 +988,7 @@ fn matrix() -> Vec { at, ReviveResult, Fixture::RevertingCallee, - InspectorLedger::default(), + ledger_intervention(), state_callee_write_revived, ); } diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 1c5e4e38..03a28320 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -513,6 +513,9 @@ fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { /// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the /// conservation law would come out over by the bonus — the same failure as not booking a real one, /// with the sign flipped. +/// +/// The interception itself is booked, on the lane that carries rewrites rather than gas: answering +/// a frame the EVM was about to build changes what the transaction did, whatever it costs. #[test] fn test_an_intercepting_callback_books_no_envelope_adjustment() { /// Raises the child's gas limit and then intercepts the call, handing back an outcome built @@ -557,11 +560,13 @@ fn test_an_intercepting_callback_books_no_envelope_adjustment() { assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); - assert!( - inspected.ledger.is_zero(), - "an edit to inputs that never reach a frame conjures nothing; got {:?}", + assert_eq!( inspected.ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ + itself a rewrite", ); + assert_eq!(inspected.ledger.conjured_gas(), 0, "no gas lane may move on this shape"); assert_identity("intercepted", &inspected); } From a2273aea79bd4741dcd504cdd4be9714bba6dc43 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 16:11:10 +0800 Subject: [PATCH 120/208] chore(state-test): retire the two chaos triage switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both existed to exclude a finding the corpus sweep had reported and the product had not yet fixed. Both findings are fixed, so the switches now exclude nothing — and leaving them would make the default sweep look narrower than it is. `--chaos-shapes` stays: it narrows by shape, which is how a flagged vector is bisected. The tool tests drop their filter with them; the comment that said this becomes `ShapeFilter::default()` when the windows close was accurate, and they are closed. The shim's contract table gains the rewrite shapes the intervention lane now books, and the counter-lane gap becomes a description of where the predicate draws its line — including the pending action, which is reachable and deliberately not measured at the boundary, with the reason. --- crates/mega-evm/src/evm/AGENTS.md | 26 ++++--- crates/mega-state-test/src/chaos.rs | 85 +++------------------- crates/mega-state-test/tests/chaos_mode.rs | 25 ++----- crates/state-test/src/main.rs | 66 +++++------------ 4 files changed, 48 insertions(+), 154 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 2087757e..2de29ba7 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -48,25 +48,27 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | Read-only observation | Supported, free | nothing | an empty ledger, and numbers identical to an uninspected run | | Gas written into a live interpreter's counter (`initialize_interp`, `step`, `step_end`, `log_full`) | Supported | `InspectorLedger::gas`, at the callback boundary | nothing: the checkpoint baseline shifts by the same amount, and the gas clamp is re-derived on the spot so injected gas buys no compute headroom | | A frame input's `gas_limit`, raised or lowered (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::env`, at the callback boundary | nothing: a frame's compute budget comes from the tracker, not from its gas limit | -| A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | nothing on the `env` lane — the edited inputs never reach a frame — and the outcome's own gas on the `result` lane | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | +| A frame input's semantic fields — target, caller, value, scheme, calldata, static flag (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing: it changes what the frame does, not what it costs | +| A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`; nothing on the `env` lane — the edited inputs never reach a frame — and the outcome's own gas on the `result` lane | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | | A finished frame result's remaining gas (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::result`, at the frame's settlement point rather than at the callback boundary | nothing | -| A successful frame result rewritten into a revert or a halt | Supported | nothing — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it | -| A failed **call** frame rewritten into a success | Supported | nothing | the journal commits, so the frame's state follows the result its caller was handed | -| A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | -| The interpreter's stack or memory | Supported, unmeasured | nothing | the EVM executes on the edited state and meters it as its own work, because it is | -| A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | +| A finished frame result's returned output (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing | +| A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | +| A failed **call** frame rewritten into a success | Supported | `InspectorLedger::interventions` | the journal commits, so the frame's state follows the result its caller was handed | +| A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | +| The interpreter's stack or memory | Supported, unmeasured | nothing — no argument the shim holds describes it | the EVM executes on the edited state and meters it as its own work, because it is | +| A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing — no argument the shim holds describes it | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | +| The pending `InterpreterAction`, reached through `LoopControl` | Supported, unmeasured | nothing | an edit to the action's gas does move the envelope; measuring it at the callback boundary would be unsound, because whether it moves anything depends on the frame's final classification | | `CfgEnv` or the active gas schedule | **Refused** | — | the gas-schedule pin panics; the schedule belongs to the spec, and a rewritten one has no accounting lane that could rescue it | Two independent stops back the creation refusal: the shim restores the classification, and `frame.rs`'s `FrameJournalVerdict::CreateRejected` carries no code and no commit branch, so even with the refusal removed such a rewrite deposits nothing. Booking is a *reported* quantity throughout. No resource limit is ever compared against the ledger, and `MegaTransactionOutcome::compute_gas_enforced` comes off the enforcement lane rather than out of the reported total — so an inspector cannot buy a transaction headroom on any dimension. -### Known gap in the counter lane +### The window a counter edit reaches nothing through -One window books an adjustment that reaches nothing: a gas-counter edit made at the `step_end` of a frame's *terminating* opcode. -revm's inspected loop runs `step_end` after the instruction that set the frame's action, and the action carries its own snapshot of the gas, so an edit there moves the interpreter's counter — which `MegaETH`'s tail settlement reads — and never the result the caller is handed. -The shim books it as conjured gas anyway, and the conservation law then over-counts by exactly that amount; `tools/eest-sweep`'s chaos mode reproduces it, and `--chaos-skip-terminal-counter-edits` is the switch that excludes it. -The same window exists at the top-of-loop `step` when the bytecode has already ended, which is reachable only when an action was set before the loop began. +A gas-counter edit made while the interpreter is already holding a `Return` action is written into an object nobody reads again: revm's inspected loop runs `step_end` after the instruction that set the action, and the action carries its own snapshot of the gas, which is what becomes the frame's result. +The shim books nothing for such an edit and still shifts the settlement baseline for it — `MegaETH`'s tail settlement reads the counter after the action is set, so without the shift the edit would read as work the frame performed. +The predicate is the pending action's variant, not "the loop is ending": a `NewFrame` action ends the loop too, and that frame resumes on exactly this counter. ### Rules for changing this @@ -78,6 +80,8 @@ The same window exists at the top-of-loop `step` when the bytecode has already e The gas an intercepting callback puts into a synthetic outcome travels through that same lane. - **Keep every rewrite out of a block.** Supporting a rewrite is not the same as admitting one: the canonical block-execution path refuses a transaction whose ledger is non-zero, in release builds as well as debug, because an inspector is one node's configuration and its edits reach the receipt. + That is why a rewrite which moves no gas still has to be booked — on `InspectorLedger::interventions` — or the guard admits it. + An EVM an embedder drives itself is deliberately not covered: it produces no block, so there is nothing for two nodes to disagree about. See `tests/block_executor/inspector_guard.rs`. ## WHERE TO LOOK diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index f8f28132..4a977606 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -53,7 +53,7 @@ use mega_evm::revm::{ handler::FrameResult, inspector::Inspector, interpreter::{ - interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, + interpreter_types::{Jumps, MemoryTr, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, Interpreter, InterpreterResult, InterpreterTypes, }, @@ -206,16 +206,6 @@ impl ChaosShape { }) } - /// Whether this shape writes to a live interpreter's gas counter. - const fn is_counter_edit(self) -> bool { - matches!(self, Self::InjectGas | Self::DrainGas) - } - - /// Whether this shape rewrites a frame result's classification. - const fn is_reclassification(self) -> bool { - matches!(self, Self::FailFrame | Self::ReviveCall) - } - /// Stable label, for reports. pub const fn label(self) -> &'static str { match self { @@ -263,7 +253,7 @@ const RESULT_SHAPES: [ChaosShape; 5] = [ /// Which mutations a chaos run is allowed to make. /// -/// Both knobs exist for triage rather than for the sweep's normal operation: a flagged vector is +/// The knob exists for triage rather than for the sweep's normal operation: a flagged vector is /// re-run with the filter narrowed until the smallest set of shapes that still reproduces it is /// found, which is the difference between "chaos broke something" and a defect report. See /// [`ChaosInspector::new`] for what narrowing does and does not preserve. @@ -271,26 +261,11 @@ const RESULT_SHAPES: [ChaosShape; 5] = [ pub struct ShapeFilter { /// Bitmask over [`ChaosShape::ALL`], by index. allowed: u16, - /// Whether a gas-counter edit may land at the `step_end` of a frame's *terminating* opcode. - /// - /// That callback is the one place where the interpreter's counter has already been copied into - /// the frame's action — revm's loop runs `step_end` after the instruction that set the action, - /// and the action carries its own snapshot of the gas — so an edit there changes the counter - /// `MegaETH`'s tail settlement reads, and nothing the caller ever sees. Turning it off narrows - /// the sweep to edits that actually move a frame's budget. - terminal_counter_edits: bool, - /// Whether a classification rewrite may land on a *precompile's* result. - /// - /// A precompile is answered inside the frame init and never becomes a child frame, so the - /// executed / destroyed split of its forwarded envelope is booked at its own recording site — - /// before any callback sees the synthetic result it produced. Turning it off narrows the sweep - /// to results whose split is decided after the last rewrite. - precompile_reclassification: bool, } impl Default for ShapeFilter { fn default() -> Self { - Self { allowed: u16::MAX, terminal_counter_edits: true, precompile_reclassification: true } + Self { allowed: u16::MAX } } } @@ -301,29 +276,7 @@ impl ShapeFilter { for shape in shapes { allowed |= 1 << Self::index(*shape); } - Self { allowed, ..Self::default() } - } - - /// This filter, with gas-counter edits at a terminating `step_end` turned off. - pub const fn without_terminal_counter_edits(mut self) -> Self { - self.terminal_counter_edits = false; - self - } - - /// This filter, with classification rewrites of a precompile's result turned off. - pub const fn without_precompile_reclassification(mut self) -> Self { - self.precompile_reclassification = false; - self - } - - /// Whether gas-counter edits at a terminating `step_end` are allowed. - pub const fn allows_terminal_counter_edits(&self) -> bool { - self.terminal_counter_edits - } - - /// Whether classification rewrites of a precompile's result are allowed. - pub const fn allows_precompile_reclassification(&self) -> bool { - self.precompile_reclassification + Self { allowed } } /// Whether `shape` may be drawn. @@ -333,9 +286,7 @@ impl ShapeFilter { /// Whether this filter allows every shape. pub fn is_complete(&self) -> bool { - self.terminal_counter_edits && - self.precompile_reclassification && - ChaosShape::ALL.into_iter().all(|s| self.allows(s)) + ChaosShape::ALL.into_iter().all(|s| self.allows(s)) } const fn index(shape: ChaosShape) -> u16 { @@ -613,13 +564,6 @@ impl ChaosInspector { outcome } - /// Whether the filter withholds this shape from a precompile's result. - fn refuses_reclassification(&self, shape: ChaosShape, is_precompile: bool) -> bool { - is_precompile && - shape.is_reclassification() && - !self.filter.allows_precompile_reclassification() - } - /// Applies a result-facing shape to a finished frame's result. /// /// `is_creation` withholds the one shape the shim refuses: a failed contract creation rewritten @@ -716,14 +660,9 @@ impl Inspector for ChaosInspe } fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) else { return }; - if shape.is_counter_edit() && - !self.filter.allows_terminal_counter_edits() && - interp.bytecode.is_end() - { - return; + if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { + self.hit_interpreter(interp, context, shape, entropy); } - self.hit_interpreter(interp, context, shape, entropy); } fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, _log: Log) { @@ -760,11 +699,9 @@ impl Inspector for ChaosInspe } fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { - let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; - if self.refuses_reclassification(shape, outcome.was_precompile_called) { - return; + if let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) { + self.hit_result(&mut outcome.result, false, shape, entropy); } - self.hit_result(&mut outcome.result, false, shape, entropy); } fn create_end( @@ -785,10 +722,6 @@ impl Inspector for ChaosInspe frame_result: &mut FrameResult, ) { let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; - let precompile = matches!(frame_result, FrameResult::Call(o) if o.was_precompile_called); - if self.refuses_reclassification(shape, precompile) { - return; - } let is_creation = matches!(frame_result, FrameResult::Create(_)); self.hit_result(frame_result.interpreter_result_mut(), is_creation, shape, entropy); } diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index a3c0b6b6..416ee65a 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -23,17 +23,6 @@ const INNER: &str = "0x3000000000000000000000000000000000000003"; /// The single transaction vector these hand-built fixtures declare. const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; -/// The filter every test here runs with: everything except the two windows the corpus sweep -/// currently reports as open findings. -/// -/// Those two windows are a statement about `MegaETH`'s accounting, not about this mode, and the -/// sweep is where they are measured. A tool test that tripped them would be testing the product, -/// and would go red for a reason that has nothing to do with what it is asserting. When they are -/// closed, this becomes `ShapeFilter::default()`. -fn tool_filter() -> ShapeFilter { - ShapeFilter::default().without_terminal_counter_edits().without_precompile_reclassification() -} - /// `SSTORE(1, 1); CALL(0x2710 gas, INNER, no value, no args, no return); POP; LOG0(0, 0); STOP`. /// /// One of everything a callback can be handed: a storage write, a child frame, a log, and enough @@ -101,8 +90,8 @@ fn mutations(seed: u64, filter: ShapeFilter) -> Vec<(String, u32)> { /// reproduction if re-running it reproduces. #[test] fn test_a_seed_reproduces_its_own_run() { - let first = mutations(0xC0FFEE, tool_filter()); - let second = mutations(0xC0FFEE, tool_filter()); + let first = mutations(0xC0FFEE, ShapeFilter::default()); + let second = mutations(0xC0FFEE, ShapeFilter::default()); assert!(!first.is_empty(), "the fixture must reach enough callbacks to mutate something"); assert_eq!(first, second, "the same seed must produce the same mutations"); } @@ -114,7 +103,7 @@ fn test_a_seed_reproduces_its_own_run() { #[test] fn test_different_seeds_produce_different_runs() { let seeds = [1u64, 2, 3, 4, 5, 6, 7, 8]; - let runs: Vec<_> = seeds.iter().map(|s| mutations(*s, tool_filter())).collect(); + let runs: Vec<_> = seeds.iter().map(|s| mutations(*s, ShapeFilter::default())).collect(); assert!( runs.iter().any(|run| *run != runs[0]), "eight seeds that all mutate identically means the seed reaches nothing: {runs:?}", @@ -145,9 +134,9 @@ fn test_a_vector_seed_separates_every_part_of_the_identity() { /// remain are applied at the same callbacks, so a flagged mutation is still there to be found. #[test] fn test_narrowing_the_filter_keeps_the_surviving_mutations() { - let full = mutations(0xC0FFEE, tool_filter()); + let full = mutations(0xC0FFEE, ShapeFilter::default()); let only = [ChaosShape::InjectGas, ChaosShape::DrainGas]; - let narrowed = mutations(0xC0FFEE, ShapeFilter::only(&only).without_terminal_counter_edits()); + let narrowed = mutations(0xC0FFEE, ShapeFilter::only(&only)); let kept: Vec<_> = only.iter().map(|s| s.label()).collect(); assert!(!narrowed.is_empty(), "the narrowed run must still mutate something"); @@ -199,7 +188,7 @@ fn test_the_control_inspector_changes_nothing() { /// A vector the rewriting run leaves executable comes back `Pass`, with mutations to show for it. #[test] fn test_a_mutated_vector_passes_with_mutations_recorded() { - let verdict = chaos_unit(&unit(), VECTOR_0, &SpecName::Rex7, 0xC0FFEE, tool_filter()); + let verdict = chaos_unit(&unit(), VECTOR_0, &SpecName::Rex7, 0xC0FFEE, ShapeFilter::default()); assert_eq!(verdict.class, ChaosClass::Pass, "{:?}", verdict.detail); assert!(verdict.applied.total() > 0, "the fixture must be mutated: {:?}", verdict.applied); assert!(verdict.applied.callbacks > 0, "and the callbacks must be counted"); @@ -243,7 +232,7 @@ fn test_a_sweep_that_mutated_passes() { ChaosRunConfig { spec: SpecName::Rex7, seed: 1, - filter: tool_filter(), + filter: ShapeFilter::default(), single_thread: true, progress: false, }, diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 50e0ac75..eb0d8539 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -114,23 +114,6 @@ pub struct Cmd { /// unspent on rejected draws, so a narrowed run can reach further into a transaction. #[arg(long, value_name = "SHAPES", value_delimiter = ',', requires = "chaos_seed")] chaos_shapes: Vec, - /// Make no gas-counter edit at the `step_end` of a frame's terminating opcode. - /// - /// That is the one callback where the interpreter's counter has already been copied into the - /// frame's action — revm runs `step_end` after the instruction that set the action, and the - /// action carries its own snapshot — so an edit there reaches the counter `MegaETH`'s tail - /// settlement reads and nothing the caller ever sees. The other half of the same triage knob - /// as `--chaos-shapes`. - #[arg(long, requires = "chaos_seed")] - chaos_skip_terminal_counter_edits: bool, - /// Rewrite no precompile result's classification. - /// - /// A precompile is answered inside the frame init and never becomes a child frame, so the - /// executed / destroyed split of its forwarded envelope is booked at its own recording site, - /// before any callback sees the synthetic result. The third triage knob, alongside - /// `--chaos-shapes` and `--chaos-skip-terminal-counter-edits`. - #[arg(long, requires = "chaos_seed")] - chaos_skip_precompile_reclassification: bool, } impl Cmd { @@ -361,31 +344,22 @@ impl Cmd { }) } - /// Build the chaos run's shape filter from `--chaos-shapes` and - /// `--chaos-skip-terminal-counter-edits`. + /// Build the chaos run's shape filter from `--chaos-shapes`. fn resolve_chaos_filter(&self) -> Result { - let mut filter = if self.chaos_shapes.is_empty() { - ShapeFilter::default() - } else { - let shapes = self - .chaos_shapes - .iter() - .map(|label| ChaosShape::parse(label)) - .collect::, _>>() - .map_err(|detail| TestError { - name: "--chaos-shapes".to_string(), - path: String::new(), - kind: TestErrorKind::FixtureError(detail), - })?; - ShapeFilter::only(&shapes) - }; - if self.chaos_skip_terminal_counter_edits { - filter = filter.without_terminal_counter_edits(); - } - if self.chaos_skip_precompile_reclassification { - filter = filter.without_precompile_reclassification(); + if self.chaos_shapes.is_empty() { + return Ok(ShapeFilter::default()); } - Ok(filter) + let shapes = self + .chaos_shapes + .iter() + .map(|label| ChaosShape::parse(label)) + .collect::, _>>() + .map_err(|detail| TestError { + name: "--chaos-shapes".to_string(), + path: String::new(), + kind: TestErrorKind::FixtureError(detail), + })?; + Ok(ShapeFilter::only(&shapes)) } /// Sweep the corpus under a deterministic rewriting inspector (see `--chaos-seed`). @@ -676,18 +650,12 @@ fn chaos_report_json( /// What a chaos run's shape filter allows, as one line. fn chaos_filter_label(filter: ShapeFilter) -> String { - let mut parts: Vec = ChaosShape::ALL + ChaosShape::ALL .into_iter() .filter(|shape| filter.allows(*shape)) .map(|shape| shape.label().to_string()) - .collect(); - if !filter.allows_terminal_counter_edits() { - parts.push("no-terminal-counter-edits".to_string()); - } - if !filter.allows_precompile_reclassification() { - parts.push("no-precompile-reclassification".to_string()); - } - parts.join(",") + .collect::>() + .join(",") } /// The machine-readable form of [`print_diff_tally`], for `--diff-report`. From 6eecfb6c79d1422a9449296aef47fcf036979304 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 16:14:03 +0800 Subject: [PATCH 121/208] docs(eest-sweep): drop the two retired chaos knobs from the runner's help The sweep script and its README still advertised `--chaos-arg --chaos-skip-*`, which the state-test binary no longer accepts. Also turns the anchor module's gas-limit premise into a compile-time assertion rather than a test, which is what clippy asks for and a stronger statement anyway. --- .../tests/rex7/inspector_settlement_window.rs | 22 +++++++------------ tools/eest-sweep/README.md | 4 +--- tools/eest-sweep/run.sh | 2 +- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index 0597e8c4..381e6faf 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -9,9 +9,9 @@ //! instruction that produced the frame's action, and that action carries its own copy of the gas //! counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail //! settlement measures work against and nothing the caller will ever see, so it must move the -//! settlement baseline and must not move the ledger. The two neighbouring windows — a step_end in -//! mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of that -//! rule: the frame resumes on the edited counter in both, so both are booked. +//! settlement baseline and must not move the ledger. The two neighbouring windows — a `step_end` +//! in mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of +//! that rule: the frame resumes on the edited counter in both, so both are booked. //! //! - **A precompile's classification.** A precompile is answered inside the frame init and never //! becomes a child frame, so its recording site is the only place that knows the forwarded @@ -51,6 +51,11 @@ const INJECT: u64 = 1_000; /// limit and well inside the default compute budget, so the forwarded envelope is exactly this. const FORWARDED: u64 = 1_000_000; +/// The transaction gas limit is not what binds any fixture here — pinned at compile time, so a +/// change to the shared limit cannot silently turn a destroyed-remainder case into an +/// out-of-gas one. +const _: () = assert!(DEFAULT_TX_GAS_LIMIT > 10 * FORWARDED); + /// The identity precompile. const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); /// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. @@ -380,14 +385,3 @@ fn test_a_priced_precompile_failure_rewritten_into_a_success_conjures_its_fee() "the fee the caller reclaimed is gas the transaction was never charged for", ); } - -/// The transaction gas limit is not what binds any of these fixtures — stated once, so a future -/// change to the shared limit cannot silently turn a destroyed-remainder case into an -/// out-of-gas one. -#[test] -fn test_the_fixtures_are_not_bound_by_the_transaction_gas_limit() { - assert!( - DEFAULT_TX_GAS_LIMIT > 10 * FORWARDED, - "the forwarded envelope must be a small part of the transaction's own", - ); -} diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md index a5135aa8..9933a5bb 100644 --- a/tools/eest-sweep/README.md +++ b/tools/eest-sweep/README.md @@ -89,7 +89,7 @@ The shim refuses that shape and asserts on it, so including it would make the de ### Narrowing a flagged vector -Three knobs, passed through with `--chaos-arg`: +One knob, passed through with `--chaos-arg`: ```bash tools/eest-sweep/run.sh --mode chaos \ @@ -97,8 +97,6 @@ tools/eest-sweep/run.sh --mode chaos \ ``` - `--chaos-shapes LIST` restricts the pool to the named shapes. Narrowing does not reshuffle the decision stream, so each surviving mutation stays where the full run put it; it does leave the mutation budget unspent on rejected draws, so a narrowed run can reach further into a transaction. -- `--chaos-skip-terminal-counter-edits` makes no gas-counter edit at the `step_end` of a frame's terminating opcode, where the interpreter's counter has already been copied into the frame's action. -- `--chaos-skip-precompile-reclassification` rewrites no precompile result's classification, whose executed / destroyed split is booked at the precompile's own recording site before any callback sees it. ## Options diff --git a/tools/eest-sweep/run.sh b/tools/eest-sweep/run.sh index a20a37e0..07d8a513 100755 --- a/tools/eest-sweep/run.sh +++ b/tools/eest-sweep/run.sh @@ -27,7 +27,7 @@ # derived from this and the vector's identity, so a flagged vector # reproduces exactly. # --chaos-arg ARG Extra argument passed through to the chaos run; repeatable. Used to -# narrow a flagged vector (`--chaos-shapes`, `--chaos-skip-*`). +# narrow a flagged vector (`--chaos-shapes`). # --corpus-dir DIR Use an already-unpacked `state_tests` tree instead of downloading. # --cache-dir DIR Where to keep the downloaded archive (default: .eest-cache). # --report-dir DIR Where to write the report and log (default: .eest-report). From a781ddb07b70524c2da6d87a2a6a531e8d73c3c0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 16:37:12 +0800 Subject: [PATCH 122/208] refactor(evm): key the precompile settlement on the staging, not on the flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two self-review findings on the commit before it. The cross-check asserted that a staged envelope and `CallOutcome::was_precompile_called` always agree. They need not: that flag sits on a result `call_end` is handed by mutable reference, so an inspector can set or clear it, and a debug assertion an untrusted callback can trip is a liability rather than a check. The accounting already keys on the staged slot — written before any callback runs, read once — so the flag is now out of the settlement path entirely, and the doc says why. The check the assertion was really after was "did an arm record work and forget to stage it". That is now structural instead: every arm yields the work it performed and the single staging call is hoisted out of the match, so the omission cannot be written. The record calls stay exactly where they were, so no arm's latch timing moves. --- crates/mega-evm/src/evm/precompiles.rs | 22 +++++++++++++++------- crates/mega-evm/src/limit/limit.rs | 11 +++++------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/mega-evm/src/evm/precompiles.rs b/crates/mega-evm/src/evm/precompiles.rs index bcfa6d0b..3d14d9e2 100644 --- a/crates/mega-evm/src/evm/precompiles.rs +++ b/crates/mega-evm/src/evm/precompiles.rs @@ -421,10 +421,10 @@ impl PrecompileProvider PrecompileProvider PrecompileProvider PrecompileProvider, ) { - debug_assert_eq!( - staged_precompile.is_some(), - self.checkpoint.rex7_enabled() && - matches!(result, FrameResult::Call(outcome) if outcome.was_precompile_called), - "every REX7 precompile call stages an envelope, and nothing else does", - ); if !self.checkpoint.rex7_enabled() || self.limit_exceeded() { return; } From df30c5dc8d4e9e98b93f353f2b418851e1883cfa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:01:56 +0800 Subject: [PATCH 123/208] test(rex7): pin the pending action as a gas-carrying surface --- .../tests/rex7/inspector_settlement_window.rs | 209 +++++++++++++++++- 1 file changed, 205 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index 381e6faf..14df7b24 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -25,7 +25,7 @@ //! tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. use crate::common::{ - transact, transact_inspected, Outcome, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, + transact, transact_inspected, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, }; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ @@ -34,11 +34,11 @@ use mega_evm::{ EvmTxRuntimeLimits, InspectorLedger, MegaSpecId, }; use revm::{ - bytecode::opcode::{CALL, POP, STOP}, + bytecode::opcode::{CALL, INVALID, POP, STOP}, context::ContextTr, interpreter::{ - interpreter_types::LoopControl, CallInputs, CallOutcome, InstructionResult, Interpreter, - InterpreterAction, InterpreterTypes, + interpreter_types::LoopControl, CallInputs, CallOutcome, FrameInput, InstructionResult, + Interpreter, InterpreterAction, InterpreterTypes, }, Inspector, }; @@ -385,3 +385,204 @@ fn test_a_priced_precompile_failure_rewritten_into_a_success_conjures_its_fee() "the fee the caller reclaimed is gas the transaction was never charged for", ); } + +// --- C: the pending action itself --------------------------------------------------------------- + +/// Gas an action edit moves. +const ACTION_DELTA: u64 = 700; + +/// Reaches past the interpreter's gas counter and into the action the interpreter is holding, once. +/// +/// The counter and the action are two different objects at exactly one moment — after a +/// terminating or suspending instruction has run and before the loop hands the action on — and +/// this is the inspector that edits the second one. +#[derive(Debug)] +struct ActionEditor { + window: Window, + /// Positive raises the gas the action carries, negative lowers it. + delta: i64, + /// Fire only on an action whose classification is (or is not) an exceptional halt. + halting: bool, + fired: u32, +} + +impl ActionEditor { + fn raise(window: Window) -> Self { + Self { window, delta: ACTION_DELTA as i64, halting: false, fired: 0 } + } + + fn lower(window: Window) -> Self { + Self { window, delta: -(ACTION_DELTA as i64), halting: false, fired: 0 } + } + + fn on_halt() -> Self { + Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } + } + + fn move_gas(&self, gas: &mut revm::interpreter::Gas) { + if self.delta >= 0 { + gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave the action enough gas for the removal to land", + ); + } + } +} + +impl Inspector for ActionEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + match interp.bytecode.action() { + Some(InterpreterAction::Return(result)) => { + if result.result.is_ok_or_revert() == self.halting { + return; + } + let mut gas = result.gas; + self.move_gas(&mut gas); + result.gas = gas; + } + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { + inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); + } + _ => return, + } + self.fired += 1; + } +} + +/// A `CALL` into a callee that halts on an invalid opcode, its failure flag popped, then `STOP`. +/// +/// The inner frame reaches a terminating `step_end` holding a *halting* `Return` action, which is +/// the branch of the settlement where an edit to the action moves nothing. +fn halting_callee_code() -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(FORWARDED) + .append(CALL) + .append(POP) + .append(STOP) + .build() +} + +fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { + db(code).account_code(CALLEE, callee) +} + +/// Gas written into a returning frame's pending action is gas the caller really reclaims, so it +/// has to be booked — the frame's classification is what says so, and the classification is only +/// known at the frame's settlement point. +#[test] +fn test_raising_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { result: i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + "an edit to the action a returning frame hands back is an edit to the envelope", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the transaction really did spend less, which is why the ledger has to carry it", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the edit is not work: the frame performed exactly what it performed uninspected", + ); +} + +/// The same edit in the other direction. +#[test] +fn test_lowering_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); + let mut inspector = ActionEditor::lower(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { result: -i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + "gas taken out of the action is gas the caller never gets back", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent + ACTION_DELTA, + "the transaction really did spend more", + ); +} + +/// The classification branch: a halting frame hands nothing back, so an edit to the gas its action +/// carries moves nothing and must not be booked — and the remainder it destroys is the EVM's own +/// number, not the edited one. +#[test] +fn test_editing_a_halting_frames_pending_action_moves_nothing() { + let callee = BytecodeBuilder::default().append(INVALID).build(); + let plain = transact( + MegaSpecId::REX7, + db_with_callee(halting_callee_code(), callee.clone()), + limits(), + ); + let mut inspector = ActionEditor::on_halt(); + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(halting_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger::default(), + "a halting frame hands its remainder to nobody, so an edit to it reaches nobody", + ); + assert_eq!( + edited.destroyed, plain.destroyed, + "the destroyed remainder is the EVM's own, not the one the inspector wrote", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "and the envelope is unmoved"); +} + +/// The other action variant: gas written into a pending `NewFrame` action is the envelope a child +/// frame is about to be built with, which the caller was never debited for. +#[test] +fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { + let plain = transact(MegaSpecId::REX7, db(suspending_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Suspending); + let edited = + transact_inspected(MegaSpecId::REX7, db(suspending_code()), limits(), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { env: i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + "the child's budget grew by gas the caller's CALL never forwarded", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the child hands the extra budget straight back, so the transaction spends less", + ); +} From 17d2e88111f7ee8e210ff32281e125da0d2c6753 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:07:52 +0800 Subject: [PATCH 124/208] feat(evm): measure the gas an inspector writes into a pending action --- crates/mega-evm/src/evm/inspector.rs | 260 +++++++++++++++++++++------ crates/mega-evm/src/limit/limit.rs | 66 +++++++ 2 files changed, 273 insertions(+), 53 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index e11821b7..1d94dacf 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -91,44 +91,181 @@ impl MeasuredInspector { } } -/// Whether an edit to this interpreter's gas counter can still reach the transaction's envelope. +/// Where the gas an interpreter is holding will go next, read off the action it is holding. /// -/// It cannot exactly when the interpreter is holding a `Return` action. revm's inspected loop runs -/// the terminating instruction first and the callback after it, and that instruction has already -/// copied the counter into the action it set — the action is what becomes the frame's result and -/// what the caller reclaims from. Whatever the callback writes into the counter afterwards is -/// written into an object nobody will read again. +/// This is the one thing that decides how gas measured at a live-interpreter callback is booked, +/// for both of the objects such a callback can write gas into — the interpreter's own counter and +/// the pending action. The three variants are the three places a frame's budget can be sitting +/// when a callback runs. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ActionLane { + /// No action pending: the frame carries straight on, so what it holds is its counter. + Counter, + /// A `NewFrame` action: the frame suspends, and the action carries the envelope a child is + /// about to be built with. The frame itself resumes on its counter afterwards. + Envelope, + /// A `Return` action: the frame is over, and the action carries what its caller reclaims. + Result, +} + +impl ActionLane { + /// The lane an interpreter holding `action` is on. + #[inline] + const fn of(action: Option<&InterpreterAction>) -> Self { + match action { + None => Self::Counter, + Some(InterpreterAction::NewFrame(_)) => Self::Envelope, + Some(InterpreterAction::Return(_)) => Self::Result, + } + } + + /// Whether an edit to this interpreter's gas counter can still reach the transaction's + /// envelope. + /// + /// It cannot exactly on [`Result`](Self::Result). revm's inspected loop runs the terminating + /// instruction first and the callback after it, and that instruction has already copied the + /// counter into the action it set — the action is what becomes the frame's result and what the + /// caller reclaims from. Whatever the callback writes into the counter afterwards is written + /// into an object nobody will read again. + /// + /// The two neighbouring shapes are live and must stay booked. With no action pending the frame + /// carries straight on; with a `NewFrame` action pending it suspends into a child and then + /// resumes on this very counter. "The loop is about to break" is therefore not the question — + /// revm breaks out of the instruction loop in both the suspending and the terminating case — + /// and a rule phrased that way would stop booking edits that really do move a frame's budget. + /// + /// Read *after* the callback returns, because the question is about the counter the callback + /// left behind: an inspector that sets or clears an action has changed what the EVM does next, + /// and the answer has to follow it. + /// + /// # Why this decides booking and not measurement + /// + /// Only the ledger is gated on it. `MegaETH`'s own tail settlement measures a frame's work as + /// a drop in this same counter and does read it after the action is set, so the settlement + /// baseline has to shift for a dead-window edit exactly as it does for a live one — otherwise + /// gas the inspector wrote in would read as work the frame performed, which is the opposite + /// error. + /// + /// # The gas the counter no longer speaks for + /// + /// What a dead-window counter edit cannot reach, an edit to the action itself can — and + /// [`measure_pending_action`] measures exactly that, against the same counter reading, so the + /// two together account for every unit of gas the frame holds. See [`held`] for the identity + /// they split. + #[inline] + const fn counter_reaches_envelope(self) -> bool { + !matches!(self, Self::Result) + } +} + +/// The gas a frame and its pending continuation hold, given the action it is carrying and its own +/// counter. /// -/// The two neighbouring shapes are live and must stay booked. With no action pending the frame -/// carries straight on; with a `NewFrame` action pending it suspends into a child and then resumes -/// on this very counter. "The loop is about to break" is therefore not the question — revm breaks -/// out of the instruction loop in both the suspending and the terminating case — and a rule phrased -/// that way would stop booking edits that really do move a frame's budget. +/// This is the quantity the two live-interpreter lanes partition between them: /// -/// Read *after* the callback returns, because the question is about the counter the callback left -/// behind: an inspector that sets or clears an action has changed what the EVM does next, and the -/// answer has to follow it. +/// ```text +/// held(None, counter) = counter +/// held(NewFrame(f), counter) = counter + f.gas_limit +/// held(Return(r), counter) = r.gas.remaining() +/// ``` +/// +/// A frame with no action pending will spend its counter. A suspending frame will spend its +/// counter when it resumes and has additionally handed the child's envelope on. A terminating +/// frame will spend nothing more — the action's own copy is what its caller reclaims, and the +/// counter is dead. +/// +/// Both readings [`measure_pending_action`] takes use the counter *the EVM left behind*, so the +/// counter cancels out of the difference wherever it appears on both sides. What is left is +/// exactly the part of the movement that is not already on the counter lane, whatever the callback +/// did to the action's shape. +#[inline] +fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { + match action { + None => i128::from(counter), + Some(InterpreterAction::NewFrame(frame_input)) => { + i128::from(counter) + frame_input_gas_limit(frame_input).map_or(0, i128::from) + } + Some(InterpreterAction::Return(result)) => i128::from(result.gas.remaining()), + } +} + +/// What a live-interpreter callback did to the interpreter's pending action. +#[derive(Clone, Copy, Debug)] +struct ActionChange { + /// Gas the callback moved through the action, over and above anything it did to the counter. + gas: i128, + /// Whether the action came back describing something other than what the EVM decided to do. + rewritten: bool, + /// Where that gas is now sitting, which is what decides how it is booked. + lane: ActionLane, +} + +/// Measures what a callback did to the pending action, against the counter the EVM left behind. /// -/// # Why this decides booking and not measurement +/// The EVM does not execute inside a callback, so the action is either the one the last +/// instruction set or one the callback wrote — and the difference between the two readings of +/// [`held`] is what the callback moved. Taking both readings at the *pre-callback* counter is +/// what keeps this lane and the counter lane from overlapping: the counter's own movement is +/// booked by [`record_inspector_gas_adjustment`](crate::AdditionalLimit:: +/// record_inspector_gas_adjustment) exactly when [`ActionLane::counter_reaches_envelope`] says the +/// EVM will read it again, and it cancels out of this difference in precisely the cases where it +/// does. +#[inline] +fn measure_pending_action( + before: Option, + after: Option<&InterpreterAction>, + counter: u64, +) -> ActionChange { + let gas = held(after, counter) - held(before.as_ref(), counter); + ActionChange { gas, rewritten: action_rewritten(before, after), lane: ActionLane::of(after) } +} + +/// Whether a callback left behind an action describing something other than what the EVM decided. /// -/// Only the ledger is gated on it. `MegaETH`'s own tail settlement measures a frame's work as a -/// drop in this same counter and does read it after the action is set, so the settlement baseline -/// has to shift for a dead-window edit exactly as it does for a live one — otherwise gas the -/// inspector wrote in would read as work the frame performed, which is the opposite error. +/// Gas is excluded, exactly as it is at every other boundary: it travels on the lanes +/// [`measure_pending_action`] routes it to, and counting it here as well would report one rewrite +/// twice. A callback that installed, removed or swapped an action has rewritten what the EVM does +/// next as thoroughly as it is possible to, so every shape change counts. +#[inline] +fn action_rewritten(before: Option, after: Option<&InterpreterAction>) -> bool { + match (before, after) { + (None, None) => false, + (Some(InterpreterAction::Return(before)), Some(InterpreterAction::Return(after))) => { + result_rewritten((before.result, &before.output), after) + } + (Some(InterpreterAction::NewFrame(before)), Some(InterpreterAction::NewFrame(after))) => { + frame_input_rewritten(before, after) + } + _ => true, + } +} + +/// Books what a callback did to the interpreter's pending action. /// -/// # What it does not cover +/// The gas goes to the lane the action the callback *left behind* names, because that is where the +/// number now lives and therefore what decides when it can still be settled: /// -/// The pending action itself. A callback holding a live interpreter can reach the action through -/// `LoopControl` and edit the gas inside it, which does move the envelope. Measuring that at the -/// callback boundary would be unsound rather than merely incomplete: whether such an edit moves -/// anything depends on the frame's *final* classification, which no callback here knows — a -/// returning frame hands its remainder back and a halting one does not — so it belongs to the -/// frame's settlement point, which books edits made at the one callback it can see -/// ([`InspectorLedger::result`](crate::InspectorLedger::result)). Extending that lane to cover -/// this one is a per-frame snapshot this shim deliberately does not carry. +/// - [`ActionLane::Result`] is staged for the frame's settlement point, like an edit made at the +/// frame's last callback — whether it moves anything depends on the classification the caller +/// ends up seeing, which no callback here knows; +/// - [`ActionLane::Envelope`] is staged for the frame-start callback of the child the action is +/// about to build, which is where an envelope edit is booked from; +/// - [`ActionLane::Counter`] is booked on the spot, on the interpreter lane: with no action left, +/// the frame carries on spending what it holds, which is exactly what a counter edit does. #[inline] -fn counter_reaches_envelope(interp: &mut Interpreter) -> bool { - !matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) +fn book_pending_action( + context: &MegaContext, + change: ActionChange, +) { + if change.gas != 0 { + let mut limit = context.additional_limit.borrow_mut(); + match change.lane { + ActionLane::Result => limit.stage_inspector_action_result_adjustment(change.gas), + ActionLane::Envelope => limit.stage_inspector_action_env_adjustment(change.gas), + ActionLane::Counter => limit.record_inspector_action_counter_adjustment(change.gas), + } + } + book_intervention(context, change.rewritten); } /// The gas limit a frame input carries, for the two variants that have one. @@ -141,13 +278,22 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { } } -/// Books what a callback did to a frame's envelope, if the edited inputs will actually reach a -/// frame. +/// Books what a callback did to a frame's envelope, together with whatever an earlier callback +/// staged into the same envelope through the pending `NewFrame` action. /// /// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped /// entirely and the EVM never reads the inputs it edited, so the edit by itself moves nothing. Gas /// the inspector then puts into that synthetic outcome travels through the result lane, which this /// lane deliberately does not cover — see [`InspectorLedger::env`](crate::InspectorLedger::env). +/// +/// The staged amount is booked either way, and the asymmetry is not an oversight. An interception +/// discards inputs *this* callback edited a moment earlier, which is why that edit reaches +/// nothing. The staged amount was written by a different callback into the action the caller's +/// `CALL` / `CREATE` opcode had already produced — the caller's debit is behind it, `MegaETH`'s own +/// CALL settlement excluded the pre-edit amount from the caller's work, and a callback deciding +/// later to answer the frame itself cannot un-make that. It is simply the earliest of the two +/// edits to the one envelope, and the last thing to touch that envelope is what its holder is +/// sized from. #[inline] fn book_env_adjustment( context: &MegaContext, @@ -155,19 +301,15 @@ fn book_env_adjustment( after: Option, intercepted: bool, ) { - if intercepted { - return; - } - let (Some(before), Some(after)) = (before, after) else { - return; + let staged = context.additional_limit.borrow_mut().take_inspector_action_env_adjustment(); + let callback = match (intercepted, before, after) { + (false, Some(before), Some(after)) => i128::from(after) - i128::from(before), + _ => 0, }; - if before == after { + if staged + callback == 0 { return; } - context - .additional_limit - .borrow_mut() - .record_inspector_env_adjustment(i128::from(after) - i128::from(before)); + context.additional_limit.borrow_mut().record_inspector_env_adjustment(staged + callback); } /// Books one rewrite that changes what the execution *did* rather than what it cost — see @@ -305,40 +447,49 @@ where interp: &mut Interpreter, context: &mut MegaContext, ) { + let action = interp.bytecode.action().clone(); let before = interp.gas.remaining(); self.inner.initialize_interp(interp, context); - let reaches_envelope = counter_reaches_envelope(interp); + let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); + book_pending_action(context, change); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, - reaches_envelope, + change.lane.counter_reaches_envelope(), ); } #[inline] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + let action = interp.bytecode.action().clone(); let before = interp.gas.remaining(); self.inner.step(interp, context); - let reaches_envelope = counter_reaches_envelope(interp); + let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); + book_pending_action(context, change); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, - reaches_envelope, + change.lane.counter_reaches_envelope(), ); } - /// The callback that sits in the dead window: revm runs it after the instruction that set the - /// frame's action, so on a terminating opcode the counter it hands out has already been copied - /// into the result the caller will be given — see [`counter_reaches_envelope`]. + /// The one callback that runs with an action already pending: revm runs it after the + /// instruction that set the frame's action, so on a terminating opcode the counter it hands + /// out has already been copied into the result the caller will be given — see + /// [`ActionLane::counter_reaches_envelope`] — and the action holding that copy is reachable + /// through `LoopControl`. Both objects are measured, on the lanes + /// [`book_pending_action`] routes them to. #[inline] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { + let action = interp.bytecode.action().clone(); let before = interp.gas.remaining(); self.inner.step_end(interp, context); - let reaches_envelope = counter_reaches_envelope(interp); + let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); + book_pending_action(context, change); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, - reaches_envelope, + change.lane.counter_reaches_envelope(), ); } @@ -356,13 +507,16 @@ where context: &mut MegaContext, log: Log, ) { + let action = interpreter.bytecode.action().clone(); let before = interpreter.gas.remaining(); self.inner.log_full(interpreter, context, log); - let reaches_envelope = counter_reaches_envelope(interpreter); + let change = + measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); + book_pending_action(context, change); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interpreter.gas, before, - reaches_envelope, + change.lane.counter_reaches_envelope(), ); } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 2d975830..29332958 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -166,6 +166,24 @@ pub struct AdditionalLimit { /// to start in between. [`finalize_frame`](Self::finalize_frame) takes it unconditionally, so /// it cannot outlive the frame that staged it. staged_precompile: Option, + + /// Gas an inspector wrote into a *terminating* pending action, waiting for the frame that + /// action ends to reach its settlement point. + /// + /// Staged rather than booked for the same reason a frame result's edit is: the action becomes + /// the frame's result, and whether an edit to it moves anything depends on the classification + /// the caller ends up seeing. Only one frame can have one outstanding — a frame that has set + /// its terminating action starts no more children — and + /// [`finalize_frame`](Self::finalize_frame) takes it. + staged_action_result_gas: i128, + + /// Gas an inspector wrote into a *suspending* pending action, waiting for the frame-start + /// callback of the child it is about to build. + /// + /// That callback is the first point at which the edit can be told apart from an interception, + /// and it runs immediately after the action is handed on, with nothing in between that could + /// stage another one. + staged_action_env_gas: i128, } /// The usage of the additional limits. @@ -196,6 +214,8 @@ impl AdditionalLimit { checkpoint: checkpoint::CheckpointTracker::new(spec), inspector: inspector_ledger::InspectorLedger::default(), staged_precompile: None, + staged_action_result_gas: 0, + staged_action_env_gas: 0, } } } @@ -240,6 +260,8 @@ impl AdditionalLimit { self.checkpoint.reset(); self.inspector = inspector_ledger::InspectorLedger::default(); self.staged_precompile = None; + self.staged_action_result_gas = 0; + self.staged_action_env_gas = 0; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -578,6 +600,46 @@ impl AdditionalLimit { self.inspector.env += delta; } + /// Stages an adjustment an inspector made to the gas a *terminating* pending action carries. + /// + /// The action is the object that becomes the frame's result, so this is the same measurement + /// as an edit made at the frame's last callback, taken one step earlier — and it is settled at + /// the same place, [`finalize_frame`](Self::finalize_frame), for the same reason: whether the + /// edit moves anything at all depends on the classification the caller ends up seeing. + #[inline] + pub(crate) fn stage_inspector_action_result_adjustment(&mut self, delta: i128) { + self.staged_action_result_gas += delta; + } + + /// Stages an adjustment an inspector made to the gas a *suspending* pending action carries — + /// the envelope the child frame is about to be built with. + /// + /// Same lane as an edit made at the frame-start callback, taken one step earlier. It is staged + /// rather than booked on the spot only so that the booking happens at a point that can see the + /// whole picture; [`take_inspector_action_env_adjustment`](Self:: + /// take_inspector_action_env_adjustment) is where it lands. + #[inline] + pub(crate) fn stage_inspector_action_env_adjustment(&mut self, delta: i128) { + self.staged_action_env_gas += delta; + } + + /// Takes the staged suspending-action adjustment, for the frame-start callback to book. + #[inline] + pub(crate) fn take_inspector_action_env_adjustment(&mut self) -> i128 { + core::mem::take(&mut self.staged_action_env_gas) + } + + /// Books an adjustment an inspector made to a pending action the same callback then removed, + /// leaving the frame to carry on from its own counter. + /// + /// With no action left there is nothing for the edit to travel in, so it lands where the + /// frame's remaining budget already lives — the same lane a counter edit takes, and for the + /// same reason: the frame will spend what it now holds. + #[inline] + pub(crate) fn record_inspector_action_counter_adjustment(&mut self, delta: i128) { + self.inspector.gas += delta; + } + /// Counts one rewrite the shim refused because its shape is forbidden — see /// [`InspectorLedger::rejected_rewrites`](inspector_ledger::InspectorLedger::rejected_rewrites). #[inline] @@ -1424,6 +1486,10 @@ impl AdditionalLimit { // Taken unconditionally, so a staged envelope can never outlive the frame that staged it. let staged_precompile = self.staged_precompile.take(); + // Everything an inspector wrote into this result, whether it wrote it into the frame's + // terminating action or into the result the action became. The two are the same number + // measured on either side of the classification, so they settle as one. + let inspector_gas_delta = inspector_gas_delta + core::mem::take(&mut self.staged_action_result_gas); // The gas the EVM itself left in this result. Every settlement below is defined against // it: the last callback's edit to the number is the inspector's, and the two are only the // same object on a frame no callback touched. From cd19e8d1edb632371dbed6a3f87588acfd5cd297 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:09:45 +0800 Subject: [PATCH 125/208] test(rex7): put the pending action in the cheat matrix --- .../tests/rex7/inspector_cheat_matrix.rs | 121 +++++++++++++++++- .../tests/rex7/inspector_settlement_window.rs | 22 ++-- 2 files changed, 125 insertions(+), 18 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 081b9d67..aedcfcca 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -46,9 +46,9 @@ use revm::{ context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr, JournalTr}, handler::{EvmTr, FrameResult}, interpreter::{ - interpreter_types::{Jumps, MemoryTr, StackTr}, + interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - Interpreter, InterpreterResult, InterpreterTypes, + Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, state::EvmState, Inspector, @@ -68,6 +68,8 @@ const DRAIN: u64 = 1_000; const ENVELOPE: u64 = 5_000; /// Gas a result cheat adds to, or removes from, a frame result's remaining gas. const RESULT: u64 = 2_000; +/// Gas an action cheat adds to, or removes from, the gas a pending `InterpreterAction` carries. +const ACTION: u64 = 1_500; /// Slot the top frame writes, last of all, so a cheat that fails the top frame is visible. const TOP_SLOT: u64 = 0x10; @@ -152,6 +154,15 @@ enum Shape { FailResult, /// Rewrite a failed frame result into a success. ReviveResult, + /// Raise the gas a pending `Return` action carries — the gas the frame it ends will hand back. + RaiseActionResultGas, + /// Lower it. + LowerActionResultGas, + /// Raise the `gas_limit` a pending `NewFrame` action carries — the envelope of the child the + /// frame is suspending into. + RaiseActionEnvelope, + /// Lower it. + LowerActionEnvelope, /// Edit the interpreter's stack or memory — the frame's working state, which the EVM reads /// back as operands and as data. EditStackOrMemory, @@ -160,7 +171,7 @@ enum Shape { } impl Shape { - const ALL: [Self; 12] = [ + const ALL: [Self; 16] = [ Self::InjectGas, Self::DrainGas, Self::RaiseEnvelope, @@ -171,9 +182,25 @@ impl Shape { Self::LowerResultGas, Self::FailResult, Self::ReviveResult, + Self::RaiseActionResultGas, + Self::LowerActionResultGas, + Self::RaiseActionEnvelope, + Self::LowerActionEnvelope, Self::EditStackOrMemory, Self::JournalWrite, ]; + + /// Whether this shape reaches through the interpreter's *pending action* rather than through + /// the interpreter itself. + const fn is_pending_action(self) -> bool { + matches!( + self, + Self::RaiseActionResultGas | + Self::LowerActionResultGas | + Self::RaiseActionEnvelope | + Self::LowerActionEnvelope + ) + } } /// Why a row × column pair cannot be covered, for every pair the matrix leaves out. @@ -207,6 +234,18 @@ fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { let input_facing = matches!(at, FrameStart | Call | Create); let result_facing = matches!(at, FrameEnd | CallEnd | CreateEnd); + if shape.is_pending_action() { + return match at { + StepEnd => None, + InitializeInterp | Step | LogFull => Some( + "no action is pending at this callback: revm's inspected loop breaks out as soon \ + as one is set, so `step` and `log_full` only ever run with none, and \ + `initialize_interp` runs before the loop on a fresh interpreter", + ), + _ => Some("no live interpreter is reachable from this callback"), + }; + } + match shape { InjectGas | DrainGas | EditStackOrMemory if !interpreter_facing => { Some("no live interpreter is reachable from this callback") @@ -302,6 +341,37 @@ impl Cheat { self.fired += 1; } + /// Applies a shape that reaches through the interpreter's *pending action* — the object the + /// terminating or suspending instruction just left behind, which carries its own copy of the + /// gas the frame is handing on. + /// + /// Leaves the action alone and does not count as fired when the pending action is not the + /// variant this shape targets, so the cheat lands on the first `step_end` that offers the + /// right one rather than on whichever comes first. + fn hit_pending_action(&mut self, interp: &mut Interpreter) { + match (self.shape, interp.bytecode.action()) { + (Shape::RaiseActionResultGas, Some(InterpreterAction::Return(result))) => { + result.gas.erase_cost(ACTION); + } + (Shape::LowerActionResultGas, Some(InterpreterAction::Return(result))) => { + assert!( + result.gas.record_regular_cost(ACTION), + "the fixture must leave the action enough gas for a {ACTION} gas removal", + ); + } + ( + Shape::RaiseActionEnvelope, + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))), + ) => inputs.gas_limit += ACTION, + ( + Shape::LowerActionEnvelope, + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))), + ) => inputs.gas_limit -= ACTION, + _ => return, + } + self.fired += 1; + } + /// Applies the one shape that reaches past the EVM entirely. fn hit_journal(&mut self, context: &mut CTX) { context.journal_mut().tstore(CONTRACT, U256::from(0xF00Du64), U256::from(1)); @@ -450,7 +520,16 @@ impl Inspector for Cheat { } fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - if !self.arm(At::StepEnd) || self.steps != self.step_at { + if !self.arm(At::StepEnd) { + return; + } + // The action shapes fire on the first `step_end` that offers the action variant they + // target, not on a fixed ordinal — only a handful of opcodes leave an action behind. + if self.shape.is_pending_action() { + self.hit_pending_action(interp); + return; + } + if self.steps != self.step_at { return; } match self.shape { @@ -916,6 +995,39 @@ fn matrix() -> Vec { ); } + // The pending action, which only `step_end` ever sees: revm's inspected loop breaks out the + // moment one is set, so it is the one callback that runs with an instruction's action already + // in place. Which lane the edit lands on is decided by the action's own variant — a `Return` + // action is what the frame hands back, a `NewFrame` action is what the child is built with. + push( + StepEnd, + RaiseActionResultGas, + Fixture::ReturningCallee, + ledger_result(i128::from(ACTION)), + state_all_committed, + ); + push( + StepEnd, + LowerActionResultGas, + Fixture::ReturningCallee, + ledger_result(-i128::from(ACTION)), + state_all_committed, + ); + push( + StepEnd, + RaiseActionEnvelope, + Fixture::ReturningCallee, + ledger_env(i128::from(ACTION)), + state_all_committed, + ); + push( + StepEnd, + LowerActionEnvelope, + Fixture::ReturningCallee, + ledger_env(-i128::from(ACTION)), + state_all_committed, + ); + // The three callbacks that are handed a frame's inputs before the frame is built. `create` // reaches the fixture's `CREATE`; the other two reach its inner `CALL`. for at in [FrameStart, Call, Create] { @@ -1092,6 +1204,7 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { (At::CreateEnd, Shape::FailResult, Fixture::ReturningCallee), (At::FrameEnd, Shape::ReviveResult, Fixture::RevertingCallee), (At::Step, Shape::JournalWrite, Fixture::ReturningCallee), + (At::StepEnd, Shape::RaiseActionResultGas, Fixture::ReturningCallee), ]; let mut plain: BTreeMap = BTreeMap::new(); diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index 14df7b24..2cb30dd4 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -418,17 +418,6 @@ impl ActionEditor { fn on_halt() -> Self { Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } } - - fn move_gas(&self, gas: &mut revm::interpreter::Gas) { - if self.delta >= 0 { - gas.erase_cost(self.delta.unsigned_abs()); - } else { - assert!( - gas.record_regular_cost(self.delta.unsigned_abs()), - "the fixture must leave the action enough gas for the removal to land", - ); - } - } } impl Inspector for ActionEditor { @@ -441,9 +430,14 @@ impl Inspector for ActionEditor { if result.result.is_ok_or_revert() == self.halting { return; } - let mut gas = result.gas; - self.move_gas(&mut gas); - result.gas = gas; + if self.delta >= 0 { + result.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + result.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave the action enough gas for the removal to land", + ); + } } Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); From 766b8ddb7978e506cf6ea266ddc06172066a81ef Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:19:24 +0800 Subject: [PATCH 126/208] feat(state-test): add the pending-action shapes to the chaos pool --- crates/mega-state-test/src/chaos.rs | 74 +++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 4a977606..7e61272b 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -53,9 +53,9 @@ use mega_evm::revm::{ handler::FrameResult, inspector::Inspector, interpreter::{ - interpreter_types::{Jumps, MemoryTr, StackTr}, + interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - Interpreter, InterpreterResult, InterpreterTypes, + Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, primitives::{Address, Bytes, Log, U256}, }; @@ -173,11 +173,17 @@ pub enum ChaosShape { /// A failed *call* frame rewritten into a success. The creation form of this shape is refused /// by the shim and is deliberately not in the pool — see the module docs. ReviveCall, + /// Gas written into the action the interpreter is already holding — the object a terminating + /// or suspending instruction left behind, which carries its own copy of what the frame is + /// handing on. + RaiseActionGas, + /// Gas taken out of one. + LowerActionGas, } impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 12] = [ + pub const ALL: [Self; 14] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -190,6 +196,8 @@ impl ChaosShape { Self::LowerResultGas, Self::FailFrame, Self::ReviveCall, + Self::RaiseActionGas, + Self::LowerActionGas, ]; /// The shape a label names. @@ -221,16 +229,24 @@ impl ChaosShape { Self::LowerResultGas => "lower_result_gas", Self::FailFrame => "fail_frame", Self::ReviveCall => "revive_call", + Self::RaiseActionGas => "raise_action_gas", + Self::LowerActionGas => "lower_action_gas", } } } /// Shapes reachable from a callback that holds a live interpreter. -const INTERPRETER_SHAPES: [ChaosShape; 4] = [ +/// +/// The last two only land at the one callback that runs with an action already pending — +/// `step_end`, which revm's inspected loop runs after the instruction that set it. A draw for them +/// anywhere else leaves the interpreter alone and spends no budget. +const INTERPRETER_SHAPES: [ChaosShape; 6] = [ ChaosShape::InjectGas, ChaosShape::DrainGas, ChaosShape::EditFrameState, ChaosShape::JournalWrite, + ChaosShape::RaiseActionGas, + ChaosShape::LowerActionGas, ]; /// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. @@ -490,6 +506,12 @@ impl ChaosInspector { } } ChaosShape::JournalWrite => write_journal(context, entropy), + ChaosShape::RaiseActionGas | ChaosShape::LowerActionGas => { + let raise = shape == ChaosShape::RaiseActionGas; + if !edit_pending_action_gas(interp, raise, Self::amount(entropy)) { + return; + } + } _ => return, } self.applied(shape); @@ -637,6 +659,50 @@ fn edit_frame_state(interp: &mut Interpreter, entr false } +/// Moves gas in or out of the action the interpreter is holding, returning whether anything moved. +/// +/// The pending action is the one gas-carrying object a live-interpreter callback can reach that is +/// not the interpreter's own counter, and the two are different numbers at exactly one moment: a +/// terminating instruction has copied the counter into a `Return` action, or a `CALL` / `CREATE` +/// has put the child's envelope into a `NewFrame` one. With no action pending there is nothing to +/// edit and no budget is spent. +fn edit_pending_action_gas( + interp: &mut Interpreter, + raise: bool, + amount: u64, +) -> bool { + match interp.bytecode.action() { + Some(InterpreterAction::Return(result)) => { + if raise { + result.gas.erase_cost(amount); + true + } else { + // The action cannot afford the removal; leave it alone rather than manufacture an + // out-of-gas the EVM did not reach. + result.gas.record_regular_cost(amount) + } + } + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { + inputs.gas_limit = move_envelope(inputs.gas_limit, raise, amount); + true + } + Some(InterpreterAction::NewFrame(FrameInput::Create(inputs))) => { + inputs.set_gas_limit(move_envelope(inputs.gas_limit(), raise, amount)); + true + } + _ => false, + } +} + +/// A child envelope moved by `amount`, saturating at both ends. +const fn move_envelope(limit: u64, raise: bool, amount: u64) -> u64 { + if raise { + limit.saturating_add(amount) + } else { + limit.saturating_sub(amount) + } +} + /// Writes one transient-storage slot on the frame's own account, behind the EVM's back. /// /// Transient storage is journalled, so the write follows the frame's checkpoint like any other From 7876e4c94329da1e18f4a7d417c204f6c5dc4c61 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:19:24 +0800 Subject: [PATCH 127/208] docs(evm): state the pending action as a measured surface --- AGENTS.md | 3 +- crates/mega-evm/src/evm/AGENTS.md | 7 ++++- crates/mega-evm/src/evm/execution.rs | 4 ++- crates/mega-evm/src/evm/inspector.rs | 26 +++++++++-------- crates/mega-evm/src/limit/inspector_ledger.rs | 28 ++++++++++++++----- crates/mega-evm/src/limit/limit.rs | 19 ++++++++----- .../tests/rex7/inspector_settlement_window.rs | 23 ++++----------- 7 files changed, 64 insertions(+), 46 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6dd040b0..8f25df70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,7 +132,8 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome — because a rewrite that costs nothing still produces different state and a different receipt. - What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes, the pending action), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. + The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). + What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 2de29ba7..5d414657 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -57,7 +57,8 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | | The interpreter's stack or memory | Supported, unmeasured | nothing — no argument the shim holds describes it | the EVM executes on the edited state and meters it as its own work, because it is | | A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing — no argument the shim holds describes it | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | -| The pending `InterpreterAction`, reached through `LoopControl` | Supported, unmeasured | nothing | an edit to the action's gas does move the envelope; measuring it at the callback boundary would be unsound, because whether it moves anything depends on the frame's final classification | +| The gas a pending `InterpreterAction` carries, reached through `LoopControl` (`step_end`) | Supported | the lane the action the callback left behind names: `InspectorLedger::result` for a `Return` action, settled at the frame's settlement point because that action *is* the frame's result a moment later; `InspectorLedger::env` for a `NewFrame` one, booked at the child's `frame_start`; `InspectorLedger::gas` when the callback removed the action, because the frame then carries on spending its counter | nothing | +| A pending action's classification or output, or an action installed, removed or swapped for the other variant | Supported | `InspectorLedger::interventions`, alongside whatever gas the change moved on the lane above | it changes what the EVM does next, not what the frame has spent | | `CfgEnv` or the active gas schedule | **Refused** | — | the gas-schedule pin panics; the schedule belongs to the spec, and a rewritten one has no accounting lane that could rescue it | Two independent stops back the creation refusal: the shim restores the classification, and `frame.rs`'s `FrameJournalVerdict::CreateRejected` carries no code and no commit branch, so even with the refusal removed such a rewrite deposits nothing. @@ -70,6 +71,10 @@ A gas-counter edit made while the interpreter is already holding a `Return` acti The shim books nothing for such an edit and still shifts the settlement baseline for it — `MegaETH`'s tail settlement reads the counter after the action is set, so without the shift the edit would read as work the frame performed. The predicate is the pending action's variant, not "the loop is ending": a `NewFrame` action ends the loop too, and that frame resumes on exactly this counter. +What the counter no longer speaks for, the action does, and the shim measures both against the same reading. +A frame holds `counter` with no action pending, `counter + f.gas_limit` with a `NewFrame` action, and the action's own copy with a `Return` one (`inspector.rs::held`); the counter lane books the counter's movement exactly when the EVM will read it again, and the action lane books the rest. +The two together account for every unit of gas the frame holds, whatever the callback did to the action's shape. + ### Rules for changing this - **Add the shim's counterpart when adding an `Inspector` callback.** diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 8af1e587..5f95aa77 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -600,7 +600,9 @@ impl MegaEvm { /// and before the journal is told what to do with it. /// /// `inspector_gas_delta` is what that callback did to the result's gas, and is zero on the - /// uninspected path, where no callback runs at all. + /// uninspected path, where no callback runs at all. An edit an earlier callback made to the + /// terminating action this result was built from is staged on the limit tracker and joins it + /// there. #[inline] fn finalize_frame( ctx: &MegaContext, diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 1d94dacf..c6ddaca4 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -3,11 +3,11 @@ //! # Why a shim //! //! An inspector is not a passive observer. Every callback that receives a live interpreter can -//! write to its gas counter, and every callback that receives a frame's inputs can change the gas -//! limit the frame is about to be built with. `MegaETH` meters compute gas by watching those exact -//! counters, and derives what a transaction destroyed from the envelope it spent, so an -//! unmeasured edit shows up as the EVM having done less work than it did, or as a transaction -//! having spent less gas than it did. +//! write to its gas counter and to the action it is holding, and every callback that receives a +//! frame's inputs can change the gas limit the frame is about to be built with. `MegaETH` meters +//! compute gas by watching those exact counters, and derives what a transaction destroyed from the +//! envelope it spent, so an unmeasured edit shows up as the EVM having done less work than it did, +//! or as a transaction having spent less gas than it did. //! //! # Why the callback boundary is enough //! @@ -25,6 +25,10 @@ //! - Interpreter-counter edits go to [`AdditionalLimit::record_inspector_gas_adjustment`], which //! books them, keeps them out of the compute-gas measurement, and re-derives the gas clamp so //! injected gas is not spendable past the compute headroom. +//! - Pending-action edits go to [`book_pending_action`], which routes them by the action the +//! callback left behind — a frame's counter, a child's envelope, or the result its caller will +//! reclaim from. The counter and the action between them hold everything a frame has, and +//! [`held`] is the identity the two lanes split. //! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. //! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! @@ -205,11 +209,10 @@ struct ActionChange { /// The EVM does not execute inside a callback, so the action is either the one the last /// instruction set or one the callback wrote — and the difference between the two readings of /// [`held`] is what the callback moved. Taking both readings at the *pre-callback* counter is -/// what keeps this lane and the counter lane from overlapping: the counter's own movement is -/// booked by [`record_inspector_gas_adjustment`](crate::AdditionalLimit:: -/// record_inspector_gas_adjustment) exactly when [`ActionLane::counter_reaches_envelope`] says the -/// EVM will read it again, and it cancels out of this difference in precisely the cases where it -/// does. +/// what keeps this lane and the counter lane from overlapping: +/// [`AdditionalLimit::record_inspector_gas_adjustment`] books the counter's own movement exactly +/// when [`ActionLane::counter_reaches_envelope`] says the EVM will read it again, and it cancels +/// out of this difference in precisely the cases where it does. #[inline] fn measure_pending_action( before: Option, @@ -510,8 +513,7 @@ where let action = interpreter.bytecode.action().clone(); let before = interpreter.gas.remaining(); self.inner.log_full(interpreter, context, log); - let change = - measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); + let change = measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); book_pending_action(context, change); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interpreter.gas, diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index e285beea..e978f24e 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -12,9 +12,10 @@ /// # Why the boundary is a sound place to measure /// /// The EVM does not execute inside an inspector callback. Every change to an interpreter's gas -/// counter, or to a frame input's gas limit, that is visible across a callback's entry and exit is -/// therefore the inspector's, by construction rather than by attribution heuristics. The shim takes -/// one snapshot before delegating and one after, and the difference lands here. +/// counter, to the action it is holding, or to a frame input's gas limit that is visible across a +/// callback's entry and exit is therefore the inspector's, by construction rather than by +/// attribution heuristics. The shim takes one snapshot before delegating and one after, and the +/// difference lands here. /// /// # Sign convention /// @@ -26,10 +27,9 @@ /// # What it does not measure /// /// What a callback does behind the shim's back. An inspector reaches state that no argument it is -/// handed describes — the interpreter's stack and memory, the journal, the pending action — and -/// telling whether any of those came back changed needs a snapshot of unbounded state that no -/// callback boundary can take at a cost the inspected path can carry. Those rewrites leave this -/// all-zero. +/// handed describes — the interpreter's stack and memory, the journal — and telling whether any of +/// those came back changed needs a snapshot of unbounded state that no callback boundary can take +/// at a cost the inspected path can carry. Those rewrites leave this all-zero. /// /// So an empty ledger says two things: no gas moved that the EVM did not move, and nothing the /// shim was handed came back different. It does not say the transaction is the one the EVM would @@ -62,6 +62,9 @@ pub struct InspectorLedger { /// /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the /// caller never forwarded and lowering it takes gas away that the caller will never get back. + /// + /// A callback that removed the interpreter's pending action lands here too: with no action + /// left the frame carries on spending what it holds, which is exactly what the counter is. pub gas: i128, /// Net gas the inspector wrote into frame *envelopes* — the `gas_limit` a call or create frame @@ -77,6 +80,13 @@ pub struct InspectorLedger { /// edit back and size the synthetic outcome from it. That gas travels through the result lane /// below, not through this one.) /// + /// The same lane carries an edit made one step earlier, to the `gas_limit` inside a pending + /// `NewFrame` action — the object the caller's `CALL` / `CREATE` opcode produced, before any + /// callback saw the inputs built from it. It is booked whether or not the frame is then + /// intercepted: an interception discards inputs the *same* callback edited a moment before, + /// which is why that edit reaches nothing, and it cannot un-make an edit another callback made + /// to the action the caller's debit is already behind. + /// /// Adjustments to a frame's *result* gas belong to [`result`](Self::result), which is booked /// from the frame's own settlement point rather than from a callback boundary. pub env: i128, @@ -91,6 +101,10 @@ pub struct InspectorLedger { /// changes nothing. The frame's settlement point knows the final classification and books /// this lane only in the first case; in the second it reconstructs the EVM's own number and /// settles the destroyed remainder against that instead. + /// + /// The same lane carries an edit made one step earlier, to the gas inside a pending `Return` + /// action. That action *is* the frame's result a moment later, so the two are one number + /// measured on either side of the classification, and they settle together. pub result: i128, /// How many rewrites the shim refused because their shape is forbidden. diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 29332958..de134ae0 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -614,10 +614,9 @@ impl AdditionalLimit { /// Stages an adjustment an inspector made to the gas a *suspending* pending action carries — /// the envelope the child frame is about to be built with. /// - /// Same lane as an edit made at the frame-start callback, taken one step earlier. It is staged - /// rather than booked on the spot only so that the booking happens at a point that can see the - /// whole picture; [`take_inspector_action_env_adjustment`](Self:: - /// take_inspector_action_env_adjustment) is where it lands. + /// Same lane as an edit made at the frame-start callback, taken one step earlier, and booked + /// there: the shim takes it back out at that callback, which is the first point that can tell + /// the edit apart from an interception. #[inline] pub(crate) fn stage_inspector_action_env_adjustment(&mut self, delta: i128) { self.staged_action_env_gas += delta; @@ -1489,7 +1488,8 @@ impl AdditionalLimit { // Everything an inspector wrote into this result, whether it wrote it into the frame's // terminating action or into the result the action became. The two are the same number // measured on either side of the classification, so they settle as one. - let inspector_gas_delta = inspector_gas_delta + core::mem::take(&mut self.staged_action_result_gas); + let inspector_gas_delta = + inspector_gas_delta + core::mem::take(&mut self.staged_action_result_gas); // The gas the EVM itself left in this result. Every settlement below is defined against // it: the last callback's edit to the number is the inspector's, and the two are only the // same object on a frame no callback touched. @@ -1520,8 +1520,13 @@ impl AdditionalLimit { } } - /// Books what the last mutating callback did to a frame result's gas, and reports the gas the - /// EVM itself left in that result. + /// Books what an inspector did to a frame result's gas, and reports the gas the EVM itself + /// left in that result. + /// + /// `delta` covers both places such an edit can be made: the frame's last mutating callback, + /// and — one step earlier, through `LoopControl` — the terminating action that *becomes* this + /// result. They are one number measured on either side of the classification, so they settle + /// as one. /// /// Whether such an edit moves anything depends on the frame's final classification, which is /// why this can only run here: diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index 2cb30dd4..32ceb203 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -478,12 +478,8 @@ fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { fn test_raising_a_returning_frames_pending_action_is_booked() { let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); let mut inspector = ActionEditor::raise(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - db(straight_line_code()), - limits(), - &mut inspector, - ); + let edited = + transact_inspected(MegaSpecId::REX7, db(straight_line_code()), limits(), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( @@ -507,12 +503,8 @@ fn test_raising_a_returning_frames_pending_action_is_booked() { fn test_lowering_a_returning_frames_pending_action_is_booked() { let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); let mut inspector = ActionEditor::lower(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - db(straight_line_code()), - limits(), - &mut inspector, - ); + let edited = + transact_inspected(MegaSpecId::REX7, db(straight_line_code()), limits(), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( @@ -533,11 +525,8 @@ fn test_lowering_a_returning_frames_pending_action_is_booked() { #[test] fn test_editing_a_halting_frames_pending_action_moves_nothing() { let callee = BytecodeBuilder::default().append(INVALID).build(); - let plain = transact( - MegaSpecId::REX7, - db_with_callee(halting_callee_code(), callee.clone()), - limits(), - ); + let plain = + transact(MegaSpecId::REX7, db_with_callee(halting_callee_code(), callee.clone()), limits()); let mut inspector = ActionEditor::on_halt(); let edited = transact_inspected( MegaSpecId::REX7, From 908a68e550235ca5876c9f5685baa8b92d36323a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:33:15 +0800 Subject: [PATCH 128/208] test(rex7): pin a pending action's classification rewrite as an intervention --- crates/mega-evm/src/evm/inspector.rs | 5 +- .../tests/rex7/inspector_settlement_window.rs | 70 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index c6ddaca4..3fc757c4 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -254,7 +254,10 @@ fn action_rewritten(before: Option, after: Option<&Interprete /// - [`ActionLane::Envelope`] is staged for the frame-start callback of the child the action is /// about to build, which is where an envelope edit is booked from; /// - [`ActionLane::Counter`] is booked on the spot, on the interpreter lane: with no action left, -/// the frame carries on spending what it holds, which is exactly what a counter edit does. +/// the frame carries on spending what it holds, which is exactly what a counter edit does. This +/// is the algebra's third case rather than a shape an inspector can reach through the API — +/// `reset_action` only clears revm's `continue_execution` flag and leaves the action in place, so +/// emptying the slot means writing `None` into it and desynchronising the two. #[inline] fn book_pending_action( context: &MegaContext, diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index 32ceb203..b51d3c0d 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -448,11 +448,10 @@ impl Inspector for ActionEditor { } } -/// A `CALL` into a callee that halts on an invalid opcode, its failure flag popped, then `STOP`. -/// -/// The inner frame reaches a terminating `step_end` holding a *halting* `Return` action, which is -/// the branch of the settlement where an edit to the action moves nothing. -fn halting_callee_code() -> Bytes { +/// A `CALL` into [`CALLEE`], its result flag popped, then `STOP` — so the first terminating +/// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries +/// is decided by the callee the fixture installs. +fn call_callee_code() -> Bytes { BytecodeBuilder::default() .push_number(0u64) // retSize .push_number(0u64) // retOffset @@ -526,11 +525,11 @@ fn test_lowering_a_returning_frames_pending_action_is_booked() { fn test_editing_a_halting_frames_pending_action_moves_nothing() { let callee = BytecodeBuilder::default().append(INVALID).build(); let plain = - transact(MegaSpecId::REX7, db_with_callee(halting_callee_code(), callee.clone()), limits()); + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); let mut inspector = ActionEditor::on_halt(); let edited = transact_inspected( MegaSpecId::REX7, - db_with_callee(halting_callee_code(), callee), + db_with_callee(call_callee_code(), callee), limits(), &mut inspector, ); @@ -569,3 +568,60 @@ fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { "the child hands the extra budget straight back, so the transaction spends less", ); } + +/// Rewrites the classification inside a pending `Return` action, once, at the terminating +/// `step_end` of the frame that set it. +#[derive(Debug)] +struct ActionReclassifier { + to: InstructionResult, + fired: u32, +} + +impl Inspector for ActionReclassifier { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { return }; + result.result = self.to; + self.fired += 1; + } +} + +/// An edit to a pending action that is not to its gas moves nothing and is booked as an +/// intervention — but it still decides what the frame did, so the frame's state follows it. +/// +/// The action is what `classify_frame_action` builds the frame's result from, so a classification +/// written here is the one the caller sees and the one the journal decision is taken on. Nothing +/// on any gas lane can see that, which is what the intervention counter is for. +#[test] +fn test_rewriting_a_pending_actions_classification_is_an_intervention() { + let callee = + BytecodeBuilder::default().sstore(U256::from(1u64), U256::from(1u64)).append(STOP).build(); + let plain = + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); + let mut inspector = ActionReclassifier { to: InstructionResult::Revert, fired: 0 }; + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(call_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + plain.storage_value(CALLEE, U256::from(1u64)), + U256::from(1u64), + "uninspected, the callee's write is committed", + ); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "no gas moved, and the only thing left to say is that the transaction was not left alone", + ); + assert_eq!( + edited.storage_value(CALLEE, U256::from(1u64)), + U256::ZERO, + "a frame the caller was told reverted must leave no write behind", + ); +} From aa42a694da0f1758944d96cdc32ca8fb840f4e5f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:51:59 +0800 Subject: [PATCH 129/208] test(rex7): pin the gas a synthetic outcome carries --- .../mega-evm/tests/rex7/interception_gas.rs | 504 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + 2 files changed, 508 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/interception_gas.rs diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs new file mode 100644 index 00000000..ce393938 --- /dev/null +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -0,0 +1,504 @@ +//! The gas a synthetic outcome carries. +//! +//! A `frame_start` / `call` / `create` callback that returns `Some(outcome)` answers the frame +//! itself: no frame is built, `frame_init` never runs, and the number the caller reclaims is +//! whatever `Gas` the inspector put in that outcome. Nothing about it is derived from the +//! execution — the inspector chooses it outright — so it is a gas figure the transaction's +//! accounting has to be told about, exactly like an edit to a result the EVM did produce. +//! +//! The tests here are laid out over the sign of that choice, because the two directions settle +//! differently and a lane that books one and drops the other is a real failure mode: +//! +//! - an outcome that hands back **less** than the envelope makes the caller spend gas no frame +//! ever performed work for; +//! - an outcome that hands back **more** conjures gas the transaction never funded; +//! - an outcome that hands back **exactly** the envelope — the echo convention every tracer that +//! intercepts follows — moves nothing, and must book nothing. +//! +//! The halt direction is the asymmetry: a halting outcome hands nothing back at all, so what the +//! inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed +//! remainder is settled against the envelope instead. + +use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, + MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, + MegaTransactionOutcome, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, MSTORE, MSTORE8, POP, RETURN, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + handler::{EvmTr, FrameResult}, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + InterpreterResult, InterpreterTypes, + }, + Inspector, +}; +use std::vec::Vec; + +/// High enough that EVM gas is never what binds. +const TX_GAS_LIMIT: u64 = 100_000_000; +/// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. +const FORWARDED: u64 = 50_000; + +/// Everything one transaction reports, plus what the shim booked for it. +struct Reading { + result: ExecutionResult, + compute_gas: u64, + enforced: u64, + destroyed: u64, + total_gas_spent: u64, + terms: ConservationTerms, + ledger: InspectorLedger, +} + +/// The conservation identity, stated with the term the measurement shim contributes. +fn assert_identity(label: &str, r: &Reading) { + assert_eq!( + r.compute_gas, + r.enforced + r.destroyed, + "{label}: reported compute must split into enforced + destroyed", + ); + assert_eq!( + r.terms.inspector_conjured_gas, + r.ledger.conjured_gas(), + "{label}: the law's `I` term is the ledger's net, and nothing else", + ); + assert_eq!( + r.terms.envelope_for(r.destroyed), + i128::from(r.total_gas_spent), + "{label}: the law must close against the envelope the receipt reports; \ + reported compute={} destroyed={} envelope={} ({})", + r.compute_gas, + r.destroyed, + r.total_gas_spent, + r.terms, + ); +} + +fn tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX7) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context +} + +fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { + assert_eq!( + outcome.inspector_ledger, + limit.inspector_ledger(), + "the outcome must report the ledger the shim booked, unchanged", + ); + let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); + Reading { + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + total_gas_spent, + terms: limit.conservation_terms(), + ledger: outcome.inspector_ledger, + } +} + +fn transact(mut db: MemoryDatabase, inspector: &mut I) -> Reading +where + I: for<'a> Inspector>, +{ + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(inspector); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +/// A straight run of plain opcodes that always succeeds. +fn plain_run_code(pairs: usize) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder.append(STOP).build() +} + +/// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. +fn call_fixture() -> MemoryDatabase { + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(u128::from(FORWARDED)) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, plain_run_code(20)) +} + +/// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. +#[derive(Clone, Copy, Debug)] +enum Sizing { + /// The echo convention: exactly the envelope. + Echo, + /// Half of it — the caller spends the other half for work no frame performed. + Half, + /// None of it. + Zero, + /// More than it — gas the transaction never funded. + Excess(u64), +} + +impl Sizing { + fn gas(self, envelope: u64) -> u64 { + match self { + Self::Echo => envelope, + Self::Half => envelope / 2, + Self::Zero => 0, + Self::Excess(extra) => envelope + extra, + } + } + + /// What the ledger must carry for this sizing, as a signed movement from the envelope. + fn expected_delta(self, envelope: u64) -> i128 { + i128::from(self.gas(envelope)) - i128::from(envelope) + } +} + +/// Intercepts the call to [`CALLEE`], sizing the outcome's gas by [`Sizing`]. +struct CallInterceptor { + sizing: Sizing, + classification: InstructionResult, + intercepted: u64, + envelope: u64, +} + +impl CallInterceptor { + fn new(sizing: Sizing, classification: InstructionResult) -> Self { + Self { sizing, classification, intercepted: 0, envelope: 0 } + } +} + +impl Inspector for CallInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.envelope = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + self.classification, + Bytes::new(), + Gas::new(self.sizing.gas(inputs.gas_limit)), + ), + inputs.return_memory_offset.clone(), + )) + } +} + +/// An outcome that hands back less than the envelope makes the caller spend gas nothing performed. +#[test] +fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { + let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); + assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); + assert_eq!( + reading.ledger, + InspectorLedger { + result: Sizing::Half.expected_delta(FORWARDED), + interventions: 1, + ..InspectorLedger::default() + }, + "the half the outcome withheld is gas the inspector destroyed", + ); + assert_identity("half-gas interception", &reading); +} + +/// The extreme of the same direction: the outcome hands back nothing at all. +#[test] +fn test_a_zero_gas_interception_books_the_whole_envelope() { + let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.ledger, + InspectorLedger { + result: Sizing::Zero.expected_delta(FORWARDED), + interventions: 1, + ..InspectorLedger::default() + }, + "an outcome that returns nothing consumed the whole envelope", + ); + assert_identity("zero-gas interception", &reading); +} + +/// The other direction: an outcome that hands back more than it was given conjures the difference. +#[test] +fn test_an_over_funded_interception_books_the_gas_it_conjured() { + const EXTRA: u64 = 7_000; + let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.ledger, + InspectorLedger { + result: Sizing::Excess(EXTRA).expected_delta(FORWARDED), + interventions: 1, + ..InspectorLedger::default() + }, + "gas the transaction never funded is gas the inspector conjured", + ); + assert_identity("over-funded interception", &reading); +} + +/// The echo convention moves nothing, and must book nothing. +/// +/// This is the shape every tool that intercepts actually uses, and the reason the lane could go +/// missing for as long as it did: with the envelope echoed back the accounting closes whether or +/// not anything measures it. Pinning the zero is what says the lane is measuring rather than +/// coincidentally agreeing. +#[test] +fn test_an_echoing_interception_books_no_gas_at_all() { + for classification in [InstructionResult::Stop, InstructionResult::Revert] { + let mut inspector = CallInterceptor::new(Sizing::Echo, classification); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", + ); + assert_eq!(reading.ledger.conjured_gas(), 0, "{classification:?}"); + assert_identity("echoing interception", &reading); + } +} + +/// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes +/// nothing the transaction spends — and the envelope is destroyed whole. +#[test] +fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { + for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { + let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); + assert_eq!( + reading.ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "{sizing:?}: a halting frame hands nothing back, so no gas lane may move", + ); + assert_eq!( + reading.destroyed, FORWARDED, + "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", + ); + assert_identity("halting interception", &reading); + } +} + +/// The generic callback intercepts too, and is measured by the same rule. +/// +/// revm runs `frame_start` before the variant-specific `call` / `create`, and an outcome returned +/// there skips both. A lane wired only to the variant hooks would leave this one unmeasured. +#[test] +fn test_the_generic_frame_start_interception_is_measured_too() { + /// Intercepts the call to [`CALLEE`] from the generic callback, handing back half. + #[derive(Default)] + struct GenericInterceptor { + intercepted: u64, + } + + impl Inspector for GenericInterceptor { + fn frame_start( + &mut self, + _context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + let FrameInput::Call(inputs) = frame_input else { return None }; + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + Some(FrameResult::Call(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit / 2), + ), + inputs.return_memory_offset.clone(), + ))) + } + } + + let mut inspector = GenericInterceptor::default(); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.ledger, + InspectorLedger { + result: Sizing::Half.expected_delta(FORWARDED), + interventions: 1, + ..InspectorLedger::default() + }, + "the generic callback's interception books on the same lane as the variant one's", + ); + assert_identity("frame_start interception", &reading); +} + +/// Init code that writes one slot and returns two bytes of runtime code. +fn init_code() -> Vec { + BytecodeBuilder::default() + .sstore(U256::from(0x30), U256::from(1)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset + .append(RETURN) + .build() + .to_vec() +} + +/// The entry contract: one `CREATE`, then `STOP`. +fn create_fixture() -> MemoryDatabase { + let init = init_code(); + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build(); + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. +/// +/// The envelope is not a constant here — `CREATE` forwards all but a sixty-fourth of what the +/// caller holds — so the test reads it back from the callback rather than asserting a figure. +#[test] +fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() { + /// Intercepts the creation, handing back half of what it was given. + #[derive(Default)] + struct CreateInterceptor { + intercepted: u64, + envelope: u64, + } + + impl Inspector for CreateInterceptor { + fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.intercepted += 1; + self.envelope = inputs.gas_limit(); + Some(CreateOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit() / 2), + ), + None, + )) + } + } + + let mut inspector = CreateInterceptor::default(); + let reading = transact(create_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); + assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); + assert_eq!( + reading.ledger, + InspectorLedger { + result: Sizing::Half.expected_delta(inspector.envelope), + interventions: 1, + ..InspectorLedger::default() + }, + "a creation's interception is measured against the envelope its CREATE forwarded", + ); + assert_identity("intercepted creation", &reading); +} + +/// The envelope an interception is measured against is the one the callback *received*. +/// +/// A callback is free to edit the inputs and then answer the frame itself. The edit reaches no +/// frame — nothing is built from those inputs — so the envelope the caller actually funded is the +/// one the callback was handed, and an outcome echoing the *edited* limit hands back more than +/// that. Measuring against the post-edit number instead would read this run as conjuring nothing. +#[test] +fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { + const BONUS: u64 = 9_000; + + /// Raises the child's gas limit and then intercepts, echoing the raised figure. + #[derive(Default)] + struct RaisingInterceptor { + intercepted: u64, + } + + impl Inspector for RaisingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + inputs.gas_limit += BONUS; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let mut inspector = RaisingInterceptor::default(); + let reading = transact(call_fixture(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.ledger, + InspectorLedger { + result: i128::from(BONUS), + interventions: 1, + ..InspectorLedger::default() + }, + "the bonus reaches the caller through the outcome, so it is booked once, on the result \ + lane — the env lane stays empty because no frame was ever built from those inputs", + ); + assert_identity("raised then intercepted", &reading); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 2128e7e5..3cb626a4 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -18,6 +18,9 @@ //! that should have read it: a terminating opcode's `step_end`, whose counter edit reaches //! nobody, and a precompile's classification, whose split has to follow the callback rather than //! the recording site. +//! - `interception_gas` — the gas an inspector puts into a synthetic outcome, over the four +//! sizings it can choose relative to the envelope it was handed, and the halt direction where +//! the choice reaches nothing. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. //! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that @@ -93,6 +96,7 @@ mod gas_leakage; mod guard_pass_static_gas; mod inspector_cheat_matrix; mod inspector_settlement_window; +mod interception_gas; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; From a747bdca1b4fd7a5068817905add566f0bc11299 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:56:43 +0800 Subject: [PATCH 130/208] feat(evm): measure the gas an inspector's synthetic outcome carries --- crates/mega-evm/src/evm/execution.rs | 102 +++++++++++++++++- crates/mega-evm/src/evm/inspector.rs | 25 ++++- crates/mega-evm/src/limit/limit.rs | 32 ++++++ .../mega-evm/tests/rex7/interception_gas.rs | 10 +- crates/mega-evm/tests/rex7/main.rs | 6 +- 5 files changed, 162 insertions(+), 13 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 5f95aa77..eaef910d 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1849,6 +1849,15 @@ where // Check if inspector wants to skip this call/create if let Some(output) = frame_start(ctx, inspector, &mut frame_init.frame_input) { + // What the transaction funded this frame with, staged by the measurement shim at the + // callback that answered it. Taken here rather than at the settlement below so that + // it cannot outlive this frame init on a spec that settles nothing. + let envelope = additional_limit.borrow_mut().take_inspector_interception_envelope(); + debug_assert!( + envelope.is_some() || matches!(frame_init.frame_input, FrameInput::Empty), + "every inspector is wrapped in the measurement shim, which stages the envelope of any frame a callback answers itself", + ); + // Inspector intercepted — `frame_init()` is skipped entirely, so neither // `frame_result_if_exceeding_limit` nor `before_frame_init` would run. The two // guards that stand in front of interceptor dispatch are the same ones the plain @@ -1868,12 +1877,18 @@ where additional_limit.borrow_mut().push_empty_frame(); } - let gas_before_callback = output.gas().remaining(); frame_end(ctx, inspector, &frame_init.frame_input, &mut output); - let inspector_gas_delta = - i128::from(output.gas().remaining()) - i128::from(gas_before_callback); if is_mini_rex_enabled { + // No frame ran, so the whole of what this result carries is the inspector's + // doing, measured against the envelope rather than across one callback: the + // synthetic outcome's own gas, whatever `frame_end` then did to it, and whatever + // of an edit to the inputs survived into a guard's replacement result. The + // settlement point splits it on the classification the caller ends up seeing, + // exactly as it does for a frame that really ran. + let inspector_gas_delta = envelope.map_or(0, |envelope| { + i128::from(output.gas().remaining()) - i128::from(envelope) + }); additional_limit.borrow_mut().finalize_frame( &mut output, exit, @@ -2150,6 +2165,87 @@ mod mutation_tests { } } + /// Raises the child's envelope and then answers the frame itself, echoing the raised figure. + /// + /// The shape the two override tests below need: an interception whose gas figure is *not* the + /// envelope the transaction funded, so that a guard replacing the outcome has something to + /// get wrong. + #[derive(Default)] + struct RaisingStopInspector; + + /// How much [`RaisingStopInspector`] adds to the envelope it is handed. + const RAISE: u64 = 4_000; + + impl Inspector for RaisingStopInspector { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + inputs.gas_limit += RAISE; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + /// A guard that replaces an interception's outcome still leaves the inspector's edit reaching + /// the caller, and it has to be booked. + /// + /// The depth rejection is built from the frame input the callback edited, and `CallTooDeep` + /// is in the revert group — so the caller reclaims the raised figure, not the one the + /// transaction funded. Measuring against the envelope the callback was handed catches this + /// without the settlement having to know which of the two results it is looking at. + #[test] + fn test_a_guard_replacing_an_interception_still_books_the_edit_that_reached_the_caller() { + let mut evm = MegaEvm::new(MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX5)) + .with_inspector(RaisingStopInspector); + let ItemOrResult::Result(result) = InspectorEvmTr::inspect_frame_init( + &mut evm, + call_frame_init(CALL_STACK_LIMIT as usize + 1), + ) + .unwrap() else { + panic!("depth guard must override the inspector result"); + }; + assert_eq!( + result.gas().remaining(), + TEST_GAS_LIMIT + RAISE, + "the rejection is built from the inputs the callback left behind", + ); + assert_eq!( + evm.ctx_ref().additional_limit.borrow().inspector_ledger().result, + i128::from(RAISE), + "the caller reclaims the raise, so the ledger must carry it", + ); + consume_synthetic_limit_frame(evm.ctx_ref(), result); + } + + /// The mirror: a rejection the caller reclaims nothing from books nothing, and the sender's + /// rescue is taken on the envelope the transaction funded rather than on the raised figure. + #[test] + fn test_a_halting_guard_rescues_the_funded_envelope_and_not_the_raised_one() { + let mut evm = + MegaEvm::new(context_with_latched_limit()).with_inspector(RaisingStopInspector); + let ItemOrResult::Result(result) = + InspectorEvmTr::inspect_frame_init(&mut evm, call_frame_init(1)).unwrap() + else { + panic!("latched limit must override the inspector result"); + }; + let limit = evm.ctx_ref().additional_limit.borrow(); + assert_eq!( + limit.inspector_ledger().result, + 0, + "a halting rejection hands nothing back, so the edit reaches nothing", + ); + assert_eq!( + limit.rescued_gas, TEST_GAS_LIMIT, + "the sender is refunded what the transaction funded, not what the inspector wrote", + ); + drop(limit); + consume_synthetic_limit_frame(evm.ctx_ref(), result); + } + #[test] fn test_inspect_frame_init_limit_short_circuit_pushes_limit_frame() { let mut evm = MegaEvm::new(context_with_latched_limit()).with_inspector(StopInspector); diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 3fc757c4..417b8a30 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -30,6 +30,10 @@ //! reclaim from. The counter and the action between them hold everything a frame has, and //! [`held`] is the identity the two lanes split. //! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. +//! - A callback that answers a frame itself stages the envelope it was handed, through +//! [`AdditionalLimit::stage_inspector_interception_envelope`], so that the frame init it +//! short-circuited can settle the gas its synthetic outcome carries against what the transaction +//! funded. //! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on @@ -285,12 +289,12 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { } /// Books what a callback did to a frame's envelope, together with whatever an earlier callback -/// staged into the same envelope through the pending `NewFrame` action. +/// staged into the same envelope through the pending `NewFrame` action — and, when the callback +/// answered the frame itself, stages that envelope for the frame's settlement point. /// /// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped -/// entirely and the EVM never reads the inputs it edited, so the edit by itself moves nothing. Gas -/// the inspector then puts into that synthetic outcome travels through the result lane, which this -/// lane deliberately does not cover — see [`InspectorLedger::env`](crate::InspectorLedger::env). +/// entirely and the EVM never reads the inputs it edited, so the edit by itself moves nothing on +/// this lane — see [`InspectorLedger::env`](crate::InspectorLedger::env). /// /// The staged amount is booked either way, and the asymmetry is not an oversight. An interception /// discards inputs *this* callback edited a moment earlier, which is why that edit reaches @@ -300,6 +304,16 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { /// later to answer the frame itself cannot un-make that. It is simply the earliest of the two /// edits to the one envelope, and the last thing to touch that envelope is what its holder is /// sized from. +/// +/// # Why an interception stages a baseline rather than booking a difference +/// +/// Every other lane measures a difference across the callback, because the EVM produced the +/// object on both sides of it. An interception has no such object: the frame is never built, and +/// the result the caller reclaims from is one the inspector wrote from nothing. What the +/// transaction funded is the envelope on the way in; what it gets back is whatever gas that +/// result turns out to carry once the last callback has run. The difference between the two is +/// the measurement, and only the frame init that asked can take it — so the way in is staged +/// here, and [`AdditionalLimit::stage_inspector_interception_envelope`] says what the number is. #[inline] fn book_env_adjustment( context: &MegaContext, @@ -312,6 +326,9 @@ fn book_env_adjustment( (false, Some(before), Some(after)) => i128::from(after) - i128::from(before), _ => 0, }; + if let (true, Some(before)) = (intercepted, before) { + context.additional_limit.borrow_mut().stage_inspector_interception_envelope(before); + } if staged + callback == 0 { return; } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index de134ae0..517423fb 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -184,6 +184,15 @@ pub struct AdditionalLimit { /// and it runs immediately after the action is handed on, with nothing in between that could /// stage another one. staged_action_env_gas: i128, + + /// The envelope a callback that answered a frame itself was handed, waiting for the + /// settlement point of the frame it answered. + /// + /// An interception produces a whole frame result out of nothing, so the reading its + /// settlement needs is not a difference the shim can take — it is this baseline against the + /// gas the result turns out to carry. At most one can be outstanding: revm stops at the + /// first callback that answers, and the frame init that asked settles a few statements later. + staged_interception_envelope: Option, } /// The usage of the additional limits. @@ -216,6 +225,7 @@ impl AdditionalLimit { staged_precompile: None, staged_action_result_gas: 0, staged_action_env_gas: 0, + staged_interception_envelope: None, } } } @@ -262,6 +272,7 @@ impl AdditionalLimit { self.staged_precompile = None; self.staged_action_result_gas = 0; self.staged_action_env_gas = 0; + self.staged_interception_envelope = None; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -628,6 +639,27 @@ impl AdditionalLimit { core::mem::take(&mut self.staged_action_env_gas) } + /// Stages the envelope a callback that answered a frame itself was handed. + /// + /// The number recorded is the gas limit as that callback *received* it, not as it left it. + /// That is the envelope the transaction actually funded: the caller's `CALL` / `CREATE` + /// opcode debited it, and any edit an earlier callback made to it on the way here was booked + /// on the envelope lane as it was made. An edit the answering callback itself makes is + /// deliberately not part of the baseline — see + /// [`record_inspector_env_adjustment`](Self::record_inspector_env_adjustment) for why it + /// reaches no frame, and note that whatever of it survives into the result the caller is + /// handed is measured here instead, as part of that result. + #[inline] + pub(crate) fn stage_inspector_interception_envelope(&mut self, envelope: u64) { + self.staged_interception_envelope = Some(envelope); + } + + /// Takes the staged interception envelope, for the frame init that asked to settle against. + #[inline] + pub(crate) fn take_inspector_interception_envelope(&mut self) -> Option { + self.staged_interception_envelope.take() + } + /// Books an adjustment an inspector made to a pending action the same callback then removed, /// leaving the frame to carry on from its own counter. /// diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index ce393938..51a9a950 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -9,8 +9,8 @@ //! The tests here are laid out over the sign of that choice, because the two directions settle //! differently and a lane that books one and drops the other is a real failure mode: //! -//! - an outcome that hands back **less** than the envelope makes the caller spend gas no frame -//! ever performed work for; +//! - an outcome that hands back **less** than the envelope makes the caller spend gas no frame ever +//! performed work for; //! - an outcome that hands back **more** conjures gas the transaction never funded; //! - an outcome that hands back **exactly** the envelope — the echo convention every tracer that //! intercepts follows — moves nothing, and must book nothing. @@ -421,7 +421,11 @@ fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() } impl Inspector for CreateInterceptor { - fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option { + fn create( + &mut self, + _context: &mut CTX, + inputs: &mut CreateInputs, + ) -> Option { self.intercepted += 1; self.envelope = inputs.gas_limit(); Some(CreateOutcome::new( diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 3cb626a4..cb00a65b 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -18,9 +18,9 @@ //! that should have read it: a terminating opcode's `step_end`, whose counter edit reaches //! nobody, and a precompile's classification, whose split has to follow the callback rather than //! the recording site. -//! - `interception_gas` — the gas an inspector puts into a synthetic outcome, over the four -//! sizings it can choose relative to the envelope it was handed, and the halt direction where -//! the choice reaches nothing. +//! - `interception_gas` — the gas an inspector puts into a synthetic outcome, over the four sizings +//! it can choose relative to the envelope it was handed, and the halt direction where the choice +//! reaches nothing. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. //! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that From 9d4cc6d99c9d1f683015ba8010bd4ab5d6c24d0c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 17:58:50 +0800 Subject: [PATCH 131/208] test(rex7): put a non-echoing interception in the cheat matrix --- .../tests/rex7/inspector_cheat_matrix.rs | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index aedcfcca..e58fd668 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -70,6 +70,9 @@ const ENVELOPE: u64 = 5_000; const RESULT: u64 = 2_000; /// Gas an action cheat adds to, or removes from, the gas a pending `InterpreterAction` carries. const ACTION: u64 = 1_500; +/// Gas an interception cheat's synthetic outcome hands back over, or under, the envelope it was +/// given. +const INTERCEPTION: u64 = 4_000; /// Slot the top frame writes, last of all, so a cheat that fails the top frame is visible. const TOP_SLOT: u64 = 0x10; @@ -146,6 +149,10 @@ enum Shape { EditInput, /// Return a synthetic outcome, so no frame is built at all. Intercept, + /// Return one whose gas hands back more than the envelope it was given. + RaiseInterceptionGas, + /// Return one whose gas hands back less. + LowerInterceptionGas, /// Raise the gas a finished frame hands back to its caller. RaiseResultGas, /// Lower it. @@ -171,13 +178,15 @@ enum Shape { } impl Shape { - const ALL: [Self; 16] = [ + const ALL: [Self; 18] = [ Self::InjectGas, Self::DrainGas, Self::RaiseEnvelope, Self::LowerEnvelope, Self::EditInput, Self::Intercept, + Self::RaiseInterceptionGas, + Self::LowerInterceptionGas, Self::RaiseResultGas, Self::LowerResultGas, Self::FailResult, @@ -190,6 +199,12 @@ impl Shape { Self::JournalWrite, ]; + /// Whether this shape answers the frame with a synthetic outcome instead of letting the EVM + /// build it. + const fn is_interception(self) -> bool { + matches!(self, Self::Intercept | Self::RaiseInterceptionGas | Self::LowerInterceptionGas) + } + /// Whether this shape reaches through the interpreter's *pending action* rather than through /// the interpreter itself. const fn is_pending_action(self) -> bool { @@ -246,11 +261,18 @@ fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { }; } + if shape.is_interception() && !input_facing { + return Some( + "only a callback that runs before the frame is built can answer it instead: the \ + `*_end` callbacks are handed a result the EVM already produced", + ); + } + match shape { InjectGas | DrainGas | EditStackOrMemory if !interpreter_facing => { Some("no live interpreter is reachable from this callback") } - RaiseEnvelope | LowerEnvelope | EditInput | Intercept if !input_facing => Some( + RaiseEnvelope | LowerEnvelope | EditInput if !input_facing => Some( "this callback receives no frame input it can build a frame from: the `*_end` \ callbacks take theirs by shared reference, after the frame has already run", ), @@ -398,13 +420,13 @@ impl Cheat { self.fired += 1; None } - Shape::Intercept => { + Shape::Intercept | Shape::RaiseInterceptionGas | Shape::LowerInterceptionGas => { self.fired += 1; Some(CallOutcome::new( InterpreterResult::new( InstructionResult::Stop, Bytes::new(), - Gas::new(inputs.gas_limit), + Gas::new(self.interception_gas(inputs.gas_limit)), ), inputs.return_memory_offset.clone(), )) @@ -413,6 +435,20 @@ impl Cheat { } } + /// The gas an interception's synthetic outcome hands back, given the envelope it was handed. + /// + /// The echo — hand back exactly what was forwarded — is the convention every tool that + /// intercepts follows, and the reason the two neighbouring columns exist: with it, the + /// accounting closes whether or not anything measures the figure. + fn interception_gas(&self, envelope: u64) -> u64 { + match self.shape { + Shape::Intercept => envelope, + Shape::RaiseInterceptionGas => envelope + INTERCEPTION, + Shape::LowerInterceptionGas => envelope - INTERCEPTION, + _ => unreachable!("{:?} is not an interception", self.shape), + } + } + /// Applies an input-facing shape to a creation's inputs, or intercepts the frame. fn hit_create_inputs(&mut self, inputs: &mut CreateInputs) -> Option { match self.shape { @@ -432,13 +468,13 @@ impl Cheat { self.fired += 1; None } - Shape::Intercept => { + Shape::Intercept | Shape::RaiseInterceptionGas | Shape::LowerInterceptionGas => { self.fired += 1; Some(CreateOutcome::new( InterpreterResult::new( InstructionResult::Stop, Bytes::new(), - Gas::new(inputs.gas_limit()), + Gas::new(self.interception_gas(inputs.gas_limit())), ), None, )) @@ -1060,6 +1096,31 @@ fn matrix() -> Vec { ledger_intervention(), if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, ); + // The same interception, sized against the envelope rather than echoing it. No frame is + // built, so the whole of what the outcome hands back is the inspector's number, and the + // difference from what the caller forwarded is what the ledger has to carry. + push( + at, + RaiseInterceptionGas, + Fixture::ReturningCallee, + InspectorLedger { + result: i128::from(INTERCEPTION), + interventions: 1, + ..InspectorLedger::default() + }, + if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, + ); + push( + at, + LowerInterceptionGas, + Fixture::ReturningCallee, + InspectorLedger { + result: -i128::from(INTERCEPTION), + interventions: 1, + ..InspectorLedger::default() + }, + if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, + ); push( at, JournalWrite, @@ -1199,6 +1260,7 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { (At::StepEnd, Shape::DrainGas, Fixture::ReturningCallee), (At::Call, Shape::RaiseEnvelope, Fixture::ReturningCallee), (At::FrameStart, Shape::Intercept, Fixture::ReturningCallee), + (At::Call, Shape::LowerInterceptionGas, Fixture::ReturningCallee), (At::Create, Shape::EditInput, Fixture::ReturningCallee), (At::CallEnd, Shape::LowerResultGas, Fixture::ReturningCallee), (At::CreateEnd, Shape::FailResult, Fixture::ReturningCallee), From 693955c1d5f1e07d8085ce9fb44a27bf6cc80ef1 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 18:00:59 +0800 Subject: [PATCH 132/208] feat(state-test): add the non-echoing interception shapes to the chaos pool --- crates/mega-state-test/src/chaos.rs | 65 ++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 7e61272b..210adc6c 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -162,8 +162,16 @@ pub enum ChaosShape { LowerEnvelope, /// A call turned static, so what the frame is allowed to do changes rather than what it costs. MakeStatic, - /// A synthetic outcome, so no frame is built at all. + /// A synthetic outcome, so no frame is built at all. Its gas echoes the envelope the callback + /// was handed, which is what every tool that intercepts does. Intercept, + /// The same, sized above the envelope, so the outcome hands the caller back gas the + /// transaction never funded. + InterceptOverGas, + /// Sized below it, so the caller spends the difference on a frame that never ran. + InterceptUnderGas, + /// Sized at nothing, the extreme of the same direction: the whole envelope is consumed. + InterceptNoGas, /// A raised remaining-gas figure on a finished frame's result. RaiseResultGas, /// A lowered one. @@ -183,7 +191,7 @@ pub enum ChaosShape { impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 14] = [ + pub const ALL: [Self; 17] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -192,6 +200,9 @@ impl ChaosShape { Self::LowerEnvelope, Self::MakeStatic, Self::Intercept, + Self::InterceptOverGas, + Self::InterceptUnderGas, + Self::InterceptNoGas, Self::RaiseResultGas, Self::LowerResultGas, Self::FailFrame, @@ -225,6 +236,9 @@ impl ChaosShape { Self::LowerEnvelope => "lower_envelope", Self::MakeStatic => "make_static", Self::Intercept => "intercept", + Self::InterceptOverGas => "intercept_over_gas", + Self::InterceptUnderGas => "intercept_under_gas", + Self::InterceptNoGas => "intercept_no_gas", Self::RaiseResultGas => "raise_result_gas", Self::LowerResultGas => "lower_result_gas", Self::FailFrame => "fail_frame", @@ -250,11 +264,19 @@ const INTERPRETER_SHAPES: [ChaosShape; 6] = [ ]; /// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. -const INPUT_SHAPES: [ChaosShape; 5] = [ +/// +/// The four interception shapes differ only in how the synthetic outcome's `Gas` is sized against +/// the envelope. That is the whole of what separates them, and it is the separation that matters: +/// the echo is the shape every real tool uses, and it is also the one shape whose accounting +/// closes without anything measuring the figure. +const INPUT_SHAPES: [ChaosShape; 8] = [ ChaosShape::RaiseEnvelope, ChaosShape::LowerEnvelope, ChaosShape::MakeStatic, ChaosShape::Intercept, + ChaosShape::InterceptOverGas, + ChaosShape::InterceptUnderGas, + ChaosShape::InterceptNoGas, ChaosShape::JournalWrite, ]; @@ -276,12 +298,12 @@ const RESULT_SHAPES: [ChaosShape; 5] = [ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ShapeFilter { /// Bitmask over [`ChaosShape::ALL`], by index. - allowed: u16, + allowed: u32, } impl Default for ShapeFilter { fn default() -> Self { - Self { allowed: u16::MAX } + Self { allowed: u32::MAX } } } @@ -305,8 +327,8 @@ impl ShapeFilter { ChaosShape::ALL.into_iter().all(|s| self.allows(s)) } - const fn index(shape: ChaosShape) -> u16 { - shape as u16 + const fn index(shape: ChaosShape) -> u32 { + shape as u32 } } @@ -534,12 +556,15 @@ impl ChaosInspector { inputs.gas_limit = inputs.gas_limit.saturating_sub(Self::amount(entropy)); } ChaosShape::MakeStatic => inputs.is_static = true, - ChaosShape::Intercept => { + ChaosShape::Intercept | + ChaosShape::InterceptOverGas | + ChaosShape::InterceptUnderGas | + ChaosShape::InterceptNoGas => { outcome = Some(CallOutcome::new( InterpreterResult::new( synthetic_result(entropy), Bytes::new(), - Gas::new(inputs.gas_limit), + Gas::new(interception_gas(shape, inputs.gas_limit, entropy)), ), inputs.return_memory_offset.clone(), )); @@ -567,12 +592,15 @@ impl ChaosInspector { ChaosShape::LowerEnvelope => { inputs.set_gas_limit(inputs.gas_limit().saturating_sub(Self::amount(entropy))); } - ChaosShape::Intercept => { + ChaosShape::Intercept | + ChaosShape::InterceptOverGas | + ChaosShape::InterceptUnderGas | + ChaosShape::InterceptNoGas => { outcome = Some(CreateOutcome::new( InterpreterResult::new( synthetic_result(entropy), Bytes::new(), - Gas::new(inputs.gas_limit()), + Gas::new(interception_gas(shape, inputs.gas_limit(), entropy)), ), None, )); @@ -627,6 +655,21 @@ impl ChaosInspector { } } +/// The gas a synthetic outcome hands back, given the envelope the callback was handed. +/// +/// The four interception shapes are exactly this function's four cases. `Intercept` echoes the +/// envelope, which is the convention every tool that intercepts follows and the one sizing whose +/// accounting closes even if nothing measures it; the other three move it, in both directions and +/// down to nothing, so a lane that books one direction and drops the other is caught. +const fn interception_gas(shape: ChaosShape, envelope: u64, entropy: u64) -> u64 { + match shape { + ChaosShape::InterceptOverGas => envelope.saturating_add(entropy % GAS_DELTA_MAX + 1), + ChaosShape::InterceptUnderGas => envelope.saturating_sub(entropy % GAS_DELTA_MAX + 1), + ChaosShape::InterceptNoGas => 0, + _ => envelope, + } +} + /// The classification a synthetic outcome carries — one of the three a real frame can end in. fn synthetic_result(entropy: u64) -> InstructionResult { match entropy % 3 { From 946d443d6b94f8ee24517202b43fc34661d836c8 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 18:15:02 +0800 Subject: [PATCH 133/208] docs(evm): close the gas-surface enumeration and pin it --- AGENTS.md | 2 + crates/mega-evm/src/evm/AGENTS.md | 40 +- crates/mega-evm/src/limit/inspector_ledger.rs | 8 + crates/mega-evm/tests/rex7/gas_surface.rs | 531 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + 5 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 crates/mega-evm/tests/rex7/gas_surface.rs diff --git a/AGENTS.md b/AGENTS.md index 8f25df70..d1df8f08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,7 +133,9 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome — because a rewrite that costs nothing still produces different state and a different receipt. The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). + A frame the inspector answers itself is the one place a difference across the callback is not the measurement, because no frame is built and the whole result is the inspector's: the shim stages the envelope the answering callback was handed, and `inspect_frame_init` settles the gas the result finally carries against it on the result lane — which also covers whatever of an edit to the inputs survives into a guard's replacement result, and which is zero for the echo convention every tool that intercepts follows. What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. + Two numbers inside the gas objects the shim *does* hold are known not to be covered — a `Gas`'s `refunded` and its EIP-8037 `reservoir` — and `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration that names them, pinned by `tests/rex7/gas_surface.rs`. The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 5d414657..0919ca3e 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -49,7 +49,7 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | Gas written into a live interpreter's counter (`initialize_interp`, `step`, `step_end`, `log_full`) | Supported | `InspectorLedger::gas`, at the callback boundary | nothing: the checkpoint baseline shifts by the same amount, and the gas clamp is re-derived on the spot so injected gas buys no compute headroom | | A frame input's `gas_limit`, raised or lowered (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::env`, at the callback boundary | nothing: a frame's compute budget comes from the tracker, not from its gas limit | | A frame input's semantic fields — target, caller, value, scheme, calldata, static flag (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing: it changes what the frame does, not what it costs | -| A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`; nothing on the `env` lane — the edited inputs never reach a frame — and the outcome's own gas on the `result` lane | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | +| A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`; nothing on the `env` lane — the edited inputs never reach a frame — and the gas the outcome carries on the `result` lane, measured against the envelope the answering callback was handed rather than as a difference across it | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | | A finished frame result's remaining gas (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::result`, at the frame's settlement point rather than at the callback boundary | nothing | | A finished frame result's returned output (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing | | A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | @@ -75,11 +75,49 @@ What the counter no longer speaks for, the action does, and the shim measures bo A frame holds `counter` with no action pending, `counter + f.gas_limit` with a `NewFrame` action, and the action's own copy with a `Return` one (`inspector.rs::held`); the counter lane books the counter's movement exactly when the EVM will read it again, and the action lane books the rest. The two together account for every unit of gas the frame holds, whatever the callback did to the action's shape. +### Every gas an inspector can reach + +The shape table above is written over rewrites this repository has thought of. +This one is written over the `Inspector` trait's own signatures, and is closed: `tests/rex7/gas_surface.rs` pins it against what upstream's derived `Debug` renders, field by field, and fails on one that has no verdict here. + +Read it by the object the gas sits in. +The first six rows are the measured lanes; the rest are the numbers that share those objects, each with the reason it needs no lane — or, for two of them, the statement that it does and has none. + +| Gas carrier | Reachable at | Verdict | +| --- | --- | --- | +| `Interpreter::gas` → `remaining` | `initialize_interp`, `step`, `step_end`, `log_full` | `InspectorLedger::gas`, at the callback boundary, when the action the callback left behind leaves the counter live | +| A pending `InterpreterAction::Return(_)`'s `gas` → `remaining` | `step_end` | `InspectorLedger::result`, staged and settled at the frame's settlement point | +| A pending `InterpreterAction::NewFrame(_)`'s `gas_limit` | `step_end` | `InspectorLedger::env`, staged and booked at the child's `frame_start` | +| `FrameInput` / `CallInputs` / `CreateInputs` → `gas_limit` | `frame_start`, `call`, `create` | `InspectorLedger::env`, at the callback boundary — unless the same callback answers the frame, in which case this value is the baseline the row below is measured against | +| The `Option` / `Option` / `Option` a callback **returns** → `gas` → `remaining` | `frame_start`, `call`, `create` | `InspectorLedger::result`, settled at `finalize_frame` against that baseline | +| `FrameResult` / `CallOutcome` / `CreateOutcome` → `result.gas` → `remaining` | `frame_end`, `call_end`, `create_end` | `InspectorLedger::result`, at the frame's settlement point | +| Every `Gas` above → `refunded` | every callback that holds one | **Not closed.** A refund written here travels to the caller on frame return and reaches the receipt's gas used, while the conservation law — stated over `limit - remaining` — stays closed and every ledger lane stays zero. | +| Every `Gas` above → `reservoir`, and `CallInputs` / `CreateInputs` → `reservoir` | every callback that holds one | **Not closed.** EIP-8037 state gas is structurally zero on every `MegaETH` path, so a reservoir an inspector writes is gas the transaction never funded; it moves the envelope, and the terminal cross-check trips on it with every ledger lane zero. | +| Every `Gas` above → `gas_limit` | every callback that holds one | Inert. op-revm normalises the top-level gas object to the transaction's own limit before the settlement point, and no REX7 lane reads a frame's limit; the two that do are the REX4 legacy stipend's burn and rescue caps, which REX5 mode does not take. | +| Every `Gas` above → `state_gas_spent` | every callback that holds one | Inert, for the same reason as `reservoir`'s spending side: with EIP-8037 off, nothing reads it. | +| Every `Gas` above → `memory` (`MemoryGas`) | every callback that holds one | Not a budget but a memo of the interpreter's memory size. Editing it without editing the memory desynchronises the two and the EVM reads out of bounds — the stack-and-memory row of the shape table, not a gas lane. | +| `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | +| The interpreter's `stack`, `memory`, `return_data`, `input`, `runtime_flag`, `extend` | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | +| `&mut CTX` — the journal, and through `MegaContext`'s `DerefMut` the transaction, the block, the configuration and `MegaETH`'s own trackers | every callback but `selfdestruct` | Not gas the EVM handed over. Unmeasured for the reason the journal is: telling whether any of it came back changed needs a snapshot of unbounded state that no callback boundary can take at a cost the inspected path can carry. The gas schedule is the exception — the schedule pin rejects a rewritten one, at the next transaction rather than within this one. | +| Everything passed by value (`Log`; `selfdestruct`'s three arguments) and the inputs the `*_end` callbacks take by shared reference | — | No mutable reach at all. | + +**The two open rows are open on purpose.** +They were found by this audit, are not what the interception lane closed, and are recorded rather than fixed so that closing them is a decision with an owner. +`tests/rex7/gas_surface.rs::test_the_open_gaps_are_the_ones_the_table_names` names them, so closing one has to touch that test and this table together. + +**What the closure pin does and does not reach.** +A field upstream adds to any of these structs shows up in its `Debug` rendering, matches no row, and fails the test by name. +A variant upstream adds to `InterpreterAction`, `FrameInput` or `FrameResult` fails the build, because the module matches all three exhaustively with no catch-all. +A *callback* upstream adds to the `Inspector` trait does neither — the trait gives every method a default body, so an unimplemented one silently does nothing — which is why the obligation below is written out. + ### Rules for changing this - **Add the shim's counterpart when adding an `Inspector` callback.** An unwrapped callback is an unmeasured hole, not a compile error. `tests/rex7/inspector_cheat_matrix.rs` enumerates every callback × shape pair and fails on one that is neither covered nor excused, which is what turns a new callback into a red test. +- **On a revm bump, re-read the trait's method list against `tests/rex7/gas_surface.rs`'s `CALLBACKS`, and give any new callback a row in the shape table and a column in the cheat matrix.** + This is the one direction no pin reaches, and it is the direction that adds reach. + The field-level and variant-level pins in the same file cover everything else, and both fail loudly on their own. - **Book a result rewrite from the frame's settlement point, not from the callback boundary.** Whether such an edit moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. The gas an intercepting callback puts into a synthetic outcome travels through that same lane. diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index e978f24e..2a2ff34b 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -105,6 +105,14 @@ pub struct InspectorLedger { /// The same lane carries an edit made one step earlier, to the gas inside a pending `Return` /// action. That action *is* the frame's result a moment later, so the two are one number /// measured on either side of the classification, and they settle together. + /// + /// And it carries the gas of a result the inspector produced outright, by answering a frame + /// with a synthetic outcome. That one is not a difference across a callback — no frame is + /// built, so there is no EVM-produced number on the other side — but against the envelope the + /// answering callback was handed, which the transaction did fund. The two are the same + /// question either way, and they settle at the same point for the same reason: a returning or + /// reverting outcome hands its gas back to the caller, a halting one hands nothing back and + /// the whole envelope is destroyed whatever figure the outcome claimed. pub result: i128, /// How many rewrites the shim refused because their shape is forbidden. diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs new file mode 100644 index 00000000..ec9967e0 --- /dev/null +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -0,0 +1,531 @@ +//! The closed enumeration of gas the `Inspector` trait puts within an inspector's reach. +//! +//! `inspector_cheat_matrix.rs` asks whether every callback × *rewrite shape* pair is covered. That +//! question is answered over a shape list this repository writes down, so it can only be as +//! complete as that list. This module asks the question one level below it, over a list +//! *upstream* writes down: is there a number that carries gas, reachable through an argument some +//! callback is handed, that nothing in `MegaETH` has classified? +//! +//! # The two levels the enumeration has +//! +//! - **Shapes.** Which objects the EVM hands a callback that carry gas at all. Every one of them +//! arrives through an enum — `InterpreterAction`, `FrameInput`, `FrameResult` — so an exhaustive +//! match with no catch-all is a compile-time pin: a variant added upstream stops the build. +//! - **Fields.** Which numbers inside those objects carry gas. Rust cannot enumerate a foreign +//! struct's fields, but a derived `Debug` renders every one of them by name, so a snapshot of +//! that name set against the classification table is a pin with the same reach: a field added +//! upstream appears in the rendering, fails to match a table row, and the test names it. +//! +//! # What the pins cannot reach, and what covers it instead +//! +//! A callback *added* to the `Inspector` trait is not a compile error anywhere — the trait gives +//! every method a default body, so an unimplemented one silently does nothing and an unwrapped one +//! is silently unmeasured. [`test_the_callback_set_is_the_one_the_shim_wraps`] pins the set that +//! exists today by overriding all of it, which catches a rename or a removal at compile time and +//! an addition only through the upgrade obligation stated in `src/evm/AGENTS.md`. That obligation +//! is the reason the classification table lives there rather than here. + +use revm::{ + handler::FrameResult, + interpreter::{ + interpreter::EthInterpreter, CallInput, CallInputs, CallOutcome, CallScheme, CallValue, + CreateInputs, CreateOutcome, CreateScheme, FrameInput, Gas, InstructionResult, Interpreter, + InterpreterAction, InterpreterResult, InterpreterTypes, + }, + primitives::{Address, Bytes, Log, U256}, + Inspector, +}; +use std::{collections::BTreeSet, string::String, vec::Vec}; + +// --- the classification ------------------------------------------------------------------------ + +/// What `MegaETH` does about one field of one object an inspector can reach. +/// +/// The whole point of the enum is that there is no fifth arm and no catch-all: a field is +/// measured, or it carries no gas, or it carries gas that reaches nothing `MegaETH` reports, or it +/// is a hole with a name. "Nobody looked at it" is not one of the options. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Coverage { + /// Measured, and booked on the named `InspectorLedger` lane. + Lane(&'static str), + /// Not a gas quantity. + NotGas(&'static str), + /// A gas quantity that moves nothing `MegaETH` reports, with the reason it cannot. + Inert(&'static str), + /// A gas quantity that moves what `MegaETH` reports, and that no lane books. + /// + /// Carrying this verdict in the type is deliberate. A gap named in a table is a gap someone + /// can close; a gap that is merely absent from a table is one nobody knows about. + NotClosed(&'static str), +} + +/// The five numbers a `Gas`'s tracker holds. +/// +/// `remaining` is the one every lane the shim books is defined over. The other four are reachable +/// through exactly the same `&mut Gas`, on all four of the objects the table in `src/evm/AGENTS.md` +/// lists, and the verdicts here are what each was measured to do. +const GAS_TRACKER_FIELDS: [(&str, Coverage); 5] = [ + ( + "remaining", + Coverage::Lane( + "gas / env / result, by which object holds the `Gas` and how its frame ends", + ), + ), + ( + "gas_limit", + Coverage::Inert( + "op-revm normalises the top-level gas object to the transaction's own limit before \ + the settlement point, and no REX7 lane reads a frame's limit — the two that do are \ + the REX4 legacy stipend's burn and rescue caps, which REX5 mode does not take", + ), + ), + ( + "refunded", + Coverage::NotClosed( + "a refund written at any callback that holds a `Gas` travels to the caller on frame \ + return and reaches the receipt's gas used, while the conservation law — stated over \ + `limit - remaining` — stays closed and every ledger lane stays zero", + ), + ), + ( + "reservoir", + Coverage::NotClosed( + "EIP-8037 state gas is structurally zero on every MegaETH path, so a reservoir an \ + inspector writes is gas the transaction never funded; it moves the envelope and the \ + terminal cross-check trips on it, with every ledger lane zero", + ), + ), + ( + "state_gas_spent", + Coverage::Inert( + "the counterpart of `reservoir` on the spending side, and dead for the same reason: \ + with EIP-8037 off nothing reads it", + ), + ), +]; + +/// The memoisation of how far a frame's memory has been paid for. +const MEMORY_GAS_FIELDS: [(&str, Coverage); 2] = [ + ( + "words_num", + Coverage::NotGas( + "a memo of the interpreter's memory size, not a budget: editing it without editing \ + the memory desynchronises the two and the EVM reads out of bounds, which is the \ + stack-and-memory row of the table rather than a gas lane", + ), + ), + ("expansion_cost", Coverage::NotGas("the memo's other half, and dead for the same reason")), +]; + +/// The two halves of a `Gas`. +const GAS_FIELDS: [(&str, Coverage); 2] = [ + ("tracker", Coverage::NotGas("a container; its own fields are classified separately")), + ("memory", Coverage::NotGas("a container; its own fields are classified separately")), +]; + +/// Everything a call frame is built from. +const CALL_INPUTS_FIELDS: [(&str, Coverage); 12] = [ + ( + "gas_limit", + Coverage::Lane( + "env, at the callback boundary — or, when the same callback answers the frame itself, \ + the baseline the interception's own gas is settled against", + ), + ), + ( + "reservoir", + Coverage::NotClosed( + "the child frame's EIP-8037 state-gas pool, and gas the caller was never debited for \ + in exactly the way a raised `gas_limit` is; no lane books it", + ), + ), + ("input", Coverage::NotGas("what the frame does")), + ("return_memory_offset", Coverage::NotGas("what the frame does")), + ("bytecode_address", Coverage::NotGas("what the frame does")), + ("known_bytecode", Coverage::NotGas("what the frame does")), + ("target_address", Coverage::NotGas("what the frame does")), + ("caller", Coverage::NotGas("what the frame does")), + ("value", Coverage::NotGas("what the frame does")), + ("scheme", Coverage::NotGas("what the frame does")), + ("is_static", Coverage::NotGas("what the frame does")), + ( + "charged_new_account_state_gas", + Coverage::NotGas( + "an EIP-8037 refund flag rather than an amount; the rewrite comparison books it as an \ + intervention like any other semantic field", + ), + ), +]; + +/// Everything a creation frame is built from. +const CREATE_INPUTS_FIELDS: [(&str, Coverage); 8] = [ + ("gas_limit", Coverage::Lane("env, exactly as a call's")), + ("reservoir", Coverage::NotClosed("a creation's copy of the call form above")), + ("caller", Coverage::NotGas("what the frame does")), + ("scheme", Coverage::NotGas("what the frame does")), + ("value", Coverage::NotGas("what the frame does")), + ("init_code", Coverage::NotGas("what the frame does")), + ("cached_address", Coverage::NotGas("a memo of the init code and scheme above")), + ("cached_init_code_hash", Coverage::NotGas("a memo of the init code above")), +]; + +/// Everything a finished frame hands back. +const INTERPRETER_RESULT_FIELDS: [(&str, Coverage); 3] = [ + ("gas", Coverage::NotGas("a container; its own fields are classified separately")), + ( + "result", + Coverage::NotGas("the classification, booked as an intervention rather than as gas"), + ), + ("output", Coverage::NotGas("the returned bytes, booked as an intervention")), +]; + +// --- reading a struct's field names off its `Debug` ---------------------------------------------- + +/// The field names a derived `Debug` rendering shows at the top level of the struct it renders. +/// +/// Depth-limited on purpose: a nested value's own fields belong to that value's own table row, and +/// pinning them here would make this test fail on churn in a type nothing reaches. +fn field_names(rendered: &str) -> BTreeSet { + let bytes: Vec = rendered.chars().collect(); + let mut names = BTreeSet::new(); + let mut depth = 0usize; + let mut index = 0usize; + while index < bytes.len() { + match bytes[index] { + '{' => depth += 1, + '}' => depth = depth.saturating_sub(1), + _ if depth == 1 => { + let starts_a_field = index > 0 && matches!(bytes[index - 1], '{' | ',') || + (index > 1 && + bytes[index - 1] == ' ' && + matches!(bytes[index - 2], '{' | ',')); + if starts_a_field && (bytes[index].is_ascii_lowercase() || bytes[index] == '_') { + let mut end = index; + while end < bytes.len() && + (bytes[end].is_ascii_alphanumeric() || bytes[end] == '_') + { + end += 1; + } + if bytes.get(end) == Some(&':') { + names.insert(bytes[index..end].iter().collect()); + index = end; + continue; + } + } + } + _ => {} + } + index += 1; + } + names +} + +/// Asserts that a struct's rendered field names are exactly the ones `table` classifies. +/// +/// Both directions are checked. A field upstream added is one the table has no verdict for; a row +/// the table keeps for a field upstream removed is a verdict about nothing, and stale prose about +/// a field that no longer exists is how a table stops being evidence. +fn assert_classified(what: &str, rendered: &str, table: &[(&str, Coverage)]) { + let seen = field_names(rendered); + let classified: BTreeSet = table.iter().map(|(name, _)| String::from(*name)).collect(); + let unclassified: Vec<&String> = seen.difference(&classified).collect(); + let vanished: Vec<&String> = classified.difference(&seen).collect(); + assert!( + unclassified.is_empty(), + "{what} has {} field(s) no verdict covers: {unclassified:?}\n rendered: {rendered}", + unclassified.len(), + ); + assert!( + vanished.is_empty(), + "{what} no longer has {} classified field(s): {vanished:?}", + vanished.len(), + ); + assert!(!seen.is_empty(), "{what}: the rendering parsed to nothing, so nothing was checked"); +} + +// --- the samples the renderings are taken from --------------------------------------------------- + +fn sample_gas() -> Gas { + Gas::new(1) +} + +fn sample_call_inputs() -> CallInputs { + CallInputs { + input: CallInput::Bytes(Bytes::new()), + return_memory_offset: 0..0, + gas_limit: 1, + reservoir: 0, + bytecode_address: Address::ZERO, + known_bytecode: Default::default(), + target_address: Address::ZERO, + caller: Address::ZERO, + value: CallValue::Transfer(U256::ZERO), + scheme: CallScheme::Call, + is_static: false, + charged_new_account_state_gas: false, + } +} + +fn sample_create_inputs() -> CreateInputs { + CreateInputs::new(Address::ZERO, CreateScheme::Create, U256::ZERO, Bytes::new(), 1, 0) +} + +fn sample_result() -> InterpreterResult { + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), sample_gas()) +} + +// --- the field-level pin ------------------------------------------------------------------------- + +/// Every field of every gas-carrying object an inspector is handed has a verdict. +/// +/// This is the closure the completeness table rests on. It is not a claim that the verdicts are +/// right — the tests in `measured_inspector.rs`, `inspector_cheat_matrix.rs` and +/// `interception_gas.rs` are — it is the claim that there is no field without one. +#[test] +fn test_every_field_of_every_gas_carrier_has_a_verdict() { + let gas = sample_gas(); + assert_classified("Gas", &std::format!("{gas:?}"), &GAS_FIELDS); + assert_classified("GasTracker", &std::format!("{:?}", gas.tracker()), &GAS_TRACKER_FIELDS); + assert_classified("MemoryGas", &std::format!("{:?}", gas.memory()), &MEMORY_GAS_FIELDS); + assert_classified( + "CallInputs", + &std::format!("{:?}", sample_call_inputs()), + &CALL_INPUTS_FIELDS, + ); + assert_classified( + "CreateInputs", + &std::format!("{:?}", sample_create_inputs()), + &CREATE_INPUTS_FIELDS, + ); + assert_classified( + "InterpreterResult", + &std::format!("{:?}", sample_result()), + &INTERPRETER_RESULT_FIELDS, + ); +} + +/// The parser the pin rests on reads what it is supposed to read. +/// +/// Without this, a change to `Debug`'s formatting that made the parser return nothing would turn +/// every assertion above into a tautology — and `assert_classified`'s emptiness check would be the +/// only thing standing in the way, which is one check too few for the thing the whole module is +/// built on. +#[test] +fn test_the_field_reader_reads_the_top_level_and_stops_there() { + let names = field_names( + "Outer { first: 1, nested: Inner { hidden: 2, deeper: Deepest { buried: 3 } }, \ + last: Tuple(0x00, Other { also_hidden: 4 }) }", + ); + let expected: BTreeSet = + ["first", "nested", "last"].into_iter().map(String::from).collect(); + assert_eq!(names, expected, "only the outermost struct's own fields may be read"); + assert!(field_names("NoFields").is_empty(), "a unit struct has no fields to read"); +} + +/// The gaps the table carries are the gaps it says it carries. +/// +/// A verdict is a claim someone has to be able to act on, so the set of open ones is pinned by +/// name rather than by count: closing one, or discovering another, has to touch this list and the +/// table in `src/evm/AGENTS.md` together. +#[test] +fn test_the_open_gaps_are_the_ones_the_table_names() { + let mut open = Vec::new(); + for (what, table) in [ + ("Gas", GAS_FIELDS.as_slice()), + ("GasTracker", GAS_TRACKER_FIELDS.as_slice()), + ("MemoryGas", MEMORY_GAS_FIELDS.as_slice()), + ("CallInputs", CALL_INPUTS_FIELDS.as_slice()), + ("CreateInputs", CREATE_INPUTS_FIELDS.as_slice()), + ("InterpreterResult", INTERPRETER_RESULT_FIELDS.as_slice()), + ] { + for (field, coverage) in table { + if matches!(coverage, Coverage::NotClosed(_)) { + open.push(std::format!("{what}::{field}")); + } + } + } + open.sort(); + assert_eq!( + open, + [ + "CallInputs::reservoir", + "CreateInputs::reservoir", + "GasTracker::refunded", + "GasTracker::reservoir", + ], + "the open gaps moved; `src/evm/AGENTS.md`'s table has to move with them", + ); +} + +// --- the shape-level pin ------------------------------------------------------------------------- + +/// Which object a gas-carrying shape puts within reach. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Carrier { + /// A `Gas`, whose fields the tables above classify. + Gas, + /// A frame's `gas_limit`, a bare `u64`. + Envelope, + /// Nothing that carries gas. + None, +} + +/// The gas a pending action carries, by variant — no catch-all, so a variant revm adds stops the +/// build here and the shim's own `held` has to grow an arm with it. +const fn action_carrier(action: &InterpreterAction) -> Carrier { + match action { + InterpreterAction::Return(_) => Carrier::Gas, + InterpreterAction::NewFrame(input) => frame_input_carrier(input), + } +} + +/// The gas a frame input carries, by variant. +const fn frame_input_carrier(input: &FrameInput) -> Carrier { + match input { + FrameInput::Call(_) | FrameInput::Create(_) => Carrier::Envelope, + FrameInput::Empty => Carrier::None, + } +} + +/// The gas a frame result carries, by variant. +const fn frame_result_carrier(result: &FrameResult) -> Carrier { + match result { + FrameResult::Call(_) | FrameResult::Create(_) => Carrier::Gas, + } +} + +/// Every gas-carrying shape the EVM hands a callback is reached through an enum this module +/// matches exhaustively. +/// +/// The assertions are the small half; the compile is the large one. A variant added to any of the +/// three enums is a build failure here, which is the only mechanical warning `MegaETH` gets that +/// upstream grew a new way to carry gas across a callback boundary. +#[test] +fn test_every_gas_carrying_shape_is_matched_without_a_catch_all() { + let call = FrameInput::Call(std::boxed::Box::new(sample_call_inputs())); + let create = FrameInput::Create(std::boxed::Box::new(sample_create_inputs())); + + assert_eq!(frame_input_carrier(&call), Carrier::Envelope); + assert_eq!(frame_input_carrier(&create), Carrier::Envelope); + assert_eq!(frame_input_carrier(&FrameInput::Empty), Carrier::None); + + assert_eq!(action_carrier(&InterpreterAction::Return(sample_result())), Carrier::Gas); + assert_eq!(action_carrier(&InterpreterAction::NewFrame(call)), Carrier::Envelope); + + let call_result = FrameResult::Call(CallOutcome::new(sample_result(), 0..0)); + let create_result = FrameResult::Create(CreateOutcome::new(sample_result(), None)); + assert_eq!(frame_result_carrier(&call_result), Carrier::Gas); + assert_eq!(frame_result_carrier(&create_result), Carrier::Gas); +} + +// --- the callback-set snapshot ------------------------------------------------------------------- + +/// Every callback the `Inspector` trait has today, in the order the trait declares them. +/// +/// The same twelve rows `inspector_cheat_matrix.rs` runs its shapes over, restated here because +/// the two pins answer different questions: that one asks whether each row is exercised, this one +/// asks whether the row set is still the trait's. +const CALLBACKS: [&str; 12] = [ + "initialize_interp", + "step", + "step_end", + "log", + "log_full", + "frame_start", + "frame_end", + "call", + "call_end", + "create", + "create_end", + "selfdestruct", +]; + +/// Overrides every callback, so that the set is pinned by the compiler rather than by the list. +struct EveryCallback { + seen: Vec<&'static str>, +} + +impl Inspector for EveryCallback { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("initialize_interp"); + } + + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("step"); + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("step_end"); + } + + fn log(&mut self, _context: &mut CTX, _log: Log) { + self.seen.push("log"); + } + + fn log_full(&mut self, _interp: &mut Interpreter, _context: &mut CTX, _log: Log) { + self.seen.push("log_full"); + } + + fn frame_start( + &mut self, + _context: &mut CTX, + _frame_input: &mut FrameInput, + ) -> Option { + self.seen.push("frame_start"); + None + } + + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + _frame_result: &mut FrameResult, + ) { + self.seen.push("frame_end"); + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.seen.push("call"); + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.seen.push("call_end"); + } + + fn create(&mut self, _context: &mut CTX, _inputs: &mut CreateInputs) -> Option { + self.seen.push("create"); + None + } + + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.seen.push("create_end"); + } + + fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) { + self.seen.push("selfdestruct"); + } +} + +/// The callback set the shim wraps is the trait's, and it is the set the tables are written over. +/// +/// A callback upstream renames or removes stops [`EveryCallback`] from compiling. A callback +/// upstream *adds* does not — the trait's default bodies see to that — and there is no compile-time +/// construct that would, which is why the upgrade obligation in `src/evm/AGENTS.md` is what covers +/// that direction, and why this list is written out rather than derived. +#[test] +fn test_the_callback_set_is_the_one_the_shim_wraps() { + let mut probe = EveryCallback { seen: Vec::new() }; + let inspector: &mut dyn Inspector<(), EthInterpreter> = &mut probe; + inspector.selfdestruct(Address::ZERO, Address::ZERO, U256::ZERO); + assert_eq!(probe.seen, ["selfdestruct"], "the override must be the one that runs"); + + assert_eq!(CALLBACKS.len(), 12, "the trait's callback count is part of the snapshot"); + let unique: BTreeSet<&str> = CALLBACKS.into_iter().collect(); + assert_eq!(unique.len(), CALLBACKS.len(), "no callback may be listed twice"); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index cb00a65b..0ddff06f 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -31,6 +31,9 @@ //! interpreter counter or a frame's gas limit is measured at the callback boundary, booked, and //! kept out of enforcement, with the clamp re-derived on the spot; reviving a failed creation is //! refused; an observation-only inspector is bit-identical to no inspector at all. +//! - `gas_surface` — the closed enumeration one level below the cheat matrix: every field of every +//! gas-carrying object an inspector callback is handed, each with a verdict, pinned against what +//! upstream's own `Debug` renders. //! - `gas_leakage` — the three paths a per-frame gas mechanism can leak through (interception, //! TX-level rescue, frame return), each with a clamp outstanding. //! - `opcode_set_parity` — all 256 opcodes probed under both specs, so the REX7 table cannot gain @@ -93,6 +96,7 @@ mod frame_init_reject_burn; mod frame_loop_parity; mod gas_clamp; mod gas_leakage; +mod gas_surface; mod guard_pass_static_gas; mod inspector_cheat_matrix; mod inspector_settlement_window; From fe8ace2401c4a5831fb86ddcdafe26feff0afaf3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 18:19:56 +0800 Subject: [PATCH 134/208] style(evm): unmangle the interception assert message --- crates/mega-evm/src/evm/execution.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index eaef910d..98b08134 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1855,7 +1855,8 @@ where let envelope = additional_limit.borrow_mut().take_inspector_interception_envelope(); debug_assert!( envelope.is_some() || matches!(frame_init.frame_input, FrameInput::Empty), - "every inspector is wrapped in the measurement shim, which stages the envelope of any frame a callback answers itself", + "every inspector is wrapped in the measurement shim, which stages the envelope \ + of any frame a callback answers itself", ); // Inspector intercepted — `frame_init()` is skipped entirely, so neither From 4ff042400c8640428fd574f0d4c569b676815d9e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 18:37:08 +0800 Subject: [PATCH 135/208] test(rex7): pin the interception lane on a frozen spec --- crates/mega-evm/tests/rex7/gas_surface.rs | 3 +- .../mega-evm/tests/rex7/interception_gas.rs | 70 +++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index ec9967e0..8b2bc57d 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -195,7 +195,8 @@ fn field_names(rendered: &str) -> BTreeSet { '{' => depth += 1, '}' => depth = depth.saturating_sub(1), _ if depth == 1 => { - let starts_a_field = index > 0 && matches!(bytes[index - 1], '{' | ',') || + // A field name follows the opening brace or a comma, with at most one space. + let starts_a_field = (index > 0 && matches!(bytes[index - 1], '{' | ',')) || (index > 1 && bytes[index - 1] == ' ' && matches!(bytes[index - 2], '{' | ',')); diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index 51a9a950..b2fc23aa 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -87,9 +87,12 @@ fn tx() -> MegaTransaction { tx } -fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = MegaContext::new(db, MegaSpecId::REX7) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); +fn context_for( + db: &mut MemoryDatabase, + spec: MegaSpecId, +) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = + MegaContext::new(db, spec).with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec)); context.modify_chain(|chain| { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); @@ -115,16 +118,23 @@ fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { } } -fn transact(mut db: MemoryDatabase, inspector: &mut I) -> Reading +fn transact_on(spec: MegaSpecId, mut db: MemoryDatabase, inspector: &mut I) -> Reading where I: for<'a> Inspector>, { - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(inspector); + let mut evm = MegaEvm::new(context_for(&mut db, spec)).with_inspector(inspector); let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); reading } +fn transact(db: MemoryDatabase, inspector: &mut I) -> Reading +where + I: for<'a> Inspector>, +{ + transact_on(MegaSpecId::REX7, db, inspector) +} + /// A straight run of plain opcodes that always succeeds. fn plain_run_code(pairs: usize) -> Bytes { let mut builder = BytecodeBuilder::default(); @@ -506,3 +516,53 @@ fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { ); assert_identity("raised then intercepted", &reading); } + +/// The lane reports on a frozen spec too, and reporting it settles nothing there. +/// +/// The measurement is not REX7-gated, and neither are the two lanes it joins: `InspectorLedger` is +/// what the canonical block path's guard reads, so a frame an inspector answered has to be visible +/// on it whatever spec is executing. What is REX7's alone is the settlement the lane feeds — the +/// envelope a refused frame init decides the fate of. REX6 derives nothing from the envelope and +/// books no destroyed remainder, so what it reports is what it always reported. +/// +/// The transaction's own gas does follow the figure the inspector wrote, on both specs. That is +/// the EVM handing the caller back what the result carries, which is upstream's arithmetic rather +/// than `MegaETH`'s, and it is the movement the lane exists to account for rather than to prevent. +#[test] +fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { + let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); + let echo = transact_on(MegaSpecId::REX6, call_fixture(), &mut echoing); + let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let half = transact_on(MegaSpecId::REX6, call_fixture(), &mut halving); + + assert_eq!(echoing.intercepted, 1, "fixture check"); + assert_eq!(halving.intercepted, 1, "fixture check"); + assert_eq!( + echo.ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "REX6: an echoed envelope moves no gas here either", + ); + assert_eq!( + half.ledger, + InspectorLedger { + result: Sizing::Half.expected_delta(FORWARDED), + interventions: 1, + ..InspectorLedger::default() + }, + "REX6: the lane reports, because the block guard has to see this frame on every spec", + ); + assert_eq!( + (echo.destroyed, half.destroyed), + (0, 0), + "REX6 has no destroyed remainder to book, on either sizing", + ); + assert_eq!( + echo.compute_gas, half.compute_gas, + "and its compute total does not follow the figure the inspector wrote", + ); + assert_eq!( + half.total_gas_spent - echo.total_gas_spent, + FORWARDED / 2, + "the caller really did lose the half the outcome withheld — that is the EVM's arithmetic", + ); +} From ba0933109451475c6c35e1a82a07f5ee64e3892a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:10:00 +0800 Subject: [PATCH 136/208] test(rex7): pin the refund and state-gas surfaces the ledger cannot see The conservation law is stated over `total_gas_spent`, which is `limit - remaining`. A receipt carries two more figures an inspector can reach through every `Gas` it is handed: the EIP-3529 refund, and the EIP-8037 state-gas dimension. Neither had a lane, so a refund rewrite moved what the sender pays with an all-zero ledger and was admitted into a block, and a reservoir write moved the envelope and tripped the terminal cross-check. Declares the three lanes and the shapes they have to book. The lanes are empty here; nothing writes them yet. --- crates/mega-evm/src/limit/inspector_ledger.rs | 119 +++- .../tests/block_executor/inspector_guard.rs | 65 ++ crates/mega-evm/tests/rex7/main.rs | 5 + .../tests/rex7/refund_and_state_gas.rs | 579 ++++++++++++++++++ 4 files changed, 760 insertions(+), 8 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/refund_and_state_gas.rs diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 2a2ff34b..caf99472 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -35,6 +35,22 @@ /// shim was handed came back different. It does not say the transaction is the one the EVM would /// have produced alone. /// +/// # The three numbers a receipt carries +/// +/// A transaction's receipt reports its spent envelope, the refund applied to it, and — under +/// EIP-8037 — the state gas it consumed. The lanes are grouped by which of the three a rewrite +/// moves, because that is what decides whether the conservation law can see it: +/// +/// - [`gas`](Self::gas), [`env`](Self::env), [`result`](Self::result) and +/// [`reservoir`](Self::reservoir) move the envelope, and are summed into +/// [`conjured_gas`](Self::conjured_gas), the law's `I` term; +/// - [`refund`](Self::refund) moves the refund, which the law — stated over `limit - remaining` — +/// cannot see at all; +/// - [`state_gas`](Self::state_gas) moves the receipt's state-gas figure, which the law does not +/// reach either. +/// +/// All six are read by [`is_zero`](Self::is_zero), which is what the block guard asks. +/// /// # What consumes it /// /// - [`conjured_gas`](Self::conjured_gas) is the term the destroyed-remainder derivation adds to @@ -115,6 +131,71 @@ pub struct InspectorLedger { /// the whole envelope is destroyed whatever figure the outcome claimed. pub result: i128, + /// The EIP-8037 state-gas pool the transaction ends holding, which is gas nothing funded. + /// + /// `MegaETH` runs with EIP-8037 off on every path and every spec, so no instruction can charge + /// against a reservoir and no `MegaETH` site ever fills one: the reservoir a transaction ends + /// with is zero unless an inspector wrote it. What a non-zero one does is move the envelope — + /// the receipt reports `limit - remaining - reservoir` as spent, and the caller is reimbursed + /// `remaining + reservoir + refunded` — so this lane is summed into + /// [`conjured_gas`](Self::conjured_gas) alongside the three above. + /// + /// Unlike them it is settled once, at the transaction's own settlement point, rather than at a + /// callback boundary. Two facts make that the only sound reading. revm propagates a reservoir + /// between frames by *replacement* — a returning child's reservoir overwrites its caller's — + /// so an edit made while a `NewFrame` action is already pending is erased by the child that + /// action builds, and a boundary difference would book gas that moved nothing. And the + /// `state_gas_spent` counter converts into a reservoir on a frame that fails, at a site no + /// callback sees. Reading the final number instead covers both: it is exactly the part of + /// every edit that survived, and `MegaETH` contributes none of it, so no difference has to be + /// taken to isolate the inspector's share. + pub reservoir: i128, + + /// Net EIP-8037 state gas the inspector wrote into the `state_gas_spent` counters. + /// + /// The reservoir's counterpart on the spending side, and dead for the same reason `MegaETH` + /// never fills one — except at the two places revm reads it regardless of whether EIP-8037 is + /// enabled: a successful transaction reports its final value on the receipt, and a failing + /// frame folds it back into its caller's reservoir. + /// + /// The second of those two effects is already inside [`reservoir`](Self::reservoir) — the + /// final reservoir is read after the fold — so this lane carries the first, and is + /// deliberately not part of [`conjured_gas`](Self::conjured_gas): the receipt's state-gas + /// figure is not the envelope, and adding it to the law's `I` term would make the law + /// wrong by exactly this amount. Settled at the same point and for the same reasons as the + /// lane above. + pub state_gas: i128, + + /// Net gas the inspector wrote into the `refunded` counters of the `Gas` objects it is handed. + /// + /// A refund is the one number on a receipt the conservation law cannot see: the law is stated + /// over `total_gas_spent`, which is `limit - remaining` and which no refund enters. What a + /// refund does reach is `tx_gas_used` — what the sender actually pays — and the caller's + /// reimbursement. So the lane exists for [`is_zero`](Self::is_zero) and the block guard behind + /// it, and is deliberately kept out of [`conjured_gas`](Self::conjured_gas). + /// + /// # Nominal, in both senses + /// + /// The figure booked is what the inspector wrote, not what survived to the receipt. + /// + /// Not what survived the *cap*, because EIP-3529 caps the transaction's whole refund at a + /// fifth of what it burnt, over the sum of every refund the transaction accumulated, at a + /// point after the envelope is final and with no frame left standing. Splitting that cap + /// between the EVM's own refunds and an inspector's needs a priority rule the protocol + /// does not have — EVM-first, inspector-first and pro rata are all defensible, which means + /// none of them is a measurement. + /// + /// And not what survived the *frame chain*, because revm hands a frame's refund to its caller + /// only when the frame succeeded: an edit reaches the receipt exactly when every frame from + /// the one that was edited up to the top returns successfully. That is a condition no callback + /// boundary and no single settlement point can answer without a refund stack aligned to the + /// EVM's frame lifecycle, which is the machinery this ledger deliberately does not have. + /// + /// Both directions of the choice are safe here because the lane feeds no identity. + /// Over-stating it costs nothing; under-stating it would let a transaction whose receipt + /// an inspector moved into a block, which is the one thing the lane exists to prevent. + pub refund: i128, + /// How many rewrites the shim refused because their shape is forbidden. /// /// Today exactly one shape is: a `create_end` (or the `frame_end` after it) turning a @@ -153,7 +234,7 @@ impl InspectorLedger { /// derivation adds to the transaction's envelope. #[inline] pub const fn conjured_gas(&self) -> i128 { - self.gas + self.env + self.result + self.gas + self.env + self.result + self.reservoir } /// Whether the inspector left the transaction's gas accounting exactly as the EVM produced it. @@ -166,6 +247,9 @@ impl InspectorLedger { self.gas == 0 && self.env == 0 && self.result == 0 && + self.reservoir == 0 && + self.state_gas == 0 && + self.refund == 0 && self.rejected_rewrites == 0 && self.interventions == 0 } @@ -180,17 +264,36 @@ mod tests { /// unmoved in that case. #[test] fn test_conjured_gas_is_the_net_of_both_lanes() { - let ledger = InspectorLedger { - gas: 2_300, - env: -2_300, - result: 0, - rejected_rewrites: 0, - interventions: 0, - }; + let ledger = InspectorLedger { gas: 2_300, env: -2_300, ..InspectorLedger::default() }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); } + /// The reservoir is envelope-moving gas and joins the law's term; the refund and the + /// state-gas figure are not, and must not. + #[test] + fn test_only_the_envelope_moving_lanes_are_conjured_gas() { + let reservoir = InspectorLedger { reservoir: 10_000, ..InspectorLedger::default() }; + assert_eq!( + reservoir.conjured_gas(), + 10_000, + "a reservoir lowers the envelope the receipt reports, so the law needs it back", + ); + assert!(!reservoir.is_zero()); + + for ledger in [ + InspectorLedger { refund: 20_000, ..InspectorLedger::default() }, + InspectorLedger { state_gas: 5_000, ..InspectorLedger::default() }, + ] { + assert_eq!( + ledger.conjured_gas(), + 0, + "the law is stated over `limit - remaining`, which neither of these enters", + ); + assert!(!ledger.is_zero(), "but the block guard still has to see it: {ledger:?}"); + } + } + /// A refused rewrite moves no gas but must still show the transaction was not left alone. #[test] fn test_a_rejected_rewrite_alone_is_not_zero() { diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 7d398e83..1526d639 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -51,6 +51,9 @@ const CALLEE: Address = address!("1000000000000000000000000000000000000002"); /// Gas the injecting inspector writes into the interpreter's counter. const INJECTED: u64 = 7_000; +/// Refund the refund-writing inspector records. +const REFUNDED: i64 = 3_000; + /// Writes gas into the running interpreter's counter, once — the smallest rewrite that moves the /// ledger's [`gas`](InspectorLedger::gas) lane. #[derive(Default)] @@ -90,6 +93,28 @@ impl Inspector for CallFailer } } +/// Writes a refund into the running interpreter's counter, once — the rewrite that moves what the +/// sender pays without moving the envelope at all. +/// +/// Every gas lane stays at zero under this inspector, and so does the conservation law: the law is +/// stated over `total_gas_spent`, which is `limit - remaining`, and a refund enters neither term. +/// What moves is the receipt's `gas_used`, which is the number the sender is billed on — so a +/// gas-lane criterion would admit it and two nodes would disagree about a receipt. +#[derive(Default)] +struct RefundWriter { + applied: bool, +} + +impl Inspector for RefundWriter { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.applied { + return; + } + self.applied = true; + interp.gas.record_refund(REFUNDED); + } +} + /// Counts callbacks and changes nothing — the shape every tracer in production has. #[derive(Default)] struct Observer { @@ -231,6 +256,46 @@ fn test_run_transaction_refuses_an_inspector_adjusted_transaction() { ); } +/// The refusal is over the whole ledger, not over the lanes the conservation law reads. +/// +/// A refund rewrite is the shape that makes the distinction load-bearing: it leaves every gas lane +/// at zero, closes the law exactly as an uninspected run does, and still changes the number the +/// sender is billed. A node that ran this inspector and a node that did not would build the same +/// block with different receipts. +#[test] +fn test_run_transaction_refuses_a_refund_rewrite() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + RefundWriter::default(), + ); + + let tx = envelope(0); + let err = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect_err("the canonical path must refuse a transaction whose receipt was rewritten"); + + assert!(executor.evm().inspector.applied, "the fixture must reach the refund write"); + let ledger = expect_refusal(&err, *tx.hash()); + assert_eq!( + ledger.refund, + i128::from(REFUNDED), + "the refusal must carry the refund that was written", + ); + assert_eq!( + ledger.conjured_gas(), + 0, + "no gas moved: a gas-only criterion would have admitted this transaction", + ); + assert_eq!( + executor.block_limiter.block_compute_gas_used, 0, + "a refused transaction must leave the block's counters where they were", + ); +} + /// The other producer entry, reached through the `alloy_evm` trait rather than the inherent /// method: the two resolve their transaction sizes differently and share no body, so a guard on /// one says nothing about the other. diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 0ddff06f..77ff04d2 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -74,6 +74,10 @@ //! - `pre_execution_intrinsic_reject` — the one envelope-keeping synthetic halt REX7 cannot reach: //! for an ordinary transaction an intrinsic overrun is a validation error from REX5 on, and a //! validation error produces no receipt for any lane to account for. +//! - `refund_and_state_gas` — the two numbers on a receipt the conservation law cannot see: the +//! EIP-3529 refund, measured at the callback boundary because the EVM produces refunds too, and +//! the EIP-8037 state-gas dimension, settled from the transaction's final figures because +//! `MegaETH` produces none of it and revm propagates it by replacement. //! - `deposit_receipt_rewrite` — the transactions that break that last step. A failed OP deposit //! does get a receipt, rebuilt to report its whole gas limit after every settlement has run; the //! boundary that rebuilds it books the difference as destroyed without moving what enforces. @@ -111,3 +115,4 @@ mod opcode_set_parity; mod parity_shapes; mod pre_execution_intrinsic_reject; mod precompile_halt; +mod refund_and_state_gas; diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs new file mode 100644 index 00000000..e889011d --- /dev/null +++ b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs @@ -0,0 +1,579 @@ +//! The two numbers on a receipt that the conservation law cannot see, and the lanes that do. +//! +//! The law is stated over `total_gas_spent`, which is `limit - remaining`. A transaction's receipt +//! carries two more figures that arithmetic does not reach: the EIP-3529 refund, which decides what +//! the sender actually pays, and the EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and its +//! `state_gas_spent` counter — which decides how much of the envelope the receipt counts as spent +//! at all. +//! +//! Both are reachable from every callback that is handed a `Gas`, and both were unmeasured. The +//! shapes here are what the two lanes now book, and each pins the *reason* its lane is measured +//! where it is: +//! +//! - a **refund** is a quantity the EVM also produces, so only a difference across a callback +//! isolates the inspector's share — the lane is measured at the boundary, and is nominal in both +//! the senses that can make it differ from what reaches the receipt (the EIP-3529 cap, and the +//! chain of successful frame returns an edit has to survive); +//! - a **reservoir** is a quantity `MegaETH` never produces at all, and one revm propagates by +//! replacement rather than by accumulation, so a boundary difference would book edits the EVM +//! goes on to erase. The lane is settled once, from the number the transaction ends with, which +//! is exactly the surviving part and is the inspector's in whole. + +use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, + MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, + MegaTransactionOutcome, +}; +use revm::{ + bytecode::opcode::{CALL, POP, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + handler::EvmTr, + interpreter::{ + interpreter_types::LoopControl, CallInputs, CallOutcome, Interpreter, InterpreterAction, + InterpreterTypes, + }, + Inspector, +}; + +/// High enough that EVM gas is never what binds. +const TX_GAS_LIMIT: u64 = 100_000_000; +/// Gas the fixture's inner `CALL` forwards. +const INNER_CALL_GAS: u64 = 200_000; + +/// Refund an edit writes, small enough that the EIP-3529 cap does not clip it. +const REFUND: i64 = 2_000; +/// Refund the cap test writes, chosen to exceed a fifth of anything the fixture can burn. +const OVERSIZED_REFUND: i64 = 60_000; +/// The EIP-8037 pool a reservoir edit fills. +const RESERVOIR: u64 = 10_000; +/// The EIP-8037 spend counter a state-gas edit writes. +const STATE_GAS: i64 = 5_000; + +/// Slot the caller writes. +const TOP_SLOT: u64 = 0x10; +/// Slot the callee writes and keeps. +const CALLEE_SLOT: u64 = 0x20; +/// Slot the callee sets and clears, so its frame carries a refund the EVM produced. +const CLEARED_SLOT: u64 = 0x30; + +// --- what one run reports ---------------------------------------------------------------------- + +struct Reading { + result: ExecutionResult, + /// Receipt `gas_used`: the envelope less the refund, floored by EIP-7623. + gas_used: u64, + /// Receipt envelope, which is what the conservation law is stated over. + total_gas_spent: u64, + /// Receipt refund, after the EIP-3529 cap. + refunded: u64, + /// Receipt EIP-8037 state gas. + state_gas_spent: u64, + destroyed: u64, + terms: ConservationTerms, + ledger: InspectorLedger, +} + +/// The conservation identity, over the envelope the receipt reports. +fn assert_identity(label: &str, r: &Reading) { + assert_eq!( + r.terms.inspector_conjured_gas, + r.ledger.conjured_gas(), + "{label}: the law's `I` term is the ledger's net, and nothing else", + ); + assert_eq!( + r.terms.envelope_for(r.destroyed), + i128::from(r.total_gas_spent), + "{label}: the law must close against the envelope the receipt reports ({})", + r.terms, + ); +} + +fn tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX7) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context +} + +fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { + assert_eq!( + outcome.inspector_ledger, + limit.inspector_ledger(), + "the outcome must report the ledger the shim booked, unchanged", + ); + let gas = *outcome.result_and_state.result.gas(); + Reading { + result: outcome.result_and_state.result, + gas_used: gas.tx_gas_used(), + total_gas_spent: gas.total_gas_spent(), + refunded: gas.inner_refunded(), + state_gas_spent: gas.state_gas_spent_final(), + destroyed: outcome.compute_gas_destroyed, + terms: limit.conservation_terms(), + ledger: outcome.inspector_ledger, + } +} + +// --- the fixture ------------------------------------------------------------------------------- + +/// How the fixture's callee ends, which is what decides whether its refund travels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Callee { + /// Writes storage, produces a refund by clearing a slot it just set, and returns. + Returning, + /// Writes storage and reverts, so the EVM discards everything the frame held. + Reverting, +} + +fn caller_code() -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(u128::from(INNER_CALL_GAS)) + .append(CALL) + .append(POP) + .sstore(U256::from(TOP_SLOT), U256::from(1u64)) + .append(STOP) + .build() +} + +fn callee_code(callee: Callee) -> Bytes { + let builder = BytecodeBuilder::default() + .sstore(U256::from(CALLEE_SLOT), U256::from(1u64)) + // Set and clear, so the frame ends holding a refund the EVM itself produced. + .sstore(U256::from(CLEARED_SLOT), U256::from(1u64)) + .sstore(U256::from(CLEARED_SLOT), U256::ZERO); + match callee { + Callee::Returning => builder.append(STOP).build(), + Callee::Reverting => builder.revert().build(), + } +} + +fn db_for(callee: Callee) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, caller_code()) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, callee_code(callee)) +} + +// --- the edit ---------------------------------------------------------------------------------- + +/// One edit, applied once, to one of the `Gas` objects a callback is handed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Edit { + /// Add to the running interpreter's refund counter. + RefundAtStep(i64), + /// Add to the finished inner call's refund counter. + RefundAtCallEnd(i64), + /// Fill the running interpreter's EIP-8037 pool. + ReservoirAtStep, + /// Fill it at the one moment the frame is holding a `NewFrame` action, whose child overwrites + /// the pool on the way back. + ReservoirAtSuspension, + /// Fill the pool the inner call's inputs seed the child frame with. + ReservoirOnInputs, + /// Fill the finished inner call's pool. + ReservoirAtCallEnd, + /// Write the running interpreter's EIP-8037 spend counter. + StateGasAtStep, + /// Write the finished inner call's spend counter. + StateGasAtCallEnd, +} + +/// Applies one [`Edit`], once, and records that it landed. +#[derive(Debug)] +struct Editor { + edit: Edit, + fired: u32, + steps: u64, +} + +impl Editor { + const fn new(edit: Edit) -> Self { + Self { edit, fired: 0, steps: 0 } + } +} + +impl Inspector for Editor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.fired > 0 || self.steps != 4 { + return; + } + match self.edit { + Edit::RefundAtStep(amount) => interp.gas.record_refund(amount), + Edit::ReservoirAtStep => interp.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtStep => interp.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || self.edit != Edit::ReservoirAtSuspension { + return; + } + // The one window where the pool the frame holds is not the pool that travels: the child + // this action builds was already sized from the pre-edit value, and its own pool + // overwrites this one when it returns. + if !matches!(interp.bytecode.action(), Some(InterpreterAction::NewFrame(_))) { + return; + } + interp.gas.set_reservoir(RESERVOIR); + self.fired += 1; + } + + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if self.fired > 0 || inputs.target_address != CALLEE || self.edit != Edit::ReservoirOnInputs + { + return None; + } + inputs.reservoir += RESERVOIR; + self.fired += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != CALLEE { + return; + } + match self.edit { + Edit::RefundAtCallEnd(amount) => outcome.result.gas.record_refund(amount), + Edit::ReservoirAtCallEnd => outcome.result.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtCallEnd => outcome.result.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } +} + +/// Runs the fixture with no inspector at all. +fn transact_plain(callee: Callee) -> Reading { + let mut db = db_for(callee); + let mut evm = MegaEvm::new(context(&mut db)); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +/// Runs it with one edit applied, asserting the edit landed exactly once. +fn transact_edited(callee: Callee, edit: Edit) -> Reading { + let mut db = db_for(callee); + let mut editor = Editor::new(edit); + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut editor); + let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + assert_eq!( + alloy_evm::Evm::inspector(&evm).fired, + 1, + "{edit:?}: the fixture must reach the edit's callback exactly once", + ); + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + reading +} + +// --- the fixture's own assumptions --------------------------------------------------------------- + +/// The uninspected run is what the cells below assume it is: it succeeds, it produces a refund of +/// its own, and it reports no EIP-8037 dimension at all. +#[test] +fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { + let plain = transact_plain(Callee::Returning); + assert!(plain.result.is_success(), "{:?}", plain.result); + assert!( + plain.refunded > 0, + "the callee's cleared slot must leave a refund for the lowering cell to take from", + ); + assert_eq!( + plain.gas_used, + plain.total_gas_spent - plain.refunded, + "the receipt's two gas numbers differ by exactly the refund", + ); + assert_eq!(plain.state_gas_spent, 0, "EIP-8037 is off on every MegaETH path"); + assert!(plain.ledger.is_zero(), "no inspector ran: {:?}", plain.ledger); + assert_identity("plain", &plain); +} + +// --- the refund lane +// ------------------------------------------------------------------------------ + +/// A refund written into a running interpreter's counter is booked, and moves what the sender pays +/// without moving the envelope. +#[test] +fn test_a_refund_written_into_a_live_interpreter_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); + + assert_eq!( + edited.ledger, + InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + "the shim must book the refund and nothing else", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "a refund does not move the envelope, which is why the law cannot see it", + ); + assert_eq!( + edited.refunded, + plain.refunded + u64::try_from(REFUND).unwrap(), + "but it does move the receipt's refund", + ); + assert_eq!( + edited.gas_used, + plain.gas_used - u64::try_from(REFUND).unwrap(), + "and through it what the sender pays", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, 0, + "the refund lane is deliberately not a term of the law", + ); + assert!(!edited.ledger.is_zero(), "and the block guard has to see it"); + assert_identity("refund at step", &edited); +} + +/// The same edit made at the last callback that holds the finished frame's result. +#[test] +fn test_a_refund_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.ledger, + InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded, plain.refunded + u64::try_from(REFUND).unwrap()); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); + assert_identity("refund at call_end", &edited); +} + +/// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction +/// would report an inspector that raised the sender's bill as having done nothing. +#[test] +fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { + let plain = transact_plain(Callee::Returning); + assert!( + plain.refunded >= u64::try_from(REFUND).unwrap(), + "fixture check: there must be a refund to take from, got {}", + plain.refunded, + ); + + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); + assert_eq!( + edited.ledger, + InspectorLedger { refund: -i128::from(REFUND), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded, plain.refunded - u64::try_from(REFUND).unwrap()); + assert_eq!( + edited.gas_used, + plain.gas_used + u64::try_from(REFUND).unwrap(), + "the sender pays more, by exactly what was taken", + ); + assert_identity("refund lowered", &edited); +} + +/// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. +/// +/// The cap applies to the transaction's whole refund at once, over a sum in which the EVM's own +/// refunds and an inspector's are indistinguishable, at a point past every callback. Splitting it +/// between them needs a priority rule the protocol does not have, so the lane states the edit and +/// the receipt states the effect — and the two are allowed to differ. +#[test] +fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); + + assert_eq!( + edited.ledger, + InspectorLedger { refund: i128::from(OVERSIZED_REFUND), ..InspectorLedger::default() }, + "the lane carries the nominal edit", + ); + assert_eq!( + edited.refunded, + edited.total_gas_spent / 5, + "while the receipt carries the EIP-3529 cap", + ); + assert!( + edited.refunded < plain.refunded + u64::try_from(OVERSIZED_REFUND).unwrap(), + "fixture check: the cap must actually bind, or this cell asserts nothing", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); + assert_identity("oversized refund", &edited); +} + +/// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. +/// +/// revm hands a frame's refund to its caller only on success, so this edit dies with the frame. +/// The lane books it anyway, because the alternative is a rule that has to track every frame +/// between the edit and the top — and because a lane that under-reports lets exactly the shape +/// this module exists to catch into a block, while over-reporting costs nothing: the law has no +/// term for it. +#[test] +fn test_a_refund_the_frame_chain_discards_is_still_booked() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.ledger, + InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + "the lane books the edit", + ); + assert_eq!( + edited.refunded, plain.refunded, + "the receipt is unmoved: a reverting frame hands its caller no refund", + ); + assert_eq!(edited.gas_used, plain.gas_used); + assert_identity("refund on a reverting frame", &edited); +} + +// --- the EIP-8037 state-gas dimension ------------------------------------------------------------ + +/// A reservoir an inspector fills is gas the transaction never funded: the receipt reports that +/// much less spent, and the law needs it back. +#[test] +fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); + + assert_eq!( + edited.ledger, + InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - RESERVOIR, + "the receipt counts the pool as unspent, so the envelope shrinks by exactly it", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + i128::from(RESERVOIR), + "which is why this lane, unlike the refund one, is a term of the law", + ); + assert_identity("reservoir at step", &edited); +} + +/// The same, written into the pool a call's inputs seed the child frame with. +#[test] +fn test_a_reservoir_written_into_a_frame_input_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); + + assert_eq!( + edited.ledger, + InspectorLedger { + reservoir: i128::from(RESERVOIR), + // The inputs came back changed in a field the envelope lane does not cover, which the + // rewrite comparison books on its own. + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); + assert_identity("reservoir on inputs", &edited); +} + +/// And into the finished frame's own pool, which its caller takes whatever the classification. +#[test] +fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); + + assert_eq!( + edited.ledger, + InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); + assert_identity("reservoir at call_end", &edited); +} + +/// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the +/// run it produces is the run the EVM would have produced alone. +/// +/// This is the window that decides where the lane is measured. A difference taken across this +/// callback would say `RESERVOIR` was conjured; the transaction says otherwise, and the settlement +/// point is the only reading that agrees with it. +#[test] +fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); + + assert!( + edited.ledger.is_zero(), + "an edit the child frame's own pool replaces moved nothing: {:?}", + edited.ledger, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); + assert_eq!(edited.gas_used, plain.gas_used); + assert_eq!(edited.refunded, plain.refunded); + assert_identity("reservoir in the dead window", &edited); +} + +/// The spend counter's own effect on the receipt: a successful transaction reports it, whether or +/// not EIP-8037 is enabled. +#[test] +fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); + + assert_eq!(plain.state_gas_spent, 0, "fixture check"); + assert_eq!( + edited.state_gas_spent, + u64::try_from(STATE_GAS).unwrap(), + "the receipt reports what was written", + ); + assert_eq!( + edited.ledger, + InspectorLedger { state_gas: i128::from(STATE_GAS), ..InspectorLedger::default() }, + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope is untouched, so this lane is not a term of the law either", + ); + assert_eq!(edited.terms.inspector_conjured_gas, 0); + assert_identity("state gas at step", &edited); +} + +/// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend +/// counter back into its caller's pool, which turns a state-gas edit into an envelope-moving one. +/// +/// The lane that catches it is the reservoir's, not the state-gas one, because the fold has +/// already happened by the time either is read. That is the second reason the two are settled from +/// the transaction's final figures rather than differenced across a callback. +#[test] +fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); + + assert_eq!( + edited.ledger, + InspectorLedger { reservoir: i128::from(STATE_GAS), ..InspectorLedger::default() }, + "the spend counter of a reverting frame arrives in its caller as a pool", + ); + assert_eq!( + edited.state_gas_spent, 0, + "and not as a spend: a failing frame's counter is not accumulated", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), + "so the envelope moves, and the law's term has to move with it", + ); + assert_identity("state gas on a reverting frame", &edited); +} From 6b5e007ddb895c467d807b9ecc218b5f7a58f0be Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:11:31 +0800 Subject: [PATCH 137/208] feat(evm): measure the refund and the state gas an inspector writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lanes, measured at two different points, because the two dimensions differ in one way that decides where a sound reading can be taken. A refund is a quantity the EVM produces as well, so only a difference across a callback isolates the inspector's share. `held_refund` is `held`'s counterpart on that dimension, and the shim books the difference at every callback that holds a `Gas` — nominally, because neither the EIP-3529 cap nor the chain of successful frame returns an edit has to survive is a quantity a boundary can attribute. The lane is deliberately not a term of the conservation law: a refund moves `tx_gas_used`, not `total_gas_spent`. The EIP-8037 state-gas dimension is a quantity MegaETH never produces at all, and one revm propagates between frames by replacement rather than by accumulation — so a boundary difference would book edits the EVM goes on to erase, and would miss the fold of a failing frame's spend counter into its caller's pool. Both figures are read once instead, where the transaction's envelope is final, and the reservoir joins the law's `I` term because the receipt reports the pool as unspent. The settlement's envelope becomes the receipt's own arithmetic, `limit - remaining - reservoir`, which is a no-op wherever the reservoir is what it structurally is. --- crates/mega-evm/src/evm/execution.rs | 16 +++- crates/mega-evm/src/evm/inspector.rs | 111 ++++++++++++++++++++++++++- crates/mega-evm/src/limit/limit.rs | 35 +++++++++ 3 files changed, 158 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 98b08134..85e4c757 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1194,9 +1194,19 @@ where // // `total_gas_spent` rather than the deprecated `spent`: the two are the same // subtraction today, and EIP-8037's state-gas split — which is what deprecated the - // latter — is pinned off for every `MegaEVM` transaction, so the reservoir is - // structurally zero here. - additional_limit.settle_destroyed_compute_gas(gas.total_gas_spent()); + // latter — is pinned off for every `MegaEVM` transaction. + // + // The reservoir is therefore structurally zero and the subtraction below is a no-op on + // every path — but it is the receipt's own arithmetic (`limit - remaining - + // reservoir`), and stating it here is what makes the settlement's envelope the one the + // receipt reports rather than one that happens to coincide with it. An inspector is + // the one thing that can fill a reservoir, and the lane booked a line earlier is what + // the law adds back so the two sides still meet. + additional_limit + .record_inspector_state_gas_dimension(gas.reservoir(), gas.state_gas_spent()); + additional_limit.settle_destroyed_compute_gas( + gas.total_gas_spent().saturating_sub(gas.reservoir()), + ); } Ok(()) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 417b8a30..8a182e30 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -30,6 +30,14 @@ //! reclaim from. The counter and the action between them hold everything a frame has, and //! [`held`] is the identity the two lanes split. //! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. +//! - Refund edits go to [`book_refund`], on their own lane: a refund moves what the sender pays +//! without moving the envelope the conservation law is stated over, so it needs a lane of its own +//! and no term in the law. [`held_refund`] is the reading it is taken against. +//! - The EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and `state_gas_spent`, and a frame +//! input's `reservoir` — is *not* measured here. `MegaETH` runs with EIP-8037 off, so it produces +//! none of it and there is no difference to take; the transaction's own settlement point books +//! whatever of it survives. See +//! [`InspectorLedger::reservoir`](crate::InspectorLedger::reservoir). //! - A callback that answers a frame itself stages the envelope it was handed, through //! [`AdditionalLimit::stage_inspector_interception_envelope`], so that the frame init it //! short-circuited can settle the gas its synthetic outcome carries against what the transaction @@ -50,7 +58,7 @@ use revm::{ handler::FrameResult, interpreter::{ interpreter_types::LoopControl, CallInputs, CallOutcome, CreateInputs, CreateOutcome, - FrameInput, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, + FrameInput, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, Inspector, @@ -197,6 +205,68 @@ fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { } } +/// The refund a frame and its pending continuation hold, given the action it is carrying and its +/// own gas object. +/// +/// [`held`]'s counterpart on the refund dimension, and it has the same three cases for the same +/// reason — the object the EVM will read next is the one the frame is holding: +/// +/// ```text +/// held_refund(None, gas) = gas.refunded() +/// held_refund(NewFrame(_), gas) = gas.refunded() +/// held_refund(Return(r), gas) = r.gas.refunded() +/// ``` +/// +/// The middle case differs from [`held`]'s: a `NewFrame` action carries a child's *envelope* but +/// no refund of its own, and the suspending frame resumes on this very counter with the child's +/// refund added to it. So the counter is the live object in two of the three cases, and only a +/// terminating action displaces it. +/// +/// Read on both sides of a callback, the difference is the refund the inspector wrote — wherever +/// it wrote it, and whichever of the two objects the EVM goes on to read. +#[inline] +fn held_refund(action: Option<&InterpreterAction>, gas: &Gas) -> i64 { + match action { + None | Some(InterpreterAction::NewFrame(_)) => gas.refunded(), + Some(InterpreterAction::Return(result)) => result.gas.refunded(), + } +} + +/// Books what a callback did to a refund counter. +/// +/// Nominal: the figure booked is what the inspector wrote, not what survives the EIP-3529 cap or +/// the chain of frame returns between here and the receipt — see +/// [`InspectorLedger::refund`](crate::InspectorLedger::refund) for why neither of those is a +/// quantity a boundary can measure, and why over-stating is the safe direction for the one +/// consumer this lane has. +#[inline] +fn book_refund( + context: &MegaContext, + before: i64, + after: i64, +) { + if before != after { + context + .additional_limit + .borrow_mut() + .record_inspector_refund_adjustment(i128::from(after) - i128::from(before)); + } +} + +/// The refund a synthetic outcome carries, for a callback that answered a frame itself. +/// +/// There is no "before" to difference against: no frame is built, so the EVM produced no refund +/// here at all and the whole of what the outcome carries is the inspector's — the same argument +/// the interception's gas baseline rests on, with the baseline being zero rather than the +/// envelope because a frame that never ran has refunded nothing. +#[inline] +fn book_synthetic_refund( + context: &MegaContext, + refunded: i64, +) { + book_refund(context, 0, refunded); +} + /// What a live-interpreter callback did to the interpreter's pending action. #[derive(Clone, Copy, Debug)] struct ActionChange { @@ -471,10 +541,16 @@ where context: &mut MegaContext, ) { let action = interp.bytecode.action().clone(); + let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); self.inner.initialize_interp(interp, context); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); + book_refund( + context, + refund_before, + held_refund(interp.bytecode.action().as_ref(), &interp.gas), + ); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, @@ -485,10 +561,16 @@ where #[inline] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { let action = interp.bytecode.action().clone(); + let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); self.inner.step(interp, context); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); + book_refund( + context, + refund_before, + held_refund(interp.bytecode.action().as_ref(), &interp.gas), + ); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, @@ -505,10 +587,16 @@ where #[inline] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { let action = interp.bytecode.action().clone(); + let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); self.inner.step_end(interp, context); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); + book_refund( + context, + refund_before, + held_refund(interp.bytecode.action().as_ref(), &interp.gas), + ); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interp.gas, before, @@ -531,10 +619,16 @@ where log: Log, ) { let action = interpreter.bytecode.action().clone(); + let refund_before = held_refund(action.as_ref(), &interpreter.gas); let before = interpreter.gas.remaining(); self.inner.log_full(interpreter, context, log); let change = measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); book_pending_action(context, change); + book_refund( + context, + refund_before, + held_refund(interpreter.bytecode.action().as_ref(), &interpreter.gas), + ); context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( &mut interpreter.gas, before, @@ -556,6 +650,9 @@ where frame_input_gas_limit(frame_input), outcome.is_some(), ); + if let Some(outcome) = &outcome { + book_synthetic_refund(context, outcome.gas().refunded()); + } book_intervention(context, outcome.is_some() || frame_input_rewritten(before, frame_input)); outcome } @@ -569,7 +666,9 @@ where ) { let before = frame_result.instruction_result(); let output = frame_result.interpreter_result().output.clone(); + let refund_before = frame_result.gas().refunded(); self.inner.frame_end(context, frame_input, frame_result); + book_refund(context, refund_before, frame_result.gas().refunded()); book_intervention( context, result_rewritten((before, &output), frame_result.interpreter_result()), @@ -595,6 +694,9 @@ where Some(inputs.gas_limit), outcome.is_some(), ); + if let Some(outcome) = &outcome { + book_synthetic_refund(context, outcome.result.gas.refunded()); + } book_intervention(context, outcome.is_some() || call_inputs_rewritten(before, inputs)); outcome } @@ -610,7 +712,9 @@ where outcome: &mut CallOutcome, ) { let before = (outcome.result.result, outcome.result.output.clone()); + let refund_before = outcome.result.gas.refunded(); self.inner.call_end(context, inputs, outcome); + book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); } @@ -628,6 +732,9 @@ where Some(inputs.gas_limit()), outcome.is_some(), ); + if let Some(outcome) = &outcome { + book_synthetic_refund(context, outcome.result.gas.refunded()); + } book_intervention(context, outcome.is_some() || create_inputs_rewritten(before, inputs)); outcome } @@ -644,7 +751,9 @@ where outcome: &mut CreateOutcome, ) { let before = (outcome.result.result, outcome.result.output.clone()); + let refund_before = outcome.result.gas.refunded(); self.inner.create_end(context, inputs, outcome); + book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 517423fb..5b2b99fa 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -671,6 +671,41 @@ impl AdditionalLimit { self.inspector.gas += delta; } + /// Books an adjustment an inspector made to a refund counter — see + /// [`InspectorLedger::refund`](inspector_ledger::InspectorLedger::refund). + /// + /// Booked and nothing else: no limit reads it, the conservation law has no term for it, and + /// the transaction's gas accounting is unmoved by it. Its one consumer is + /// [`InspectorLedger::is_zero`](inspector_ledger::InspectorLedger::is_zero), which is what the + /// canonical block path asks before admitting a transaction — and a refund is what the sender + /// pays, so a receipt an inspector moved this way has to be refused like any other. + #[inline] + pub(crate) fn record_inspector_refund_adjustment(&mut self, delta: i128) { + self.inspector.refund += delta; + } + + /// Books the EIP-8037 state-gas dimension a transaction ends holding, at the one point it is + /// final — see [`InspectorLedger::reservoir`](inspector_ledger::InspectorLedger::reservoir). + /// + /// Both numbers are structurally zero on every `MegaETH` path and every spec: EIP-8037 is off, + /// so no instruction charges state gas, no site fills a reservoir, and nothing here fires for + /// a transaction that ran without a rewriting inspector. What is non-zero is therefore the + /// inspector's in whole, which is why this reads the final figures rather than differencing + /// two readings the way every other lane does. + /// + /// Call this after op-revm has normalised the top-level gas object and before the destroyed + /// remainder is settled: the reservoir is what the settlement's envelope has to be reduced by, + /// and the conservation law reads the lane back out of that envelope. + #[inline] + pub(crate) fn record_inspector_state_gas_dimension( + &mut self, + reservoir: u64, + state_gas_spent: i64, + ) { + self.inspector.reservoir += i128::from(reservoir); + self.inspector.state_gas += i128::from(state_gas_spent); + } + /// Counts one rewrite the shim refused because its shape is forbidden — see /// [`InspectorLedger::rejected_rewrites`](inspector_ledger::InspectorLedger::rejected_rewrites). #[inline] From 6b32771688b5b7f656958e90323fe49887e0dd2e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:17:21 +0800 Subject: [PATCH 138/208] test(rex7): put the receipt's other two numbers in the cheat matrix Four columns: a refund raised and lowered, a state-gas pool filled, and a state-gas spend counter written. The matrix grows from 12 x 18 to 12 x 22, with 84 cells covered and 180 excused. Every frame in the fixture now sets and clears a slot, so each ends holding a refund the EVM produced for the lowering column to take from, and the caller logs a second time so the `log_full` row has a callback that runs after its frame has one. The EIP-8037 columns are single-direction on purpose: MegaETH runs with the EIP off, so both figures reach every callback at zero and there is nothing to lower. --- .../tests/rex7/inspector_cheat_matrix.rs | 271 +++++++++++++++++- 1 file changed, 262 insertions(+), 9 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index e58fd668..37e259f0 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -73,6 +73,12 @@ const ACTION: u64 = 1_500; /// Gas an interception cheat's synthetic outcome hands back over, or under, the envelope it was /// given. const INTERCEPTION: u64 = 4_000; +/// Refund a refund cheat adds to, or removes from, a `Gas`'s refund counter. +const REFUND: i64 = 2_000; +/// The EIP-8037 pool a reservoir cheat fills. +const RESERVOIR: u64 = 3_500; +/// The EIP-8037 spend counter a state-gas cheat writes. +const STATE_GAS: i64 = 1_200; /// Slot the top frame writes, last of all, so a cheat that fails the top frame is visible. const TOP_SLOT: u64 = 0x10; @@ -80,6 +86,9 @@ const TOP_SLOT: u64 = 0x10; const CALLEE_SLOT: u64 = 0x20; /// Slot the fixture's constructor writes. const INIT_SLOT: u64 = 0x30; +/// Slot every frame sets and then clears, so each ends holding a refund the EVM itself produced — +/// which is what the refund-lowering column needs to take from. +const CLEARED_SLOT: u64 = 0x40; /// Value every fixture write stores, so a stack cheat that bumps it is visible as `2`. const STORED: u64 = 1; @@ -135,6 +144,10 @@ impl At { /// and in which direction it moves it. Two shapes that move the same argument in opposite /// directions are separate columns because the ledger's sign convention is exactly that /// distinction, and a lane that books one direction and drops the other is a real failure mode. +/// +/// The EIP-8037 dimensions are the one place that pairing does not apply: `MegaETH` runs with the +/// EIP off, so a `Gas` reaches every callback with both of its state-gas figures at zero and there +/// is nothing to lower. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum Shape { /// Write gas into a live interpreter's counter. @@ -170,6 +183,15 @@ enum Shape { RaiseActionEnvelope, /// Lower it. LowerActionEnvelope, + /// Add to a `Gas`'s refund counter — what the sender is billed, which the envelope does not + /// reach. + RaiseRefund, + /// Take from one. + LowerRefund, + /// Fill a `Gas`'s or a frame input's EIP-8037 state-gas pool. + WriteReservoir, + /// Write a `Gas`'s EIP-8037 spend counter. + WriteStateGas, /// Edit the interpreter's stack or memory — the frame's working state, which the EVM reads /// back as operands and as data. EditStackOrMemory, @@ -178,7 +200,7 @@ enum Shape { } impl Shape { - const ALL: [Self; 18] = [ + const ALL: [Self; 22] = [ Self::InjectGas, Self::DrainGas, Self::RaiseEnvelope, @@ -195,6 +217,10 @@ impl Shape { Self::LowerActionResultGas, Self::RaiseActionEnvelope, Self::LowerActionEnvelope, + Self::RaiseRefund, + Self::LowerRefund, + Self::WriteReservoir, + Self::WriteStateGas, Self::EditStackOrMemory, Self::JournalWrite, ]; @@ -205,6 +231,16 @@ impl Shape { matches!(self, Self::Intercept | Self::RaiseInterceptionGas | Self::LowerInterceptionGas) } + /// Whether this shape reaches through a `Gas`'s refund counter. + const fn is_refund(self) -> bool { + matches!(self, Self::RaiseRefund | Self::LowerRefund) + } + + /// Whether this shape reaches through the EIP-8037 state-gas dimension. + const fn is_state_gas(self) -> bool { + matches!(self, Self::WriteReservoir | Self::WriteStateGas) + } + /// Whether this shape reaches through the interpreter's *pending action* rather than through /// the interpreter itself. const fn is_pending_action(self) -> bool { @@ -268,6 +304,30 @@ fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { ); } + if shape.is_refund() || shape.is_state_gas() { + if !interpreter_facing && !input_facing && !result_facing { + return Some("no `Gas` and no frame input is reachable from this callback"); + } + if input_facing && shape != WriteReservoir { + return Some( + "a frame's inputs carry no refund counter and no state-gas spend counter; the \ + pool is the one figure of either dimension they do carry", + ); + } + if shape == WriteReservoir && at == Create { + return Some( + "`CreateInputs` keeps its pool private and offers no setter, so the only rewrite \ + that reaches it replaces the whole struct — which is the `EditInput` column", + ); + } + if shape == LowerRefund && at == InitializeInterp { + return Some( + "a frame is handed a fresh `Gas` whose refund counter is zero, so there is \ + nothing to lower before its first instruction runs", + ); + } + } + match shape { InjectGas | DrainGas | EditStackOrMemory if !interpreter_facing => { Some("no live interpreter is reachable from this callback") @@ -334,6 +394,22 @@ impl Cheat { self.moved_gas -= i128::from(DRAIN); self.fired += 1; } + Shape::RaiseRefund => { + interp.gas.record_refund(REFUND); + self.fired += 1; + } + Shape::LowerRefund => { + interp.gas.record_refund(-REFUND); + self.fired += 1; + } + Shape::WriteReservoir => { + interp.gas.set_reservoir(RESERVOIR); + self.fired += 1; + } + Shape::WriteStateGas => { + interp.gas.set_state_gas_spent(STATE_GAS); + self.fired += 1; + } Shape::EditStackOrMemory => { // Bump the value an `SSTORE` is about to write, so the edit is visible in the // produced state rather than only in the absence of an accounting change. @@ -347,6 +423,26 @@ impl Cheat { } } + /// Whether a live-interpreter callback is one this cheat's shape can land at. + /// + /// Two shapes are choosy about the moment rather than about the callback. A refund the + /// interpreter is to *lose* needs one it already holds, which only exists once a frame has + /// cleared a storage slot; and any refund edit made while a terminating action is pending is + /// written into a counter the action has already copied, so it would land on the action's + /// number rather than on this column's mechanism. + fn interpreter_moment_is_right( + &self, + interp: &mut Interpreter, + ) -> bool { + if !self.shape.is_refund() { + return true; + } + if matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) { + return false; + } + self.shape != Shape::LowerRefund || interp.gas.refunded() >= REFUND + } + /// Overwrites the frame's first memory word — the edit the three interpreter-facing rows /// that cannot safely touch the stack use instead. /// @@ -420,6 +516,11 @@ impl Cheat { self.fired += 1; None } + Shape::WriteReservoir => { + inputs.reservoir += RESERVOIR; + self.fired += 1; + None + } Shape::Intercept | Shape::RaiseInterceptionGas | Shape::LowerInterceptionGas => { self.fired += 1; Some(CallOutcome::new( @@ -514,6 +615,27 @@ impl Cheat { result.result = InstructionResult::Stop; self.fired += 1; } + Shape::RaiseRefund => { + result.gas.record_refund(REFUND); + self.fired += 1; + } + Shape::LowerRefund => { + assert!( + result.gas.refunded() >= REFUND, + "the fixture must hand this cell a frame that refunded something, got {}", + result.gas.refunded(), + ); + result.gas.record_refund(-REFUND); + self.fired += 1; + } + Shape::WriteReservoir => { + result.gas.set_reservoir(RESERVOIR); + self.fired += 1; + } + Shape::WriteStateGas => { + result.gas.set_state_gas_spent(STATE_GAS); + self.fired += 1; + } _ => unreachable!("{:?} is not a result-facing shape", self.shape), } } @@ -533,6 +655,7 @@ impl Inspector for Cheat { match self.shape { Shape::JournalWrite => self.hit_journal(context), Shape::EditStackOrMemory => self.hit_frame_state(interp), + _ if !self.interpreter_moment_is_right(interp) => {} _ => self.hit_interpreter(interp), } } @@ -550,6 +673,13 @@ impl Inspector for Cheat { self.hit_interpreter(interp) } Shape::EditStackOrMemory | Shape::JournalWrite => {} + // The refund columns fire on the first callback that offers the moment they need, + // rather than on a fixed ordinal. + _ if self.shape.is_refund() => { + if self.interpreter_moment_is_right(interp) { + self.hit_interpreter(interp); + } + } _ if self.steps == self.step_at => self.hit_interpreter(interp), _ => {} } @@ -565,6 +695,12 @@ impl Inspector for Cheat { self.hit_pending_action(interp); return; } + if self.shape.is_refund() { + if self.interpreter_moment_is_right(interp) { + self.hit_interpreter(interp); + } + return; + } if self.steps != self.step_at { return; } @@ -582,6 +718,7 @@ impl Inspector for Cheat { match self.shape { Shape::JournalWrite => self.hit_journal(context), Shape::EditStackOrMemory => self.hit_frame_state(interp), + _ if !self.interpreter_moment_is_right(interp) => {} _ => self.hit_interpreter(interp), } } @@ -677,10 +814,10 @@ enum Fixture { RevertingCallee, } -/// Init code that writes [`INIT_SLOT`] and returns two bytes of runtime code. +/// Init code that writes [`INIT_SLOT`], leaves a refund behind, and returns two bytes of runtime +/// code. fn init_code() -> Vec { - BytecodeBuilder::default() - .sstore(U256::from(INIT_SLOT), U256::from(STORED)) + clear_a_slot(BytecodeBuilder::default().sstore(U256::from(INIT_SLOT), U256::from(STORED))) .push_number(0x6000u64) .push_number(0u64) .append(MSTORE) @@ -691,10 +828,24 @@ fn init_code() -> Vec { .to_vec() } -/// The transaction's entry contract: one `LOG1`, one inner `CALL`, one `CREATE`, one `SSTORE`. +/// Sets a slot and clears it again, which leaves the frame holding a refund the EVM produced. +/// +/// Every frame in the fixture does this, because the refund-lowering column needs a refund to take +/// from wherever it lands — an interpreter's counter, a finished call's result, or a finished +/// creation's. +fn clear_a_slot(builder: BytecodeBuilder) -> BytecodeBuilder { + builder + .sstore(U256::from(CLEARED_SLOT), U256::from(STORED)) + .sstore(U256::from(CLEARED_SLOT), U256::ZERO) +} + +/// The transaction's entry contract: one `LOG1`, one inner `CALL`, one `CREATE`, a slot set and +/// cleared, a second `LOG1`, and one `SSTORE`. /// /// One fixture rather than one per row, so that every callback fires in the same transaction and -/// a cell's assertions are about the cheat rather than about which fixture it got. +/// a cell's assertions are about the cheat rather than about which fixture it got. The second +/// `LOG1` is there so the `log_full` row has a callback that runs *after* the frame has a refund; +/// the first one runs before anything has cleared a slot. fn caller_code() -> Bytes { let init = init_code(); let mut builder = BytecodeBuilder::default() @@ -721,19 +872,27 @@ fn caller_code() -> Bytes { for (offset, byte) in init.iter().enumerate() { builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); } - builder + let builder = builder .push_number(init.len() as u64) // size .push_number(0u64) // offset .push_number(0u64) // value .append(CREATE) - .append(POP) + .append(POP); + clear_a_slot(builder) + // LOG1(offset=0, size=32, topic=2), now that the frame carries a refund. + .push_number(2u64) + .push_number(32u64) + .push_number(0u64) + .append(LOG1) .sstore(U256::from(TOP_SLOT), U256::from(STORED)) .append(STOP) .build() } fn callee_code(fixture: Fixture) -> Bytes { - let builder = BytecodeBuilder::default().sstore(U256::from(CALLEE_SLOT), U256::from(STORED)); + let builder = clear_a_slot( + BytecodeBuilder::default().sstore(U256::from(CALLEE_SLOT), U256::from(STORED)), + ); match fixture { Fixture::ReturningCallee => builder.append(STOP).build(), Fixture::RevertingCallee => builder.revert().build(), @@ -924,6 +1083,22 @@ fn ledger_result(result: i128) -> InspectorLedger { InspectorLedger { result, ..InspectorLedger::default() } } +/// The ledger a rewrite of one of the receipt's other two numbers books. +/// +/// Separate helpers rather than one, because which of the three figures a shape moves is exactly +/// what decides whether the conservation law can see it: only the pool is a term of it. +fn ledger_refund(refund: i128) -> InspectorLedger { + InspectorLedger { refund, ..InspectorLedger::default() } +} + +fn ledger_reservoir(reservoir: i128) -> InspectorLedger { + InspectorLedger { reservoir, ..InspectorLedger::default() } +} + +fn ledger_state_gas(state_gas: i128) -> InspectorLedger { + InspectorLedger { state_gas, ..InspectorLedger::default() } +} + /// The ledger of a rewrite that moves no gas: the shim saw the argument it was handed come back /// changed, and that is the whole of what it books. /// @@ -1029,6 +1204,37 @@ fn matrix() -> Vec { InspectorLedger::default(), state_all_committed, ); + // The receipt's other two numbers, reached through the same `Gas` as the counter above. + push( + at, + RaiseRefund, + Fixture::ReturningCallee, + ledger_refund(i128::from(REFUND)), + state_all_committed, + ); + if at != InitializeInterp { + push( + at, + LowerRefund, + Fixture::ReturningCallee, + ledger_refund(-i128::from(REFUND)), + state_all_committed, + ); + } + push( + at, + WriteReservoir, + Fixture::ReturningCallee, + ledger_reservoir(i128::from(RESERVOIR)), + state_all_committed, + ); + push( + at, + WriteStateGas, + Fixture::ReturningCallee, + ledger_state_gas(i128::from(STATE_GAS)), + state_all_committed, + ); } // The pending action, which only `step_end` ever sees: revm's inspected loop breaks out the @@ -1128,6 +1334,22 @@ fn matrix() -> Vec { InspectorLedger::default(), state_all_committed, ); + if !deployment_side { + // The pool a call's inputs seed the child with. It travels to the child and back, so + // it is booked as gas — and the inputs came back changed in a field the envelope lane + // does not cover, which the rewrite comparison books separately. + push( + at, + WriteReservoir, + Fixture::ReturningCallee, + InspectorLedger { + reservoir: i128::from(RESERVOIR), + interventions: 1, + ..InspectorLedger::default() + }, + state_all_committed, + ); + } } // The three callbacks that are handed a finished frame's result. @@ -1172,6 +1394,34 @@ fn matrix() -> Vec { InspectorLedger::default(), state_all_committed, ); + push( + at, + RaiseRefund, + Fixture::ReturningCallee, + ledger_refund(i128::from(REFUND)), + state_all_committed, + ); + push( + at, + LowerRefund, + Fixture::ReturningCallee, + ledger_refund(-i128::from(REFUND)), + state_all_committed, + ); + push( + at, + WriteReservoir, + Fixture::ReturningCallee, + ledger_reservoir(i128::from(RESERVOIR)), + state_all_committed, + ); + push( + at, + WriteStateGas, + Fixture::ReturningCallee, + ledger_state_gas(i128::from(STATE_GAS)), + state_all_committed, + ); } cells @@ -1267,6 +1517,9 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { (At::FrameEnd, Shape::ReviveResult, Fixture::RevertingCallee), (At::Step, Shape::JournalWrite, Fixture::ReturningCallee), (At::StepEnd, Shape::RaiseActionResultGas, Fixture::ReturningCallee), + (At::Step, Shape::LowerRefund, Fixture::ReturningCallee), + (At::CallEnd, Shape::WriteReservoir, Fixture::ReturningCallee), + (At::FrameEnd, Shape::WriteStateGas, Fixture::ReturningCallee), ]; let mut plain: BTreeMap = BTreeMap::new(); From 114ecce16ccd92f283ff331cd1d8fd2dd01f7bd2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:18:59 +0800 Subject: [PATCH 139/208] test(rex7): lock the gas-surface table against open gaps The three verdicts P7 left open are lanes now, so the pin that named them becomes structural: any row saying "this reaches what MegaETH reports and no lane books it" fails the test. Writing a gap down is how it gets closed; leaving it written down is how a table stops being a statement about the code. A companion test builds a probe table with a gap in it, so the empty expected value is not passing against a predicate that matches nothing. And the per-struct renderings are now looked up in the same table list the lock walks, so the two cannot drift apart. `state_gas_spent` moves from Inert to a lane. It sat under "EIP-8037 is off, so nothing reads it", which is true of every instruction and not of the receipt: revm reports the final figure regardless, and a failing frame folds it into its caller's pool. --- crates/mega-evm/tests/rex7/gas_surface.rs | 157 ++++++++++++++-------- 1 file changed, 104 insertions(+), 53 deletions(-) diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index 8b2bc57d..68f327bf 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -16,6 +16,13 @@ //! that name set against the classification table is a pin with the same reach: a field added //! upstream appears in the rendering, fails to match a table row, and the test names it. //! +//! # The lock +//! +//! A verdict of "this reaches the receipt and nothing books it" is nameable — `Coverage` has an arm +//! for it — but not keepable: [`test_the_table_carries_no_open_gap`] fails on any row that carries +//! one. Writing a gap down is how it gets closed; leaving it written down is how a table stops +//! being a statement about the code and becomes a list of things somebody meant to do. +//! //! # What the pins cannot reach, and what covers it instead //! //! A callback *added* to the `Inspector` trait is not a compile error anywhere — the trait gives @@ -44,6 +51,11 @@ use std::{collections::BTreeSet, string::String, vec::Vec}; /// The whole point of the enum is that there is no fifth arm and no catch-all: a field is /// measured, or it carries no gas, or it carries gas that reaches nothing `MegaETH` reports, or it /// is a hole with a name. "Nobody looked at it" is not one of the options. +/// +/// The fourth arm exists and is unused, which is the state +/// [`test_the_table_carries_no_open_gap`] holds the table in. A hole is nameable, so that +/// discovering one is a change to this file rather than a silence — and it is not *keepable*, so +/// that adding one means closing it in the same change or taking the decision to an owner. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Coverage { /// Measured, and booked on the named `InspectorLedger` lane. @@ -81,25 +93,26 @@ const GAS_TRACKER_FIELDS: [(&str, Coverage); 5] = [ ), ( "refunded", - Coverage::NotClosed( - "a refund written at any callback that holds a `Gas` travels to the caller on frame \ - return and reaches the receipt's gas used, while the conservation law — stated over \ - `limit - remaining` — stays closed and every ledger lane stays zero", + Coverage::Lane( + "refund, at the callback boundary — nominal, because neither the EIP-3529 cap nor the \ + chain of successful frame returns an edit must survive is attributable to one \ + callback", ), ), ( "reservoir", - Coverage::NotClosed( - "EIP-8037 state gas is structurally zero on every MegaETH path, so a reservoir an \ - inspector writes is gas the transaction never funded; it moves the envelope and the \ - terminal cross-check trips on it, with every ledger lane zero", + Coverage::Lane( + "reservoir, settled once from the figure the transaction ends with: `MegaETH` runs \ + with EIP-8037 off and produces none of it, and revm propagates it between frames by \ + replacement, so there is no difference across a callback to take", ), ), ( "state_gas_spent", - Coverage::Inert( - "the counterpart of `reservoir` on the spending side, and dead for the same reason: \ - with EIP-8037 off nothing reads it", + Coverage::Lane( + "state_gas, settled at the same point — the receipt reports the final figure whether \ + or not EIP-8037 is on, and a failing frame folds it into its caller's reservoir, \ + where the lane above picks it up", ), ), ]; @@ -134,9 +147,11 @@ const CALL_INPUTS_FIELDS: [(&str, Coverage); 12] = [ ), ( "reservoir", - Coverage::NotClosed( - "the child frame's EIP-8037 state-gas pool, and gas the caller was never debited for \ - in exactly the way a raised `gas_limit` is; no lane books it", + Coverage::Lane( + "reservoir, at the transaction's settlement point — the child frame is seeded from \ + this pool and hands it back, so an edit here reaches the receipt; the rewrite \ + comparison books it as an intervention as well, which is not a second reading of the \ + same edit because no lane books anything at this boundary", ), ), ("input", Coverage::NotGas("what the frame does")), @@ -160,7 +175,7 @@ const CALL_INPUTS_FIELDS: [(&str, Coverage); 12] = [ /// Everything a creation frame is built from. const CREATE_INPUTS_FIELDS: [(&str, Coverage); 8] = [ ("gas_limit", Coverage::Lane("env, exactly as a call's")), - ("reservoir", Coverage::NotClosed("a creation's copy of the call form above")), + ("reservoir", Coverage::Lane("reservoir, exactly as a call's")), ("caller", Coverage::NotGas("what the frame does")), ("scheme", Coverage::NotGas("what the frame does")), ("value", Coverage::NotGas("what the frame does")), @@ -280,28 +295,33 @@ fn sample_result() -> InterpreterResult { /// Every field of every gas-carrying object an inspector is handed has a verdict. /// /// This is the closure the completeness table rests on. It is not a claim that the verdicts are -/// right — the tests in `measured_inspector.rs`, `inspector_cheat_matrix.rs` and -/// `interception_gas.rs` are — it is the claim that there is no field without one. +/// right — the tests in `measured_inspector.rs`, `inspector_cheat_matrix.rs`, +/// `interception_gas.rs` and `refund_and_state_gas.rs` are — it is the claim that there is no +/// field without one. #[test] fn test_every_field_of_every_gas_carrier_has_a_verdict() { let gas = sample_gas(); - assert_classified("Gas", &std::format!("{gas:?}"), &GAS_FIELDS); - assert_classified("GasTracker", &std::format!("{:?}", gas.tracker()), &GAS_TRACKER_FIELDS); - assert_classified("MemoryGas", &std::format!("{:?}", gas.memory()), &MEMORY_GAS_FIELDS); - assert_classified( - "CallInputs", - &std::format!("{:?}", sample_call_inputs()), - &CALL_INPUTS_FIELDS, - ); - assert_classified( - "CreateInputs", - &std::format!("{:?}", sample_create_inputs()), - &CREATE_INPUTS_FIELDS, - ); - assert_classified( - "InterpreterResult", - &std::format!("{:?}", sample_result()), - &INTERPRETER_RESULT_FIELDS, + let renderings = [ + ("Gas", std::format!("{gas:?}")), + ("GasTracker", std::format!("{:?}", gas.tracker())), + ("MemoryGas", std::format!("{:?}", gas.memory())), + ("CallInputs", std::format!("{:?}", sample_call_inputs())), + ("CreateInputs", std::format!("{:?}", sample_create_inputs())), + ("InterpreterResult", std::format!("{:?}", sample_result())), + ]; + // Looked up rather than listed, so the set of tables the lock walks and the set this test + // checks the renderings against cannot drift apart. + for (what, rendered) in &renderings { + let (_, table) = tables() + .into_iter() + .find(|(name, _)| name == what) + .unwrap_or_else(|| panic!("{what} has a rendering but no table in `tables()`")); + assert_classified(what, rendered, table); + } + assert_eq!( + renderings.len(), + tables().len(), + "every table must have a rendering checked against it", ); } @@ -323,41 +343,72 @@ fn test_the_field_reader_reads_the_top_level_and_stops_there() { assert!(field_names("NoFields").is_empty(), "a unit struct has no fields to read"); } -/// The gaps the table carries are the gaps it says it carries. -/// -/// A verdict is a claim someone has to be able to act on, so the set of open ones is pinned by -/// name rather than by count: closing one, or discovering another, has to touch this list and the -/// table in `src/evm/AGENTS.md` together. -#[test] -fn test_the_open_gaps_are_the_ones_the_table_names() { - let mut open = Vec::new(); - for (what, table) in [ +/// Every table this module classifies, by the name its rendering is checked under. +fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 6] { + [ ("Gas", GAS_FIELDS.as_slice()), ("GasTracker", GAS_TRACKER_FIELDS.as_slice()), ("MemoryGas", MEMORY_GAS_FIELDS.as_slice()), ("CallInputs", CALL_INPUTS_FIELDS.as_slice()), ("CreateInputs", CREATE_INPUTS_FIELDS.as_slice()), ("InterpreterResult", INTERPRETER_RESULT_FIELDS.as_slice()), - ] { - for (field, coverage) in table { + ] +} + +/// The `Owner::Field` names a set of tables leaves with no lane, in sorted order. +fn open_gaps(tables: &[(&'static str, &'static [(&'static str, Coverage)])]) -> Vec { + let mut open = Vec::new(); + for (what, table) in tables { + for (field, coverage) in *table { if matches!(coverage, Coverage::NotClosed(_)) { open.push(std::format!("{what}::{field}")); } } } open.sort(); + open +} + +/// ★ The table carries no open gap, and cannot be left carrying one. +/// +/// Every earlier version of this test named the gaps that were open, which made a hole something a +/// change could add as long as it also added a line here. There are none left, so the pin becomes +/// structural: a field that reaches what `MegaETH` reports and that no lane books fails this test +/// the moment it is written down. +/// +/// That is deliberately awkward. Closing a surface is work, and a test that merely *records* an +/// open one lets the work be deferred indefinitely while the table still reads as complete. With +/// this pin the two options are to close the gap in the same change or to take the decision +/// somewhere a person owns it — and either way somebody has looked. +/// +/// It does not, and cannot, stop a hole from being *mis*classified as `Inert` or `NotGas`. Nothing +/// mechanical can: those verdicts are claims about what the EVM does with a number, and what backs +/// them is the measurement each one was written from. `state_gas_spent` is the cautionary case — +/// it sat under `Inert` on the strength of "EIP-8037 is off", which is true and which the receipt +/// does not care about. +#[test] +fn test_the_table_carries_no_open_gap() { assert_eq!( - open, - [ - "CallInputs::reservoir", - "CreateInputs::reservoir", - "GasTracker::refunded", - "GasTracker::reservoir", - ], - "the open gaps moved; `src/evm/AGENTS.md`'s table has to move with them", + open_gaps(&tables()), + Vec::::new(), + "a gas surface with no lane cannot be left in the table; close it, or take the decision \ + to an owner and record it there", ); } +/// The lock detects what it claims to detect. +/// +/// Without this, the assertion above would pass just as happily against a predicate that never +/// matched anything — which is the failure mode of every test whose expected value is empty. +#[test] +fn test_the_lock_names_an_open_gap_when_there_is_one() { + const PROBE: [(&str, Coverage); 2] = [ + ("measured", Coverage::Lane("somewhere")), + ("unmeasured", Coverage::NotClosed("moves the receipt, and no lane books it")), + ]; + assert_eq!(open_gaps(&[("Probe", PROBE.as_slice())]), ["Probe::unmeasured"]); +} + // --- the shape-level pin ------------------------------------------------------------------------- /// Which object a gas-carrying shape puts within reach. From e38076fa9750ab8f83153ac76208239b48a5ee69 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:22:42 +0800 Subject: [PATCH 140/208] style(rex7): lift the refund columns' moment guard out of the match Clippy's collapse suggestion would have changed what the arm does: a refund shape whose moment is wrong must not fall through to the fixed-ordinal arm and fire anyway. --- .../mega-evm/tests/rex7/inspector_cheat_matrix.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 37e259f0..29f46b43 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -665,6 +665,14 @@ impl Inspector for Cheat { if !self.arm(At::Step) { return; } + // The refund columns fire on the first callback that offers the moment they need, rather + // than on a fixed ordinal. + if self.shape.is_refund() { + if self.interpreter_moment_is_right(interp) { + self.hit_interpreter(interp); + } + return; + } match self.shape { Shape::JournalWrite if self.steps == self.step_at => self.hit_journal(context), // Fire on the first `SSTORE` the transaction reaches, whose operands are on the stack @@ -673,13 +681,6 @@ impl Inspector for Cheat { self.hit_interpreter(interp) } Shape::EditStackOrMemory | Shape::JournalWrite => {} - // The refund columns fire on the first callback that offers the moment they need, - // rather than on a fixed ordinal. - _ if self.shape.is_refund() => { - if self.interpreter_moment_is_right(interp) { - self.hit_interpreter(interp); - } - } _ if self.steps == self.step_at => self.hit_interpreter(interp), _ => {} } From 9a619dca544b50d286a155ee3927b775d90d9c30 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:22:42 +0800 Subject: [PATCH 141/208] feat(state-test): add the receipt's other two numbers to the chaos pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four shapes: a refund raised and lowered, an EIP-8037 pool written, and an EIP-8037 spend counter written. The interpreter pool grows to ten, the result pool to nine, and the input pool to nine — a call's inputs carry a pool, a creation's keeps it private. Lowering a refund a `Gas` does not hold is skipped rather than driving the counter negative, which is the same rule the gas-draining shape follows: a state the EVM cannot reach on its own is not the state under test. --- crates/mega-state-test/src/chaos.rs | 90 +++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 210adc6c..e52fadc3 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -187,11 +187,23 @@ pub enum ChaosShape { RaiseActionGas, /// Gas taken out of one. LowerActionGas, + /// A refund added to a `Gas`'s refund counter — what the sender is billed, which the envelope + /// the conservation law is stated over does not reach. + RaiseRefund, + /// A refund taken out of one. Skipped when the `Gas` has none, rather than driving the counter + /// negative — a state revm documents as invalid at the end of a transaction. + LowerRefund, + /// An EIP-8037 state-gas pool written into a `Gas` or a call's inputs. `MegaETH` runs with the + /// EIP off and fills no pool, so anything found in one is gas the transaction never funded. + WriteReservoir, + /// An EIP-8037 spend counter written into a `Gas`. Structurally zero for the same reason, and + /// reachable through two different receipt figures depending on how the frame ends. + WriteStateGas, } impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 17] = [ + pub const ALL: [Self; 21] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -209,6 +221,10 @@ impl ChaosShape { Self::ReviveCall, Self::RaiseActionGas, Self::LowerActionGas, + Self::RaiseRefund, + Self::LowerRefund, + Self::WriteReservoir, + Self::WriteStateGas, ]; /// The shape a label names. @@ -245,6 +261,10 @@ impl ChaosShape { Self::ReviveCall => "revive_call", Self::RaiseActionGas => "raise_action_gas", Self::LowerActionGas => "lower_action_gas", + Self::RaiseRefund => "raise_refund", + Self::LowerRefund => "lower_refund", + Self::WriteReservoir => "write_reservoir", + Self::WriteStateGas => "write_state_gas", } } } @@ -254,13 +274,17 @@ impl ChaosShape { /// The last two only land at the one callback that runs with an action already pending — /// `step_end`, which revm's inspected loop runs after the instruction that set it. A draw for them /// anywhere else leaves the interpreter alone and spends no budget. -const INTERPRETER_SHAPES: [ChaosShape; 6] = [ +const INTERPRETER_SHAPES: [ChaosShape; 10] = [ ChaosShape::InjectGas, ChaosShape::DrainGas, ChaosShape::EditFrameState, ChaosShape::JournalWrite, ChaosShape::RaiseActionGas, ChaosShape::LowerActionGas, + ChaosShape::RaiseRefund, + ChaosShape::LowerRefund, + ChaosShape::WriteReservoir, + ChaosShape::WriteStateGas, ]; /// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. @@ -269,7 +293,7 @@ const INTERPRETER_SHAPES: [ChaosShape; 6] = [ /// the envelope. That is the whole of what separates them, and it is the separation that matters: /// the echo is the shape every real tool uses, and it is also the one shape whose accounting /// closes without anything measuring the figure. -const INPUT_SHAPES: [ChaosShape; 8] = [ +const INPUT_SHAPES: [ChaosShape; 9] = [ ChaosShape::RaiseEnvelope, ChaosShape::LowerEnvelope, ChaosShape::MakeStatic, @@ -278,15 +302,20 @@ const INPUT_SHAPES: [ChaosShape; 8] = [ ChaosShape::InterceptUnderGas, ChaosShape::InterceptNoGas, ChaosShape::JournalWrite, + ChaosShape::WriteReservoir, ]; /// Shapes reachable from a callback that holds a finished frame's result. -const RESULT_SHAPES: [ChaosShape; 5] = [ +const RESULT_SHAPES: [ChaosShape; 9] = [ ChaosShape::RaiseResultGas, ChaosShape::LowerResultGas, ChaosShape::FailFrame, ChaosShape::ReviveCall, ChaosShape::JournalWrite, + ChaosShape::RaiseRefund, + ChaosShape::LowerRefund, + ChaosShape::WriteReservoir, + ChaosShape::WriteStateGas, ]; /// Which mutations a chaos run is allowed to make. @@ -534,6 +563,14 @@ impl ChaosInspector { return; } } + ChaosShape::RaiseRefund | + ChaosShape::LowerRefund | + ChaosShape::WriteReservoir | + ChaosShape::WriteStateGas => { + if !edit_receipt_figure(&mut interp.gas, shape, entropy) { + return; + } + } _ => return, } self.applied(shape); @@ -556,6 +593,9 @@ impl ChaosInspector { inputs.gas_limit = inputs.gas_limit.saturating_sub(Self::amount(entropy)); } ChaosShape::MakeStatic => inputs.is_static = true, + ChaosShape::WriteReservoir => { + inputs.reservoir = inputs.reservoir.saturating_add(Self::amount(entropy)); + } ChaosShape::Intercept | ChaosShape::InterceptOverGas | ChaosShape::InterceptUnderGas | @@ -606,8 +646,10 @@ impl ChaosInspector { )); } ChaosShape::JournalWrite => write_journal(context, entropy), - // `MakeStatic` has no counterpart here — a creation carries no static flag — and the - // rest are not input-facing at all. Both leave the inputs alone and spend no budget. + // `MakeStatic` has no counterpart here — a creation carries no static flag — and + // `WriteReservoir` has none either, because `CreateInputs` keeps its pool private and + // offers no setter. The rest are not input-facing at all. All of them leave the inputs + // alone and spend no budget. _ => return None, } self.applied(shape); @@ -649,12 +691,48 @@ impl ChaosInspector { } result.result = InstructionResult::Stop; } + ChaosShape::RaiseRefund | + ChaosShape::LowerRefund | + ChaosShape::WriteReservoir | + ChaosShape::WriteStateGas => { + if !edit_receipt_figure(&mut result.gas, shape, entropy) { + return; + } + } _ => return, } self.applied(shape); } } +/// Writes one of the receipt figures that is not the envelope, returning whether anything moved. +/// +/// The three are grouped because they are one surface — every `Gas` an inspector is handed carries +/// all of them — and separated from the gas lanes because the conservation law reaches only one of +/// the three. A refund reaches what the sender is billed; the EIP-8037 pool reaches the envelope +/// the receipt reports as spent; the EIP-8037 spend counter reaches the receipt's state-gas figure, +/// or its caller's pool when the frame fails. +/// +/// Lowering a refund the `Gas` does not have is skipped rather than driving the counter negative: +/// revm documents a negative refund at the end of a transaction as invalid, so producing one would +/// be testing a state the EVM cannot reach on its own. +fn edit_receipt_figure(gas: &mut Gas, shape: ChaosShape, entropy: u64) -> bool { + let amount = entropy % GAS_DELTA_MAX + 1; + match shape { + ChaosShape::RaiseRefund => gas.record_refund(amount as i64), + ChaosShape::LowerRefund => { + if gas.refunded() < amount as i64 { + return false; + } + gas.record_refund(-(amount as i64)); + } + ChaosShape::WriteReservoir => gas.set_reservoir(amount), + ChaosShape::WriteStateGas => gas.set_state_gas_spent(amount as i64), + _ => return false, + } + true +} + /// The gas a synthetic outcome hands back, given the envelope the callback was handed. /// /// The four interception shapes are exactly this function's four cases. `Intercept` echoes the From e2ccd66437c020c303bd817d5dcc6e243cb096ec Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:25:40 +0800 Subject: [PATCH 142/208] feat(evm): check the receipt's other two numbers against the lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The terminal check reached the envelope and nothing else, so it passed unchanged on a transaction whose sender was billed a different amount. The used figure is now stated against the accounted envelope rather than the reported one, so the same lanes have to account for both numbers the receipt carries. That is a consistency pin and not a measurement of what an inspector did to the refund — the EIP-3529 cap applies to a sum in which the EVM's refunds and an inspector's are indistinguishable — which is why the guard, not this check, is what stops such a transaction. The state-gas figure is stated against its lane, and that one bites: with EIP-8037 off the transaction's own contribution is structurally zero, so the receipt's figure is exactly what the lane booked. That structural zero is the assumption both EIP-8037 lanes rest on, and this is where it is pinned. --- crates/mega-evm/src/evm/mod.rs | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 0cd0b6a3..a35de708 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -497,6 +497,27 @@ where /// having burnt the difference; both are carried on the result as their own fields and applied /// after the envelope is final, so the envelope this compares against is unaffected by either. /// +/// # The receipt's other two numbers +/// +/// The law reaches one of the three figures a receipt carries. The other two are checked here, each +/// on the terms available to it, because a check that looked only at the envelope would pass on a +/// transaction whose sender was billed a different amount. +/// +/// The **used** figure is stated against the accounted envelope rather than against the reported +/// one, so the same lanes have to account for both numbers the receipt carries. It is a +/// consistency pin rather than an independent measurement of what an inspector did to the refund: +/// the EIP-3529 cap applies to the transaction's whole refund at once, over a sum in which the +/// EVM's own refunds and an inspector's are indistinguishable, so no in-process reading separates +/// them. What stops such a transaction is the block guard, which reads +/// [`InspectorLedger::is_zero`](crate::InspectorLedger::is_zero) and therefore sees the refund +/// lane. +/// +/// The **state-gas** figure is stated against the state-gas lane, and that one does bite. `MegaETH` +/// runs with EIP-8037 off on every path and every spec, so the transaction's own contribution to +/// it — the intrinsic state gas, the per-authorization state refund — is structurally zero and the +/// receipt's figure is exactly what the lane booked. That structural zero is the assumption both +/// EIP-8037 lanes rest on, and this is where it is pinned. +/// /// What this catches that the settlement site's own cross-check cannot: a result whose envelope is /// decided *after* settlement, or a path that produces a receipt without settling at all. Both /// leave the settlement site's derived-versus-booked comparison perfectly happy and the reported @@ -534,6 +555,26 @@ fn debug_assert_envelope_accounted( outcome.compute_gas_used, outcome.compute_gas_destroyed, ); + let gas = outcome.result_and_state.result.gas(); + let ledger = outcome.inspector_ledger; + let used_accounted = accounted - i128::from(gas.inner_refunded()); + debug_assert!( + i128::from(gas.tx_gas_used()) == used_accounted.max(i128::from(gas.floor_gas())), + "the same lanes must account for the used figure the receipt reports: \ + used {} vs accounted {used_accounted} (refunded {}, floor {}, \ + inspector refund lane {})", + gas.tx_gas_used(), + gas.inner_refunded(), + gas.floor_gas(), + ledger.refund, + ); + debug_assert!( + i128::from(gas.state_gas_spent_final()) == ledger.state_gas.max(0), + "EIP-8037 is off on every MegaETH path, so the receipt's state gas is exactly what \ + the inspector lane booked: reported {} vs lane {}", + gas.state_gas_spent_final(), + ledger.state_gas, + ); } } From f353e882afe8ac4d1d51fba4ba09b0457c4e526a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:27:53 +0800 Subject: [PATCH 143/208] docs(evm): close the gas-surface enumeration on the receipt's other numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three rows move from open (or from a wrong Inert verdict) to lanes, the shape table gains the two rewrite shapes they cover, and the paragraph about deliberately-open rows becomes the statement that there are none and that the table cannot be left with one. Also records the one thing no test reaches — a gap misclassified as Inert or NotGas — with `state_gas_spent` as the case in point: it sat under "EIP-8037 is off, so nothing reads it", which is true of every instruction and not of the receipt. --- AGENTS.md | 4 +++- crates/mega-evm/src/evm/AGENTS.md | 18 +++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d1df8f08..966ec3ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,9 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). A frame the inspector answers itself is the one place a difference across the callback is not the measurement, because no frame is built and the whole result is the inspector's: the shim stages the envelope the answering callback was handed, and `inspect_frame_init` settles the gas the result finally carries against it on the result lane — which also covers whatever of an edit to the inputs survives into a guard's replacement result, and which is zero for the echo convention every tool that intercepts follows. What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. - Two numbers inside the gas objects the shim *does* hold are known not to be covered — a `Gas`'s `refunded` and its EIP-8037 `reservoir` — and `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration that names them, pinned by `tests/rex7/gas_surface.rs`. + The receipt's other two numbers have lanes of their own, measured at two different points because `MegaETH` produces one of the two quantities and none of the other: a refund is booked nominally across the callback boundary, since only a difference there separates an inspector's share from the EVM's own refunds, while the EIP-8037 state-gas dimension (`reservoir` and `state_gas_spent`, on a `Gas` or on a call's inputs) is settled once from the figures the transaction ends with — revm propagates it by replacement rather than accumulation, so a boundary difference would book edits the EVM goes on to erase. + The reservoir is a term of the conservation law because it lowers the envelope the receipt reports; the refund and the spend counter are not, and are refused by the block guard rather than accounted for. + `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration, pinned by `tests/rex7/gas_surface.rs`, which also fails on any row left saying a surface reaches the receipt and no lane books it. The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 0919ca3e..2a5c6940 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -52,6 +52,8 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`; nothing on the `env` lane — the edited inputs never reach a frame — and the gas the outcome carries on the `result` lane, measured against the envelope the answering callback was handed rather than as a difference across it | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | | A finished frame result's remaining gas (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::result`, at the frame's settlement point rather than at the callback boundary | nothing | | A finished frame result's returned output (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing | +| A refund written into any `Gas` a callback holds | Supported | `InspectorLedger::refund`, at the callback boundary, nominally | nothing: a refund moves `tx_gas_used`, not the `limit - remaining` the conservation law is stated over | +| The EIP-8037 state-gas pool or spend counter, on any `Gas` or on a call's inputs | Supported | `InspectorLedger::reservoir` / `::state_gas`, settled once from the figures the transaction ends with | the pool lowers the envelope the receipt reports, so it joins the law's `I` term; the spend counter moves the receipt's state-gas figure and nothing else | | A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | | A failed **call** frame rewritten into a success | Supported | `InspectorLedger::interventions` | the journal commits, so the frame's state follows the result its caller was handed | | A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | @@ -81,7 +83,7 @@ The shape table above is written over rewrites this repository has thought of. This one is written over the `Inspector` trait's own signatures, and is closed: `tests/rex7/gas_surface.rs` pins it against what upstream's derived `Debug` renders, field by field, and fails on one that has no verdict here. Read it by the object the gas sits in. -The first six rows are the measured lanes; the rest are the numbers that share those objects, each with the reason it needs no lane — or, for two of them, the statement that it does and has none. +The first six rows are the lanes measured across a callback boundary; the three after them are the numbers that share those objects and are measured somewhere else; the rest are the ones that need no lane, each with the reason. | Gas carrier | Reachable at | Verdict | | --- | --- | --- | @@ -91,19 +93,21 @@ The first six rows are the measured lanes; the rest are the numbers that share t | `FrameInput` / `CallInputs` / `CreateInputs` → `gas_limit` | `frame_start`, `call`, `create` | `InspectorLedger::env`, at the callback boundary — unless the same callback answers the frame, in which case this value is the baseline the row below is measured against | | The `Option` / `Option` / `Option` a callback **returns** → `gas` → `remaining` | `frame_start`, `call`, `create` | `InspectorLedger::result`, settled at `finalize_frame` against that baseline | | `FrameResult` / `CallOutcome` / `CreateOutcome` → `result.gas` → `remaining` | `frame_end`, `call_end`, `create_end` | `InspectorLedger::result`, at the frame's settlement point | -| Every `Gas` above → `refunded` | every callback that holds one | **Not closed.** A refund written here travels to the caller on frame return and reaches the receipt's gas used, while the conservation law — stated over `limit - remaining` — stays closed and every ledger lane stays zero. | -| Every `Gas` above → `reservoir`, and `CallInputs` / `CreateInputs` → `reservoir` | every callback that holds one | **Not closed.** EIP-8037 state gas is structurally zero on every `MegaETH` path, so a reservoir an inspector writes is gas the transaction never funded; it moves the envelope, and the terminal cross-check trips on it with every ledger lane zero. | +| Every `Gas` above → `refunded` | every callback that holds one | `InspectorLedger::refund`, at the callback boundary. Nominal: neither the EIP-3529 cap nor the chain of successful frame returns an edit must survive is attributable to one callback, and the lane feeds no identity — so over-stating it costs nothing, while under-stating it would let a rewritten receipt into a block. | +| Every `Gas` above → `reservoir`, and `CallInputs` / `CreateInputs` → `reservoir` | every callback that holds one | `InspectorLedger::reservoir`, settled once from the figure the transaction ends with, and a term of the law because the receipt reports the pool as unspent. `MegaETH` produces none of it, so there is no difference to take; revm propagates it between frames by replacement, so a boundary difference would book edits the EVM goes on to erase. | | Every `Gas` above → `gas_limit` | every callback that holds one | Inert. op-revm normalises the top-level gas object to the transaction's own limit before the settlement point, and no REX7 lane reads a frame's limit; the two that do are the REX4 legacy stipend's burn and rescue caps, which REX5 mode does not take. | -| Every `Gas` above → `state_gas_spent` | every callback that holds one | Inert, for the same reason as `reservoir`'s spending side: with EIP-8037 off, nothing reads it. | +| Every `Gas` above → `state_gas_spent` | every callback that holds one | `InspectorLedger::state_gas`, settled at the same point. Not a term of the law: it moves the receipt's state-gas figure, not the envelope. Its *other* effect — a failing frame folds it into its caller's pool — arrives inside the reservoir lane, which is read after the fold. | | Every `Gas` above → `memory` (`MemoryGas`) | every callback that holds one | Not a budget but a memo of the interpreter's memory size. Editing it without editing the memory desynchronises the two and the EVM reads out of bounds — the stack-and-memory row of the shape table, not a gas lane. | | `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | | The interpreter's `stack`, `memory`, `return_data`, `input`, `runtime_flag`, `extend` | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | | `&mut CTX` — the journal, and through `MegaContext`'s `DerefMut` the transaction, the block, the configuration and `MegaETH`'s own trackers | every callback but `selfdestruct` | Not gas the EVM handed over. Unmeasured for the reason the journal is: telling whether any of it came back changed needs a snapshot of unbounded state that no callback boundary can take at a cost the inspected path can carry. The gas schedule is the exception — the schedule pin rejects a rewritten one, at the next transaction rather than within this one. | | Everything passed by value (`Log`; `selfdestruct`'s three arguments) and the inputs the `*_end` callbacks take by shared reference | — | No mutable reach at all. | -**The two open rows are open on purpose.** -They were found by this audit, are not what the interception lane closed, and are recorded rather than fixed so that closing them is a decision with an owner. -`tests/rex7/gas_surface.rs::test_the_open_gaps_are_the_ones_the_table_names` names them, so closing one has to touch that test and this table together. +**There are no open rows, and the table cannot be left with one.** +`Coverage::NotClosed` still exists, so a surface that reaches what `MegaETH` reports and that no lane books is nameable — but `tests/rex7/gas_surface.rs::test_the_table_carries_no_open_gap` fails on any row that carries it. +Writing a gap down is how it gets closed; leaving it written down is how a table stops being a statement about the code. +What no test can catch is a gap _mis_-classified as `Inert` or `NotGas`: those verdicts are claims about what the EVM does with a number, and only the measurement each was written from backs them. +`state_gas_spent` is the cautionary case — it sat under "EIP-8037 is off, so nothing reads it", which is true of every instruction and not of the receipt. **What the closure pin does and does not reach.** A field upstream adds to any of these structs shows up in its `Debug` rendering, matches no row, and fails the test by name. From c4c257a3419044b7ff722546862cdec9e951d8b4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:31:51 +0800 Subject: [PATCH 144/208] test(rex7): pin the figures a synthetic outcome carries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cheat matrix excuses the refund and state-gas columns at the input-facing callbacks, because a frame's inputs carry neither counter. An outcome the callback returns does carry both, and there is no EVM-produced number on the other side of that callback to difference against — so the whole of what it carries is the inspector's, measured against nothing rather than against a baseline. Read against an echoing interception, which moves no figure at all. --- .../tests/rex7/refund_and_state_gas.rs | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs index e889011d..7beb44fc 100644 --- a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs +++ b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs @@ -32,8 +32,8 @@ use revm::{ context::{result::ExecutionResult, tx::TxEnvBuilder}, handler::EvmTr, interpreter::{ - interpreter_types::LoopControl, CallInputs, CallOutcome, Interpreter, InterpreterAction, - InterpreterTypes, + interpreter_types::LoopControl, CallInputs, CallOutcome, Gas, InstructionResult, + Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, Inspector, }; @@ -197,6 +197,23 @@ enum Edit { StateGasAtStep, /// Write the finished inner call's spend counter. StateGasAtCallEnd, + /// Answer the inner call with a synthetic outcome that echoes the envelope and carries + /// neither figure — the control the two below are read against. + InterceptEcho, + /// The same, carrying a refund the frame never earned. + InterceptWithRefund, + /// The same, carrying an EIP-8037 pool. + InterceptWithReservoir, +} + +impl Edit { + /// Whether this edit answers the frame itself instead of letting the EVM build it. + const fn intercepts(self) -> bool { + matches!( + self, + Self::InterceptEcho | Self::InterceptWithRefund | Self::InterceptWithReservoir + ) + } } /// Applies one [`Edit`], once, and records that it landed. @@ -243,13 +260,30 @@ impl Inspector for Editor { } fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if self.fired > 0 || inputs.target_address != CALLEE || self.edit != Edit::ReservoirOnInputs - { + if self.fired > 0 || inputs.target_address != CALLEE { return None; } - inputs.reservoir += RESERVOIR; + if self.edit == Edit::ReservoirOnInputs { + inputs.reservoir += RESERVOIR; + self.fired += 1; + return None; + } + if !self.edit.intercepts() { + return None; + } + // The echo convention every tool that intercepts follows: hand back exactly what was + // forwarded, so the gas lanes see nothing and only the figures under test move. + let mut gas = Gas::new(inputs.gas_limit); + match self.edit { + Edit::InterceptWithRefund => gas.record_refund(REFUND), + Edit::InterceptWithReservoir => gas.set_reservoir(RESERVOIR), + _ => {} + } self.fired += 1; - None + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas), + inputs.return_memory_offset.clone(), + )) } fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { @@ -577,3 +611,58 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { ); assert_identity("state gas on a reverting frame", &edited); } + +// --- a frame the inspector answers itself +// --------------------------------------------------------- + +/// A synthetic outcome carries figures of its own, and there is no EVM-produced number on the +/// other side of the callback to difference against — so the whole of what it carries is the +/// inspector's, measured against nothing rather than against a baseline. +/// +/// The echo control is what makes the two cells below readings of the figures rather than of the +/// interception: it moves the gas lanes not at all, which is the convention every tool that +/// intercepts follows. +#[test] +fn test_a_synthetic_outcome_carries_its_own_figures() { + let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); + assert_eq!( + echo.ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an echoing interception moves no figure at all", + ); + assert_identity("interception, echo", &echo); + + let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); + assert_eq!( + refunding.ledger, + InspectorLedger { + refund: i128::from(REFUND), + interventions: 1, + ..InspectorLedger::default() + }, + "the refund a frame that never ran hands back is the inspector's in whole", + ); + assert_eq!( + refunding.refunded, + echo.refunded + u64::try_from(REFUND).unwrap(), + "and it reaches the receipt: the outcome succeeded, so its caller records it", + ); + assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); + assert_identity("interception, refunding", &refunding); + + let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); + assert_eq!( + pooled.ledger, + InspectorLedger { + reservoir: i128::from(RESERVOIR), + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!( + pooled.total_gas_spent, + echo.total_gas_spent - RESERVOIR, + "a pool does move the envelope, wherever it came from", + ); + assert_identity("interception, pooled", &pooled); +} From 8b0f856a56624143b8b9ef39c24dce78c2a6f680 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Tue, 1 Sep 2026 22:34:24 +0800 Subject: [PATCH 145/208] test(rex7): pin the two lanes on a frozen spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shim is not spec-gated and must not be — the block guard has to see a rewritten receipt whichever spec produced it — so a frozen spec reports the lanes and settles nothing. Read by comparing an edited REX6 run against an unedited one: every term the transaction records is identical, and the law's inspector term moves with the lane exactly as it does under REX7, because that term is a reading of the ledger rather than something the transaction recorded. --- .../tests/rex7/refund_and_state_gas.rs | 86 ++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs index 7beb44fc..68b63ef1 100644 --- a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs +++ b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs @@ -99,9 +99,12 @@ fn tx() -> MegaTransaction { tx } -fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = MegaContext::new(db, MegaSpecId::REX7) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); +fn context_on( + db: &mut MemoryDatabase, + spec: MegaSpecId, +) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = + MegaContext::new(db, spec).with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec)); context.modify_chain(|chain| { chain.operator_fee_scalar = Some(U256::ZERO); chain.operator_fee_constant = Some(U256::ZERO); @@ -109,6 +112,10 @@ fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExt context } +fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + context_on(db, MegaSpecId::REX7) +} + fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { assert_eq!( outcome.inspector_ledger, @@ -666,3 +673,76 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { ); assert_identity("interception, pooled", &pooled); } + +// --- the frozen specs +// ----------------------------------------------------------------------------- + +/// On a frozen spec the two lanes report and settle nothing. +/// +/// The shim is not spec-gated, and must not be: the block guard has to see a rewritten receipt +/// whichever spec produced it. What is gated is the accounting the lanes feed, so a frozen spec's +/// own numbers have to be exactly what they were — which is what this reads, by comparing an +/// edited run against an unedited one on the same spec. +#[test] +fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { + fn run(edit: Option) -> (Reading, u64, u64) { + let mut db = db_for(Callee::Returning); + let mut editor = edit.map(Editor::new); + match &mut editor { + Some(editor) => { + let mut evm = + MegaEvm::new(context_on(&mut db, MegaSpecId::REX6)).with_inspector(editor); + let outcome = evm.execute_transaction(tx()).expect("no EVMError"); + assert_eq!(alloy_evm::Evm::inspector(&evm).fired, 1, "{edit:?} must land"); + let compute = outcome.compute_gas_used; + let destroyed = outcome.compute_gas_destroyed; + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + (reading, compute, destroyed) + } + None => { + let mut evm = MegaEvm::new(context_on(&mut db, MegaSpecId::REX6)); + let outcome = evm.execute_transaction(tx()).expect("no EVMError"); + let compute = outcome.compute_gas_used; + let destroyed = outcome.compute_gas_destroyed; + let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); + (reading, compute, destroyed) + } + } + } + + let (plain, plain_compute, plain_destroyed) = run(None); + assert!(plain.ledger.is_zero()); + + for (edit, expected) in [ + ( + Edit::RefundAtStep(REFUND), + InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + ), + ( + Edit::ReservoirAtStep, + InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + ), + ( + Edit::StateGasAtStep, + InspectorLedger { state_gas: i128::from(STATE_GAS), ..InspectorLedger::default() }, + ), + ] { + let (edited, compute, destroyed) = run(Some(edit)); + assert_eq!(edited.ledger, expected, "{edit:?}: the lane reports on every spec"); + assert_eq!(compute, plain_compute, "{edit:?}: a frozen spec's compute total must not move",); + assert_eq!(destroyed, plain_destroyed, "{edit:?}: nor its destroyed lane"); + // `inspector_conjured_gas` is a reading of the ledger rather than something the + // transaction recorded, so it moves with the lane on every spec. Every other term is what + // a frozen spec must leave alone. + assert_eq!( + ConservationTerms { inspector_conjured_gas: 0, ..edited.terms }, + plain.terms, + "{edit:?}: nothing a frozen spec records may move", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + edited.ledger.conjured_gas(), + "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", + ); + } +} From b3fcdd6e95d863d7140f9ba542c31ef390411faa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:03:58 +0800 Subject: [PATCH 146/208] feat(evm): make the ledger see the rewrites a net-only reading admitted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four shapes reached the shim, changed what the transaction produced, and left every lane at zero. Each signed lane is now a `Lane` carrying a gross beside its net. The law reads the nets, `is_zero` — the block guard's question — reads the grosses, and `Lane::book` moves both, so two edits to one lane that cancel are no longer a lane nobody touched. That covers a `+1` before a frame reads its own remaining gas and a `-1` after it, and the same pair split across a surviving frame and a rolled-back one. The shim now also snapshots a live interpreter's stack and memory sizes and the memo of how far that memory has been paid for, and a finished outcome's metadata around the `InterpreterResult` inside it. Growing the memory and the memo together leaves every interpreter invariant intact and skips the next expanding opcode's charge; a rewritten `memory_offset` feeds the caller a word the callee never wrote; a rewritten `CreateOutcome::address` reports a contract where none was deployed. All three are booked as interventions. Tables and matrix follow: `MemoryGas`'s rows are re-adjudicated (the old reason covered each field alone and not the pair with the memory beside it), the two outcome types get tables of their own, and the cheat matrix gets a `GrowMemoryFree` and an `EditOutcomeMetadata` column. --- AGENTS.md | 5 +- crates/mega-evm/src/evm/AGENTS.md | 19 +- crates/mega-evm/src/evm/execution.rs | 6 +- crates/mega-evm/src/evm/inspector.rs | 130 ++++- crates/mega-evm/src/evm/mod.rs | 6 +- crates/mega-evm/src/limit/inspector_ledger.rs | 250 ++++++++-- crates/mega-evm/src/limit/limit.rs | 16 +- .../tests/block_executor/inspector_guard.rs | 25 +- crates/mega-evm/tests/rex7/gas_surface.rs | 92 +++- .../tests/rex7/inspector_cheat_matrix.rs | 153 +++++- .../tests/rex7/inspector_settlement_window.rs | 18 +- .../mega-evm/tests/rex7/interception_gas.rs | 18 +- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 447 ++++++++++++++++++ crates/mega-evm/tests/rex7/main.rs | 4 + .../mega-evm/tests/rex7/measured_inspector.rs | 14 +- .../tests/rex7/refund_and_state_gas.rs | 58 ++- 16 files changed, 1117 insertions(+), 144 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/ledger_blind_spots.rs diff --git a/AGENTS.md b/AGENTS.md index 094047ed..d39069ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,10 +131,11 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. - The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome — because a rewrite that costs nothing still produces different state and a different receipt. + The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome, and the size of a live interpreter's stack or memory together with the memo of how far that memory has been paid for — because a rewrite that costs nothing still produces different state and a different receipt, and because a memory grown together with its memo skips the next expanding opcode's charge while leaving every interpreter invariant intact. + Every gas lane carries a gross alongside its net, and it is the gross that `is_zero` — the guard's question — reads: two edits to one lane that cancel are two edits, whether they cancel inside one frame or across a surviving frame and a rolled-back one, and a net-only reading calls that pair untouched while the execution saw a number the EVM would never have produced. The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). A frame the inspector answers itself is the one place a difference across the callback is not the measurement, because no frame is built and the whole result is the inspector's: the shim stages the envelope the answering callback was handed, and `inspect_frame_init` settles the gas the result finally carries against it on the result lane — which also covers whatever of an edit to the inputs survives into a guard's replacement result, and which is zero for the echo convention every tool that intercepts follows. - What no callback boundary can see stays invisible (the interpreter's stack and memory, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed come back changed, not that the transaction is the one the EVM would have produced alone. + What no callback boundary can see stays invisible (the *contents* of the interpreter's stack and memory at unchanged sizes, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed or could read in constant time come back changed, not that the transaction is the one the EVM would have produced alone. The receipt's other two numbers have lanes of their own, measured at two different points because `MegaETH` produces one of the two quantities and none of the other: a refund is booked nominally across the callback boundary, since only a difference there separates an inspector's share from the EVM's own refunds, while the EIP-8037 state-gas dimension (`reservoir` and `state_gas_spent`, on a `Gas` or on a call's inputs) is settled once from the figures the transaction ends with — revm propagates it by replacement rather than accumulation, so a boundary difference would book edits the EVM goes on to erase. The reservoir is a term of the conservation law because it lowers the envelope the receipt reports; the refund and the spend counter are not, and are refused by the block guard rather than accounted for. `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration, pinned by `tests/rex7/gas_surface.rs`, which also fails on any row left saying a surface reaches the receipt and no lane books it. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 2a5c6940..db9672ad 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -39,6 +39,12 @@ The shim's soundness rests on one fact: the EVM does not execute inside an inspe Anything that changes between the moment the shim delegates to the user's inspector and the moment control comes back is therefore the inspector's doing by construction, not by attribution — which is what makes the callback boundary a place a measurement can be taken at all. The shim snapshots what it cares about on the way in, compares on the way out, and books the difference on `InspectorLedger` (`../limit/inspector_ledger.rs`), which travels out on `MegaTransactionOutcome::inspector_ledger`. +Every gas lane is two numbers, because the ledger's two consumers ask different questions. +The conservation law needs the **net**, since gas written into one object and taken back out of another really did leave the envelope where it was. +The block guard needs the **gross**, since two edits that cancel are two edits: a `+1` before a frame reads its own remaining gas and a `−1` after it has read it net to nothing and leave the frame holding a number the EVM would never have produced, and the same pair split across a surviving frame and a rolled-back one moves what the sender pays. +`InspectorLedger::is_zero` — the guard's question — is defined over the gross halves; `conjured_gas` — the law's term — over the nets. +`Lane::book` moves both, which is what makes it impossible to move a lane without the guard seeing it. + ### What each rewrite shape costs Read the table by the *argument the rewrite reaches through*, not by the tool that makes it: two tools editing the same argument are one row. @@ -52,12 +58,14 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | A synthetic outcome that skips the frame entirely (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`; nothing on the `env` lane — the edited inputs never reach a frame — and the gas the outcome carries on the `result` lane, measured against the envelope the answering callback was handed rather than as a difference across it | the frame's envelope is settled at `finalize_frame` as `FrameExit::RefusedSynthetically` | | A finished frame result's remaining gas (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::result`, at the frame's settlement point rather than at the callback boundary | nothing | | A finished frame result's returned output (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing | +| A finished outcome's metadata — a call's `memory_offset`, a creation's `address`, the two flags beside them (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing: it changes what the caller reads next, not what the frame cost | | A refund written into any `Gas` a callback holds | Supported | `InspectorLedger::refund`, at the callback boundary, nominally | nothing: a refund moves `tx_gas_used`, not the `limit - remaining` the conservation law is stated over | | The EIP-8037 state-gas pool or spend counter, on any `Gas` or on a call's inputs | Supported | `InspectorLedger::reservoir` / `::state_gas`, settled once from the figures the transaction ends with | the pool lowers the envelope the receipt reports, so it joins the law's `I` term; the spend counter moves the receipt's state-gas figure and nothing else | | A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | | A failed **call** frame rewritten into a success | Supported | `InspectorLedger::interventions` | the journal commits, so the frame's state follows the result its caller was handed | | A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | -| The interpreter's stack or memory | Supported, unmeasured | nothing — no argument the shim holds describes it | the EVM executes on the edited state and meters it as its own work, because it is | +| The *size* of the interpreter's stack or memory, or the memo of how far that memory has been paid for (`Gas::memory`) | Supported | `InspectorLedger::interventions`, at the callback boundary, off the constant-time reading `inspector.rs::WorkingSet` takes | nothing directly — but growing the memory and the memo together leaves the interpreter consistent and skips the next expanding opcode's charge, which is why it needs a booking at all | +| The *contents* of the interpreter's stack or memory, at unchanged sizes | Supported, unmeasured | nothing — telling whether they came back changed needs a snapshot of unbounded state | the EVM executes on the edited state and meters it as its own work, because it is | | A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing — no argument the shim holds describes it | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | | The gas a pending `InterpreterAction` carries, reached through `LoopControl` (`step_end`) | Supported | the lane the action the callback left behind names: `InspectorLedger::result` for a `Return` action, settled at the frame's settlement point because that action *is* the frame's result a moment later; `InspectorLedger::env` for a `NewFrame` one, booked at the child's `frame_start`; `InspectorLedger::gas` when the callback removed the action, because the frame then carries on spending its counter | nothing | | A pending action's classification or output, or an action installed, removed or swapped for the other variant | Supported | `InspectorLedger::interventions`, alongside whatever gas the change moved on the lane above | it changes what the EVM does next, not what the frame has spent | @@ -97,9 +105,10 @@ The first six rows are the lanes measured across a callback boundary; the three | Every `Gas` above → `reservoir`, and `CallInputs` / `CreateInputs` → `reservoir` | every callback that holds one | `InspectorLedger::reservoir`, settled once from the figure the transaction ends with, and a term of the law because the receipt reports the pool as unspent. `MegaETH` produces none of it, so there is no difference to take; revm propagates it between frames by replacement, so a boundary difference would book edits the EVM goes on to erase. | | Every `Gas` above → `gas_limit` | every callback that holds one | Inert. op-revm normalises the top-level gas object to the transaction's own limit before the settlement point, and no REX7 lane reads a frame's limit; the two that do are the REX4 legacy stipend's burn and rescue caps, which REX5 mode does not take. | | Every `Gas` above → `state_gas_spent` | every callback that holds one | `InspectorLedger::state_gas`, settled at the same point. Not a term of the law: it moves the receipt's state-gas figure, not the envelope. Its *other* effect — a failing frame folds it into its caller's pool — arrives inside the reservoir lane, which is read after the fold. | -| Every `Gas` above → `memory` (`MemoryGas`) | every callback that holds one | Not a budget but a memo of the interpreter's memory size. Editing it without editing the memory desynchronises the two and the EVM reads out of bounds — the stack-and-memory row of the shape table, not a gas lane. | -| `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | -| The interpreter's `stack`, `memory`, `return_data`, `input`, `runtime_flag`, `extend` | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | +| Every `Gas` above → `memory` (`MemoryGas`: `words_num`, `expansion_cost`) | every callback that holds one | Not a budget but a memo of how far the frame's memory has been paid for — and the number the next expanding opcode compares its requirement against, so moving it *together with the memory* skips that opcode's charge while leaving every interpreter invariant intact. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | +| `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output`; `CallOutcome::memory_offset` / `::was_precompile_called` / `::precompile_call_logs` / `::charged_new_account_state_gas`; `CreateOutcome::address` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | +| The interpreter's `stack` and `memory` **sizes** | the four live-interpreter callbacks | Not gas, and the one part of the interpreter's working state a boundary can read in constant time. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | +| The interpreter's `stack` / `memory` **contents**, `return_data`, `input`, `runtime_flag`, `extend` | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | | `&mut CTX` — the journal, and through `MegaContext`'s `DerefMut` the transaction, the block, the configuration and `MegaETH`'s own trackers | every callback but `selfdestruct` | Not gas the EVM handed over. Unmeasured for the reason the journal is: telling whether any of it came back changed needs a snapshot of unbounded state that no callback boundary can take at a cost the inspected path can carry. The gas schedule is the exception — the schedule pin rejects a rewritten one, at the next transaction rather than within this one. | | Everything passed by value (`Log`; `selfdestruct`'s three arguments) and the inputs the `*_end` callbacks take by shared reference | — | No mutable reach at all. | @@ -125,6 +134,8 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Book a result rewrite from the frame's settlement point, not from the callback boundary.** Whether such an edit moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. The gas an intercepting callback puts into a synthetic outcome travels through that same lane. +- **Book a lane through `Lane::book`, never by writing its net.** + The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. - **Keep every rewrite out of a block.** Supporting a rewrite is not the same as admitting one: the canonical block-execution path refuses a transaction whose ledger is non-zero, in release builds as well as debug, because an inspector is one node's configuration and its edits reach the receipt. That is why a rewrite which moves no gas still has to be booked — on `InspectorLedger::interventions` — or the guard admits it. diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 85e4c757..dc2de124 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -2079,7 +2079,7 @@ fn gen_oog_frame_result(tx_kind: TxKind, gas_limit: u64) -> FrameResult { mod mutation_tests { use super::*; use crate::{ - test_utils::MemoryDatabase, AdditionalLimit, EmptyExternalEnv, EvmTxRuntimeLimits, + test_utils::MemoryDatabase, AdditionalLimit, EmptyExternalEnv, EvmTxRuntimeLimits, Lane, LimitCheck, LimitKind, }; use alloy_primitives::{Address, Log}; @@ -2226,7 +2226,7 @@ mod mutation_tests { ); assert_eq!( evm.ctx_ref().additional_limit.borrow().inspector_ledger().result, - i128::from(RAISE), + Lane::once(i128::from(RAISE)), "the caller reclaims the raise, so the ledger must carry it", ); consume_synthetic_limit_frame(evm.ctx_ref(), result); @@ -2246,7 +2246,7 @@ mod mutation_tests { let limit = evm.ctx_ref().additional_limit.borrow(); assert_eq!( limit.inspector_ledger().result, - 0, + Lane::default(), "a halting rejection hands nothing back, so the edit reaches nothing", ); assert_eq!( diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 8a182e30..0068d390 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -42,6 +42,10 @@ //! [`AdditionalLimit::stage_inspector_interception_envelope`], so that the frame init it //! short-circuited can settle the gas its synthetic outcome carries against what the transaction //! funded. +//! - Rewrites that move no gas go to [`book_intervention`]: a frame result's classification or +//! output, a frame's inputs outside their gas limit, a finished outcome's metadata +//! ([`OutcomeMetadata`]) — and the constant-time readings the shim can take off a live +//! interpreter ([`WorkingSet`]), which is what makes a frame's memory grown for free visible. //! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on @@ -49,17 +53,18 @@ #[cfg(not(feature = "std"))] use alloc as std; -use std::string::String; +use std::{string::String, vec::Vec}; use alloy_evm::Database; use alloy_primitives::{Address, Bytes, Log, U256}; +use core::ops::Range; use revm::{ context::{ContextError, ContextTr}, handler::FrameResult, interpreter::{ - interpreter_types::LoopControl, CallInputs, CallOutcome, CreateInputs, CreateOutcome, - FrameInput, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, - InterpreterTypes, + interpreter_types::{LoopControl, MemoryTr, StackTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, Inspector, }; @@ -414,9 +419,11 @@ fn book_env_adjustment( /// /// - **Gas.** A frame input's gas limit and a frame result's remaining gas are booked as gas, on /// the ledger's own lanes; counting them here as well would report one rewrite twice. -/// - **Anything the argument does not describe.** The interpreter's stack and memory, the journal, -/// the pending action. Telling whether those came back changed needs a snapshot of unbounded -/// state, which no callback boundary can take at a cost the inspected path can carry. +/// - **Anything neither the argument nor a constant-time reading off it describes.** The contents +/// of the interpreter's stack and memory, and the journal. Telling whether those came back +/// changed needs a snapshot of unbounded state, which no callback boundary can take at a cost the +/// inspected path can carry. Their *sizes* are a constant-time reading and are covered, by +/// [`WorkingSet`]; so is a finished outcome's metadata, by [`OutcomeMetadata`]. #[inline] fn book_intervention( context: &MegaContext, @@ -427,6 +434,101 @@ fn book_intervention( } } +/// The part of a live interpreter's state a callback boundary can read in constant time. +/// +/// The interpreter's stack and memory *contents* are outside the shim's reach — telling whether +/// either came back changed needs a snapshot of unbounded state — but their sizes are not, and +/// neither is the memo of how far the memory has been paid for. Those four readings are `O(1)`, +/// and the one rewrite they close is the only one in this area that leaves every interpreter +/// invariant intact: +/// +/// The memo (`Gas::memory`) is what the next expanding opcode compares its requirement against. An +/// inspector that raises it without growing the memory desynchronises the two and the EVM reads out +/// of bounds; one that grows the memory without raising it is charged for the growth twice over. +/// Moving *both*, together, is neither — the interpreter is in a state it could have reached by +/// paying, having paid nothing, and every later expansion inside the new bound is free. That pair +/// moves no gas anywhere at the moment it is made, so no gas lane can see it; what it changes is +/// what the EVM charges afterwards. +/// +/// A stack or memory edit that leaves both sizes where they were is deliberately still invisible: +/// it is a rewrite of contents, which is the row of the shape table that has no lane. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct WorkingSet { + /// How many words the frame has on its stack. + stack_len: usize, + /// How many bytes of memory the frame has. + memory_size: usize, + /// How many words of that memory the frame has been charged for. + memory_words: usize, + /// What that charge came to. + memory_expansion_cost: u64, +} + +impl WorkingSet { + /// Reads the four numbers off a live interpreter. + #[inline] + fn of(interp: &Interpreter) -> Self { + let memory = interp.gas.memory(); + Self { + stack_len: interp.stack.len(), + memory_size: interp.memory.size(), + memory_words: memory.words_num, + memory_expansion_cost: memory.expansion_cost, + } + } +} + +/// Everything a `CallOutcome` carries besides the `InterpreterResult` inside it. +/// +/// The result is compared on its own, by [`result_rewritten`]; this is the rest of the object, and +/// it is not bookkeeping. `memory_offset` is where the caller copies the callee's output to, so +/// moving it feeds the caller a word the callee never wrote. `charged_new_account_state_gas` tells +/// the caller whether to refund an EIP-8037 upfront charge. `was_precompile_called` and +/// `precompile_call_logs` decide which logs an inspector is shown next. +#[derive(Clone, Debug, PartialEq, Eq)] +struct CallMetadata { + memory_offset: Range, + was_precompile_called: bool, + precompile_call_logs: Vec, + charged_new_account_state_gas: bool, +} + +impl CallMetadata { + #[inline] + fn of(outcome: &CallOutcome) -> Self { + Self { + memory_offset: outcome.memory_offset.clone(), + was_precompile_called: outcome.was_precompile_called, + precompile_call_logs: outcome.precompile_call_logs.clone(), + charged_new_account_state_gas: outcome.charged_new_account_state_gas, + } + } +} + +/// [`CallMetadata`] for the generic callback, which is handed the variant rather than the outcome. +/// +/// A creation's own metadata is one field: the address the caller's stack is about to receive. +/// Rewriting it reports a contract at an address holding no code, while the code the EVM deployed +/// stays where it was — a split the result's classification cannot express, and one no gas lane +/// sees. +/// +/// Matched without a catch-all, so a `FrameResult` variant added upstream stops the build here. +#[derive(Clone, Debug, PartialEq, Eq)] +enum OutcomeMetadata { + Call(CallMetadata), + Create(Option
), +} + +impl OutcomeMetadata { + #[inline] + fn of(result: &FrameResult) -> Self { + match result { + FrameResult::Call(outcome) => Self::Call(CallMetadata::of(outcome)), + FrameResult::Create(outcome) => Self::Create(outcome.address), + } + } +} + /// Whether two output buffers are the same buffer. /// /// Compared by address and length rather than by content. `Bytes` is immutable, so a callback can @@ -543,7 +645,9 @@ where let action = interp.bytecode.action().clone(); let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); + let working_set = WorkingSet::of(interp); self.inner.initialize_interp(interp, context); + book_intervention(context, WorkingSet::of(interp) != working_set); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); book_refund( @@ -563,7 +667,9 @@ where let action = interp.bytecode.action().clone(); let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); + let working_set = WorkingSet::of(interp); self.inner.step(interp, context); + book_intervention(context, WorkingSet::of(interp) != working_set); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); book_refund( @@ -589,7 +695,9 @@ where let action = interp.bytecode.action().clone(); let refund_before = held_refund(action.as_ref(), &interp.gas); let before = interp.gas.remaining(); + let working_set = WorkingSet::of(interp); self.inner.step_end(interp, context); + book_intervention(context, WorkingSet::of(interp) != working_set); let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); book_pending_action(context, change); book_refund( @@ -621,7 +729,9 @@ where let action = interpreter.bytecode.action().clone(); let refund_before = held_refund(action.as_ref(), &interpreter.gas); let before = interpreter.gas.remaining(); + let working_set = WorkingSet::of(interpreter); self.inner.log_full(interpreter, context, log); + book_intervention(context, WorkingSet::of(interpreter) != working_set); let change = measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); book_pending_action(context, change); book_refund( @@ -666,6 +776,7 @@ where ) { let before = frame_result.instruction_result(); let output = frame_result.interpreter_result().output.clone(); + let metadata = OutcomeMetadata::of(frame_result); let refund_before = frame_result.gas().refunded(); self.inner.frame_end(context, frame_input, frame_result); book_refund(context, refund_before, frame_result.gas().refunded()); @@ -673,6 +784,7 @@ where context, result_rewritten((before, &output), frame_result.interpreter_result()), ); + book_intervention(context, OutcomeMetadata::of(frame_result) != metadata); // `frame_end` runs after `create_end` and is the last chance to rewrite a creation's // classification, so the same refusal applies here. if let FrameResult::Create(outcome) = frame_result { @@ -712,10 +824,12 @@ where outcome: &mut CallOutcome, ) { let before = (outcome.result.result, outcome.result.output.clone()); + let metadata = CallMetadata::of(outcome); let refund_before = outcome.result.gas.refunded(); self.inner.call_end(context, inputs, outcome); book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); + book_intervention(context, CallMetadata::of(outcome) != metadata); } #[inline] @@ -751,10 +865,12 @@ where outcome: &mut CreateOutcome, ) { let before = (outcome.result.result, outcome.result.output.clone()); + let address = outcome.address; let refund_before = outcome.result.gas.refunded(); self.inner.create_end(context, inputs, outcome); book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); + book_intervention(context, outcome.address != address); reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); } diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index a35de708..7220824e 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -566,14 +566,14 @@ fn debug_assert_envelope_accounted( gas.tx_gas_used(), gas.inner_refunded(), gas.floor_gas(), - ledger.refund, + ledger.refund.net(), ); debug_assert!( - i128::from(gas.state_gas_spent_final()) == ledger.state_gas.max(0), + i128::from(gas.state_gas_spent_final()) == ledger.state_gas.net().max(0), "EIP-8037 is off on every MegaETH path, so the receipt's state gas is exactly what \ the inspector lane booked: reported {} vs lane {}", gas.state_gas_spent_final(), - ledger.state_gas, + ledger.state_gas.net(), ); } } diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index caf99472..9a0365d9 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -6,30 +6,114 @@ //! so that enforcement can ignore it and the conservation law can account for it, and the two //! counters record rewrites that move no gas at all. +/// One signed lane of the ledger, and how much traffic it carried. +/// +/// # Why a lane is two numbers +/// +/// The lanes answer two different questions, and one number cannot answer both. +/// +/// The conservation law needs the **net**: gas an inspector wrote into one object and took back +/// out of another really has left the transaction's envelope where it was, and a law stated over +/// the gross would be wrong by exactly the round trip. +/// +/// The block guard needs the **gross**: it asks whether the transaction was left alone, and two +/// edits that cancel are two edits. A `+1` before the frame reads its own remaining gas and a `−1` +/// after it has read it net to nothing and leave the frame holding a number the EVM would never +/// have given it — and the same cancellation split across two frames, where only one of the two +/// survives to the receipt, moves what the sender pays while netting to zero. +/// +/// So the gross is not a diagnostic beside the net; it is the number +/// [`InspectorLedger::is_zero`] is defined over. [`book`](Self::book) moves both, which is what +/// makes it impossible to move a lane without the guard seeing it. +/// +/// # Saturation +/// +/// Both halves saturate. A ledger is a reported quantity that feeds no identity beyond the law's +/// own term, and a saturated lane still answers the guard's question the same way an exact one +/// would; an overflow panic on the inspected path would not. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Lane { + /// The sum of every booking, signed — what the transaction's envelope actually moved by. + net: i128, + /// The sum of every booking's magnitude — how much traffic this lane carried, in either + /// direction. + gross: u128, +} + +impl Lane { + /// A lane that carried one booking of `net`. + /// + /// The gross is `|net|`, which is what a single booking always produces. This is the + /// constructor for a caller stating an expectation over a lane moved in one direction; a lane + /// moved in both needs [`of`](Self::of), because the two numbers are then independent. + #[inline] + pub const fn once(net: i128) -> Self { + Self { net, gross: net.unsigned_abs() } + } + + /// A lane with both numbers stated, for a caller expecting bookings in both directions. + #[inline] + pub const fn of(net: i128, gross: u128) -> Self { + Self { net, gross } + } + + /// What the transaction's envelope moved by on this lane. + #[inline] + pub const fn net(self) -> i128 { + self.net + } + + /// How much traffic this lane carried, counting both directions. + #[inline] + pub const fn gross(self) -> u128 { + self.gross + } + + /// Whether nothing was ever booked here. + /// + /// Read off the gross, not the net: a lane whose bookings cancelled carried traffic, and the + /// whole point of the pair is that the guard can tell that from a lane nobody touched. + #[inline] + pub const fn is_zero(self) -> bool { + self.gross == 0 + } + + /// Books one movement on this lane. + #[inline] + pub(crate) const fn book(&mut self, delta: i128) { + self.net = self.net.saturating_add(delta); + self.gross = self.gross.saturating_add(delta.unsigned_abs()); + } +} + /// What an inspector conjured, destroyed, rewrote, or had refused, as measured at the callback /// boundaries. /// /// # Why the boundary is a sound place to measure /// /// The EVM does not execute inside an inspector callback. Every change to an interpreter's gas -/// counter, to the action it is holding, or to a frame input's gas limit that is visible across a -/// callback's entry and exit is therefore the inspector's, by construction rather than by -/// attribution heuristics. The shim takes one snapshot before delegating and one after, and the -/// difference lands here. +/// counter, to the action it is holding, to its working state, or to a frame input's gas limit +/// that is visible across a callback's entry and exit is therefore the inspector's, by +/// construction rather than by attribution heuristics. The shim takes one snapshot before +/// delegating and one after, and the difference lands here. /// /// # Sign convention /// -/// Every field measuring gas is signed and reads *from the transaction's point of view*: a positive -/// value is gas the inspector conjured — gas that exists in the execution but that nothing debited -/// from the transaction's envelope — and a negative value is gas it destroyed. Both directions are -/// recorded, because the conservation law needs the net, not the gross. +/// Every field measuring gas is a [`Lane`], whose net reads *from the transaction's point of +/// view*: a positive value is gas the inspector conjured — gas that exists in the execution but +/// that nothing debited from the transaction's envelope — and a negative value is gas it +/// destroyed. Both directions are recorded, because the conservation law needs the net; and each +/// lane carries the gross beside it, because the block guard needs to know a lane moved at all. /// /// # What it does not measure /// /// What a callback does behind the shim's back. An inspector reaches state that no argument it is -/// handed describes — the interpreter's stack and memory, the journal — and telling whether any of -/// those came back changed needs a snapshot of unbounded state that no callback boundary can take -/// at a cost the inspected path can carry. Those rewrites leave this all-zero. +/// handed describes — the interpreter's stack and memory contents, the journal — and telling +/// whether any of those came back changed needs a snapshot of unbounded state that no callback +/// boundary can take at a cost the inspected path can carry. The *sizes* of the interpreter's +/// stack and memory are the exception, because they are constant-time readings: the shim takes +/// them, and a frame whose memory was grown lands on [`interventions`](Self::interventions). +/// A same-size rewrite of what is in them leaves this all-zero. /// /// So an empty ledger says two things: no gas moved that the EVM did not move, and nothing the /// shim was handed came back different. It does not say the transaction is the one the EVM would @@ -42,14 +126,15 @@ /// moves, because that is what decides whether the conservation law can see it: /// /// - [`gas`](Self::gas), [`env`](Self::env), [`result`](Self::result) and -/// [`reservoir`](Self::reservoir) move the envelope, and are summed into +/// [`reservoir`](Self::reservoir) move the envelope, and their nets are summed into /// [`conjured_gas`](Self::conjured_gas), the law's `I` term; /// - [`refund`](Self::refund) moves the refund, which the law — stated over `limit - remaining` — /// cannot see at all; /// - [`state_gas`](Self::state_gas) moves the receipt's state-gas figure, which the law does not /// reach either. /// -/// All six are read by [`is_zero`](Self::is_zero), which is what the block guard asks. +/// All six are read by [`is_zero`](Self::is_zero), which is what the block guard asks — through +/// their gross halves, so that a lane whose bookings cancelled is not a lane nobody touched. /// /// # What consumes it /// @@ -73,7 +158,7 @@ /// window between them, whatever happened inside it. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct InspectorLedger { - /// Net gas the inspector wrote into interpreter gas counters, across every callback that is + /// Gas the inspector wrote into interpreter gas counters, across every callback that is /// handed a live [`Interpreter`](revm::interpreter::Interpreter). /// /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the @@ -81,9 +166,9 @@ pub struct InspectorLedger { /// /// A callback that removed the interpreter's pending action lands here too: with no action /// left the frame carries on spending what it holds, which is exactly what the counter is. - pub gas: i128, + pub gas: Lane, - /// Net gas the inspector wrote into frame *envelopes* — the `gas_limit` a call or create frame + /// Gas the inspector wrote into frame *envelopes* — the `gas_limit` a call or create frame /// is about to be built with. /// /// The caller was debited the forwarded amount by its own `CALL` / `CREATE` opcode, before any @@ -105,9 +190,9 @@ pub struct InspectorLedger { /// /// Adjustments to a frame's *result* gas belong to [`result`](Self::result), which is booked /// from the frame's own settlement point rather than from a callback boundary. - pub env: i128, + pub env: Lane, - /// Net gas the inspector wrote into a frame *result* — what the frame hands back to its + /// Gas the inspector wrote into a frame *result* — what the frame hands back to its /// caller — across the last callback that can rewrite that result. /// /// Unlike the other two lanes this one cannot be booked at the callback boundary, because @@ -129,7 +214,7 @@ pub struct InspectorLedger { /// question either way, and they settle at the same point for the same reason: a returning or /// reverting outcome hands its gas back to the caller, a halting one hands nothing back and /// the whole envelope is destroyed whatever figure the outcome claimed. - pub result: i128, + pub result: Lane, /// The EIP-8037 state-gas pool the transaction ends holding, which is gas nothing funded. /// @@ -149,9 +234,9 @@ pub struct InspectorLedger { /// callback sees. Reading the final number instead covers both: it is exactly the part of /// every edit that survived, and `MegaETH` contributes none of it, so no difference has to be /// taken to isolate the inspector's share. - pub reservoir: i128, + pub reservoir: Lane, - /// Net EIP-8037 state gas the inspector wrote into the `state_gas_spent` counters. + /// EIP-8037 state gas the inspector wrote into the `state_gas_spent` counters. /// /// The reservoir's counterpart on the spending side, and dead for the same reason `MegaETH` /// never fills one — except at the two places revm reads it regardless of whether EIP-8037 is @@ -164,9 +249,9 @@ pub struct InspectorLedger { /// figure is not the envelope, and adding it to the law's `I` term would make the law /// wrong by exactly this amount. Settled at the same point and for the same reasons as the /// lane above. - pub state_gas: i128, + pub state_gas: Lane, - /// Net gas the inspector wrote into the `refunded` counters of the `Gas` objects it is handed. + /// Gas the inspector wrote into the `refunded` counters of the `Gas` objects it is handed. /// /// A refund is the one number on a receipt the conservation law cannot see: the law is stated /// over `total_gas_spent`, which is `limit - remaining` and which no refund enters. What a @@ -191,10 +276,15 @@ pub struct InspectorLedger { /// boundary and no single settlement point can answer without a refund stack aligned to the /// EVM's frame lifecycle, which is the machinery this ledger deliberately does not have. /// + /// The lane's gross half is what makes the second of those safe. A `+R` on a frame that + /// survives and a `−R` on one that is rolled back are equal and opposite where they are + /// booked and not where they land, so a net-only reading would call that pair untouched while + /// the sender pays `R` less. + /// /// Both directions of the choice are safe here because the lane feeds no identity. /// Over-stating it costs nothing; under-stating it would let a transaction whose receipt /// an inspector moved into a block, which is the one thing the lane exists to prevent. - pub refund: i128, + pub refund: Lane, /// How many rewrites the shim refused because their shape is forbidden. /// @@ -207,18 +297,26 @@ pub struct InspectorLedger { /// How many rewrites the shim saw that change what the execution *did* rather than what it /// cost. /// - /// The three gas lanes above answer "did the transaction's numbers move". This answers the + /// The six gas lanes above answer "did the transaction's numbers move". This answers the /// other half — "was the transaction left alone" — for the part of it a callback boundary can - /// see, which is the arguments the shim itself is handed: + /// see, which is the arguments the shim itself is handed and the constant-time readings it + /// can take off a live interpreter: /// /// - a frame result whose classification or returned output came back changed, at each of the /// three callbacks that can change one (`call_end`, `create_end`, `frame_end` — revm runs /// the variant-specific one and then the generic one over the same result, so each is /// counted where it happens rather than once at the end); + /// - a finished outcome's metadata — where a call's return data lands in its caller's memory, + /// which address a creation reports, the two EIP-8037 and precompile-log flags beside them — + /// which sits outside the `InterpreterResult` those callbacks also hold; /// - a frame's inputs edited anywhere but in their gas limit, at each of the three callbacks /// that can edit them (`frame_start`, `call`, `create`); /// - a frame the inspector answered itself, with a synthetic outcome instead of letting the - /// EVM build it. + /// EVM build it; + /// - the size of a live interpreter's stack or memory, or the memo of how far that memory has + /// been paid for, at each of the four callbacks handed a live interpreter. Moving the memory + /// and the memo together is the one edit that leaves every interpreter invariant intact and + /// still changes what the next expanding opcode is charged. /// /// Gas edits are deliberately excluded — a gas limit or a result's remaining gas moving is /// what the lanes above are for, and counting it here would say the same thing twice. @@ -234,7 +332,7 @@ impl InspectorLedger { /// derivation adds to the transaction's envelope. #[inline] pub const fn conjured_gas(&self) -> i128 { - self.gas + self.env + self.result + self.reservoir + self.gas.net() + self.env.net() + self.result.net() + self.reservoir.net() } /// Whether the inspector left the transaction's gas accounting exactly as the EVM produced it. @@ -242,14 +340,17 @@ impl InspectorLedger { /// True for every observation-only inspector, and for every transaction that ran without one. /// Not the converse of "an inspector changed something": see the type's own documentation for /// the rewrites that move no gas and so leave this true. + /// + /// Each lane is asked through its gross half, so a lane an inspector moved and moved back is + /// not a lane it left alone. #[inline] pub const fn is_zero(&self) -> bool { - self.gas == 0 && - self.env == 0 && - self.result == 0 && - self.reservoir == 0 && - self.state_gas == 0 && - self.refund == 0 && + self.gas.is_zero() && + self.env.is_zero() && + self.result.is_zero() && + self.reservoir.is_zero() && + self.state_gas.is_zero() && + self.refund.is_zero() && self.rejected_rewrites == 0 && self.interventions == 0 } @@ -264,16 +365,89 @@ mod tests { /// unmoved in that case. #[test] fn test_conjured_gas_is_the_net_of_both_lanes() { - let ledger = InspectorLedger { gas: 2_300, env: -2_300, ..InspectorLedger::default() }; + let ledger = InspectorLedger { + gas: Lane::once(2_300), + env: Lane::once(-2_300), + ..InspectorLedger::default() + }; assert_eq!(ledger.conjured_gas(), 0); assert!(!ledger.is_zero(), "the lanes moved, even though they cancel"); } + /// ★ Two bookings on *one* lane that cancel are the shape a net-only guard admitted. + /// + /// The net is what the conservation law needs and it is genuinely zero here — the transaction's + /// envelope really did end where it started. What is not zero is that the lane carried + /// traffic, and between the two bookings the execution saw a number the EVM would never have + /// produced. + #[test] + fn test_bookings_that_cancel_on_one_lane_are_not_zero() { + let mut lane = Lane::default(); + lane.book(1); + lane.book(-1); + assert_eq!(lane.net(), 0, "the envelope is unmoved, and the law must read it that way"); + assert_eq!(lane.gross(), 2, "but the lane carried two bookings"); + assert!(!lane.is_zero()); + + let ledger = InspectorLedger { gas: lane, ..InspectorLedger::default() }; + assert_eq!(ledger.conjured_gas(), 0); + assert!(!ledger.is_zero(), "the guard must refuse a transaction whose lanes cancelled"); + } + + /// The same, on the lane where the two halves land in different frames — one that survives to + /// the receipt and one the journal rolls back. + #[test] + fn test_refund_bookings_that_cancel_are_not_zero() { + let mut refund = Lane::default(); + refund.book(2_000); + refund.book(-2_000); + let ledger = InspectorLedger { refund, ..InspectorLedger::default() }; + assert_eq!( + ledger.conjured_gas(), + 0, + "the refund lane is not a term of the law in either direction", + ); + assert!(!ledger.is_zero()); + } + + /// A lane nobody booked is the only zero lane. + #[test] + fn test_an_untouched_lane_is_the_only_zero_one() { + assert!(Lane::default().is_zero()); + assert!(!Lane::once(1).is_zero()); + assert!(!Lane::once(-1).is_zero()); + assert!(!Lane::of(0, 2).is_zero(), "a cancelled lane is not an untouched one"); + } + + /// `once` states the gross a single booking produces, which is what a caller expecting one + /// booking means. + #[test] + fn test_once_is_a_single_booking() { + let mut lane = Lane::default(); + lane.book(-2_300); + assert_eq!(lane, Lane::once(-2_300)); + } + + /// Both halves saturate rather than overflow. + #[test] + fn test_a_lane_saturates() { + let mut lane = Lane::of(i128::MAX, u128::MAX); + lane.book(i128::MAX); + assert_eq!(lane.net(), i128::MAX); + assert_eq!(lane.gross(), u128::MAX); + + let mut down = Lane::of(i128::MIN, 0); + down.book(i128::MIN); + assert_eq!(down.net(), i128::MIN); + assert_eq!(down.gross(), i128::MIN.unsigned_abs()); + } + /// The reservoir is envelope-moving gas and joins the law's term; the refund and the /// state-gas figure are not, and must not. #[test] fn test_only_the_envelope_moving_lanes_are_conjured_gas() { - let reservoir = InspectorLedger { reservoir: 10_000, ..InspectorLedger::default() }; + let reservoir = + InspectorLedger { reservoir: Lane::once(10_000), ..InspectorLedger::default() }; assert_eq!( reservoir.conjured_gas(), 10_000, @@ -282,8 +456,8 @@ mod tests { assert!(!reservoir.is_zero()); for ledger in [ - InspectorLedger { refund: 20_000, ..InspectorLedger::default() }, - InspectorLedger { state_gas: 5_000, ..InspectorLedger::default() }, + InspectorLedger { refund: Lane::once(20_000), ..InspectorLedger::default() }, + InspectorLedger { state_gas: Lane::once(5_000), ..InspectorLedger::default() }, ] { assert_eq!( ledger.conjured_gas(), diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index cc3b98ec..e39749f2 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -568,7 +568,7 @@ impl AdditionalLimit { return; } if reaches_envelope { - self.inspector.gas += i128::from(remaining_after) - i128::from(remaining_before); + self.inspector.gas.book(i128::from(remaining_after) - i128::from(remaining_before)); } if !IN_OPEN_SEGMENT || !self.rex7_enabled() { @@ -608,7 +608,7 @@ impl AdditionalLimit { /// being read. #[inline] pub(crate) fn record_inspector_env_adjustment(&mut self, delta: i128) { - self.inspector.env += delta; + self.inspector.env.book(delta); } /// Stages an adjustment an inspector made to the gas a *terminating* pending action carries. @@ -668,7 +668,7 @@ impl AdditionalLimit { /// same reason: the frame will spend what it now holds. #[inline] pub(crate) fn record_inspector_action_counter_adjustment(&mut self, delta: i128) { - self.inspector.gas += delta; + self.inspector.gas.book(delta); } /// Books an adjustment an inspector made to a refund counter — see @@ -681,7 +681,7 @@ impl AdditionalLimit { /// pays, so a receipt an inspector moved this way has to be refused like any other. #[inline] pub(crate) fn record_inspector_refund_adjustment(&mut self, delta: i128) { - self.inspector.refund += delta; + self.inspector.refund.book(delta); } /// Books the EIP-8037 state-gas dimension a transaction ends holding, at the one point it is @@ -702,8 +702,8 @@ impl AdditionalLimit { reservoir: u64, state_gas_spent: i64, ) { - self.inspector.reservoir += i128::from(reservoir); - self.inspector.state_gas += i128::from(state_gas_spent); + self.inspector.reservoir.book(i128::from(reservoir)); + self.inspector.state_gas.book(i128::from(state_gas_spent)); } /// Counts one rewrite the shim refused because its shape is forbidden — see @@ -1626,7 +1626,7 @@ impl AdditionalLimit { if destroyed::remaining_is_destroyed(result.instruction_result()) { evm_remaining } else { - self.inspector.result += delta; + self.inspector.result.book(delta); result.gas().remaining() } } @@ -2062,7 +2062,7 @@ impl AdditionalLimit { }; let consumed = staged.forwarded.saturating_sub(returned); self.compute_gas.record_burned_gas(consumed.saturating_sub(staged.executed)); - self.inspector.result += i128::from(staged.executed.saturating_sub(consumed)); + self.inspector.result.book(i128::from(staged.executed.saturating_sub(consumed))); } /// Merges resource usage from a sandbox execution into this tracker. diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 1526d639..7b02fa29 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -30,8 +30,9 @@ use mega_evm::{ alloy_consensus::{transaction::Recovered, Signed, TxLegacy}, alloy_evm::block::BlockExecutionError, test_utils::{BytecodeBuilder, MemoryDatabase}, - BlockLimits, InspectorLedger, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory, - MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, MegaTxEnvelope, TestExternalEnvs, + BlockLimits, InspectorLedger, Lane, MegaBlockExecutionCtx, MegaBlockExecutorFactory, + MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, MegaTxEnvelope, + TestExternalEnvs, }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, @@ -246,10 +247,10 @@ fn test_run_transaction_refuses_an_inspector_adjusted_transaction() { let ledger = expect_refusal(&err, *tx.hash()); assert_eq!( ledger.gas, - i128::from(INJECTED), + Lane::once(i128::from(INJECTED)), "the refusal must carry what was actually injected, so a caller can see the size of it", ); - assert_eq!(ledger.env, 0, "no frame envelope was touched"); + assert_eq!(ledger.env, Lane::default(), "no frame envelope was touched"); assert_eq!( executor.block_limiter.block_compute_gas_used, 0, "a refused transaction must leave the block's counters where they were", @@ -282,7 +283,7 @@ fn test_run_transaction_refuses_a_refund_rewrite() { let ledger = expect_refusal(&err, *tx.hash()); assert_eq!( ledger.refund, - i128::from(REFUNDED), + Lane::once(i128::from(REFUNDED)), "the refusal must carry the refund that was written", ); assert_eq!( @@ -315,7 +316,7 @@ fn test_execute_transaction_without_commit_refuses_it_too() { .execute_transaction_without_commit(&Recovered::new_unchecked(&tx, CALLER)) .expect_err("the trait entry must refuse it as well"); - assert_eq!(expect_refusal(&err, *tx.hash()).gas, i128::from(INJECTED)); + assert_eq!(expect_refusal(&err, *tx.hash()).gas, Lane::once(i128::from(INJECTED))); assert!(executor.receipts.is_empty(), "nothing may have been recorded"); } @@ -347,12 +348,12 @@ fn test_commit_refuses_a_result_an_inspector_took_part_in() { // The shape a result produced elsewhere arrives in: the numbers are execution's, the ledger // says an inspector moved some of them. - outcome.inner.inspector_ledger = InspectorLedger { gas: 1, ..Default::default() }; + outcome.inner.inspector_ledger = InspectorLedger { gas: Lane::once(1), ..Default::default() }; let err = executor.commit_transaction_outcome(outcome).expect_err("the commit funnel must refuse it"); - assert_eq!(expect_refusal(&err, *tx.hash()).gas, 1); + assert_eq!(expect_refusal(&err, *tx.hash()).gas, Lane::once(1)); assert!(executor.receipts.is_empty(), "no receipt may have been pushed"); assert_eq!( executor.block_limiter.block_gas_used, 0, @@ -389,14 +390,14 @@ fn test_the_infallible_commit_hook_latches_the_refusal() { depositor: outcome.depositor, inner: outcome.inner, }; - result.inner.inspector_ledger = InspectorLedger { env: -5, ..Default::default() }; + result.inner.inspector_ledger = InspectorLedger { env: Lane::once(-5), ..Default::default() }; let gas = executor.commit_transaction(result); assert_eq!(gas.tx_gas_used(), 0, "a transaction that contributed nothing must report zero gas",); let latched = executor .pending_commit_error() .expect("the refusal must be latched where `finish` will find it"); - assert_eq!(expect_refusal(latched, *tx.hash()).env, -5); + assert_eq!(expect_refusal(latched, *tx.hash()).env, Lane::once(-5)); let err = executor.finish().expect_err("the block must not finish over a latched refusal"); expect_refusal(&err, *tx.hash()); @@ -427,7 +428,7 @@ fn test_the_guard_is_not_spec_gated() { .unwrap_or_else(|| panic!("{spec:?}: the rewrite must be refused on every spec")); assert_eq!( expect_refusal(&err, *tx.hash()).gas, - i128::from(INJECTED), + Lane::once(i128::from(INJECTED)), "{spec:?}: and the measurement it is refused over must be the same one", ); } @@ -580,7 +581,7 @@ fn test_a_rewrite_that_moves_no_gas_is_refused_too() { let ledger = expect_refusal(&err, *tx.hash()); assert_eq!( (ledger.gas, ledger.env, ledger.result), - (0, 0, 0), + (Lane::default(), Lane::default(), Lane::default()), "the point of this shape is that no gas lane moves; got {ledger:?}", ); assert_eq!(ledger.interventions, 1, "the rewrite must be the thing the refusal names"); diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index fe5506fa..3331fa03 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -3,8 +3,14 @@ //! `inspector_cheat_matrix.rs` asks whether every callback × *rewrite shape* pair is covered. That //! question is answered over a shape list this repository writes down, so it can only be as //! complete as that list. This module asks the question one level below it, over a list -//! *upstream* writes down: is there a number that carries gas, reachable through an argument some -//! callback is handed, that nothing in `MegaETH` has classified? +//! *upstream* writes down: is there a field, reachable through an argument some callback is +//! handed, that nothing in `MegaETH` has classified? +//! +//! Gas is what the question was originally about and is still what most of the verdicts are about, +//! but the table covers every field of every object rather than the numeric ones — a field that +//! carries no gas and changes what the execution *does* needs a verdict just as much, and the two +//! cannot be told apart without looking. `CallOutcome::memory_offset` is the case that settled it: +//! not gas, not bookkeeping, and for a while not in the table at all. //! //! # The two levels the enumeration has //! @@ -127,16 +133,31 @@ const GAS_TRACKER_FIELDS: [(&str, Coverage); 5] = [ ]; /// The memoisation of how far a frame's memory has been paid for. +/// +/// Both rows were once excused as "editing this alone desynchronises the memo from the memory, and +/// the EVM then reads out of bounds" — which is true of each field on its own and not of the pair +/// with the memory beside it. An inspector that grows the memory *and* moves the memo leaves the +/// interpreter in a state it could have reached by paying, having paid nothing, and every later +/// expansion inside the new bound is free. The verdict stands — neither field is a budget, and +/// nothing here carries gas across the boundary — but the reason it needs no lane is now that it is +/// booked as an intervention, from the constant-time reading the shim takes off a live interpreter. const MEMORY_GAS_FIELDS: [(&str, Coverage); 2] = [ ( "words_num", Coverage::NotGas( - "a memo of the interpreter's memory size, not a budget: editing it without editing \ - the memory desynchronises the two and the EVM reads out of bounds, which is the \ - stack-and-memory row of the table rather than a gas lane", + "a memo of how far the frame's memory has been paid for, not a budget — but one the \ + next expanding opcode compares its requirement against, so moving it together with \ + the memory skips that opcode's charge. Booked as an intervention at each of the four \ + live-interpreter callbacks, off `WorkingSet`", + ), + ), + ( + "expansion_cost", + Coverage::NotGas( + "the memo's other half, which prices the *next* expansion incrementally; booked \ + the same way and for the same reason", ), ), - ("expansion_cost", Coverage::NotGas("the memo's other half, and dead for the same reason")), ]; /// The two halves of a `Gas`. @@ -193,6 +214,47 @@ const CREATE_INPUTS_FIELDS: [(&str, Coverage); 8] = [ ("cached_init_code_hash", Coverage::NotGas("a memo of the init code above")), ]; +/// Everything a finished call hands back besides the result inside it. +const CALL_OUTCOME_FIELDS: [(&str, Coverage); 5] = [ + ("result", Coverage::NotGas("a container; its own fields are classified separately")), + ( + "memory_offset", + Coverage::NotGas( + "the range of its caller's memory the callee's output is copied into — what the \ + caller reads next, not what the frame cost. Booked as an intervention", + ), + ), + ( + "was_precompile_called", + Coverage::NotGas("which logs the inspector is shown next; booked as an intervention"), + ), + ( + "precompile_call_logs", + Coverage::NotGas("the logs themselves, carried past a revert; booked as an intervention"), + ), + ( + "charged_new_account_state_gas", + Coverage::NotGas( + "an EIP-8037 refund flag rather than an amount, copied here from the call's inputs \ + so the caller knows whether to give the upfront charge back; booked as an \ + intervention like the inputs' own copy of it", + ), + ), +]; + +/// Everything a finished creation hands back besides the result inside it. +const CREATE_OUTCOME_FIELDS: [(&str, Coverage); 2] = [ + ("result", Coverage::NotGas("a container; its own fields are classified separately")), + ( + "address", + Coverage::NotGas( + "the address the caller's stack receives. Not gas, and not the same question as the \ + classification: the code stays deployed where the EVM put it, so a rewrite here \ + reports a contract at an address holding nothing. Booked as an intervention", + ), + ), +]; + /// Everything a finished frame hands back. const INTERPRETER_RESULT_FIELDS: [(&str, Coverage); 3] = [ ("gas", Coverage::NotGas("a container; its own fields are classified separately")), @@ -299,6 +361,14 @@ fn sample_result() -> InterpreterResult { InterpreterResult::new(InstructionResult::Stop, Bytes::new(), sample_gas()) } +fn sample_call_outcome() -> CallOutcome { + CallOutcome::new(sample_result(), 0..0) +} + +fn sample_create_outcome() -> CreateOutcome { + CreateOutcome::new(sample_result(), None) +} + // --- the field-level pin ------------------------------------------------------------------------- /// Every field of every gas-carrying object an inspector is handed has a verdict. @@ -317,6 +387,8 @@ fn test_every_field_of_every_gas_carrier_has_a_verdict() { ("CallInputs", std::format!("{:?}", sample_call_inputs())), ("CreateInputs", std::format!("{:?}", sample_create_inputs())), ("InterpreterResult", std::format!("{:?}", sample_result())), + ("CallOutcome", std::format!("{:?}", sample_call_outcome())), + ("CreateOutcome", std::format!("{:?}", sample_create_outcome())), ]; // Looked up rather than listed, so the set of tables the lock walks and the set this test // checks the renderings against cannot drift apart. @@ -353,7 +425,7 @@ fn test_the_field_reader_reads_the_top_level_and_stops_there() { } /// Every table this module classifies, by the name its rendering is checked under. -fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 6] { +fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 8] { [ ("Gas", GAS_FIELDS.as_slice()), ("GasTracker", GAS_TRACKER_FIELDS.as_slice()), @@ -361,6 +433,8 @@ fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 6] { ("CallInputs", CALL_INPUTS_FIELDS.as_slice()), ("CreateInputs", CREATE_INPUTS_FIELDS.as_slice()), ("InterpreterResult", INTERPRETER_RESULT_FIELDS.as_slice()), + ("CallOutcome", CALL_OUTCOME_FIELDS.as_slice()), + ("CreateOutcome", CREATE_OUTCOME_FIELDS.as_slice()), ] } @@ -473,8 +547,8 @@ fn test_every_gas_carrying_shape_is_matched_without_a_catch_all() { assert_eq!(action_carrier(&InterpreterAction::Return(sample_result())), Carrier::Gas); assert_eq!(action_carrier(&InterpreterAction::NewFrame(call)), Carrier::Envelope); - let call_result = FrameResult::Call(CallOutcome::new(sample_result(), 0..0)); - let create_result = FrameResult::Create(CreateOutcome::new(sample_result(), None)); + let call_result = FrameResult::Call(sample_call_outcome()); + let create_result = FrameResult::Create(sample_create_outcome()); assert_eq!(frame_result_carrier(&call_result), Carrier::Gas); assert_eq!(frame_result_carrier(&create_result), Carrier::Gas); } diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 29f46b43..758c673d 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -38,8 +38,8 @@ use alloy_primitives::{Address, Bytes, Log, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, - MegaTransactionOutcome, + Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, }; use revm::{ bytecode::opcode::{CALL, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, SSTORE, STOP}, @@ -92,6 +92,11 @@ const CLEARED_SLOT: u64 = 0x40; /// Value every fixture write stores, so a stack cheat that bumps it is visible as `2`. const STORED: u64 = 1; +/// The address the outcome-metadata column makes a successful creation report instead of the one +/// it deployed to. +const RELABELLED_DEPLOYMENT: Address = + alloy_primitives::address!("00000000000000000000000000000000000f00d0"); + /// The address the fixture's `CREATE` deploys to. fn deployed_address() -> Address { CONTRACT.create(0) @@ -194,13 +199,25 @@ enum Shape { WriteStateGas, /// Edit the interpreter's stack or memory — the frame's working state, which the EVM reads /// back as operands and as data. + /// + /// Two different rewrites share this column, and the shim can see one of them. A push or a pop + /// moves the stack's *length*, which is a constant-time reading the shim takes; overwriting a + /// word in place moves neither length and is the contents rewrite that has no lane. Which one + /// a row gets is decided by what the frame is doing at that callback, and each row's cell + /// states the ledger that follows. EditStackOrMemory, + /// Grow the frame's memory and the memo of how far it has been paid for, together — so the + /// interpreter stays consistent and the next expanding opcode is charged nothing. + GrowMemoryFree, + /// Edit a finished outcome's metadata: the range a call's return data lands in, or the address + /// a creation reports. Neither is part of the `InterpreterResult` the same callback holds. + EditOutcomeMetadata, /// Write to the journal directly, behind the EVM's back. JournalWrite, } impl Shape { - const ALL: [Self; 22] = [ + const ALL: [Self; 24] = [ Self::InjectGas, Self::DrainGas, Self::RaiseEnvelope, @@ -222,6 +239,8 @@ impl Shape { Self::WriteReservoir, Self::WriteStateGas, Self::EditStackOrMemory, + Self::GrowMemoryFree, + Self::EditOutcomeMetadata, Self::JournalWrite, ]; @@ -329,14 +348,16 @@ fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { } match shape { - InjectGas | DrainGas | EditStackOrMemory if !interpreter_facing => { + InjectGas | DrainGas | EditStackOrMemory | GrowMemoryFree if !interpreter_facing => { Some("no live interpreter is reachable from this callback") } RaiseEnvelope | LowerEnvelope | EditInput if !input_facing => Some( "this callback receives no frame input it can build a frame from: the `*_end` \ callbacks take theirs by shared reference, after the frame has already run", ), - RaiseResultGas | LowerResultGas | FailResult | ReviveResult if !result_facing => { + RaiseResultGas | LowerResultGas | FailResult | ReviveResult | EditOutcomeMetadata + if !result_facing => + { Some("no frame result exists yet at this callback") } ReviveResult if at == CreateEnd => Some( @@ -412,13 +433,19 @@ impl Cheat { } Shape::EditStackOrMemory => { // Bump the value an `SSTORE` is about to write, so the edit is visible in the - // produced state rather than only in the absence of an accounting change. + // produced state rather than only in the absence of an accounting change. Two + // pops and two pushes, so the stack's length is where it was: this is the + // contents half of the column. let [key, value] = interp.stack.popn::<2>().expect("an SSTORE has both its operands on the stack"); assert!(interp.stack.push(value.wrapping_add(U256::from(1)))); assert!(interp.stack.push(key)); self.fired += 1; } + Shape::GrowMemoryFree => { + Self::grow_memory_free(interp); + self.fired += 1; + } _ => unreachable!("{:?} is not an interpreter-facing shape", self.shape), } } @@ -459,6 +486,20 @@ impl Cheat { self.fired += 1; } + /// Grows the frame's memory by one word and moves the memo with it, so that the interpreter + /// is left in a state it could have reached by paying, having paid nothing. + /// + /// The fixture's first `MSTORE` then finds its word already paid for, and the transaction + /// spends exactly that expansion less than the uninspected run — which is what makes this a + /// rewrite the guard has to see rather than a curiosity. + fn grow_memory_free(interp: &mut Interpreter) { + let words = interp.memory.size() / 32 + 1; + assert!(interp.memory.resize(words * 32), "the fixture must allow a one-word growth"); + let memo = interp.gas.memory_mut(); + memo.words_num = words; + memo.expansion_cost = 3 * words as u64 + (words * words) as u64 / 512; + } + /// Applies a shape that reaches through the interpreter's *pending action* — the object the /// terminating or suspending instruction just left behind, which carries its own copy of the /// gas the frame is handing on. @@ -584,6 +625,27 @@ impl Cheat { } } + /// Applies the one result-facing shape that reaches past the `InterpreterResult` — a finished + /// outcome's own metadata. + /// + /// A call's return range is shrunk to nothing rather than moved, because moving it past the + /// caller's allocated memory is a panic in revm and this fixture's caller holds one word. The + /// visible-effect form, where the caller then reads a word the callee never wrote, is pinned + /// in `ledger_blind_spots.rs`. + fn hit_outcome_metadata(&mut self, result: &mut FrameResult) { + match result { + FrameResult::Call(outcome) => { + assert!( + !outcome.memory_offset.is_empty(), + "the fixture's inner CALL must ask for a return range, or there is nothing to shrink", + ); + outcome.memory_offset = outcome.memory_offset.start..outcome.memory_offset.start; + } + FrameResult::Create(outcome) => outcome.address = Some(RELABELLED_DEPLOYMENT), + } + self.fired += 1; + } + /// Applies a result-facing shape to a finished frame's result. fn hit_result(&mut self, result: &mut InterpreterResult) { match self.shape { @@ -748,6 +810,10 @@ impl Inspector for Cheat { self.hit_journal(context); return; } + if self.shape == Shape::EditOutcomeMetadata { + self.hit_outcome_metadata(result); + return; + } self.hit_result(result.interpreter_result_mut()); } @@ -770,6 +836,12 @@ impl Inspector for Cheat { self.hit_journal(context); return; } + if self.shape == Shape::EditOutcomeMetadata { + assert!(!outcome.memory_offset.is_empty(), "the inner CALL must ask for a range"); + outcome.memory_offset = outcome.memory_offset.start..outcome.memory_offset.start; + self.fired += 1; + return; + } self.hit_result(&mut outcome.result); } @@ -797,6 +869,11 @@ impl Inspector for Cheat { self.hit_journal(context); return; } + if self.shape == Shape::EditOutcomeMetadata { + outcome.address = Some(RELABELLED_DEPLOYMENT); + self.fired += 1; + return; + } self.hit_result(&mut outcome.result); } } @@ -859,8 +936,12 @@ fn caller_code() -> Bytes { .push_number(32u64) .push_number(0u64) .append(LOG1) - // CALL(gas, CALLEE, value=0, argsOffset=0, argsSize=0, retOffset=0, retSize=0) - .push_number(0u64) + // CALL(gas, CALLEE, value=0, argsOffset=0, argsSize=0, retOffset=0, retSize=32). + // + // The return range is asked for so that the outcome-metadata column has one to shrink; the + // callee returns nothing, so no byte is ever copied and no other cell's numbers move — the + // word of memory the range covers was already allocated by the `MSTORE` above. + .push_number(32u64) .push_number(0u64) .push_number(0u64) .push_number(0u64) @@ -1073,15 +1154,15 @@ struct Cell { } fn ledger_gas(gas: i128) -> InspectorLedger { - InspectorLedger { gas, ..InspectorLedger::default() } + InspectorLedger { gas: Lane::once(gas), ..InspectorLedger::default() } } fn ledger_env(env: i128) -> InspectorLedger { - InspectorLedger { env, ..InspectorLedger::default() } + InspectorLedger { env: Lane::once(env), ..InspectorLedger::default() } } fn ledger_result(result: i128) -> InspectorLedger { - InspectorLedger { result, ..InspectorLedger::default() } + InspectorLedger { result: Lane::once(result), ..InspectorLedger::default() } } /// The ledger a rewrite of one of the receipt's other two numbers books. @@ -1089,15 +1170,15 @@ fn ledger_result(result: i128) -> InspectorLedger { /// Separate helpers rather than one, because which of the three figures a shape moves is exactly /// what decides whether the conservation law can see it: only the pool is a term of it. fn ledger_refund(refund: i128) -> InspectorLedger { - InspectorLedger { refund, ..InspectorLedger::default() } + InspectorLedger { refund: Lane::once(refund), ..InspectorLedger::default() } } fn ledger_reservoir(reservoir: i128) -> InspectorLedger { - InspectorLedger { reservoir, ..InspectorLedger::default() } + InspectorLedger { reservoir: Lane::once(reservoir), ..InspectorLedger::default() } } fn ledger_state_gas(state_gas: i128) -> InspectorLedger { - InspectorLedger { state_gas, ..InspectorLedger::default() } + InspectorLedger { state_gas: Lane::once(state_gas), ..InspectorLedger::default() } } /// The ledger of a rewrite that moves no gas: the shim saw the argument it was handed come back @@ -1191,13 +1272,32 @@ fn matrix() -> Vec { ledger_gas(-i128::from(DRAIN)), state_all_committed, ); + // The two halves of the working-state column. `step` fires on an `SSTORE`'s operands and + // pops and pushes the same two words, so both sizes come back where they were and nothing + // is booked — the contents rewrite that has no lane. The other three rows run where there + // is no operand to swap, so the cheat leaves a word on the stack instead, and a stack that + // came back one word longer is a constant-time reading the shim takes. push( at, EditStackOrMemory, Fixture::ReturningCallee, - InspectorLedger::default(), + if at == At::InitializeInterp { + ledger_intervention() + } else { + InspectorLedger::default() + }, if at == Step { state_callee_write_bumped } else { state_all_committed }, ); + // Growing the memory and its memo together leaves every interpreter invariant intact and + // still skips the next expansion's charge, so no gas lane sees it and the intervention + // counter must. + push( + at, + GrowMemoryFree, + Fixture::ReturningCallee, + ledger_intervention(), + state_all_committed, + ); push( at, JournalWrite, @@ -1311,7 +1411,7 @@ fn matrix() -> Vec { RaiseInterceptionGas, Fixture::ReturningCallee, InspectorLedger { - result: i128::from(INTERCEPTION), + result: Lane::once(i128::from(INTERCEPTION)), interventions: 1, ..InspectorLedger::default() }, @@ -1322,7 +1422,7 @@ fn matrix() -> Vec { LowerInterceptionGas, Fixture::ReturningCallee, InspectorLedger { - result: -i128::from(INTERCEPTION), + result: Lane::once(-i128::from(INTERCEPTION)), interventions: 1, ..InspectorLedger::default() }, @@ -1344,7 +1444,7 @@ fn matrix() -> Vec { WriteReservoir, Fixture::ReturningCallee, InspectorLedger { - reservoir: i128::from(RESERVOIR), + reservoir: Lane::once(i128::from(RESERVOIR)), interventions: 1, ..InspectorLedger::default() }, @@ -1388,6 +1488,18 @@ fn matrix() -> Vec { state_callee_write_revived, ); } + // The half of a finished outcome that sits outside the `InterpreterResult`: where a call's + // return data lands, and which address a creation reports. Neither moves gas, and this + // fixture discards both — the caller asks for a range the callee never fills and pops the + // address — so what the cell pins is the booking. `ledger_blind_spots.rs` pins the forms + // that change the produced state. + push( + at, + EditOutcomeMetadata, + Fixture::ReturningCallee, + ledger_intervention(), + state_all_committed, + ); push( at, JournalWrite, @@ -1452,7 +1564,8 @@ fn test_every_cheat_shape_is_booked_and_the_law_still_closes() { // independent readings of the same edit. if cheat.moved_gas != 0 { assert_eq!( - reading.ledger.gas, cheat.moved_gas, + reading.ledger.gas.net(), + cheat.moved_gas, "{label}: the shim's reading of the counter edit must match the cheat's own", ); } @@ -1521,6 +1634,8 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { (At::Step, Shape::LowerRefund, Fixture::ReturningCallee), (At::CallEnd, Shape::WriteReservoir, Fixture::ReturningCallee), (At::FrameEnd, Shape::WriteStateGas, Fixture::ReturningCallee), + (At::Step, Shape::GrowMemoryFree, Fixture::ReturningCallee), + (At::CreateEnd, Shape::EditOutcomeMetadata, Fixture::ReturningCallee), ]; let mut plain: BTreeMap = BTreeMap::new(); diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index b51d3c0d..b8d669fa 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -31,7 +31,7 @@ use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ kzg_point_evaluation, test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, InspectorLedger, MegaSpecId, + EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, }; use revm::{ bytecode::opcode::{CALL, INVALID, POP, STOP}, @@ -196,7 +196,7 @@ fn test_an_edit_in_mid_frame_is_still_booked() { assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); assert_eq!( edited.inspector_ledger.gas, - i128::from(INJECT), + Lane::once(i128::from(INJECT)), "gas written into a counter the frame will keep spending is conjured gas", ); } @@ -212,7 +212,7 @@ fn test_an_edit_in_the_suspending_window_is_still_booked() { assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); assert_eq!( edited.inspector_ledger.gas, - i128::from(INJECT), + Lane::once(i128::from(INJECT)), "a suspended frame resumes on the edited counter, so the edit reaches the envelope", ); } @@ -483,7 +483,10 @@ fn test_raising_a_returning_frames_pending_action_is_booked() { assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( edited.inspector_ledger, - InspectorLedger { result: i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + InspectorLedger { + result: Lane::once(i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, "an edit to the action a returning frame hands back is an edit to the envelope", ); assert_eq!( @@ -508,7 +511,10 @@ fn test_lowering_a_returning_frames_pending_action_is_booked() { assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( edited.inspector_ledger, - InspectorLedger { result: -i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + InspectorLedger { + result: Lane::once(-i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, "gas taken out of the action is gas the caller never gets back", ); assert_eq!( @@ -559,7 +565,7 @@ fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); assert_eq!( edited.inspector_ledger, - InspectorLedger { env: i128::from(ACTION_DELTA), ..InspectorLedger::default() }, + InspectorLedger { env: Lane::once(i128::from(ACTION_DELTA)), ..InspectorLedger::default() }, "the child's budget grew by gas the caller's CALL never forwarded", ); assert_eq!( diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index b2fc23aa..10bebd0f 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -24,8 +24,8 @@ use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, - MegaTransactionOutcome, + Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, }; use revm::{ bytecode::opcode::{CALL, CREATE, MSTORE, MSTORE8, POP, RETURN, STOP}, @@ -238,7 +238,7 @@ fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { assert_eq!( reading.ledger, InspectorLedger { - result: Sizing::Half.expected_delta(FORWARDED), + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, ..InspectorLedger::default() }, @@ -257,7 +257,7 @@ fn test_a_zero_gas_interception_books_the_whole_envelope() { assert_eq!( reading.ledger, InspectorLedger { - result: Sizing::Zero.expected_delta(FORWARDED), + result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), interventions: 1, ..InspectorLedger::default() }, @@ -277,7 +277,7 @@ fn test_an_over_funded_interception_books_the_gas_it_conjured() { assert_eq!( reading.ledger, InspectorLedger { - result: Sizing::Excess(EXTRA).expected_delta(FORWARDED), + result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), interventions: 1, ..InspectorLedger::default() }, @@ -373,7 +373,7 @@ fn test_the_generic_frame_start_interception_is_measured_too() { assert_eq!( reading.ledger, InspectorLedger { - result: Sizing::Half.expected_delta(FORWARDED), + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, ..InspectorLedger::default() }, @@ -457,7 +457,7 @@ fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() assert_eq!( reading.ledger, InspectorLedger { - result: Sizing::Half.expected_delta(inspector.envelope), + result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), interventions: 1, ..InspectorLedger::default() }, @@ -507,7 +507,7 @@ fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { assert_eq!( reading.ledger, InspectorLedger { - result: i128::from(BONUS), + result: Lane::once(i128::from(BONUS)), interventions: 1, ..InspectorLedger::default() }, @@ -545,7 +545,7 @@ fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { assert_eq!( half.ledger, InspectorLedger { - result: Sizing::Half.expected_delta(FORWARDED), + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, ..InspectorLedger::default() }, diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs new file mode 100644 index 00000000..379a699d --- /dev/null +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -0,0 +1,447 @@ +//! The rewrite shapes an all-zero ledger used to admit. +//! +//! The measurement shim's contract is that a transaction an inspector rewrote never reaches a +//! block: the canonical path refuses one whose [`InspectorLedger`] is non-zero, so every rewrite +//! has to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that +//! per mechanism and per callback × shape pair. This module pins the four shapes that slipped +//! *between* those two questions — each one a rewrite the shim was handed, that changes what the +//! transaction produces, and that every lane read as nothing: +//! +//! - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it +//! has been paid for in the same step, so that neither goes out of bounds and the next expanding +//! opcode charges nothing; +//! - a `CallOutcome` / `CreateOutcome` metadata field — where the callee's return data lands, and +//! which address a creation reports — rewritten without touching the `InterpreterResult` inside +//! it, which is the only part the rewrite comparison used to read; +//! - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels +//! to zero; +//! - the same cancellation spread across two frames, where only one of the two survives to the +//! receipt, so the net is zero and the effect is not. +//! +//! The first three shapes are booked on [`InspectorLedger::interventions`]; the last two are what +//! the per-lane gross activity counters exist for. Every test here asserts the ledger the shim +//! books *and* the effect the rewrite had, because a shape that no longer changes anything is a +//! shape that stopped testing the guard. + +use crate::common::{ + transact, transact_inspected, CALLEE, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, STOP}, + interpreter::{ + interpreter_types::{Jumps, MemoryTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, + }, + Inspector, +}; + +/// A second callee, whose frame reverts. +const REVERTER: Address = EMPTY_TARGET; + +/// The address a rewritten `CreateOutcome` reports instead of the one the code was deployed at. +const FAKE_DEPLOYMENT: Address = address!("00000000000000000000000000000000000f00d0"); + +/// Slot the fixtures write their observable result to. +const RESULT_SLOT: u64 = 0x11; + +/// Refund a cancelling pair of refund edits moves, in each direction. +/// +/// Small enough to stay well under the EIP-3529 cap on every fixture here, so that what survives +/// to the receipt is the whole of the surviving half rather than whatever the cap left of it. +const REFUND: i64 = 2_000; + +fn db_with(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(crate::common::CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// The mainnet memory expansion cost of a memory `words` words long. +const fn memory_cost(words: u64) -> u64 { + 3 * words + words * words / 512 +} + +// --- a frame's memory, grown for free ------------------------------------------------------------ + +/// How far the free-expansion inspector grows the frame's memory, in words. +/// +/// The fixture's own `MSTORE` lands inside it, so the expansion the EVM would have charged for is +/// exactly the one the inspector already did for nothing. +const STOLEN_WORDS: u64 = 129; + +/// Grows the frame's memory and tells the EVM it is already paid for. +/// +/// Both halves are needed and neither is a rewrite on its own. Moving the memory alone leaves the +/// memo behind, and the next expanding opcode charges for an expansion that already happened; +/// moving the memo alone leaves the memory behind, and the EVM reads out of bounds. Moving both +/// keeps every invariant the interpreter has and skips the charge, which is why the pair was the +/// hole and neither half was. +#[derive(Default)] +struct FreeExpansion { + fired: u32, +} + +impl Inspector for FreeExpansion { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != MSTORE { + return; + } + assert!( + interp.memory.resize(STOLEN_WORDS as usize * 32), + "the fixture must allow the memory to be grown", + ); + let memo = interp.gas.memory_mut(); + memo.words_num = STOLEN_WORDS as usize; + memo.expansion_cost = memory_cost(STOLEN_WORDS); + self.fired += 1; + } +} + +/// ★ A frame whose memory was grown for free is not an all-zero ledger. +/// +/// The rewrite reaches through no argument the shim used to compare: the interpreter's gas counter +/// is untouched, no action is pending, no frame input and no frame result exists yet. What it +/// moves is the interpreter's memory and the memo beside it, and the transaction then pays less +/// than it would have — which is the one thing the guard exists to keep out of a block. +#[test] +fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { + // MSTORE(offset = STOLEN_WORDS * 32 - 32, value = 0xAA), which expands memory to exactly the + // size the inspector already grew it to. + let offset = (STOLEN_WORDS - 1) * 32; + let code = BytecodeBuilder::default() + .push_number(0xAAu64) + .push_number(offset) + .append(MSTORE) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = FreeExpansion::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + memory_cost(STOLEN_WORDS), + "the expansion the inspector performed is the charge the EVM then skipped", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction that paid less because an inspector moved its memory must not read as \ + untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- a call outcome's metadata ------------------------------------------------------------------- + +/// Where the fixture's `CALL` asks for its return data, and where the inspector moves it to. +const RETURN_AT: usize = 0; +const MOVED_TO: usize = 32; + +/// Moves a finished call's return data somewhere else in the caller's memory. +/// +/// The `InterpreterResult` inside the outcome — its classification, its output bytes, its gas — +/// comes back exactly as the EVM produced it. Only the range the caller will copy the output into +/// changes, which is not a field the result carries. +#[derive(Default)] +struct MoveReturnData { + fired: u32, +} + +impl Inspector for MoveReturnData { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.memory_offset = MOVED_TO..MOVED_TO + 32; + self.fired += 1; + } +} + +/// ★ A call outcome whose return range was moved is not an all-zero ledger. +#[test] +fn test_a_moved_return_range_is_booked() { + // Size the caller's memory to two words, call the callee for one word of output at offset 0, + // then store what landed there. + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(32u64) + .append(MSTORE) + .push_number(32u64) // retSize + .push_number(u64::try_from(RETURN_AT).unwrap()) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(u64::try_from(RETURN_AT).unwrap()) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(revm::bytecode::opcode::SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db(), limits); + let mut inspector = MoveReturnData::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the return data lands where the caller asked for it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the caller reads a word the callee never wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a rewritten return range changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +/// Reports a different address than the one the creation deployed to. +#[derive(Default)] +struct MoveDeploymentAddress { + fired: u32, +} + +impl Inspector for MoveDeploymentAddress { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if self.fired > 0 || outcome.address.is_none() { + return; + } + outcome.address = Some(FAKE_DEPLOYMENT); + self.fired += 1; + } +} + +/// ★ A creation outcome whose reported address was rewritten is not an all-zero ledger. +/// +/// The code is still deployed where the EVM put it; only the address the caller's stack receives +/// changes, so the caller goes on to talk to an account that holds nothing. +#[test] +fn test_a_rewritten_deployment_address_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(revm::bytecode::opcode::SSTORE) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = MoveDeploymentAddress::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); + let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); + assert_ne!(deployed, U256::ZERO, "the fixture's CREATE must succeed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(FAKE_DEPLOYMENT.as_slice()), + "the caller must have been handed the address the inspector wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction told a contract lives somewhere it does not must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +// --- two edits to one lane, in opposite directions ----------------------------------------------- + +/// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. +/// +/// Both edits land on the interpreter counter, which is one signed lane. Their net is zero and +/// the transaction's envelope is unmoved — and in between them the frame read a number one higher +/// than the EVM would have given it, and wrote that number to storage. +#[derive(Default)] +struct CancellingCounterEdits { + /// 0 before the injection, 1 between the two edits, 2 once both have landed. + phase: u8, +} + +impl Inspector for CancellingCounterEdits { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + match self.phase { + 0 if interp.bytecode.opcode() == GAS => { + interp.gas.erase_cost(1); + self.phase = 1; + } + 1 => { + assert!(interp.gas.record_regular_cost(1), "the frame must afford the give-back"); + self.phase = 2; + } + _ => {} + } + } +} + +/// ★ Two edits to the same lane that cancel are not an all-zero ledger. +/// +/// The net of the gas lane really is zero — the transaction spent exactly what it would have — so +/// nothing the conservation law reads has moved. What moved is the number the frame read in +/// between, and a guard that asks the net cannot see it. The gross activity counter is what does. +#[test] +fn test_cancelling_counter_edits_are_booked() { + let code = BytecodeBuilder::default() + .append(GAS) + .push_number(RESULT_SLOT) + .append(revm::bytecode::opcode::SSTORE) + .append(STOP) + .build(); + + // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of + // its remaining gas is the counter the injection moved. + let limits = EvmTxRuntimeLimits::no_limits(); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = CancellingCounterEdits::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)) + U256::from(1), + "the frame must have read one gas more than the EVM would have given it", + ); + assert_eq!( + cheated.total_gas_spent, plain.total_gas_spent, + "the two edits cancel, so the envelope the receipt reports is unmoved", + ); + assert_eq!( + cheated.inspector_conjured_gas, 0, + "and so is the law's term: this is exactly the shape a net-only reading cannot see", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the transaction was rewritten, and the guard has to see that: {:?}", + cheated.inspector_ledger, + ); +} + +/// Adds a refund to one child frame's result and takes the same amount out of another's. +/// +/// The frame that gets the addition returns, so its refund reaches the receipt. The frame that +/// gets the subtraction reverts, so revm discards its whole refund counter — the subtraction never +/// reaches anything. Net zero on the lane, one refund's worth of difference on the receipt. +#[derive(Default)] +struct CancellingRefundsAcrossFrames { + added: u32, + removed: u32, +} + +impl Inspector for CancellingRefundsAcrossFrames { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address == CALLEE && self.added == 0 { + outcome.result.gas.record_refund(REFUND); + self.added += 1; + } else if inputs.target_address == REVERTER && self.removed == 0 { + assert!( + outcome.result.gas.refunded() >= REFUND, + "the reverting callee must hold a refund of its own to take from, got {}", + outcome.result.gas.refunded(), + ); + outcome.result.gas.record_refund(-REFUND); + self.removed += 1; + } + } +} + +/// ★ A cancellation split across a surviving frame and a discarded one is not an all-zero ledger. +/// +/// This is the previous shape with the asymmetry made explicit: the two halves are equal and +/// opposite where the ledger books them, and only one of them is still standing by the time the +/// receipt is built. +#[test] +fn test_cancelling_refunds_across_frames_are_booked() { + let call_to = |builder: BytecodeBuilder, target: Address| { + builder + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(target) + .push_number(200_000u64) + .append(CALL) + .append(POP) + }; + let code = call_to(call_to(BytecodeBuilder::default(), CALLEE), REVERTER).append(STOP).build(); + // Both callees set a slot and clear it again, so each ends holding a refund the EVM produced. + let clearing = |builder: BytecodeBuilder| { + builder + .sstore(U256::from(RESULT_SLOT), U256::from(1u64)) + .sstore(U256::from(RESULT_SLOT), U256::ZERO) + }; + let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); + let reverting = clearing(BytecodeBuilder::default()).revert().build(); + let db = || { + db_with(code.clone()) + .account_code(CALLEE, returning.clone()) + .account_code(REVERTER, reverting.clone()) + }; + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db(), limits); + let mut inspector = CancellingRefundsAcrossFrames::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + + assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); + assert!( + plain.total_gas_spent >= 5 * u64::try_from(REFUND).unwrap(), + "the fixture must burn enough that the EIP-3529 cap does not hide the difference", + ); + assert_eq!( + plain.gas_used - cheated.gas_used, + u64::try_from(REFUND).unwrap(), + "only the surviving frame's half reaches the receipt, so the sender pays that much less", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a receipt an inspector moved must not read as untouched: {:?}", + cheated.inspector_ledger, + ); + let _ = DEFAULT_TX_GAS_LIMIT; +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index ea054046..3203a069 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -27,6 +27,9 @@ //! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. +//! - `ledger_blind_spots` — the four rewrite shapes an all-zero ledger used to admit: a frame's +//! memory grown for free, a call or create outcome's metadata rewritten around the result inside +//! it, and two edits to one signed lane that cancel — within a frame and across two of them. //! - `measured_inspector` — the shim every inspector is wrapped in: gas an inspector writes into an //! interpreter counter or a frame's gas limit is measured at the callback boundary, booked, and //! kept out of enforcement, with the clamp re-derived on the spot; reviving a failed creation is @@ -113,6 +116,7 @@ mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; mod late_frame_local; +mod ledger_blind_spots; mod measured_inspector; mod modexp_gas; mod opcode_set_parity; diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 03a28320..51139f53 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -21,8 +21,8 @@ use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, - MegaTransactionOutcome, + Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, }; use revm::{ bytecode::opcode::{ @@ -397,10 +397,10 @@ fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { ); assert_eq!( inspected.ledger.gas, - i128::from(INJECTED), + Lane::once(i128::from(INJECTED)), "the ledger must hold exactly what was injected", ); - assert_eq!(inspected.ledger.env, 0, "no frame envelope was touched"); + assert_eq!(inspected.ledger.env, Lane::default(), "no frame envelope was touched"); assert_eq!( i128::from(inspected.total_gas_spent) + i128::from(INJECTED), i128::from(plain.total_gas_spent), @@ -431,7 +431,7 @@ fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); assert_eq!( inspected.ledger.gas, - -i128::from(REMOVED), + Lane::once(-i128::from(REMOVED)), "the ledger must hold the removal as a negative entry", ); assert_eq!( @@ -485,10 +485,10 @@ fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { assert!(inspected.result.is_success(), "the inner call must still succeed"); assert_eq!( inspected.ledger.env, - i128::from(BONUS), + Lane::once(i128::from(BONUS)), "the ledger must hold exactly the gas the inspector added to the child's envelope", ); - assert_eq!(inspected.ledger.gas, 0, "no interpreter counter was touched"); + assert_eq!(inspected.ledger.gas, Lane::default(), "no interpreter counter was touched"); assert_eq!( inspected.total_gas_spent + BONUS, plain.total_gas_spent, diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs index 68b63ef1..58b66473 100644 --- a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs +++ b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs @@ -24,8 +24,8 @@ use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, - MegaTransactionOutcome, + Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, MegaTransactionOutcome, }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, @@ -365,7 +365,7 @@ fn test_a_refund_written_into_a_live_interpreter_is_booked() { assert_eq!( edited.ledger, - InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, "the shim must book the refund and nothing else", ); assert_eq!( @@ -398,7 +398,7 @@ fn test_a_refund_written_into_a_finished_frame_result_is_booked() { assert_eq!( edited.ledger, - InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, ); assert_eq!(edited.refunded, plain.refunded + u64::try_from(REFUND).unwrap()); assert_eq!(edited.total_gas_spent, plain.total_gas_spent); @@ -419,7 +419,7 @@ fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); assert_eq!( edited.ledger, - InspectorLedger { refund: -i128::from(REFUND), ..InspectorLedger::default() }, + InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, ); assert_eq!(edited.refunded, plain.refunded - u64::try_from(REFUND).unwrap()); assert_eq!( @@ -443,7 +443,10 @@ fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() assert_eq!( edited.ledger, - InspectorLedger { refund: i128::from(OVERSIZED_REFUND), ..InspectorLedger::default() }, + InspectorLedger { + refund: Lane::once(i128::from(OVERSIZED_REFUND)), + ..InspectorLedger::default() + }, "the lane carries the nominal edit", ); assert_eq!( @@ -473,7 +476,7 @@ fn test_a_refund_the_frame_chain_discards_is_still_booked() { assert_eq!( edited.ledger, - InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, "the lane books the edit", ); assert_eq!( @@ -495,7 +498,10 @@ fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes assert_eq!( edited.ledger, - InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, ); assert_eq!( edited.total_gas_spent, @@ -519,7 +525,7 @@ fn test_a_reservoir_written_into_a_frame_input_is_booked() { assert_eq!( edited.ledger, InspectorLedger { - reservoir: i128::from(RESERVOIR), + reservoir: Lane::once(i128::from(RESERVOIR)), // The inputs came back changed in a field the envelope lane does not cover, which the // rewrite comparison books on its own. interventions: 1, @@ -538,7 +544,10 @@ fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { assert_eq!( edited.ledger, - InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, ); assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); assert_identity("reservoir at call_end", &edited); @@ -581,7 +590,10 @@ fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_boo ); assert_eq!( edited.ledger, - InspectorLedger { state_gas: i128::from(STATE_GAS), ..InspectorLedger::default() }, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, ); assert_eq!( edited.total_gas_spent, plain.total_gas_spent, @@ -604,7 +616,10 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { assert_eq!( edited.ledger, - InspectorLedger { reservoir: i128::from(STATE_GAS), ..InspectorLedger::default() }, + InspectorLedger { + reservoir: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, "the spend counter of a reverting frame arrives in its caller as a pool", ); assert_eq!( @@ -643,7 +658,7 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { assert_eq!( refunding.ledger, InspectorLedger { - refund: i128::from(REFUND), + refund: Lane::once(i128::from(REFUND)), interventions: 1, ..InspectorLedger::default() }, @@ -661,7 +676,7 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { assert_eq!( pooled.ledger, InspectorLedger { - reservoir: i128::from(RESERVOIR), + reservoir: Lane::once(i128::from(RESERVOIR)), interventions: 1, ..InspectorLedger::default() }, @@ -716,15 +731,24 @@ fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { for (edit, expected) in [ ( Edit::RefundAtStep(REFUND), - InspectorLedger { refund: i128::from(REFUND), ..InspectorLedger::default() }, + InspectorLedger { + refund: Lane::once(i128::from(REFUND)), + ..InspectorLedger::default() + }, ), ( Edit::ReservoirAtStep, - InspectorLedger { reservoir: i128::from(RESERVOIR), ..InspectorLedger::default() }, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, ), ( Edit::StateGasAtStep, - InspectorLedger { state_gas: i128::from(STATE_GAS), ..InspectorLedger::default() }, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, ), ] { let (edited, compute, destroyed) = run(Some(edit)); From 18b5975e5d9e918dce060e91f4ec43502af20d8a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:17:42 +0800 Subject: [PATCH 147/208] feat(state-test): put the four blind-spot shapes in the chaos pool, with a gate for them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `grow_memory_free`, `move_outcome_metadata`, `cancel_gas_edit` and `cancel_refund_edit` join the pool. The first two are booked as interventions and the last two are what the per-lane gross exists for, so none of them is judged by the PANIC gate alone: none breaks the conservation law, which is the point — they are guard bypasses, not accounting breaks. So the sweep gets a third question. A run that applied a shape the shim is contracted to book unconditionally must not end with an all-zero ledger, because the canonical block path admits a transaction whose ledger is zero. `ChaosShape::is_always_booked` is the partition, and it is a subset on purpose: most shapes are booked only when what they moved still reaches something, and a gate stated over those would fail on a working shim. Writing the gate immediately found a case the pool had been producing all along: a refund written into an interpreter counter while a terminating action is pending lands in an object nobody reads again, so it is correctly booked nowhere. The cheat matrix already withholds that moment; the pool now does too. `test_every_always_booked_shape_moves_the_ledger` checks the gate's premise shape by shape against a fixture built to reach all of them, so the partition is evidence rather than a claim. --- .../tests/rex7/inspector_cheat_matrix.rs | 37 ++- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 24 +- crates/mega-state-test/src/chaos.rs | 295 +++++++++++++++++- crates/mega-state-test/tests/chaos_mode.rs | 97 +++++- crates/state-test/src/main.rs | 3 +- tools/eest-sweep/README.md | 5 +- 6 files changed, 412 insertions(+), 49 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 758c673d..338f1694 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -43,7 +43,7 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{CALL, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, SSTORE, STOP}, - context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr, JournalTr}, + context::{result::ExecutionResult, tx::TxEnvBuilder, Cfg, ContextTr, JournalTr}, handler::{EvmTr, FrameResult}, interpreter::{ interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, @@ -400,7 +400,11 @@ impl Cheat { } /// Applies an interpreter-facing shape. - fn hit_interpreter(&mut self, interp: &mut Interpreter) { + fn hit_interpreter( + &mut self, + interp: &mut Interpreter, + context: &CTX, + ) { match self.shape { Shape::InjectGas => { interp.gas.erase_cost(INJECT); @@ -443,7 +447,7 @@ impl Cheat { self.fired += 1; } Shape::GrowMemoryFree => { - Self::grow_memory_free(interp); + Self::grow_memory_free(interp, context); self.fired += 1; } _ => unreachable!("{:?} is not an interpreter-facing shape", self.shape), @@ -492,12 +496,17 @@ impl Cheat { /// The fixture's first `MSTORE` then finds its word already paid for, and the transaction /// spends exactly that expansion less than the uninspected run — which is what makes this a /// rewrite the guard has to see rather than a curiosity. - fn grow_memory_free(interp: &mut Interpreter) { + fn grow_memory_free( + interp: &mut Interpreter, + context: &CTX, + ) { let words = interp.memory.size() / 32 + 1; assert!(interp.memory.resize(words * 32), "the fixture must allow a one-word growth"); - let memo = interp.gas.memory_mut(); - memo.words_num = words; - memo.expansion_cost = 3 * words as u64 + (words * words) as u64 / 512; + // Priced through revm's own table rather than a restatement of the formula: the memo has + // to be exactly what the EVM would have written, or a later expansion prices its + // increment from a baseline that never existed. + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); } /// Applies a shape that reaches through the interpreter's *pending action* — the object the @@ -718,7 +727,7 @@ impl Inspector for Cheat { Shape::JournalWrite => self.hit_journal(context), Shape::EditStackOrMemory => self.hit_frame_state(interp), _ if !self.interpreter_moment_is_right(interp) => {} - _ => self.hit_interpreter(interp), + _ => self.hit_interpreter(interp, context), } } @@ -731,7 +740,7 @@ impl Inspector for Cheat { // than on a fixed ordinal. if self.shape.is_refund() { if self.interpreter_moment_is_right(interp) { - self.hit_interpreter(interp); + self.hit_interpreter(interp, context); } return; } @@ -740,10 +749,10 @@ impl Inspector for Cheat { // Fire on the first `SSTORE` the transaction reaches, whose operands are on the stack // and about to be consumed. Shape::EditStackOrMemory if interp.bytecode.opcode() == SSTORE => { - self.hit_interpreter(interp) + self.hit_interpreter(interp, context) } Shape::EditStackOrMemory | Shape::JournalWrite => {} - _ if self.steps == self.step_at => self.hit_interpreter(interp), + _ if self.steps == self.step_at => self.hit_interpreter(interp, context), _ => {} } } @@ -760,7 +769,7 @@ impl Inspector for Cheat { } if self.shape.is_refund() { if self.interpreter_moment_is_right(interp) { - self.hit_interpreter(interp); + self.hit_interpreter(interp, context); } return; } @@ -770,7 +779,7 @@ impl Inspector for Cheat { match self.shape { Shape::JournalWrite => self.hit_journal(context), Shape::EditStackOrMemory => self.hit_frame_state(interp), - _ => self.hit_interpreter(interp), + _ => self.hit_interpreter(interp, context), } } @@ -782,7 +791,7 @@ impl Inspector for Cheat { Shape::JournalWrite => self.hit_journal(context), Shape::EditStackOrMemory => self.hit_frame_state(interp), _ if !self.interpreter_moment_is_right(interp) => {} - _ => self.hit_interpreter(interp), + _ => self.hit_interpreter(interp, context), } } diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 379a699d..746e4da2 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -23,9 +23,7 @@ //! books *and* the effect the rewrite had, because a shape that no longer changes anything is a //! shape that stopped testing the guard. -use crate::common::{ - transact, transact_inspected, CALLEE, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, -}; +use crate::common::{transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET, ONE_ETH}; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, @@ -33,6 +31,7 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{CALL, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, STOP}, + context::{Cfg, ContextTr}, interpreter::{ interpreter_types::{Jumps, MemoryTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, @@ -87,18 +86,18 @@ struct FreeExpansion { fired: u32, } -impl Inspector for FreeExpansion { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { +impl Inspector for FreeExpansion { + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { if self.fired > 0 || interp.bytecode.opcode() != MSTORE { return; } - assert!( - interp.memory.resize(STOLEN_WORDS as usize * 32), - "the fixture must allow the memory to be grown", - ); - let memo = interp.gas.memory_mut(); - memo.words_num = STOLEN_WORDS as usize; - memo.expansion_cost = memory_cost(STOLEN_WORDS); + let words = STOLEN_WORDS as usize; + assert!(interp.memory.resize(words * 32), "the fixture must allow the memory to be grown",); + // Priced through revm's own table, so the memo is exactly what the EVM would have written + // had the frame paid; the assertion below restates the formula independently, which is + // what makes the two a check rather than one number written twice. + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); self.fired += 1; } } @@ -443,5 +442,4 @@ fn test_cancelling_refunds_across_frames_are_booked() { "a receipt an inspector moved must not read as untouched: {:?}", cheated.inspector_ledger, ); - let _ = DEFAULT_TX_GAS_LIMIT; } diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index e52fadc3..73d97185 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -21,6 +21,11 @@ //! inspector at all, on every quantity the differential classifier compares. That is the property //! every tracer in production depends on, and it is checked here against 44,000 transactions //! rather than against a handful of fixtures. +//! - **Can the guard still see it?** A rewriting run that applied a mutation the shim is contracted +//! to book unconditionally must not end with an all-zero ledger. The canonical block path admits +//! a transaction whose ledger is zero, so a rewrite that leaves it zero is a rewrite that reaches +//! a block — see [`ChaosClass::LedgerBlind`] and [`ChaosShape::is_always_booked`] for why the +//! gate is stated over a subset of the pool rather than over all of it. //! //! # Why the randomness is not random //! @@ -49,7 +54,7 @@ use crate::{ }; use indicatif::{ProgressBar, ProgressDrawTarget}; use mega_evm::revm::{ - context::{ContextTr, JournalTr}, + context::{Cfg, ContextTr, JournalTr}, handler::FrameResult, inspector::Inspector, interpreter::{ @@ -199,11 +204,25 @@ pub enum ChaosShape { /// An EIP-8037 spend counter written into a `Gas`. Structurally zero for the same reason, and /// reachable through two different receipt figures depending on how the frame ends. WriteStateGas, + /// The frame's memory grown, together with the memo of how far it has been paid for, so that + /// the interpreter stays consistent and the next expanding opcode is charged nothing. + GrowMemoryFree, + /// A finished outcome's metadata rewritten around the `InterpreterResult` inside it: the range + /// a call's return data lands in, shrunk to nothing, or the address a creation reports. + MoveOutcomeMetadata, + /// Gas injected into an interpreter counter and taken straight back out at the next + /// live-interpreter callback, so the lane's net is zero and the frame saw a number in between + /// that the EVM would never have produced. + CancelGasEdit, + /// A refund added to one finished frame's result and taken out of the next one's, so the + /// lane's net is zero and — whenever the two frames end differently — the receipt's is + /// not. + CancelRefundEdit, } impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 21] = [ + pub const ALL: [Self; 25] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -225,6 +244,10 @@ impl ChaosShape { Self::LowerRefund, Self::WriteReservoir, Self::WriteStateGas, + Self::GrowMemoryFree, + Self::MoveOutcomeMetadata, + Self::CancelGasEdit, + Self::CancelRefundEdit, ]; /// The shape a label names. @@ -265,8 +288,49 @@ impl ChaosShape { Self::LowerRefund => "lower_refund", Self::WriteReservoir => "write_reservoir", Self::WriteStateGas => "write_state_gas", + Self::GrowMemoryFree => "grow_memory_free", + Self::MoveOutcomeMetadata => "move_outcome_metadata", + Self::CancelGasEdit => "cancel_gas_edit", + Self::CancelRefundEdit => "cancel_refund_edit", } } + + /// Whether the shim is contracted to book a mutation of this shape unconditionally. + /// + /// Most shapes are booked *when the thing they moved still reaches something*: gas written into + /// a counter the interpreter is about to stop reading moves nothing, a result's remaining gas + /// edited on a halting frame is never handed back, an envelope edited by the same callback that + /// then answers the frame reaches no frame at all. Those are all correct non-bookings, and a + /// gate stated over them would be wrong. + /// + /// The shapes below have no such escape. Each of them either changes an argument the shim holds + /// — which the rewrite comparison reads on the spot — or moves a lane at a boundary that books + /// unconditionally. So a run that applied one of them and ended with an all-zero ledger is a + /// rewrite the canonical block path would admit, which is what + /// [`ChaosClass::LedgerBlind`] names. + pub const fn is_always_booked(self) -> bool { + matches!( + self, + // The rewrite comparison sees the argument come back changed. + Self::MakeStatic | + Self::Intercept | + Self::InterceptOverGas | + Self::InterceptUnderGas | + Self::InterceptNoGas | + Self::FailFrame | + Self::ReviveCall | + Self::GrowMemoryFree | + Self::MoveOutcomeMetadata | + // Refunds are booked at the callback boundary, whatever becomes of the frame — + // which is why the interpreter-facing arm of `hit_interpreter` withholds one that + // would land in a counter a terminating action has already displaced. That is the + // single window in which a refund edit reaches nothing, and the pool skips it + // rather than leaving these three shapes out of the gate. + Self::RaiseRefund | + Self::LowerRefund | + Self::CancelRefundEdit + ) + } } /// Shapes reachable from a callback that holds a live interpreter. @@ -274,7 +338,7 @@ impl ChaosShape { /// The last two only land at the one callback that runs with an action already pending — /// `step_end`, which revm's inspected loop runs after the instruction that set it. A draw for them /// anywhere else leaves the interpreter alone and spends no budget. -const INTERPRETER_SHAPES: [ChaosShape; 10] = [ +const INTERPRETER_SHAPES: [ChaosShape; 12] = [ ChaosShape::InjectGas, ChaosShape::DrainGas, ChaosShape::EditFrameState, @@ -285,6 +349,8 @@ const INTERPRETER_SHAPES: [ChaosShape; 10] = [ ChaosShape::LowerRefund, ChaosShape::WriteReservoir, ChaosShape::WriteStateGas, + ChaosShape::GrowMemoryFree, + ChaosShape::CancelGasEdit, ]; /// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. @@ -306,7 +372,7 @@ const INPUT_SHAPES: [ChaosShape; 9] = [ ]; /// Shapes reachable from a callback that holds a finished frame's result. -const RESULT_SHAPES: [ChaosShape; 9] = [ +const RESULT_SHAPES: [ChaosShape; 11] = [ ChaosShape::RaiseResultGas, ChaosShape::LowerResultGas, ChaosShape::FailFrame, @@ -316,6 +382,8 @@ const RESULT_SHAPES: [ChaosShape; 9] = [ ChaosShape::LowerRefund, ChaosShape::WriteReservoir, ChaosShape::WriteStateGas, + ChaosShape::MoveOutcomeMetadata, + ChaosShape::CancelRefundEdit, ]; /// Which mutations a chaos run is allowed to make. @@ -481,6 +549,15 @@ pub struct ChaosInspector { tick: u64, /// Mutations left in this transaction's budget. budget: u32, + /// Gas a [`ChaosShape::CancelGasEdit`] injected and has not taken back out yet. + /// + /// The give-back is taken at the next live-interpreter callback rather than on a second draw, + /// so the pair closes tightly and usually inside the same frame. Both halves land on the + /// interpreter lane, and their net is zero — which is the whole point of the shape. + pending_gas: u64, + /// A refund a [`ChaosShape::CancelRefundEdit`] added and has not taken back out yet, settled + /// at the next result-facing callback for the same reason. + pending_refund: i64, tally: ChaosTally, } @@ -494,7 +571,15 @@ impl ChaosInspector { /// the full run did. Narrowing therefore reproduces a flagged mutation; it does not reproduce /// a flagged run. pub fn new(seed: u64, filter: ShapeFilter) -> Self { - Self { seed, filter, tick: 0, budget: MUTATION_BUDGET, tally: ChaosTally::default() } + Self { + seed, + filter, + tick: 0, + budget: MUTATION_BUDGET, + pending_gas: 0, + pending_refund: 0, + tally: ChaosTally::default(), + } } /// What this run mutated. @@ -563,19 +648,66 @@ impl ChaosInspector { return; } } - ChaosShape::RaiseRefund | - ChaosShape::LowerRefund | - ChaosShape::WriteReservoir | - ChaosShape::WriteStateGas => { + ChaosShape::RaiseRefund | ChaosShape::LowerRefund => { + // A refund written into the interpreter's own counter while a terminating action + // is pending lands in an object nobody reads again: the action carries its own + // `Gas`, and that is what becomes the frame's result. The edit would be applied, + // correctly booked nowhere, and would then look to the ledger gate like a rewrite + // the shim missed. Leave the counter alone and spend no budget; the same edit + // reaches the live object at the next callback, and the dead window itself is + // pinned by `tests/rex7/inspector_settlement_window.rs`. + if matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) { + return; + } + if !edit_receipt_figure(&mut interp.gas, shape, entropy) { + return; + } + } + ChaosShape::WriteReservoir | ChaosShape::WriteStateGas => { if !edit_receipt_figure(&mut interp.gas, shape, entropy) { return; } } + ChaosShape::GrowMemoryFree => { + if !grow_memory_free(interp, context) { + return; + } + } + ChaosShape::CancelGasEdit => { + // The give-back is taken at the next live-interpreter callback, by + // `settle_pending_gas`. A second draw while one is outstanding leaves the + // interpreter alone and spends no budget. + if self.pending_gas != 0 { + return; + } + let amount = Self::amount(entropy); + interp.gas.erase_cost(amount); + self.pending_gas = amount; + } _ => return, } self.applied(shape); } + /// Takes back the gas a [`ChaosShape::CancelGasEdit`] injected, closing the pair. + /// + /// Skipped when the frame cannot afford it, rather than manufacturing an out-of-gas the EVM + /// did not reach — the pair then stays open, its net stays non-zero, and the ledger is + /// non-zero either way. + fn settle_pending_gas(&mut self, interp: &mut Interpreter) { + if self.pending_gas != 0 && interp.gas.record_regular_cost(self.pending_gas) { + self.pending_gas = 0; + } + } + + /// The refund half of the same mechanism, settled against a finished frame's result. + fn settle_pending_refund(&mut self, gas: &mut Gas) { + if self.pending_refund != 0 && gas.refunded() >= self.pending_refund { + gas.record_refund(-self.pending_refund); + self.pending_refund = 0; + } + } + /// Applies an input-facing shape to a call's inputs, or intercepts the frame. fn hit_call_inputs( &mut self, @@ -592,7 +724,14 @@ impl ChaosInspector { ChaosShape::LowerEnvelope => { inputs.gas_limit = inputs.gas_limit.saturating_sub(Self::amount(entropy)); } - ChaosShape::MakeStatic => inputs.is_static = true, + ChaosShape::MakeStatic => { + // A call already made static by its caller would come back unchanged, which is a + // mutation nothing has to book; leave it alone and spend no budget. + if inputs.is_static { + return None; + } + inputs.is_static = true; + } ChaosShape::WriteReservoir => { inputs.reservoir = inputs.reservoir.saturating_add(Self::amount(entropy)); } @@ -699,12 +838,45 @@ impl ChaosInspector { return; } } + ChaosShape::CancelRefundEdit => { + if self.pending_refund != 0 { + return; + } + let amount = Self::amount(entropy) as i64; + result.gas.record_refund(amount); + self.pending_refund = amount; + } _ => return, } self.applied(shape); } } +/// Grows the frame's memory and moves the memo of how far it has been paid for with it, returning +/// whether anything moved. +/// +/// The pair is what makes this a rewrite rather than a corruption: moving the memo alone leaves the +/// EVM reading out of bounds, moving the memory alone leaves the growth charged for twice, and +/// moving both leaves the interpreter in a state it could have reached by paying, having paid +/// nothing. The next expanding opcode inside the new bound is then charged nothing at all. +/// +/// The memo is priced through revm's own table rather than a restatement of the formula, because +/// `MemoryGas::set_words_num` hands revm's caller a `checked_sub` it unwraps unchecked: a memo +/// higher than the schedule would have written is undefined behaviour at the next expansion, not a +/// wrong number. +fn grow_memory_free( + interp: &mut Interpreter, + context: &CTX, +) -> bool { + let words = interp.memory.size() / 32 + 1; + if !interp.memory.resize(words * 32) { + return false; + } + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); + true +} + /// Writes one of the receipt figures that is not the envelope, returning whether anything moved. /// /// The three are grouped because they are one surface — every `Gas` an inspector is handed carries @@ -835,24 +1007,28 @@ fn write_journal(context: &mut CTX, entropy: u64) { impl Inspector for ChaosInspector { fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.settle_pending_gas(interp); if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { self.hit_interpreter(interp, context, shape, entropy); } } fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.settle_pending_gas(interp); if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { self.hit_interpreter(interp, context, shape, entropy); } } fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.settle_pending_gas(interp); if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { self.hit_interpreter(interp, context, shape, entropy); } } fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, _log: Log) { + self.settle_pending_gas(interp); if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { self.hit_interpreter(interp, context, shape, entropy); } @@ -886,9 +1062,15 @@ impl Inspector for ChaosInspe } fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { - if let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) { - self.hit_result(&mut outcome.result, false, shape, entropy); + self.settle_pending_refund(&mut outcome.result.gas); + let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; + if shape == ChaosShape::MoveOutcomeMetadata { + if shrink_return_range(outcome) { + self.applied(shape); + } + return; } + self.hit_result(&mut outcome.result, false, shape, entropy); } fn create_end( @@ -897,9 +1079,15 @@ impl Inspector for ChaosInspe _inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { - if let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) { - self.hit_result(&mut outcome.result, true, shape, entropy); + self.settle_pending_refund(&mut outcome.result.gas); + let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; + if shape == ChaosShape::MoveOutcomeMetadata { + if relabel_deployment(outcome) { + self.applied(shape); + } + return; } + self.hit_result(&mut outcome.result, true, shape, entropy); } fn frame_end( @@ -908,12 +1096,47 @@ impl Inspector for ChaosInspe _frame_input: &FrameInput, frame_result: &mut FrameResult, ) { + self.settle_pending_refund(frame_result.gas_mut()); let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; + if shape == ChaosShape::MoveOutcomeMetadata { + let moved = match frame_result { + FrameResult::Call(outcome) => shrink_return_range(outcome), + FrameResult::Create(outcome) => relabel_deployment(outcome), + }; + if moved { + self.applied(shape); + } + return; + } let is_creation = matches!(frame_result, FrameResult::Create(_)); self.hit_result(frame_result.interpreter_result_mut(), is_creation, shape, entropy); } } +/// Shrinks a finished call's return range to nothing, returning whether anything moved. +/// +/// Shrunk rather than moved: revm copies the callee's output into the caller's memory at this +/// range and panics if the range is outside what the caller has allocated, so the one edit that is +/// safe on an arbitrary corpus is the one that copies less. The caller then reads whatever was in +/// its memory before the call, which is the same semantic change the moved form makes. +fn shrink_return_range(outcome: &mut CallOutcome) -> bool { + if outcome.memory_offset.is_empty() { + return false; + } + outcome.memory_offset = outcome.memory_offset.start..outcome.memory_offset.start; + true +} + +/// Reports a successful creation at an address it did not deploy to, returning whether anything +/// moved. +fn relabel_deployment(outcome: &mut CreateOutcome) -> bool { + if outcome.address.is_none() || outcome.address == Some(CHAOS_ADDRESS) { + return false; + } + outcome.address = Some(CHAOS_ADDRESS); + true +} + // --- the sweep ------------------------------------------------------------------------------ /// How one vector's three runs came out. @@ -930,6 +1153,15 @@ pub enum ChaosClass { /// all: one produced a receipt and the other an `EVMError`. No inspector callback runs before /// validation, so the two cannot legitimately differ here. ChaosRejected, + /// The rewriting run applied a mutation the shim is contracted to book unconditionally — see + /// [`ChaosShape::is_always_booked`] — and still ended with an all-zero ledger. + /// + /// That is the one thing the ledger exists to prevent: the canonical block path admits a + /// transaction whose ledger is zero, so a rewrite that leaves it zero is a rewrite that + /// reaches a block. Unlike every other verdict here it is not about the transaction's numbers + /// being wrong — they may be exactly what a rewriting inspector should produce — but about the + /// guard being unable to tell that anything happened. + LedgerBlind, /// Neither run executed the transaction, and the runner declined it identically. Skipped, /// A run panicked — which, in a build with debug assertions live, is how a broken conservation @@ -944,6 +1176,7 @@ impl ChaosClass { Self::Pass => "PASS", Self::ControlDrift => "CONTROL_DRIFT", Self::ChaosRejected => "CHAOS_REJECTED", + Self::LedgerBlind => "LEDGER_BLIND", Self::Skipped => "SKIPPED", Self::Panic => "PANIC", } @@ -951,7 +1184,7 @@ impl ChaosClass { /// Whether this verdict fails the gate. pub const fn is_failure(self) -> bool { - matches!(self, Self::ControlDrift | Self::ChaosRejected | Self::Panic) + matches!(self, Self::ControlDrift | Self::ChaosRejected | Self::LedgerBlind | Self::Panic) } } @@ -1037,7 +1270,16 @@ pub fn chaos_unit( let applied = chaos.as_ref().ok().and_then(|run| run.chaos.clone()).unwrap_or_default(); let (class, detail) = match (reference.is_ok(), &chaos) { - (true, Ok(_)) => (ChaosClass::Pass, None), + (true, Ok(run)) => match blind_shapes(&applied, run.ledger.is_zero()) { + None => (ChaosClass::Pass, None), + Some(shapes) => ( + ChaosClass::LedgerBlind, + Some(format!( + "the run applied {shapes} and the ledger is still all-zero, so the canonical \ + block path would admit it" + )), + ), + }, // The runner declined this vector before execution — an intrinsic-gas overrun, an // unsupported transaction shape — and declined it the same way with the inspector // attached. Nothing executed, so nothing was tested; counted rather than passed. @@ -1054,6 +1296,27 @@ pub fn chaos_unit( ChaosVerdict { class, applied, detail } } +/// The shapes a run applied that the shim must have booked, when its ledger says it booked +/// nothing at all. +/// +/// `None` when the ledger is non-zero, or when every shape the run applied is one whose booking is +/// conditional on something the run may not have reached — gas written into a counter the +/// interpreter is about to stop reading, a result's gas edited on a frame that hands nothing back, +/// a pool a later frame overwrote. Those non-bookings are correct, and a gate stated over them +/// would fail on a working shim. +fn blind_shapes(applied: &ChaosTally, ledger_is_zero: bool) -> Option { + if !ledger_is_zero { + return None; + } + let blind: Vec<&str> = applied + .applied + .keys() + .copied() + .filter(|label| ChaosShape::parse(label).is_ok_and(ChaosShape::is_always_booked)) + .collect(); + (!blind.is_empty()).then(|| blind.join(", ")) +} + /// What one vector's three runs produced. #[derive(Debug, Clone, Default)] pub struct ChaosVerdict { diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index 416ee65a..cf2c1c12 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -35,7 +35,22 @@ fn callee_code() -> String { /// `SSTORE(2, 2); CREATE(0, 0, 0); POP; STOP` — a child frame the create callbacks see. const INNER_CODE: &str = "0x60026002556000600060006000f05000"; +/// [`callee_code`] with two things the shape pool needs and the plain fixture does not offer: a +/// slot set and cleared again, so every frame ends holding a refund the EVM produced, and a `CALL` +/// that asks for a word of return data, so a finished call outcome has a range to rewrite. +fn refunding_callee_code() -> String { + format!( + "0x6001600155 6000600155 6020 6000 6000 6000 6000 73{} 612710 f1 50 60006000a000", + &INNER[2..] + ) + .replace(' ', "") +} + fn unit_json() -> serde_json::Value { + unit_json_with(callee_code()) +} + +fn unit_json_with(callee: String) -> serde_json::Value { serde_json::json!({ "env": { "currentChainID": "0x18c6", @@ -50,7 +65,7 @@ fn unit_json() -> serde_json::Value { }, "pre": { SENDER: { "balance": "0xde0b6b3a7640000", "code": "0x", "nonce": "0x0", "storage": {} }, - CALLEE: { "balance": "0x0", "code": callee_code(), "nonce": "0x0", "storage": {} }, + CALLEE: { "balance": "0x0", "code": callee, "nonce": "0x0", "storage": {} }, INNER: { "balance": "0x0", "code": INNER_CODE, "nonce": "0x0", "storage": {} }, }, "transaction": { @@ -72,6 +87,11 @@ fn unit() -> TestUnit { serde_json::from_value(unit_json()).expect("valid unit json") } +/// The fixture the ledger-gate test uses — see [`refunding_callee_code`]. +fn refunding_unit() -> TestUnit { + serde_json::from_value(unit_json_with(refunding_callee_code())).expect("valid unit json") +} + /// The chaos run's tally for `unit` under `seed` and `filter`. fn mutations(seed: u64, filter: ShapeFilter) -> Vec<(String, u32)> { let run = @@ -134,9 +154,12 @@ fn test_a_vector_seed_separates_every_part_of_the_identity() { /// remain are applied at the same callbacks, so a flagged mutation is still there to be found. #[test] fn test_narrowing_the_filter_keeps_the_surviving_mutations() { - let full = mutations(0xC0FFEE, ShapeFilter::default()); + // A seed whose full run draws both of the shapes the narrowed one keeps; which seeds those are + // is a function of the pool's size, so a shape added to the pool can move it. + const SEED: u64 = 3; + let full = mutations(SEED, ShapeFilter::default()); let only = [ChaosShape::InjectGas, ChaosShape::DrainGas]; - let narrowed = mutations(0xC0FFEE, ShapeFilter::only(&only)); + let narrowed = mutations(SEED, ShapeFilter::only(&only)); let kept: Vec<_> = only.iter().map(|s| s.label()).collect(); assert!(!narrowed.is_empty(), "the narrowed run must still mutate something"); @@ -155,6 +178,74 @@ fn test_narrowing_the_filter_keeps_the_surviving_mutations() { } } +/// ★ Every shape the ledger gate is stated over really does move the ledger, run on its own. +/// +/// [`ChaosClass::LedgerBlind`] fires when a run applied one of these and the ledger is still +/// all-zero. That verdict is only meaningful if the premise holds — so this checks the premise +/// directly, shape by shape, rather than trusting the partition. Three of the shapes here +/// (`grow_memory_free`, `move_outcome_metadata`, `cancel_refund_edit`) were added because the +/// shim did *not* book them, and this is the test that would have said so. +#[test] +fn test_every_always_booked_shape_moves_the_ledger() { + let always_booked: Vec = + ChaosShape::ALL.into_iter().filter(|s| s.is_always_booked()).collect(); + assert!(!always_booked.is_empty(), "the gate must be stated over something"); + + let unit = refunding_unit(); + for shape in always_booked { + let filter = ShapeFilter::only(&[shape]); + let mut reached = 0u32; + // A narrow filter only lands where the stream happens to draw that shape, so sweep seeds + // until enough of them do. A shape no seed reaches is a hole in the pool, not a pass. + for seed in 0u64..1_024 { + let run = execute_unit_in_mode( + &unit, + VECTOR_0, + &SpecName::Rex7, + RunMode::Chaos { seed, filter }, + ) + .expect("the fixture executes"); + if run.chaos.as_ref().expect("a chaos run reports its tally").total() == 0 { + continue; + } + reached += 1; + assert!( + !run.ledger.is_zero(), + "{} applied under seed {seed} and the shim booked nothing: {:?}", + shape.label(), + run.ledger, + ); + if reached == 3 { + break; + } + } + assert_eq!(reached, 3, "{}: too few seeds in the sweep applied it", shape.label()); + } +} + +/// The partition the gate rests on is not vacuous in either direction. +/// +/// A gate stated over every shape would fail on a working shim — several shapes are booked only +/// when what they moved still reaches something — and one stated over none would never fire. +#[test] +fn test_the_always_booked_partition_is_not_vacuous() { + let (booked, conditional): (Vec<_>, Vec<_>) = + ChaosShape::ALL.into_iter().partition(|s| s.is_always_booked()); + assert!(!booked.is_empty(), "the gate must have shapes to fire on"); + assert!( + !conditional.is_empty(), + "a shape whose booking is conditional must stay out of the gate; if none is left, the \ + gate should be stated over the whole pool instead", + ); + for shape in [ChaosShape::InjectGas, ChaosShape::RaiseResultGas, ChaosShape::WriteReservoir] { + assert!( + !shape.is_always_booked(), + "{} is booked only when what it moved still reaches something", + shape.label(), + ); + } +} + /// Every shape label round-trips, and an unknown one is refused with a message that lists them. #[test] fn test_every_shape_label_parses_and_an_unknown_one_does_not() { diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index eb0d8539..10d502b0 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -566,10 +566,11 @@ fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { } /// Every chaos verdict, in the order a reader wants them. -const CHAOS_CLASSES: [ChaosClass; 5] = [ +const CHAOS_CLASSES: [ChaosClass; 6] = [ ChaosClass::Pass, ChaosClass::ControlDrift, ChaosClass::ChaosRejected, + ChaosClass::LedgerBlind, ChaosClass::Skipped, ChaosClass::Panic, ]; diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md index 9933a5bb..4359fef4 100644 --- a/tools/eest-sweep/README.md +++ b/tools/eest-sweep/README.md @@ -72,16 +72,17 @@ runs per-PR in CI and needs neither the corpus nor a build. `--mode chaos` asks a different question of the same corpus: not whether two specs agree, but whether the accounting survives an inspector that rewrites what it is handed. -Every vector is executed three times under the target spec — with no inspector, with a read-only one, and with a deterministic rewriting one — and two things are checked. +Every vector is executed three times under the target spec — with no inspector, with a read-only one, and with a deterministic rewriting one — and three things are checked. - **Observation is free.** The read-only run must be identical to the run with no inspector on every quantity the differential classifier compares, and must leave an empty inspector ledger. That is the property every tracer in production depends on, checked against the whole corpus rather than a handful of fixtures. - **Rewriting does not break the books.** Every gas-accounting cross-check MegaETH has is a debug assertion, so under the default `hivetests` profile a broken conservation law is a panic, and a panic is that vector's verdict rather than a lost worker thread. +- **The guard can still see the rewrite.** A run that applied a shape the shim is contracted to book unconditionally must not end with an all-zero inspector ledger, because the canonical block-execution path admits a transaction whose ledger is zero. The gate is stated over a subset of the pool on purpose: most shapes are booked only when what they moved still reaches something — gas written into a counter the interpreter is about to stop reading moves nothing, a result's remaining gas edited on a halting frame is never handed back — and a gate over those would fail on a working shim. `ChaosShape::is_always_booked` is the partition. The rewriting inspector's decisions come from a hash of the global seed and the vector's own identity — its fixture path, unit name and transaction indexes. No clock, no address, no iteration order. The same seed and the same corpus produce the same mutations on any machine, in any thread count, so a flagged vector's report line carries everything needed to re-run exactly it. -What fails the run: a `PANIC`, a `CONTROL_DRIFT` (the read-only run moved something), a `CHAOS_REJECTED` (the rewriting run changed whether the transaction executes at all), a file the sweep could not read, a run that judged no vector — and a run whose inspector mutated nothing, which would report every count truthfully zero while testing nothing. +What fails the run: a `PANIC`, a `CONTROL_DRIFT` (the read-only run moved something), a `CHAOS_REJECTED` (the rewriting run changed whether the transaction executes at all), a `LEDGER_BLIND` (the run rewrote something the guard cannot see), a file the sweep could not read, a run that judged no vector — and a run whose inspector mutated nothing, which would report every count truthfully zero while testing nothing. One rewrite shape is deliberately absent from the pool: turning a failed contract creation into a successful one. The shim refuses that shape and asserts on it, so including it would make the detector's own firing the sweep's dominant result. From b7000de07e4c10fe3612d3296bea6727172c3951 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:21:30 +0800 Subject: [PATCH 148/208] docs(evm): name MemoryGas as the second cautionary case in the closed table The first, state_gas_spent, had the wrong verdict. This one had the right verdict and a reason that covered half its own input space: editing either field alone really does desynchronise it from the memory, and moving both together with the memory is neither desynchronised nor charged for. A row that reads as considered is the harder of the two to see. --- crates/mega-evm/src/evm/AGENTS.md | 5 ++++- crates/mega-evm/tests/rex7/gas_surface.rs | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index db9672ad..84b75f1c 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -116,7 +116,10 @@ The first six rows are the lanes measured across a callback boundary; the three `Coverage::NotClosed` still exists, so a surface that reaches what `MegaETH` reports and that no lane books is nameable — but `tests/rex7/gas_surface.rs::test_the_table_carries_no_open_gap` fails on any row that carries it. Writing a gap down is how it gets closed; leaving it written down is how a table stops being a statement about the code. What no test can catch is a gap _mis_-classified as `Inert` or `NotGas`: those verdicts are claims about what the EVM does with a number, and only the measurement each was written from backs them. -`state_gas_spent` is the cautionary case — it sat under "EIP-8037 is off, so nothing reads it", which is true of every instruction and not of the receipt. +There are two cautionary cases, and they failed differently. +`state_gas_spent` sat under "EIP-8037 is off, so nothing reads it", which is true of every instruction and not of the receipt — a wrong verdict. +`MemoryGas` had the right verdict and the wrong reason: "editing this desynchronises it from the memory and the EVM reads out of bounds" is true of each field alone and false of the pair moved together with the memory, which is exactly the rewrite the row was excusing. +A reason that only covers half its own input space is the harder of the two to see, because the row reads as considered. **What the closure pin does and does not reach.** A field upstream adds to any of these structs shows up in its `Debug` rendering, matches no row, and fails the test by name. diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index 3331fa03..41556968 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -466,9 +466,14 @@ fn open_gaps(tables: &[(&'static str, &'static [(&'static str, Coverage)])]) -> /// /// It does not, and cannot, stop a hole from being *mis*classified as `Inert` or `NotGas`. Nothing /// mechanical can: those verdicts are claims about what the EVM does with a number, and what backs -/// them is the measurement each one was written from. `state_gas_spent` is the cautionary case — -/// it sat under `Inert` on the strength of "EIP-8037 is off", which is true and which the receipt -/// does not care about. +/// them is the measurement each one was written from. There are two cautionary cases, and they +/// failed differently. `state_gas_spent` sat under `Inert` on the strength of "EIP-8037 is off", +/// which is true and which the receipt does not care about — a wrong verdict. `MemoryGas`'s two +/// fields had the right verdict and the wrong reason: "editing this desynchronises it from the +/// memory and the EVM reads out of bounds" is true of each field alone and false of the pair moved +/// together with the memory, which is a rewrite that leaves every interpreter invariant intact and +/// is charged for nothing. A reason that only covers half its own input space is the harder of the +/// two to see, because the row reads as considered. #[test] fn test_the_table_carries_no_open_gap() { assert_eq!( From cf60d7a67227de013e6bee6c74d7e1018b45a82d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:27:25 +0800 Subject: [PATCH 149/208] test(block-executor): the block path refuses a frame grown for free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The other rewrite a gas-only guard could not see, alongside the classification one already here. It reaches through nothing the shim is handed — not a frame input, not a frame result, not the pending action, not any gas counter — so all six lanes stay at zero and the intervention counter is the whole of the refusal. --- .../tests/block_executor/inspector_guard.rs | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 7b02fa29..8dfcaec0 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -36,9 +36,12 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, - context::{BlockEnv, ContextTr}, + context::{BlockEnv, Cfg, ContextTr}, database::State, - interpreter::{CallInputs, CallOutcome, InstructionResult, Interpreter, InterpreterTypes}, + interpreter::{ + interpreter_types::MemoryTr, CallInputs, CallOutcome, InstructionResult, Interpreter, + InterpreterTypes, + }, Inspector, }; @@ -116,6 +119,37 @@ impl Inspector for RefundWriter { } } +/// Grows the frame's memory and the memo of how far it has been paid for, once — the rewrite that +/// reaches through no argument the shim is handed at all. +/// +/// Neither half is a rewrite on its own: moving the memo alone leaves the EVM reading out of +/// bounds, moving the memory alone leaves the growth charged for twice. Moving both leaves the +/// interpreter in a state it could have reached by paying, having paid nothing, and the next +/// expanding opcode inside the new bound is charged nothing at all. No gas moves at the moment the +/// edit is made, no frame input and no frame result exists, and the pending action is untouched — +/// so this is the shape the constant-time working-set reading exists for. +#[derive(Default)] +struct MemoryGrower { + applied: bool, +} + +impl Inspector for MemoryGrower { + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if self.applied { + return; + } + let words = interp.memory.size() / 32 + 1; + if !interp.memory.resize(words * 32) { + return; + } + // Priced through revm's own table, so the memo is exactly what the EVM would have written + // had the frame paid for the growth. + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); + self.applied = true; + } +} + /// Counts callbacks and changes nothing — the shape every tracer in production has. #[derive(Default)] struct Observer { @@ -591,6 +625,43 @@ fn test_a_rewrite_that_moves_no_gas_is_refused_too() { ); } +/// The other rewrite a gas-only guard could not see: a frame's memory, grown for free. +/// +/// This one reaches through nothing the shim is *handed* — not a frame input, not a frame result, +/// not the pending action, not any gas counter. What it moves is the interpreter's own working +/// state, and it moves the two halves of it that have to agree, so the EVM finds nothing wrong and +/// simply charges less. The transaction pays less than it would have, which is the one thing the +/// guard exists to keep out of a block. +#[test] +fn test_a_frame_grown_for_free_is_refused_too() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + MemoryGrower::default(), + ); + + let tx = envelope(0); + let err = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect_err("a frame grown for free must be refused like any other rewrite"); + + assert!(executor.evm().inspector.applied, "the fixture must reach the growth point"); + let ledger = expect_refusal(&err, *tx.hash()); + assert_eq!( + (ledger.gas, ledger.env, ledger.result, ledger.refund), + (Lane::default(), Lane::default(), Lane::default(), Lane::default()), + "no lane can see this shape; got {ledger:?}", + ); + assert_eq!(ledger.interventions, 1, "the growth must be the thing the refusal names"); + assert_eq!( + executor.block_limiter.block_compute_gas_used, 0, + "a refused transaction must leave the block's counters where they were", + ); +} + /// An EVM driven off the canonical path is not covered by the guard, however much its inspector /// rewrites. /// From 8eb7d6e9b0f11882f38b42d4fa14a668d7d0a65e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:31:36 +0800 Subject: [PATCH 150/208] docs(rex7): correct the blind-spot module's own count of what it pins --- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 746e4da2..45c400c7 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -1,11 +1,11 @@ //! The rewrite shapes an all-zero ledger used to admit. //! //! The measurement shim's contract is that a transaction an inspector rewrote never reaches a -//! block: the canonical path refuses one whose [`InspectorLedger`] is non-zero, so every rewrite -//! has to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that -//! per mechanism and per callback × shape pair. This module pins the four shapes that slipped -//! *between* those two questions — each one a rewrite the shim was handed, that changes what the -//! transaction produces, and that every lane read as nothing: +//! block: the canonical path refuses one whose `InspectorLedger` is non-zero, so every rewrite has +//! to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that per +//! mechanism and per callback × shape pair. This module pins the shapes that slipped *between* +//! those two questions — each one a rewrite the shim was handed, that changes what the transaction +//! produces, and that every lane read as nothing: //! //! - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it //! has been paid for in the same step, so that neither goes out of bounds and the next expanding @@ -18,10 +18,10 @@ //! - the same cancellation spread across two frames, where only one of the two survives to the //! receipt, so the net is zero and the effect is not. //! -//! The first three shapes are booked on [`InspectorLedger::interventions`]; the last two are what -//! the per-lane gross activity counters exist for. Every test here asserts the ledger the shim -//! books *and* the effect the rewrite had, because a shape that no longer changes anything is a -//! shape that stopped testing the guard. +//! The first two are booked on `InspectorLedger::interventions`, from a snapshot the shim did not +//! used to take; the last two are what the per-lane gross activity counters exist for. Every test +//! here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no +//! longer changes anything is a shape that stopped testing the guard. use crate::common::{transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET, ONE_ETH}; use alloy_primitives::{address, Address, Bytes, U256}; From 53b3df78364cc179d5586d7cae5a6945da7ffff5 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 10:33:08 +0800 Subject: [PATCH 151/208] docs(evm): state what the working-set snapshot covers without overclaiming The stack's length is closed by the same reading as the memory's, so the memory pair is the rewrite that made the snapshot necessary rather than the only one it reaches. --- crates/mega-evm/src/evm/inspector.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 0068d390..8fb87a6c 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -439,8 +439,10 @@ fn book_intervention( /// The interpreter's stack and memory *contents* are outside the shim's reach — telling whether /// either came back changed needs a snapshot of unbounded state — but their sizes are not, and /// neither is the memo of how far the memory has been paid for. Those four readings are `O(1)`, -/// and the one rewrite they close is the only one in this area that leaves every interpreter -/// invariant intact: +/// so the shim takes them on the way in and on the way out and books a difference as an +/// intervention. +/// +/// The pair that made them necessary is the memory and its memo, moved together. /// /// The memo (`Gas::memory`) is what the next expanding opcode compares its requirement against. An /// inspector that raises it without growing the memory desynchronises the two and the EVM reads out @@ -450,8 +452,11 @@ fn book_intervention( /// moves no gas anywhere at the moment it is made, so no gas lane can see it; what it changes is /// what the EVM charges afterwards. /// -/// A stack or memory edit that leaves both sizes where they were is deliberately still invisible: -/// it is a rewrite of contents, which is the row of the shape table that has no lane. +/// The stack's length is here for the same reason and at the same cost, and it moves whenever a +/// callback pushes or pops. +/// +/// A stack or memory edit that leaves both sizes where they were stays invisible: it is a rewrite +/// of contents, which is the row of the shape table that has no lane. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct WorkingSet { /// How many words the frame has on its stack. From 9688e9aa53497a1230cb26b393bf447980b193d2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 11:14:23 +0800 Subject: [PATCH 152/208] test(rex7): anchor the two shapes an all-zero ledger still admits An instruction deleted from a frame by stepping its program counter past it, and a return buffer put in front of a frame that made no call. Both change what the transaction produces and what it pays, and every lane reads zero. Red as committed: the shim's working-set snapshot holds four chosen readings and neither of these moves one of them. Each test asserts the effect first, so the only failing assertion is the ledger's. --- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 170 +++++++++++++++++- crates/mega-evm/tests/rex7/main.rs | 6 +- 2 files changed, 166 insertions(+), 10 deletions(-) diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 45c400c7..92342e0f 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -16,13 +16,18 @@ //! - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels //! to zero; //! - the same cancellation spread across two frames, where only one of the two survives to the -//! receipt, so the net is zero and the effect is not. +//! receipt, so the net is zero and the effect is not; +//! - an instruction deleted from a frame, by stepping the program counter past it, so the work is +//! never performed and there is nothing for any counter to meter; +//! - a return buffer put in front of a frame that made no call, so `RETURNDATASIZE` reads a length +//! no call produced. //! -//! The first two are booked on `InspectorLedger::interventions`, from a snapshot the shim did not -//! used to take; the last two are what the per-lane gross activity counters exist for. Every test +//! Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use +//! to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test //! here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no //! longer changes anything is a shape that stopped testing the guard. + use crate::common::{transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET, ONE_ETH}; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ @@ -30,10 +35,12 @@ use mega_evm::{ EvmTxRuntimeLimits, MegaSpecId, }; use revm::{ - bytecode::opcode::{CALL, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, STOP}, + bytecode::opcode::{ + CALL, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, STOP, + }, context::{Cfg, ContextTr}, interpreter::{ - interpreter_types::{Jumps, MemoryTr}, + interpreter_types::{Jumps, MemoryTr, ReturnData}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, }, Inspector, @@ -187,7 +194,7 @@ fn test_a_moved_return_range_is_booked() { .push_number(u64::try_from(RETURN_AT).unwrap()) .append(MLOAD) .push_number(RESULT_SLOT) - .append(revm::bytecode::opcode::SSTORE) + .append(SSTORE) .append(STOP) .build(); // The callee returns one word of 0x11s. @@ -264,7 +271,7 @@ fn test_a_rewritten_deployment_address_is_booked() { .push_number(0u64) .append(CREATE) .push_number(RESULT_SLOT) - .append(revm::bytecode::opcode::SSTORE) + .append(SSTORE) .append(STOP) .build(); @@ -328,7 +335,7 @@ fn test_cancelling_counter_edits_are_booked() { let code = BytecodeBuilder::default() .append(GAS) .push_number(RESULT_SLOT) - .append(revm::bytecode::opcode::SSTORE) + .append(SSTORE) .append(STOP) .build(); @@ -443,3 +450,150 @@ fn test_cancelling_refunds_across_frames_are_booked() { cheated.inspector_ledger, ); } + +// --- an opcode skipped, and a return buffer conjured +// ---------------------------------------------- + +/// What the fixture's `SSTORE` writes when it runs. +const STORED: u64 = 0x99; + +/// The gas a cold `SSTORE` into a zero slot costs, which is what skipping it saves. +const COLD_SSTORE_SET: u64 = 22_100; + +/// How many bytes of return data the forging inspector conjures. +/// +/// Non-zero and a whole number of words, so that the `SSTORE` that stores it turns a zero slot +/// into a non-zero one — which is a different charge as well as a different value. +const CONJURED_RETURN_DATA: u64 = 96; + +/// Advances the program counter past the frame's `SSTORE`, so the EVM never executes it. +/// +/// revm's inspected loop runs this callback *before* the instruction, and the interpreter reads +/// the opcode it is about to execute from the very pointer this moves. Stepping the pointer on by +/// one byte therefore deletes one instruction from the frame: the two operands the `SSTORE` would +/// have consumed stay on the stack, the `STOP` after it runs instead, and the frame ends where it +/// was going to end. +/// +/// Nothing about this reaches a gas counter. The work is not performed, so there is nothing for +/// the EVM to meter and nothing for a gas lane to see. +#[derive(Default)] +struct SkipTheStore { + fired: u32, +} + +impl Inspector for SkipTheStore { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != SSTORE { + return; + } + interp.bytecode.relative_jump(1); + self.fired += 1; + } +} + +/// ★ A frame with an opcode skipped out from under it is not an all-zero ledger. +/// +/// The rewrite is the free-expansion shape's twin and is strictly worse: it does not merely make +/// the frame's next charge cheaper, it deletes an instruction from the frame. The transaction ends +/// with different storage *and* a smaller bill, and every gas lane reads zero because the gas that +/// went missing was never spent by anybody. +#[test] +fn test_a_skipped_opcode_is_booked() { + let code = BytecodeBuilder::default() + .sstore(U256::from(RESULT_SLOT), U256::from(STORED)) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = SkipTheStore::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(STORED), + "without the rewrite the frame stores what its bytecode says", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the store never happens", + ); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + COLD_SSTORE_SET, + "the deleted instruction is the charge the transaction then did not pay", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction an inspector deleted an instruction from must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Puts return data in front of a frame that has made no call. +/// +/// `RETURNDATASIZE` reads the buffer's length, so the frame goes on to store a number no call +/// produced. The buffer is the interpreter's own, reachable through `ReturnData` on any live +/// interpreter, and its length is a constant-time reading exactly like the memory's size. +#[derive(Default)] +struct ForgeReturnData { + fired: u32, +} + +impl Inspector for ForgeReturnData { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != RETURNDATASIZE { + return; + } + interp.return_data.set_buffer(Bytes::from(vec![0u8; CONJURED_RETURN_DATA as usize])); + self.fired += 1; + } +} + +/// ★ A frame handed return data it never received is not an all-zero ledger. +/// +/// The frame made no call, so the EVM's own buffer is empty and the store is a zero-to-zero +/// no-op. With the rewrite the same store turns a zero slot into a non-zero one, which changes the +/// post-state and costs the transaction more — in the opposite direction to every other shape +/// here, and just as invisible to a lane that only watches gas counters. +#[test] +fn test_a_forged_return_buffer_is_booked() { + let code = BytecodeBuilder::default() + .append(RETURNDATASIZE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = ForgeReturnData::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "a frame that made no call has no return data", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(CONJURED_RETURN_DATA), + "with the rewrite it reads the length of a buffer no call produced", + ); + assert!( + cheated.total_gas_spent > plain.total_gas_spent, + "and pays for the non-zero store the rewrite turned it into: {} vs {}", + cheated.total_gas_spent, + plain.total_gas_spent, + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a forged buffer changed must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 3203a069..4b46235a 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -27,9 +27,11 @@ //! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. -//! - `ledger_blind_spots` — the four rewrite shapes an all-zero ledger used to admit: a frame's +//! - `ledger_blind_spots` — the six rewrite shapes an all-zero ledger used to admit: a frame's //! memory grown for free, a call or create outcome's metadata rewritten around the result inside -//! it, and two edits to one signed lane that cancel — within a frame and across two of them. +//! it, two edits to one signed lane that cancel — within a frame and across two of them — an +//! instruction deleted from a frame by stepping its program counter past it, and a return buffer +//! conjured in front of a frame that made no call. //! - `measured_inspector` — the shim every inspector is wrapped in: gas an inspector writes into an //! interpreter counter or a frame's gas limit is measured at the callback boundary, booked, and //! kept out of enforcement, with the clamp re-derived on the spot; reviving a failed creation is From a293183dd0e3c64aaadc6d60c72ef8d745b126a2 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 11:14:47 +0800 Subject: [PATCH 153/208] feat(evm): take every constant-time reading of the interpreter, not a chosen four MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The working-set snapshot the four live-interpreter callbacks are compared across was an enumeration — stack length, memory size, and the two halves of the memo of how far that memory had been paid for. An enumeration is only as complete as whoever wrote it, and this one left out `Interpreter::bytecode` entirely, so an inspector could step the program counter past an instruction and delete it from the frame with every lane reading zero. State the rule over the cost of the reading instead: if it is O(1) off a field of `Interpreter`, it is in the snapshot. Field by field that adds the program counter, the code's identity and revm's `continue_execution` flag; the return buffer's identity; the memory's window offset; the frame's four identifying fields and its calldata's identity; and the static flag and spec id. `extend` is the one field with no reading, because its associated type carries no trait bound. Contents stay out — telling whether they came back changed needs unbounded work, which a per-opcode boundary cannot do. Two unit tests hold the rule from both sides: each reading has a rewrite that moves it and nothing else, and the set of readings is destructured exhaustively, so one added to the snapshot is a compile error until it is named and a test failure until it is exercised. Turns the two anchors from the previous commit green. --- crates/mega-evm/src/evm/inspector.rs | 363 ++++++++++++++++-- crates/mega-evm/src/limit/inspector_ledger.rs | 28 +- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 8 +- 3 files changed, 363 insertions(+), 36 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 8fb87a6c..00837142 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -44,8 +44,9 @@ //! funded. //! - Rewrites that move no gas go to [`book_intervention`]: a frame result's classification or //! output, a frame's inputs outside their gas limit, a finished outcome's metadata -//! ([`OutcomeMetadata`]) — and the constant-time readings the shim can take off a live -//! interpreter ([`WorkingSet`]), which is what makes a frame's memory grown for free visible. +//! ([`OutcomeMetadata`]) — and every constant-time reading the shim can take off a live +//! interpreter ([`WorkingSet`]), which is what makes a frame's memory grown for free, its program +//! counter stepped past an instruction, or its return buffer conjured all visible. //! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on @@ -62,10 +63,14 @@ use revm::{ context::{ContextError, ContextTr}, handler::FrameResult, interpreter::{ - interpreter_types::{LoopControl, MemoryTr, StackTr}, - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, + interpreter_types::{ + InputsTr, Jumps, LegacyBytecode, LoopControl, MemoryTr, ReturnData, RuntimeFlag, + StackTr, + }, + CallInput, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, + InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, + primitives::hardfork::SpecId, Inspector, }; @@ -434,51 +439,163 @@ fn book_intervention( } } -/// The part of a live interpreter's state a callback boundary can read in constant time. +/// An `O(1)` identity for a byte buffer: where it starts and how long it is. /// -/// The interpreter's stack and memory *contents* are outside the shim's reach — telling whether -/// either came back changed needs a snapshot of unbounded state — but their sizes are not, and -/// neither is the memo of how far the memory has been paid for. Those four readings are `O(1)`, -/// so the shim takes them on the way in and on the way out and books a difference as an -/// intervention. +/// The same comparison [`same_buffer`] makes, in a form that can be stored in a snapshot. Neither +/// reads a byte: a buffer's *contents* at an unchanged address and length are content-class, which +/// is the row of the shape table that has no lane. What this does catch is the buffer being +/// replaced, which is the only way an inspector can change one that is immutable. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct BufferId { + /// Where the buffer starts, as a bare address rather than a live pointer. + addr: usize, + /// How long it is. + len: usize, +} + +impl BufferId { + #[inline] + fn of(bytes: &[u8]) -> Self { + Self { addr: bytes.as_ptr() as usize, len: bytes.len() } + } +} + +/// An `O(1)` identity for a frame's calldata, which is either an owned buffer or a window into the +/// shared one. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CallInputId { + /// An owned buffer, identified the way every other buffer here is. + Bytes(BufferId), + /// A window into the context's shared memory buffer, identified by its bounds. Where the + /// window points is the reading; what is inside it lives in the shared buffer and is + /// content-class like any other buffer's contents. + SharedBuffer(usize, usize), +} + +impl CallInputId { + #[inline] + fn of(input: &CallInput) -> Self { + match input { + CallInput::Bytes(bytes) => Self::Bytes(BufferId::of(bytes)), + CallInput::SharedBuffer(range) => Self::SharedBuffer(range.start, range.end), + } + } +} + +/// Every constant-time reading a callback boundary can take off a live interpreter. +/// +/// # The rule +/// +/// **Every `O(1)` reading of the interpreter's working set is in this snapshot.** Not a list of +/// the readings someone thought of — the readings themselves, enumerated field by field against +/// revm's `Interpreter` and the traits each field is reachable through, and pinned that way by +/// `tests/rex7/gas_surface.rs`. +/// +/// The rule is stated over readings rather than over fields because that is the shape of what a +/// boundary can do. An inspector reaches the whole interpreter; the shim can only compare what it +/// can *read back* in constant time, and a snapshot the inspected path takes twice per opcode +/// cannot walk unbounded state. So the line is drawn at the cost of the reading, and everything on +/// the cheap side of it is taken. /// -/// The pair that made them necessary is the memory and its memo, moved together. +/// The earlier version of this snapshot held four readings and was written as an enumeration: the +/// stack's length, the memory's size, and the two halves of the memo of how far that memory has +/// been paid for. Enumerations of this kind are only as complete as whoever wrote them, and this +/// one was not — the `bytecode` field was not in it at all, so an inspector could step the program +/// counter past an instruction and delete it from the frame with every lane reading zero. Stating +/// the rule over the cost of the reading is what closes that class rather than that instance. /// -/// The memo (`Gas::memory`) is what the next expanding opcode compares its requirement against. An -/// inspector that raises it without growing the memory desynchronises the two and the EVM reads out -/// of bounds; one that grows the memory without raising it is charged for the growth twice over. -/// Moving *both*, together, is neither — the interpreter is in a state it could have reached by -/// paying, having paid nothing, and every later expansion inside the new bound is free. That pair -/// moves no gas anywhere at the moment it is made, so no gas lane can see it; what it changes is -/// what the EVM charges afterwards. +/// # What is here, by the field it is read from /// -/// The stack's length is here for the same reason and at the same cost, and it moves whenever a -/// callback pushes or pops. +/// - `bytecode` — the program counter, the code's identity, and revm's `continue_execution` flag, +/// which is what the inspected loop breaks on and is a separate object from the pending action. +/// - `stack` — its length. +/// - `return_data` — the buffer's identity. A frame's `RETURNDATASIZE` and `RETURNDATACOPY` read +/// it, so putting a buffer there hands the frame data no call produced. +/// - `memory` — its size, and the offset of the frame's window into the shared buffer. +/// - `gas` — the memory memo's two halves. The budget half of a `Gas` is not here: it moves on the +/// gas lanes, and reading it here as well would report one edit twice. +/// - `input` — the four addresses and values a frame's identity is made of, and its calldata's +/// identity. `target_address` is the one every storage instruction resolves against, so moving it +/// redirects the frame's writes to another account. +/// - `runtime_flag` — the static flag and the spec id. /// -/// A stack or memory edit that leaves both sizes where they were stays invisible: it is a rewrite -/// of contents, which is the row of the shape table that has no lane. +/// `extend` is the one field with no reading, by construction: `InterpreterTypes::Extend` carries +/// no trait bound at all, so a shim generic over the interpreter has nothing it can call on it. +/// +/// # What is deliberately not here +/// +/// The *contents* of the stack, the memory, the return buffer, the calldata and the code. Telling +/// whether any of those came back changed means walking unbounded state, which is the one thing a +/// per-opcode boundary cannot do. Their identities and sizes are here; what is inside them is the +/// row of the shape table that has no lane. +/// +/// # The pair that made the snapshot necessary +/// +/// The memory and its memo, moved together. The memo (`Gas::memory`) is what the next expanding +/// opcode compares its requirement against. An inspector that raises it without growing the memory +/// desynchronises the two and the EVM reads out of bounds; one that grows the memory without +/// raising it is charged for the growth twice over. Moving *both* is neither — the interpreter is +/// in a state it could have reached by paying, having paid nothing, and every later expansion +/// inside the new bound is free. That pair moves no gas at the moment it is made, so no gas lane +/// can see it; what it changes is what the EVM charges afterwards. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct WorkingSet { + /// Where in its code the frame is about to execute. + pc: usize, + /// Which code that is. + code: BufferId, + /// Whether revm's inspected instruction loop will take another turn. + running: bool, /// How many words the frame has on its stack. stack_len: usize, + /// The return data the frame's `RETURNDATASIZE` and `RETURNDATACOPY` will read. + return_data: BufferId, /// How many bytes of memory the frame has. memory_size: usize, + /// Where the frame's window into the shared memory buffer starts. + memory_offset: usize, /// How many words of that memory the frame has been charged for. memory_words: usize, /// What that charge came to. memory_expansion_cost: u64, + /// The account the frame's storage instructions resolve against. + target_address: Address, + /// The account whose code the frame is running. + bytecode_address: Option
, + /// Who called it. + caller_address: Address, + /// With what value. + call_value: U256, + /// And with what calldata. + call_input: CallInputId, + /// Whether the frame may write state. + is_static: bool, + /// Which gas schedule and opcode set it runs under. + spec_id: SpecId, } impl WorkingSet { - /// Reads the four numbers off a live interpreter. + /// Takes every reading off a live interpreter. #[inline] fn of(interp: &Interpreter) -> Self { let memory = interp.gas.memory(); Self { + pc: interp.bytecode.pc(), + code: BufferId::of(interp.bytecode.bytecode_slice()), + running: interp.bytecode.is_not_end(), stack_len: interp.stack.len(), + return_data: BufferId::of(interp.return_data.buffer()), memory_size: interp.memory.size(), + memory_offset: interp.memory.local_memory_offset(), memory_words: memory.words_num, memory_expansion_cost: memory.expansion_cost, + target_address: interp.input.target_address(), + bytecode_address: interp.input.bytecode_address().copied(), + caller_address: interp.input.caller_address(), + call_value: interp.input.call_value(), + call_input: CallInputId::of(interp.input.input()), + is_static: interp.runtime_flag.is_static(), + spec_id: interp.runtime_flag.spec_id(), } } } @@ -885,3 +1002,201 @@ where self.inner.selfdestruct(contract, target, value); } } + +#[cfg(test)] +mod tests { + use super::*; + use revm::{ + bytecode::Bytecode, + interpreter::{ + interpreter::{EthInterpreter, ExtBytecode}, + InputsImpl, InterpreterAction, SharedMemory, + }, + }; + + /// A second address, for the cases that move one of the frame's identifying addresses. + const OTHER: Address = Address::repeat_byte(0x0B); + + /// How many bytes of memory the probe starts with. + /// + /// Non-zero so that the window-offset case can move the frame's checkpoint into the shared + /// buffer without the size moving with it. + const PROBE_MEMORY: usize = 32; + + /// The interpreter every case moves a reading on. + fn probe() -> Interpreter { + let mut interp = Interpreter::new( + SharedMemory::new(), + ExtBytecode::new(Bytecode::new_raw(Bytes::from_static(&[0x5B, 0x5B, 0x00]))), + InputsImpl::default(), + false, + SpecId::default(), + 1_000_000, + ); + interp.memory.resize(PROBE_MEMORY); + interp + } + + /// The readings that differ between two snapshots, by name. + /// + /// Destructured exhaustively rather than compared with `PartialEq`, and that is the whole + /// point: a reading added to [`WorkingSet`] is a compile error here until it is named, and one + /// removed is a compile error too. Rust cannot enumerate a struct's fields at run time, and + /// this is the substitute — the same role the derived `Debug` rendering plays for the foreign + /// structs in `tests/rex7/gas_surface.rs`. + fn moved(before: &WorkingSet, after: &WorkingSet) -> Vec<&'static str> { + let WorkingSet { + pc, + code, + running, + stack_len, + return_data, + memory_size, + memory_offset, + memory_words, + memory_expansion_cost, + target_address, + bytecode_address, + caller_address, + call_value, + call_input, + is_static, + spec_id, + } = *before; + [ + ("pc", pc == after.pc), + ("code", code == after.code), + ("running", running == after.running), + ("stack_len", stack_len == after.stack_len), + ("return_data", return_data == after.return_data), + ("memory_size", memory_size == after.memory_size), + ("memory_offset", memory_offset == after.memory_offset), + ("memory_words", memory_words == after.memory_words), + ("memory_expansion_cost", memory_expansion_cost == after.memory_expansion_cost), + ("target_address", target_address == after.target_address), + ("bytecode_address", bytecode_address == after.bytecode_address), + ("caller_address", caller_address == after.caller_address), + ("call_value", call_value == after.call_value), + ("call_input", call_input == after.call_input), + ("is_static", is_static == after.is_static), + ("spec_id", spec_id == after.spec_id), + ] + .into_iter() + .filter_map(|(name, same)| (!same).then_some(name)) + .collect() + } + + /// One case: the name of a reading, and a rewrite that moves it. + type Case = (&'static str, fn(&mut Interpreter)); + + /// One rewrite per reading, each moving the reading it is named for and nothing else. + /// + /// Every one is something an inspector can do to a live interpreter through the traits the + /// shim itself reads through, and several are rewrites with teeth: stepping the program + /// counter deletes an instruction from the frame, clearing the static flag lets a + /// `STATICCALL` write state, and moving the target address redirects every storage + /// instruction to another account. + const CASES: [Case; 16] = [ + ("pc", |interp| interp.bytecode.relative_jump(1)), + ("code", |interp| { + interp.bytecode = ExtBytecode::new(Bytecode::new_raw(Bytes::from_static(&[0x00]))); + }), + ("running", |interp| { + let gas = interp.gas; + interp.bytecode.set_action(InterpreterAction::new_halt(InstructionResult::Stop, gas)); + }), + ("stack_len", |interp| assert!(interp.stack.push(U256::ZERO))), + ("return_data", |interp| interp.return_data.set_buffer(Bytes::from_static(&[0x01]))), + ("memory_size", |interp| interp.memory.resize(PROBE_MEMORY * 2)), + ("memory_offset", |interp| { + // A child window over the same shared buffer, resized to the size the parent had, so + // that the frame's memory looks the same and starts somewhere else. + let mut child = interp.memory.new_child_context(); + child.resize(PROBE_MEMORY); + interp.memory = child; + }), + ("memory_words", |interp| interp.gas.memory_mut().words_num += 1), + ("memory_expansion_cost", |interp| interp.gas.memory_mut().expansion_cost += 1), + ("target_address", |interp| interp.input.target_address = OTHER), + ("bytecode_address", |interp| interp.input.bytecode_address = Some(OTHER)), + ("caller_address", |interp| interp.input.caller_address = OTHER), + ("call_value", |interp| interp.input.call_value = U256::from(1)), + ("call_input", |interp| { + interp.input.input = CallInput::Bytes(Bytes::from_static(&[0x01])); + }), + ("is_static", |interp| interp.runtime_flag.is_static = true), + ("spec_id", |interp| interp.runtime_flag.spec_id = SpecId::FRONTIER), + ]; + + /// ★ Every reading the snapshot holds is one the shim really takes off the interpreter. + /// + /// The rule the snapshot is built on is "every `O(1)` reading of the interpreter's working + /// set", and a rule of that shape fails in two ways: a reading that is declared and never + /// read, and a reading that is read and never declared. Each case moves exactly one reading + /// and asserts that exactly that one name comes back — so a field dropped from + /// [`WorkingSet::of`] leaves its case detecting nothing, and a field left out of the snapshot + /// entirely never compiles past [`moved`]. + #[test] + fn test_every_reading_moves_exactly_the_reading_it_is_named_for() { + let unchanged = probe(); + assert!( + moved(&WorkingSet::of(&unchanged), &WorkingSet::of(&unchanged)).is_empty(), + "a snapshot compared against itself must report nothing moved", + ); + assert_ne!(SpecId::default(), SpecId::FRONTIER, "the spec-id case must move something"); + + for (name, rewrite) in CASES { + let mut interp = probe(); + let before = WorkingSet::of(&interp); + rewrite(&mut interp); + assert_eq!( + moved(&before, &WorkingSet::of(&interp)), + [name], + "the rewrite for {name} must move that reading and no other", + ); + } + } + + /// The case list covers the snapshot, and covers each reading once. + /// + /// [`moved`]'s destructuring is what keeps [`WorkingSet`] and this module in step at compile + /// time; this is the run-time half of the same closure. The snapshot below is written out + /// field by field with every reading different, so what [`moved`] reports on it is the set of + /// readings [`moved`] can see at all — and that set has to be exactly the set the cases above + /// exercise. A reading added to the snapshot is a compile error in two places before it gets + /// here, and an unexercised one fails this. + #[test] + fn test_the_case_list_covers_every_reading_exactly_once() { + let mut names: Vec<&str> = CASES.iter().map(|(name, _)| *name).collect(); + let declared = names.len(); + names.sort_unstable(); + names.dedup(); + assert_eq!(names.len(), declared, "no reading may be listed twice"); + + let before = WorkingSet::of(&probe()); + let everything = WorkingSet { + pc: before.pc + 1, + code: BufferId { addr: before.code.addr + 1, len: before.code.len + 1 }, + running: !before.running, + stack_len: before.stack_len + 1, + return_data: BufferId { + addr: before.return_data.addr + 1, + len: before.return_data.len + 1, + }, + memory_size: before.memory_size + 1, + memory_offset: before.memory_offset + 1, + memory_words: before.memory_words + 1, + memory_expansion_cost: before.memory_expansion_cost + 1, + target_address: OTHER, + bytecode_address: Some(OTHER), + caller_address: OTHER, + call_value: before.call_value + U256::from(1), + call_input: CallInputId::SharedBuffer(0, 1), + is_static: !before.is_static, + spec_id: SpecId::FRONTIER, + }; + let mut all = moved(&before, &everything); + all.sort_unstable(); + assert_eq!(all, names, "every reading the snapshot holds must have a case, and vice versa"); + } +} diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 9a0365d9..37202d3b 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -108,12 +108,14 @@ impl Lane { /// # What it does not measure /// /// What a callback does behind the shim's back. An inspector reaches state that no argument it is -/// handed describes — the interpreter's stack and memory contents, the journal — and telling -/// whether any of those came back changed needs a snapshot of unbounded state that no callback -/// boundary can take at a cost the inspected path can carry. The *sizes* of the interpreter's -/// stack and memory are the exception, because they are constant-time readings: the shim takes -/// them, and a frame whose memory was grown lands on [`interventions`](Self::interventions). -/// A same-size rewrite of what is in them leaves this all-zero. +/// handed describes — the *contents* of the interpreter's stack, memory, return buffer, calldata +/// and code, and the journal — and telling whether any of those came back changed needs a snapshot +/// of unbounded state that no callback boundary can take at a cost the inspected path can carry. +/// Everything about the interpreter that *is* a constant-time reading is the exception, and the +/// shim takes all of it: a frame whose memory was grown, whose program counter was stepped past an +/// instruction, or whose return buffer was conjured lands on +/// [`interventions`](Self::interventions). A rewrite that leaves every one of those readings where +/// it was leaves this all-zero. /// /// So an empty ledger says two things: no gas moved that the EVM did not move, and nothing the /// shim was handed came back different. It does not say the transaction is the one the EVM would @@ -313,10 +315,16 @@ pub struct InspectorLedger { /// that can edit them (`frame_start`, `call`, `create`); /// - a frame the inspector answered itself, with a synthetic outcome instead of letting the /// EVM build it; - /// - the size of a live interpreter's stack or memory, or the memo of how far that memory has - /// been paid for, at each of the four callbacks handed a live interpreter. Moving the memory - /// and the memo together is the one edit that leaves every interpreter invariant intact and - /// still changes what the next expanding opcode is charged. + /// - any constant-time reading of a live interpreter's working set, at each of the four + /// callbacks handed one. The rule the shim's snapshot is built on is stated over the cost of + /// the reading rather than over a list, so it covers the program counter and the code's + /// identity, revm's `continue_execution` flag, the stack's length, the return buffer's + /// identity, the memory's size and window offset, the memo of how far that memory has been + /// paid for, the frame's four identifying fields and its calldata's identity, the static + /// flag and the spec id. Two of those are rewrites with no other trace at all: a stepped + /// program counter deletes an instruction from the frame, and moving the memory together + /// with its memo leaves every interpreter invariant intact while changing what the next + /// expanding opcode is charged. /// /// Gas edits are deliberately excluded — a gas limit or a result's remaining gas moving is /// what the lanes above are for, and counting it here would say the same thing twice. diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 92342e0f..36f91a3b 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -25,8 +25,12 @@ //! Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use //! to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test //! here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no -//! longer changes anything is a shape that stopped testing the guard. - +//! longer changes anything is a shape that stopped testing the guard.//! +//! The last two are also why the snapshot the first shape needed is now a *rule* rather than a +//! list. A snapshot of four chosen readings caught the memory pair and let the program counter +//! through, because `Interpreter::bytecode` was not among the things anyone had thought to name. +//! What the shim takes now is every constant-time reading of the interpreter, and what pins that is +//! the `Interpreter` row of `gas_surface.rs`'s closed table. use crate::common::{transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET, ONE_ETH}; use alloy_primitives::{address, Address, Bytes, U256}; From 90ce614db2ddcac368d1e5daa770fcc9ade2de1f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 11:15:14 +0800 Subject: [PATCH 154/208] test(rex7): pin the interpreter's own field set against upstream's Debug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The closed table classified every field of every object reachable *through* a callback argument, but not the fields of the live interpreter itself — those were named in prose, and the prose did not say `bytecode`. That is the gap that let a program-counter rewrite go unclassified. Add `Interpreter` as a ninth table, checked the same way as the other eight: a field upstream adds renders, matches no row, and fails by name; a row kept for a field that no longer exists fails too. --- crates/mega-evm/tests/rex7/gas_surface.rs | 95 ++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index 41556968..fbd625a4 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -12,6 +12,12 @@ //! cannot be told apart without looking. `CallOutcome::memory_offset` is the case that settled it: //! not gas, not bookkeeping, and for a while not in the table at all. //! +//! The live `Interpreter` is in the table for the same reason and at some cost to the module's +//! name: it is the one argument whose own fields, rather than a field of a field, are what an +//! inspector reaches. Leaving it out is what let `Interpreter::bytecode` go unclassified while the +//! interpreter's other fields were named in prose, and an inspector could step the program counter +//! past an instruction with every lane reading zero. +//! //! # The two levels the enumeration has //! //! - **Shapes.** Which objects the EVM hands a callback that carry gas at all. Every one of them @@ -255,6 +261,85 @@ const CREATE_OUTCOME_FIELDS: [(&str, Coverage); 2] = [ ), ]; +/// Every field of the live interpreter a callback is handed. +/// +/// The row this table exists for is `bytecode`. Before it, the interpreter's fields were named in +/// prose — "stack, memory, `return_data`, input, `runtime_flag`, extend" — and `bytecode` was +/// simply not in the sentence, so an inspector could step the program counter past an instruction +/// and delete it from the frame with every lane and every counter reading zero. A prose list is +/// only as complete as whoever wrote it; this one is checked against what upstream's `Debug` +/// renders, like every other table here. +/// +/// The verdicts are stated over *readings*, because that is what a boundary can compare. Every +/// constant-time reading of every field below is in `inspector.rs::WorkingSet` and books an +/// intervention when it moves; what is left over in each row is content-class, which is the one +/// row of the shape table with no lane. +const INTERPRETER_FIELDS: [(&str, Coverage); 8] = [ + ( + "bytecode", + Coverage::NotGas( + "the code and the position in it. Three constant-time readings — the program counter, \ + the code buffer's identity, and revm's `continue_execution` flag, which is what the \ + inspected loop breaks on and is a separate object from the pending action. Moving the \ + counter deletes an instruction from the frame, which costs the transaction the work \ + that instruction would have done; nothing meters that, because it never happens. \ + Booked as interventions, off `WorkingSet`", + ), + ), + ( + "gas", + Coverage::NotGas("a container; its own fields are classified separately"), + ), + ( + "stack", + Coverage::NotGas( + "its length is a constant-time reading and is booked as an intervention; the words in \ + it are content-class", + ), + ), + ( + "return_data", + Coverage::NotGas( + "the buffer a frame's `RETURNDATASIZE` and `RETURNDATACOPY` read. Its identity is a \ + constant-time reading and is booked as an intervention, so a frame handed data no \ + call produced is visible; the bytes inside it are content-class", + ), + ), + ( + "memory", + Coverage::NotGas( + "its size and the offset of the frame's window into the shared buffer are both \ + constant-time readings and are booked as interventions — the size because moving it \ + together with the memo in `gas` skips the next expanding opcode's charge; the bytes \ + are content-class", + ), + ), + ( + "input", + Coverage::NotGas( + "what the frame is: the account its storage instructions resolve against, the code \ + address, the caller, the value, and the calldata's identity. All constant-time, all \ + booked as interventions; the calldata's bytes are content-class", + ), + ), + ( + "runtime_flag", + Coverage::NotGas( + "the static flag and the spec id, both constant-time and both booked as \ + interventions — clearing the static flag mid-frame would let a `STATICCALL` write \ + state", + ), + ), + ( + "extend", + Coverage::NotGas( + "the one field with no reading, by construction: `InterpreterTypes::Extend` carries no \ + trait bound, so a shim generic over the interpreter has nothing it can call on it. \ + `MegaETH` configures it as `()`, which holds nothing to rewrite", + ), + ), +]; + /// Everything a finished frame hands back. const INTERPRETER_RESULT_FIELDS: [(&str, Coverage); 3] = [ ("gas", Coverage::NotGas("a container; its own fields are classified separately")), @@ -369,6 +454,12 @@ fn sample_create_outcome() -> CreateOutcome { CreateOutcome::new(sample_result(), None) } +/// A live interpreter, for the one table whose object is not a callback argument's field but the +/// argument itself. +fn sample_interpreter() -> Interpreter { + Interpreter::default() +} + // --- the field-level pin ------------------------------------------------------------------------- /// Every field of every gas-carrying object an inspector is handed has a verdict. @@ -389,6 +480,7 @@ fn test_every_field_of_every_gas_carrier_has_a_verdict() { ("InterpreterResult", std::format!("{:?}", sample_result())), ("CallOutcome", std::format!("{:?}", sample_call_outcome())), ("CreateOutcome", std::format!("{:?}", sample_create_outcome())), + ("Interpreter", std::format!("{:?}", sample_interpreter())), ]; // Looked up rather than listed, so the set of tables the lock walks and the set this test // checks the renderings against cannot drift apart. @@ -425,7 +517,7 @@ fn test_the_field_reader_reads_the_top_level_and_stops_there() { } /// Every table this module classifies, by the name its rendering is checked under. -fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 8] { +fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 9] { [ ("Gas", GAS_FIELDS.as_slice()), ("GasTracker", GAS_TRACKER_FIELDS.as_slice()), @@ -435,6 +527,7 @@ fn tables() -> [(&'static str, &'static [(&'static str, Coverage)]); 8] { ("InterpreterResult", INTERPRETER_RESULT_FIELDS.as_slice()), ("CallOutcome", CALL_OUTCOME_FIELDS.as_slice()), ("CreateOutcome", CREATE_OUTCOME_FIELDS.as_slice()), + ("Interpreter", INTERPRETER_FIELDS.as_slice()), ] } From 7d67456dd2dc2ee0e7592015274e3a255421e760 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 11:15:14 +0800 Subject: [PATCH 155/208] feat(state-test): put the skipped opcode and the forged return buffer in the chaos pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are interpreter-facing and both are unconditionally booked, so they join the partition the ledger gate is stated over. A program-counter edit is the one shape that can move a frame off an instruction boundary, so it is guarded three ways: only while the frame is still running, never on a PUSH (whose immediate bytes would then decode as opcodes), and never on a terminator (which would carry execution past where the frame was going to stop). Under those conditions the frame runs its own bytecode with one instruction removed — well-formed, reproducible, and still bounded by gas. A forged return buffer always gets a length the current one does not have, which is what makes it visible whatever the allocator does with the old buffer. The narrowing test's seed moves with the pool's size, as its own comment says; 3 no longer draws both shapes it needs, 2 does. The interpreter-shape list's "last two" comment named positions and had been stale since the list grew from six, so it now names the shapes instead. --- crates/mega-state-test/src/chaos.rs | 100 +++++++++++++++++++-- crates/mega-state-test/tests/chaos_mode.rs | 9 +- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 73d97185..6853f6cc 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -58,7 +58,7 @@ use mega_evm::revm::{ handler::FrameResult, inspector::Inspector, interpreter::{ - interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, + interpreter_types::{Jumps, LoopControl, MemoryTr, ReturnData, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, @@ -218,11 +218,17 @@ pub enum ChaosShape { /// lane's net is zero and — whenever the two frames end differently — the receipt's is /// not. CancelRefundEdit, + /// The program counter stepped past the instruction the frame was about to execute, deleting + /// it from the frame. The work is never performed, so no counter falls and no lane moves. + SkipOpcode, + /// A return buffer put in front of the frame, so its `RETURNDATASIZE` and `RETURNDATACOPY` + /// read data no call produced. + RewriteReturnData, } impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 25] = [ + pub const ALL: [Self; 27] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -248,6 +254,8 @@ impl ChaosShape { Self::MoveOutcomeMetadata, Self::CancelGasEdit, Self::CancelRefundEdit, + Self::SkipOpcode, + Self::RewriteReturnData, ]; /// The shape a label names. @@ -292,6 +300,8 @@ impl ChaosShape { Self::MoveOutcomeMetadata => "move_outcome_metadata", Self::CancelGasEdit => "cancel_gas_edit", Self::CancelRefundEdit => "cancel_refund_edit", + Self::SkipOpcode => "skip_opcode", + Self::RewriteReturnData => "rewrite_return_data", } } @@ -328,17 +338,26 @@ impl ChaosShape { // rather than leaving these three shapes out of the gate. Self::RaiseRefund | Self::LowerRefund | - Self::CancelRefundEdit + Self::CancelRefundEdit | + // Both move a constant-time reading the shim takes off every live interpreter, + // and both are drawn only in the window where they move it: a skip is withheld + // unless the frame is still running, and a rewritten return buffer always gets a + // length the current one does not have. + Self::SkipOpcode | + Self::RewriteReturnData ) } } /// Shapes reachable from a callback that holds a live interpreter. /// -/// The last two only land at the one callback that runs with an action already pending — -/// `step_end`, which revm's inspected loop runs after the instruction that set it. A draw for them -/// anywhere else leaves the interpreter alone and spends no budget. -const INTERPRETER_SHAPES: [ChaosShape; 12] = [ +/// `RaiseActionGas` and `LowerActionGas` only land at the one callback that runs with an action +/// already pending — `step_end`, which revm's inspected loop runs after the instruction that set +/// it. A draw for them anywhere else leaves the interpreter alone and spends no budget. +/// `SkipOpcode` is withheld in the opposite window, at a callback whose frame is already +/// terminating, and at an instruction whose deletion would move the frame off an instruction +/// boundary. +const INTERPRETER_SHAPES: [ChaosShape; 14] = [ ChaosShape::InjectGas, ChaosShape::DrainGas, ChaosShape::EditFrameState, @@ -351,6 +370,8 @@ const INTERPRETER_SHAPES: [ChaosShape; 12] = [ ChaosShape::WriteStateGas, ChaosShape::GrowMemoryFree, ChaosShape::CancelGasEdit, + ChaosShape::SkipOpcode, + ChaosShape::RewriteReturnData, ]; /// Shapes reachable from a callback that holds a frame's inputs, before the frame is built. @@ -673,6 +694,12 @@ impl ChaosInspector { return; } } + ChaosShape::SkipOpcode => { + if !skip_opcode(interp) { + return; + } + } + ChaosShape::RewriteReturnData => rewrite_return_data(interp, entropy), ChaosShape::CancelGasEdit => { // The give-back is taken at the next live-interpreter callback, by // `settle_pending_gas`. A second draw while one is outstanding leaves the @@ -877,6 +904,65 @@ fn grow_memory_free( true } +/// Steps the program counter past the instruction the frame was about to execute, returning +/// whether anything moved. +/// +/// This is the shape with the sharpest teeth in the pool — it does not make an instruction cheaper, +/// it deletes one — and it is also the only one that can move the frame off an instruction +/// boundary, so its guard is what keeps the sweep's executions well-formed rather than merely +/// different. +/// +/// Three conditions, each load-bearing: +/// +/// - **The frame is still running.** With a terminating action already pending, revm's inspected +/// loop breaks without reading the counter again, so a skip there changes nothing about the +/// execution while still costing budget. +/// - **The instruction is not a `PUSH`.** A `PUSH` is the one instruction whose bytes are not all +/// opcodes, so skipping its opcode byte leaves its immediate data to be executed as code and the +/// whole rest of the frame decodes at the wrong offsets. Skipping any other single-byte +/// instruction lands exactly on the next instruction boundary: the frame executes its own +/// bytecode with one instruction removed, which is a well-formed program and a reproducible one. +/// - **The instruction does not end the frame.** Skipping a `STOP` or a `RETURN` would carry +/// execution past the point the frame was going to stop, into whatever follows it. That is +/// bounded — the analysed bytecode is padded with `STOP`, and gas bounds it in any case — but it +/// makes a frame's cost a function of what happens to sit after its terminator, which is the +/// opposite of what a corpus sweep wants. +/// +/// Termination is not at risk under those conditions. Deleting an instruction cannot create a +/// backward jump the bytecode did not already contain, every path still ends at a terminator or +/// runs out of gas, and the budget bounds how many instructions one transaction can lose. +fn skip_opcode(interp: &mut Interpreter) -> bool { + /// `PUSH1` through `PUSH32` — every opcode that carries immediate bytes. + const PUSH_RANGE: core::ops::RangeInclusive = 0x60..=0x7F; + /// `STOP`, `RETURN`, `REVERT`, `INVALID`, `SELFDESTRUCT`. + const TERMINATORS: [u8; 5] = [0x00, 0xF3, 0xFD, 0xFE, 0xFF]; + + if !interp.bytecode.is_not_end() { + return false; + } + let opcode = interp.bytecode.opcode(); + if PUSH_RANGE.contains(&opcode) || TERMINATORS.contains(&opcode) { + return false; + } + interp.bytecode.relative_jump(1); + true +} + +/// Puts a return buffer in front of the frame that no call of its own produced. +/// +/// The length always differs from the one the buffer has, which is what makes the shape +/// unconditionally visible: the shim reads the buffer's identity, and a replacement of a different +/// length moves it whatever the allocator does with the old one. It is also what the frame reads — +/// `RETURNDATASIZE` returns exactly this number. +/// +/// Growing rather than shrinking, and by at most a word or so at a time, so that a +/// `RETURNDATACOPY` the fixture makes can only succeed where it would have reverted, and the +/// buffer stays small enough that the copy it pays for is bounded by the mutation budget. +fn rewrite_return_data(interp: &mut Interpreter, entropy: u64) { + let len = interp.return_data.buffer().len() + 1 + (entropy % 32) as usize; + interp.return_data.set_buffer(Bytes::from(vec![(entropy % 256) as u8; len])); +} + /// Writes one of the receipt figures that is not the envelope, returning whether anything moved. /// /// The three are grouped because they are one surface — every `Gas` an inspector is handed carries diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index cf2c1c12..b7e33a05 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -156,7 +156,7 @@ fn test_a_vector_seed_separates_every_part_of_the_identity() { fn test_narrowing_the_filter_keeps_the_surviving_mutations() { // A seed whose full run draws both of the shapes the narrowed one keeps; which seeds those are // is a function of the pool's size, so a shape added to the pool can move it. - const SEED: u64 = 3; + const SEED: u64 = 2; let full = mutations(SEED, ShapeFilter::default()); let only = [ChaosShape::InjectGas, ChaosShape::DrainGas]; let narrowed = mutations(SEED, ShapeFilter::only(&only)); @@ -182,9 +182,10 @@ fn test_narrowing_the_filter_keeps_the_surviving_mutations() { /// /// [`ChaosClass::LedgerBlind`] fires when a run applied one of these and the ledger is still /// all-zero. That verdict is only meaningful if the premise holds — so this checks the premise -/// directly, shape by shape, rather than trusting the partition. Three of the shapes here -/// (`grow_memory_free`, `move_outcome_metadata`, `cancel_refund_edit`) were added because the -/// shim did *not* book them, and this is the test that would have said so. +/// directly, shape by shape, rather than trusting the partition. Five of the shapes here +/// (`grow_memory_free`, `move_outcome_metadata`, `cancel_refund_edit`, `skip_opcode`, +/// `rewrite_return_data`) were added because the shim did *not* book them, and this is the test +/// that would have said so. #[test] fn test_every_always_booked_shape_moves_the_ledger() { let always_booked: Vec = From 2df729a804c6375f7c1be19c2c307e7bea3a5c6d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 11:15:14 +0800 Subject: [PATCH 156/208] docs(evm): state the O(1) reading rule and its upgrade obligation The inspector contract described the working-set snapshot as "the size of the interpreter's stack or memory", and listed the interpreter's other fields as unmeasured. Both are now wrong in the safe direction and were wrong in the unsafe one before: `return_data`, `input` and `runtime_flag` sat in the unmeasured row while carrying readings that change what a frame does. Restate both tables over the rule, add the revm-bump obligation to re-read the accessor traits the snapshot reads through, and correct the contents row to name identities rather than sizes. --- AGENTS.md | 5 +++-- crates/mega-evm/src/evm/AGENTS.md | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d39069ad..1aee3dc6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,11 +131,12 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. - The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome, and the size of a live interpreter's stack or memory together with the memo of how far that memory has been paid for — because a rewrite that costs nothing still produces different state and a different receipt, and because a memory grown together with its memo skips the next expanding opcode's charge while leaving every interpreter invariant intact. + The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome, and every constant-time reading it can take off a live interpreter — because a rewrite that costs nothing still produces different state and a different receipt. + That last group is stated as a rule rather than as a list: every `O(1)` reading of the interpreter's working set enters the boundary snapshot, which is what makes a program counter stepped past an instruction, a memory grown together with its memo, or a return buffer conjured in front of a frame that made no call all visible on the same lane. Every gas lane carries a gross alongside its net, and it is the gross that `is_zero` — the guard's question — reads: two edits to one lane that cancel are two edits, whether they cancel inside one frame or across a surviving frame and a rolled-back one, and a net-only reading calls that pair untouched while the execution saw a number the EVM would never have produced. The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). A frame the inspector answers itself is the one place a difference across the callback is not the measurement, because no frame is built and the whole result is the inspector's: the shim stages the envelope the answering callback was handed, and `inspect_frame_init` settles the gas the result finally carries against it on the result lane — which also covers whatever of an edit to the inputs survives into a guard's replacement result, and which is zero for the echo convention every tool that intercepts follows. - What no callback boundary can see stays invisible (the *contents* of the interpreter's stack and memory at unchanged sizes, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed or could read in constant time come back changed, not that the transaction is the one the EVM would have produced alone. + What no callback boundary can see stays invisible (the *contents* of the interpreter's stack, memory, return buffer, calldata and code at unchanged identities, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed or could read in constant time come back changed, not that the transaction is the one the EVM would have produced alone. The receipt's other two numbers have lanes of their own, measured at two different points because `MegaETH` produces one of the two quantities and none of the other: a refund is booked nominally across the callback boundary, since only a difference there separates an inspector's share from the EVM's own refunds, while the EIP-8037 state-gas dimension (`reservoir` and `state_gas_spent`, on a `Gas` or on a call's inputs) is settled once from the figures the transaction ends with — revm propagates it by replacement rather than accumulation, so a boundary difference would book edits the EVM goes on to erase. The reservoir is a term of the conservation law because it lowers the envelope the receipt reports; the refund and the spend counter are not, and are refused by the block guard rather than accounted for. `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration, pinned by `tests/rex7/gas_surface.rs`, which also fails on any row left saying a surface reaches the receipt and no lane books it. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 84b75f1c..b54a7bdd 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -64,8 +64,8 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | | A failed **call** frame rewritten into a success | Supported | `InspectorLedger::interventions` | the journal commits, so the frame's state follows the result its caller was handed | | A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | -| The *size* of the interpreter's stack or memory, or the memo of how far that memory has been paid for (`Gas::memory`) | Supported | `InspectorLedger::interventions`, at the callback boundary, off the constant-time reading `inspector.rs::WorkingSet` takes | nothing directly — but growing the memory and the memo together leaves the interpreter consistent and skips the next expanding opcode's charge, which is why it needs a booking at all | -| The *contents* of the interpreter's stack or memory, at unchanged sizes | Supported, unmeasured | nothing — telling whether they came back changed needs a snapshot of unbounded state | the EVM executes on the edited state and meters it as its own work, because it is | +| Any constant-time reading of a live interpreter's working set — its program counter, its code's identity, revm's `continue_execution` flag, the stack's length, the return buffer's identity, the memory's size and window offset, the memo of how far that memory has been paid for (`Gas::memory`), the frame's four identifying fields and its calldata's identity, the static flag, the spec id | Supported | `InspectorLedger::interventions`, at the callback boundary, off `inspector.rs::WorkingSet` | nothing directly — but a stepped program counter deletes an instruction from the frame, and growing the memory and the memo together skips the next expanding opcode's charge, which is why these need a booking at all | +| The *contents* of the interpreter's stack, memory, return buffer, calldata or code, at unchanged identities | Supported, unmeasured | nothing — telling whether they came back changed needs a snapshot of unbounded state | the EVM executes on the edited state and meters it as its own work, because it is | | A direct journal write (`tstore`, `log`, …) | Supported, unmetered | nothing — no argument the shim holds describes it | `MegaETH`'s data-size / KV / state-growth lanes do not see it; it moves no gas, so the conservation law is unaffected | | The gas a pending `InterpreterAction` carries, reached through `LoopControl` (`step_end`) | Supported | the lane the action the callback left behind names: `InspectorLedger::result` for a `Return` action, settled at the frame's settlement point because that action *is* the frame's result a moment later; `InspectorLedger::env` for a `NewFrame` one, booked at the child's `frame_start`; `InspectorLedger::gas` when the callback removed the action, because the frame then carries on spending its counter | nothing | | A pending action's classification or output, or an action installed, removed or swapped for the other variant | Supported | `InspectorLedger::interventions`, alongside whatever gas the change moved on the lane above | it changes what the EVM does next, not what the frame has spent | @@ -107,8 +107,9 @@ The first six rows are the lanes measured across a callback boundary; the three | Every `Gas` above → `state_gas_spent` | every callback that holds one | `InspectorLedger::state_gas`, settled at the same point. Not a term of the law: it moves the receipt's state-gas figure, not the envelope. Its *other* effect — a failing frame folds it into its caller's pool — arrives inside the reservoir lane, which is read after the fold. | | Every `Gas` above → `memory` (`MemoryGas`: `words_num`, `expansion_cost`) | every callback that holds one | Not a budget but a memo of how far the frame's memory has been paid for — and the number the next expanding opcode compares its requirement against, so moving it *together with the memory* skips that opcode's charge while leaving every interpreter invariant intact. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | | `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output`; `CallOutcome::memory_offset` / `::was_precompile_called` / `::precompile_call_logs` / `::charged_new_account_state_gas`; `CreateOutcome::address` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | -| The interpreter's `stack` and `memory` **sizes** | the four live-interpreter callbacks | Not gas, and the one part of the interpreter's working state a boundary can read in constant time. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | -| The interpreter's `stack` / `memory` **contents**, `return_data`, `input`, `runtime_flag`, `extend` | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | +| Every constant-time reading of `Interpreter`'s own fields — `bytecode` (program counter, code identity, `continue_execution`), `stack` (length), `return_data` (buffer identity), `memory` (size, window offset), `gas` → `memory` (the memo), `input` (target, code address, caller, value, calldata identity), `runtime_flag` (static flag, spec id) | the four live-interpreter callbacks | Not gas, and the whole of what a boundary can read off a live interpreter in constant time. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | +| `Interpreter::extend` | the four live-interpreter callbacks | Not gas, and not readable: `InterpreterTypes::Extend` carries no trait bound, so a shim generic over the interpreter has nothing it can call on it. `MegaETH` configures it as `()`. | +| The **contents** of the interpreter's `stack`, `memory`, `return_data` buffer, calldata and code, at unchanged identities | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | | `&mut CTX` — the journal, and through `MegaContext`'s `DerefMut` the transaction, the block, the configuration and `MegaETH`'s own trackers | every callback but `selfdestruct` | Not gas the EVM handed over. Unmeasured for the reason the journal is: telling whether any of it came back changed needs a snapshot of unbounded state that no callback boundary can take at a cost the inspected path can carry. The gas schedule is the exception — the schedule pin rejects a rewritten one, at the next transaction rather than within this one. | | Everything passed by value (`Log`; `selfdestruct`'s three arguments) and the inputs the `*_end` callbacks take by shared reference | — | No mutable reach at all. | @@ -123,6 +124,7 @@ A reason that only covers half its own input space is the harder of the two to s **What the closure pin does and does not reach.** A field upstream adds to any of these structs shows up in its `Debug` rendering, matches no row, and fails the test by name. +`Interpreter` itself is one of those structs, which is the row that had been missing: its fields were named in prose, the prose did not say `bytecode`, and an inspector could step the program counter past an instruction with every lane and every counter reading zero. A variant upstream adds to `InterpreterAction`, `FrameInput` or `FrameResult` fails the build, because the module matches all three exhaustively with no catch-all. A *callback* upstream adds to the `Inspector` trait does neither — the trait gives every method a default body, so an unimplemented one silently does nothing — which is why the obligation below is written out. @@ -137,6 +139,12 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Book a result rewrite from the frame's settlement point, not from the callback boundary.** Whether such an edit moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. The gas an intercepting callback puts into a synthetic outcome travels through that same lane. +- **Take every constant-time reading of the interpreter, not a chosen list of them.** + `WorkingSet` is the snapshot the four live-interpreter callbacks are compared across, and the rule it is built on is stated over the *cost* of a reading rather than over a list of interesting ones: if it is `O(1)` off a field of `Interpreter`, it is in the snapshot. + A list is only as complete as whoever wrote it, and the four-reading list that preceded this rule left `bytecode` out entirely. + Two tests hold the rule: `tests/rex7/gas_surface.rs` pins `Interpreter`'s field set against upstream's `Debug`, and `inspector.rs`'s own unit tests move each reading in turn and fail if it moves nothing, or if a reading exists that no case moves. + On a revm bump, re-read the trait methods the snapshot reads through — `Jumps`, `LoopControl`, `LegacyBytecode`, `StackTr`, `ReturnData`, `MemoryTr`, `InputsTr`, `RuntimeFlag` — for a new constant-time accessor, which is a new reading and not a compile error anywhere. + A reading that would need unbounded work is the one thing the rule does not ask for; it belongs in the contents row, which has no lane. - **Book a lane through `Lane::book`, never by writing its net.** The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. - **Keep every rewrite out of a block.** From 4ef26e8f2ba35a323c7a232edddafd3b9898777d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 12:00:31 +0800 Subject: [PATCH 157/208] bench: add inspected-path rows to transact --- crates/mega-evm/benches/common/mod.rs | 30 ++++++++- crates/mega-evm/benches/common/subject.rs | 80 ++++++++++++++++++++++- crates/mega-evm/benches/transact.rs | 15 ++++- 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/crates/mega-evm/benches/common/mod.rs b/crates/mega-evm/benches/common/mod.rs index c51eec9c..1bb5d873 100644 --- a/crates/mega-evm/benches/common/mod.rs +++ b/crates/mega-evm/benches/common/mod.rs @@ -20,7 +20,7 @@ use core::convert::Infallible; use mega_evm::{MegaSpecId, TestExternalEnvs}; pub use subject::MegaWithEnv; -use subject::{Mega, OpRevmPinned, RevmPinned, Subject}; +use subject::{InspectKind, Mega, MegaInspected, OpRevmPinned, RevmPinned, Subject}; pub use workload::{Account, TxSpec, Workload}; /// Mega specs registered by [`register_all`] and [`register_mega`]. Shared so @@ -89,6 +89,34 @@ pub fn register_mega_suffixed(group: &mut Group<'_>, variant: &str, w: &Workload run_subjects(group, variant, w, &mega_subjects(SPEC_IDS)); } +/// Specs the inspected-path rows cover: frozen control (REX6) and the unstable +/// target (REX7). The plain rows for these specs already come from +/// [`register_all`]; [`register_inspected`] only adds the inspect variants. +const INSPECTED_SPEC_IDS: &[(&str, MegaSpecId)] = + &[("rex6", MegaSpecId::REX6), ("rex7", MegaSpecId::REX7)]; + +/// Register inspected-path rows for REX6 and REX7 on the current group. +/// +/// Each spec gets two extra rows: +/// - `/inspect_noop` — inspect loop + measurement shim around a +/// [`NoOpInspector`](revm::inspector::NoOpInspector) +/// - `/inspect_tracer` — the same loop with `revm-inspectors`' geth default +/// (`debug_traceTransaction`) +/// +/// Pair with [`register_all`] (or [`register_mega`]) so the unsuffixed +/// `` row remains the plain baseline. Does not re-register that row. +pub fn register_inspected(group: &mut Group<'_>, w: &Workload) { + run_subjects(group, "inspect_noop", w, &inspected_subjects(InspectKind::NoOp)); + run_subjects(group, "inspect_tracer", w, &inspected_subjects(InspectKind::GethTracer)); +} + +fn inspected_subjects(kind: InspectKind) -> Vec> { + INSPECTED_SPEC_IDS + .iter() + .map(|&(name, spec)| Box::new(MegaInspected { name, spec, kind }) as Box) + .collect() +} + /// Register mega rows for a caller-supplied spec list (e.g. a single spec, or /// the SELFDESTRUCT-relevant specs). pub fn register_mega_specs( diff --git a/crates/mega-evm/benches/common/subject.rs b/crates/mega-evm/benches/common/subject.rs index 84d38aed..189d9797 100644 --- a/crates/mega-evm/benches/common/subject.rs +++ b/crates/mega-evm/benches/common/subject.rs @@ -24,8 +24,10 @@ use revm::{ context::{tx::TxEnvBuilder, TxEnv}, database::EmptyDB as EmptyDBPinned, primitives::hardfork::SpecId as SpecIdPinned, - Context as ContextPinned, ExecuteEvm, MainBuilder as _, MainContext as _, + Context as ContextPinned, ExecuteEvm, InspectEvm, Inspector, MainBuilder as _, + MainContext as _, }; +use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; use std::{cell::RefCell, rc::Rc}; use super::workload::{Account, TxSpec, Workload}; @@ -180,6 +182,82 @@ impl Subject for Mega { } } +// +// ============================================================================ +// Mega inspected-path subject. +// ============================================================================ +// + +/// Which inspector an inspected-path row attaches. +/// +/// `NoOp` is the floor: the inspect loop and the measurement shim run, but the +/// inner inspector is empty, so the row is the cost of snapshotting. +/// `GethTracer` is `revm-inspectors`' `debug_traceTransaction` default — the +/// production tracer this crate already admits on the block path. `all()` is +/// not used: it clones memory on every opcode and is not an RPC default. +#[derive(Clone, Copy)] +pub enum InspectKind { + NoOp, + GethTracer, +} + +/// `MegaEvm` on the inspected frame loop, with the measurement shim live. +/// +/// The plain [`Mega`] row calls `ExecuteEvm::transact`, which never enters an +/// inspector callback. This subject calls [`InspectEvm::inspect_tx`] after +/// [`MegaEvm::with_inspector`], which is the path RPC tracers take and the only +/// path the shim runs on. +pub struct MegaInspected { + pub name: &'static str, + pub spec: MegaSpecId, + pub kind: InspectKind, +} + +impl Subject for MegaInspected { + fn name(&self) -> &str { + self.name + } + + fn run(&self, workload: &Workload) { + match self.kind { + InspectKind::NoOp => { + run_inspected(self.name, self.spec, workload, || NoOpInspector); + } + InspectKind::GethTracer => { + run_inspected(self.name, self.spec, workload, || { + TracingInspector::new(TracingInspectorConfig::default_geth()) + }); + } + } + } +} + +fn run_inspected(name: &str, spec: MegaSpecId, workload: &Workload, make_inspector: Make) +where + I: Inspector>, + Make: FnOnce() -> I, +{ + run_workload( + name, + workload, + || { + let mut context = MegaContext::new(build_pinned_db(&workload.accounts), spec); + context.modify_chain(|chain| zero_operator_fee!(chain)); + MegaEvm::new(context).with_inspector(make_inspector()) + }, + |evm, tx| { + let mut mega_tx = MegaTransaction(OpTransactionPinned::new(pinned_tx_env(tx))); + mega_tx.enveloped_tx = Some(Bytes::new()); + // `ExecuteEvm::transact` ignores `inspect` and stays on the plain + // loop; `inspect_tx` is the inspected loop the shim actually sits on. + let r = InspectEvm::inspect_tx(evm, mega_tx).expect("mega inspect"); + let success = r.result.is_success(); + black_box(r); + success + }, + ); +} + // // ============================================================================ // MegaWithEnv subject. diff --git a/crates/mega-evm/benches/transact.rs b/crates/mega-evm/benches/transact.rs index 8c30516d..c4db6c18 100644 --- a/crates/mega-evm/benches/transact.rs +++ b/crates/mega-evm/benches/transact.rs @@ -1,8 +1,13 @@ //! Benchmarks for the `ExecuteEvm::transact()` interface. //! //! Each workload runs against two vanilla baselines (`revm_pinned`, -//! `op_revm_pinned`) and four mega specs (`EQUIVALENCE`, `MINI_REX`, `REX4`, -//! `REX5`) so a single bench produces a cross-row gap table. +//! `op_revm_pinned`) and the mega specs (`EQUIVALENCE` … `REX7`) on the plain +//! `transact` path, so a single bench produces a cross-row gap table. +//! +//! Every workload also registers inspected-path rows for REX6 and REX7: +//! `/inspect_noop` (measurement shim around a `NoOpInspector`) and +//! `/inspect_tracer` (`revm-inspectors` geth default). Those rows call +//! `InspectEvm::inspect_tx`, which is the path RPC tracers take. #![allow(missing_docs)] use alloy_primitives::{address, bytes, Address, Bytes, U256}; @@ -10,7 +15,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use revm::primitives::{keccak256, B256}; mod common; -use common::{register_all, Account, TxSpec, Workload}; +use common::{register_all, register_inspected, Account, TxSpec, Workload}; const CALLER: Address = address!("0000000000000000000000000000000000100000"); const CALLEE: Address = address!("0000000000000000000000000000000000100001"); @@ -37,6 +42,7 @@ fn bench_empty_transaction(c: &mut Criterion) { // the caller needs no balance), matching the original workload. let workload = Workload::single(vec![], TxSpec::call(CALLER, CALLEE)); register_all(&mut group, &workload); + register_inspected(&mut group, &workload); group.finish(); } @@ -51,6 +57,7 @@ fn bench_simple_ether_transfer(c: &mut Criterion) { TxSpec::call(CALLER, CALLEE), ); register_all(&mut group, &workload); + register_inspected(&mut group, &workload); group.finish(); } @@ -83,6 +90,7 @@ fn bench_weth9_transfer(c: &mut Criterion) { TxSpec::call(CALLER, WETH9_ADDRESS).data(calldata), ); register_all(&mut group, &workload); + register_inspected(&mut group, &workload); group.finish(); } @@ -128,6 +136,7 @@ fn bench_interpreter_hotloop(c: &mut Criterion) { TxSpec::call(CALLER, CALLEE), ); register_all(&mut group, &workload); + register_inspected(&mut group, &workload); group.finish(); } From 750ab3c3688ff76eb9b3fdd5005982971cf48bb3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 13:47:52 +0800 Subject: [PATCH 158/208] test(rex7): pin the frame invariants against a per-frame comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fields a frame's identity is made of cannot change while it runs, which makes them the readings a cheaper shim would compare once per frame rather than at every callback. An inspector that moves one in `step` and moves it back in `step_end` leaves that identity equal to the EVM's everywhere a per-frame comparison could look, while the instruction in between reads something else. The rewrite costs the transaction nothing — both runs execute the same instructions and pay the same cold `SSTORE` — so no gas lane stands in for the comparison either. What separates the two runs is the value the frame stores. --- crates/mega-evm/src/evm/AGENTS.md | 5 + .../mega-evm/tests/rex7/ledger_blind_spots.rs | 96 ++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index b54a7bdd..65c2308d 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -145,6 +145,11 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g Two tests hold the rule: `tests/rex7/gas_surface.rs` pins `Interpreter`'s field set against upstream's `Debug`, and `inspector.rs`'s own unit tests move each reading in turn and fail if it moves nothing, or if a reading exists that no case moves. On a revm bump, re-read the trait methods the snapshot reads through — `Jumps`, `LoopControl`, `LegacyBytecode`, `StackTr`, `ReturnData`, `MemoryTr`, `InputsTr`, `RuntimeFlag` — for a new constant-time accessor, which is a new reading and not a compile error anywhere. A reading that would need unbounded work is the one thing the rule does not ask for; it belongs in the contents row, which has no lane. +- **Compare every reading at every callback, not once per frame.** + The four fields a frame's identity is made of — its target, the address of the code it runs, its caller and its value — together with its calldata identity, its static flag and its spec id, cannot change while it runs, which makes them the readings a cheaper shim would compare once per frame instead of twice per opcode. + They can change — an inspector writes them — and the shape that exploits a per-frame comparison is an edit made in `step` and undone in `step_end`, which leaves the frame's identity equal to the EVM's at every point outside those two callbacks while the instruction in between reads something else. + `tests/rex7/ledger_blind_spots.rs::test_a_frame_invariant_moved_and_moved_back_is_booked` is that shape, and it costs the transaction nothing, so no gas lane can stand in for the comparison. + Making a reading cheaper is free to do; taking it less often needs an argument that this test survives. - **Book a lane through `Lane::book`, never by writing its net.** The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. - **Keep every rewrite out of a block.** diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 36f91a3b..25b4172a 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -40,10 +40,12 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{ - CALL, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, STOP, + CALL, CALLER, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, + STOP, }, context::{Cfg, ContextTr}, interpreter::{ + interpreter::EthInterpreter, interpreter_types::{Jumps, MemoryTr, ReturnData}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, }, @@ -601,3 +603,95 @@ fn test_a_forged_return_buffer_is_booked() { cheated.inspector_ledger, ); } + +// --- a frame invariant moved and moved back -------------------------------------------------- + +/// The caller the rewriting inspector shows the frame instead of the one that called it. +const IMPOSTOR: Address = address!("00000000000000000000000000000000000ca11e"); + +/// Moves the frame's caller for the length of one instruction, and puts it back. +/// +/// `CALLER` reads `input.caller_address`, so the frame pushes an address nobody called it from and +/// goes on to store that. The rewrite is undone in the very next callback, which is what makes the +/// shape worth pinning: the frame's identity is the one the EVM gave it at every point a *frame* +/// could be inspected — at its start, at its end, and at every callback but the two this touches. +/// +/// Nothing about it reaches a gas counter. Both runs execute the same instructions and pay the +/// same cold `SSTORE`; only the value written differs. +#[derive(Default)] +struct BorrowTheCaller { + /// The caller the EVM gave the frame, kept so it can be handed back. + original: Option
, + /// How many times each half of the rewrite ran. + moved: u32, + restored: u32, +} + +impl Inspector for BorrowTheCaller { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.moved > 0 || interp.bytecode.opcode() != CALLER { + return; + } + self.original = Some(interp.input.caller_address); + interp.input.caller_address = IMPOSTOR; + self.moved += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + let Some(original) = self.original.filter(|_| self.restored == 0) else { + return; + }; + interp.input.caller_address = original; + self.restored += 1; + } +} + +/// ★ A frame invariant moved in `step` and moved back in `step_end` is not an all-zero ledger. +/// +/// The four addresses and the value a frame is identified by cannot change while it runs, which +/// makes them the readings a cheaper shim would be tempted to compare once per frame rather than +/// once per callback. This is the shape that answers that: an inspector borrows one of them for +/// exactly as long as it takes the frame to read it, and gives it back before anything outside the +/// two callbacks could look. A per-frame comparison sees the address it started with; a per-opcode +/// one sees it move twice. +#[test] +fn test_a_frame_invariant_moved_and_moved_back_is_booked() { + let code = BytecodeBuilder::default() + .append(CALLER) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = BorrowTheCaller::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); + assert_eq!( + inspector.original, + Some(crate::common::CALLER), + "and the half that gives the address back must have the one the EVM gave the frame", + ); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(crate::common::CALLER.as_slice()), + "without the rewrite the frame stores the address that called it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(IMPOSTOR.as_slice()), + "with it, the frame stores one nobody called it from", + ); + assert_eq!( + plain.total_gas_spent, cheated.total_gas_spent, + "the two runs cost the same, so no gas lane can tell them apart", + ); + assert!( + cheated.inspector_ledger.interventions >= 2, + "each half of the rewrite is a rewrite: {:?}", + cheated.inspector_ledger, + ); +} From c53b909d0065cbe79e3dee3e8f921004616abcb4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 13:48:22 +0800 Subject: [PATCH 159/208] perf(evm): make the shim's per-callback measurement cheaper without taking less MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four live-interpreter callbacks carried four copies of the same measurement. They now share `LiveReading`, which takes the way-in readings and settles them. Three things in that body got cheaper. The way-out working-set comparison reads the interpreter in place rather than building a second two-hundred-byte snapshot to compare against. The way-in action reading holds what the rewrite comparison needs — a classification and an owned output buffer, or a frame input — instead of a copy of the whole action, so the shape a running frame is almost always in costs a discriminant. And the counter adjustment's `RefCell` borrow is skipped when the counter did not move, which is the only thing that borrow was reaching. Every one of the sixteen readings is still taken and compared at every one of the four callbacks; nothing moved to per-frame. `WorkingSet::unchanged` is a second enumeration of the readings, and the unit tests that move each one in turn now pin it as well as the snapshot it is compared against. --- crates/mega-evm/src/evm/AGENTS.md | 1 + crates/mega-evm/src/evm/inspector.rs | 330 +++++++++++++++++---------- 2 files changed, 216 insertions(+), 115 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 65c2308d..9984bb25 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -143,6 +143,7 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g `WorkingSet` is the snapshot the four live-interpreter callbacks are compared across, and the rule it is built on is stated over the *cost* of a reading rather than over a list of interesting ones: if it is `O(1)` off a field of `Interpreter`, it is in the snapshot. A list is only as complete as whoever wrote it, and the four-reading list that preceded this rule left `bytecode` out entirely. Two tests hold the rule: `tests/rex7/gas_surface.rs` pins `Interpreter`'s field set against upstream's `Debug`, and `inspector.rs`'s own unit tests move each reading in turn and fail if it moves nothing, or if a reading exists that no case moves. + The snapshot is stated twice — once as `WorkingSet::of`, which records the readings, and once as `WorkingSet::unchanged`, which compares them against a live interpreter without building a second snapshot — and the same unit tests hold the two lists together: each case asserts both that the reading it moved is named and that `unchanged` returns `false`, so a reading in one list and not the other is a reading the shim takes and never compares. On a revm bump, re-read the trait methods the snapshot reads through — `Jumps`, `LoopControl`, `LegacyBytecode`, `StackTr`, `ReturnData`, `MemoryTr`, `InputsTr`, `RuntimeFlag` — for a new constant-time accessor, which is a new reading and not a compile error anywhere. A reading that would need unbounded work is the one thing the rule does not ask for; it belongs in the contents row, which has no lane. - **Compare every reading at every callback, not once per frame.** diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 00837142..3edf6754 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -67,7 +67,7 @@ use revm::{ InputsTr, Jumps, LegacyBytecode, LoopControl, MemoryTr, ReturnData, RuntimeFlag, StackTr, }, - CallInput, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, + CallInput, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, primitives::hardfork::SpecId, @@ -175,9 +175,9 @@ impl ActionLane { /// # The gas the counter no longer speaks for /// /// What a dead-window counter edit cannot reach, an edit to the action itself can — and - /// [`measure_pending_action`] measures exactly that, against the same counter reading, so the - /// two together account for every unit of gas the frame holds. See [`held`] for the identity - /// they split. + /// [`LiveReading`] measures exactly that, against the same counter reading, so the two + /// together account for every unit of gas the frame holds. See [`held`] for the identity they + /// split. #[inline] const fn counter_reaches_envelope(self) -> bool { !matches!(self, Self::Result) @@ -200,10 +200,10 @@ impl ActionLane { /// frame will spend nothing more — the action's own copy is what its caller reclaims, and the /// counter is dead. /// -/// Both readings [`measure_pending_action`] takes use the counter *the EVM left behind*, so the -/// counter cancels out of the difference wherever it appears on both sides. What is left is -/// exactly the part of the movement that is not already on the counter lane, whatever the callback -/// did to the action's shape. +/// Both readings [`LiveReading`] takes use the counter *the EVM left behind*, so the counter +/// cancels out of the difference wherever it appears on both sides. What is left is exactly the +/// part of the movement that is not already on the counter lane, whatever the callback did to the +/// action's shape. #[inline] fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { match action { @@ -215,16 +215,16 @@ fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { } } -/// The refund a frame and its pending continuation hold, given the action it is carrying and its -/// own gas object. +/// The refund a frame and its pending continuation hold, given the action it is carrying and the +/// refund on its own gas counter. /// /// [`held`]'s counterpart on the refund dimension, and it has the same three cases for the same /// reason — the object the EVM will read next is the one the frame is holding: /// /// ```text -/// held_refund(None, gas) = gas.refunded() -/// held_refund(NewFrame(_), gas) = gas.refunded() -/// held_refund(Return(r), gas) = r.gas.refunded() +/// held_refund(None, counter) = counter +/// held_refund(NewFrame(_), counter) = counter +/// held_refund(Return(r), counter) = r.gas.refunded() /// ``` /// /// The middle case differs from [`held`]'s: a `NewFrame` action carries a child's *envelope* but @@ -235,9 +235,9 @@ fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { /// Read on both sides of a callback, the difference is the refund the inspector wrote — wherever /// it wrote it, and whichever of the two objects the EVM goes on to read. #[inline] -fn held_refund(action: Option<&InterpreterAction>, gas: &Gas) -> i64 { +fn held_refund(action: Option<&InterpreterAction>, counter: i64) -> i64 { match action { - None | Some(InterpreterAction::NewFrame(_)) => gas.refunded(), + None | Some(InterpreterAction::NewFrame(_)) => counter, Some(InterpreterAction::Return(result)) => result.gas.refunded(), } } @@ -288,42 +288,64 @@ struct ActionChange { lane: ActionLane, } -/// Measures what a callback did to the pending action, against the counter the EVM left behind. +/// The pending action a callback was handed, in the form the boundary compares it in. /// -/// The EVM does not execute inside a callback, so the action is either the one the last -/// instruction set or one the callback wrote — and the difference between the two readings of -/// [`held`] is what the callback moved. Taking both readings at the *pre-callback* counter is -/// what keeps this lane and the counter lane from overlapping: -/// [`AdditionalLimit::record_inspector_gas_adjustment`] books the counter's own movement exactly -/// when [`ActionLane::counter_reaches_envelope`] says the EVM will read it again, and it cancels -/// out of this difference in precisely the cases where it does. -#[inline] -fn measure_pending_action( - before: Option, - after: Option<&InterpreterAction>, - counter: u64, -) -> ActionChange { - let gas = held(after, counter) - held(before.as_ref(), counter); - ActionChange { gas, rewritten: action_rewritten(before, after), lane: ActionLane::of(after) } +/// Only one question is asked of the way-in reading that the numbers beside it in [`LiveReading`] +/// do not already answer: did the action come back describing something other than what the EVM +/// decided? So this holds what that comparison reads and nothing else — the gas is taken as +/// [`held`] at the moment the reading is made, and never needs the action again. +/// +/// Which matters because the way-in reading is taken twice per opcode. Copying the action itself +/// would carry an `InterpreterResult`'s output buffer and a frame input's boxed inputs across +/// every one of them; here the shape a running frame is almost always in costs a discriminant, and +/// the two that carry something are taken once per frame and once per call. +/// +/// The output buffer is held rather than reduced to its identity, and that is the point of holding +/// it: [`same_buffer`] compares by address, and only an owner keeps the address it compares from +/// being reused underneath it. +#[derive(Clone, Debug)] +enum ActionSnapshot { + /// No action pending: the frame carries straight on. + Empty, + /// A `Return` action — the classification and output the frame's caller will be handed. + Return(InstructionResult, Bytes), + /// A `NewFrame` action — the inputs a child is about to be built from. The one comparison + /// here that needs the object rather than a reading off it. + NewFrame(FrameInput), } -/// Whether a callback left behind an action describing something other than what the EVM decided. -/// -/// Gas is excluded, exactly as it is at every other boundary: it travels on the lanes -/// [`measure_pending_action`] routes it to, and counting it here as well would report one rewrite -/// twice. A callback that installed, removed or swapped an action has rewritten what the EVM does -/// next as thoroughly as it is possible to, so every shape change counts. -#[inline] -fn action_rewritten(before: Option, after: Option<&InterpreterAction>) -> bool { - match (before, after) { - (None, None) => false, - (Some(InterpreterAction::Return(before)), Some(InterpreterAction::Return(after))) => { - result_rewritten((before.result, &before.output), after) +impl ActionSnapshot { + /// Takes the way-in reading off the action an interpreter is holding. + #[inline(always)] + fn of(action: Option<&InterpreterAction>) -> Self { + match action { + None => Self::Empty, + Some(InterpreterAction::Return(result)) => { + Self::Return(result.result, result.output.clone()) + } + Some(InterpreterAction::NewFrame(frame_input)) => Self::NewFrame(frame_input.clone()), } - (Some(InterpreterAction::NewFrame(before)), Some(InterpreterAction::NewFrame(after))) => { - frame_input_rewritten(before, after) + } + + /// Whether a callback left behind an action describing something other than what the EVM + /// decided. + /// + /// Gas is excluded, exactly as it is at every other boundary: it travels on the lanes + /// [`book_pending_action`] routes it to, and counting it here as well would report one rewrite + /// twice. A callback that installed, removed or swapped an action has rewritten what the EVM + /// does next as thoroughly as it is possible to, so every shape change counts. + #[inline(always)] + fn rewritten(self, after: Option<&InterpreterAction>) -> bool { + match (self, after) { + (Self::Empty, None) => false, + (Self::Return(result, output), Some(InterpreterAction::Return(after))) => { + result_rewritten((result, &output), after) + } + (Self::NewFrame(before), Some(InterpreterAction::NewFrame(after))) => { + frame_input_rewritten(before, after) + } + _ => true, } - _ => true, } } @@ -575,6 +597,44 @@ struct WorkingSet { } impl WorkingSet { + /// Whether a live interpreter still reads the way this snapshot recorded it. + /// + /// The same question as `*self == Self::of(interp)`, asked without building the second + /// snapshot. That is the whole difference, and it is worth stating because this is the way-out + /// half of a measurement taken twice per opcode: a comparison written that way materialises + /// two hundred bytes onto the stack for the length of one `==`, and the optimiser does not + /// reliably take them away again. + /// + /// Every reading in [`of`](Self::of) is compared here, in the same order, and neither list may + /// be shortened without the other. What holds them together is the unit tests below: each of + /// them moves one reading and asserts both that [`moved`] names it and that this returns + /// `false`, so a reading present in the snapshot and missing here is a reading the shim takes + /// and never compares. + /// + /// Left as `inline` rather than `inline(always)` on purpose. Forced into all four callbacks it + /// is faster beside an empty inspector and slower beside a real tracer, which is the inspector + /// the inspected path actually carries; one copy per interpreter type is faster beside both. + #[inline] + fn unchanged(&self, interp: &Interpreter) -> bool { + let memory = interp.gas.memory(); + (self.pc == interp.bytecode.pc()) && + (self.code == BufferId::of(interp.bytecode.bytecode_slice())) && + (self.running == interp.bytecode.is_not_end()) && + (self.stack_len == interp.stack.len()) && + (self.return_data == BufferId::of(interp.return_data.buffer())) && + (self.memory_size == interp.memory.size()) && + (self.memory_offset == interp.memory.local_memory_offset()) && + (self.memory_words == memory.words_num) && + (self.memory_expansion_cost == memory.expansion_cost) && + (self.target_address == interp.input.target_address()) && + (self.bytecode_address.as_ref() == interp.input.bytecode_address()) && + (self.caller_address == interp.input.caller_address()) && + (self.call_value == interp.input.call_value()) && + (self.call_input == CallInputId::of(interp.input.input())) && + (self.is_static == interp.runtime_flag.is_static()) && + (self.spec_id == interp.runtime_flag.spec_id()) + } + /// Takes every reading off a live interpreter. #[inline] fn of(interp: &Interpreter) -> Self { @@ -748,6 +808,92 @@ fn reject_forbidden_create_rewrite( ); } +/// What the shim reads off a live interpreter on the way into a callback, and settles on the way +/// out. +/// +/// The four callbacks that are handed a live interpreter run the same measurement, and it is +/// written once here rather than four times: [`enter`](Self::enter) takes the way-in readings, the +/// user's inspector runs, and [`leave`](Self::leave) takes them again and books the differences. +/// The four used to carry a copy of that body each, which is four places for a boundary to be +/// measured differently at. +/// +/// `IN_OPEN_SEGMENT` on [`leave`](Self::leave) is the one thing that differs between the four: +/// `initialize_interp` runs before the frame's settlement window is opened, so there is no open +/// segment for a counter edit to be moved out of. +struct LiveReading { + /// Every constant-time reading of the interpreter's working set. + working_set: WorkingSet, + /// The pending action, in the form the rewrite comparison reads it in. + action: ActionSnapshot, + /// [`held`], taken at the counter below — the way-in half of the action lane's difference. + held: i128, + /// [`held_refund`], taken at the same moment. + refund: i64, + /// The interpreter's own gas counter, which is also the counter both [`held`] readings are + /// taken at. + gas: u64, +} + +impl LiveReading { + /// Takes every way-in reading off a live interpreter. + #[inline(always)] + fn enter(interp: &mut Interpreter) -> Self { + let working_set = WorkingSet::of(interp); + let gas = interp.gas.remaining(); + let refunded = interp.gas.refunded(); + let action = interp.bytecode.action().as_ref(); + Self { + working_set, + held: held(action, gas), + refund: held_refund(action, refunded), + action: ActionSnapshot::of(action), + gas, + } + } + + /// Takes the readings again and books what the callback moved. + #[inline(always)] + fn leave( + self, + interp: &mut Interpreter, + context: &MegaContext, + ) where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + { + let moved = !self.working_set.unchanged(interp); + let gas = interp.gas.remaining(); + let refunded = interp.gas.refunded(); + let action = interp.bytecode.action().as_ref(); + let lane = ActionLane::of(action); + let change = ActionChange { + // Both readings are taken at the counter the EVM left behind, so the counter cancels + // out of the difference wherever it appears on both sides. + gas: held(action, self.gas) - self.held, + rewritten: self.action.rewritten(action), + lane, + }; + let refund = held_refund(action, refunded); + + book_intervention(context, moved); + book_pending_action(context, change); + book_refund(context, self.refund, refund); + // `record_inspector_gas_adjustment` returns on its own when the counter did not move, and + // the borrow it would take to find that out is not free on a path taken twice per opcode. + if gas != self.gas { + context + .additional_limit + .borrow_mut() + .record_inspector_gas_adjustment::( + &mut interp.gas, + self.gas, + lane.counter_reaches_envelope(), + ); + } + } +} + impl Inspector, INTR> for MeasuredInspector where DB: Database, @@ -764,46 +910,16 @@ where interp: &mut Interpreter, context: &mut MegaContext, ) { - let action = interp.bytecode.action().clone(); - let refund_before = held_refund(action.as_ref(), &interp.gas); - let before = interp.gas.remaining(); - let working_set = WorkingSet::of(interp); + let reading = LiveReading::enter(interp); self.inner.initialize_interp(interp, context); - book_intervention(context, WorkingSet::of(interp) != working_set); - let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); - book_pending_action(context, change); - book_refund( - context, - refund_before, - held_refund(interp.bytecode.action().as_ref(), &interp.gas), - ); - context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( - &mut interp.gas, - before, - change.lane.counter_reaches_envelope(), - ); + reading.leave::(interp, context); } #[inline] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { - let action = interp.bytecode.action().clone(); - let refund_before = held_refund(action.as_ref(), &interp.gas); - let before = interp.gas.remaining(); - let working_set = WorkingSet::of(interp); + let reading = LiveReading::enter(interp); self.inner.step(interp, context); - book_intervention(context, WorkingSet::of(interp) != working_set); - let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); - book_pending_action(context, change); - book_refund( - context, - refund_before, - held_refund(interp.bytecode.action().as_ref(), &interp.gas), - ); - context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( - &mut interp.gas, - before, - change.lane.counter_reaches_envelope(), - ); + reading.leave::(interp, context); } /// The one callback that runs with an action already pending: revm runs it after the @@ -814,24 +930,9 @@ where /// [`book_pending_action`] routes them to. #[inline] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { - let action = interp.bytecode.action().clone(); - let refund_before = held_refund(action.as_ref(), &interp.gas); - let before = interp.gas.remaining(); - let working_set = WorkingSet::of(interp); + let reading = LiveReading::enter(interp); self.inner.step_end(interp, context); - book_intervention(context, WorkingSet::of(interp) != working_set); - let change = measure_pending_action(action, interp.bytecode.action().as_ref(), before); - book_pending_action(context, change); - book_refund( - context, - refund_before, - held_refund(interp.bytecode.action().as_ref(), &interp.gas), - ); - context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( - &mut interp.gas, - before, - change.lane.counter_reaches_envelope(), - ); + reading.leave::(interp, context); } /// No interpreter and no frame inputs are reachable here, so there is nothing to measure — @@ -848,24 +949,9 @@ where context: &mut MegaContext, log: Log, ) { - let action = interpreter.bytecode.action().clone(); - let refund_before = held_refund(action.as_ref(), &interpreter.gas); - let before = interpreter.gas.remaining(); - let working_set = WorkingSet::of(interpreter); + let reading = LiveReading::enter(interpreter); self.inner.log_full(interpreter, context, log); - book_intervention(context, WorkingSet::of(interpreter) != working_set); - let change = measure_pending_action(action, interpreter.bytecode.action().as_ref(), before); - book_pending_action(context, change); - book_refund( - context, - refund_before, - held_refund(interpreter.bytecode.action().as_ref(), &interpreter.gas), - ); - context.additional_limit.borrow_mut().record_inspector_gas_adjustment::( - &mut interpreter.gas, - before, - change.lane.counter_reaches_envelope(), - ); + reading.leave::(interpreter, context); } #[inline] @@ -1136,6 +1222,11 @@ mod tests { /// and asserts that exactly that one name comes back — so a field dropped from /// [`WorkingSet::of`] leaves its case detecting nothing, and a field left out of the snapshot /// entirely never compiles past [`moved`]. + /// + /// Each case also asserts that [`WorkingSet::unchanged`] sees the same movement, which is the + /// third way the rule can fail. That comparison is written out rather than derived from the + /// snapshot, so a reading missing from it is a reading the shim takes, stores, and never + /// compares — [`moved`] would still name it and the shim would still book nothing. #[test] fn test_every_reading_moves_exactly_the_reading_it_is_named_for() { let unchanged = probe(); @@ -1143,17 +1234,26 @@ mod tests { moved(&WorkingSet::of(&unchanged), &WorkingSet::of(&unchanged)).is_empty(), "a snapshot compared against itself must report nothing moved", ); + assert!( + WorkingSet::of(&unchanged).unchanged(&unchanged), + "and an interpreter nothing touched must still read the way it was recorded", + ); assert_ne!(SpecId::default(), SpecId::FRONTIER, "the spec-id case must move something"); for (name, rewrite) in CASES { let mut interp = probe(); let before = WorkingSet::of(&interp); rewrite(&mut interp); + let after = WorkingSet::of(&interp); assert_eq!( - moved(&before, &WorkingSet::of(&interp)), + moved(&before, &after), [name], "the rewrite for {name} must move that reading and no other", ); + assert!( + !before.unchanged(&interp), + "and the comparison the shim makes must see {name} move", + ); } } From bed640e78a4f6456be3911dbd263a0986cfb5fb0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 15:59:05 +0800 Subject: [PATCH 160/208] feat(evm): refuse a classification rewrite of a result frame init produced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other rewrite of a frame's result is supported because REX7 withholds the journal decision until the result is final. A result that comes out of frame init has no such window: revm decides the journal inside `make_call_frame` — a value-transferring call into an empty-code account commits the transfer and returns `Stop`, a failing precompile reverts it — and MegaETH's interceptors decide theirs before they return, the KeylessDeploy one by merging a whole sandbox's state. Honouring a rewrite that moves such a result across the success / revert / halt boundary hands the caller an answer the state behind it contradicts. The window is opened at the one call site that runs the last callback over a result `init_frame_unsettled` returned, so the boundary is a call site rather than a per-arm judgement over revm's early-fail returns. A result an inspector answered the frame with itself stays outside it: nothing in the EVM decided anything for that one. The refusal restores the classification, counts itself on the ledger and fails the transaction with `EVMError::Custom`. Unlike the creation refusal it does not assert: failing a call is the most ordinary rewrite a tool makes, and a corpus that produces it has to be able to report it. Three anchors, one per path that reaches the split, plus the near boundary. The precompile settlement cases that pinned the old behaviour now pin the uninspected split and the refusal that keeps it the one settlement reads. --- crates/mega-evm/src/evm/execution.rs | 38 +- crates/mega-evm/src/evm/inspector.rs | 138 +++++- crates/mega-evm/src/limit/inspector_ledger.rs | 18 +- crates/mega-evm/src/limit/limit.rs | 45 ++ crates/mega-evm/tests/rex7/common.rs | 51 +- .../tests/rex7/frame_init_result_rewrite.rs | 435 ++++++++++++++++++ .../tests/rex7/inspector_settlement_window.rs | 118 +++-- crates/mega-evm/tests/rex7/main.rs | 1 + 8 files changed, 772 insertions(+), 72 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index dc2de124..6585cc7e 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -1888,6 +1888,11 @@ where additional_limit.borrow_mut().push_empty_frame(); } + // Deliberately *not* the marked window below: this result is the inspector's own, + // produced by a callback that answered the frame before anything in the EVM decided + // anything for it. There is no journal decision behind it for a later rewrite to + // contradict — no checkpoint was opened, no state was written — so moving its + // classification is supported like any other result rewrite. frame_end(ctx, inspector, &frame_init.frame_input, &mut output); if is_mini_rex_enabled { @@ -1919,7 +1924,7 @@ where forward_precompile_logs(ctx, inspector, logs_before_init, &output); let gas_before_callback = output.gas().remaining(); - frame_end(ctx, inspector, &frame_input, &mut output); + frame_end_on_frame_init_result(ctx, inspector, &frame_input, &mut output); let inspector_gas_delta = i128::from(output.gas().remaining()) - i128::from(gas_before_callback); @@ -1980,6 +1985,37 @@ where } } +/// Runs the inspector's last callback over a result [`init_frame_unsettled`] produced, inside the +/// window the measurement shim reads to tell such a result apart from one a frame produced. +/// +/// What the shim does inside it is refuse a rewrite that moves the result's classification. The +/// journal decision behind such a result was taken before any callback ran and no callback can +/// reach it: revm's `make_call_frame` commits a value-transferring call into an empty-code account +/// and reverts a failing precompile inside itself, and a system contract interceptor decides its +/// own before it returns — the `KeylessDeploy` one by merging a whole sandbox's state. Gas and +/// output are untouched by the window; they are measured on their own lanes either way. +/// +/// The boundary is the *call site*, not a classification of the arms behind it, and deliberately +/// so: the arms are revm's early-fail returns plus `MegaETH`'s interceptors and guards, a set with +/// no type-level tie to anything here, and one that a revm bump grows without a compile error. +/// Some of them — a depth rejection, a refusal `MegaETH` took before opening a checkpoint — carry +/// no state a rewrite could contradict, and are covered anyway. +/// +/// [`init_frame_unsettled`]: MegaEvm::init_frame_unsettled +#[inline] +fn frame_end_on_frame_init_result( + ctx: &mut MegaContext, + inspector: &mut INSP, + frame_input: &FrameInput, + output: &mut FrameResult, +) where + INSP: Inspector>, +{ + ctx.additional_limit.borrow_mut().set_settling_frame_init_result(true); + frame_end(ctx, inspector, frame_input, output); + ctx.additional_limit.borrow_mut().set_settling_frame_init_result(false); +} + /// Hands an inspector the logs a precompile emitted, which no other callback would show it. /// /// A precompile is dispatched inside the frame init and comes back as a result rather than a diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 3edf6754..c8735d8c 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -77,9 +77,19 @@ use revm::{ use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; /// The message a refused `create_end` rewrite surfaces as `EVMError::Custom`. -pub(crate) const FORBIDDEN_CREATE_REVIVAL: &str = +/// +/// Public because a refusal is a designed outcome and not an execution failure: a harness that +/// drives rewriting inspectors over a corpus has to tell the two apart, and the error's message is +/// what carries the difference. +pub const FORBIDDEN_CREATE_REVIVAL: &str = "inspector rewrote a failed contract creation into a successful one"; +/// The message a refused rewrite of a frame-init result surfaces as `EVMError::Custom`. +/// +/// Public for the same reason as [`FORBIDDEN_CREATE_REVIVAL`]. +pub const FORBIDDEN_FRAME_INIT_REWRITE: &str = + "inspector moved the classification of a result frame init produced"; + /// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// /// `MegaETH` applies this itself — [`MegaEvm::with_inspector`](crate::MegaEvm::with_inspector) and @@ -768,6 +778,122 @@ fn frame_input_rewritten(before: FrameInput, after: &FrameInput) -> bool { } } +/// What an inspector can ask `MegaETH` about the frame result it is holding. +/// +/// One question, with one purpose: telling a result a frame *ran* to produce apart from one frame +/// init produced without ever building a frame. The two arrive at the same callback holding the +/// same type, and the difference decides what a rewrite of the classification does — a running +/// frame's journal decision is still outstanding and follows the rewrite, while an init-produced +/// result's was taken before the callback existed and is refused (see +/// [`MeasuredInspector`](MeasuredInspector#impl-Inspector)). +/// +/// A tool that only observes never needs this. One that rewrites classifications does, because +/// otherwise the only way to find out which kind of result it is holding is to have its +/// transaction refused. +pub trait FrameResultOriginTr { + /// Whether the frame result the `*_end` callbacks are being handed came out of frame init. + /// + /// False everywhere else, including at every callback that is not one of those three. + fn is_frame_init_result(&self) -> bool; +} + +impl FrameResultOriginTr for MegaContext { + #[inline] + fn is_frame_init_result(&self) -> bool { + self.additional_limit.borrow().is_settling_frame_init_result() + } +} + +/// Which of the three things a frame's result says, which is the granularity the refusal below is +/// stated over. +/// +/// A result's gas and its returned output move freely — those are what the lanes measure. What +/// cannot move is which of these three the caller is handed, because that is the question the +/// journal decision answers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ResultClass { + /// The frame returned, and its writes stand. + Success, + /// The frame reverted, and its writes are rolled back with its gas handed back. + Revert, + /// The frame halted exceptionally, and its writes are rolled back with its gas destroyed. + Halt, +} + +impl ResultClass { + #[inline] + const fn of(result: InstructionResult) -> Self { + if result.is_ok() { + Self::Success + } else if result.is_revert() { + Self::Revert + } else { + Self::Halt + } + } +} + +/// Refuses a rewrite that moves a frame-init result across the success / revert / halt boundary. +/// +/// Every other classification rewrite is supported because REX7 withholds the journal decision +/// until the result is final: the frame loops park it and `frame_return_result` carries it out +/// after the last callback, so a frame rewritten into a revert has its state rolled back with it. +/// +/// A result that comes out of frame *init* has no such window, and cannot be given one from here. +/// Upstream takes the decision inside `make_call_frame`, statements before it returns — a +/// value-transferring call into an empty-code account commits the transfer and returns `Stop`, a +/// precompile that fails reverts it and returns its own failure — and `MegaETH`'s system contract +/// interceptors take theirs before they return, the `KeylessDeploy` one by merging a whole +/// sandbox's state into the journal. All of it has happened by the time a callback sees the +/// result. Honouring a rewrite would hand the caller an answer the state behind it contradicts: +/// a transfer the recipient keeps and the sender is told failed, or a deployment the caller is +/// told reverted and that stands anyway. +/// +/// A result an inspector answered the frame with itself is deliberately outside the refusal, even +/// though it too reaches a callback with no frame having run: nothing in the EVM decided anything +/// for it — no checkpoint was opened, no state written — so its classification is the inspector's +/// to state and rewriting it contradicts nothing. What separates the two is which of the two +/// callback sites in `inspect_frame_init` ran, and that is where the window is opened; nothing +/// here can tell them apart on its own. +/// +/// Detection only; nothing here compensates the journal. The original classification is restored, +/// the ledger counts the refusal, and the context's error slot carries the reason so the +/// transaction fails with an error rather than with a receipt built on the rewrite. +/// +/// Deliberately loud but not fatal, which is where it differs from +/// [`reject_forbidden_create_rewrite`]. That shape is a mistake with no reading behind it and +/// asserting on it costs nothing. This one is the most ordinary rewrite a tool makes — failing a +/// call — landing on the one kind of frame it cannot be applied to, so a corpus that produces it +/// should be able to report it rather than die on it. +/// +/// Gated to REX7+. On a frozen spec, an inspector's rewrite reaches no accounting lane that can be +/// made unsound by it, and the specs' behaviour — including on the inspected path — is closed. +#[inline] +fn reject_forbidden_frame_init_rewrite( + context: &mut MegaContext, + before: InstructionResult, + result: &mut InterpreterResult, +) { + if !context.spec.is_enabled(MegaSpecId::REX7) || + ResultClass::of(before) == ResultClass::of(result.result) || + !context.additional_limit.borrow().is_settling_frame_init_result() + { + return; + } + result.result = before; + context.additional_limit.borrow_mut().record_inspector_rejected_rewrite(); + let slot = context.error(); + if slot.is_ok() { + *slot = Err(ContextError::Custom(String::from(FORBIDDEN_FRAME_INIT_REWRITE))); + } + debug_assert_eq!( + ResultClass::of(result.result), + ResultClass::of(before), + "{FORBIDDEN_FRAME_INIT_REWRITE}: the refusal must leave the caller holding the \ + classification the EVM produced", + ); +} + /// Refuses a rewrite that turns a non-successful contract creation into a successful one, and says /// so loudly. /// @@ -993,8 +1119,10 @@ where result_rewritten((before, &output), frame_result.interpreter_result()), ); book_intervention(context, OutcomeMetadata::of(frame_result) != metadata); - // `frame_end` runs after `create_end` and is the last chance to rewrite a creation's - // classification, so the same refusal applies here. + // `frame_end` runs after `call_end` / `create_end` and is the last chance to rewrite a + // classification, so both refusals apply here too. The frame-init one runs first: it + // restores whatever it refuses, which leaves the creation refusal below nothing to see. + reject_forbidden_frame_init_rewrite(context, before, frame_result.interpreter_result_mut()); if let FrameResult::Create(outcome) = frame_result { reject_forbidden_create_rewrite(context, before, &mut outcome.result); } @@ -1038,6 +1166,7 @@ where book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); book_intervention(context, CallMetadata::of(outcome) != metadata); + reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); } #[inline] @@ -1079,6 +1208,9 @@ where book_refund(context, refund_before, outcome.result.gas.refunded()); book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); book_intervention(context, outcome.address != address); + // The frame-init refusal runs first, and restores whatever it refuses — so a creation + // refused as an init result is not counted a second time by the refusal below. + reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); } diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 37202d3b..9bf3a348 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -290,10 +290,20 @@ pub struct InspectorLedger { /// How many rewrites the shim refused because their shape is forbidden. /// - /// Today exactly one shape is: a `create_end` (or the `frame_end` after it) turning a - /// non-successful contract creation into a successful one. Such a rewrite runs after the - /// journal has already reverted the frame and after the deposit predicates have already - /// rejected the code, so honouring it would report a deployment that never happened. + /// Two shapes are, and both for the same reason: the journal decision they would need to move + /// with was already taken, at a point no callback can reach. + /// + /// - A `create_end` (or the `frame_end` after it) turning a non-successful contract creation + /// into a successful one. Such a rewrite runs after the journal has already reverted the + /// frame and after the deposit predicates have already rejected the code, so honouring it + /// would report a deployment that never happened. + /// - Any of the three `*_end` callbacks moving the classification of a result *frame init* + /// produced across the success / revert / halt boundary. revm decides the journal inside + /// `make_call_frame` and `MegaETH`'s interceptors decide theirs before they return, so + /// honouring it would hand the caller an answer the state behind it contradicts. + /// + /// A non-zero count means the transaction was failed with an `EVMError::Custom` rather than + /// given a receipt. pub rejected_rewrites: u32, /// How many rewrites the shim saw that change what the execution *did* rather than what it diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index e39749f2..2a9a2c8a 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -193,6 +193,15 @@ pub struct AdditionalLimit { /// gas the result turns out to carry. At most one can be outstanding: revm stops at the /// first callback that answers, and the frame init that asked settles a few statements later. staged_interception_envelope: Option, + + /// Whether the frame result the `*_end` callbacks are being handed came out of frame init + /// rather than out of a frame that ran. + /// + /// Set by the inspected frame-init path around the one place it runs those callbacks over a + /// result the EVM produced, and cleared as soon as they return. The measurement shim reads it + /// to decide whether a classification rewrite is one the journal decision can still follow — + /// see [`is_settling_frame_init_result`](Self::is_settling_frame_init_result). + settling_frame_init_result: bool, } /// The usage of the additional limits. @@ -226,6 +235,7 @@ impl AdditionalLimit { staged_action_result_gas: 0, staged_action_env_gas: 0, staged_interception_envelope: None, + settling_frame_init_result: false, } } } @@ -273,6 +283,7 @@ impl AdditionalLimit { self.staged_action_result_gas = 0; self.staged_action_env_gas = 0; self.staged_interception_envelope = None; + self.settling_frame_init_result = false; } /// Whether compute gas settles at checkpoints (REX7+) rather than per opcode. @@ -660,6 +671,40 @@ impl AdditionalLimit { self.staged_interception_envelope.take() } + /// Marks the window in which the `*_end` callbacks are being handed a result frame init + /// produced, with no child frame ever built. + /// + /// The inspected frame-init path opens this immediately before it runs those callbacks over a + /// result the frame init produced — an empty-code call, a precompile, a system contract + /// interceptor, a refusal — and closes it as soon as they return. The other two shapes that + /// reach the same callbacks never open it: a frame that ran settles somewhere else entirely, + /// and a result an inspector answered the frame with is the inspector's own, with no journal + /// decision behind it for a rewrite to contradict. + /// + /// It is a plain flag rather than a counter because it cannot nest: the EVM does not execute + /// inside a callback, so nothing can start a second frame init while one is open. + #[inline] + pub(crate) fn set_settling_frame_init_result(&mut self, settling: bool) { + debug_assert!( + self.settling_frame_init_result != settling, + "the frame-init settlement window is opened and closed in pairs, and cannot nest", + ); + self.settling_frame_init_result = settling; + } + + /// Whether the frame result a `*_end` callback is holding came out of frame init. + /// + /// Such a result carries a journal decision that was taken before any callback ran, and that + /// no callback can reach: revm's `make_call_frame` commits an empty-code call's value transfer + /// and reverts a failing precompile's inside itself, and `MegaETH`'s interceptors decide + /// theirs before they return — the `KeylessDeploy` one by merging a sandbox's whole state. + /// The REX7 deferral covers the frame loops and not this, so a rewrite that moves such a + /// result across the success / revert / halt boundary is refused rather than followed. + #[inline] + pub fn is_settling_frame_init_result(&self) -> bool { + self.settling_frame_init_result + } + /// Books an adjustment an inspector made to a pending action the same callback then removed, /// leaving the frame to carry on from its own counter. /// diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 78b99096..cab64d67 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -12,7 +12,7 @@ use revm::{ state::EvmState, Inspector, }; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, string::String}; /// Transaction sender. pub(crate) const CALLER: Address = address!("0000000000000000000000000000000000300000"); @@ -299,6 +299,55 @@ where finish(spec, outcome, detained_compute_gas_limit, terms) } +/// What a transaction the shim refused reports: the error it surfaced and the refusals counted. +/// +/// A refused rewrite produces no receipt at all, so there is no [`Outcome`] to read — these two +/// numbers are the whole of what such a run leaves behind. +pub(crate) struct Refusal { + /// The `EVMError` the refusal surfaced, rendered. + pub(crate) error: String, + /// How many rewrites the shim refused over the transaction. + pub(crate) rejected_rewrites: u32, +} + +/// [`transact_inspected`] for a run the shim is expected to refuse. +/// +/// Panics when the transaction produced a receipt, so a fixture that stops reaching the refused +/// shape fails rather than passing as a run that was never refused. +pub(crate) fn transact_inspected_refused( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + inspector: &mut I, +) -> Refusal +where + I: for<'a> Inspector>, +{ + let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::from(0)); + chain.operator_fee_constant = Some(U256::from(0)); + }); + let tx = TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + let mut evm = MegaEvm::new(context).with_inspector(inspector); + let outcome = evm.execute_transaction(tx); + let rejected_rewrites = + evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites; + match outcome { + Ok(outcome) => panic!( + "the run was expected to be refused, but produced {:?}", + outcome.result_and_state.result, + ), + Err(e) => Refusal { error: std::format!("{e:?}"), rejected_rewrites }, + } +} + /// Runs [`transact`] with the spec's default runtime limits. pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome { transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) diff --git a/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs b/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs new file mode 100644 index 00000000..51c12d90 --- /dev/null +++ b/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs @@ -0,0 +1,435 @@ +//! A result frame init produced itself cannot have its classification rewritten. +//! +//! Every other rewrite of a frame's result is supported, because REX7 withholds the journal +//! decision until the result is final: the frame loops park it and `frame_return_result` carries +//! it out, so a frame rewritten into a revert has its state rolled back with it. +//! +//! A result that comes out of frame *init* has no such window. Upstream decides the journal inside +//! `make_call_frame` — a value-transferring call into an empty-code account commits the transfer +//! and returns `Stop`, a failing precompile reverts the transfer and returns its own failure — and +//! `MegaETH`'s interceptors decide it before they return, the `KeylessDeploy` one by merging a +//! whole sandbox's state. All of that has happened by the time any callback sees the result, and +//! none of it is reachable from one. +//! +//! So a rewrite that moves such a result across the success / revert / halt boundary hands the +//! caller an answer the state behind it contradicts. Each test below reaches that split by a +//! different door, and asserts the absence of the split before it asserts the refusal — so a run +//! that honours the rewrite reports the two halves that disagree rather than only the missing +//! counter. + +use crate::common::{CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_sol_types::SolCall as _; +use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, + KEYLESS_DEPLOY_ADDRESS, +}; +use revm::{ + bytecode::opcode::{CALL, RETURN, SSTORE, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr}, + handler::EvmTr, + interpreter::{ + CallInputs, CallOutcome, Gas, InstructionResult, InterpreterResult, InterpreterTypes, + }, + state::EvmState, + Inspector, +}; +use std::{string::String, vec::Vec}; + +/// Transaction gas limit: high enough that EVM gas never binds. +const TX_GAS_LIMIT: u64 = 30_000_000; + +/// `ecrecover`, the precompile the failing-precompile case calls. +const ECRECOVER: Address = address!("0000000000000000000000000000000000000001"); + +/// The relayer that sends the keyless deployment. +const RELAYER: Address = address!("0000000000000000000000000000000000340009"); + +/// The slot `CONTRACT` writes its `CALL`'s success flag to. +const FLAG_SLOT: U256 = U256::from_limbs([7, 0, 0, 0]); + +/// The wei the value-transferring cases send. +const SENT: u128 = 1; + +/// What one run produced, in the shape the splits are asserted over. +struct Reading { + result: Result, String>, + rejected_rewrites: u32, + state: EvmState, +} + +impl Reading { + /// The balance the produced state gives `address`, or zero when it never touched it. + fn balance(&self, address: Address) -> U256 { + self.state.get(&address).map(|a| a.info.balance).unwrap_or_default() + } + + /// The value at `slot` on `address` in the produced state. + fn storage(&self, address: Address, slot: U256) -> U256 { + self.state + .get(&address) + .and_then(|a| a.storage.get(&slot)) + .map(|s| s.present_value()) + .unwrap_or_default() + } + + /// Whether the produced state gives `address` any code. + fn has_code(&self, address: Address) -> bool { + self.state + .get(&address) + .is_some_and(|a| a.info.code_hash != B256::ZERO && !a.info.is_empty_code_hash()) + } + + /// Whether the transaction produced a receipt at all, and a successful one. + fn succeeded(&self) -> bool { + matches!(&self.result, Ok(r) if r.is_success()) + } +} + +/// Rewrites the classification of the result of every call into `target`, once. +#[derive(Debug)] +struct RewriteInitResult { + target: Address, + to: InstructionResult, + /// How many results it actually rewrote. Asserted, so a fixture that stops reaching the + /// callback fails rather than passing as a run that rewrote nothing. + fired: u32, +} + +impl RewriteInitResult { + const fn new(target: Address, to: InstructionResult) -> Self { + Self { target, to, fired: 0 } + } +} + +impl Inspector for RewriteInitResult +where + CTX: ContextTr, + INTR: InterpreterTypes, +{ + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != self.target || outcome.result.result == self.to { + return; + } + outcome.result.result = self.to; + self.fired += 1; + } +} + +/// Runs `tx` under REX7 with `inspector` attached. +fn run(mut db: MemoryDatabase, tx: MegaTransaction, inspector: &mut I) -> Reading +where + I: for<'a> Inspector>, +{ + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + 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(inspector); + let outcome: Result = evm.execute_transaction(tx); + let rejected_rewrites = + evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites; + match outcome { + Ok(outcome) => Reading { + result: Ok(outcome.result_and_state.result), + rejected_rewrites, + state: outcome.result_and_state.state, + }, + Err(e) => Reading { + result: Err(std::format!("{e:?}")), + rejected_rewrites, + state: EvmState::default(), + }, + } +} + +/// The two facts every case here pins: the rewrite was counted as refused, and the transaction +/// failed with an error rather than reporting a receipt built on it. +fn assert_refused(reading: &Reading) { + assert_eq!(reading.rejected_rewrites, 1, "the shim must count the refusal"); + assert!( + reading.result.is_err(), + "a refused rewrite must fail the transaction, got {:?}", + reading.result, + ); +} + +fn call_tx(to: Address) -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(to).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// A contract that calls `target` with `value` wei and `gas`, then records whether the call +/// reported success. +/// +/// The recorded flag is what makes the split visible: it is the answer the *caller* was given, +/// which the state the call left behind has to agree with. +fn calls_and_records(target: Address, gas: u64, value: u128) -> Bytes { + BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(value) + .push_address(target) + .push_number(u128::from(gas)) + .append(CALL) + .push_u256(FLAG_SLOT) + .append(SSTORE) + .append(STOP) + .build() +} + +fn caller_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// A value-transferring `CALL` into an empty-code account, rewritten from its `Stop` into a +/// revert. +/// +/// `make_call_frame` transfers the value and commits its checkpoint before returning `Stop`, so +/// the transfer is already in the journal by the time any callback runs and no journal decision is +/// left to follow the rewrite. Honouring it tells the caller its transfer failed while the +/// recipient keeps the wei. +#[test] +fn test_rewriting_an_empty_code_call_into_a_revert_is_refused() { + let mut inspector = RewriteInitResult::new(EMPTY_TARGET, InstructionResult::Revert); + let reading = run( + caller_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)), + call_tx(CONTRACT), + &mut inspector, + ); + assert_eq!(inspector.fired, 1, "the fixture must reach the callback exactly once"); + assert!( + !reading.succeeded(), + "the rewrite was honoured: the caller recorded the transfer as {} and {EMPTY_TARGET} \ + holds {}", + reading.storage(CONTRACT, FLAG_SLOT), + reading.balance(EMPTY_TARGET), + ); + assert_refused(&reading); +} + +/// A value-transferring `CALL` into a precompile that cannot afford its own fee, rewritten from +/// its out-of-gas into a success. +/// +/// The split runs the other way: the precompile's failure made `make_call_frame` revert its +/// checkpoint, so the transfer is already rolled back. A success there tells the caller the +/// precompile was paid. +#[test] +fn test_reviving_a_failed_precompile_call_is_refused() { + // The 2,300 gas stipend a value-transferring call mints is under `ecrecover`'s 3,000 fee, so + // the precompile is reached and cannot pay. + let mut inspector = RewriteInitResult::new(ECRECOVER, InstructionResult::Stop); + let reading = + run(caller_db(calls_and_records(ECRECOVER, 0, SENT)), call_tx(CONTRACT), &mut inspector); + assert_eq!(inspector.fired, 1, "the fixture must reach the callback exactly once"); + assert!( + !reading.succeeded(), + "the rewrite was honoured: the caller recorded the call as {} and {ECRECOVER} holds {}", + reading.storage(CONTRACT, FLAG_SLOT), + reading.balance(ECRECOVER), + ); + assert_refused(&reading); +} + +/// A deterministic pre-EIP-155 keyless deployment transaction whose init code returns one byte of +/// runtime code, so the deployment it makes is visible in the produced state. +fn keyless_tx_bytes() -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit: 200_000, + to: TxKind::Create, + value: U256::ZERO, + // MSTORE8 a STOP at offset 0, then return that one byte as the runtime code. + input: BytecodeBuilder::default() + .push_number(u128::from(STOP)) + .push_number(0u64) + .append(0x53) // MSTORE8 + .push_number(1u64) + .push_number(0u64) + .append(RETURN) + .build(), + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + +/// The address the keyless transaction above deploys to, recovered from the receipt of an +/// unrewritten run. +fn deployed_address(reading: &Reading) -> Option
{ + let Ok(ExecutionResult::Success { output, .. }) = &reading.result else { return None }; + IKeylessDeploy::keylessDeployCall::abi_decode_returns(output.data()) + .ok() + .map(|returns| returns.deployedAddress) +} + +/// The `KeylessDeploy` interceptor's synthetic result, rewritten across the boundary. +/// +/// The interceptor runs a whole sandbox EVM and merges its state into the journal before it +/// returns, and it returns out of frame init, so there is no frame checkpoint the rewrite could +/// unwind. The deployment stands whatever the caller is told. +#[test] +fn test_rewriting_the_keyless_deploy_synthetic_result_is_refused() { + let deploy_tx = || { + let data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: keyless_tx_bytes(), + gasLimitOverride: U256::from(1_000_000u64), + } + .abi_encode(); + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(RELAYER) + .call(KEYLESS_DEPLOY_ADDRESS) + .chain_id(Some(1)) + .data(Bytes::from(data)) + .gas_limit(TX_GAS_LIMIT) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx + }; + let db = || MemoryDatabase::default().account_balance(RELAYER, U256::from(10 * ONE_ETH)); + + // The unrewritten run, which says where the deployment lands and that it lands at all. + let mut observer = RewriteInitResult::new(Address::ZERO, InstructionResult::Stop); + let plain = run(db(), deploy_tx(), &mut observer); + assert!(plain.succeeded(), "the fixture must deploy, got {:?}", plain.result); + let deployed = deployed_address(&plain).expect("the fixture must report a deployed address"); + assert!(plain.has_code(deployed), "the fixture must leave code at {deployed}"); + + let mut inspector = RewriteInitResult::new(KEYLESS_DEPLOY_ADDRESS, InstructionResult::Revert); + let reading = run(db(), deploy_tx(), &mut inspector); + assert_eq!(inspector.fired, 1, "the fixture must reach the callback exactly once"); + assert!( + !reading.has_code(deployed), + "the rewrite was honoured: the caller was told {:?} and {deployed} holds the sandbox's \ + deployed code anyway", + reading.result, + ); + assert_refused(&reading); +} + +/// Answers the frame itself and then moves the classification of its own answer. +/// +/// The near boundary of the refusal: this result also comes back out of frame init with no child +/// frame built, and it is not refused — because nothing in the EVM decided anything for it. No +/// checkpoint was opened and no state was written, so there is no journal decision for a later +/// rewrite to contradict. +#[derive(Debug)] +struct AnswerThenRewrite { + target: Address, + answered: u32, + rewrote: u32, +} + +impl Inspector for AnswerThenRewrite +where + CTX: ContextTr, + INTR: InterpreterTypes, +{ + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != self.target { + return None; + } + self.answered += 1; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != self.target || outcome.result.result.is_revert() { + return; + } + outcome.result.result = InstructionResult::Revert; + self.rewrote += 1; + } +} + +/// A frame the inspector answered itself, and then reclassified, is supported. +/// +/// The refusal is stated over what frame init *produced*, not over every result that reaches a +/// callback without a frame having run. An intercepting callback's own outcome is the inspector's +/// in whole — the EVM opened no checkpoint for it and wrote no state — so moving its +/// classification contradicts nothing, and is booked as an ordinary intervention. +#[test] +fn test_rewriting_an_inspector_s_own_synthetic_outcome_is_supported() { + let mut inspector = AnswerThenRewrite { target: CALLEE, answered: 0, rewrote: 0 }; + let reading = run( + caller_db(calls_and_records(CALLEE, 200_000, 0)).account_code( + CALLEE, + BytecodeBuilder::default().sstore(U256::from(1), U256::from(7)).stop().build(), + ), + call_tx(CONTRACT), + &mut inspector, + ); + assert_eq!(inspector.answered, 1, "the fixture must reach the answering callback once"); + assert_eq!(inspector.rewrote, 1, "and the rewriting one once"); + assert_eq!(reading.rejected_rewrites, 0, "an inspector's own answer is not refused"); + assert!(reading.succeeded(), "the transaction must still execute, got {:?}", reading.result); + assert_eq!( + reading.storage(CONTRACT, FLAG_SLOT), + U256::ZERO, + "the caller must be handed the classification the callback last wrote", + ); + assert_eq!( + reading.storage(CALLEE, U256::from(1)), + U256::ZERO, + "no frame ran, so there is no write for the rewrite to disagree with", + ); +} + +/// The same three rewrites under the frozen spec, which does not defend against them. +/// +/// REX6 is closed: what it replays includes whatever an inspector on that path produced, so the +/// refusal is REX7-only and this pins that it is. +#[test] +fn test_the_frozen_spec_refuses_nothing() { + let mut inspector = RewriteInitResult::new(EMPTY_TARGET, InstructionResult::Revert); + let mut db = caller_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX6) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6)); + 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(&mut inspector); + let outcome = evm.execute_transaction(call_tx(CONTRACT)).expect("REX6 must not refuse"); + assert_eq!( + evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites, + 0, + "a frozen spec refuses nothing", + ); + assert!(outcome.result_and_state.result.is_success(), "the frozen run must still succeed"); +} + +/// Silences the unused-import warning the external-env type would otherwise carry when only the +/// empty environment is used above. +#[allow(dead_code)] +fn _envs() -> TestExternalEnvs { + TestExternalEnvs::default() +} diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index b8d669fa..c5add5aa 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -15,17 +15,21 @@ //! //! - **A precompile's classification.** A precompile is answered inside the frame init and never //! becomes a child frame, so its recording site is the only place that knows the forwarded -//! envelope and the work performed. It is not, however, the place that knows how the call ends: -//! `call_end` runs afterwards and can rewrite the classification, and the classification is what -//! decides whether the caller reclaims the remainder. So the split has to be settled at the -//! frame's settlement point, from what the recording site staged, exactly as an ordinary frame's -//! is. +//! envelope and the work performed. The split is nonetheless settled at the frame's settlement +//! point, from what that site staged, exactly as an ordinary frame's is — because a callback runs +//! in between, and the classification is what decides whether the caller reclaims the remainder. +//! What that callback may do to the classification is bounded: the journal decision behind a +//! result frame init produced was taken before any callback ran and is not reachable from one, so +//! a rewrite that moves such a result across the success / revert / halt boundary is refused and +//! the settlement reads the classification the EVM produced. The cases below pin the uninspected +//! split each precompile arm produces, and the refusal that keeps it the one the settlement sees. //! //! Every case here is checked by the identity `common::finish` runs on every transaction: the //! tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. use crate::common::{ - transact, transact_inspected, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, + transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, CALLEE, CALLER, + CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, }; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ @@ -63,9 +67,6 @@ const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); /// KZG point evaluation. const KZG: Address = address!("000000000000000000000000000000000000000a"); -/// What the identity precompile charges for an empty input: its base cost, with no words to copy. -const IDENTITY_GAS: u64 = 15; - fn limits() -> EvmTxRuntimeLimits { EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) } @@ -294,95 +295,86 @@ fn kzg_verification_failure() -> Vec { input } -fn run_reclassified( - target: Address, - calldata: &[u8], - to: InstructionResult, -) -> (Outcome, Outcome, u32) { +/// Runs the fixture twice: once uninspected, and once with the classification rewritten across +/// the boundary the shim refuses. +/// +/// The refusal is asserted here rather than in each case, so every case below is left stating the +/// one thing that differs between them — which arm of the precompile it reaches, and what the +/// uninspected run's split therefore is. +fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { let code = call_precompile(target, calldata); let plain = transact(MegaSpecId::REX7, db(code.clone()), limits()); let mut inspector = Reclassifier::new(target, to); - let rewritten = transact_inspected(MegaSpecId::REX7, db(code), limits(), &mut inspector); - (plain, rewritten, inspector.fired) + let refusal = transact_inspected_refused(MegaSpecId::REX7, db(code), limits(), &mut inspector); + assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); + assert!( + refusal.error.contains("classification of a result frame init produced"), + "the transaction must fail with the refusal's own reason, got {}", + refusal.error, + ); + (plain, refusal) } -/// A successful precompile rewritten into a halt destroys the rest of its forwarded envelope, and -/// the transaction has to report it. +/// A successful precompile rewritten into a halt is refused, and the uninspected run destroys +/// nothing. /// -/// The caller reclaims nothing from a halted call, so everything the identity precompile did not -/// charge for is gone. Its recording site booked a destroyed remainder of zero, because at that -/// moment the call had succeeded. +/// The rewrite is the direction with state behind it: `make_call_frame` commits the checkpoint +/// before it returns a successful precompile's result, so a caller told the call halted would be +/// told so with the transfer that funded it standing. #[test] -fn test_a_precompile_rewritten_into_a_halt_destroys_its_remainder() { - let (plain, rewritten, fired) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); +fn test_rewriting_a_successful_precompile_into_a_halt_is_refused() { + let (plain, _) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); - assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); assert_eq!( - rewritten.destroyed, - FORWARDED - IDENTITY_GAS, - "everything the precompile did not spend is destroyed once the call halts", - ); - assert_eq!( - rewritten.enforced(), + plain.compute_gas, plain.enforced(), - "the same work ran either way, so the enforcing lane must not move", - ); - assert_eq!( - rewritten.compute_gas, - plain.compute_gas + rewritten.destroyed, - "the destroyed remainder is reported on top of the work performed", + "with nothing destroyed the reported total is the work performed", ); } -/// A halted precompile rewritten into a success destroys nothing, because the caller reclaims the -/// envelope its recording site had already written off. +/// A rejected precompile rewritten into a success is refused, and the uninspected run destroys the +/// whole envelope. +/// +/// The other direction, and the other half of the split: `blake2f` rejects the input before any +/// work, so `make_call_frame` reverted the checkpoint and nothing was performed. #[test] -fn test_a_precompile_rewritten_into_a_success_destroys_nothing() { - let (plain, rewritten, fired) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); +fn test_reviving_a_rejected_precompile_is_refused() { + let (plain, _) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); - assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); assert_eq!( plain.destroyed, FORWARDED, "blake2f rejects the input before any work, so the uninspected run destroys all of it", ); - assert_eq!(rewritten.destroyed, 0, "a reclaimed envelope is not a destroyed one"); assert_eq!( - rewritten.compute_gas, - rewritten.enforced(), - "with nothing destroyed the reported total is the work performed", + plain.enforced(), + plain.compute_gas - plain.destroyed, + "nothing was performed, so nothing enforces", ); } -/// The corner where the two halves of the split move in opposite directions: a KZG failure that -/// `MegaETH` prices as work, rewritten into a success. +/// The third arm, and the only one whose failure `MegaETH` prices as work: a KZG verification that +/// ran and rejected. /// -/// The fixed fee really was performed and stays on the enforcing lane. But the halt's gas object -/// carries the whole forwarded envelope as remaining — a halting precompile's gas is reset rather -/// than spent down — so a caller told the call succeeded reclaims all of it, including the fee. -/// That fee is then gas the execution priced and the envelope never paid: conjured gas, which the -/// ledger has to carry or the law reads the transaction as having spent less than it did. +/// The refusal matters most here. A halting precompile's gas object carries the whole forwarded +/// envelope as remaining — it is reset rather than spent down — so a caller told such a call +/// succeeded would reclaim all of it, the fixed fee included. That fee is gas the execution priced +/// and the envelope never paid, which is exactly the shape the refusal keeps out. #[test] -fn test_a_priced_precompile_failure_rewritten_into_a_success_conjures_its_fee() { +fn test_reviving_a_priced_precompile_failure_is_refused() { let calldata = kzg_verification_failure(); - let (plain, rewritten, fired) = run_reclassified(KZG, &calldata, InstructionResult::Stop); + let (plain, _) = run_reclassified(KZG, &calldata, InstructionResult::Stop); - assert_eq!(fired, 1, "the fixture must reach the precompile's call_end exactly once"); assert_eq!( plain.destroyed, FORWARDED - kzg_point_evaluation::GAS_COST, "verification ran, so the uninspected run destroys the envelope less the fixed fee", ); - assert_eq!(rewritten.destroyed, 0, "a reclaimed envelope is not a destroyed one"); assert_eq!( - rewritten.enforced(), + plain.compute_gas - plain.destroyed, plain.enforced(), - "the verification work is the same on both runs", - ); - assert_eq!( - rewritten.inspector_conjured_gas, - i128::from(kzg_point_evaluation::GAS_COST), - "the fee the caller reclaimed is gas the transaction was never charged for", + "the fee is the work performed, and it is what enforces", ); } diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 4b46235a..6592886f 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -106,6 +106,7 @@ mod detention_window; mod double_exceed_corner; mod exceptional_halt; mod frame_init_reject_burn; +mod frame_init_result_rewrite; mod frame_loop_parity; mod gas_clamp; mod gas_leakage; From c63e6b93566eb3f69e1bf9c529da27af6ad3bf9c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 15:59:16 +0800 Subject: [PATCH 161/208] test(rex7): drive the frame-loop matrix through a rewriting inspector The matrix ran on `NoOpInspector` alone, which is the wrong tool for one question: an inspector that changes nothing cannot tell a result the frame loops settled from one frame init produced, because both arrive at the same callback holding the same type. The difference is in what stands behind them, and only a rewrite makes it visible. Each case now declares which of the two its first completed frame is, and a second pass drives every one of them through an inspector that moves the classification across the boundary: a running frame's rewrite is followed, an init-produced one's is refused, and nothing else decides which. A third pass runs the same rewriting inspector with the inspected loops switched off and requires the uninspected run bit for bit, which is what says the window and the comparison live entirely inside the inspected path. --- .../mega-evm/tests/rex7/frame_loop_parity.rs | 253 +++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/tests/rex7/frame_loop_parity.rs b/crates/mega-evm/tests/rex7/frame_loop_parity.rs index c847a435..b2496544 100644 --- a/crates/mega-evm/tests/rex7/frame_loop_parity.rs +++ b/crates/mega-evm/tests/rex7/frame_loop_parity.rs @@ -15,6 +15,15 @@ //! `0xEF`-prefixed, unaffordable deposit, reverted constructor, occupied address), a call frame's //! three outcomes, the frame inits that refuse to build a frame at all, a precompile, and the two //! suspension shapes that must settle nothing. +//! +//! An observation-only inspector is the wrong tool for one question, though, and the same matrix +//! answers it with a rewriting one. Whether a frame's result came out of a frame that *ran* or out +//! of frame init decides whether a classification rewrite can be followed at all: the running +//! frame's journal decision is withheld until after the last callback, and the init-produced one's +//! was taken before the first. Each case therefore also declares which of the two its first +//! completed frame is, and [`test_a_classification_rewrite_is_followed_or_refused_by_that_alone`] +//! drives every one of them through an inspector that moves the classification across the +//! boundary. use crate::common::{CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}; use alloy_primitives::{address, Address, Bytes, B256, U256}; @@ -26,10 +35,13 @@ use mega_evm::{ }; use revm::{ bytecode::opcode::{CALL, CREATE, INVALID, MSTORE8, PUSH0, RETURN, REVERT, STATICCALL, STOP}, - context::{tx::TxEnvBuilder, CfgEnv}, + context::{tx::TxEnvBuilder, CfgEnv, ContextTr}, + handler::{EvmTr, FrameResult}, inspector::NoOpInspector, + interpreter::{FrameInput, InstructionResult, InterpreterTypes}, primitives::TxKind, state::EvmState, + Inspector, }; use std::{collections::BTreeMap, string::String}; @@ -140,6 +152,21 @@ struct Case { /// Asserted against the plain run, so a case that stops reaching its shape fails loudly /// instead of comparing two runs of something else. expect: fn(&Reading), + /// Where this case's first completed frame result comes from, which is the whole of what + /// decides whether a classification rewrite of it can be followed. + origin: Origin, +} + +/// Where a frame result was produced, for the rewrite the second matrix applies to it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Origin { + /// A frame ran and reached its own settlement point. Its journal decision is parked until + /// after the last callback, so a rewrite is followed and the state follows it. + FrameRan, + /// Frame init produced the result without ever building a frame — an empty-code call, a + /// precompile, a refusal upstream or `MegaETH` declined to build. The journal decision behind + /// it was taken before any callback ran, so a rewrite is refused. + FrameInit, } fn base_db() -> MemoryDatabase { @@ -218,6 +245,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameRan, }, Case { name: "CALL reverts and its storage write is rolled back", @@ -236,6 +264,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameRan, }, Case { name: "CALL halts on INVALID and destroys its forwarded budget", @@ -252,6 +281,7 @@ fn cases() -> Vec { assert_success(r); assert_destroyed(r); }, + origin: Origin::FrameRan, }, Case { name: "CALL into empty code stops without a frame", @@ -262,6 +292,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameInit, }, Case { name: "CALL is refused for want of balance", @@ -273,6 +304,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameInit, }, Case { name: "STATICCALL reaches a precompile, which returns without a frame", @@ -296,6 +328,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameInit, }, Case { name: "CREATE deposits its code", @@ -306,6 +339,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameRan, }, Case { name: "CREATE is rejected for an oversized runtime code", @@ -319,6 +353,7 @@ fn cases() -> Vec { assert_halt_reason(r, "CreateContractSizeLimit"); assert_destroyed(r); }, + origin: Origin::FrameRan, }, Case { name: "CREATE is rejected for an 0xEF-prefixed runtime code", @@ -332,6 +367,7 @@ fn cases() -> Vec { assert_halt_reason(r, "CreateContractStartingWithEF"); assert_destroyed(r); }, + origin: Origin::FrameRan, }, Case { name: "CREATE runs out of gas paying for its own code", @@ -344,6 +380,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: 150_000, expect: |r| assert_halt_reason(r, "OutOfGas"), + origin: Origin::FrameRan, }, Case { name: "CREATE's constructor reverts", @@ -354,6 +391,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_revert, + origin: Origin::FrameRan, }, Case { name: "CREATE onto an occupied address collides", @@ -380,6 +418,7 @@ fn cases() -> Vec { assert_success(r); assert_destroyed(r); }, + origin: Origin::FrameInit, }, Case { name: "a nested CALL suspends its caller without settling it", @@ -393,6 +432,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_success, + origin: Origin::FrameInit, }, Case { name: "the top-level frame itself halts", @@ -403,6 +443,7 @@ fn cases() -> Vec { code_size_limit: None, gas_limit: TX_GAS_LIMIT, expect: assert_revert, + origin: Origin::FrameRan, }, ] } @@ -427,6 +468,216 @@ fn test_both_frame_loops_agree_on_every_frame_outcome() { } } +/// Moves the classification of the first frame result it is handed across the success / revert / +/// halt boundary, once. +/// +/// It fires at the generic `frame_end`, which revm runs after the variant-specific callback over +/// the same result — so one frame receives exactly one rewrite whatever shape it has, and the +/// count below is the number of frames rewritten rather than the number of callbacks reached. +#[derive(Debug, Default)] +struct MoveTheClassification { + fired: u32, +} + +/// The class a result is moved *to*, which is any class but its own. +fn across_the_boundary(from: InstructionResult) -> InstructionResult { + if from.is_ok() { + InstructionResult::Revert + } else if from.is_revert() { + InstructionResult::OutOfGas + } else { + InstructionResult::Revert + } +} + +impl Inspector for MoveTheClassification { + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + if self.fired > 0 { + return; + } + self.fired += 1; + let result = frame_result.interpreter_result_mut(); + result.result = across_the_boundary(result.result); + } +} + +/// What one rewritten run came to. +#[derive(Debug, PartialEq, Eq)] +enum Rewritten { + /// The transaction produced a receipt, and the shim refused nothing. + Followed, + /// The shim restored the classification and failed the transaction. + Refused, +} + +/// Runs `case` under REX7 with the rewriting inspector attached, and says which way it came out. +fn run_rewritten(case: &Case) -> Rewritten { + let mut db = (case.db)(); + let mut cfg = CfgEnv::default(); + cfg.spec = MegaSpecId::REX7; + cfg.limit_contract_code_size = Some(case.code_size_limit.unwrap_or(MAX_CONTRACT_SIZE)); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(CALLER) + .kind(case.kind) + .data(case.data.clone()) + .value(case.value) + .gas_limit(case.gas_limit) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + + let mut inspector = MoveTheClassification::default(); + let (outcome, ledger) = { + let mut evm = MegaEvm::new(context).with_inspector(&mut inspector); + let outcome: Result = evm.execute_transaction(tx); + let ledger = evm.ctx_ref().additional_limit.borrow().inspector_ledger(); + (outcome, ledger) + }; + assert_eq!( + inspector.fired, 1, + "{}: the rewriting inspector must reach exactly one frame result", + case.name, + ); + match outcome { + Ok(_) => { + assert_eq!( + ledger.rejected_rewrites, 0, + "{}: a followed rewrite refuses nothing", + case.name, + ); + assert!( + ledger.interventions > 0, + "{}: a followed rewrite must still be booked as an intervention", + case.name, + ); + Rewritten::Followed + } + Err(_) => { + assert_eq!( + ledger.rejected_rewrites, 1, + "{}: a refused rewrite is counted exactly once", + case.name, + ); + Rewritten::Refused + } + } +} + +/// Whether a classification rewrite is followed or refused is decided by where the result came +/// from, and by nothing else. +/// +/// This is the case the observation-only matrix above cannot reach: an inspector that changes +/// nothing cannot tell a result the frame loops settled from one frame init produced, because both +/// arrive at the same callback holding the same type. The difference is in what stands behind +/// them, and only a rewrite makes it visible — a running frame's journal decision is still +/// outstanding and follows the rewrite, while an init-produced result's was taken inside +/// `make_call_frame` or inside an interceptor before the callback existed. +/// +/// Every case of the matrix runs here, so the split is stated over the whole frame lifecycle +/// rather than over the three shapes that happen to have their own fixtures. +#[test] +fn test_a_classification_rewrite_is_followed_or_refused_by_that_alone() { + for case in cases() { + let expected = match case.origin { + Origin::FrameRan => Rewritten::Followed, + Origin::FrameInit => Rewritten::Refused, + }; + assert_eq!( + run_rewritten(&case), + expected, + "{}: a {:?} result must be {expected:?}", + case.name, + case.origin, + ); + } +} + +/// The matrix covers both origins, so the test above is a comparison rather than a restatement of +/// one verdict. +#[test] +fn test_the_matrix_reaches_both_frame_origins() { + let cases = cases(); + for origin in [Origin::FrameRan, Origin::FrameInit] { + assert!( + cases.iter().any(|case| case.origin == origin), + "the matrix must contain a {origin:?} case", + ); + } +} + +/// The two-loop comparison for the rewriting inspector: with the inspected loops switched off, the +/// same inspector is handed no callback and the run is the uninspected one, bit for bit. +/// +/// This is what says the refusal and the marker that drives it live entirely inside the inspected +/// path — that neither the window `inspect_frame_init` opens nor the comparison the shim makes in +/// it can move a transaction the plain loops ran. +#[test] +fn test_a_rewriting_inspector_with_the_loops_switched_off_changes_nothing() { + for case in cases() { + let plain = run_under(&case, MegaSpecId::REX7, false); + let mut db = (case.db)(); + let mut cfg = CfgEnv::default(); + cfg.spec = MegaSpecId::REX7; + cfg.limit_contract_code_size = Some(case.code_size_limit.unwrap_or(MAX_CONTRACT_SIZE)); + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let mut tx = MegaTransaction::new( + TxEnvBuilder::default() + .caller(CALLER) + .kind(case.kind) + .data(case.data.clone()) + .value(case.value) + .gas_limit(case.gas_limit) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + + let mut inspector = MoveTheClassification::default(); + let outcome: MegaTransactionOutcome = { + let mut evm = MegaEvm::new(context).with_inspector(&mut inspector); + alloy_evm::Evm::set_inspector_enabled(&mut evm, false); + evm.execute_transaction(tx).expect("tx should not surface EVMError") + }; + assert_eq!(inspector.fired, 0, "{}: no callback may run", case.name); + let switched_off = Reading { + result: std::format!("{:?}", outcome.result_and_state.result), + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, + gas_used: outcome.result_and_state.result.tx_gas_used(), + total_gas_spent: outcome.result_and_state.result.gas().total_gas_spent(), + state: render_state(&outcome.result_and_state.state), + }; + assert_eq!( + plain, switched_off, + "{}: a rewriting inspector with no callbacks must change nothing", + case.name, + ); + } +} + /// The same matrix under the frozen spec the REX7 loops share their body with. /// /// The loops are not spec-gated — only where they take the journal decision is — so a settlement From 44ccdcef27d37d5ba6d542344304beb2b6324d9a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:01:27 +0800 Subject: [PATCH 162/208] feat(state-test): draw the refused init-result rewrite, and count it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A refusal is the one legitimate way an inspector can stop a transaction that would otherwise execute, so `CHAOS_REJECTED` — which means the two runs disagreed about whether the transaction executes at all — is the wrong verdict for it. `REFUSED` is the right one, and it does not fail the sweep. With that verdict in place the shape can be drawn rather than withheld: `move_init_result_class` moves the classification of a result frame init produced, and fires only where such a result is what the callback is holding, which `FrameResultOriginTr` is what tells it. `fail_frame` and `revive_call` reach the same refusal whenever they land on one, and are counted the same way. The four interception shapes do not: a result an inspector answered a frame with is its own, so reclassifying it is supported and the two halves of the pool stay composable. A declined run reports nothing through the ordinary return, so the chaos tally now travels out of `execute_unit_reporting_chaos` whether or not the run produced a receipt. --- crates/mega-state-test/src/chaos.rs | 164 ++++++++++++++++----- crates/mega-state-test/src/diff.rs | 22 ++- crates/mega-state-test/tests/chaos_mode.rs | 81 +++++++++- crates/state-test/src/main.rs | 3 +- 4 files changed, 230 insertions(+), 40 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 6853f6cc..ecb08045 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -36,33 +36,51 @@ //! corpus produce the same mutations on any machine, in any thread count, in any order — so a //! flagged vector comes with everything needed to re-run exactly it. //! -//! # What the pool leaves out +//! # What the pool leaves out, and the one refusal it draws on purpose //! -//! One rewrite shape is missing on purpose: turning a *failed contract creation* into a successful -//! one. The shim refuses that shape and asserts on it, deliberately — by the time `create_end` -//! runs, the journal has been reverted and no code was deposited, so a success there reports a -//! deployment that did not happen, and a corpus that produces it should stop rather than quietly -//! take the rejection path. Including it here would make the detector's own firing the sweep's -//! dominant result. The refusal is pinned end-to-end by the two tests named in +//! One rewrite shape is missing: turning a *failed contract creation* into a successful one. The +//! shim refuses that shape and asserts on it, deliberately — by the time `create_end` runs, the +//! journal has been reverted and no code was deposited, so a success there reports a deployment +//! that did not happen, and a corpus that produces it should stop rather than quietly take the +//! rejection path. Including it here would make the detector's own firing the sweep's dominant +//! result. The refusal is pinned end-to-end by the two tests named in //! `tests/rex7/inspector_cheat_matrix.rs`'s `inapplicable` table instead. +//! +//! The other refused shape *is* in the pool: [`ChaosShape::MoveInitResultClass`], which moves the +//! classification of a result frame init produced. It is drawn rather than withheld because the +//! shim answers it by declining the transaction rather than by asserting, and a decline is +//! something a sweep can count — [`ChaosClass::Refused`] is that count. So the corpus exercises +//! the refusal at scale instead of leaving it to the fixtures that reach it on purpose, and the +//! number says how much of the corpus its draws actually land on. The two general shapes +//! `FailFrame` and `ReviveCall` reach the same refusal whenever they happen to land on an +//! init-produced result, and are counted the same way. +//! +//! The four interception shapes do not, and that is a boundary rather than an omission: a result +//! an inspector answered a frame with is the inspector's in whole, with no checkpoint opened and +//! no state written behind it, so rewriting its classification contradicts nothing and is +//! supported. A draw that intercepts a frame and then reclassifies its own answer therefore +//! executes, which is what keeps the two halves of the pool composable. use crate::{ - diff::{compare, execute_unit_in_mode, RunMode}, + diff::{compare, execute_unit_in_mode, execute_unit_reporting_chaos, RunMode}, panic_capture, runner::{is_skipped_fixture, skip_test, vector_label, FixtureScan, TestError, TestErrorKind}, types::{SpecName, TestSuite, TestUnit, TxPartIndices}, }; use indicatif::{ProgressBar, ProgressDrawTarget}; -use mega_evm::revm::{ - context::{Cfg, ContextTr, JournalTr}, - handler::FrameResult, - inspector::Inspector, - interpreter::{ - interpreter_types::{Jumps, LoopControl, MemoryTr, ReturnData, StackTr}, - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, +use mega_evm::{ + revm::{ + context::{Cfg, ContextTr, JournalTr}, + handler::FrameResult, + inspector::Inspector, + interpreter::{ + interpreter_types::{Jumps, LoopControl, MemoryTr, ReturnData, StackTr}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, + InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, + }, + primitives::{Address, Bytes, Log, U256}, }, - primitives::{Address, Bytes, Log, U256}, + FrameResultOriginTr, FORBIDDEN_CREATE_REVIVAL, FORBIDDEN_FRAME_INIT_REWRITE, }; use std::{ collections::BTreeMap, @@ -224,11 +242,15 @@ pub enum ChaosShape { /// A return buffer put in front of the frame, so its `RETURNDATASIZE` and `RETURNDATACOPY` /// read data no call produced. RewriteReturnData, + /// The classification of a result *frame init* produced, moved across the success / revert / + /// halt boundary. The shim refuses this one, so the run it lands in is declined rather than + /// executed — which is the verdict [`ChaosClass::Refused`] names. + MoveInitResultClass, } impl ChaosShape { /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 27] = [ + pub const ALL: [Self; 28] = [ Self::InjectGas, Self::DrainGas, Self::EditFrameState, @@ -256,6 +278,7 @@ impl ChaosShape { Self::CancelRefundEdit, Self::SkipOpcode, Self::RewriteReturnData, + Self::MoveInitResultClass, ]; /// The shape a label names. @@ -302,6 +325,7 @@ impl ChaosShape { Self::CancelRefundEdit => "cancel_refund_edit", Self::SkipOpcode => "skip_opcode", Self::RewriteReturnData => "rewrite_return_data", + Self::MoveInitResultClass => "move_init_result_class", } } @@ -344,7 +368,10 @@ impl ChaosShape { // unless the frame is still running, and a rewritten return buffer always gets a // length the current one does not have. Self::SkipOpcode | - Self::RewriteReturnData + Self::RewriteReturnData | + // The rewrite comparison books it before the shim refuses it, and the refusal is + // counted beside that — so the ledger carries two reasons to be non-zero. + Self::MoveInitResultClass ) } } @@ -393,7 +420,7 @@ const INPUT_SHAPES: [ChaosShape; 9] = [ ]; /// Shapes reachable from a callback that holds a finished frame's result. -const RESULT_SHAPES: [ChaosShape; 11] = [ +const RESULT_SHAPES: [ChaosShape; 12] = [ ChaosShape::RaiseResultGas, ChaosShape::LowerResultGas, ChaosShape::FailFrame, @@ -405,6 +432,7 @@ const RESULT_SHAPES: [ChaosShape; 11] = [ ChaosShape::WriteStateGas, ChaosShape::MoveOutcomeMetadata, ChaosShape::CancelRefundEdit, + ChaosShape::MoveInitResultClass, ]; /// Which mutations a chaos run is allowed to make. @@ -824,13 +852,19 @@ impl ChaosInspector { /// Applies a result-facing shape to a finished frame's result. /// - /// `is_creation` withholds the one shape the shim refuses: a failed contract creation rewritten - /// into a success. The pool never offers it, so a creation drawing `ReviveCall` leaves the - /// result alone and spends no budget. + /// `is_creation` withholds the one shape the shim refuses with an assertion: a failed contract + /// creation rewritten into a success. The pool never offers it, so a creation drawing + /// `ReviveCall` leaves the result alone and spends no budget. + /// + /// `is_frame_init` is the other way round — it is what *arms* + /// [`ChaosShape::MoveInitResultClass`], which is only that shape when the result it lands on + /// came out of frame init. A draw for it anywhere else leaves the result alone, so the shape's + /// tally counts refusals reached rather than draws made. fn hit_result( &mut self, result: &mut InterpreterResult, is_creation: bool, + is_frame_init: bool, shape: ChaosShape, entropy: u64, ) { @@ -873,12 +907,33 @@ impl ChaosInspector { result.gas.record_refund(amount); self.pending_refund = amount; } + ChaosShape::MoveInitResultClass => { + if !is_frame_init { + return; + } + result.result = across_the_class_boundary(result.result); + } _ => return, } self.applied(shape); } } +/// The class a result is moved *to*, which is any class but its own. +/// +/// Stated over all three so the shape reaches every arm of frame init, not only the ones that +/// return successfully: an empty-code call and a precompile come back `Stop`, a refusal comes back +/// a halt, and `MegaETH`'s own frame-local exceed comes back a revert. +const fn across_the_class_boundary(from: InstructionResult) -> InstructionResult { + if from.is_ok() { + InstructionResult::Revert + } else if from.is_revert() { + InstructionResult::OutOfGas + } else { + InstructionResult::Revert + } +} + /// Grows the frame's memory and moves the memo of how far it has been paid for with it, returning /// whether anything moved. /// @@ -1091,7 +1146,9 @@ fn write_journal(context: &mut CTX, entropy: u64) { context.journal_mut().tstore(CHAOS_ADDRESS, U256::from(CHAOS_SLOT), U256::from(entropy)); } -impl Inspector for ChaosInspector { +impl Inspector + for ChaosInspector +{ fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { self.settle_pending_gas(interp); if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { @@ -1147,8 +1204,9 @@ impl Inspector for ChaosInspe self.hit_create_inputs(context, inputs, shape, entropy) } - fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { + fn call_end(&mut self, context: &mut CTX, _inputs: &CallInputs, outcome: &mut CallOutcome) { self.settle_pending_refund(&mut outcome.result.gas); + let is_frame_init = context.is_frame_init_result(); let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; if shape == ChaosShape::MoveOutcomeMetadata { if shrink_return_range(outcome) { @@ -1156,16 +1214,17 @@ impl Inspector for ChaosInspe } return; } - self.hit_result(&mut outcome.result, false, shape, entropy); + self.hit_result(&mut outcome.result, false, is_frame_init, shape, entropy); } fn create_end( &mut self, - _context: &mut CTX, + context: &mut CTX, _inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { self.settle_pending_refund(&mut outcome.result.gas); + let is_frame_init = context.is_frame_init_result(); let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; if shape == ChaosShape::MoveOutcomeMetadata { if relabel_deployment(outcome) { @@ -1173,16 +1232,17 @@ impl Inspector for ChaosInspe } return; } - self.hit_result(&mut outcome.result, true, shape, entropy); + self.hit_result(&mut outcome.result, true, is_frame_init, shape, entropy); } fn frame_end( &mut self, - _context: &mut CTX, + context: &mut CTX, _frame_input: &FrameInput, frame_result: &mut FrameResult, ) { self.settle_pending_refund(frame_result.gas_mut()); + let is_frame_init = context.is_frame_init_result(); let Some((shape, entropy)) = self.pick(&RESULT_SHAPES) else { return }; if shape == ChaosShape::MoveOutcomeMetadata { let moved = match frame_result { @@ -1195,7 +1255,13 @@ impl Inspector for ChaosInspe return; } let is_creation = matches!(frame_result, FrameResult::Create(_)); - self.hit_result(frame_result.interpreter_result_mut(), is_creation, shape, entropy); + self.hit_result( + frame_result.interpreter_result_mut(), + is_creation, + is_frame_init, + shape, + entropy, + ); } } @@ -1237,8 +1303,20 @@ pub enum ChaosClass { ControlDrift, /// The rewriting run and the reference disagreed about whether the transaction executes at /// all: one produced a receipt and the other an `EVMError`. No inspector callback runs before - /// validation, so the two cannot legitimately differ here. + /// validation, so the two cannot legitimately differ here — with the one exception the verdict + /// below names. ChaosRejected, + /// The rewriting run was declined because the measurement shim *refused* one of its rewrites. + /// + /// The one legitimate way an inspector can stop a transaction that would otherwise execute, + /// and the designed outcome of two rewrite shapes rather than a defect: moving the + /// classification of a result frame init produced, and reviving a failed contract creation. + /// Both leave the caller an answer the state behind it contradicts, so the shim restores the + /// classification and fails the transaction rather than let a receipt be built on it. + /// + /// Counted rather than passed, because the number is worth seeing: it says how much of the + /// corpus the pool's [`ChaosShape::MoveInitResultClass`] draws actually reach. + Refused, /// The rewriting run applied a mutation the shim is contracted to book unconditionally — see /// [`ChaosShape::is_always_booked`] — and still ended with an all-zero ledger. /// @@ -1262,6 +1340,7 @@ impl ChaosClass { Self::Pass => "PASS", Self::ControlDrift => "CONTROL_DRIFT", Self::ChaosRejected => "CHAOS_REJECTED", + Self::Refused => "REFUSED", Self::LedgerBlind => "LEDGER_BLIND", Self::Skipped => "SKIPPED", Self::Panic => "PANIC", @@ -1352,8 +1431,14 @@ pub fn chaos_unit( } } - let chaos = execute_unit_in_mode(unit, indexes, spec, RunMode::Chaos { seed, filter }); - let applied = chaos.as_ref().ok().and_then(|run| run.chaos.clone()).unwrap_or_default(); + let mut applied = ChaosTally::default(); + let chaos = execute_unit_reporting_chaos( + unit, + indexes, + spec, + RunMode::Chaos { seed, filter }, + &mut applied, + ); let (class, detail) = match (reference.is_ok(), &chaos) { (true, Ok(run)) => match blind_shapes(&applied, run.ledger.is_zero()) { @@ -1370,6 +1455,10 @@ pub fn chaos_unit( // unsupported transaction shape — and declined it the same way with the inspector // attached. Nothing executed, so nothing was tested; counted rather than passed. (false, Err(_)) => (ChaosClass::Skipped, None), + // A decline the shim itself produced is the designed outcome of a refused rewrite, not a + // disagreement about whether the transaction executes. It is told apart by the reason the + // error carries, which is the shim's own message. + (true, Err(e)) if is_refusal(e) => (ChaosClass::Refused, Some(e.to_string())), (true, Err(e)) => ( ChaosClass::ChaosRejected, Some(format!("the rewriting run was declined where the reference executed: {e}")), @@ -1382,6 +1471,15 @@ pub fn chaos_unit( ChaosVerdict { class, applied, detail } } +/// Whether a decline is one the measurement shim produced by refusing a rewrite. +/// +/// Read off the reason the error carries, against the shim's own message constants, so a message +/// that changes changes here too rather than silently reclassifying a whole corpus. +fn is_refusal(error: &TestErrorKind) -> bool { + let rendered = error.to_string(); + rendered.contains(FORBIDDEN_FRAME_INIT_REWRITE) || rendered.contains(FORBIDDEN_CREATE_REVIVAL) +} + /// The shapes a run applied that the shim must have booked, when its ledger says it booked /// nothing at all. /// diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index a2fbbaa1..8e47c7eb 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -46,7 +46,7 @@ //! over-reports instead of granting an exemption on the strength of bytes the fixture chose. use crate::{ - chaos::{CallbackCounter, ChaosInspector, ShapeFilter}, + chaos::{CallbackCounter, ChaosInspector, ChaosTally, ShapeFilter}, panic_capture, runner::{ configure_max_blobs, execution_status, external_envs_for, find_all_json_tests, halt_reason, @@ -859,6 +859,22 @@ pub fn execute_unit_in_mode( indexes: TxPartIndices, spec: &SpecName, mode: RunMode, +) -> Result { + execute_unit_reporting_chaos(unit, indexes, spec, mode, &mut ChaosTally::default()) +} + +/// [`execute_unit_in_mode`], with the chaos run's tally written to `chaos_out` whether or not the +/// run produced a receipt. +/// +/// The ordinary return carries the tally inside [`UnitExecution`], which a run that did not +/// execute never reaches — and a run the measurement shim refused is exactly such a run. What it +/// mutated is the whole of what it has to report, so it cannot travel on the success path. +pub fn execute_unit_reporting_chaos( + unit: &TestUnit, + indexes: TxPartIndices, + spec: &SpecName, + mode: RunMode, + chaos_out: &mut ChaosTally, ) -> Result { let mut cfg = CfgEnv::default(); // See `execute_test_suite`: revm-27 chain-id gate-off (revm 40 default is true). @@ -909,7 +925,9 @@ pub fn execute_unit_in_mode( MegaEvm::new(evm_context).with_inspector(ChaosInspector::new(seed, filter)); let executed = evm.execute_transaction(megatx); let inner = evm.into_inner(); - chaos_tally = Some(inner.inspector.tally()); + let tally = inner.inspector.tally(); + *chaos_out = tally.clone(); + chaos_tally = Some(tally); (executed, None, inner.ctx) } RunMode::Plain => { diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index b7e33a05..99689b67 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -6,11 +6,13 @@ //! narrowing the shape filter narrows and nothing else, that the read-only control is read-only, //! and that a sweep which mutated nothing fails its own gate rather than reporting a clean corpus. +use mega_evm::FORBIDDEN_FRAME_INIT_REWRITE; use state_test::{ chaos::{ - chaos_unit, run_chaos, vector_seed, ChaosClass, ChaosRunConfig, ChaosShape, ShapeFilter, + chaos_unit, run_chaos, vector_seed, ChaosClass, ChaosRunConfig, ChaosShape, ChaosTally, + ShapeFilter, }, - diff::{execute_unit_in_mode, RunMode}, + diff::{execute_unit_in_mode, execute_unit_reporting_chaos, RunMode}, runner::FixtureScan, types::{SpecName, TestUnit, TxPartIndices}, }; @@ -19,6 +21,9 @@ use std::path::PathBuf; const SENDER: &str = "0x1000000000000000000000000000000000000001"; const CALLEE: &str = "0x2000000000000000000000000000000000000002"; const INNER: &str = "0x3000000000000000000000000000000000000003"; +/// An address with no code and no `pre` entry, so a `CALL` to it comes back out of frame init +/// without a frame ever being built. +const EMPTY: &str = "0x4000000000000000000000000000000000000004"; /// The single transaction vector these hand-built fixtures declare. const VECTOR_0: TxPartIndices = TxPartIndices { data: 0, gas: 0, value: 0 }; @@ -92,6 +97,19 @@ fn refunding_unit() -> TestUnit { serde_json::from_value(unit_json_with(refunding_callee_code())).expect("valid unit json") } +/// [`callee_code`] pointed at an account with no code, so its `CALL` returns out of frame init +/// with no child frame ever built. +fn empty_target_callee_code() -> String { + format!("0x600160015560006000600060006000 73{} 612710 f1 50 60006000a000", &EMPTY[2..]) + .replace(' ', "") +} + +/// The fixture the refused-shape test uses: the cheapest way to reach a result frame init +/// produced, which is the only kind [`ChaosShape::MoveInitResultClass`] fires on. +fn init_result_unit() -> TestUnit { + serde_json::from_value(unit_json_with(empty_target_callee_code())).expect("valid unit json") +} + /// The chaos run's tally for `unit` under `seed` and `filter`. fn mutations(seed: u64, filter: ShapeFilter) -> Vec<(String, u32)> { let run = @@ -188,8 +206,10 @@ fn test_narrowing_the_filter_keeps_the_surviving_mutations() { /// that would have said so. #[test] fn test_every_always_booked_shape_moves_the_ledger() { - let always_booked: Vec = - ChaosShape::ALL.into_iter().filter(|s| s.is_always_booked()).collect(); + let always_booked: Vec = ChaosShape::ALL + .into_iter() + .filter(|s| s.is_always_booked() && *s != ChaosShape::MoveInitResultClass) + .collect(); assert!(!always_booked.is_empty(), "the gate must be stated over something"); let unit = refunding_unit(); @@ -224,6 +244,59 @@ fn test_every_always_booked_shape_moves_the_ledger() { } } +/// The one always-booked shape a successful run cannot be measured on, and the stronger premise +/// that stands in for it. +/// +/// The shim answers [`ChaosShape::MoveInitResultClass`] by declining the transaction, so there is +/// no receipt and no ledger to read — and nothing for the ledger gate to be stated over. What +/// replaces it is stronger than an all-zero-ledger check: a run that never executed cannot reach a +/// block at all. What this pins is that the decline really is the shim's refusal, that the sweep +/// classifies it as the designed outcome rather than as a defect, and that the tally still reports +/// what such a run mutated even though it produced nothing. +#[test] +fn test_the_refused_shape_is_declined_and_counted() { + let unit = init_result_unit(); + let filter = ShapeFilter::only(&[ChaosShape::MoveInitResultClass]); + let mut reached = 0u32; + for seed in 0u64..1_024 { + let mut applied = ChaosTally::default(); + let run = execute_unit_reporting_chaos( + &unit, + VECTOR_0, + &SpecName::Rex7, + RunMode::Chaos { seed, filter }, + &mut applied, + ); + if applied.total() == 0 { + assert!(run.is_ok(), "a run that mutated nothing must execute: {:?}", run.err()); + continue; + } + reached += 1; + let error = run.expect_err("a refused rewrite declines the transaction").to_string(); + assert!( + error.contains(FORBIDDEN_FRAME_INIT_REWRITE), + "seed {seed} was declined for something other than the refusal: {error}", + ); + let verdict = chaos_unit(&unit, VECTOR_0, &SpecName::Rex7, seed, filter); + assert_eq!( + verdict.class, + ChaosClass::Refused, + "a refusal is the designed outcome, not a disagreement about whether the \ + transaction executes", + ); + assert!(!verdict.class.is_failure(), "a refusal must not fail the sweep"); + assert_eq!( + verdict.applied.total(), + applied.total(), + "a declined run still has to report what it mutated", + ); + if reached == 3 { + break; + } + } + assert_eq!(reached, 3, "too few seeds in the sweep reached a result frame init produced"); +} + /// The partition the gate rests on is not vacuous in either direction. /// /// A gate stated over every shape would fail on a working shim — several shapes are booked only diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index 10d502b0..fad535f8 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -566,8 +566,9 @@ fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { } /// Every chaos verdict, in the order a reader wants them. -const CHAOS_CLASSES: [ChaosClass; 6] = [ +const CHAOS_CLASSES: [ChaosClass; 7] = [ ChaosClass::Pass, + ChaosClass::Refused, ChaosClass::ControlDrift, ChaosClass::ChaosRejected, ChaosClass::LedgerBlind, From 1233ee7fac8b5cbc37ea0ce55e7f651d0b6c82aa Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 16:01:45 +0800 Subject: [PATCH 163/208] docs(evm): give the init-result refusal a row and a reopening condition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract table's rows all rest on the REX7 deferral, which reaches frames that ran and nothing else. States what the refused set is — the results `init_frame_unsettled` returns, taken as a whole rather than arm by arm — why the boundary is a call site, why an inspector's own synthetic outcome is outside it, and why this refusal does not assert where the creation one does. The reopening condition is written down: mirroring `make_call_frame` inside MegaETH would give an init-produced result the same parked journal decision a running frame has, at about a hundred lines of duplicated upstream logic with no type-level tie, for one rewrite shape. --- crates/mega-evm/src/evm/AGENTS.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 9984bb25..f6b034d1 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -61,8 +61,9 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | A finished outcome's metadata — a call's `memory_offset`, a creation's `address`, the two flags beside them (`call_end`, `create_end`, `frame_end`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing: it changes what the caller reads next, not what the frame cost | | A refund written into any `Gas` a callback holds | Supported | `InspectorLedger::refund`, at the callback boundary, nominally | nothing: a refund moves `tx_gas_used`, not the `limit - remaining` the conservation law is stated over | | The EIP-8037 state-gas pool or spend counter, on any `Gas` or on a call's inputs | Supported | `InspectorLedger::reservoir` / `::state_gas`, settled once from the figures the transaction ends with | the pool lowers the envelope the receipt reports, so it joins the law's `I` term; the spend counter moves the receipt's state-gas figure and nothing else | -| A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it; a precompile's executed/destroyed split follows it too | +| A successful frame result rewritten into a revert or a halt | Supported | `InspectorLedger::interventions` — no gas moves | the journal decision follows the final result, so the frame's state is rolled back with it | | A failed **call** frame rewritten into a success | Supported | `InspectorLedger::interventions` | the journal commits, so the frame's state follows the result its caller was handed | +| The classification of a result **frame init** produced, moved across the success / revert / halt boundary | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_frame_init_rewrite` restores the original classification and fails the transaction with `EVMError::Custom` | | A failed **contract creation** rewritten into a success | **Refused** | `InspectorLedger::rejected_rewrites`, alongside `interventions` | `reject_forbidden_create_rewrite` restores the original classification and fails the transaction with `EVMError::Custom`; debug builds assert | | Any constant-time reading of a live interpreter's working set — its program counter, its code's identity, revm's `continue_execution` flag, the stack's length, the return buffer's identity, the memory's size and window offset, the memo of how far that memory has been paid for (`Gas::memory`), the frame's four identifying fields and its calldata's identity, the static flag, the spec id | Supported | `InspectorLedger::interventions`, at the callback boundary, off `inspector.rs::WorkingSet` | nothing directly — but a stepped program counter deletes an instruction from the frame, and growing the memory and the memo together skips the next expanding opcode's charge, which is why these need a booking at all | | The *contents* of the interpreter's stack, memory, return buffer, calldata or code, at unchanged identities | Supported, unmeasured | nothing — telling whether they came back changed needs a snapshot of unbounded state | the EVM executes on the edited state and meters it as its own work, because it is | @@ -73,6 +74,28 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th Two independent stops back the creation refusal: the shim restores the classification, and `frame.rs`'s `FrameJournalVerdict::CreateRejected` carries no code and no commit branch, so even with the refusal removed such a rewrite deposits nothing. +### Why an init-produced result's classification is refused + +The rest of the table rests on the REX7 deferral: a frame's journal decision is parked until after the last callback, so a rewrite of its classification is followed by the state it leaves behind. +A result that comes out of frame init has no such window, and cannot be given one from here. +revm decides the journal inside `make_call_frame`, statements before it returns — a value-transferring call into an empty-code account commits the transfer and returns `Stop`, a precompile that fails reverts it and returns its own failure — and `MegaETH`'s system contract interceptors decide theirs before they return, the `KeylessDeploy` one by merging a whole sandbox's state. +All of it has happened by the time a callback sees the result, so honouring a rewrite hands the caller an answer the state behind it contradicts: a transfer the recipient keeps and the sender is told failed, or a deployment the caller is told reverted and that stands anyway. + +The refused set is the results `MegaEvm::init_frame_unsettled` returns, taken as a whole rather than arm by arm. +Some of those arms carry no state a rewrite could contradict — a depth rejection, a limit refusal `MegaETH` took before revm opened a checkpoint — and are covered anyway, because the arms are revm's early-fail returns plus `MegaETH`'s interceptors and guards, a set with no type-level tie to anything here and one a revm bump grows without a compile error. +A result an inspector answered the frame with itself is deliberately outside the set: nothing in the EVM decided anything for it, no checkpoint was opened and no state written, so its classification is the inspector's to state and rewriting it contradicts nothing. +`execution.rs::frame_end_on_frame_init_result` is the one place the window is opened, which is what keeps the boundary a call site rather than a judgement repeated per arm. + +Gas and output are untouched by the refusal, on both sides of that boundary: they are measured on the lanes above either way. + +Unlike the creation refusal this one does not assert. +The shape it catches is the most ordinary rewrite a tool makes — failing a call — landing on the one kind of frame it cannot be applied to, so a corpus that produces it has to be able to report it rather than die on it. +`mega-state-test`'s chaos pool draws it on purpose (`ChaosShape::MoveInitResultClass`) and counts the refusals as `ChaosClass::Refused`. + +**Reopening condition.** A rewriting inspector could be given the same window a running frame has, by mirroring `make_call_frame` inside `MegaETH` so that the journal decision behind an init-produced result is parked on `deferred_journal` like every other. +That is roughly a hundred lines of upstream frame-init logic duplicated, with no type-level tie to the original — the same exposure `evm/frame.rs` already carries once — and it buys one rewrite shape. +Take it only if the shape turns out to be needed. + Booking is a *reported* quantity throughout. No resource limit is ever compared against the ledger, and `MegaTransactionOutcome::compute_gas_enforced` comes off the enforcement lane rather than out of the reported total — so an inspector cannot buy a transaction headroom on any dimension. ### The window a counter edit reaches nothing through From e0a8908b4c442f5138d366d13a34629e80626501 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:03:26 +0800 Subject: [PATCH 164/208] feat(evm): let a read-only inspector declare itself and skip the measurement The shim's sixteen readings, taken twice per opcode, add between a third and two thirds to a production tracer's run. An inspector type whose author implements `TrustedObserver` for it is now delegated to without any of them. The declaration is a trait rather than a `Trusted` wrapper because a wrapper cannot be seen by the shim: `MeasuredInspector::new` is generic over the inspector, `MegaEvm` names the shim as `MeasuredInspector` for whatever the caller chose, and `EvmFactory` hands inspectors in under an `I: Inspector` bound, so asking whether `I` is a `Trusted<_>` would need specialisation or a bound on every inspector the crate can be handed. The question is answered instead at the one constructor whose bound is the declaration, and carried on the shim as a flag. A wrapper is also the weaker promise: `Trusted::new(inspector)` inside a generic function declares whatever it is handed, which is how a request-supplied tracer would arrive on the fast path; an implementation names a concrete type. Debug builds take the measuring path anyway and assert the ledger came back empty after every callback, and `execute_transaction` asks once more at the end of the transaction as a backstop for a callback added later whose own check is missing. Both read the same `debug_assertions` flag, so the two builds cannot disagree. The four live-interpreter callbacks keep only the branch; their measuring bodies are outlined, which is what the declared path was actually paying for. --- crates/mega-evm/src/evm/inspector.rs | 313 +++++++++++++++++++++++++-- crates/mega-evm/src/evm/mod.rs | 50 +++++ 2 files changed, 346 insertions(+), 17 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index c8735d8c..c2e7655f 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -49,6 +49,15 @@ //! counter stepped past an instruction, or its return buffer conjured all visible. //! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. //! +//! # The one inspector the shim does not measure +//! +//! An inspector type whose author has declared it read-only, by implementing [`TrustedObserver`] +//! in source, is delegated to without any of the above. The declaration is the only way to reach +//! that path: it is a bound on [`MeasuredInspector::new_trusted`], so an inspector chosen by a +//! request or by configuration cannot arrive on it. Debug builds take the measuring path anyway +//! and assert the ledger stayed empty, so a type declared wrongly fails where it is exercised +//! rather than where it is deployed. +//! //! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on //! the uninspected path — revm's plain interpreter loop never calls an inspector at all. @@ -90,6 +99,71 @@ pub const FORBIDDEN_CREATE_REVIVAL: &str = pub const FORBIDDEN_FRAME_INIT_REWRITE: &str = "inspector moved the classification of a result frame init produced"; +/// A promise, made in source about one inspector type, that none of its callbacks writes anything +/// back to the EVM. +/// +/// # What implementing this declares +/// +/// Every callback of this type leaves the EVM exactly as it found it: it writes nothing to an +/// interpreter's gas counter or its pending action, nothing to a frame's inputs, nothing to a +/// frame result's classification, gas, output or metadata, nothing to a refund, and it never +/// answers a frame with a synthetic outcome. It may read whatever it likes and it may write to +/// its own state. That is the whole of the promise, and it is exactly the "read-only observation" +/// row of the shape table in `evm/AGENTS.md`. +/// +/// A type that keeps the promise is measured to zero on every lane of +/// [`InspectorLedger`](crate::InspectorLedger), which is the same thing the shim would have +/// concluded by measuring it — so declaring it changes what the measurement *costs* and never +/// what it *says*. +/// +/// # What it buys +/// +/// [`MeasuredInspector`] delegates to a declared type without taking any of its readings, which +/// puts the inspected path back on revm's own cost. Measuring costs about a nanosecond per +/// reading per opcode and there are sixteen readings taken twice per opcode, which adds between a +/// third and two thirds to a production tracer's run. +/// +/// # Why a declaration and not a detection +/// +/// There is nothing to detect. The shim measures at a callback boundary precisely because it +/// cannot see inside the callback, so "does this type write anything back" is not a question it +/// can ask ahead of time — only one it can answer afterwards, at the cost the declaration exists +/// to avoid. +/// +/// # The rules this trait is under +/// +/// - **No blanket implementation, ever.** Each implementation names one concrete type, so a +/// declaration is a line someone wrote about a type they had read. A blanket implementation would +/// make the promise about types nobody has looked at. +/// - **Not reachable from data.** The only route to the fast path is +/// [`MeasuredInspector::new_trusted`], whose bound is this trait, so an inspector selected by a +/// request, a configuration file or any other run-time value cannot arrive on it — an +/// RPC-supplied tracer is a value, and no value can carry an implementation. +/// - **Do not implement it for anything that intercepts.** An inspector that answers a frame +/// itself, edits inputs, or rewrites a result is a rewriting inspector however little it +/// rewrites; those are supported, measured, and must stay measured. +/// - **A foreign inspector needs a newtype.** The orphan rule wants one of the trait and the type +/// to be local, and for a `revm-inspectors` tracer neither is — so a node declares a newtype of +/// its own that forwards every callback. `benches/common/subject.rs` does exactly that, and is +/// the shape to copy. +/// +/// # Verified in debug builds +/// +/// A declared type still takes the full measurement under `debug_assertions`, and the shim +/// asserts the ledger stayed empty after every callback. A wrong declaration therefore fails in +/// tests, in CI and under the chaos sweep, at the callback that broke it. +pub trait TrustedObserver {} + +/// The inspector `MegaETH` runs with when none was supplied observes nothing at all. +impl TrustedObserver for revm::inspector::NoOpInspector {} + +/// A declared observer stays declared when it is handed over by reference. +/// +/// revm implements `Inspector` for `&mut I`, which is how a caller keeps an inspector it can read +/// back afterwards. This lifts the declaration to the same shape, and it grants nothing: `&mut T` +/// is declared exactly when `T` is, so no type becomes trusted that was not trusted already. +impl TrustedObserver for &mut T {} + /// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// /// `MegaETH` applies this itself — [`MegaEvm::with_inspector`](crate::MegaEvm::with_inspector) and @@ -97,18 +171,52 @@ pub const FORBIDDEN_FRAME_INIT_REWRITE: &str = /// value and store it wrapped, and the accessors hand back the unwrapped inspector — so the wrapper /// is not something a caller opts into or can opt out of. /// +/// What a caller *can* opt into is being measured more cheaply, by declaring the inspector's type +/// [`TrustedObserver`] and building the shim with [`new_trusted`](Self::new_trusted). See +/// [`measures`](Self::measures) for what that changes and where it stops. +/// /// Derefs to the wrapped inspector, so `evm.inspector().whatever()` reaches the user's own type. #[derive(Clone, Copy, Debug, Default, derive_more::Deref, derive_more::DerefMut)] pub struct MeasuredInspector { #[deref] #[deref_mut] inner: I, + /// Whether the wrapped type's author declared it [`TrustedObserver`]. + /// + /// A flag rather than a type parameter, because the type the shim is asked about is chosen by + /// whoever calls `with_inspector`, and `MegaEvm` names the shim as `MeasuredInspector` + /// for whatever `INSP` that is. Answering it in the type system would mean either a bound on + /// every inspector `MegaETH` can be handed — including the foreign ones it cannot implement + /// anything for — or a second impl of `Inspector` that overlaps the first. So the question is + /// answered where it is asked, at the one constructor whose bound is the declaration, and + /// carried here. + /// + /// `Default` leaves it false, which is the safe direction: an unbuilt shim measures. + trusted: bool, } impl MeasuredInspector { /// Wraps `inner` in the measurement shim. pub const fn new(inner: I) -> Self { - Self { inner } + Self { inner, trusted: false } + } + + /// Whether this callback takes the measuring path. + /// + /// False only for a declared [`TrustedObserver`] in a release build. Under `debug_assertions` + /// every inspector is measured, declared or not, and a declared one is additionally asserted + /// to have booked nothing — which is what makes the declaration a claim the build system + /// checks rather than a comment. + /// + /// Both halves are compile-time constants beside a `bool` the shim was built with, so the + /// release fast path is one predictable branch and the debug path folds away entirely. + /// + /// This and [`verify_trusted`] read the same `debug_assertions` flag, which is what keeps the + /// two builds from disagreeing: a profile that turns assertions on in an optimised build gets + /// the measured path *and* the check, never one without the other. + #[inline(always)] + const fn measures(&self) -> bool { + !self.trusted || cfg!(debug_assertions) } /// The wrapped inspector. @@ -125,6 +233,56 @@ impl MeasuredInspector { pub fn into_inner(self) -> I { self.inner } + + /// Whether the wrapped inspector's type was declared [`TrustedObserver`]. + /// + /// True only for a shim built by [`new_trusted`](Self::new_trusted). What it is for is the + /// transaction-level backstop in `MegaEvm::execute_transaction`: the per-callback verification + /// names the callback that broke a declaration, and this catches one broken at a callback + /// whose verification is missing. + pub const fn is_trusted(&self) -> bool { + self.trusted + } +} + +impl MeasuredInspector { + /// Wraps `inner` in a shim that delegates to it without measuring, on the strength of its + /// type's [`TrustedObserver`] declaration. + /// + /// This is the only constructor that produces the fast path, and its bound is the only way to + /// reach it. Debug builds measure anyway and assert the result is empty. + pub const fn new_trusted(inner: I) -> Self { + Self { inner, trusted: true } + } +} + +/// Asserts, in debug builds, that a declared [`TrustedObserver`] really booked nothing. +/// +/// Called after every measured callback. The ledger accumulates over the whole transaction and is +/// reset at its start, so the first callback that breaks the promise is the one that fails — +/// later ones would fail too, but this one names the site. +/// +/// Compiled out of release builds together with the measurement it checks: a declared type never +/// reaches a measuring body there at all. +#[inline] +fn verify_trusted( + trusted: bool, + context: &MegaContext, + callback: &'static str, +) { + #[cfg(debug_assertions)] + if trusted { + let ledger = context.additional_limit.borrow().inspector_ledger(); + assert!( + ledger.is_zero(), + "an inspector declared `TrustedObserver` wrote something back at `{callback}`: \ + {ledger:?}", + ); + } + #[cfg(not(debug_assertions))] + { + let _ = (trusted, context, callback); + } } /// Where the gas an interpreter is holding will go next, read off the action it is holding. @@ -1020,6 +1178,99 @@ impl LiveReading { } } +/// The measuring bodies of the four live-interpreter callbacks, kept out of line. +/// +/// Each callback is two things: a branch on the declaration, and — when it is taken — the +/// measurement. Only the branch belongs in revm's instruction loop, and `inline(never)` is what +/// puts it there alone. Inlined, the measurement's two hundred bytes of readings are laid down +/// inside the loop for a declared observer that never executes them, and the loop pays for them +/// in registers and instruction cache all the same. +/// +/// That cost is most of what a declared observer was paying. On `interpreter_hotloop` with an +/// empty inspector, outlining takes the declared path from 1.13× the pre-shim inspected loop to +/// 1.04× on REX6, and from 1.07× to 1.00× on REX7 — the branch alone is nearly free, and the +/// bloat was not. +/// +/// It is not free for the undeclared path, and the trade was measured both ways rather than +/// assumed. Beside a real tracer — the inspector the inspected path carries in production — the +/// undeclared path gets 3–6% *faster*, for the same reason the declared one does. Beside an empty +/// inspector it gets 12–19% slower, because there the call is the whole of the work. The empty +/// inspector is an instrument for isolating this shim's cost and not a workload, and it is the +/// only row that moves the wrong way. +/// +/// Written out four times rather than taken as a function pointer, which would cost the +/// undeclared path the inlining of the inner inspector's own callback. +impl MeasuredInspector { + #[inline(never)] + fn initialize_interp_measured( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, + ) where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + I: Inspector, INTR>, + { + let reading = LiveReading::enter(interp); + self.inner.initialize_interp(interp, context); + reading.leave::(interp, context); + verify_trusted(self.trusted, context, "initialize_interp"); + } + + #[inline(never)] + fn step_measured( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, + ) where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + I: Inspector, INTR>, + { + let reading = LiveReading::enter(interp); + self.inner.step(interp, context); + reading.leave::(interp, context); + verify_trusted(self.trusted, context, "step"); + } + + #[inline(never)] + fn step_end_measured( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, + ) where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + I: Inspector, INTR>, + { + let reading = LiveReading::enter(interp); + self.inner.step_end(interp, context); + reading.leave::(interp, context); + verify_trusted(self.trusted, context, "step_end"); + } + + #[inline(never)] + fn log_full_measured( + &mut self, + interp: &mut Interpreter, + context: &mut MegaContext, + log: Log, + ) where + DB: Database, + ExtEnvs: ExternalEnvTypes, + INTR: InterpreterTypes, + I: Inspector, INTR>, + { + let reading = LiveReading::enter(interp); + self.inner.log_full(interp, context, log); + reading.leave::(interp, context); + verify_trusted(self.trusted, context, "log_full"); + } +} + impl Inspector, INTR> for MeasuredInspector where DB: Database, @@ -1030,22 +1281,24 @@ where /// Measured, but without settling a segment: this runs after the frame is built and before its /// settlement window is opened, so there is nothing open to close. The frame's own entry hook /// opens the window on whatever counter this callback leaves behind. - #[inline] + #[inline(always)] fn initialize_interp( &mut self, interp: &mut Interpreter, context: &mut MegaContext, ) { - let reading = LiveReading::enter(interp); - self.inner.initialize_interp(interp, context); - reading.leave::(interp, context); + if !self.measures() { + return self.inner.initialize_interp(interp, context); + } + self.initialize_interp_measured(interp, context); } - #[inline] + #[inline(always)] fn step(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { - let reading = LiveReading::enter(interp); - self.inner.step(interp, context); - reading.leave::(interp, context); + if !self.measures() { + return self.inner.step(interp, context); + } + self.step_measured(interp, context); } /// The one callback that runs with an action already pending: revm runs it after the @@ -1054,11 +1307,12 @@ where /// [`ActionLane::counter_reaches_envelope`] — and the action holding that copy is reachable /// through `LoopControl`. Both objects are measured, on the lanes /// [`book_pending_action`] routes them to. - #[inline] + #[inline(always)] fn step_end(&mut self, interp: &mut Interpreter, context: &mut MegaContext) { - let reading = LiveReading::enter(interp); - self.inner.step_end(interp, context); - reading.leave::(interp, context); + if !self.measures() { + return self.inner.step_end(interp, context); + } + self.step_end_measured(interp, context); } /// No interpreter and no frame inputs are reachable here, so there is nothing to measure — @@ -1068,16 +1322,17 @@ where self.inner.log(context, log); } - #[inline] + #[inline(always)] fn log_full( &mut self, interpreter: &mut Interpreter, context: &mut MegaContext, log: Log, ) { - let reading = LiveReading::enter(interpreter); - self.inner.log_full(interpreter, context, log); - reading.leave::(interpreter, context); + if !self.measures() { + return self.inner.log_full(interpreter, context, log); + } + self.log_full_measured(interpreter, context, log); } #[inline] @@ -1086,6 +1341,9 @@ where context: &mut MegaContext, frame_input: &mut FrameInput, ) -> Option { + if !self.measures() { + return self.inner.frame_start(context, frame_input); + } let before = frame_input.clone(); let outcome = self.inner.frame_start(context, frame_input); book_env_adjustment( @@ -1098,6 +1356,7 @@ where book_synthetic_refund(context, outcome.gas().refunded()); } book_intervention(context, outcome.is_some() || frame_input_rewritten(before, frame_input)); + verify_trusted(self.trusted, context, "frame_start"); outcome } @@ -1108,6 +1367,9 @@ where frame_input: &FrameInput, frame_result: &mut FrameResult, ) { + if !self.measures() { + return self.inner.frame_end(context, frame_input, frame_result); + } let before = frame_result.instruction_result(); let output = frame_result.interpreter_result().output.clone(); let metadata = OutcomeMetadata::of(frame_result); @@ -1126,6 +1388,7 @@ where if let FrameResult::Create(outcome) = frame_result { reject_forbidden_create_rewrite(context, before, &mut outcome.result); } + verify_trusted(self.trusted, context, "frame_end"); } #[inline] @@ -1134,6 +1397,9 @@ where context: &mut MegaContext, inputs: &mut CallInputs, ) -> Option { + if !self.measures() { + return self.inner.call(context, inputs); + } let before = inputs.clone(); let outcome = self.inner.call(context, inputs); book_env_adjustment( @@ -1146,6 +1412,7 @@ where book_synthetic_refund(context, outcome.result.gas.refunded()); } book_intervention(context, outcome.is_some() || call_inputs_rewritten(before, inputs)); + verify_trusted(self.trusted, context, "call"); outcome } @@ -1159,6 +1426,9 @@ where inputs: &CallInputs, outcome: &mut CallOutcome, ) { + if !self.measures() { + return self.inner.call_end(context, inputs, outcome); + } let before = (outcome.result.result, outcome.result.output.clone()); let metadata = CallMetadata::of(outcome); let refund_before = outcome.result.gas.refunded(); @@ -1167,6 +1437,7 @@ where book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); book_intervention(context, CallMetadata::of(outcome) != metadata); reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); + verify_trusted(self.trusted, context, "call_end"); } #[inline] @@ -1175,6 +1446,9 @@ where context: &mut MegaContext, inputs: &mut CreateInputs, ) -> Option { + if !self.measures() { + return self.inner.create(context, inputs); + } let before = inputs.clone(); let outcome = self.inner.create(context, inputs); book_env_adjustment( @@ -1187,6 +1461,7 @@ where book_synthetic_refund(context, outcome.result.gas.refunded()); } book_intervention(context, outcome.is_some() || create_inputs_rewritten(before, inputs)); + verify_trusted(self.trusted, context, "create"); outcome } @@ -1201,6 +1476,9 @@ where inputs: &CreateInputs, outcome: &mut CreateOutcome, ) { + if !self.measures() { + return self.inner.create_end(context, inputs, outcome); + } let before = (outcome.result.result, outcome.result.output.clone()); let address = outcome.address; let refund_before = outcome.result.gas.refunded(); @@ -1212,6 +1490,7 @@ where // refused as an init result is not counted a second time by the refusal below. reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); + verify_trusted(self.trusted, context, "create_end"); } /// Everything this callback receives is passed by value, so it cannot change execution state. diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 7220824e..5ea8fb30 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -220,6 +220,35 @@ impl MegaEvm { MegaEvm { inner, inspect: true, mega_cfg, deferred_journal: None } } + /// Creates a new `MegaETH` EVM instance with the given read-only inspector enabled at + /// runtime, and the measurement shim's per-callback work skipped. + /// + /// The bound is the whole of the difference from [`with_inspector`](Self::with_inspector): + /// `I`'s author has declared, in source, that none of its callbacks writes anything back to + /// the EVM, so there is nothing for the shim to measure and it delegates directly. Debug + /// builds measure anyway and assert that the declaration held. + /// + /// See [`TrustedObserver`] for what the declaration promises and what it may not be written + /// for. + /// + /// [`EvmFactory::create_evm_with_inspector`](alloy_evm::EvmFactory::create_evm_with_inspector) + /// cannot reach this — its bound is `I: Inspector` and its return type is fixed — so a node + /// that builds through the factory takes `create_evm(..).with_trusted_inspector(..)`, which + /// keeps the factory's dynamic precompiles. + pub fn with_trusted_inspector( + self, + inspector: I, + ) -> MegaEvm { + let mega_cfg = self.mega_cfg; + let inner = revm::context::Evm::new_with_inspector( + self.inner.ctx, + MeasuredInspector::new_trusted(inspector), + self.inner.instruction, + self.inner.precompiles, + ); + MegaEvm { inner, inspect: true, mega_cfg, deferred_journal: None } + } + /// Creates a new `MegaETH` EVM instance with the inspector disabled at runtime. /// /// # Returns @@ -400,6 +429,7 @@ where } else { ExecuteEvm::transact(self, tx)? }; + let trusted_inspector = self.inner.inspector.is_trusted(); let is_inside_sandbox = self.ctx().is_inside_sandbox(); let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); @@ -416,6 +446,7 @@ where inspector_ledger: additional_limit.inspector_ledger(), }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); + debug_assert_trusted_observer_kept_its_promise(trusted_inspector, &outcome); Ok(outcome) } @@ -438,6 +469,7 @@ where tx: MegaTransaction, ) -> Result> { let result_and_state = InspectEvm::inspect_tx(self, tx)?; + let trusted_inspector = self.inner.inspector.is_trusted(); let is_inside_sandbox = self.ctx().is_inside_sandbox(); let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); @@ -454,6 +486,7 @@ where inspector_ledger: additional_limit.inspector_ledger(), }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); + debug_assert_trusted_observer_kept_its_promise(trusted_inspector, &outcome); Ok(outcome) } @@ -467,6 +500,23 @@ where } } +/// Debug-only backstop on a `TrustedObserver` declaration, read once per transaction. +/// +/// The shim verifies the same thing after every callback it measures, which is what names the +/// callback that broke the promise. This asks it again where nothing can be missing: a rewrite +/// made at a callback whose own verification was never written — the shape a callback added later +/// takes — has no later callback to be caught at if it was the transaction's last. +/// +/// Both are debug-only for the same reason. A declared inspector books nothing, so in a release +/// build there is nothing here to read that is not zero by construction. +fn debug_assert_trusted_observer_kept_its_promise(trusted: bool, outcome: &MegaTransactionOutcome) { + debug_assert!( + !trusted || outcome.inspector_ledger.is_zero(), + "an inspector declared `TrustedObserver` wrote something back: {:?}", + outcome.inspector_ledger, + ); +} + /// Debug-only check that a transaction's tracker lanes account for the whole envelope its receipt /// reports (REX7+; before REX7 there is no destroyed lane and no non-compute lane, so there is /// nothing to reconcile). From 2aaee703bde16760fecef20d96b5879d2d36ee97 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:03:35 +0800 Subject: [PATCH 165/208] test(rex7): pin a declared observer against a measured and an uninspected run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two properties hold the declaration up, and each has an anchor here. One observer runs the same fixture three ways — uninspected, measured, and declared — and all three produce the same receipt, the same four resource dimensions and the same state, with both inspected runs leaving an empty ledger. The callback counts are asserted equal too: every other assertion would also pass for a fast path that skipped the inspector rather than the measurement. Two rewriting inspectors then wear a declaration they have no right to, one moving gas and one moving none at all, and both must panic. Red-checked with the verification removed: neither panics, and the ledger the transaction reports carries the rewrite. The second exists because a check written over the gas lanes alone would pass a stepped program counter, which costs nothing and changes what the frame executes. The declared run only differs from the other two in a release build, so the three-way comparison is stated as an acceptance gate there as well. --- crates/mega-evm/tests/rex7/main.rs | 4 + .../mega-evm/tests/rex7/trusted_observer.rs | 300 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 crates/mega-evm/tests/rex7/trusted_observer.rs diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 6592886f..a114c2a6 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -90,6 +90,9 @@ //! remainder class (swallow / return / unreachable), with no catch-all, so a revm bump that adds //! a variant fails to compile until a human assigns it; the early-fail arms of frame init are //! listed beside it for the upgrade diff that those arms have no type-level tie to. +//! - `trusted_observer` — the declared read-only fast path: a declaration that holds produces the +//! same transaction as no inspector and as a measured one, and a declaration that does not panics +//! in debug builds at the callback that broke it. mod burn_split; mod call_body_halt_charges; @@ -128,3 +131,4 @@ mod pre_execution_intrinsic_reject; mod precompile_halt; mod refund_and_state_gas; mod result_space_tripwire; +mod trusted_observer; diff --git a/crates/mega-evm/tests/rex7/trusted_observer.rs b/crates/mega-evm/tests/rex7/trusted_observer.rs new file mode 100644 index 00000000..76bb1144 --- /dev/null +++ b/crates/mega-evm/tests/rex7/trusted_observer.rs @@ -0,0 +1,300 @@ +//! The one inspector the shim does not measure, and the two things that keep that safe. +//! +//! A type whose author has declared it `TrustedObserver` is delegated to without any of the +//! shim's readings. The declaration is a promise about source, not a detection, so it is held up +//! by exactly two things and both are here: +//! +//! - **A declared type that keeps the promise is indistinguishable from one that is measured.** The +//! same observer, over the same fixture, run three ways — with no inspector, declared, and +//! undeclared — produces the same receipt, the same four resource dimensions, the same state, and +//! the same callbacks in the same numbers. +//! - **A declared type that breaks it fails where it is exercised.** Debug builds take the +//! measuring path anyway and assert the ledger stayed empty, so a wrong declaration panics at the +//! callback that broke it rather than reaching a node. +//! +//! The second half is a `debug_assertions` property by construction, so the two anchors below are +//! compiled only into debug builds — which is how this repository's tests run. +//! +//! Which leaves the first half needing a release run to mean anything: in a debug build the +//! declared run takes the measuring path like every other, so it is the same code the other two +//! runs exercise. `cargo test -p mega-evm --release --test rex7` is where the comparison is +//! actually against the fast path, and it is an acceptance gate for that reason. + +use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, + MegaTransaction, MegaTransactionNew as _, TrustedObserver, +}; +use revm::{ + bytecode::opcode::{CALL, POP, STOP}, + context::{result::ExecutionResult, tx::TxEnvBuilder}, + interpreter::{CallInputs, CallOutcome, Interpreter, InterpreterTypes}, + state::EvmState, + Inspector, +}; + +/// Transaction gas limit used throughout: high enough that EVM gas is never what binds. +const TX_GAS_LIMIT: u64 = 100_000_000; + +/// Everything one run of the fixture produces, for the three-way comparison. +#[derive(Debug)] +struct Reading { + result: ExecutionResult, + compute_gas: u64, + enforced: u64, + destroyed: u64, + data_size: u64, + kv_updates: u64, + state_growth: u64, + gas_used: u64, + total_gas_spent: u64, + ledger: InspectorLedger, + state: EvmState, +} + +/// Asserts two runs of the fixture are the same run, field by field. +/// +/// Written out rather than derived from `PartialEq` on the whole struct so that the field that +/// disagrees is the one the failure names. +fn assert_same(label: &str, left: &Reading, right: &Reading) { + assert_eq!(format!("{:?}", left.result), format!("{:?}", right.result), "{label}: result"); + assert_eq!(left.compute_gas, right.compute_gas, "{label}: compute gas"); + assert_eq!(left.enforced, right.enforced, "{label}: enforced compute gas"); + assert_eq!(left.destroyed, right.destroyed, "{label}: destroyed compute gas"); + assert_eq!(left.data_size, right.data_size, "{label}: data size"); + assert_eq!(left.kv_updates, right.kv_updates, "{label}: kv updates"); + assert_eq!(left.state_growth, right.state_growth, "{label}: state growth"); + assert_eq!(left.gas_used, right.gas_used, "{label}: gas used"); + assert_eq!(left.total_gas_spent, right.total_gas_spent, "{label}: total gas spent"); + assert_eq!(left.state, right.state, "{label}: produced state"); +} + +fn tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { + let mut context = MegaContext::new(db, MegaSpecId::REX7); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context +} + +fn read(outcome: mega_evm::MegaTransactionOutcome) -> Reading { + let gas_used = outcome.result_and_state.result.tx_gas_used(); + let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); + Reading { + result: outcome.result_and_state.result, + compute_gas: outcome.compute_gas_used, + enforced: outcome.compute_gas_enforced, + destroyed: outcome.compute_gas_destroyed, + data_size: outcome.data_size, + kv_updates: outcome.kv_updates, + state_growth: outcome.state_growth_used, + gas_used, + total_gas_spent, + ledger: outcome.inspector_ledger, + state: outcome.result_and_state.state, + } +} + +/// No inspector at all: revm's plain frame loops, which never call one. +fn transact_plain(mut db: MemoryDatabase) -> Reading { + let mut evm = MegaEvm::new(context(&mut db)); + read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) +} + +/// The inspected loop with the shim measuring, which is what every undeclared inspector gets. +fn transact_measured(mut db: MemoryDatabase, inspector: &mut I) -> Reading +where + I: for<'a> Inspector>, +{ + let mut evm = MegaEvm::new(context(&mut db)).with_inspector(inspector); + read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) +} + +/// The inspected loop with the shim delegating on the strength of the declaration. +fn transact_trusted(mut db: MemoryDatabase, inspector: &mut I) -> Reading +where + I: for<'a> Inspector> + TrustedObserver, +{ + let mut evm = MegaEvm::new(context(&mut db)).with_trusted_inspector(inspector); + read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) +} + +/// Counts the callbacks it is handed and changes nothing — a declaration that holds. +#[derive(Default, Debug, PartialEq, Eq)] +struct Observer { + initialize_interps: u64, + steps: u64, + step_ends: u64, + calls: u64, + call_ends: u64, +} + +impl TrustedObserver for Observer {} + +impl Inspector for Observer { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.initialize_interps += 1; + } + + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.step_ends += 1; + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.calls += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.call_ends += 1; + } +} + +/// The fixture: a frame that writes storage and makes an inner call, so every dimension the +/// comparison covers has something in it. +fn fixture_db() -> MemoryDatabase { + let callee = + BytecodeBuilder::default().sstore(U256::from(0x11), U256::from(0x22)).append(STOP).build(); + let code = BytecodeBuilder::default() + .sstore(U256::from(0x20), U256::from(0x99)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) + .account_code(CALLEE, callee) +} + +/// ★ Declaring an observer read-only changes what the measurement costs and nothing it says. +/// +/// The same observer runs the same fixture three ways: uninspected, measured, and declared. All +/// three produce the same receipt, the same four resource dimensions and the same state; both +/// inspected runs leave an empty ledger; and the declared run is handed exactly the callbacks the +/// measured one was, in the same numbers. +/// +/// That last part is what separates "the shim skipped its own work" from "the shim skipped the +/// inspector": a fast path that delegated less would pass every other assertion here. +#[test] +fn test_a_declared_observer_runs_the_transaction_the_other_two_runs_produce() { + let plain = transact_plain(fixture_db()); + + let mut measured_observer = Observer::default(); + let measured = transact_measured(fixture_db(), &mut measured_observer); + + let mut trusted_observer = Observer::default(); + let trusted = transact_trusted(fixture_db(), &mut trusted_observer); + + assert!(measured_observer.steps > 0, "the fixture must run opcodes under the inspector"); + assert_eq!(measured_observer.calls, 2, "one top-level frame plus one inner call"); + assert_eq!( + measured_observer, trusted_observer, + "the declared run must be handed the same callbacks as the measured one", + ); + + assert!(measured.ledger.is_zero(), "measured: {:?}", measured.ledger); + assert!( + trusted.ledger.is_zero(), + "the fast path books nothing by construction: {:?}", + trusted.ledger, + ); + + assert_same("declared against uninspected", &trusted, &plain); + assert_same("declared against measured", &trusted, &measured); +} + +/// A rewriting inspector that moves gas, wearing a declaration it has no right to. +/// +/// `TrustedObserver` is implemented for it here and nowhere else: this is the only place in the +/// repository where the promise is deliberately broken, and it exists so that breaking it is +/// known to be caught. +#[cfg(debug_assertions)] +#[derive(Default)] +struct LiarThatMovesGas { + fired: bool, +} + +#[cfg(debug_assertions)] +impl TrustedObserver for LiarThatMovesGas {} + +#[cfg(debug_assertions)] +impl Inspector for LiarThatMovesGas { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if !self.fired { + self.fired = true; + interp.gas.set_remaining(interp.gas.remaining() + 10_000); + } + } +} + +/// A rewriting inspector that moves no gas at all, wearing the same declaration. +/// +/// Stepping the program counter deletes an instruction from the frame and costs the transaction +/// nothing, so no gas lane sees it — only `interventions` does. It is here because a verification +/// written over the gas lanes alone would pass this one. +#[cfg(debug_assertions)] +#[derive(Default)] +struct LiarThatMovesNoGas { + fired: bool, +} + +#[cfg(debug_assertions)] +impl TrustedObserver for LiarThatMovesNoGas {} + +#[cfg(debug_assertions)] +impl Inspector for LiarThatMovesNoGas { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + use revm::interpreter::interpreter_types::{Jumps, LoopControl}; + if !self.fired && interp.bytecode.is_not_end() { + self.fired = true; + interp.bytecode.relative_jump(1); + } + } +} + +/// ★ A declaration that is false fails at the callback that made it false. +/// +/// Debug builds run the whole measurement behind the declaration and assert the ledger came back +/// empty, so this is what "trust, and verify" means in practice: the release build pays nothing +/// and the build every test, every CI job and every chaos sweep runs catches the mis-declaration +/// on the spot. +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "declared `TrustedObserver` wrote something back at `step`")] +fn test_a_declared_inspector_that_moves_gas_fails_the_debug_verification() { + let mut liar = LiarThatMovesGas::default(); + let _ = transact_trusted(fixture_db(), &mut liar); +} + +/// ★ And so does one whose rewrite moves no gas. +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "declared `TrustedObserver` wrote something back at `step`")] +fn test_a_declared_inspector_that_moves_no_gas_fails_the_debug_verification_too() { + let mut liar = LiarThatMovesNoGas::default(); + let _ = transact_trusted(fixture_db(), &mut liar); +} From 41494a2eef2a9299fdcfe4d827731b92d9efa548 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:03:43 +0800 Subject: [PATCH 166/208] feat(state-test): run the control declared, and compare all three MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RunMode::ObserveTrusted` drives the same callback-counting control the rewriting runs are judged against, declared `TrustedObserver` so the shim delegates without measuring. It is a third run of a vector rather than a variant of the second, because what it is for is to be compared against both of the others. The comparison goes through `diff::compare`, so it covers everything a `SpecOutcome` carries — the receipt, the resource dimensions and the roots — not just the lanes the ledger touches. The chaos pool is untouched and stays on the measured path: what it exercises is the measurement, and a declared inspector has none. --- crates/mega-state-test/src/chaos.rs | 5 ++++ crates/mega-state-test/src/diff.rs | 18 ++++++++++- crates/mega-state-test/tests/chaos_mode.rs | 35 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index ecb08045..0a612316 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -521,6 +521,11 @@ impl CallbackCounter { } } +/// The control counts callbacks and writes nothing back, which is exactly what the declaration +/// promises — so it is also what [`RunMode::ObserveTrusted`](crate::diff::RunMode::ObserveTrusted) +/// drives the shim's fast path with. +impl mega_evm::TrustedObserver for CallbackCounter {} + impl Inspector for CallbackCounter { fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { self.callbacks += 1; diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index 8e47c7eb..6917b88b 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -823,6 +823,13 @@ pub enum RunMode { /// A read-only inspector that counts the callbacks it is handed and changes nothing — the /// control a rewriting run is judged against. Observe, + /// The same control, declared `TrustedObserver`, so the measurement shim delegates to it + /// without taking any of its readings. + /// + /// A third run of one vector rather than a variant of the second: what it is for is to be + /// compared against both of the others, since the declaration's whole claim is that skipping + /// the measurement changes nothing an execution produces. + ObserveTrusted, /// [`ChaosInspector`](crate::chaos::ChaosInspector), seeded with `seed` and restricted to /// what `filter` allows. Chaos { @@ -840,7 +847,8 @@ pub struct UnitExecution { pub outcome: SpecOutcome, /// What the chaos inspector did, in [`RunMode::Chaos`]. pub chaos: Option, - /// How many callbacks the observing inspector was handed, in [`RunMode::Observe`]. + /// How many callbacks the observing inspector was handed, in [`RunMode::Observe`] and + /// [`RunMode::ObserveTrusted`]. pub observed: u64, /// What the measurement shim booked for the transaction. /// @@ -920,6 +928,14 @@ pub fn execute_unit_reporting_chaos( observed = inner.inspector.callbacks(); (executed, None, inner.ctx) } + RunMode::ObserveTrusted => { + let mut evm = + MegaEvm::new(evm_context).with_trusted_inspector(CallbackCounter::default()); + let executed = evm.execute_transaction(megatx); + let inner = evm.into_inner(); + observed = inner.inspector.callbacks(); + (executed, None, inner.ctx) + } RunMode::Chaos { seed, filter } => { let mut evm = MegaEvm::new(evm_context).with_inspector(ChaosInspector::new(seed, filter)); diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index 99689b67..ab1eb37e 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -350,6 +350,41 @@ fn test_the_control_inspector_changes_nothing() { ); } +/// The declared control produces the run the other two produce. +/// +/// Three runs of one vector: no inspector, the control measured, and the control declared +/// `TrustedObserver` so the shim delegates without measuring. The declaration's whole claim is +/// that the third is the first, and the field-by-field comparison is what says so — the receipt, +/// the four resource dimensions, the roots, and everything else `SpecOutcome` carries. +/// +/// The callback count is asserted equal too, because every other assertion here would also pass +/// for a fast path that skipped the inspector rather than the measurement. +#[test] +fn test_the_declared_control_changes_nothing_either() { + let unit = unit(); + let plain = execute_unit_in_mode(&unit, VECTOR_0, &SpecName::Rex7, RunMode::Plain) + .expect("the fixture executes"); + let observed = execute_unit_in_mode(&unit, VECTOR_0, &SpecName::Rex7, RunMode::Observe) + .expect("the fixture executes"); + let trusted = execute_unit_in_mode(&unit, VECTOR_0, &SpecName::Rex7, RunMode::ObserveTrusted) + .expect("the fixture executes"); + + assert!(trusted.observed > 0, "the declared control must still be handed callbacks"); + assert_eq!( + trusted.observed, observed.observed, + "and the same ones the measured control was handed", + ); + assert!(trusted.ledger.is_zero(), "the fast path books nothing: {:?}", trusted.ledger); + assert!( + state_test::diff::compare(&trusted.outcome, &plain.outcome).is_empty(), + "a declared observation-only inspector moved something", + ); + assert!( + state_test::diff::compare(&trusted.outcome, &observed.outcome).is_empty(), + "declaring the control changed what its run produced", + ); +} + /// A vector the rewriting run leaves executable comes back `Pass`, with mutations to show for it. #[test] fn test_a_mutated_vector_passes_with_mutations_recorded() { From d2f963006a03f9429efeee8c58826f944647bde3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:03:50 +0800 Subject: [PATCH 167/208] bench: add the declared-observer rows to transact `inspect_noop_trusted` and `inspect_tracer_trusted` attach the same two inspectors the existing inspected rows do, through `with_trusted_inspector`, so each pair differs only in whether the shim measures. Against its untrusted twin a trusted row is the measurement's whole cost; against the pre-shim baseline it is what the declaration buys back. The tracer row goes through a local newtype that forwards every callback, because the orphan rule wants one of the trait and the type to be local and for a `revm-inspectors` tracer neither is. That is the shape a node has to write for the same reason, so the row measures what a node would actually run. --- crates/mega-evm/benches/common/mod.rs | 13 +- crates/mega-evm/benches/common/subject.rs | 167 +++++++++++++++++++--- 2 files changed, 162 insertions(+), 18 deletions(-) diff --git a/crates/mega-evm/benches/common/mod.rs b/crates/mega-evm/benches/common/mod.rs index 1bb5d873..0e14ca5f 100644 --- a/crates/mega-evm/benches/common/mod.rs +++ b/crates/mega-evm/benches/common/mod.rs @@ -97,17 +97,28 @@ const INSPECTED_SPEC_IDS: &[(&str, MegaSpecId)] = /// Register inspected-path rows for REX6 and REX7 on the current group. /// -/// Each spec gets two extra rows: +/// Each spec gets four extra rows: /// - `/inspect_noop` — inspect loop + measurement shim around a /// [`NoOpInspector`](revm::inspector::NoOpInspector) /// - `/inspect_tracer` — the same loop with `revm-inspectors`' geth default /// (`debug_traceTransaction`) +/// - `/inspect_noop_trusted`, `/inspect_tracer_trusted` — the same two inspectors +/// declared read-only, so the shim delegates without measuring. Each trusted row against its +/// untrusted twin is the measurement's cost; against the pre-shim baseline it is what the +/// declaration buys back. /// /// Pair with [`register_all`] (or [`register_mega`]) so the unsuffixed /// `` row remains the plain baseline. Does not re-register that row. pub fn register_inspected(group: &mut Group<'_>, w: &Workload) { run_subjects(group, "inspect_noop", w, &inspected_subjects(InspectKind::NoOp)); run_subjects(group, "inspect_tracer", w, &inspected_subjects(InspectKind::GethTracer)); + run_subjects(group, "inspect_noop_trusted", w, &inspected_subjects(InspectKind::NoOpTrusted)); + run_subjects( + group, + "inspect_tracer_trusted", + w, + &inspected_subjects(InspectKind::GethTracerTrusted), + ); } fn inspected_subjects(kind: InspectKind) -> Vec> { diff --git a/crates/mega-evm/benches/common/subject.rs b/crates/mega-evm/benches/common/subject.rs index 189d9797..74f24d2b 100644 --- a/crates/mega-evm/benches/common/subject.rs +++ b/crates/mega-evm/benches/common/subject.rs @@ -9,12 +9,12 @@ //! operator-fee zero-out — is defined once in the "Comparability baseline" //! section below, not repeated per stack. -use alloy_primitives::{Bytes, U256}; +use alloy_primitives::{Address, Bytes, Log, U256}; use core::convert::Infallible; use criterion::black_box; use mega_evm::{ revm::inspector::NoOpInspector, test_utils::MemoryDatabase, EmptyExternalEnv, MegaContext, - MegaEvm, MegaSpecId, MegaTransaction, TestExternalEnvs, + MegaEvm, MegaSpecId, MegaTransaction, TestExternalEnvs, TrustedObserver, }; use op_revm::{ DefaultOp as _, OpBuilder as _, OpContext as OpContextPinned, OpSpecId as OpSpecIdPinned, @@ -23,6 +23,11 @@ use op_revm::{ use revm::{ context::{tx::TxEnvBuilder, TxEnv}, database::EmptyDB as EmptyDBPinned, + handler::FrameResult, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, + InterpreterTypes, + }, primitives::hardfork::SpecId as SpecIdPinned, Context as ContextPinned, ExecuteEvm, InspectEvm, Inspector, MainBuilder as _, MainContext as _, @@ -195,10 +200,97 @@ impl Subject for Mega { /// `GethTracer` is `revm-inspectors`' `debug_traceTransaction` default — the /// production tracer this crate already admits on the block path. `all()` is /// not used: it clones memory on every opcode and is not an RPC default. +/// +/// The two `*Trusted` kinds attach the same two inspectors through +/// [`MegaEvm::with_trusted_inspector`], so each pair differs only in whether +/// the shim measures. The gap between a pair is the measurement's whole cost, +/// and the trusted row is what the inspected path costs without it. #[derive(Clone, Copy)] pub enum InspectKind { NoOp, + NoOpTrusted, GethTracer, + GethTracerTrusted, +} + +/// A read-only declaration around `revm-inspectors`' geth tracer. +/// +/// The orphan rule keeps `TrustedObserver` from being implemented for +/// `TracingInspector` directly — from here both are foreign — so the +/// declaration is made about a local newtype that forwards every callback +/// unchanged. That is the same shape a downstream node has to write for the +/// same reason, which is why the row is worth running against it rather than +/// against a bare tracer that could not be declared at all. +pub struct TrustedGethTracer(TracingInspector); + +impl TrustedObserver for TrustedGethTracer {} + +impl Inspector for TrustedGethTracer +where + INTR: InterpreterTypes, + TracingInspector: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step_end(interp, context); + } + + fn log(&mut self, context: &mut CTX, log: Log) { + self.0.log(context, log); + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.log_full(interp, context, log); + } + + fn frame_start( + &mut self, + context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + self.0.frame_start(context, frame_input) + } + + fn frame_end( + &mut self, + context: &mut CTX, + frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + self.0.frame_end(context, frame_input, frame_result); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.0.selfdestruct(contract, target, value); + } } /// `MegaEvm` on the inspected frame loop, with the measurement shim live. @@ -223,11 +315,19 @@ impl Subject for MegaInspected { InspectKind::NoOp => { run_inspected(self.name, self.spec, workload, || NoOpInspector); } + InspectKind::NoOpTrusted => { + run_inspected_trusted(self.name, self.spec, workload, || NoOpInspector); + } InspectKind::GethTracer => { run_inspected(self.name, self.spec, workload, || { TracingInspector::new(TracingInspectorConfig::default_geth()) }); } + InspectKind::GethTracerTrusted => { + run_inspected_trusted(self.name, self.spec, workload, || { + TrustedGethTracer(TracingInspector::new(TracingInspectorConfig::default_geth())) + }); + } } } } @@ -240,24 +340,57 @@ where run_workload( name, workload, - || { - let mut context = MegaContext::new(build_pinned_db(&workload.accounts), spec); - context.modify_chain(|chain| zero_operator_fee!(chain)); - MegaEvm::new(context).with_inspector(make_inspector()) - }, - |evm, tx| { - let mut mega_tx = MegaTransaction(OpTransactionPinned::new(pinned_tx_env(tx))); - mega_tx.enveloped_tx = Some(Bytes::new()); - // `ExecuteEvm::transact` ignores `inspect` and stays on the plain - // loop; `inspect_tx` is the inspected loop the shim actually sits on. - let r = InspectEvm::inspect_tx(evm, mega_tx).expect("mega inspect"); - let success = r.result.is_success(); - black_box(r); - success - }, + || MegaEvm::new(inspected_context(spec, workload)).with_inspector(make_inspector()), + inspect_one_tx, + ); +} + +/// [`run_inspected`] through [`MegaEvm::with_trusted_inspector`]. +/// +/// Everything else is identical, which is the point: the pair of rows differs +/// only in whether the shim measures. +fn run_inspected_trusted( + name: &str, + spec: MegaSpecId, + workload: &Workload, + make_inspector: Make, +) where + I: Inspector> + TrustedObserver, + Make: FnOnce() -> I, +{ + run_workload( + name, + workload, + || MegaEvm::new(inspected_context(spec, workload)).with_trusted_inspector(make_inspector()), + inspect_one_tx, ); } +/// The context both inspected variants build their EVM over. +fn inspected_context( + spec: MegaSpecId, + workload: &Workload, +) -> MegaContext { + let mut context = MegaContext::new(build_pinned_db(&workload.accounts), spec); + context.modify_chain(|chain| zero_operator_fee!(chain)); + context +} + +/// Runs one transaction on the inspected loop, for either variant. +fn inspect_one_tx(evm: &mut MegaEvm, tx: &TxSpec) -> bool +where + I: Inspector>, +{ + let mut mega_tx = MegaTransaction(OpTransactionPinned::new(pinned_tx_env(tx))); + mega_tx.enveloped_tx = Some(Bytes::new()); + // `ExecuteEvm::transact` ignores `inspect` and stays on the plain + // loop; `inspect_tx` is the inspected loop the shim actually sits on. + let r = InspectEvm::inspect_tx(evm, mega_tx).expect("mega inspect"); + let success = r.result.is_success(); + black_box(r); + success +} + // // ============================================================================ // MegaWithEnv subject. From de480e475778dae72169d040a556d5607179af43 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 18:03:57 +0800 Subject: [PATCH 168/208] docs(evm): give the declared observer a contract section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What the declaration promises, why it is a trait and not a wrapper, what the debug verification does and does not reach, and the two lists a node needs: what may be declared (read-only tracers, through a newtype the orphan rule forces) and what may not (anything that intercepts, `OracleSetSlotInspector` first among them). Also the route: `EvmFactory::create_evm_with_inspector` cannot reach the fast path, which is both an inconvenience and a fence — the block executor builds through that factory and nothing else, so a declared observer cannot reach the canonical block path at all. The rule for adding an `Inspector` callback grows from one counterpart to three. --- crates/mega-evm/src/evm/AGENTS.md | 43 ++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index f6b034d1..bda8684e 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -51,7 +51,7 @@ Read the table by the *argument the rewrite reaches through*, not by the tool th | Rewrite shape | Support | Where it is booked | What enforcement sees | | --- | --- | --- | --- | -| Read-only observation | Supported, free | nothing | an empty ledger, and numbers identical to an uninspected run | +| Read-only observation | Supported, free — and, when the type is declared, not even measured (see **The declared observer** below) | nothing | an empty ledger, and numbers identical to an uninspected run | | Gas written into a live interpreter's counter (`initialize_interp`, `step`, `step_end`, `log_full`) | Supported | `InspectorLedger::gas`, at the callback boundary | nothing: the checkpoint baseline shifts by the same amount, and the gas clamp is re-derived on the spot so injected gas buys no compute headroom | | A frame input's `gas_limit`, raised or lowered (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::env`, at the callback boundary | nothing: a frame's compute budget comes from the tracker, not from its gas limit | | A frame input's semantic fields — target, caller, value, scheme, calldata, static flag (`frame_start`, `call`, `create`) | Supported | `InspectorLedger::interventions`, at the callback boundary | nothing: it changes what the frame does, not what it costs | @@ -98,6 +98,45 @@ Take it only if the shape turns out to be needed. Booking is a *reported* quantity throughout. No resource limit is ever compared against the ledger, and `MegaTransactionOutcome::compute_gas_enforced` comes off the enforcement lane rather than out of the reported total — so an inspector cannot buy a transaction headroom on any dimension. +### The declared observer + +Measuring costs about a nanosecond per reading per opcode, and there are sixteen readings taken twice per opcode, which adds between a third and two thirds to a production tracer's run. +An inspector type whose author has implemented `TrustedObserver` for it is delegated to without any of that: `MeasuredInspector::new_trusted`, reached through `MegaEvm::with_trusted_inspector`, builds a shim that forwards every callback and takes no reading. +The block guard is unchanged and needs no change — a declared type's ledger is empty by construction, which is the same answer measuring it would have given. + +**What the declaration promises.** Every callback of the type leaves the EVM exactly as it found it: nothing written to an interpreter's gas counter or its pending action, nothing to a frame's inputs, nothing to a frame result's classification, gas, output or metadata, nothing to a refund, and no frame answered with a synthetic outcome. +It may read whatever it likes and write to its own state. +That is exactly the table's first row, and a type that keeps it is measured to zero on every lane. + +**Why a declaration and not a detection.** The shim measures at a callback boundary precisely because it cannot see inside the callback, so "does this type write anything back" is not a question it can ask ahead of time — only one it can answer afterwards, at the cost the declaration exists to avoid. + +**Why a trait and not a wrapper.** A `Trusted` wrapper cannot be seen by the shim at all. +`MeasuredInspector::new` is generic over the inspector, `MegaEvm` names the shim as `MeasuredInspector` for whatever the caller chose, and `EvmFactory` hands inspectors in under an `I: Inspector` bound — so asking "is this `I` a `Trusted<_>`" would need either specialisation or a bound on every inspector `MegaETH` can be handed, including the foreign ones it can implement nothing for. +The question is therefore answered where it can be type-checked, at the one constructor whose bound is the declaration, and carried on the shim as a flag. +The wrapper's other weakness is worse than its unimplementability: `Trusted::new(inspector)` written once inside a generic function declares whatever that function is handed, which is how an RPC-supplied tracer would arrive on the fast path. +An implementation names a concrete type and no value can carry one. + +**Trust, and verify.** Under `debug_assertions` a declared type takes the measuring path anyway, and the shim asserts the ledger came back empty after every callback it measures. +`MegaEvm::execute_transaction` asks the same question once more at the end of the transaction, which is the backstop for a callback added later whose own verification was never written — the per-callback assert names the site, the transaction-level one cannot be missing. +Neither costs anything in release, where a declared type reaches no measuring body at all. +There is no behavioural fork between the two builds for a declaration that holds: the measurement of a type that writes nothing back is a sequence of reads that books nothing, so debug and release execute the same transaction and only a false declaration tells them apart — by panicking. +`tests/rex7/trusted_observer.rs` holds both halves, and `mega-state-test`'s `RunMode::ObserveTrusted` holds the three-way comparison against a plain and a measured run of the same observer. + +**What may be declared.** Read-only tracers: the `revm-inspectors` `TracingInspector` family (`debug_traceTransaction`, `trace_*`, the call and prestate tracers) and anything else that only records what it is shown. +`NoOpInspector` is declared here, being the only inspector this crate can reach. +The rest cannot be declared from here or from `mega-reth`, because the orphan rule wants one of the two to be local and neither the trait nor `TracingInspector` is: a node declares a newtype of its own that forwards every callback, which `benches/common/subject.rs` does for the `inspect_tracer_trusted` rows and is the shape to copy. +A wrapper that does nothing but forward may lift a declaration — `&mut T` does — but only from a concrete declared type; a wrapper that adds behaviour of its own is a type in its own right and has to be read on its own terms. + +**What may not be declared.** Anything that intercepts or rewrites, however little. +In `mega-reth` that is `OracleSetSlotInspector` (`crates/megaeth/engine/src/oracle/executor.rs`), which answers a call to the oracle contract with a synthetic `CallOutcome` — the "synthetic outcome that skips the frame entirely" row of the table above, and the clearest thing a declaration may not cover. +`ToggleInspector` (`crates/megaeth/rpc/src/toggle_inspector.rs`) forwards or does nothing, so it may be declared for a concrete declared inner type and never generically over its parameter. +The firewall `Tracer` (`crates/megaeth/payload/src/tx_firewall_trace/tracer.rs`) writes nothing back to the EVM and is a candidate, but it reads through `db_mut()` during `step`, so declaring it needs someone to have read what that does to the state cache — the marking is `mega-reth`'s to make, and this list is the input to it, not the decision. +Anything supplied by a request — a JavaScript tracer, an RPC-selected tracer config — cannot be declared at all, because a declaration is about a type and a request carries a value. + +**How a node reaches it.** `EvmFactory::create_evm_with_inspector` cannot: its bound is `I: Inspector` and its return type is fixed, so it has no way to select the constructor. +The route is `factory.create_evm(db, env).with_trusted_inspector(tracer)`, which keeps the factory's own dynamic precompiles and differs from the two-step untrusted form only in the method name. +The same limitation is a fence: `MegaBlockExecutorFactory` builds its EVM through those two factory methods and nothing else, so a declared observer cannot reach the canonical block-execution path at all — what it reaches is an EVM an embedder drives itself, which is what RPC tracing and off-band simulation are. + ### The window a counter edit reaches nothing through A gas-counter edit made while the interpreter is already holding a `Return` action is written into an object nobody reads again: revm's inspected loop runs `step_end` after the instruction that set the action, and the action carries its own snapshot of the gas, which is what becomes the frame's result. @@ -156,6 +195,8 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Add the shim's counterpart when adding an `Inspector` callback.** An unwrapped callback is an unmeasured hole, not a compile error. `tests/rex7/inspector_cheat_matrix.rs` enumerates every callback × shape pair and fails on one that is neither covered nor excused, which is what turns a new callback into a red test. + The counterpart is three things, not one: the measurement, the `if !self.measures()` delegation that skips it for a declared observer, and the `verify_trusted` call that checks the declaration held. + Leaving out the second costs a declared observer its fast path at that callback and nothing else; leaving out the third is the one that loses something, and it is what the transaction-level backstop in `MegaEvm::execute_transaction` exists to catch. - **On a revm bump, re-read the trait's method list against `tests/rex7/gas_surface.rs`'s `CALLBACKS`, and give any new callback a row in the shape table and a column in the cheat matrix.** This is the one direction no pin reaches, and it is the direction that adds reach. The field-level and variant-level pins in the same file cover everything else, and both fail loudly on their own. From d81deaf49317e34defbcf0f8b3fc7a8413227a6d Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 19:45:20 +0800 Subject: [PATCH 169/208] refactor(limit): drop the precompile settlement's unreachable conjure branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The branch books gas onto the ledger when a precompile performed more work than the envelope it consumed. That needs a halting precompile's classification rewritten to a success or a revert, which the frame-init refusal turns away before the settlement runs — both are gated on the same spec. Every arm now satisfies executed <= consumed, which a debug assertion states. --- crates/mega-evm/src/limit/limit.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 2a9a2c8a..0f9e8667 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -2084,16 +2084,12 @@ impl AdditionalLimit { /// /// and everything in it that was not the work performed is destroyed. /// - /// # Why the difference can go the other way - /// - /// A halting precompile's `Gas` is reset rather than spent down, so it reports the whole - /// budget as remaining even when `MegaETH` priced the call as having done work — the KZG fixed - /// fee for a failure raised inside verification is the one case that exists today. Told that - /// such a call succeeded, the caller reclaims all of it, fee included. The work stays on the - /// enforcing lane, because it really was performed; the fee nobody paid for is gas the rewrite - /// conjured, and goes to the ledger so the conservation law still closes. That direction is - /// unreachable without an inspector: no classification the EVM itself produces both prices - /// work and hands the budget back. + /// The difference cannot go the other way. A swallowed classification consumes the whole + /// forwarded envelope, which every arm's `executed` fits inside; a returned one leaves a `Gas` + /// normalised onto that envelope and spent down by exactly `executed`. The one shape that + /// would break both — a halting precompile whose classification is rewritten to a success, + /// whose reset `Gas` then hands the caller the fixed fee back — is refused before this runs, + /// because a precompile's result comes out of frame init. fn settle_precompile_envelope( &mut self, staged: PrecompileEnvelope, @@ -2106,8 +2102,11 @@ impl AdditionalLimit { evm_remaining }; let consumed = staged.forwarded.saturating_sub(returned); + debug_assert!( + consumed >= staged.executed, + "a precompile cannot perform more work than the envelope it consumed", + ); self.compute_gas.record_burned_gas(consumed.saturating_sub(staged.executed)); - self.inspector.result.book(i128::from(staged.executed.saturating_sub(consumed))); } /// Merges resource usage from a sandbox execution into this tracker. From a82d5c845ff10ccbc574df04fad75ab65aa19b1c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 19:48:12 +0800 Subject: [PATCH 170/208] test(rex7): pin the two rewrite surfaces nothing was checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing either left the whole suite green. Neither is dead code — the closed gas-surface table already claims both are booked as interventions — so what was missing was the check, not the mechanism. A frame result's returned output buffer gets a fixture: the caller reads a word no frame produced, and the ledger must say so. A finished call's three fields outside its result move nothing MegaETH produces today, so they get a per-field comparison instead, in the same shape as the interpreter's reading cases. --- crates/mega-evm/src/evm/inspector.rs | 54 +++++++++++++ .../mega-evm/tests/rex7/ledger_blind_spots.rs | 79 +++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index c2e7655f..96932e3c 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -1668,6 +1668,60 @@ mod tests { } } + /// One rewrite per field a finished call carries besides its `InterpreterResult`. + /// + /// `CallMetadata` is the same kind of snapshot as [`WorkingSet`] over a different object, and + /// it fails the same way: a field held and never compared is a rewrite the shim is handed and + /// books nothing for. Three of these move nothing `MegaETH` produces today — it runs with + /// EIP-8037 off and no wired precompile emits a log — so no fixture can show their effect, and + /// a per-field check is the only thing that holds the claim that they are seen at all. + const OUTCOME_CASES: [(&str, fn(&mut CallOutcome)); 4] = [ + ("memory_offset", |outcome| outcome.memory_offset = 1..2), + ("was_precompile_called", |outcome| outcome.was_precompile_called = true), + ("precompile_call_logs", |outcome| { + outcome.precompile_call_logs.push(Log::new_unchecked(OTHER, Vec::new(), Bytes::new())); + }), + ("charged_new_account_state_gas", |outcome| { + outcome.charged_new_account_state_gas = true; + }), + ]; + + fn call_outcome() -> CallOutcome { + CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + revm::interpreter::Gas::new(0), + ), + 0..0, + ) + } + + /// Every field of a finished call outside its result is compared, and each one on its own. + /// + /// The derived `PartialEq` is what makes a new upstream field visible: it joins the struct, + /// joins the equality, and the identical pair below still agrees until someone gives it a + /// case. What the cases add is that each existing field is compared *individually* — one of + /// them moving is enough on its own. + #[test] + fn test_every_finished_call_field_outside_the_result_is_compared() { + let base = call_outcome(); + assert_eq!( + CallMetadata::of(&base), + CallMetadata::of(&call_outcome()), + "two identical outcomes must compare equal", + ); + for (name, rewrite) in OUTCOME_CASES { + let mut moved = call_outcome(); + rewrite(&mut moved); + assert_ne!( + CallMetadata::of(&base), + CallMetadata::of(&moved), + "a rewritten {name} must be visible to the shim", + ); + } + } + /// The case list covers the snapshot, and covers each reading once. /// /// [`moved`]'s destructuring is what keeps [`WorkingSet`] and this module in step at compile diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index 25b4172a..b4c92eb2 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -238,6 +238,85 @@ fn test_a_moved_return_range_is_booked() { ); } +// --- a frame's returned output ------------------------------------------------------------------ + +/// The word a rewritten output buffer feeds the caller instead of the one the callee returned. +const FORGED_OUTPUT: u64 = 0xdead; + +/// Replaces the output buffer a finished call hands back, leaving its classification alone. +/// +/// The classification and the remaining gas are what every other lane reads. The output is +/// neither, and it is what the caller copies into its own memory. +#[derive(Default)] +struct ForgeCallOutput { + fired: u32, +} + +impl Inspector for ForgeCallOutput { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.result.output = Bytes::from(U256::from(FORGED_OUTPUT).to_be_bytes::<32>().to_vec()); + self.fired += 1; + } +} + +/// ★ A call outcome whose returned output was replaced is not an all-zero ledger. +#[test] +fn test_a_forged_call_output_is_booked() { + // Call the callee for one word of output, then store what landed there. + let code = BytecodeBuilder::default() + .push_number(32u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(0u64) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db(), limits); + let mut inspector = ForgeCallOutput::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the caller reads what the callee returned", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(FORGED_OUTPUT), + "with it, the caller reads a word no frame produced", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a replaced output buffer changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + /// Reports a different address than the one the creation deployed to. #[derive(Default)] struct MoveDeploymentAddress { From 306cb250fac83b8e9f54c2f1f5d0eec8790f6533 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 19:51:17 +0800 Subject: [PATCH 171/208] refactor(evm): measure a frame's entry and its ending in one body each The three entry callbacks and the three ending callbacks were six copies of two booking sequences, which is six places for the boundary to be measured differently at. Each becomes one body the three call sites hand their own readings to. --- crates/mega-evm/src/evm/inspector.rs | 145 +++++++++++++++++---------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 96932e3c..1956b4c6 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -1271,6 +1271,65 @@ impl MeasuredInspector { } } +/// Books what an entry callback did to the frame it was handed. +/// +/// `frame_start`, `call` and `create` are one measurement over three argument types, and this is +/// the body: the envelope moved, whether anything else about the inputs came back changed, and — +/// when the callback answered the frame itself — the refund its synthetic outcome carries. +/// `intercepted_refund` is `Some` exactly when it did. +#[inline] +fn book_frame_entry( + context: &MegaContext, + before: Option, + after: Option, + intercepted_refund: Option, + rewritten: bool, +) { + let intercepted = intercepted_refund.is_some(); + book_env_adjustment(context, before, after, intercepted); + if let Some(refund) = intercepted_refund { + book_synthetic_refund(context, refund); + } + book_intervention(context, intercepted || rewritten); +} + +/// What a finished frame reads as on the way into an `*_end` callback. +/// +/// The three `*_end` callbacks are one measurement over three argument types, the way the three +/// entry callbacks are. `M` is whatever the object carries outside the `InterpreterResult` the +/// three of them share. +struct FrameEnding { + result: InstructionResult, + output: Bytes, + metadata: M, + refund: i64, +} + +impl FrameEnding { + /// Books what the callback did to a finished frame, and refuses the rewrites that are + /// forbidden. + /// + /// `is_create` selects the second refusal. Both read the classification as it stood on the way + /// in, and the frame-init one runs first because it restores whatever it refuses — which + /// leaves the creation refusal nothing to see. + #[inline] + fn book( + self, + context: &mut MegaContext, + result: &mut InterpreterResult, + metadata: M, + is_create: bool, + ) { + book_refund(context, self.refund, result.gas.refunded()); + book_intervention(context, result_rewritten((self.result, &self.output), result)); + book_intervention(context, metadata != self.metadata); + reject_forbidden_frame_init_rewrite(context, self.result, result); + if is_create { + reject_forbidden_create_rewrite(context, self.result, result); + } + } +} + impl Inspector, INTR> for MeasuredInspector where DB: Database, @@ -1346,16 +1405,13 @@ where } let before = frame_input.clone(); let outcome = self.inner.frame_start(context, frame_input); - book_env_adjustment( + book_frame_entry( context, frame_input_gas_limit(&before), frame_input_gas_limit(frame_input), - outcome.is_some(), + outcome.as_ref().map(|outcome| outcome.gas().refunded()), + frame_input_rewritten(before, frame_input), ); - if let Some(outcome) = &outcome { - book_synthetic_refund(context, outcome.gas().refunded()); - } - book_intervention(context, outcome.is_some() || frame_input_rewritten(before, frame_input)); verify_trusted(self.trusted, context, "frame_start"); outcome } @@ -1370,24 +1426,16 @@ where if !self.measures() { return self.inner.frame_end(context, frame_input, frame_result); } - let before = frame_result.instruction_result(); - let output = frame_result.interpreter_result().output.clone(); - let metadata = OutcomeMetadata::of(frame_result); - let refund_before = frame_result.gas().refunded(); + let entry = FrameEnding { + result: frame_result.instruction_result(), + output: frame_result.interpreter_result().output.clone(), + metadata: OutcomeMetadata::of(frame_result), + refund: frame_result.gas().refunded(), + }; self.inner.frame_end(context, frame_input, frame_result); - book_refund(context, refund_before, frame_result.gas().refunded()); - book_intervention( - context, - result_rewritten((before, &output), frame_result.interpreter_result()), - ); - book_intervention(context, OutcomeMetadata::of(frame_result) != metadata); - // `frame_end` runs after `call_end` / `create_end` and is the last chance to rewrite a - // classification, so both refusals apply here too. The frame-init one runs first: it - // restores whatever it refuses, which leaves the creation refusal below nothing to see. - reject_forbidden_frame_init_rewrite(context, before, frame_result.interpreter_result_mut()); - if let FrameResult::Create(outcome) = frame_result { - reject_forbidden_create_rewrite(context, before, &mut outcome.result); - } + let metadata = OutcomeMetadata::of(frame_result); + let is_create = matches!(frame_result, FrameResult::Create(_)); + entry.book(context, frame_result.interpreter_result_mut(), metadata, is_create); verify_trusted(self.trusted, context, "frame_end"); } @@ -1402,16 +1450,13 @@ where } let before = inputs.clone(); let outcome = self.inner.call(context, inputs); - book_env_adjustment( + book_frame_entry( context, Some(before.gas_limit), Some(inputs.gas_limit), - outcome.is_some(), + outcome.as_ref().map(|outcome| outcome.result.gas.refunded()), + call_inputs_rewritten(before, inputs), ); - if let Some(outcome) = &outcome { - book_synthetic_refund(context, outcome.result.gas.refunded()); - } - book_intervention(context, outcome.is_some() || call_inputs_rewritten(before, inputs)); verify_trusted(self.trusted, context, "call"); outcome } @@ -1429,14 +1474,15 @@ where if !self.measures() { return self.inner.call_end(context, inputs, outcome); } - let before = (outcome.result.result, outcome.result.output.clone()); - let metadata = CallMetadata::of(outcome); - let refund_before = outcome.result.gas.refunded(); + let entry = FrameEnding { + result: outcome.result.result, + output: outcome.result.output.clone(), + metadata: CallMetadata::of(outcome), + refund: outcome.result.gas.refunded(), + }; self.inner.call_end(context, inputs, outcome); - book_refund(context, refund_before, outcome.result.gas.refunded()); - book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); - book_intervention(context, CallMetadata::of(outcome) != metadata); - reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); + let metadata = CallMetadata::of(outcome); + entry.book(context, &mut outcome.result, metadata, false); verify_trusted(self.trusted, context, "call_end"); } @@ -1451,16 +1497,13 @@ where } let before = inputs.clone(); let outcome = self.inner.create(context, inputs); - book_env_adjustment( + book_frame_entry( context, Some(before.gas_limit()), Some(inputs.gas_limit()), - outcome.is_some(), + outcome.as_ref().map(|outcome| outcome.result.gas.refunded()), + create_inputs_rewritten(before, inputs), ); - if let Some(outcome) = &outcome { - book_synthetic_refund(context, outcome.result.gas.refunded()); - } - book_intervention(context, outcome.is_some() || create_inputs_rewritten(before, inputs)); verify_trusted(self.trusted, context, "create"); outcome } @@ -1479,17 +1522,15 @@ where if !self.measures() { return self.inner.create_end(context, inputs, outcome); } - let before = (outcome.result.result, outcome.result.output.clone()); - let address = outcome.address; - let refund_before = outcome.result.gas.refunded(); + let entry = FrameEnding { + result: outcome.result.result, + output: outcome.result.output.clone(), + metadata: outcome.address, + refund: outcome.result.gas.refunded(), + }; self.inner.create_end(context, inputs, outcome); - book_refund(context, refund_before, outcome.result.gas.refunded()); - book_intervention(context, result_rewritten((before.0, &before.1), &outcome.result)); - book_intervention(context, outcome.address != address); - // The frame-init refusal runs first, and restores whatever it refuses — so a creation - // refused as an init result is not counted a second time by the refusal below. - reject_forbidden_frame_init_rewrite(context, before.0, &mut outcome.result); - reject_forbidden_create_rewrite(context, before.0, &mut outcome.result); + let address = outcome.address; + entry.book(context, &mut outcome.result, address, true); verify_trusted(self.trusted, context, "create_end"); } From 2641614a4a67da38546a1d8611d8eb6dd4e94f77 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:03:42 +0800 Subject: [PATCH 172/208] fix(limit): book a staged gas edit's traffic where it is measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inspector could delete a contract and leave the ledger reading all zeros. It lowers the gas a construction frame's pending Return action carries at step_end; classify_frame_action then charges the code deposit out of that same action, the creation can no longer pay it, and the frame becomes an OutOfGas that deploys nothing. The classification and the output are untouched, so no intervention is booked; the edit is staged for the settlement point, and that point declines to book it because the result it finally sees is a swallowed one. Six lanes zero, both counters zero, and the block guard admits it. The contract the result lane rested on — that an action is the frame's result a moment later — does not hold for a creation, because the deposit charge sits between the two. Lane now separates the two questions it was answering with one booking: traffic is booked where the edit is measured, and movement where the classification says whether the envelope moved. The staged halves of the result and envelope lanes book their own traffic at the boundary, so a second edit in the same frame cannot sum the first one away before either is counted. Anchors: a drained construction action, and a pair cancelling across the two windows. Both were red before this. The chaos pool's action-gas shapes join the always-booked gate, and its envelope move reports a saturated no-op as not applied so the gate stays exact. --- crates/mega-evm/src/evm/execution.rs | 21 +- crates/mega-evm/src/evm/inspector.rs | 11 +- crates/mega-evm/src/limit/inspector_ledger.rs | 26 ++- crates/mega-evm/src/limit/limit.rs | 22 ++- .../tests/rex7/inspector_settlement_window.rs | 25 ++- .../mega-evm/tests/rex7/interception_gas.rs | 17 +- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 184 +++++++++++++++++- crates/mega-state-test/src/chaos.rs | 22 ++- 8 files changed, 304 insertions(+), 24 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index 6585cc7e..e6e41a23 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -2268,8 +2268,13 @@ mod mutation_tests { consume_synthetic_limit_frame(evm.ctx_ref(), result); } - /// The mirror: a rejection the caller reclaims nothing from books nothing, and the sender's - /// rescue is taken on the envelope the transaction funded rather than on the raised figure. + /// The mirror: a rejection the caller reclaims nothing from moves the envelope by nothing, and + /// the sender's rescue is taken on the envelope the transaction funded rather than on the + /// raised figure. + /// + /// The lane's two halves separate here. Its net is zero, because the halting rejection hands + /// the raise to nobody; its gross is not, because the inspector still made the edit and the + /// block guard's question is whether the transaction was left alone. #[test] fn test_a_halting_guard_rescues_the_funded_envelope_and_not_the_raised_one() { let mut evm = @@ -2280,10 +2285,16 @@ mod mutation_tests { panic!("latched limit must override the inspector result"); }; let limit = evm.ctx_ref().additional_limit.borrow(); + let result_lane = limit.inspector_ledger().result; + assert_eq!( + result_lane.net(), + 0, + "a halting rejection hands nothing back, so the edit reaches the envelope not at all", + ); assert_eq!( - limit.inspector_ledger().result, - Lane::default(), - "a halting rejection hands nothing back, so the edit reaches nothing", + result_lane.gross(), + u128::from(RAISE), + "but the inspector still wrote it, and the guard has to see that", ); assert_eq!( limit.rescued_gas, TEST_GAS_LIMIT, diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 1956b4c6..5ae3e7c5 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -599,10 +599,15 @@ fn book_env_adjustment( if let (true, Some(before)) = (intercepted, before) { context.additional_limit.borrow_mut().stage_inspector_interception_envelope(before); } - if staged + callback == 0 { - return; + // Booked separately rather than summed first: the two were written in different callbacks, so + // an envelope raised in one and lowered back in the other is two edits, not none. The staged + // half already counted its own traffic where it was measured, so only its movement lands here. + if staged != 0 { + context.additional_limit.borrow_mut().record_staged_inspector_env_movement(staged); + } + if callback != 0 { + context.additional_limit.borrow_mut().record_inspector_env_adjustment(callback); } - context.additional_limit.borrow_mut().record_inspector_env_adjustment(staged + callback); } /// Books one rewrite that changes what the execution *did* rather than what it cost — see diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 9bf3a348..1efc2132 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -81,9 +81,33 @@ impl Lane { /// Books one movement on this lane. #[inline] pub(crate) const fn book(&mut self, delta: i128) { - self.net = self.net.saturating_add(delta); + self.book_crossing(delta); + self.book_movement(delta); + } + + /// Records that an edit of `delta` crossed a callback boundary, without saying yet whether it + /// moved the transaction's envelope. + /// + /// The pair with [`book_movement`](Self::book_movement), for the lanes that cannot answer the + /// second question where they answer the first. An edit to a frame's result moves the envelope + /// only if the frame hands its remainder back, which the classification decides and no + /// boundary knows — so the traffic is recorded here, at the boundary, and the movement is + /// booked at the frame's settlement point. + /// + /// Splitting them is what keeps the guard's question answerable on those lanes. An edit whose + /// frame then halts moves nothing and must stay out of the net, but it is still an edit the + /// inspector made, and one that can change what the transaction produces before the + /// classification catches up with it. + #[inline] + pub(crate) const fn book_crossing(&mut self, delta: i128) { self.gross = self.gross.saturating_add(delta.unsigned_abs()); } + + /// Books a movement whose traffic [`book_crossing`](Self::book_crossing) already recorded. + #[inline] + pub(crate) const fn book_movement(&mut self, delta: i128) { + self.net = self.net.saturating_add(delta); + } } /// What an inspector conjured, destroyed, rewrote, or had refused, as measured at the callback diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 0f9e8667..d81039f0 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -622,6 +622,14 @@ impl AdditionalLimit { self.inspector.env.book(delta); } + /// Books the envelope movement of an edit whose traffic + /// [`stage_inspector_action_env_adjustment`](Self::stage_inspector_action_env_adjustment) + /// already counted. + #[inline] + pub(crate) fn record_staged_inspector_env_movement(&mut self, delta: i128) { + self.inspector.env.book_movement(delta); + } + /// Stages an adjustment an inspector made to the gas a *terminating* pending action carries. /// /// The action is the object that becomes the frame's result, so this is the same measurement @@ -630,6 +638,10 @@ impl AdditionalLimit { /// edit moves anything at all depends on the classification the caller ends up seeing. #[inline] pub(crate) fn stage_inspector_action_result_adjustment(&mut self, delta: i128) { + // The traffic is booked here, at the boundary that measured it, and only the movement + // waits for the classification. Two edits to one frame's action would otherwise sum to + // nothing before either was counted. + self.inspector.result.book_crossing(delta); self.staged_action_result_gas += delta; } @@ -641,6 +653,7 @@ impl AdditionalLimit { /// the edit apart from an interception. #[inline] pub(crate) fn stage_inspector_action_env_adjustment(&mut self, delta: i128) { + self.inspector.env.book_crossing(delta); self.staged_action_env_gas += delta; } @@ -1598,8 +1611,11 @@ impl AdditionalLimit { // Taken unconditionally, so a staged envelope can never outlive the frame that staged it. let staged_precompile = self.staged_precompile.take(); // Everything an inspector wrote into this result, whether it wrote it into the frame's - // terminating action or into the result the action became. The two are the same number - // measured on either side of the classification, so they settle as one. + // terminating action or into the result the action became. The two settle as one number, + // because whether either moved the envelope is the one question the classification below + // answers. Their traffic is not summed: the staged half booked its own at the boundary + // that measured it, and this books the last callback's. + self.inspector.result.book_crossing(inspector_gas_delta); let inspector_gas_delta = inspector_gas_delta + core::mem::take(&mut self.staged_action_result_gas); // The gas the EVM itself left in this result. Every settlement below is defined against @@ -1671,7 +1687,7 @@ impl AdditionalLimit { if destroyed::remaining_is_destroyed(result.instruction_result()) { evm_remaining } else { - self.inspector.result.book(delta); + self.inspector.result.book_movement(delta); result.gas().remaining() } } diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index c5add5aa..a9ec7ab8 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -517,8 +517,12 @@ fn test_lowering_a_returning_frames_pending_action_is_booked() { } /// The classification branch: a halting frame hands nothing back, so an edit to the gas its action -/// carries moves nothing and must not be booked — and the remainder it destroys is the EVM's own -/// number, not the edited one. +/// carries moves nothing and must not reach the lane's *net* — and the remainder it destroys is +/// the EVM's own number, not the edited one. +/// +/// The lane's gross carries the edit all the same. Whether it moved the envelope is what the +/// classification decides; whether the inspector made it is not, and the block guard asks the +/// second question. #[test] fn test_editing_a_halting_frames_pending_action_moves_nothing() { let callee = BytecodeBuilder::default().append(INVALID).build(); @@ -535,8 +539,21 @@ fn test_editing_a_halting_frames_pending_action_moves_nothing() { assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); assert_eq!( edited.inspector_ledger, - InspectorLedger::default(), - "a halting frame hands its remainder to nobody, so an edit to it reaches nobody", + InspectorLedger { + result: Lane::of(0, u128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "a halting frame hands its remainder to nobody, so the edit moves the envelope by nothing \ + — and the lane still has to show it was made", + ); + assert_eq!( + edited.inspector_ledger.conjured_gas(), + 0, + "the conservation law reads the net, which is what stays zero", + ); + assert!( + !edited.inspector_ledger.is_zero(), + "and the block guard reads the gross, which is what does not", ); assert_eq!( edited.destroyed, plain.destroyed, diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index 10bebd0f..d6b11af0 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -311,6 +311,10 @@ fn test_an_echoing_interception_books_no_gas_at_all() { /// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes /// nothing the transaction spends — and the envelope is destroyed whole. +/// +/// What the outcome claimed is still traffic on the result lane: the sizings below differ from the +/// envelope by different amounts, and each one is an edit the inspector made whether or not the +/// classification let it reach anybody. #[test] fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { @@ -319,10 +323,19 @@ fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); + assert_eq!( + reading.ledger.conjured_gas(), + 0, + "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", + ); assert_eq!( reading.ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "{sizing:?}: a halting frame hands nothing back, so no gas lane may move", + InspectorLedger { + interventions: 1, + result: Lane::of(0, reading.ledger.result.gross()), + ..InspectorLedger::default() + }, + "{sizing:?}: and no lane but the result lane's traffic may carry anything", ); assert_eq!( reading.destroyed, FORWARDED, diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index b4c92eb2..dcf15876 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -46,8 +46,9 @@ use revm::{ context::{Cfg, ContextTr}, interpreter::{ interpreter::EthInterpreter, - interpreter_types::{Jumps, MemoryTr, ReturnData}, - CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterTypes, + interpreter_types::{InputsTr, Jumps, LoopControl, MemoryTr, ReturnData}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterAction, + InterpreterTypes, }, Inspector, }; @@ -381,6 +382,185 @@ fn test_a_rewritten_deployment_address_is_booked() { ); } +// --- a construction frame's pending action ------------------------------------------------------- + +/// Drains the gas a construction frame's pending `Return` action carries. +/// +/// The contract this module's other cases rest on — that an action is the frame's result a moment +/// later, so an edit to it settles with that result — does not hold for a creation. Between the +/// two, `classify_frame_action` charges the code deposit out of the gas *this action* carries, and +/// a creation that cannot pay it becomes an `OutOfGas` that deploys nothing. So this edit changes +/// what the transaction produces, and it does it by a route that leaves the classification and the +/// output the boundary compares exactly where they were. +#[derive(Default)] +struct DrainConstructionAction { + fired: u32, +} + +impl Inspector for DrainConstructionAction { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + // A construction frame runs no deployed code, so it has no bytecode address. + if self.fired > 0 || interp.input.bytecode_address().is_some() { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + let remaining = result.gas.remaining(); + assert!( + result.gas.record_regular_cost(remaining), + "the fixture must be able to drain the action it found", + ); + self.fired += 1; + } +} + +/// ★ A construction frame whose pending action was drained is not an all-zero ledger. +/// +/// Every lane the boundary reads stays put: the action's classification and output are untouched, +/// so nothing is an intervention; the gas edit is staged for the frame's settlement point, and +/// that point declines to book it because the result it finally sees is a swallowed one. The +/// deposit the drained action could no longer pay is what turned it into one. +#[test] +fn test_a_drained_construction_action_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let mut inspector = DrainConstructionAction::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); + assert_ne!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "without the edit the creation must succeed", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it the creation cannot pay its code deposit and deploys nothing", + ); + assert_ne!( + plain.gas_used, cheated.gas_used, + "and the receipt the sender is billed on moves with it", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose contract an inspector deleted must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Gas a cancelling pair moves through the result lane's two windows. +const ACTION_DELTA: u64 = 700; + +/// Raises the gas an inner call frame's pending `Return` action carries, then takes the same +/// amount back out of the result that action became. +/// +/// The two windows are one lane and one frame, and the pair nets to zero. They are still two +/// edits, made in two different callbacks, and the lane's traffic is what says so — the sum alone +/// reads as an inspector that did nothing. +#[derive(Default)] +struct CancellingActionAndResultEdits { + raised: u32, + lowered: u32, +} + +impl Inspector for CancellingActionAndResultEdits { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.raised > 0 || interp.input.bytecode_address() != Some(&CALLEE) { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + result.gas.erase_cost(ACTION_DELTA); + self.raised += 1; + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.lowered > 0 || inputs.target_address != CALLEE { + return; + } + assert!( + outcome.result.gas.record_regular_cost(ACTION_DELTA), + "the fixture must leave the result enough gas for the removal to land", + ); + self.lowered += 1; + } +} + +/// ★ An edit staged at one callback and undone at the next is two edits, not none. +/// +/// Nothing about this transaction changes: a call frame's remaining gas is read by nobody between +/// the two windows, so the pair really is invisible in what the transaction produces. That is the +/// point — the lane's traffic is the only thing that separates it from an inspector that never +/// ran, and on a *creation* frame the same pair is the shape that deletes a contract. +#[test] +fn test_cancelling_action_and_result_edits_are_booked() { + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let callee = BytecodeBuilder::default().append(STOP).build(); + let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + + let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); + let plain = transact(MegaSpecId::REX7, db(), limits); + let mut inspector = CancellingActionAndResultEdits::default(); + let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + + assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); + assert_eq!( + cheated.gas_used, plain.gas_used, + "the pair cancels, so the receipt really is the one the EVM would have produced", + ); + assert_eq!( + cheated.inspector_ledger.conjured_gas(), + 0, + "and the conservation law must read the net, which is zero", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the guard must still see that the lane carried two edits: {:?}", + cheated.inspector_ledger, + ); + assert_eq!( + cheated.inspector_ledger.result.gross(), + 2 * u128::from(ACTION_DELTA), + "one edit in each window, counted where each was made", + ); +} + // --- two edits to one lane, in opposite directions ----------------------------------------------- /// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 0a612316..7ba1d87b 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -369,6 +369,13 @@ impl ChaosShape { // length the current one does not have. Self::SkipOpcode | Self::RewriteReturnData | + // Gas written into a pending action is staged for the point that can say whether + // it moved the envelope, and the lane's traffic is booked at this boundary rather + // than at that point — so a staged edit shows up whatever the frame's + // classification turns out to be. The draw is withheld when the action cannot + // take the edit, so every applied one moves a number. + Self::RaiseActionGas | + Self::LowerActionGas | // The rewrite comparison books it before the shim refuses it, and the refusal is // counted beside that — so the ledger carries two reasons to be non-zero. Self::MoveInitResultClass @@ -1121,13 +1128,20 @@ fn edit_pending_action_gas( result.gas.record_regular_cost(amount) } } + // Both saturate, so an envelope already at either end does not move. Report that as "not + // applied" rather than spending the budget on it: the ledger gate below is stated over + // shapes that always book, and a mutation that moved nothing has nothing to book. Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { - inputs.gas_limit = move_envelope(inputs.gas_limit, raise, amount); - true + let moved = move_envelope(inputs.gas_limit, raise, amount); + let applied = moved != inputs.gas_limit; + inputs.gas_limit = moved; + applied } Some(InterpreterAction::NewFrame(FrameInput::Create(inputs))) => { - inputs.set_gas_limit(move_envelope(inputs.gas_limit(), raise, amount)); - true + let moved = move_envelope(inputs.gas_limit(), raise, amount); + let applied = moved != inputs.gas_limit(); + inputs.set_gas_limit(moved); + applied } _ => false, } From 399cbc21eb922e0ab73117789751e11fd039fe8c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:04:53 +0800 Subject: [PATCH 173/208] docs(evm): cut the shim's rustdoc to its constraints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module's prose had grown a narrative layer: what each phase added, what an earlier version of a snapshot held, how a measurement compares against the one it replaced. None of it constrains the code. What is left is the reasoning a reader cannot re-derive from the signature — why a boundary is enough, why a lane is split the way it is, why a reading is taken where it is. Verified as comment-only: the file's non-comment lines are byte-identical. --- crates/mega-evm/src/evm/inspector.rs | 613 +++++++++------------------ 1 file changed, 207 insertions(+), 406 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 5ae3e7c5..ec92389b 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -1,66 +1,31 @@ //! The measurement shim every inspector handed to `MegaETH` is wrapped in. //! -//! # Why a shim -//! //! An inspector is not a passive observer. Every callback that receives a live interpreter can //! write to its gas counter and to the action it is holding, and every callback that receives a //! frame's inputs can change the gas limit the frame is about to be built with. `MegaETH` meters -//! compute gas by watching those exact counters, and derives what a transaction destroyed from the -//! envelope it spent, so an unmeasured edit shows up as the EVM having done less work than it did, -//! or as a transaction having spent less gas than it did. -//! -//! # Why the callback boundary is enough -//! -//! The EVM does not execute inside an inspector callback. Anything that changes between the moment -//! the shim delegates to the user's inspector and the moment control comes back is therefore the -//! inspector's doing — not by attribution, but by construction. The shim snapshots the counters it -//! cares about on the way in, compares on the way out, and books the difference. +//! compute gas by watching those exact counters and derives what a transaction destroyed from the +//! envelope it spent, so an unmeasured edit reads as the EVM having done less work than it did. //! -//! That is why the shim lives at the `Inspector` implementation layer and not inside revm's -//! dispatch loop: wrapping the object is sufficient to sit on every boundary, and mirroring -//! `inspect_instructions` would take on a core dispatch loop for no additional reach. +//! The EVM does not execute inside a callback, so anything that changes between the moment the +//! shim delegates and the moment control comes back is the inspector's by construction. The shim +//! snapshots on the way in, compares on the way out, and books the difference — which is why it +//! sits at the `Inspector` implementation layer: wrapping the object reaches every boundary, and +//! mirroring `inspect_instructions` would take on a core dispatch loop for no additional reach. //! -//! # What the shim does with what it measures -//! -//! - Interpreter-counter edits go to [`AdditionalLimit::record_inspector_gas_adjustment`], which -//! books them, keeps them out of the compute-gas measurement, and re-derives the gas clamp so -//! injected gas is not spendable past the compute headroom. -//! - Pending-action edits go to [`book_pending_action`], which routes them by the action the -//! callback left behind — a frame's counter, a child's envelope, or the result its caller will -//! reclaim from. The counter and the action between them hold everything a frame has, and -//! [`held`] is the identity the two lanes split. -//! - Frame-envelope edits go to [`AdditionalLimit::record_inspector_env_adjustment`]. -//! - Refund edits go to [`book_refund`], on their own lane: a refund moves what the sender pays -//! without moving the envelope the conservation law is stated over, so it needs a lane of its own -//! and no term in the law. [`held_refund`] is the reading it is taken against. -//! - The EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and `state_gas_spent`, and a frame -//! input's `reservoir` — is *not* measured here. `MegaETH` runs with EIP-8037 off, so it produces -//! none of it and there is no difference to take; the transaction's own settlement point books -//! whatever of it survives. See -//! [`InspectorLedger::reservoir`](crate::InspectorLedger::reservoir). -//! - A callback that answers a frame itself stages the envelope it was handed, through -//! [`AdditionalLimit::stage_inspector_interception_envelope`], so that the frame init it -//! short-circuited can settle the gas its synthetic outcome carries against what the transaction -//! funded. -//! - Rewrites that move no gas go to [`book_intervention`]: a frame result's classification or -//! output, a frame's inputs outside their gas limit, a finished outcome's metadata -//! ([`OutcomeMetadata`]) — and every constant-time reading the shim can take off a live -//! interpreter ([`WorkingSet`]), which is what makes a frame's memory grown for free, its program -//! counter stepped past an instruction, or its return buffer conjured all visible. -//! - One rewrite shape is refused outright: see [`MeasuredInspector::create_end`]. -//! -//! # The one inspector the shim does not measure +//! Where each measurement goes is [`InspectorLedger`](crate::InspectorLedger)'s own documentation. +//! Two things here are not differences across a boundary. A callback that answers a frame itself +//! stages the envelope it was handed, because no frame is built and there is no other side to +//! compare against. And the EIP-8037 state-gas dimension is settled once by the transaction, +//! because revm propagates it by replacement and a boundary difference would book edits the EVM +//! goes on to erase. //! //! An inspector type whose author has declared it read-only, by implementing [`TrustedObserver`] -//! in source, is delegated to without any of the above. The declaration is the only way to reach -//! that path: it is a bound on [`MeasuredInspector::new_trusted`], so an inspector chosen by a -//! request or by configuration cannot arrive on it. Debug builds take the measuring path anyway -//! and assert the ledger stayed empty, so a type declared wrongly fails where it is exercised -//! rather than where it is deployed. +//! in source, is delegated to without any of this. Debug builds measure it anyway and assert the +//! ledger stayed empty, so a wrong declaration fails where it is exercised rather than where it is +//! deployed. //! -//! Nothing here changes what the inspector is allowed to do to the EVM, and nothing here runs on -//! the uninspected path — revm's plain interpreter loop never calls an inspector at all. - +//! Nothing here changes what an inspector may do to the EVM, and nothing here runs on the +//! uninspected path — revm's plain interpreter loop never calls an inspector at all. #[cfg(not(feature = "std"))] use alloc as std; use std::{string::String, vec::Vec}; @@ -102,56 +67,33 @@ pub const FORBIDDEN_FRAME_INIT_REWRITE: &str = /// A promise, made in source about one inspector type, that none of its callbacks writes anything /// back to the EVM. /// -/// # What implementing this declares -/// -/// Every callback of this type leaves the EVM exactly as it found it: it writes nothing to an -/// interpreter's gas counter or its pending action, nothing to a frame's inputs, nothing to a +/// Every callback of a declared type leaves the EVM exactly as it found it: it writes nothing to +/// an interpreter's gas counter or its pending action, nothing to a frame's inputs, nothing to a /// frame result's classification, gas, output or metadata, nothing to a refund, and it never -/// answers a frame with a synthetic outcome. It may read whatever it likes and it may write to -/// its own state. That is the whole of the promise, and it is exactly the "read-only observation" -/// row of the shape table in `evm/AGENTS.md`. -/// -/// A type that keeps the promise is measured to zero on every lane of -/// [`InspectorLedger`](crate::InspectorLedger), which is the same thing the shim would have -/// concluded by measuring it — so declaring it changes what the measurement *costs* and never -/// what it *says*. -/// -/// # What it buys -/// -/// [`MeasuredInspector`] delegates to a declared type without taking any of its readings, which -/// puts the inspected path back on revm's own cost. Measuring costs about a nanosecond per -/// reading per opcode and there are sixteen readings taken twice per opcode, which adds between a -/// third and two thirds to a production tracer's run. -/// -/// # Why a declaration and not a detection +/// answers a frame with a synthetic outcome. It may read whatever it likes and write to its own +/// state. /// -/// There is nothing to detect. The shim measures at a callback boundary precisely because it -/// cannot see inside the callback, so "does this type write anything back" is not a question it -/// can ask ahead of time — only one it can answer afterwards, at the cost the declaration exists -/// to avoid. +/// What the declaration buys is the cost of the measurement, never its verdict: a type that keeps +/// the promise measures to zero on every lane anyway. It is a declaration rather than a detection +/// because the shim measures at a boundary precisely because it cannot see inside a callback, so +/// "does this write anything back" is not a question it can ask ahead of time. /// /// # The rules this trait is under /// /// - **No blanket implementation, ever.** Each implementation names one concrete type, so a -/// declaration is a line someone wrote about a type they had read. A blanket implementation would -/// make the promise about types nobody has looked at. +/// declaration is a line someone wrote about a type they had read. /// - **Not reachable from data.** The only route to the fast path is -/// [`MeasuredInspector::new_trusted`], whose bound is this trait, so an inspector selected by a -/// request, a configuration file or any other run-time value cannot arrive on it — an -/// RPC-supplied tracer is a value, and no value can carry an implementation. +/// [`MeasuredInspector::new_trusted`], whose bound is this trait — an RPC-supplied tracer is a +/// value, and no value can carry an implementation. /// - **Do not implement it for anything that intercepts.** An inspector that answers a frame /// itself, edits inputs, or rewrites a result is a rewriting inspector however little it /// rewrites; those are supported, measured, and must stay measured. /// - **A foreign inspector needs a newtype.** The orphan rule wants one of the trait and the type -/// to be local, and for a `revm-inspectors` tracer neither is — so a node declares a newtype of -/// its own that forwards every callback. `benches/common/subject.rs` does exactly that, and is -/// the shape to copy. +/// to be local, and for a `revm-inspectors` tracer neither is, so a node declares a forwarding +/// newtype of its own. /// -/// # Verified in debug builds -/// -/// A declared type still takes the full measurement under `debug_assertions`, and the shim -/// asserts the ledger stayed empty after every callback. A wrong declaration therefore fails in -/// tests, in CI and under the chaos sweep, at the callback that broke it. +/// Debug builds measure a declared type anyway and assert the ledger stayed empty after every +/// callback, so a wrong declaration fails at the callback that broke it. pub trait TrustedObserver {} /// The inspector `MegaETH` runs with when none was supplied observes nothing at all. @@ -166,14 +108,10 @@ impl TrustedObserver for &mut T {} /// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// -/// `MegaETH` applies this itself — [`MegaEvm::with_inspector`](crate::MegaEvm::with_inspector) and -/// [`InspectEvm::set_inspector`](revm::InspectEvm::set_inspector) take the user's inspector by -/// value and store it wrapped, and the accessors hand back the unwrapped inspector — so the wrapper -/// is not something a caller opts into or can opt out of. -/// -/// What a caller *can* opt into is being measured more cheaply, by declaring the inspector's type -/// [`TrustedObserver`] and building the shim with [`new_trusted`](Self::new_trusted). See -/// [`measures`](Self::measures) for what that changes and where it stops. +/// `MegaETH` applies this itself, so the wrapper is not something a caller opts into or can opt +/// out of. What a caller *can* opt into is being measured more cheaply, by declaring the +/// inspector's type [`TrustedObserver`] and building the shim with +/// [`new_trusted`](Self::new_trusted). /// /// Derefs to the wrapped inspector, so `evm.inspector().whatever()` reaches the user's own type. #[derive(Clone, Copy, Debug, Default, derive_more::Deref, derive_more::DerefMut)] @@ -183,15 +121,11 @@ pub struct MeasuredInspector { inner: I, /// Whether the wrapped type's author declared it [`TrustedObserver`]. /// - /// A flag rather than a type parameter, because the type the shim is asked about is chosen by - /// whoever calls `with_inspector`, and `MegaEvm` names the shim as `MeasuredInspector` - /// for whatever `INSP` that is. Answering it in the type system would mean either a bound on - /// every inspector `MegaETH` can be handed — including the foreign ones it cannot implement - /// anything for — or a second impl of `Inspector` that overlaps the first. So the question is - /// answered where it is asked, at the one constructor whose bound is the declaration, and - /// carried here. - /// - /// `Default` leaves it false, which is the safe direction: an unbuilt shim measures. + /// A flag rather than a type parameter: answering it in the type system would need either a + /// bound on every inspector `MegaETH` can be handed, including the foreign ones it cannot + /// implement anything for, or a second overlapping impl of `Inspector`. So it is answered at + /// the one constructor whose bound is the declaration and carried here. `Default` leaves it + /// false, which is the safe direction: an unbuilt shim measures. trusted: bool, } @@ -204,16 +138,11 @@ impl MeasuredInspector { /// Whether this callback takes the measuring path. /// /// False only for a declared [`TrustedObserver`] in a release build. Under `debug_assertions` - /// every inspector is measured, declared or not, and a declared one is additionally asserted - /// to have booked nothing — which is what makes the declaration a claim the build system - /// checks rather than a comment. + /// every inspector is measured and a declared one is additionally asserted to have booked + /// nothing, which is what makes the declaration a checked claim rather than a comment. /// - /// Both halves are compile-time constants beside a `bool` the shim was built with, so the - /// release fast path is one predictable branch and the debug path folds away entirely. - /// - /// This and [`verify_trusted`] read the same `debug_assertions` flag, which is what keeps the - /// two builds from disagreeing: a profile that turns assertions on in an optimised build gets - /// the measured path *and* the check, never one without the other. + /// This and [`verify_trusted`] read the same flag, so a profile that turns assertions on in an + /// optimised build gets the measured path *and* the check, never one without the other. #[inline(always)] const fn measures(&self) -> bool { !self.trusted || cfg!(debug_assertions) @@ -236,10 +165,8 @@ impl MeasuredInspector { /// Whether the wrapped inspector's type was declared [`TrustedObserver`]. /// - /// True only for a shim built by [`new_trusted`](Self::new_trusted). What it is for is the - /// transaction-level backstop in `MegaEvm::execute_transaction`: the per-callback verification - /// names the callback that broke a declaration, and this catches one broken at a callback - /// whose verification is missing. + /// Read by the transaction-level backstop, which catches a declaration broken at a callback + /// whose own verification is missing. pub const fn is_trusted(&self) -> bool { self.trusted } @@ -258,12 +185,8 @@ impl MeasuredInspector { /// Asserts, in debug builds, that a declared [`TrustedObserver`] really booked nothing. /// -/// Called after every measured callback. The ledger accumulates over the whole transaction and is -/// reset at its start, so the first callback that breaks the promise is the one that fails — -/// later ones would fail too, but this one names the site. -/// -/// Compiled out of release builds together with the measurement it checks: a declared type never -/// reaches a measuring body there at all. +/// Called after every measured callback, so the first one to break the promise is the one that +/// fails and names the site. Compiled out of release builds along with the measurement it checks. #[inline] fn verify_trusted( trusted: bool, @@ -316,36 +239,17 @@ impl ActionLane { /// Whether an edit to this interpreter's gas counter can still reach the transaction's /// envelope. /// - /// It cannot exactly on [`Result`](Self::Result). revm's inspected loop runs the terminating - /// instruction first and the callback after it, and that instruction has already copied the - /// counter into the action it set — the action is what becomes the frame's result and what the - /// caller reclaims from. Whatever the callback writes into the counter afterwards is written - /// into an object nobody will read again. - /// - /// The two neighbouring shapes are live and must stay booked. With no action pending the frame - /// carries straight on; with a `NewFrame` action pending it suspends into a child and then - /// resumes on this very counter. "The loop is about to break" is therefore not the question — - /// revm breaks out of the instruction loop in both the suspending and the terminating case — - /// and a rule phrased that way would stop booking edits that really do move a frame's budget. + /// It cannot exactly on [`Result`](Self::Result): the terminating instruction has already + /// copied the counter into the action that becomes the frame's result, so a callback writing + /// to the counter afterwards writes into an object nobody reads again. The other two shapes + /// are live — a frame with no action carries straight on, and a suspending one resumes on this + /// very counter — so "the loop is about to break" is not the question, and a rule phrased that + /// way would stop booking edits that really do move a budget. /// - /// Read *after* the callback returns, because the question is about the counter the callback - /// left behind: an inspector that sets or clears an action has changed what the EVM does next, - /// and the answer has to follow it. - /// - /// # Why this decides booking and not measurement - /// - /// Only the ledger is gated on it. `MegaETH`'s own tail settlement measures a frame's work as - /// a drop in this same counter and does read it after the action is set, so the settlement - /// baseline has to shift for a dead-window edit exactly as it does for a live one — otherwise - /// gas the inspector wrote in would read as work the frame performed, which is the opposite - /// error. - /// - /// # The gas the counter no longer speaks for - /// - /// What a dead-window counter edit cannot reach, an edit to the action itself can — and - /// [`LiveReading`] measures exactly that, against the same counter reading, so the two - /// together account for every unit of gas the frame holds. See [`held`] for the identity they - /// split. + /// Read *after* the callback returns, because the question is about the counter it left + /// behind. Only the ledger is gated on this: the checkpoint baseline shifts for a dead-window + /// edit exactly as it does for a live one, or gas the inspector wrote in would read as work + /// the frame performed. #[inline] const fn counter_reaches_envelope(self) -> bool { !matches!(self, Self::Result) @@ -355,23 +259,16 @@ impl ActionLane { /// The gas a frame and its pending continuation hold, given the action it is carrying and its own /// counter. /// -/// This is the quantity the two live-interpreter lanes partition between them: -/// /// ```text -/// held(None, counter) = counter -/// held(NewFrame(f), counter) = counter + f.gas_limit -/// held(Return(r), counter) = r.gas.remaining() +/// held(None, counter) = counter // will spend its counter +/// held(NewFrame(f), counter) = counter + f.gas_limit // and has handed the child's on +/// held(Return(r), counter) = r.gas.remaining() // the caller reclaims the action's copy /// ``` /// -/// A frame with no action pending will spend its counter. A suspending frame will spend its -/// counter when it resumes and has additionally handed the child's envelope on. A terminating -/// frame will spend nothing more — the action's own copy is what its caller reclaims, and the -/// counter is dead. -/// /// Both readings [`LiveReading`] takes use the counter *the EVM left behind*, so the counter -/// cancels out of the difference wherever it appears on both sides. What is left is exactly the -/// part of the movement that is not already on the counter lane, whatever the callback did to the -/// action's shape. +/// cancels out of the difference wherever it appears on both sides. What is left is the part of +/// the movement that is not already on the counter lane, whatever the callback did to the action's +/// shape. #[inline] fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { match action { @@ -383,25 +280,11 @@ fn held(action: Option<&InterpreterAction>, counter: u64) -> i128 { } } -/// The refund a frame and its pending continuation hold, given the action it is carrying and the -/// refund on its own gas counter. -/// -/// [`held`]'s counterpart on the refund dimension, and it has the same three cases for the same -/// reason — the object the EVM will read next is the one the frame is holding: -/// -/// ```text -/// held_refund(None, counter) = counter -/// held_refund(NewFrame(_), counter) = counter -/// held_refund(Return(r), counter) = r.gas.refunded() -/// ``` +/// [`held`]'s counterpart on the refund dimension. /// -/// The middle case differs from [`held`]'s: a `NewFrame` action carries a child's *envelope* but -/// no refund of its own, and the suspending frame resumes on this very counter with the child's -/// refund added to it. So the counter is the live object in two of the three cases, and only a +/// The middle case differs from [`held`]'s: a `NewFrame` action carries a child's envelope but no +/// refund of its own, so the counter is the live object in two of the three cases and only a /// terminating action displaces it. -/// -/// Read on both sides of a callback, the difference is the refund the inspector wrote — wherever -/// it wrote it, and whichever of the two objects the EVM goes on to read. #[inline] fn held_refund(action: Option<&InterpreterAction>, counter: i64) -> i64 { match action { @@ -413,10 +296,8 @@ fn held_refund(action: Option<&InterpreterAction>, counter: i64) -> i64 { /// Books what a callback did to a refund counter. /// /// Nominal: the figure booked is what the inspector wrote, not what survives the EIP-3529 cap or -/// the chain of frame returns between here and the receipt — see -/// [`InspectorLedger::refund`](crate::InspectorLedger::refund) for why neither of those is a -/// quantity a boundary can measure, and why over-stating is the safe direction for the one -/// consumer this lane has. +/// the chain of frame returns between here and the receipt. Neither of those is a quantity a +/// boundary can measure, and over-stating is the safe direction for the lane's one consumer. #[inline] fn book_refund( context: &MegaContext, @@ -433,10 +314,8 @@ fn book_refund( /// The refund a synthetic outcome carries, for a callback that answered a frame itself. /// -/// There is no "before" to difference against: no frame is built, so the EVM produced no refund -/// here at all and the whole of what the outcome carries is the inspector's — the same argument -/// the interception's gas baseline rests on, with the baseline being zero rather than the -/// envelope because a frame that never ran has refunded nothing. +/// There is no "before" to difference against, so the whole of it is the inspector's. The baseline +/// is zero rather than the envelope because a frame that never ran has refunded nothing. #[inline] fn book_synthetic_refund( context: &MegaContext, @@ -458,19 +337,13 @@ struct ActionChange { /// The pending action a callback was handed, in the form the boundary compares it in. /// -/// Only one question is asked of the way-in reading that the numbers beside it in [`LiveReading`] -/// do not already answer: did the action come back describing something other than what the EVM -/// decided? So this holds what that comparison reads and nothing else — the gas is taken as -/// [`held`] at the moment the reading is made, and never needs the action again. +/// Holds only what the rewrite comparison reads — the gas is taken as [`held`] when the reading is +/// made and never needs the action again. That matters because this reading is taken twice per +/// opcode: copying the action itself would carry an output buffer and a frame input's boxed inputs +/// across every one, while the shape a running frame is almost always in costs a discriminant. /// -/// Which matters because the way-in reading is taken twice per opcode. Copying the action itself -/// would carry an `InterpreterResult`'s output buffer and a frame input's boxed inputs across -/// every one of them; here the shape a running frame is almost always in costs a discriminant, and -/// the two that carry something are taken once per frame and once per call. -/// -/// The output buffer is held rather than reduced to its identity, and that is the point of holding -/// it: [`same_buffer`] compares by address, and only an owner keeps the address it compares from -/// being reused underneath it. +/// The output buffer is held rather than reduced to its identity because [`same_buffer`] compares +/// by address, and only an owner keeps that address from being reused underneath it. #[derive(Clone, Debug)] enum ActionSnapshot { /// No action pending: the frame carries straight on. @@ -498,10 +371,10 @@ impl ActionSnapshot { /// Whether a callback left behind an action describing something other than what the EVM /// decided. /// - /// Gas is excluded, exactly as it is at every other boundary: it travels on the lanes - /// [`book_pending_action`] routes it to, and counting it here as well would report one rewrite - /// twice. A callback that installed, removed or swapped an action has rewritten what the EVM - /// does next as thoroughly as it is possible to, so every shape change counts. + /// Gas is excluded, as it is at every other boundary: it travels on the lanes + /// [`book_pending_action`] routes it to, and counting it here would report one rewrite twice. + /// Every shape change counts — installing, removing or swapping an action rewrites what the + /// EVM does next as thoroughly as it is possible to. #[inline(always)] fn rewritten(self, after: Option<&InterpreterAction>) -> bool { match (self, after) { @@ -520,18 +393,16 @@ impl ActionSnapshot { /// Books what a callback did to the interpreter's pending action. /// /// The gas goes to the lane the action the callback *left behind* names, because that is where the -/// number now lives and therefore what decides when it can still be settled: -/// -/// - [`ActionLane::Result`] is staged for the frame's settlement point, like an edit made at the -/// frame's last callback — whether it moves anything depends on the classification the caller -/// ends up seeing, which no callback here knows; -/// - [`ActionLane::Envelope`] is staged for the frame-start callback of the child the action is -/// about to build, which is where an envelope edit is booked from; -/// - [`ActionLane::Counter`] is booked on the spot, on the interpreter lane: with no action left, -/// the frame carries on spending what it holds, which is exactly what a counter edit does. This -/// is the algebra's third case rather than a shape an inspector can reach through the API — -/// `reset_action` only clears revm's `continue_execution` flag and leaves the action in place, so -/// emptying the slot means writing `None` into it and desynchronising the two. +/// number now lives and so what decides when it can still be settled: +/// +/// - [`ActionLane::Result`] is staged for the frame's settlement point, like an edit at the frame's +/// last callback — whether it moves anything depends on the classification the caller ends up +/// seeing, which no callback here knows; +/// - [`ActionLane::Envelope`] is staged for the frame-start callback of the child it will build; +/// - [`ActionLane::Counter`] is booked on the spot: with no action left the frame carries on +/// spending what it holds, which is what a counter edit does. This is the algebra's third case +/// rather than a shape the API offers — `reset_action` leaves the action in place, so emptying +/// the slot means writing `None` and desynchronising the two. #[inline] fn book_pending_action( context: &MegaContext, @@ -562,28 +433,17 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { /// staged into the same envelope through the pending `NewFrame` action — and, when the callback /// answered the frame itself, stages that envelope for the frame's settlement point. /// -/// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped -/// entirely and the EVM never reads the inputs it edited, so the edit by itself moves nothing on -/// this lane — see [`InspectorLedger::env`](crate::InspectorLedger::env). -/// -/// The staged amount is booked either way, and the asymmetry is not an oversight. An interception -/// discards inputs *this* callback edited a moment earlier, which is why that edit reaches -/// nothing. The staged amount was written by a different callback into the action the caller's -/// `CALL` / `CREATE` opcode had already produced — the caller's debit is behind it, `MegaETH`'s own -/// CALL settlement excluded the pre-edit amount from the caller's work, and a callback deciding -/// later to answer the frame itself cannot un-make that. It is simply the earliest of the two -/// edits to the one envelope, and the last thing to touch that envelope is what its holder is -/// sized from. -/// -/// # Why an interception stages a baseline rather than booking a difference -/// -/// Every other lane measures a difference across the callback, because the EVM produced the -/// object on both sides of it. An interception has no such object: the frame is never built, and -/// the result the caller reclaims from is one the inspector wrote from nothing. What the -/// transaction funded is the envelope on the way in; what it gets back is whatever gas that -/// result turns out to carry once the last callback has run. The difference between the two is -/// the measurement, and only the frame init that asked can take it — so the way in is staged -/// here, and [`AdditionalLimit::stage_inspector_interception_envelope`] says what the number is. +/// `intercepted` is true when the callback returned a synthetic outcome: the frame is skipped and +/// the EVM never reads the inputs it edited, so that edit moves nothing on this lane. The staged +/// amount is booked either way, and the asymmetry is not an oversight — it was written by a +/// *different* callback into the action the caller's `CALL` / `CREATE` opcode had already +/// produced, so the caller's debit is behind it and a later decision to answer the frame cannot +/// un-make that. +/// +/// An interception stages a baseline rather than booking a difference because it has no object on +/// the other side: the frame is never built, and what the caller reclaims from is a result the +/// inspector wrote from nothing. What the transaction funded is the envelope on the way in, and +/// only the frame init that asked can take that difference. #[inline] fn book_env_adjustment( context: &MegaContext, @@ -610,20 +470,15 @@ fn book_env_adjustment( } } -/// Books one rewrite that changes what the execution *did* rather than what it cost — see -/// [`InspectorLedger::interventions`](crate::InspectorLedger::interventions). +/// Books one rewrite that changes what the execution *did* rather than what it cost. /// /// Every caller answers the same question about the argument it was handed: did it come back /// describing something other than what the EVM was about to do? Two things are deliberately not -/// part of that question: -/// -/// - **Gas.** A frame input's gas limit and a frame result's remaining gas are booked as gas, on -/// the ledger's own lanes; counting them here as well would report one rewrite twice. -/// - **Anything neither the argument nor a constant-time reading off it describes.** The contents -/// of the interpreter's stack and memory, and the journal. Telling whether those came back -/// changed needs a snapshot of unbounded state, which no callback boundary can take at a cost the -/// inspected path can carry. Their *sizes* are a constant-time reading and are covered, by -/// [`WorkingSet`]; so is a finished outcome's metadata, by [`OutcomeMetadata`]. +/// part of it. **Gas**, because it is booked on the ledger's own lanes and counting it here would +/// report one rewrite twice. And **anything neither the argument nor a constant-time reading off +/// it describes** — the contents of the interpreter's stack and memory, and the journal — because +/// telling whether those came back changed needs a snapshot of unbounded state that no per-opcode +/// boundary can take. Their sizes are constant-time readings and are covered. #[inline] fn book_intervention( context: &MegaContext, @@ -636,10 +491,9 @@ fn book_intervention( /// An `O(1)` identity for a byte buffer: where it starts and how long it is. /// -/// The same comparison [`same_buffer`] makes, in a form that can be stored in a snapshot. Neither -/// reads a byte: a buffer's *contents* at an unchanged address and length are content-class, which -/// is the row of the shape table that has no lane. What this does catch is the buffer being -/// replaced, which is the only way an inspector can change one that is immutable. +/// The same comparison [`same_buffer`] makes, stored. Neither reads a byte — a buffer's contents +/// at an unchanged address and length are the class with no lane — but replacing the buffer is the +/// only way to change an immutable one, and that is what this catches. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct BufferId { /// Where the buffer starts, as a bare address rather than a live pointer. @@ -681,58 +535,41 @@ impl CallInputId { /// /// # The rule /// -/// **Every `O(1)` reading of the interpreter's working set is in this snapshot.** Not a list of -/// the readings someone thought of — the readings themselves, enumerated field by field against -/// revm's `Interpreter` and the traits each field is reachable through, and pinned that way by -/// `tests/rex7/gas_surface.rs`. -/// -/// The rule is stated over readings rather than over fields because that is the shape of what a -/// boundary can do. An inspector reaches the whole interpreter; the shim can only compare what it -/// can *read back* in constant time, and a snapshot the inspected path takes twice per opcode -/// cannot walk unbounded state. So the line is drawn at the cost of the reading, and everything on -/// the cheap side of it is taken. +/// **Every `O(1)` reading of the interpreter's working set is in this snapshot** — not a list of +/// the readings someone thought of, but the readings themselves, enumerated field by field against +/// revm's `Interpreter` and the traits each field is reachable through. /// -/// The earlier version of this snapshot held four readings and was written as an enumeration: the -/// stack's length, the memory's size, and the two halves of the memo of how far that memory has -/// been paid for. Enumerations of this kind are only as complete as whoever wrote them, and this -/// one was not — the `bytecode` field was not in it at all, so an inspector could step the program -/// counter past an instruction and delete it from the frame with every lane reading zero. Stating -/// the rule over the cost of the reading is what closes that class rather than that instance. +/// Stated over readings rather than over fields because that is the shape of what a boundary can +/// do: an inspector reaches the whole interpreter, and a snapshot taken twice per opcode can only +/// compare what it can read back in constant time. So the line is drawn at the cost of the +/// reading, and everything on the cheap side of it is taken. An enumeration is only as complete as +/// whoever wrote it, and the four-reading version this replaced left out `bytecode` — which let an +/// inspector step the program counter past an instruction, deleting it from the frame, with every +/// lane reading zero. /// /// # What is here, by the field it is read from /// /// - `bytecode` — the program counter, the code's identity, and revm's `continue_execution` flag, -/// which is what the inspected loop breaks on and is a separate object from the pending action. +/// which the inspected loop breaks on and which is a separate object from the pending action. /// - `stack` — its length. -/// - `return_data` — the buffer's identity. A frame's `RETURNDATASIZE` and `RETURNDATACOPY` read -/// it, so putting a buffer there hands the frame data no call produced. +/// - `return_data` — the buffer's identity, which `RETURNDATASIZE` and `RETURNDATACOPY` read. /// - `memory` — its size, and the offset of the frame's window into the shared buffer. -/// - `gas` — the memory memo's two halves. The budget half of a `Gas` is not here: it moves on the -/// gas lanes, and reading it here as well would report one edit twice. +/// - `gas` — the memory memo's two halves. The budget half moves on the gas lanes instead. /// - `input` — the four addresses and values a frame's identity is made of, and its calldata's -/// identity. `target_address` is the one every storage instruction resolves against, so moving it -/// redirects the frame's writes to another account. +/// identity. `target_address` is what every storage instruction resolves against. /// - `runtime_flag` — the static flag and the spec id. /// /// `extend` is the one field with no reading, by construction: `InterpreterTypes::Extend` carries -/// no trait bound at all, so a shim generic over the interpreter has nothing it can call on it. -/// -/// # What is deliberately not here +/// no trait bound, so a shim generic over the interpreter has nothing to call on it. /// -/// The *contents* of the stack, the memory, the return buffer, the calldata and the code. Telling -/// whether any of those came back changed means walking unbounded state, which is the one thing a -/// per-opcode boundary cannot do. Their identities and sizes are here; what is inside them is the -/// row of the shape table that has no lane. +/// The *contents* of the stack, memory, return buffer, calldata and code are deliberately absent: +/// walking unbounded state is the one thing a per-opcode boundary cannot do. /// -/// # The pair that made the snapshot necessary -/// -/// The memory and its memo, moved together. The memo (`Gas::memory`) is what the next expanding -/// opcode compares its requirement against. An inspector that raises it without growing the memory -/// desynchronises the two and the EVM reads out of bounds; one that grows the memory without -/// raising it is charged for the growth twice over. Moving *both* is neither — the interpreter is -/// in a state it could have reached by paying, having paid nothing, and every later expansion -/// inside the new bound is free. That pair moves no gas at the moment it is made, so no gas lane -/// can see it; what it changes is what the EVM charges afterwards. +/// The pair that made the snapshot necessary is the memory and its memo, moved together. Raising +/// the memo alone desynchronises the two and the EVM reads out of bounds; growing the memory alone +/// is charged twice. Moving both leaves every interpreter invariant intact, having paid nothing, +/// and makes every later expansion inside the new bound free — which no gas lane can see, because +/// what it changes is what the EVM charges afterwards. #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct WorkingSet { /// Where in its code the frame is about to execute. @@ -773,20 +610,15 @@ impl WorkingSet { /// Whether a live interpreter still reads the way this snapshot recorded it. /// /// The same question as `*self == Self::of(interp)`, asked without building the second - /// snapshot. That is the whole difference, and it is worth stating because this is the way-out - /// half of a measurement taken twice per opcode: a comparison written that way materialises - /// two hundred bytes onto the stack for the length of one `==`, and the optimiser does not - /// reliably take them away again. + /// snapshot: written that way it materialises two hundred bytes onto the stack for the length + /// of one `==`, twice per opcode, and the optimiser does not reliably take them away again. /// - /// Every reading in [`of`](Self::of) is compared here, in the same order, and neither list may - /// be shortened without the other. What holds them together is the unit tests below: each of - /// them moves one reading and asserts both that [`moved`] names it and that this returns - /// `false`, so a reading present in the snapshot and missing here is a reading the shim takes - /// and never compares. + /// Every reading in [`of`](Self::of) is compared here and neither list may be shortened + /// without the other. The unit tests below hold them together: a reading in the snapshot and + /// missing here is one the shim takes and never compares. /// - /// Left as `inline` rather than `inline(always)` on purpose. Forced into all four callbacks it - /// is faster beside an empty inspector and slower beside a real tracer, which is the inspector - /// the inspected path actually carries; one copy per interpreter type is faster beside both. + /// `inline` rather than `inline(always)` on purpose — forced into all four callbacks it is + /// slower beside the real tracer the inspected path carries. #[inline] fn unchanged(&self, interp: &Interpreter) -> bool { let memory = interp.gas.memory(); @@ -835,11 +667,11 @@ impl WorkingSet { /// Everything a `CallOutcome` carries besides the `InterpreterResult` inside it. /// -/// The result is compared on its own, by [`result_rewritten`]; this is the rest of the object, and -/// it is not bookkeeping. `memory_offset` is where the caller copies the callee's output to, so -/// moving it feeds the caller a word the callee never wrote. `charged_new_account_state_gas` tells -/// the caller whether to refund an EIP-8037 upfront charge. `was_precompile_called` and -/// `precompile_call_logs` decide which logs an inspector is shown next. +/// The result is compared on its own, by [`result_rewritten`]; this is the rest, and it is not +/// bookkeeping. `memory_offset` is where the caller copies the callee's output to, so moving it +/// feeds the caller a word the callee never wrote. `charged_new_account_state_gas` tells the +/// caller whether to refund an EIP-8037 upfront charge, and the two precompile fields decide which +/// logs an inspector is shown next. #[derive(Clone, Debug, PartialEq, Eq)] struct CallMetadata { memory_offset: Range, @@ -863,9 +695,8 @@ impl CallMetadata { /// [`CallMetadata`] for the generic callback, which is handed the variant rather than the outcome. /// /// A creation's own metadata is one field: the address the caller's stack is about to receive. -/// Rewriting it reports a contract at an address holding no code, while the code the EVM deployed -/// stays where it was — a split the result's classification cannot express, and one no gas lane -/// sees. +/// Rewriting it reports a contract at an address holding no code while the deployed code stays +/// where it was — a split no classification expresses and no gas lane sees. /// /// Matched without a catch-all, so a `FrameResult` variant added upstream stops the build here. #[derive(Clone, Debug, PartialEq, Eq)] @@ -886,10 +717,9 @@ impl OutcomeMetadata { /// Whether two output buffers are the same buffer. /// -/// Compared by address and length rather than by content. `Bytes` is immutable, so a callback can -/// only change an output by putting a different buffer there, and the caller holds its snapshot -/// across the comparison — which keeps the original alive, so its address cannot be reused -/// underneath. A replacement that copies the same bytes reads as unchanged, which is what it is. +/// By address and length, not content. `Bytes` is immutable, so a callback can only change an +/// output by putting a different buffer there, and the caller holds its snapshot across the +/// comparison — which keeps the original alive, so its address cannot be reused underneath. #[inline] fn same_buffer(before: &Bytes, after: &Bytes) -> bool { before.as_ptr() == after.as_ptr() && before.len() == after.len() @@ -898,10 +728,9 @@ fn same_buffer(before: &Bytes, after: &Bytes) -> bool { /// Whether a callback rewrote what a finished frame *did*: the classification its caller will see, /// or the output it will read. /// -/// The classification is the one that carries the most: it decides whether the caller sees a -/// success, and — through the frame's settlement point — whether the frame's state is committed -/// and whether its remainder is handed back or destroyed. None of that moves gas by itself, so -/// none of it leaves a trace in any gas lane. +/// The classification carries the most — whether the caller sees a success, whether the frame's +/// state is committed, and whether its remainder is handed back or destroyed — and none of it +/// moves gas, so none of it leaves a trace in any gas lane. #[inline] fn result_rewritten(before: (InstructionResult, &Bytes), after: &InterpreterResult) -> bool { before.0 != after.result || !same_buffer(before.1, &after.output) @@ -943,16 +772,14 @@ fn frame_input_rewritten(before: FrameInput, after: &FrameInput) -> bool { /// What an inspector can ask `MegaETH` about the frame result it is holding. /// -/// One question, with one purpose: telling a result a frame *ran* to produce apart from one frame -/// init produced without ever building a frame. The two arrive at the same callback holding the -/// same type, and the difference decides what a rewrite of the classification does — a running -/// frame's journal decision is still outstanding and follows the rewrite, while an init-produced -/// result's was taken before the callback existed and is refused (see -/// [`MeasuredInspector`](MeasuredInspector#impl-Inspector)). +/// One question: is this a result a frame *ran* to produce, or one frame init produced without +/// ever building a frame? The two arrive at the same callback holding the same type, and the +/// difference decides what a classification rewrite does — a running frame's journal decision is +/// still outstanding and follows the rewrite, an init-produced result's was taken before the +/// callback existed and is refused. /// -/// A tool that only observes never needs this. One that rewrites classifications does, because -/// otherwise the only way to find out which kind of result it is holding is to have its -/// transaction refused. +/// A tool that only observes never needs this. One that rewrites classifications does, because the +/// only other way to find out is to have its transaction refused. pub trait FrameResultOriginTr { /// Whether the frame result the `*_end` callbacks are being handed came out of frame init. /// @@ -967,12 +794,11 @@ impl FrameResultOriginTr for MegaContex } } -/// Which of the three things a frame's result says, which is the granularity the refusal below is -/// stated over. +/// Which of the three things a frame's result says — the granularity the refusals are stated over. /// -/// A result's gas and its returned output move freely — those are what the lanes measure. What -/// cannot move is which of these three the caller is handed, because that is the question the -/// journal decision answers. +/// A result's gas and its returned output move freely; the lanes measure those. What cannot move +/// is which of these three the caller is handed, because that is what the journal decision +/// answers. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ResultClass { /// The frame returned, and its writes stand. @@ -999,38 +825,27 @@ impl ResultClass { /// Refuses a rewrite that moves a frame-init result across the success / revert / halt boundary. /// /// Every other classification rewrite is supported because REX7 withholds the journal decision -/// until the result is final: the frame loops park it and `frame_return_result` carries it out -/// after the last callback, so a frame rewritten into a revert has its state rolled back with it. -/// -/// A result that comes out of frame *init* has no such window, and cannot be given one from here. -/// Upstream takes the decision inside `make_call_frame`, statements before it returns — a -/// value-transferring call into an empty-code account commits the transfer and returns `Stop`, a -/// precompile that fails reverts it and returns its own failure — and `MegaETH`'s system contract -/// interceptors take theirs before they return, the `KeylessDeploy` one by merging a whole -/// sandbox's state into the journal. All of it has happened by the time a callback sees the -/// result. Honouring a rewrite would hand the caller an answer the state behind it contradicts: -/// a transfer the recipient keeps and the sender is told failed, or a deployment the caller is -/// told reverted and that stands anyway. -/// -/// A result an inspector answered the frame with itself is deliberately outside the refusal, even -/// though it too reaches a callback with no frame having run: nothing in the EVM decided anything -/// for it — no checkpoint was opened, no state written — so its classification is the inspector's -/// to state and rewriting it contradicts nothing. What separates the two is which of the two -/// callback sites in `inspect_frame_init` ran, and that is where the window is opened; nothing -/// here can tell them apart on its own. -/// -/// Detection only; nothing here compensates the journal. The original classification is restored, -/// the ledger counts the refusal, and the context's error slot carries the reason so the -/// transaction fails with an error rather than with a receipt built on the rewrite. -/// -/// Deliberately loud but not fatal, which is where it differs from -/// [`reject_forbidden_create_rewrite`]. That shape is a mistake with no reading behind it and -/// asserting on it costs nothing. This one is the most ordinary rewrite a tool makes — failing a -/// call — landing on the one kind of frame it cannot be applied to, so a corpus that produces it -/// should be able to report it rather than die on it. -/// -/// Gated to REX7+. On a frozen spec, an inspector's rewrite reaches no accounting lane that can be -/// made unsound by it, and the specs' behaviour — including on the inspected path — is closed. +/// until the result is final, so a frame rewritten into a revert has its state rolled back with +/// it. A result out of frame *init* has no such window and cannot be given one from here: upstream +/// decides inside `make_call_frame` — an empty-code call commits its transfer and returns `Stop`, +/// a failing precompile reverts and returns its own failure — and `MegaETH`'s interceptors decide +/// before they return, the `KeylessDeploy` one by merging a whole sandbox's state. Honouring a +/// rewrite would hand the caller an answer the state behind it contradicts. +/// +/// A result an inspector answered the frame with itself is deliberately outside the refusal: +/// nothing in the EVM decided anything for it, so its classification is the inspector's to state. +/// What separates the two is which callback site in `inspect_frame_init` ran, which is where the +/// window is opened. +/// +/// Detection only; nothing here compensates the journal. The classification is restored, the +/// ledger counts the refusal, and the error slot carries the reason so the transaction fails +/// rather than producing a receipt built on the rewrite. Loud but not fatal, unlike +/// [`reject_forbidden_create_rewrite`]: this is the most ordinary rewrite a tool makes — failing a +/// call — landing on the one frame kind it cannot be applied to, so a corpus should be able to +/// report it rather than die on it. +/// +/// Gated to REX7+: on a frozen spec a rewrite reaches no accounting lane it can make unsound, and +/// those specs' behaviour is closed. #[inline] fn reject_forbidden_frame_init_rewrite( context: &mut MegaContext, @@ -1060,21 +875,16 @@ fn reject_forbidden_frame_init_rewrite( /// Refuses a rewrite that turns a non-successful contract creation into a successful one, and says /// so loudly. /// -/// The rewrite is forbidden rather than supported because there is no state behind it. By the time -/// `create_end` runs, revm has already reverted the frame's journal checkpoint and has already -/// declined to deposit the code — the size limit, the `0xEF` prefix rule and the code-deposit -/// charge are all evaluated before the callback. A result rewritten to success therefore reports a -/// deployment that did not happen, at an address holding no code, with the constructor's state -/// changes rolled back. Honouring it would hand the caller a contract that does not exist. +/// Forbidden rather than supported because there is no state behind it: by the time `create_end` +/// runs, revm has reverted the frame's checkpoint and declined to deposit the code — the size +/// limit, the `0xEF` prefix rule and the code-deposit charge are all evaluated before the +/// callback. Honouring it would report a deployment at an address holding no code. /// -/// Detection only; nothing here compensates the journal. The original classification is restored, -/// the ledger counts the refusal, and the context's error slot carries the reason so that the -/// transaction fails with an error rather than with a fabricated receipt. Debug builds assert: -/// this is a detector, and a test corpus that produces this shape should stop rather than quietly -/// take the rejection path. +/// Detection only, on the same terms as [`reject_forbidden_frame_init_rewrite`], except that debug +/// builds assert: this shape is a mistake with no reading behind it, and a corpus that produces it +/// should stop rather than quietly take the rejection path. /// -/// Gated to REX7+. On a frozen spec, an inspector's rewrite reaches no accounting lane that can be -/// made unsound by it, and the specs' behaviour — including on the inspected path — is closed. +/// Gated to REX7+ for the same reason as that one. #[inline] fn reject_forbidden_create_rewrite( context: &mut MegaContext, @@ -1100,11 +910,9 @@ fn reject_forbidden_create_rewrite( /// What the shim reads off a live interpreter on the way into a callback, and settles on the way /// out. /// -/// The four callbacks that are handed a live interpreter run the same measurement, and it is -/// written once here rather than four times: [`enter`](Self::enter) takes the way-in readings, the -/// user's inspector runs, and [`leave`](Self::leave) takes them again and books the differences. -/// The four used to carry a copy of that body each, which is four places for a boundary to be -/// measured differently at. +/// The four callbacks handed a live interpreter run the same measurement, written once here: +/// [`enter`](Self::enter) takes the way-in readings, the user's inspector runs, and +/// [`leave`](Self::leave) takes them again and books the differences. /// /// `IN_OPEN_SEGMENT` on [`leave`](Self::leave) is the one thing that differs between the four: /// `initialize_interp` runs before the frame's settlement window is opened, so there is no open @@ -1185,23 +993,16 @@ impl LiveReading { /// The measuring bodies of the four live-interpreter callbacks, kept out of line. /// -/// Each callback is two things: a branch on the declaration, and — when it is taken — the -/// measurement. Only the branch belongs in revm's instruction loop, and `inline(never)` is what -/// puts it there alone. Inlined, the measurement's two hundred bytes of readings are laid down -/// inside the loop for a declared observer that never executes them, and the loop pays for them -/// in registers and instruction cache all the same. -/// -/// That cost is most of what a declared observer was paying. On `interpreter_hotloop` with an -/// empty inspector, outlining takes the declared path from 1.13× the pre-shim inspected loop to -/// 1.04× on REX6, and from 1.07× to 1.00× on REX7 — the branch alone is nearly free, and the -/// bloat was not. +/// Each callback is a branch on the declaration and, when taken, the measurement. Only the branch +/// belongs in revm's instruction loop, and `inline(never)` is what puts it there alone: inlined, +/// the measurement's two hundred bytes of readings are laid down inside the loop for a declared +/// observer that never executes them, and the loop pays for them in registers and instruction +/// cache regardless. /// -/// It is not free for the undeclared path, and the trade was measured both ways rather than -/// assumed. Beside a real tracer — the inspector the inspected path carries in production — the -/// undeclared path gets 3–6% *faster*, for the same reason the declared one does. Beside an empty -/// inspector it gets 12–19% slower, because there the call is the whole of the work. The empty -/// inspector is an instrument for isolating this shim's cost and not a workload, and it is the -/// only row that moves the wrong way. +/// The trade was measured both ways. Beside the real tracer the inspected path carries in +/// production, outlining makes the undeclared path faster too; beside an empty inspector it is +/// slower, because there the call is the whole of the work — and an empty inspector is an +/// instrument for isolating this shim's cost, not a workload. /// /// Written out four times rather than taken as a function pointer, which would cost the /// undeclared path the inlining of the inner inspector's own callback. From 98c784f41272aa2f951bec7eb2100d49aabbbb6f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:06:45 +0800 Subject: [PATCH 174/208] docs(evm): correct the declared-observer fence, and check it where it stops The contract claimed a declared observer could not reach the canonical block path at all. The two factory methods cannot build one, but create_executor takes an EVM its caller built, so the fence is a convention rather than a type guarantee. It now says so, and create_executor carries a debug assertion on the new MegaEvm::has_trusted_inspector, which is what a test build can check. Two smaller corrections: the per-callback verification names the site for every lane booked at its own boundary, which is all but a finished frame result's gas; and the interpreter's bytecode row now says why Jumps::opcode and ExtBytecode::bytecode_hash are absent rather than leaving them to look missed. --- crates/mega-evm/src/block/factory.rs | 11 +++++++++++ crates/mega-evm/src/evm/AGENTS.md | 9 +++++++-- crates/mega-evm/src/evm/mod.rs | 10 ++++++++++ crates/mega-evm/tests/rex7/gas_surface.rs | 6 +++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/block/factory.rs b/crates/mega-evm/src/block/factory.rs index ad9dd4b0..cab44e62 100644 --- a/crates/mega-evm/src/block/factory.rs +++ b/crates/mega-evm/src/block/factory.rs @@ -177,6 +177,17 @@ where DB: StateDB, I: Inspector<::Context>, { + // The canonical block path measures every inspector it runs, because the guard that + // refuses an inspector-adjusted transaction reads a ledger the measurement fills. A + // declared observer is delegated to unmeasured in release builds, so its ledger is empty + // by construction and the guard would be reading nothing. The two factory methods cannot + // build one; this entry point takes an EVM the caller built, so it says so here. + debug_assert!( + !evm.has_trusted_inspector(), + "a declared TrustedObserver must not be handed to the canonical block path: the \ + inspector guard reads a ledger that is empty by construction for one", + ); + // Synchronize EVM tx runtime limits with the block context's BlockLimits. // This mirrors the inherent factory paths above which apply this // unconditionally on every spec since introduction. Without this, the diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index bda8684e..8618b0f0 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -117,7 +117,8 @@ The wrapper's other weakness is worse than its unimplementability: `Trusted::new An implementation names a concrete type and no value can carry one. **Trust, and verify.** Under `debug_assertions` a declared type takes the measuring path anyway, and the shim asserts the ledger came back empty after every callback it measures. -`MegaEvm::execute_transaction` asks the same question once more at the end of the transaction, which is the backstop for a callback added later whose own verification was never written — the per-callback assert names the site, the transaction-level one cannot be missing. +`MegaEvm::execute_transaction` asks the same question once more at the end of the transaction, which is the backstop for a callback added later whose own verification was never written. +The per-callback assert names the site for every edit booked at the boundary that measured it, which since the traffic/movement split is every lane but one: an edit to a *finished frame result*'s gas is booked at the frame's settlement point, after the last `*_end` callback has returned, so a declaration broken only there is caught by the transaction-level backstop rather than named at the callback. Neither costs anything in release, where a declared type reaches no measuring body at all. There is no behavioural fork between the two builds for a declaration that holds: the measurement of a type that writes nothing back is a sequence of reads that books nothing, so debug and release execute the same transaction and only a false declaration tells them apart — by panicking. `tests/rex7/trusted_observer.rs` holds both halves, and `mega-state-test`'s `RunMode::ObserveTrusted` holds the three-way comparison against a plain and a measured run of the same observer. @@ -135,7 +136,11 @@ Anything supplied by a request — a JavaScript tracer, an RPC-selected tracer c **How a node reaches it.** `EvmFactory::create_evm_with_inspector` cannot: its bound is `I: Inspector` and its return type is fixed, so it has no way to select the constructor. The route is `factory.create_evm(db, env).with_trusted_inspector(tracer)`, which keeps the factory's own dynamic precompiles and differs from the two-step untrusted form only in the method name. -The same limitation is a fence: `MegaBlockExecutorFactory` builds its EVM through those two factory methods and nothing else, so a declared observer cannot reach the canonical block-execution path at all — what it reaches is an EVM an embedder drives itself, which is what RPC tracing and off-band simulation are. +The same limitation is most of a fence, and it is worth being exact about where it stops. +`MegaBlockExecutorFactory`'s own two factory methods cannot produce a declared EVM, so nothing a node reaches *through them* arrives on the canonical block path unmeasured. +But `create_executor` takes an EVM the caller already built, so `factory.create_evm(db, env).with_trusted_inspector(tracer)` handed to it does reach that path — the fence is a convention the node keeps, not something the types enforce. +`create_executor` therefore carries a `debug_assert!` on `MegaEvm::has_trusted_inspector`, which is what turns the convention into something a test build checks. +What a declaration is *for* is an EVM an embedder drives itself, which is what RPC tracing and off-band simulation are. ### The window a counter edit reaches nothing through diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 5ea8fb30..739c521e 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -450,6 +450,16 @@ where Ok(outcome) } + /// Whether this EVM's inspector was built from a [`TrustedObserver`](crate::TrustedObserver) + /// declaration, and so is delegated to unmeasured in release builds. + /// + /// Read by a caller that must not be handed one. The block executor factory is the case that + /// matters: it takes an EVM its caller built, so nothing in its own signature keeps a declared + /// observer off the canonical block path. + pub const fn has_trusted_inspector(&self) -> bool { + self.inner.inspector.is_trusted() + } + /// Inspect a transaction and return the outcome. The inspector used is the one set up already /// in the EVM. Use [`MegaEvm::with_inspector`] to set up a custom inspector. /// diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index fbd625a4..156b879d 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -283,7 +283,11 @@ const INTERPRETER_FIELDS: [(&str, Coverage); 8] = [ inspected loop breaks on and is a separate object from the pending action. Moving the \ counter deletes an instruction from the frame, which costs the transaction the work \ that instruction would have done; nothing meters that, because it never happens. \ - Booked as interventions, off `WorkingSet`", + Booked as interventions, off `WorkingSet`. Two further readings are deliberately \ + absent: `Jumps::opcode` is derived from the counter and the code buffer, both of \ + which are here, and `ExtBytecode::bytecode_hash` is a cache on the concrete type \ + that a shim generic over `InterpreterTypes` cannot reach and that no execution path \ + reads back", ), ), ( From 2c1750ab956f8e8d534a0065dcff632b7144d31e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:08:32 +0800 Subject: [PATCH 175/208] style(evm): name the outcome case tuple, as the reading cases are named --- crates/mega-evm/src/evm/inspector.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index ec92389b..0e5436c1 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -1433,6 +1433,9 @@ mod tests { /// One case: the name of a reading, and a rewrite that moves it. type Case = (&'static str, fn(&mut Interpreter)); + /// One case: the name of a field, and a rewrite that moves it. + type OutcomeCase = (&'static str, fn(&mut CallOutcome)); + /// One rewrite per reading, each moving the reading it is named for and nothing else. /// /// Every one is something an inspector can do to a live interpreter through the traits the @@ -1522,7 +1525,7 @@ mod tests { /// books nothing for. Three of these move nothing `MegaETH` produces today — it runs with /// EIP-8037 off and no wired precompile emits a log — so no fixture can show their effect, and /// a per-field check is the only thing that holds the claim that they are seen at all. - const OUTCOME_CASES: [(&str, fn(&mut CallOutcome)); 4] = [ + const OUTCOME_CASES: [OutcomeCase; 4] = [ ("memory_offset", |outcome| outcome.memory_offset = 1..2), ("was_precompile_called", |outcome| outcome.was_precompile_called = true), ("precompile_call_logs", |outcome| { From cca43efaf9a304f62b3d74699f8904a3df99a6cf Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:15:55 +0800 Subject: [PATCH 176/208] docs(limit): cut the ledger's rustdoc, and correct the result lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The module was 62% comment. What is left states each lane's constraint once. One correction rather than a cut: the result lane's doc said the lane cannot be booked at a callback boundary. Since the traffic/movement split that is only true of its net — the traffic is booked at the boundary, which is what makes a staged edit visible whatever the frame's classification turns out to be — and the doc now says which half goes where and why the split is load-bearing. Verified as comment-only apart from that: the file's non-comment lines are byte-identical. --- crates/mega-evm/src/limit/inspector_ledger.rs | 320 +++++++----------- 1 file changed, 122 insertions(+), 198 deletions(-) diff --git a/crates/mega-evm/src/limit/inspector_ledger.rs b/crates/mega-evm/src/limit/inspector_ledger.rs index 1efc2132..2ef4fcde 100644 --- a/crates/mega-evm/src/limit/inspector_ledger.rs +++ b/crates/mega-evm/src/limit/inspector_ledger.rs @@ -1,36 +1,22 @@ //! The ledger of what an inspector did to a transaction. //! -//! `MegaETH` wraps every inspector it is handed in a measurement shim (`MeasuredInspector`), and -//! the shim books what it measures here. Nothing in this module enforces anything: the gas lanes -//! are exactly the part of a transaction's gas movement that the EVM did not produce, kept separate -//! so that enforcement can ignore it and the conservation law can account for it, and the two -//! counters record rewrites that move no gas at all. +//! Nothing here enforces anything. The gas lanes are the part of a transaction's gas movement the +//! EVM did not produce, kept apart so enforcement can ignore it and the conservation law can +//! account for it; the two counters record rewrites that move no gas at all. /// One signed lane of the ledger, and how much traffic it carried. /// -/// # Why a lane is two numbers +/// Two numbers because two consumers ask different questions. The conservation law needs the +/// **net**: gas written into one object and taken back out of another really did leave the +/// envelope where it was. The block guard needs the **gross**: two edits that cancel are two +/// edits, and in between them the frame held a number the EVM would never have given it — or the +/// two landed in different frames and only one survived to the receipt. /// -/// The lanes answer two different questions, and one number cannot answer both. +/// So the gross is not a diagnostic beside the net; it is what [`InspectorLedger::is_zero`] is +/// defined over. /// -/// The conservation law needs the **net**: gas an inspector wrote into one object and took back -/// out of another really has left the transaction's envelope where it was, and a law stated over -/// the gross would be wrong by exactly the round trip. -/// -/// The block guard needs the **gross**: it asks whether the transaction was left alone, and two -/// edits that cancel are two edits. A `+1` before the frame reads its own remaining gas and a `−1` -/// after it has read it net to nothing and leave the frame holding a number the EVM would never -/// have given it — and the same cancellation split across two frames, where only one of the two -/// survives to the receipt, moves what the sender pays while netting to zero. -/// -/// So the gross is not a diagnostic beside the net; it is the number -/// [`InspectorLedger::is_zero`] is defined over. [`book`](Self::book) moves both, which is what -/// makes it impossible to move a lane without the guard seeing it. -/// -/// # Saturation -/// -/// Both halves saturate. A ledger is a reported quantity that feeds no identity beyond the law's -/// own term, and a saturated lane still answers the guard's question the same way an exact one -/// would; an overflow panic on the inspected path would not. +/// Both halves saturate. A ledger feeds no identity beyond the law's own term, and a saturated +/// lane answers the guard the same way an exact one would; an overflow panic would not. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Lane { /// The sum of every booking, signed — what the transaction's envelope actually moved by. @@ -41,11 +27,10 @@ pub struct Lane { } impl Lane { - /// A lane that carried one booking of `net`. + /// A lane that carried one booking of `net`, whose gross is therefore `|net|`. /// - /// The gross is `|net|`, which is what a single booking always produces. This is the - /// constructor for a caller stating an expectation over a lane moved in one direction; a lane - /// moved in both needs [`of`](Self::of), because the two numbers are then independent. + /// A lane moved in both directions needs [`of`](Self::of): the two numbers are then + /// independent. #[inline] pub const fn once(net: i128) -> Self { Self { net, gross: net.unsigned_abs() } @@ -113,220 +98,159 @@ impl Lane { /// What an inspector conjured, destroyed, rewrote, or had refused, as measured at the callback /// boundaries. /// -/// # Why the boundary is a sound place to measure -/// -/// The EVM does not execute inside an inspector callback. Every change to an interpreter's gas -/// counter, to the action it is holding, to its working state, or to a frame input's gas limit -/// that is visible across a callback's entry and exit is therefore the inspector's, by -/// construction rather than by attribution heuristics. The shim takes one snapshot before -/// delegating and one after, and the difference lands here. +/// The EVM does not execute inside a callback, so anything visible across one is the inspector's +/// by construction rather than by attribution. The shim snapshots before delegating and after, and +/// the difference lands here. /// /// # Sign convention /// -/// Every field measuring gas is a [`Lane`], whose net reads *from the transaction's point of -/// view*: a positive value is gas the inspector conjured — gas that exists in the execution but -/// that nothing debited from the transaction's envelope — and a negative value is gas it -/// destroyed. Both directions are recorded, because the conservation law needs the net; and each -/// lane carries the gross beside it, because the block guard needs to know a lane moved at all. +/// Every gas field is a [`Lane`] whose net reads from the transaction's point of view: positive is +/// gas that exists in the execution but that nothing debited from the envelope, negative is gas +/// the envelope funded that no frame received. /// /// # What it does not measure /// -/// What a callback does behind the shim's back. An inspector reaches state that no argument it is -/// handed describes — the *contents* of the interpreter's stack, memory, return buffer, calldata -/// and code, and the journal — and telling whether any of those came back changed needs a snapshot -/// of unbounded state that no callback boundary can take at a cost the inspected path can carry. -/// Everything about the interpreter that *is* a constant-time reading is the exception, and the -/// shim takes all of it: a frame whose memory was grown, whose program counter was stepped past an -/// instruction, or whose return buffer was conjured lands on -/// [`interventions`](Self::interventions). A rewrite that leaves every one of those readings where -/// it was leaves this all-zero. +/// What a callback does behind the shim's back: the *contents* of the interpreter's stack, memory, +/// return buffer, calldata and code, and the journal. Telling whether those came back changed +/// needs a snapshot of unbounded state that a per-opcode boundary cannot take. Every constant-time +/// reading of the interpreter is the exception and all of it is taken, landing on +/// [`interventions`](Self::interventions). /// -/// So an empty ledger says two things: no gas moved that the EVM did not move, and nothing the -/// shim was handed came back different. It does not say the transaction is the one the EVM would -/// have produced alone. +/// So an empty ledger says no gas moved that the EVM did not move, and nothing the shim was handed +/// came back different. It does not say the transaction is the one the EVM would have produced +/// alone. /// -/// # The three numbers a receipt carries +/// # Why the lanes are grouped as they are /// -/// A transaction's receipt reports its spent envelope, the refund applied to it, and — under -/// EIP-8037 — the state gas it consumed. The lanes are grouped by which of the three a rewrite -/// moves, because that is what decides whether the conservation law can see it: +/// A receipt reports its spent envelope, the refund applied to it, and — under EIP-8037 — the state +/// gas consumed. Which of the three a lane moves is what decides whether the conservation law can +/// see it: /// /// - [`gas`](Self::gas), [`env`](Self::env), [`result`](Self::result) and -/// [`reservoir`](Self::reservoir) move the envelope, and their nets are summed into +/// [`reservoir`](Self::reservoir) move the envelope, and their nets sum into /// [`conjured_gas`](Self::conjured_gas), the law's `I` term; /// - [`refund`](Self::refund) moves the refund, which the law — stated over `limit - remaining` — -/// cannot see at all; -/// - [`state_gas`](Self::state_gas) moves the receipt's state-gas figure, which the law does not -/// reach either. +/// cannot see; +/// - [`state_gas`](Self::state_gas) moves the receipt's state-gas figure, which it cannot reach +/// either. /// -/// All six are read by [`is_zero`](Self::is_zero), which is what the block guard asks — through -/// their gross halves, so that a lane whose bookings cancelled is not a lane nobody touched. +/// All six are read by [`is_zero`](Self::is_zero) through their gross halves. /// /// # What consumes it /// -/// - [`conjured_gas`](Self::conjured_gas) is the term the destroyed-remainder derivation adds to -/// the envelope, so that a transaction run under a rewriting inspector still satisfies `destroyed -/// = spent + minted + conjured − non_compute − enforced`. Without it, gas the inspector created -/// out of nothing would show up as the transaction having spent less than it really did, and the -/// derived destroyed total would go negative. -/// - The ledger as a whole is a *reported* quantity. No resource limit is ever compared against it, -/// and enforcement never sees an inspector's adjustment: `record_inspector_gas_adjustment` shifts -/// the checkpoint baseline by the same amount it books here, so the compute-gas measurement of a -/// frame covers the work the EVM performed and nothing else. +/// [`conjured_gas`](Self::conjured_gas) is the term the destroyed-remainder derivation adds to the +/// envelope; without it, gas created out of nothing reads as the transaction having spent less +/// than it did and the derived total can go negative. Everything else here is reported and nothing +/// more — no limit is compared against it, and enforcement never sees an inspector's adjustment, +/// because the site that books one shifts the checkpoint baseline by the same amount. /// -/// # Reading a frame-level aggregate -/// -/// The ledger is cumulative over the whole transaction and is deliberately not a per-frame stack: -/// aligning another stack with the EVM's frame lifecycle is exactly the machinery the frame-loop -/// rework replaces. A caller that wants what an inspector did to *one* frame reads the whole -/// ledger at the frame's entry and again at its exit and takes the difference — the type is `Copy` -/// and every field is a running total, so the difference of two readings is the aggregate over the -/// window between them, whatever happened inside it. +/// Cumulative over the transaction and deliberately not a per-frame stack. A caller wanting one +/// frame's aggregate reads the ledger at that frame's entry and exit and subtracts; the type is +/// `Copy` and every field is a running total. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct InspectorLedger { - /// Gas the inspector wrote into interpreter gas counters, across every callback that is - /// handed a live [`Interpreter`](revm::interpreter::Interpreter). - /// - /// A running frame's counter is the frame's own budget, so raising it hands the frame gas the - /// caller never forwarded and lowering it takes gas away that the caller will never get back. + /// Gas the inspector wrote into interpreter gas counters, at every callback handed a live + /// [`Interpreter`](revm::interpreter::Interpreter). /// - /// A callback that removed the interpreter's pending action lands here too: with no action - /// left the frame carries on spending what it holds, which is exactly what the counter is. + /// A running frame's counter is its own budget, so raising it hands the frame gas the caller + /// never forwarded and lowering it takes gas the caller will never get back. A callback that + /// removed the pending action lands here too: with no action left, the frame carries on + /// spending exactly what the counter holds. pub gas: Lane, - /// Gas the inspector wrote into frame *envelopes* — the `gas_limit` a call or create frame - /// is about to be built with. - /// - /// The caller was debited the forwarded amount by its own `CALL` / `CREATE` opcode, before any - /// inspector callback ran, so a raised limit is gas nobody paid for and a lowered one is gas - /// the caller paid for and no frame ever receives. + /// Gas the inspector wrote into a frame *envelope* — the `gas_limit` a call or create frame is + /// about to be built with. /// - /// Only adjustments that actually reach a frame are booked. When the callback returns a - /// synthetic outcome it has intercepted the frame entirely, and the EVM never reads the inputs - /// it edited — so the edit by itself moves nothing. (The inspector can of course read its own - /// edit back and size the synthetic outcome from it. That gas travels through the result lane - /// below, not through this one.) + /// The caller's own `CALL` / `CREATE` opcode debited the forwarded amount before any callback + /// ran, so a raised limit is gas nobody paid for and a lowered one is gas the caller paid for + /// and no frame receives. /// - /// The same lane carries an edit made one step earlier, to the `gas_limit` inside a pending - /// `NewFrame` action — the object the caller's `CALL` / `CREATE` opcode produced, before any - /// callback saw the inputs built from it. It is booked whether or not the frame is then - /// intercepted: an interception discards inputs the *same* callback edited a moment before, - /// which is why that edit reaches nothing, and it cannot un-make an edit another callback made - /// to the action the caller's debit is already behind. + /// A synthetic outcome moves this lane's net by nothing: the frame is intercepted and the EVM + /// never reads the inputs the same callback edited. Gas the inspector then sizes that outcome + /// from travels on [`result`](Self::result) instead. /// - /// Adjustments to a frame's *result* gas belong to [`result`](Self::result), which is booked - /// from the frame's own settlement point rather than from a callback boundary. + /// The lane also carries an edit made one step earlier, to the `gas_limit` inside a pending + /// `NewFrame` action. That object is the one the caller's opcode produced, so its debit is + /// already behind it and a later interception cannot un-make the edit — it is booked either + /// way. Its traffic is booked where it is made and its movement when the child's frame-start + /// callback can tell an edit from an interception. pub env: Lane, - /// Gas the inspector wrote into a frame *result* — what the frame hands back to its - /// caller — across the last callback that can rewrite that result. - /// - /// Unlike the other two lanes this one cannot be booked at the callback boundary, because - /// whether the edit moves anything depends on how the frame ends: a returning or reverting - /// frame's remaining gas is reclaimed by its caller, so an edit to it changes what the - /// transaction spends, while a halting frame's is not handed back at all and an edit to it - /// changes nothing. The frame's settlement point knows the final classification and books - /// this lane only in the first case; in the second it reconstructs the EVM's own number and - /// settles the destroyed remainder against that instead. - /// - /// The same lane carries an edit made one step earlier, to the gas inside a pending `Return` - /// action. That action *is* the frame's result a moment later, so the two are one number - /// measured on either side of the classification, and they settle together. - /// - /// And it carries the gas of a result the inspector produced outright, by answering a frame - /// with a synthetic outcome. That one is not a difference across a callback — no frame is - /// built, so there is no EVM-produced number on the other side — but against the envelope the - /// answering callback was handed, which the transaction did fund. The two are the same - /// question either way, and they settle at the same point for the same reason: a returning or - /// reverting outcome hands its gas back to the caller, a halting one hands nothing back and - /// the whole envelope is destroyed whatever figure the outcome claimed. + /// Gas the inspector wrote into a frame *result* — what the frame hands back to its caller. + /// + /// The lane's two halves are booked in two different places, because the two questions are + /// answered in two different places. Whether an edit *moved the envelope* depends on how the + /// frame ends: a returning or reverting frame's remainder is reclaimed by its caller, a + /// halting one's is not handed back at all. Only the frame's settlement point knows that, so + /// it books the net. Whether the inspector *made* an edit is known at the boundary, so that is + /// where the traffic is booked. + /// + /// Splitting them is load-bearing rather than tidy. An edit staged at `step_end` into a + /// construction frame's pending `Return` action is charged the code deposit out of that same + /// action before anything settles, so it can turn a successful creation into an `OutOfGas` + /// that deploys nothing — while the classification and output a boundary compares stay exactly + /// where they were. Booking only the net would leave that transaction reading as untouched. + /// + /// The same lane carries the gas of a result the inspector produced outright by answering a + /// frame with a synthetic outcome. There is no EVM-produced number on the other side, so it is + /// measured against the envelope the answering callback was handed, which the transaction did + /// fund; it settles at the same point and by the same classification. pub result: Lane, /// The EIP-8037 state-gas pool the transaction ends holding, which is gas nothing funded. /// - /// `MegaETH` runs with EIP-8037 off on every path and every spec, so no instruction can charge - /// against a reservoir and no `MegaETH` site ever fills one: the reservoir a transaction ends - /// with is zero unless an inspector wrote it. What a non-zero one does is move the envelope — - /// the receipt reports `limit - remaining - reservoir` as spent, and the caller is reimbursed - /// `remaining + reservoir + refunded` — so this lane is summed into - /// [`conjured_gas`](Self::conjured_gas) alongside the three above. + /// `MegaETH` runs with EIP-8037 off on every path and every spec, so a non-zero reservoir is + /// the inspector's in whole. It moves the envelope — the receipt reports + /// `limit - remaining - reservoir` as spent — so it joins + /// [`conjured_gas`](Self::conjured_gas). /// - /// Unlike them it is settled once, at the transaction's own settlement point, rather than at a - /// callback boundary. Two facts make that the only sound reading. revm propagates a reservoir - /// between frames by *replacement* — a returning child's reservoir overwrites its caller's — - /// so an edit made while a `NewFrame` action is already pending is erased by the child that - /// action builds, and a boundary difference would book gas that moved nothing. And the - /// `state_gas_spent` counter converts into a reservoir on a frame that fails, at a site no - /// callback sees. Reading the final number instead covers both: it is exactly the part of - /// every edit that survived, and `MegaETH` contributes none of it, so no difference has to be - /// taken to isolate the inspector's share. + /// Settled once from the final figure rather than differenced at a boundary, for two reasons + /// that each rule a boundary out. revm propagates a reservoir between frames by *replacement*, + /// so an edit made with a `NewFrame` action pending is erased by the child that action builds. + /// And `state_gas_spent` converts into a reservoir on a failing frame, at a site no callback + /// sees. The final number is exactly the part of every edit that survived. pub reservoir: Lane, /// EIP-8037 state gas the inspector wrote into the `state_gas_spent` counters. /// - /// The reservoir's counterpart on the spending side, and dead for the same reason `MegaETH` - /// never fills one — except at the two places revm reads it regardless of whether EIP-8037 is - /// enabled: a successful transaction reports its final value on the receipt, and a failing - /// frame folds it back into its caller's reservoir. - /// - /// The second of those two effects is already inside [`reservoir`](Self::reservoir) — the - /// final reservoir is read after the fold — so this lane carries the first, and is - /// deliberately not part of [`conjured_gas`](Self::conjured_gas): the receipt's state-gas - /// figure is not the envelope, and adding it to the law's `I` term would make the law - /// wrong by exactly this amount. Settled at the same point and for the same reasons as the - /// lane above. + /// The reservoir's counterpart on the spending side, and settled the same way. revm reads it + /// at two places regardless of whether the EIP is enabled: a successful transaction reports + /// its final value, and a failing frame folds it into its caller's reservoir. The second is + /// already inside [`reservoir`](Self::reservoir), so this lane carries the first — and stays + /// out of [`conjured_gas`](Self::conjured_gas), because the receipt's state-gas figure is not + /// the envelope and adding it would make the law wrong by exactly this amount. pub state_gas: Lane, /// Gas the inspector wrote into the `refunded` counters of the `Gas` objects it is handed. /// - /// A refund is the one number on a receipt the conservation law cannot see: the law is stated - /// over `total_gas_spent`, which is `limit - remaining` and which no refund enters. What a - /// refund does reach is `tx_gas_used` — what the sender actually pays — and the caller's - /// reimbursement. So the lane exists for [`is_zero`](Self::is_zero) and the block guard behind - /// it, and is deliberately kept out of [`conjured_gas`](Self::conjured_gas). - /// - /// # Nominal, in both senses - /// - /// The figure booked is what the inspector wrote, not what survived to the receipt. - /// - /// Not what survived the *cap*, because EIP-3529 caps the transaction's whole refund at a - /// fifth of what it burnt, over the sum of every refund the transaction accumulated, at a - /// point after the envelope is final and with no frame left standing. Splitting that cap - /// between the EVM's own refunds and an inspector's needs a priority rule the protocol - /// does not have — EVM-first, inspector-first and pro rata are all defensible, which means - /// none of them is a measurement. - /// - /// And not what survived the *frame chain*, because revm hands a frame's refund to its caller - /// only when the frame succeeded: an edit reaches the receipt exactly when every frame from - /// the one that was edited up to the top returns successfully. That is a condition no callback - /// boundary and no single settlement point can answer without a refund stack aligned to the - /// EVM's frame lifecycle, which is the machinery this ledger deliberately does not have. - /// - /// The lane's gross half is what makes the second of those safe. A `+R` on a frame that - /// survives and a `−R` on one that is rolled back are equal and opposite where they are - /// booked and not where they land, so a net-only reading would call that pair untouched while - /// the sender pays `R` less. - /// - /// Both directions of the choice are safe here because the lane feeds no identity. - /// Over-stating it costs nothing; under-stating it would let a transaction whose receipt - /// an inspector moved into a block, which is the one thing the lane exists to prevent. + /// The one receipt number the conservation law cannot see: the law is stated over + /// `limit - remaining`, which no refund enters. What a refund does reach is what the sender + /// pays. So the lane exists for [`is_zero`](Self::is_zero) and is kept out of + /// [`conjured_gas`](Self::conjured_gas). + /// + /// Nominal in two senses, and deliberately so. Not what survived the EIP-3529 *cap*, because + /// splitting that cap between the EVM's refunds and an inspector's needs a priority rule the + /// protocol does not have — EVM-first, inspector-first and pro rata are all defensible, so + /// none of them is a measurement. And not what survived the *frame chain*, because a refund + /// reaches the receipt only if every frame above the edited one returns successfully, which no + /// boundary can answer without a refund stack aligned to the frame lifecycle. + /// + /// The gross half is what makes the second safe: a `+R` on a surviving frame and a `−R` on a + /// rolled-back one are equal and opposite where they are booked and not where they land. + /// Over-stating costs nothing here, because the lane feeds no identity; under-stating would + /// admit a transaction whose receipt an inspector moved. pub refund: Lane, /// How many rewrites the shim refused because their shape is forbidden. /// - /// Two shapes are, and both for the same reason: the journal decision they would need to move - /// with was already taken, at a point no callback can reach. - /// - /// - A `create_end` (or the `frame_end` after it) turning a non-successful contract creation - /// into a successful one. Such a rewrite runs after the journal has already reverted the - /// frame and after the deposit predicates have already rejected the code, so honouring it - /// would report a deployment that never happened. - /// - Any of the three `*_end` callbacks moving the classification of a result *frame init* - /// produced across the success / revert / halt boundary. revm decides the journal inside - /// `make_call_frame` and `MegaETH`'s interceptors decide theirs before they return, so - /// honouring it would hand the caller an answer the state behind it contradicts. + /// Two shapes are, both because the journal decision they would have to move with was already + /// taken where no callback can reach it: a contract creation rewritten from failure into + /// success, after the journal reverted and the deposit predicates rejected the code; and any + /// `*_end` moving the classification of a result *frame init* produced across the success / + /// revert / halt boundary, which revm and `MegaETH`'s interceptors both decide before + /// returning. /// - /// A non-zero count means the transaction was failed with an `EVMError::Custom` rather than + /// A non-zero count means the transaction failed with an `EVMError::Custom` rather than being /// given a receipt. pub rejected_rewrites: u32, From e8e40f58b4c01f1d90b9002a8c7dd892a816be1f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:17:09 +0800 Subject: [PATCH 177/208] docs(limit): compress the destroyed-remainder module's prose The producer table and the upgrade obligations are the load-bearing part and stay; the paragraphs restating them around the table go. Non-comment lines are byte-identical. --- crates/mega-evm/src/limit/destroyed.rs | 73 +++++++++++--------------- 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/crates/mega-evm/src/limit/destroyed.rs b/crates/mega-evm/src/limit/destroyed.rs index b984645a..c089beb7 100644 --- a/crates/mega-evm/src/limit/destroyed.rs +++ b/crates/mega-evm/src/limit/destroyed.rs @@ -1,71 +1,60 @@ //! Destroyed-remainder classification for a frame result's [`InstructionResult`]. //! //! The conservation law defines a transaction's destroyed total from the envelope. The per-site -//! bookings that cross-check it still have to decide, for each result, whether the remaining gas -//! was swallowed (book it) or handed back (book nothing). That decision used to be -//! `is_ok_or_revert()` — a catch-all on the halt side, so a variant revm added later would be -//! swallowed without anyone classifying it. +//! bookings that cross-check it still have to decide, per result, whether the remaining gas was +//! swallowed (book it) or handed back (book nothing). //! -//! [`destroyed_disposition`] is the closed table: every [`InstructionResult`] variant has an -//! explicit arm, and there is no `_`. A revm bump that adds a variant fails to compile here until -//! a human assigns it. +//! [`destroyed_disposition`] is the closed table for that: every variant has an explicit arm and +//! there is no `_`, so a revm bump that adds one fails to compile until a human assigns it. It +//! used to be `is_ok_or_revert()` — a catch-all on the halt side, which would have swallowed a new +//! variant without anyone classifying it. //! //! # Where the table is read //! -//! Four sites ask it, all of them inside the frame's single settlement point -//! [`AdditionalLimit::finalize_frame`](super::AdditionalLimit::finalize_frame): -//! `settle_exceptional_halt_burn`, `settle_frame_init_reject_burn`, +//! Four sites, all inside [`AdditionalLimit::finalize_frame`](super::AdditionalLimit:: +//! finalize_frame): `settle_exceptional_halt_burn`, `settle_frame_init_reject_burn`, //! `settle_precompile_envelope`, and `settle_inspector_result_gas`. The first three book a -//! destroyed remainder; the fourth books nothing destroyed but decides the same question for an -//! inspector's edit — an edit to a returned result moves what the transaction spends and goes to -//! the ledger, an edit to a swallowed one does not and is undone. +//! destroyed remainder; the fourth books none but answers the same question for an inspector's +//! edit — an edit to a returned result moves what the transaction spends, an edit to a swallowed +//! one does not. //! //! A site that has to stay byte-identical with an upstream branch keyed on `is_ok_or_revert()` -//! keeps that predicate instead: the precompile dispatch in `evm/precompiles.rs` undoes revm's -//! own `spend_all()`, rebuilds the `Gas` object revm's refund logic will read, and reads -//! `total_gas_spent()` only where revm did not spend it down. Those mirror an upstream decision -//! rather than stating one on `MegaETH`'s books, so they follow upstream's predicate wherever it -//! goes. +//! keeps that predicate instead: the precompile dispatch mirrors an upstream decision rather than +//! stating one on `MegaETH`'s books, so it follows upstream's predicate wherever it goes. //! //! # Producers × accounting sites //! -//! After the frame-settlement single point, every producer that can destroy an envelope books at -//! exactly one of the sites below. Completeness of the *reported* total is still the conservation -//! law; this table is what the per-site bookings — and a revm-upgrade diff — are checked against. +//! Every producer that can destroy an envelope books at exactly one site below. Completeness of +//! the *reported* total is still the conservation law; this is what the per-site bookings — and a +//! revm-upgrade diff — are checked against. //! //! | Producer | Accounting site | Notes | //! | --- | --- | --- | -//! | Frame-run exceptional halt, including create-return rejects (`CreateContractSizeLimit`, `CreateContractStartingWithEF`, deposit `OutOfGas`) | [`AdditionalLimit::finalize_frame`](super::AdditionalLimit::finalize_frame) on `FrameExit::Ran` → `settle_exceptional_halt_burn` | [`DestroyedDisposition::Swallow`] | +//! | Frame-run exceptional halt, including create-return rejects | `finalize_frame` on `FrameExit::Ran` → `settle_exceptional_halt_burn` | [`DestroyedDisposition::Swallow`] | //! | Frame-init refusal from revm (`make_call_frame` / `make_create_frame` early-fail arms) | `finalize_frame` on `FrameExit::Refused` → `settle_frame_init_reject_burn` | per variant: collision / overflow-payment swallow; depth / funds / empty-code / nonce-overflow return | -//! | Synthetic frame-init refusal (system-contract interceptor, inspector intercept, REX5 depth guard) | `finalize_frame` on `FrameExit::RefusedSynthetically` → the same burn | same classification; a `KeylessDeploy` destroying halt is the row below, not this one | -//! | Precompile halt | `finalize_frame` → `settle_frame_init_reject_burn` → `settle_precompile_envelope`, against the forwarded envelope and the executed work staged by `evm/precompiles.rs` | the staged slot, not `CallOutcome::was_precompile_called`, is what routes a result here instead of to the generic burn; KZG splits executed / destroyed by halt reason | -//! | `KeylessDeploy` synthetic halt that keeps the call's gas (cannot pay dispatch overhead; cannot pay signer-materialization storage gas) | `sandbox/execution.rs::destroying_oog_frame_result`, which books remaining *before* the result spends the envelope | swallow; `finalize_frame` then sees remaining 0 and books nothing | -//! | Failed-deposit receipt rewrite | [`AdditionalLimit::settle_rewritten_envelope`](super::AdditionalLimit::settle_rewritten_envelope) | not an `InstructionResult` decision: the gap between the rebuilt envelope and the per-site bookings | -//! | Intrinsic pre-frame out-of-gas (`before_execution`) | `MegaHandler::before_execution` | swallow of `gas_limit − performed`; unreachable on REX7 (REX5+ rejects that transaction in validation) | +//! | Synthetic frame-init refusal (interceptor, inspector intercept, REX5 depth guard) | the same burn, on `FrameExit::RefusedSynthetically` | same classification | +//! | Precompile halt | → `settle_precompile_envelope`, against the staged forwarded envelope and executed work | the staged slot, not `CallOutcome::was_precompile_called`, routes a result here | +//! | `KeylessDeploy` synthetic halt that keeps the call's gas | `sandbox/execution.rs::destroying_oog_frame_result`, which books remaining *before* the result spends the envelope | swallow; `finalize_frame` then sees remaining 0 | +//! | Failed-deposit receipt rewrite | [`AdditionalLimit::settle_rewritten_envelope`](super::AdditionalLimit::settle_rewritten_envelope) | the gap between the rebuilt envelope and the per-site bookings | +//! | Intrinsic pre-frame out-of-gas | `MegaHandler::before_execution` | unreachable on REX7 (REX5+ rejects that transaction in validation) | //! //! A new producer belongs on this table with its own site, not as a silent extra call to //! `record_burned_gas`. A new [`InstructionResult`] variant belongs in [`destroyed_disposition`]. //! -//! # The other closed table +//! # The other closed tables //! -//! `tests/rex7/gas_surface.rs` closes a different axis and the two do not overlap: it enumerates -//! the *carriers* — which field of which object an inspector callback is handed carries gas, and -//! which lane books it — while this one enumerates the *endings*, which classification decides -//! whether a carrier's remainder is handed back or swallowed. A number reaches the receipt -//! through a carrier that table names and an ending this one names, and the two answers are -//! composed at `finalize_frame`. +//! `tests/rex7/gas_surface.rs` closes a perpendicular axis: it enumerates the *carriers* — which +//! field of which object carries gas and which lane books it — while this one enumerates the +//! *endings*. `finalize_frame` composes the two answers. //! -//! # Early-fail arms are a second closed set -//! -//! `make_call_frame`, `make_create_frame`, and `return_create` / `classify_create_return` each -//! return a result without running a child body on a fixed list of arms. Those arms are not an -//! enum: a revm bump that adds one does not fail this match. The upgrade checklist diffs them by -//! hand against the list in `tests/rex7/result_space_tripwire.rs`. +//! `make_call_frame`, `make_create_frame` and `classify_create_return` each return a result without +//! running a child body on a fixed list of arms. Those arms are not an enum, so a revm bump that +//! adds one does not fail this match; the upgrade checklist diffs them by hand against the list in +//! `tests/rex7/result_space_tripwire.rs`. //! //! One live mismatch is load-bearing: a CREATE whose nonce cannot be bumped returns //! [`InstructionResult::Return`], not [`InstructionResult::NonceOverflow`]. The variant is still -//! classified (swallow, because it is a halt), so that if an arm ever starts producing it the -//! booking is defined. +//! classified, so that if an arm ever starts producing it the booking is defined. use revm::interpreter::InstructionResult; From 472b36121597bf58148d1766b633a41c9c57e0ca Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:18:34 +0800 Subject: [PATCH 178/208] docs(limit): tighten the inspector settlement prose, and drop a stale claim The precompile envelope's doc still said the settlement point exists because an inspector's call_end can rewrite the classification afterwards. That rewrite is refused, and the branch that served it is gone; what the staging is for is taking the split beside every other frame outcome. Non-comment lines are byte-identical. --- crates/mega-evm/src/limit/limit.rs | 83 +++++++++++++----------------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index d81039f0..3866b400 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -50,12 +50,12 @@ pub enum FrameExit { /// point can decide the split (REX7+). /// /// A precompile is answered inside the frame init and never becomes a child frame, so its -/// recording site is the only place that knows both of these numbers: the envelope is the -/// caller-supplied forwarded amount rather than the REX5-capped effective limit, and the work is -/// `MegaETH`'s own price for what the call performed, which a halting precompile's gas object does -/// not carry. What that site cannot know is how the call ends — an inspector's `call_end` runs -/// afterwards and can rewrite the classification, and the classification is what decides whether -/// the caller reclaims the remainder. +/// recording site is the only place that knows both numbers: the envelope is the caller-supplied +/// forwarded amount rather than the REX5-capped effective limit, and the work is `MegaETH`'s own +/// price for what the call performed, which a halting precompile's gas object does not carry. +/// Carrying them to the settlement point is what lets the split be taken against the +/// classification the caller is finally handed, beside every other frame outcome, rather than +/// being decided early and separately. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct PrecompileEnvelope { /// The gas the caller forwarded, uncapped. @@ -526,48 +526,35 @@ impl AdditionalLimit { /// Books an adjustment an inspector made to a live interpreter's gas counter, and restores /// correct accounting and enforcement around it. /// - /// This is the single entry point for interpreter-counter adjustments. `remaining_before` is - /// the counter the shim snapshotted before delegating to the user's callback, and - /// `gas.remaining()` is what the callback left behind; the difference is the adjustment, - /// because the EVM does not execute inside a callback. - /// - /// `reaches_envelope` is whether the counter the callback left behind is one the EVM will read - /// again — false exactly when the interpreter is already holding a terminating action, whose - /// own copy of the counter is what the caller reclaims from. It gates the ledger and - /// nothing else: an edit nobody will read moves no gas and must not be booked, but - /// `MegaETH`'s own tail settlement does read this counter after the action is set, so the - /// baseline still has to shift or the edit would be measured as work the frame performed. - /// - /// Three things happen, in this order: - /// - /// 1. **The ledger** takes the adjustment, if it can reach the envelope at all, so the - /// conservation law can account for gas nobody funded (or gas that vanished) when it derives - /// the destroyed remainder. - /// 2. **The open segment is settled against the pre-callback counter** (REX7+, - /// `IN_OPEN_SEGMENT`). This is what keeps the adjustment out of enforcement: compute gas is - /// measured as a drop in the interpreter's counter, so an injection made mid-segment would - /// otherwise show up as *less* work than the frame performed — the frame would have been - /// handed free compute headroom. Closing the segment at `remaining_before` and re-opening it - /// at the adjusted counter measures exactly the work, and nothing else. - /// 3. **The gas clamp is re-derived** from the freshly settled usage, exactly as a checkpoint's - /// epilogue does. Without this, an injection would be spendable past the compute headroom: - /// the clamp hides gas beyond the headroom from the interpreter, and gas written in after - /// the clamp was applied is not hidden by it. - /// - /// `IN_OPEN_SEGMENT` is false at `initialize_interp`, the one callback that runs after a frame - /// is built but before its settlement window is opened. There is no segment to settle and no - /// clamp to re-derive there; the frame's own entry hook opens the window on the adjusted - /// counter a moment later, which absorbs the adjustment for free. - /// - /// The settlement records through the unguarded entry point for the same reason the frame-exit - /// tail settlement does: a callback can run immediately after an opcode whose pre-inner - /// recorder deliberately left a non-compute dimension unlatched, and the latch-protocol guard - /// would trip on it. - /// - /// A settlement that latches an exceed does not stop the interpreter here — a callback has no - /// way to fail an instruction. The latch is sticky, so the next checkpoint or the frame's own - /// exit surfaces it as it would have anyway; the adjustment only moves *when* the halt lands, - /// never whether it does. + /// The single entry point for interpreter-counter adjustments. `remaining_before` is what the + /// shim snapshotted before delegating and `gas.remaining()` is what the callback left behind. + /// + /// `reaches_envelope` is whether the EVM will read that counter again — false exactly when the + /// interpreter is already holding a terminating action, whose own copy is what the caller + /// reclaims from. It gates the ledger and nothing else: an edit nobody will read moves no gas, + /// but `MegaETH`'s tail settlement does read this counter after the action is set, so the + /// baseline shifts either way or the edit would be measured as work the frame performed. + /// + /// Then, in order: + /// + /// 1. **The open segment is settled against the pre-callback counter** (REX7+, + /// `IN_OPEN_SEGMENT`), which is what keeps the adjustment out of enforcement. Compute gas is + /// a drop in this counter, so an injection left inside the segment would read as *less* work + /// than the frame performed — free compute headroom. Closing at `remaining_before` and + /// reopening at the adjusted counter measures exactly the work. + /// 2. **The clamp is re-derived** from the usage just settled, as a checkpoint's epilogue does. + /// The clamp hides gas beyond the headroom from the interpreter, and gas written in after it + /// was applied is not hidden by it. + /// + /// `IN_OPEN_SEGMENT` is false at `initialize_interp`, which runs after a frame is built and + /// before its settlement window opens; the frame's entry hook opens that window on the adjusted + /// counter a moment later and absorbs the adjustment for free. + /// + /// Records through the unguarded entry for the same reason the frame-exit tail settlement does: + /// a callback can run right after an opcode whose pre-inner recorder deliberately left a + /// non-compute dimension unlatched, and the latch-protocol guard would trip on it. A latched + /// exceed does not stop the interpreter here — a callback cannot fail an instruction — and the + /// latch is sticky, so this moves *when* a halt lands, never whether it does. pub(crate) fn record_inspector_gas_adjustment( &mut self, gas: &mut Gas, From 674458a1f66c1ddc3440721ed43ee88d3d547b85 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:24:51 +0800 Subject: [PATCH 179/208] test(rex7): state the halting interception's traffic instead of reading it back The gross half was asserted against itself, which pins nothing. Each sizing already knows the movement it makes off the envelope; the traffic is its magnitude. --- crates/mega-evm/tests/rex7/interception_gas.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index d6b11af0..ca20c406 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -332,10 +332,10 @@ fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { reading.ledger, InspectorLedger { interventions: 1, - result: Lane::of(0, reading.ledger.result.gross()), + result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), ..InspectorLedger::default() }, - "{sizing:?}: and no lane but the result lane's traffic may carry anything", + "{sizing:?}: and the traffic is what the outcome claimed, off the envelope", ); assert_eq!( reading.destroyed, FORWARDED, From e8283ea972c3832dfd8fa44de302269118de75a9 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:27:09 +0800 Subject: [PATCH 180/208] docs(limit): keep the intra-doc link on one line --- crates/mega-evm/src/limit/destroyed.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/src/limit/destroyed.rs b/crates/mega-evm/src/limit/destroyed.rs index c089beb7..3f623476 100644 --- a/crates/mega-evm/src/limit/destroyed.rs +++ b/crates/mega-evm/src/limit/destroyed.rs @@ -11,8 +11,8 @@ //! //! # Where the table is read //! -//! Four sites, all inside [`AdditionalLimit::finalize_frame`](super::AdditionalLimit:: -//! finalize_frame): `settle_exceptional_halt_burn`, `settle_frame_init_reject_burn`, +//! Four sites, all inside [`finalize_frame`](super::AdditionalLimit::finalize_frame): +//! `settle_exceptional_halt_burn`, `settle_frame_init_reject_burn`, //! `settle_precompile_envelope`, and `settle_inspector_result_gas`. The first three book a //! destroyed remainder; the fourth books none but answers the same question for an inspector's //! edit — an edit to a returned result moves what the transaction spends, an edit to a swallowed From 6851bd17ef9aadd8de1a7a435477a06d5d4426ea Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:43:39 +0800 Subject: [PATCH 181/208] test(rex7): fold the suite's five transaction drivers into one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `common.rs` ran five near-identical bodies (build context, zero the operator fee, build the call, execute, read the tracker, assemble). One `drive` now takes the EVM the caller built, so the three files that needed their own context reach the same funnel instead of restating its tail. The shim files' own `read` and `assert_identity` moved into that funnel with them: `Outcome` carries `ConservationTerms` whole, the envelope check is stated as `envelope_for`, and the two checks the shim files ran and `common` did not — the outcome's ledger equals the tracker's, and the law's `I` term is the ledger's net — now run on every transaction the suite drives. `base_db` and `plain_filler` lose their thirteen copies. Assertions 1066 -> 1068 (0 removed; 1 restated as the same identity through `envelope_for`, 2 added). --- crates/mega-evm/tests/rex7/burn_split.rs | 40 +- .../tests/rex7/call_body_halt_charges.rs | 2 +- .../tests/rex7/checkpoint_families.rs | 19 +- .../tests/rex7/checkpoint_settlement.rs | 21 +- .../tests/rex7/checkpoint_static_fee_edges.rs | 16 +- .../tests/rex7/clamp_classification.rs | 21 +- crates/mega-evm/tests/rex7/common.rs | 502 ++++++++++-------- .../mega-evm/tests/rex7/conservation_terms.rs | 43 +- .../tests/rex7/create_code_deposit_charge.rs | 26 +- .../tests/rex7/deposit_receipt_rewrite.rs | 15 +- .../tests/rex7/double_exceed_corner.rs | 18 +- .../mega-evm/tests/rex7/exceptional_halt.rs | 18 +- .../tests/rex7/frame_init_reject_burn.rs | 19 +- crates/mega-evm/tests/rex7/gas_clamp.rs | 24 +- crates/mega-evm/tests/rex7/gas_leakage.rs | 19 +- .../tests/rex7/guard_pass_static_gas.rs | 34 +- .../mega-evm/tests/rex7/interceptor_resume.rs | 19 +- crates/mega-evm/tests/rex7/latch_surfacing.rs | 26 +- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 3 +- .../mega-evm/tests/rex7/measured_inspector.rs | 9 +- crates/mega-evm/tests/rex7/parity_shapes.rs | 20 +- 21 files changed, 381 insertions(+), 533 deletions(-) diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 28411850..2aa92d91 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -25,8 +25,9 @@ //! which side of the enforcing boundary each part lands on. use crate::common::{ - finish, transact, transact_default, transact_tx, transact_with_bucket_capacity, - transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, + base_db, drive, plain_filler, transact, transact_default, transact_tx, + transact_with_bucket_capacity, transact_with_gas_limit, zero_operator_fee, Outcome, CALLEE, + CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, }; use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall as _; @@ -63,22 +64,6 @@ const CHILD_PAIRS: usize = 1_000; /// Compute gas one `PUSH1 1; POP` pair costs: `PUSH1` is 3, `POP` is 2. const PAIR_GAS: u64 = 5; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that settle only at the next checkpoint. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// A callee that performs [`CHILD_PAIRS`] pairs of real work and then ends its frame with a stack /// underflow — an exceptional halt that is not a gas shortage, so the interpreter keeps its /// counter and nothing about the failure is a resource-limit exceed. @@ -413,24 +398,17 @@ fn transact_create_reject( let mut cfg = CfgEnv::default(); cfg.spec = MegaSpecId::REX7; cfg.limit_contract_code_size = code_size_limit.or(Some(MAX_CONTRACT_SIZE)); - let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) - .with_cfg(cfg) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); + let context = zero_operator_fee( + MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ); let tx = TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(MegaSpecId::REX7, outcome, detained_compute_gas_limit, terms) + drive(MegaSpecId::REX7, &mut evm, tx) } /// Runtime length the CREATE cases deploy — small enough that the per-byte code-deposit storage diff --git a/crates/mega-evm/tests/rex7/call_body_halt_charges.rs b/crates/mega-evm/tests/rex7/call_body_halt_charges.rs index c565ad85..3e4aa100 100644 --- a/crates/mega-evm/tests/rex7/call_body_halt_charges.rs +++ b/crates/mega-evm/tests/rex7/call_body_halt_charges.rs @@ -110,7 +110,7 @@ fn run_arm(opcode: u8, value: Option, ret_size: u64, budget: u64) -> Outcom "an out-of-gas frame's counter is zeroed by the interpreter, so it destroys nothing \ the frame-exit delta cannot already see as work", ); - assert_eq!(outcome.booked_destroyed, 0, "and no site books a destroyed remainder for it"); + assert_eq!(outcome.booked_destroyed(), 0, "and no site books a destroyed remainder for it"); outcome } diff --git a/crates/mega-evm/tests/rex7/checkpoint_families.rs b/crates/mega-evm/tests/rex7/checkpoint_families.rs index 5289f17c..3d5535f3 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_families.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_families.rs @@ -9,7 +9,9 @@ //! Each opcode is run twice: once plainly, and once with a detention cap engaged before it so a //! clamp is outstanding when its prologue runs. Both runs must be indistinguishable from REX6. -use crate::common::{assert_outcomes_identical, transact, CALLEE, CALLER, CONTRACT, ONE_ETH}; +use crate::common::{ + assert_outcomes_identical, base_db as common_base_db, plain_filler, transact, CALLEE, +}; use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, @@ -22,20 +24,7 @@ use revm::bytecode::opcode::{ }; fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, BytecodeBuilder::default().append(STOP).build()) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder + common_base_db(code).account_code(CALLEE, BytecodeBuilder::default().append(STOP).build()) } /// Wraps `snippet` in plain segments on both sides, optionally engaging a detention cap first, so diff --git a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs index e68714df..1285e0fa 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs @@ -16,8 +16,8 @@ //! enforcement mechanism behind the first — the gas clamp — has its own suite in `gas_clamp`. use crate::common::{ - transact, transact_default, transact_with_bucket_capacity, Outcome, CALLEE, CALLER, CONTRACT, - EMPTY_TARGET, ONE_ETH, + base_db, plain_filler, transact, transact_default, transact_with_bucket_capacity, Outcome, + CALLEE, CONTRACT, EMPTY_TARGET, }; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ @@ -33,13 +33,6 @@ use revm::bytecode::opcode::{ /// A third contract, so a CALL chain can reach depth 2. const INNER: Address = address!("0000000000000000000000000000000000300004"); -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// A SALT bucket capacity four times the minimum, so every SALT-scaled storage-gas charge /// (`SSTORE` set, new account, contract creation) is non-zero and the settlement sites that have /// to exclude those charges from the compute window are actually exercised. @@ -152,16 +145,6 @@ fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { Bytes::from(code) } -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own under checkpoint -/// accounting. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// A pure arithmetic loop settles only at the frame-exit checkpoint, and that single settlement /// must equal the sum the per-opcode wrappers would have recorded opcode by opcode. #[test] diff --git a/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs index 1c3a142a..40b00d0e 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs @@ -15,13 +15,12 @@ //! Each family has two top-frame edges, calibrated so the named headroom is the remaining compute //! at the opcode itself (prefix `PUSH` opcodes are measured out first). -use crate::common::{transact, Outcome, CALLER, CONTRACT, ONE_ETH}; -use alloy_primitives::{Address, Bytes, U256}; +use crate::common::{base_db, transact, Outcome, CONTRACT}; +use alloy_primitives::{Address, Bytes}; use alloy_sol_types::SolError; use mega_evm::{ - constants::mini_rex::LOG_TOPIC_STORAGE_GAS, - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, + constants::mini_rex::LOG_TOPIC_STORAGE_GAS, test_utils::BytecodeBuilder, EvmTxRuntimeLimits, + LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, }; use revm::{ bytecode::opcode::{CREATE, GAS, LOG1, STOP}, @@ -40,13 +39,6 @@ const LOG1_BODY_COMPUTE: u64 = 750; /// `CREATE` body fee. The REX7 table entry is 0; revm charges this inside the body. const CREATE_BODY_GAS: u64 = 32_000; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) } diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index 8167a71d..e2e2c5d9 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -15,13 +15,14 @@ //! settlement that runs after the exceed is latched. use crate::common::{ - transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH, + base_db, plain_filler as common_plain_filler, transact, transact_default, + transact_with_gas_limit, Outcome, CALLEE, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::SolError; use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, + test_utils::BytecodeBuilder, EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, + MegaSpecId, }; use revm::{ bytecode::opcode::{ @@ -31,25 +32,13 @@ use revm::{ context::result::ExecutionResult, }; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// Per-spec runtime limits with the TX compute gas limit replaced. fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) } -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own, five gas each. fn plain_filler(pairs: usize) -> Vec { - let mut builder = BytecodeBuilder::default(); - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder.build_vec() + common_plain_filler(BytecodeBuilder::default(), pairs).build_vec() } // --------------------------------------------------------------------------------------------- diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index cab64d67..6d1c4c05 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -2,11 +2,12 @@ use alloy_primitives::{address, Address, Bytes, B256, U256}; use mega_evm::{ - test_utils::MemoryDatabase, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, - InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, - MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, + test_utils::{BytecodeBuilder, MemoryDatabase}, + ConservationTerms, EvmTxRuntimeLimits, ExternalEnvTypes, InspectorLedger, MegaContext, MegaEvm, + MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, }; use revm::{ + bytecode::opcode::POP, context::{result::ExecutionResult, tx::TxEnvBuilder, TxEnv}, handler::EvmTr, state::EvmState, @@ -26,6 +27,29 @@ pub(crate) const EMPTY_TARGET: Address = address!("00000000000000000000000000000 /// One ether, in wei. pub(crate) const ONE_ETH: u128 = 1_000_000_000_000_000_000; +/// The transaction gas limit [`transact`] runs with — high enough that EVM gas is never the +/// binding constraint. +pub(crate) const DEFAULT_TX_GAS_LIMIT: u64 = 100_000_000; + +/// The standard fixture: a funded [`CALLER`], `code` at [`CONTRACT`], and a balance there for the +/// value transfers and SELFDESTRUCTs the fixtures make. +pub(crate) fn base_db(code: Bytes) -> MemoryDatabase { + MemoryDatabase::default() + .account_balance(CALLER, U256::from(10 * ONE_ETH)) + .account_code(CONTRACT, code) + .account_balance(CONTRACT, U256::from(ONE_ETH)) +} + +/// `pairs` PUSH/POP pairs: plain opcodes that touch nothing, for padding a segment out to a known +/// compute cost. +pub(crate) fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { + let mut builder = builder; + for _ in 0..pairs { + builder = builder.push_number(1u64).append(POP); + } + builder +} + /// The post-transaction readings compared across specs. pub(crate) struct Outcome { pub(crate) result: ExecutionResult, @@ -44,24 +68,15 @@ pub(crate) struct Outcome { pub(crate) destroyed: u64, /// Post-tx enforced compute gas — the part of [`compute_gas`](Self::compute_gas) every limit /// comparison and the block's admission counter run against. - pub(crate) enforced_lane: u64, + enforced_lane: u64, /// Receipt envelope before the EIP-3529 refund and the EIP-7623 floor: exactly the number /// settlement derives the destroyed total from. pub(crate) total_gas_spent: u64, /// Post-tx detained compute gas limit — equal to the configured TX limit unless volatile /// access lowered it. pub(crate) detained_compute_gas_limit: u64, - /// Post-tx non-compute EVM gas — the `MegaETH` storage gas and sandbox residue the destroyed - /// derivation subtracts. Signed: the sandbox boundary contributes a difference, not a charge. - pub(crate) non_compute_gas: i128, - /// Post-tx `CALL_STIPEND` total minted into child frames by value-transferring calls. - pub(crate) minted_call_stipend: u64, - /// Post-tx sum of the per-site destroyed bookings — the second opinion the derived - /// [`destroyed`](Self::destroyed) is cross-checked against, never the reported number. - pub(crate) booked_destroyed: u64, - /// Post-tx net gas an inspector conjured — zero for every transaction that ran without one, - /// and for every observation-only inspector. - pub(crate) inspector_conjured_gas: i128, + /// The conservation law's terms, as the tracker held them when the transaction ended. + pub(crate) terms: ConservationTerms, /// What the measurement shim booked for this transaction, as the outcome reports it. pub(crate) inspector_ledger: InspectorLedger, /// The state the transaction produced. @@ -90,6 +105,38 @@ impl Outcome { self.enforced_lane } + /// `S` — `MegaETH` storage gas plus the sandbox boundary residue. Signed. + pub(crate) fn non_compute_gas(&self) -> i128 { + self.terms.non_compute_gas + } + + /// `K` — the `CALL_STIPEND` total minted into child frames by value-transferring calls. + pub(crate) fn minted_call_stipend(&self) -> u64 { + self.terms.minted_call_stipend + } + + /// The sum of the per-site destroyed bookings — the second opinion the derived + /// [`destroyed`](Self::destroyed) is cross-checked against, never the reported number. + pub(crate) fn booked_destroyed(&self) -> u64 { + self.terms.booked_destroyed_compute_gas + } + + /// `I` — the net gas an inspector conjured. Zero for every transaction that ran without one, + /// and for every observation-only inspector. + pub(crate) fn inspector_conjured_gas(&self) -> i128 { + self.terms.inspector_conjured_gas + } + + /// The receipt's raw EIP-3529 refund, before the cap that decides how much of it applies. + pub(crate) fn refunded(&self) -> u64 { + self.result.gas().inner_refunded() + } + + /// The receipt's final EIP-8037 state-gas spend. + pub(crate) fn state_gas_spent(&self) -> u64 { + self.result.gas().state_gas_spent_final() + } + /// Reads a storage slot out of the produced state, defaulting to zero when the transaction /// never touched it. pub(crate) fn storage_value(&self, address: Address, slot: U256) -> U256 { @@ -101,58 +148,65 @@ impl Outcome { } } -/// The transaction gas limit [`transact`] runs with — high enough that EVM gas is never the -/// binding constraint. -pub(crate) const DEFAULT_TX_GAS_LIMIT: u64 = 100_000_000; +/// The transaction every helper here runs unless a test supplies its own: a plain call from +/// [`CALLER`] into [`CONTRACT`]. +fn call_contract_tx(gas_limit: u64) -> MegaTransaction { + let tx = + TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + tx +} -/// Runs a single transaction that calls [`CONTRACT`] under `spec` with the given DB and runtime -/// limits, returning the execution result plus the post-tx tracker readings and `gas_used`. -pub(crate) fn transact( - spec: MegaSpecId, - db: MemoryDatabase, - limits: EvmTxRuntimeLimits, -) -> Outcome { - transact_with_gas_limit(spec, db, limits, DEFAULT_TX_GAS_LIMIT) +/// Zeroes the operator fee, which otherwise adds an L1 charge to every receipt the suite reads. +pub(crate) fn zero_operator_fee( + mut context: MegaContext<&mut MemoryDatabase, EXT>, +) -> MegaContext<&mut MemoryDatabase, EXT> { + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + context } -/// [`transact`] with an explicit transaction gas limit, for cases that need EVM gas itself to run -/// out. -pub(crate) fn transact_with_gas_limit( +/// The context every helper here runs on: `spec`, `limits`, no operator fee, no external +/// environment. +pub(crate) fn context( + db: &mut MemoryDatabase, spec: MegaSpecId, - mut db: MemoryDatabase, limits: EvmTxRuntimeLimits, - gas_limit: u64, -) -> Outcome { - let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); - let tx = - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); - let mut evm = MegaEvm::new(context); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(spec, outcome, detained_compute_gas_limit, terms) +) -> MegaContext<&mut MemoryDatabase, mega_evm::EmptyExternalEnv> { + zero_operator_fee(MegaContext::new(db, spec).with_tx_runtime_limits(limits)) } -/// Assembles an [`Outcome`] from what a transaction reported and checks the terminal identity -/// before handing it back. +/// Runs `tx` on `evm` and assembles the [`Outcome`], checking the terminal identity before handing +/// it back. /// -/// Every helper in this module funnels through here, so every REX7 transaction the suite runs — -/// not just the ones written to look at gas — is a check that the tracker lanes reconcile with the -/// receipt the transaction produced. -pub(crate) fn finish( +/// Every helper in this module funnels through here, and so does every test that builds its own +/// EVM — so every REX7 transaction the suite runs, not just the ones written to look at gas, is a +/// check that the tracker lanes reconcile with the receipt the transaction produced. +pub(crate) fn drive<'db, INSP, EXT>( spec: MegaSpecId, - outcome: MegaTransactionOutcome, - detained_compute_gas_limit: u64, - terms: ConservationTerms, -) -> Outcome { + evm: &mut MegaEvm<&'db mut MemoryDatabase, INSP, EXT>, + tx: MegaTransaction, +) -> Outcome +where + INSP: Inspector>, + EXT: ExternalEnvTypes, +{ + let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + let (detained_compute_gas_limit, terms, tracker_ledger) = { + let additional_limit = EvmTr::ctx_ref(evm).additional_limit.borrow(); + ( + additional_limit.detained_compute_gas_limit(), + additional_limit.conservation_terms(), + additional_limit.inspector_ledger(), + ) + }; + assert_eq!( + outcome.inspector_ledger, tracker_ledger, + "the outcome must report the ledger the shim booked, unchanged", + ); let gas_used = outcome.result_and_state.result.tx_gas_used(); let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); let outcome = Outcome { @@ -166,10 +220,7 @@ pub(crate) fn finish( enforced_lane: outcome.compute_gas_enforced, total_gas_spent, detained_compute_gas_limit, - non_compute_gas: terms.non_compute_gas, - minted_call_stipend: terms.minted_call_stipend, - booked_destroyed: terms.booked_destroyed_compute_gas, - inspector_conjured_gas: terms.inspector_conjured_gas, + terms, inspector_ledger: outcome.inspector_ledger, state: outcome.result_and_state.state, }; @@ -177,91 +228,31 @@ pub(crate) fn finish( outcome } -/// The identity every REX7 transaction that produces a receipt must satisfy, connecting what the -/// trackers hold to the number the receipt reports. -/// -/// # The identity -/// -/// For one transaction, write -/// -/// ```text -/// C = compute_gas reported compute total -/// E = enforced_lane the part limits and block admission compare against -/// D = destroyed the part that is reported and accounted but never enforced -/// N = non_compute_gas MegaETH storage gas plus the sandbox boundary residue (signed) -/// M = minted_call_stipend CALL_STIPEND minted into child frames and never debited from a caller -/// I = inspector_conjured_gas gas an inspector wrote into the execution that nothing debited -/// S = total_gas_spent the receipt envelope, before the refund and the floor -/// R = the receipt's raw refund -/// F = the receipt's EIP-7623 floor gas -/// ``` -/// -/// then -/// -/// ```text -/// (1) C = E + D -/// (2) C + N − M − I = S -/// (3) receipt gas_used = max(S − R, F) -/// ``` -/// -/// (1) is the split of the reported total. (2) is the conservation law rearranged: settlement -/// defines `D = S + M + I − N − E`, so `S = D + E + N − M − I`, and substituting (1) gives -/// `S = C + N − M − I`. (3) is how a receipt's gas number is built from its envelope. -/// -/// `I` is zero for every transaction that runs without an inspector and for every -/// observation-only one, so (2) is the plain two-term identity on all but the handful of runs -/// that attach a rewriting inspector — which is exactly where it earns its keep. -/// -/// # Why (2) needs no refund or floor correction -/// -/// The EIP-3529 refund and the EIP-7623 floor both move the number the receipt reports without -/// anyone having burnt the difference. Both are applied strictly after the envelope is final, and -/// both are carried on the result as their own fields rather than folded into the envelope, so -/// anchoring on `S` — the same value settlement reads — keeps them out of the identity entirely. -/// Substituting (2) into (3) gives the receipt-level form, which is what a reader normally wants: -/// -/// ```text -/// receipt gas_used = max(C + N − M − R, F) -/// ``` -/// -/// # What it catches -/// -/// (2) fails whenever a transaction's envelope moves without a `MegaETH` site accounting for it — -/// a settlement that never ran, a result rewritten after settlement, an upstream subsidy nobody -/// records. (1) fails when the reported split disagrees with the per-site bookings, which is what -/// the block's admission counter reads. Pre-REX7 specs have neither a destroyed lane nor a -/// non-compute lane, so the identity is REX7-only by construction. -fn assert_terminal_identity(spec: MegaSpecId, outcome: &Outcome) { - if !spec.is_enabled(MegaSpecId::REX7) { - return; - } - assert_eq!( - outcome.compute_gas, - outcome.enforced_lane + outcome.destroyed, - "reported compute gas must split into enforced + destroyed; \ - compute={} enforced={} destroyed={} result={:?}", - outcome.compute_gas, - outcome.enforced_lane, - outcome.destroyed, - outcome.result, - ); - let accounted = i128::from(outcome.compute_gas) + outcome.non_compute_gas - - i128::from(outcome.minted_call_stipend) - - outcome.inspector_conjured_gas; - assert_eq!( - accounted, - i128::from(outcome.total_gas_spent), - "the tracker lanes must account for the whole receipt envelope; \ - compute={} non_compute={} minted_stipend={} conjured={} accounted={accounted} \ - envelope={} (receipt gas_used={}) result={:?}", - outcome.compute_gas, - outcome.non_compute_gas, - outcome.minted_call_stipend, - outcome.inspector_conjured_gas, - outcome.total_gas_spent, - outcome.gas_used, - outcome.result, - ); +/// Runs a single transaction that calls [`CONTRACT`] under `spec` with the given DB and runtime +/// limits, returning the execution result plus the post-tx tracker readings and `gas_used`. +pub(crate) fn transact( + spec: MegaSpecId, + db: MemoryDatabase, + limits: EvmTxRuntimeLimits, +) -> Outcome { + transact_with_gas_limit(spec, db, limits, DEFAULT_TX_GAS_LIMIT) +} + +/// [`transact`] with an explicit transaction gas limit, for cases that need EVM gas itself to run +/// out. +pub(crate) fn transact_with_gas_limit( + spec: MegaSpecId, + mut db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + gas_limit: u64, +) -> Outcome { + let mut evm = MegaEvm::new(context(&mut db, spec, limits)); + drive(spec, &mut evm, call_contract_tx(gas_limit)) +} + +/// Runs [`transact`] with the spec's default runtime limits. +pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome { + transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) } /// [`transact`] with an inspector attached, borrowed so the caller can read it back afterwards. @@ -276,27 +267,10 @@ pub(crate) fn transact_inspected( inspector: &mut I, ) -> Outcome where - I: for<'a> Inspector>, + I: for<'a> Inspector>, { - let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); - let tx = TxEnvBuilder::default() - .caller(CALLER) - .call(CONTRACT) - .gas_limit(DEFAULT_TX_GAS_LIMIT) - .build_fill(); - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); - let mut evm = MegaEvm::new(context).with_inspector(inspector); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(spec, outcome, detained_compute_gas_limit, terms) + let mut evm = MegaEvm::new(context(&mut db, spec, limits)).with_inspector(inspector); + drive(spec, &mut evm, call_contract_tx(DEFAULT_TX_GAS_LIMIT)) } /// What a transaction the shim refused reports: the error it surfaced and the refusals counted. @@ -321,24 +295,12 @@ pub(crate) fn transact_inspected_refused( inspector: &mut I, ) -> Refusal where - I: for<'a> Inspector>, + I: for<'a> Inspector>, { - let mut context = MegaContext::new(&mut db, spec).with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); - let tx = TxEnvBuilder::default() - .caller(CALLER) - .call(CONTRACT) - .gas_limit(DEFAULT_TX_GAS_LIMIT) - .build_fill(); - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); - let mut evm = MegaEvm::new(context).with_inspector(inspector); - let outcome = evm.execute_transaction(tx); + let mut evm = MegaEvm::new(context(&mut db, spec, limits)).with_inspector(inspector); + let outcome = evm.execute_transaction(call_contract_tx(DEFAULT_TX_GAS_LIMIT)); let rejected_rewrites = - evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites; + EvmTr::ctx_ref(&evm).additional_limit.borrow().inspector_ledger().rejected_rewrites; match outcome { Ok(outcome) => panic!( "the run was expected to be refused, but produced {:?}", @@ -348,11 +310,6 @@ where } } -/// Runs [`transact`] with the spec's default runtime limits. -pub(crate) fn transact_default(spec: MegaSpecId, db: MemoryDatabase) -> Outcome { - transact(spec, db, EvmTxRuntimeLimits::from_spec(spec)) -} - /// The external environment [`transact_tx`] runs with when a test does not need SALT buckets or /// oracle storage of its own. Equivalent to the empty environment the other helpers use: every /// bucket reports the minimum capacity and the oracle has no data. @@ -387,20 +344,130 @@ pub(crate) fn transact_mega_tx( tx: MegaTransaction, envs: &TestExternalEnvs, ) -> Outcome { - let mut context = MegaContext::new(&mut db, spec) - .with_external_envs(envs.into()) - .with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); + let context = zero_operator_fee( + MegaContext::new(&mut db, spec) + .with_external_envs(envs.into()) + .with_tx_runtime_limits(limits), + ); let mut evm = MegaEvm::new(context); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(spec, outcome, detained_compute_gas_limit, terms) + drive(spec, &mut evm, tx) +} + +/// [`transact`] with every SALT bucket reporting `bucket_capacity`. +/// +/// The SALT-scaled storage-gas charges (`SSTORE` set, new account, contract creation) are +/// `base × (capacity / MIN_BUCKET_SIZE − 1)`, so only a capacity above +/// [`mega_evm::MIN_BUCKET_SIZE`] makes them non-zero and exercises the paths that have to +/// exclude them from the compute-gas window. +pub(crate) fn transact_with_bucket_capacity( + spec: MegaSpecId, + db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + bucket_capacity: u64, +) -> Outcome { + let envs = TestExternalEnvs::default().with_default_bucket_capacity(bucket_capacity); + transact_tx( + spec, + db, + limits, + TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(DEFAULT_TX_GAS_LIMIT) + .build_fill(), + &envs, + ) +} + +/// The identity every REX7 transaction that produces a receipt must satisfy, connecting what the +/// trackers hold to the number the receipt reports. +/// +/// # The identity +/// +/// For one transaction, write +/// +/// ```text +/// C = compute_gas reported compute total +/// E = enforced_lane the part limits and block admission compare against +/// D = destroyed the part that is reported and accounted but never enforced +/// N = non_compute_gas MegaETH storage gas plus the sandbox boundary residue (signed) +/// M = minted_call_stipend CALL_STIPEND minted into child frames and never debited from a caller +/// I = inspector_conjured_gas gas an inspector wrote into the execution that nothing debited +/// S = total_gas_spent the receipt envelope, before the refund and the floor +/// R = the receipt's raw refund +/// F = the receipt's EIP-7623 floor gas +/// ``` +/// +/// then +/// +/// ```text +/// (1) C = E + D +/// (2) E + N + D − M − I = S +/// (3) I = the ledger's own net +/// (4) receipt gas_used = max(S − R, F) +/// ``` +/// +/// (1) is the split of the reported total. (2) is `ConservationTerms::envelope_for`, the law +/// solved for the envelope; substituting (1) gives the equivalent receipt-facing form +/// `C + N − M − I = S`. (3) pins the law's inspector term to the ledger it is read from, so a +/// lane the shim books but the law never sees cannot pass. (4) is how a receipt's gas number is +/// built from its envelope. +/// +/// `I` is zero for every transaction that runs without an inspector and for every +/// observation-only one, so (2) is the plain two-term identity on all but the handful of runs +/// that attach a rewriting inspector — which is exactly where it earns its keep. +/// +/// # Why (2) needs no refund or floor correction +/// +/// The EIP-3529 refund and the EIP-7623 floor both move the number the receipt reports without +/// anyone having burnt the difference. Both are applied strictly after the envelope is final, and +/// both are carried on the result as their own fields rather than folded into the envelope, so +/// anchoring on `S` — the same value settlement reads — keeps them out of the identity entirely. +/// Substituting (2) into (4) gives the receipt-level form, which is what a reader normally wants: +/// +/// ```text +/// receipt gas_used = max(C + N − M − R, F) +/// ``` +/// +/// # What it catches +/// +/// (2) fails whenever a transaction's envelope moves without a `MegaETH` site accounting for it — +/// a settlement that never ran, a result rewritten after settlement, an upstream subsidy nobody +/// records. (1) fails when the reported split disagrees with the per-site bookings, which is what +/// the block's admission counter reads. Pre-REX7 specs have neither a destroyed lane nor a +/// non-compute lane, so (1) and (2) are REX7-only by construction; (3) is not, because the shim +/// is not spec-gated. +fn assert_terminal_identity(spec: MegaSpecId, outcome: &Outcome) { + assert_eq!( + outcome.terms.inspector_conjured_gas, + outcome.inspector_ledger.conjured_gas(), + "the law's `I` term is the ledger's net, and nothing else", + ); + if !spec.is_enabled(MegaSpecId::REX7) { + return; + } + assert_eq!( + outcome.compute_gas, + outcome.enforced_lane + outcome.destroyed, + "reported compute gas must split into enforced + destroyed; \ + compute={} enforced={} destroyed={} result={:?}", + outcome.compute_gas, + outcome.enforced_lane, + outcome.destroyed, + outcome.result, + ); + assert_eq!( + outcome.terms.envelope_for(outcome.destroyed), + i128::from(outcome.total_gas_spent), + "the tracker lanes must account for the whole receipt envelope; \ + reported compute={} destroyed={} envelope={} (receipt gas_used={}) result={:?} ({})", + outcome.compute_gas, + outcome.destroyed, + outcome.total_gas_spent, + outcome.gas_used, + outcome.result, + outcome.terms, + ); } /// The part of an account a transaction's state actually asserts. @@ -502,36 +569,3 @@ pub(crate) fn assert_outcomes_identical(label: &str, r6: &Outcome, r7: &Outcome) ); } } - -/// [`transact`] with every SALT bucket reporting `bucket_capacity`. -/// -/// The SALT-scaled storage-gas charges (`SSTORE` set, new account, contract creation) are -/// `base × (capacity / MIN_BUCKET_SIZE − 1)`, so only a capacity above -/// [`mega_evm::MIN_BUCKET_SIZE`] makes them non-zero and exercises the paths that have to -/// exclude them from the compute-gas window. -pub(crate) fn transact_with_bucket_capacity( - spec: MegaSpecId, - mut db: MemoryDatabase, - limits: EvmTxRuntimeLimits, - bucket_capacity: u64, -) -> Outcome { - let envs = TestExternalEnvs::default().with_default_bucket_capacity(bucket_capacity); - let mut context = MegaContext::new(&mut db, spec) - .with_external_envs(envs.into()) - .with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); - let tx = - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(100_000_000).build_fill(); - let mut tx = MegaTransaction::new(tx); - tx.enveloped_tx = Some(Bytes::new()); - let mut evm = MegaEvm::new(context); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(spec, outcome, detained_compute_gas_limit, terms) -} diff --git a/crates/mega-evm/tests/rex7/conservation_terms.rs b/crates/mega-evm/tests/rex7/conservation_terms.rs index 4dd55e4b..66e1e99f 100644 --- a/crates/mega-evm/tests/rex7/conservation_terms.rs +++ b/crates/mega-evm/tests/rex7/conservation_terms.rs @@ -125,8 +125,12 @@ fn test_minted_stipend_and_destroyed_envelope_in_one_transaction() { ); // Both terms are live, and each is live only where it should be. - assert_eq!(alone.minted_call_stipend, 0, "no value transfer, no mint"); - assert_eq!(together.minted_call_stipend, CALL_STIPEND, "one value transfer mints one stipend"); + assert_eq!(alone.minted_call_stipend(), 0, "no value transfer, no mint"); + assert_eq!( + together.minted_call_stipend(), + CALL_STIPEND, + "one value transfer mints one stipend" + ); assert!(together.gas_used > alone.gas_used, "the value transfer must really have happened"); assert_eq!( @@ -138,7 +142,8 @@ fn test_minted_stipend_and_destroyed_envelope_in_one_transaction() { "a minted stipend must not move the destroyed remainder in either direction", ); assert_eq!( - together.destroyed, together.booked_destroyed, + together.destroyed, + together.booked_destroyed(), "the derived remainder and the per-site bookings must agree", ); } @@ -171,7 +176,7 @@ fn test_several_minted_stipends_in_one_transaction() { assert!(outcome.is_success(), "{transfers} transfers: {:?}", outcome.result); assert_eq!( - outcome.minted_call_stipend, + outcome.minted_call_stipend(), CALL_STIPEND * transfers as u64, "{transfers} transfers: every value-transferring call mints its own stipend", ); @@ -180,7 +185,8 @@ fn test_several_minted_stipends_in_one_transaction() { "{transfers} transfers: the destroyed remainder must not drift with the mint count", ); assert_eq!( - outcome.destroyed, outcome.booked_destroyed, + outcome.destroyed, + outcome.booked_destroyed(), "{transfers} transfers: the derived remainder and the per-site bookings must agree", ); } @@ -235,7 +241,8 @@ fn test_stipend_is_minted_by_a_value_call_whose_child_frame_never_runs() { ); assert_eq!( - alone.minted_call_stipend, CALL_STIPEND, + alone.minted_call_stipend(), + CALL_STIPEND, "the mint is created by the CALL opcode, not by the child frame", ); assert_eq!( @@ -244,7 +251,8 @@ fn test_stipend_is_minted_by_a_value_call_whose_child_frame_never_runs() { ); assert_eq!( - with_destroyed_envelope.minted_call_stipend, CALL_STIPEND, + with_destroyed_envelope.minted_call_stipend(), + CALL_STIPEND, "the failed value call still mints, with a destroying sibling alongside it", ); assert_eq!( @@ -254,7 +262,8 @@ fn test_stipend_is_minted_by_a_value_call_whose_child_frame_never_runs() { EXPECTED_DESTROYED - CALL_STIPEND, ); assert_eq!( - with_destroyed_envelope.destroyed, with_destroyed_envelope.booked_destroyed, + with_destroyed_envelope.destroyed, + with_destroyed_envelope.booked_destroyed(), "the derived remainder and the per-site bookings must agree", ); } @@ -359,15 +368,15 @@ fn test_sandbox_refund_drives_the_non_compute_lane_negative() { assert!(control.is_success(), "the control deployment must run: {:?}", control.result); assert!(refunding.is_success(), "the refunding deployment must run: {:?}", refunding.result); assert!( - control.non_compute_gas > 0, + control.non_compute_gas() > 0, "the control must leave the lane positive, or it proves nothing; got {}", - control.non_compute_gas, + control.non_compute_gas(), ); assert!( - refunding.non_compute_gas < 0, + refunding.non_compute_gas() < 0, "the sandbox's refund must drive the lane negative; got {}", - refunding.non_compute_gas, + refunding.non_compute_gas(), ); // The signature of a negative lane: the transaction records more compute work than its // envelope ever paid for. @@ -378,7 +387,8 @@ fn test_sandbox_refund_drives_the_non_compute_lane_negative() { refunding.gas_used, ); assert_eq!( - refunding.destroyed, refunding.booked_destroyed, + refunding.destroyed, + refunding.booked_destroyed(), "the derivation must stay exact across the sign change", ); } @@ -405,12 +415,13 @@ fn test_negative_non_compute_lane_composes_with_a_destroyed_envelope() { cross the sandbox boundary as destroyed", ); assert!( - outcome.non_compute_gas < 0, + outcome.non_compute_gas() < 0, "the refunds must still drive the lane negative; got {}", - outcome.non_compute_gas, + outcome.non_compute_gas(), ); assert_eq!( - outcome.destroyed, outcome.booked_destroyed, + outcome.destroyed, + outcome.booked_destroyed(), "the derivation must stay exact with both terms live", ); } diff --git a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs index 1a50b78c..f84eb13d 100644 --- a/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs +++ b/crates/mega-evm/tests/rex7/create_code_deposit_charge.rs @@ -26,7 +26,9 @@ //! any input. What survives here is the pair that is still decidable: installing the built-in //! rate explicitly changes nothing, and installing anything else is turned away. -use crate::common::{default_envs, finish, transact_tx, Outcome, CALLER, ONE_ETH}; +use crate::common::{ + default_envs, drive, transact_tx, zero_operator_fee, Outcome, CALLER, ONE_ETH, +}; use alloy_primitives::{Address, Bytes, TxKind, U256}; use alloy_sol_types::SolError as _; use mega_evm::{ @@ -38,7 +40,6 @@ use revm::{ bytecode::opcode::{MSTORE, POP, RETURN, TIMESTAMP}, context::{result::ExecutionResult, tx::TxEnvBuilder, CfgEnv}, context_interface::cfg::GasId, - handler::EvmTr, state::EvmState, }; @@ -185,7 +186,7 @@ fn test_create_frame_local_code_deposit_exceed_records_nothing() { // Nothing was destroyed and nothing was latched: with the charge not made, the transaction is // within its limits and the reverted frame is an ordinary revert. assert_eq!(rex7.destroyed, 0, "a reverted frame keeps its gas; nothing is destroyed"); - assert_eq!(rex7.booked_destroyed, 0, "no site may book a destroyed remainder here"); + assert_eq!(rex7.booked_destroyed(), 0, "no site may book a destroyed remainder here"); assert_eq!( rex7.enforced(), rex7.compute_gas, @@ -448,13 +449,11 @@ fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { let mut db = MemoryDatabase::default().account_balance(CALLER, U256::from(10 * ONE_ETH)); let mut cfg = CfgEnv::new_with_spec(MegaSpecId::REX7); cfg.gas_params.override_gas([(GasId::code_deposit_cost(), rate)]); - let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) - .with_cfg(cfg) - .with_tx_runtime_limits(EvmTxRuntimeLimits::no_limits()); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); + let context = zero_operator_fee( + MegaContext::new(&mut db, MegaSpecId::REX7) + .with_cfg(cfg) + .with_tx_runtime_limits(EvmTxRuntimeLimits::no_limits()), + ); let tx = TxEnvBuilder::default() .caller(CALLER) .kind(TxKind::Create) @@ -465,12 +464,7 @@ fn create_at_rate(rate: u64, gas_limit: u64) -> Outcome { let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); let mut evm = MegaEvm::new(context); - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, terms) = { - let additional_limit = EvmTr::ctx_ref(&evm).additional_limit.borrow(); - (additional_limit.detained_compute_gas_limit(), additional_limit.conservation_terms()) - }; - finish(MegaSpecId::REX7, outcome, detained_compute_gas_limit, terms) + drive(MegaSpecId::REX7, &mut evm, tx) } /// Installing a rate explicitly is not itself a deviation: a schedule that names revm's built-in diff --git a/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs index a55eb570..b7d6c514 100644 --- a/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs +++ b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs @@ -170,11 +170,13 @@ fn test_underfunded_deposit_reject_settles_the_rewritten_envelope() { the whole envelope is compute", ); assert_eq!( - outcome.non_compute_gas, 0, + outcome.non_compute_gas(), + 0, "the reject returns before the MegaETH share of intrinsic gas is booked", ); assert_eq!( - outcome.booked_destroyed, outcome.destroyed, + outcome.booked_destroyed(), + outcome.destroyed, "the per-site booking and the derived total must agree", ); } @@ -200,7 +202,7 @@ fn test_deposit_runtime_halt_keeps_its_settlement() { "the frame's whole budget is destroyed", ); assert_eq!( - outcome.non_compute_gas, + outcome.non_compute_gas(), i128::from(TX_INTRINSIC_STORAGE_GAS), "the MegaETH share of intrinsic gas is booked as non-compute", ); @@ -264,7 +266,8 @@ fn test_deposit_resource_limit_halt_destroys_what_the_rescue_returned() { "the reported total grows by the same amount, so it covers the rewritten receipt", ); assert_eq!( - deposit.non_compute_gas, plain.non_compute_gas, + deposit.non_compute_gas(), + plain.non_compute_gas(), "the storage-gas lane is untouched by the rewrite", ); } @@ -342,8 +345,8 @@ fn test_exempt_deposit_reject_still_accounts_for_the_envelope() { "the rest of its rewritten envelope is destroyed, exemption or not", ); assert_eq!( - (exempt.compute_gas, exempt.enforced(), exempt.destroyed, exempt.non_compute_gas), - (user.compute_gas, user.enforced(), user.destroyed, user.non_compute_gas), + (exempt.compute_gas, exempt.enforced(), exempt.destroyed, exempt.non_compute_gas()), + (user.compute_gas, user.enforced(), user.destroyed, user.non_compute_gas()), "an exemption suppresses limit enforcement, not accounting", ); } diff --git a/crates/mega-evm/tests/rex7/double_exceed_corner.rs b/crates/mega-evm/tests/rex7/double_exceed_corner.rs index 73293188..9e2c1184 100644 --- a/crates/mega-evm/tests/rex7/double_exceed_corner.rs +++ b/crates/mega-evm/tests/rex7/double_exceed_corner.rs @@ -25,7 +25,7 @@ //! measured. use crate::common::{ - transact, transact_default, transact_with_gas_limit, Outcome, CALLER, CONTRACT, ONE_ETH, + base_db, plain_filler, transact, transact_default, transact_with_gas_limit, Outcome, }; use alloy_primitives::{Bytes, U256}; use mega_evm::{ @@ -41,22 +41,6 @@ const CROSSING_OFFSET: u64 = 0x2000; /// How far either side of the knife edge to sweep. const SWEEP: i64 = 3; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// The run leading up to the crossing MSTORE: an optional volatile access, a plain segment, and the /// MSTORE's two stack operands. Everything here is cheap and fully paid for in every sweep point. fn approach(volatile: bool) -> BytecodeBuilder { diff --git a/crates/mega-evm/tests/rex7/exceptional_halt.rs b/crates/mega-evm/tests/rex7/exceptional_halt.rs index db189e6c..ad1cbf75 100644 --- a/crates/mega-evm/tests/rex7/exceptional_halt.rs +++ b/crates/mega-evm/tests/rex7/exceptional_halt.rs @@ -22,25 +22,13 @@ //! `MemoryLimitOOG` is not in the sweep: it needs revm's `memory_limit` cfg, which this workspace //! does not enable, so no bytecode can reach it. -use crate::common::{ - transact, transact_with_gas_limit, Outcome, CALLEE, CALLER, CONTRACT, ONE_ETH, -}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaSpecId, -}; +use crate::common::{base_db, transact, transact_with_gas_limit, Outcome, CALLEE}; +use alloy_primitives::Bytes; +use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaSpecId}; use revm::bytecode::opcode::{ ADD, CALL, DUP1, JUMP, JUMPDEST, JUMPI, MSTORE, POP, STOP, SUB, SWAP1, }; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// The two transaction-intrinsic readings every case below calibrates against, measured from a /// transaction that runs a single `STOP`: the total EVM gas the receipt charges before the frame /// does anything (`gas`), and the part of it that counts as compute (`compute`). diff --git a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs index d5cdf95b..da9776a5 100644 --- a/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs +++ b/crates/mega-evm/tests/rex7/frame_init_reject_burn.rs @@ -366,7 +366,8 @@ fn test_top_level_create_collision_destroys_the_rest_of_the_envelope() { "the rest of the envelope is what the refused frame swallowed", ); assert_eq!( - r7.booked_destroyed, r7.destroyed, + r7.booked_destroyed(), + r7.destroyed, "the per-site booking and the conservation law must agree", ); @@ -428,7 +429,8 @@ fn test_inner_create2_collision_destroys_the_forwarded_budget() { "the colliding CREATE2's forwarded budget is swallowed and must be booked", ); assert_eq!( - r7.booked_destroyed, r7.destroyed, + r7.booked_destroyed(), + r7.destroyed, "the per-site booking and the conservation law must agree", ); assert_eq!( @@ -466,7 +468,7 @@ fn test_inner_create_out_of_funds_destroys_nothing() { assert!(r7.is_success(), "the caller absorbs the failed CREATE: {:?}", r7.result); assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); assert_eq!(r7.destroyed, 0, "an OutOfFunds create hands its budget back"); - assert_eq!(r7.booked_destroyed, 0, "and so books nothing"); + assert_eq!(r7.booked_destroyed(), 0, "and so books nothing"); assert_eq!( r7.compute_gas, r6.compute_gas, "with nothing destroyed the two specs report the same compute total", @@ -496,7 +498,7 @@ fn test_inner_create_nonce_overflow_destroys_nothing() { assert!(r7.is_success(), "the caller survives the refused CREATE: {:?}", r7.result); assert_eq!(r6.gas_used, r7.gas_used, "receipt gas_used must be unchanged"); assert_eq!(r7.destroyed, 0, "a nonce-overflow create hands its budget back"); - assert_eq!(r7.booked_destroyed, 0, "and so books nothing"); + assert_eq!(r7.booked_destroyed(), 0, "and so books nothing"); assert_eq!( r7.compute_gas, r6.compute_gas, "with nothing destroyed the two specs report the same compute total", @@ -537,7 +539,8 @@ fn test_precompile_halt_stays_booked_once_end_to_end() { 2 * forwarded, ); assert_eq!( - r7.booked_destroyed, r7.destroyed, + r7.booked_destroyed(), + r7.destroyed, "the per-site booking and the conservation law must agree", ); } @@ -598,7 +601,8 @@ fn test_failed_deposit_whose_create_collides_books_the_envelope_once() { "the rewritten envelope, less what was performed, is destroyed exactly once", ); assert_eq!( - r7.booked_destroyed, r7.destroyed, + r7.booked_destroyed(), + r7.destroyed, "the per-site bookings and the conservation law must agree after the rewrite too", ); } @@ -671,7 +675,8 @@ fn test_keyless_sandbox_create_collision_crosses_the_merge_boundary() { that across", ); assert_eq!( - r7.booked_destroyed, r7.destroyed, + r7.booked_destroyed(), + r7.destroyed, "the per-site booking and the conservation law must agree across the merge", ); assert_eq!( diff --git a/crates/mega-evm/tests/rex7/gas_clamp.rs b/crates/mega-evm/tests/rex7/gas_clamp.rs index 8b9eb68b..3b795438 100644 --- a/crates/mega-evm/tests/rex7/gas_clamp.rs +++ b/crates/mega-evm/tests/rex7/gas_clamp.rs @@ -21,13 +21,11 @@ //! `VolatileDataAccessOutOfGas`. use crate::common::{ - transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, CONTRACT, ONE_ETH, + base_db, plain_filler, transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, + CONTRACT, }; use alloy_primitives::{Address, Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId, -}; +use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId}; use revm::bytecode::opcode::{ CALL, DUP1, EXTCODECOPY, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, TIMESTAMP, @@ -38,13 +36,6 @@ const CALL_RESULT_SLOT: u64 = 0x10; /// Slot a callee writes to, so a reverted sub-frame can be told from a committed one. const CALLEE_SLOT: u64 = 0x11; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(crate::common::CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body: /// /// ```text @@ -69,15 +60,6 @@ fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { Bytes::from(code) } -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// Per-spec runtime limits with the TX compute gas limit replaced. fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) diff --git a/crates/mega-evm/tests/rex7/gas_leakage.rs b/crates/mega-evm/tests/rex7/gas_leakage.rs index 6a46d902..a68c3eb7 100644 --- a/crates/mega-evm/tests/rex7/gas_leakage.rs +++ b/crates/mega-evm/tests/rex7/gas_leakage.rs @@ -21,8 +21,8 @@ //! all. use crate::common::{ - assert_outcomes_identical, transact, transact_with_gas_limit, Outcome, CALLEE, CALLER, - CONTRACT, ONE_ETH, + assert_outcomes_identical, base_db as common_base_db, plain_filler, transact, + transact_with_gas_limit, Outcome, CALLEE, CONTRACT, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::SolCall as _; @@ -42,20 +42,7 @@ const GAS_READING_SLOT: u64 = 0x40; const CALLEE_SLOT: u64 = 0x41; fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder + common_base_db(code).account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) } /// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body. diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index 6e84f513..13a5e658 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -14,15 +14,14 @@ //! remaining, which is what the interceptor reads once the frames have been popped. use crate::common::{ - finish, transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, + base_db, context, drive, transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::{SolCall as _, SolError}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, IMegaLimitControl, LimitKind, MegaContext, MegaEvm, MegaLimitExceeded, - MegaSpecId, MegaTransaction, MegaTransactionNew as _, VolatileDataAccess, - LIMIT_CONTROL_ADDRESS, + EvmTxRuntimeLimits, IMegaLimitControl, LimitKind, MegaEvm, MegaLimitExceeded, MegaSpecId, + MegaTransaction, MegaTransactionNew as _, VolatileDataAccess, LIMIT_CONTROL_ADDRESS, }; use revm::{ bytecode::opcode::{CALL, MLOAD, POP, SELFBALANCE, SSTORE, STATICCALL, STOP, TIMESTAMP}, @@ -43,13 +42,6 @@ fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) } -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// Codex / knife-edge program: `TIMESTAMP; POP; STOP`. fn timestamp_pop_stop() -> Bytes { BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build() @@ -86,11 +78,7 @@ fn run(code: Bytes, limits: EvmTxRuntimeLimits) -> GuardPassRun { } fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { - let mut context = MegaContext::new(&mut db, MegaSpecId::REX7).with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::from(0)); - chain.operator_fee_constant = Some(U256::from(0)); - }); + let mut evm = MegaEvm::new(context(&mut db, MegaSpecId::REX7, limits)); let tx = TxEnvBuilder::default() .caller(CALLER) .call(CONTRACT) @@ -98,18 +86,10 @@ fn run_db(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> GuardPassRun { .build_fill(); let mut tx = MegaTransaction::new(tx); tx.enveloped_tx = Some(Bytes::new()); - let mut evm = MegaEvm::new(context); - let executed = evm.execute_transaction(tx).expect("tx should not surface EVMError"); - let (detained_compute_gas_limit, remaining_compute_gas, terms) = { - let additional_limit = evm.ctx_ref().additional_limit.borrow(); - ( - additional_limit.detained_compute_gas_limit(), - additional_limit.current_call_remaining_compute_gas(), - additional_limit.conservation_terms(), - ) - }; + let outcome = drive(MegaSpecId::REX7, &mut evm, tx); + let remaining_compute_gas = + evm.ctx_ref().additional_limit.borrow().current_call_remaining_compute_gas(); let accessed = evm.ctx_ref().volatile_data_tracker.borrow().get_volatile_data_accessed(); - let outcome = finish(MegaSpecId::REX7, executed, detained_compute_gas_limit, terms); GuardPassRun { outcome, accessed, remaining_compute_gas } } diff --git a/crates/mega-evm/tests/rex7/interceptor_resume.rs b/crates/mega-evm/tests/rex7/interceptor_resume.rs index 0eafcea8..627e4b85 100644 --- a/crates/mega-evm/tests/rex7/interceptor_resume.rs +++ b/crates/mega-evm/tests/rex7/interceptor_resume.rs @@ -23,8 +23,8 @@ //! is still stopped at the clamp boundary rather than overshooting to the next checkpoint. use crate::common::{ - assert_outcomes_identical, transact, transact_default, Outcome, CALLEE, CALLER, CONTRACT, - ONE_ETH, + assert_outcomes_identical, base_db as common_base_db, plain_filler, transact, transact_default, + Outcome, CALLEE, CONTRACT, }; use alloy_primitives::{address, Address, Bytes, U256}; use alloy_sol_types::SolCall as _; @@ -50,20 +50,7 @@ const RET_OFFSET: u64 = 0x40; const FORWARDED_GAS: u64 = 1_000_000; fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder + common_base_db(code).account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) } /// Per-spec runtime limits with the TX compute gas limit replaced. diff --git a/crates/mega-evm/tests/rex7/latch_surfacing.rs b/crates/mega-evm/tests/rex7/latch_surfacing.rs index 6fc8bad7..5a26fec7 100644 --- a/crates/mega-evm/tests/rex7/latch_surfacing.rs +++ b/crates/mega-evm/tests/rex7/latch_surfacing.rs @@ -21,15 +21,15 @@ //! caller's segment. use crate::common::{ - assert_outcomes_identical, transact, transact_default, transact_tx, Outcome, CALLER, CONTRACT, - DEFAULT_TX_GAS_LIMIT, ONE_ETH, + assert_outcomes_identical, base_db, plain_filler, transact, transact_default, transact_tx, + Outcome, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, }; use alloy_primitives::{Bytes, B256, U256}; use alloy_sol_types::{SolCall as _, SolError as _}; use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, IOracle, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, - TestExternalEnvs, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2, + test_utils::BytecodeBuilder, EvmTxRuntimeLimits, IOracle, LimitKind, MegaHaltReason, + MegaLimitExceeded, MegaSpecId, TestExternalEnvs, ORACLE_CONTRACT_ADDRESS, + ORACLE_CONTRACT_CODE_REX2, }; use revm::{ bytecode::opcode::{CALL, LOG1, POP, STOP}, @@ -42,22 +42,6 @@ const DOWNSTREAM_SLOT: u64 = 0x31; /// Slot written by the latching SSTORE itself. const LATCHING_SLOT: u64 = 0x30; -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// The plain segment placed between the latching site and the checkpoint downstream of it. Long /// enough that including it in the recorded compute gas would be unmistakable. const GAP_PAIRS: usize = 40; diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index dcf15876..a38dd4e2 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -622,7 +622,8 @@ fn test_cancelling_counter_edits_are_booked() { "the two edits cancel, so the envelope the receipt reports is unmoved", ); assert_eq!( - cheated.inspector_conjured_gas, 0, + cheated.inspector_conjured_gas(), + 0, "and so is the law's term: this is exactly the shape a net-only reading cannot see", ); assert!( diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 51139f53..8ac8b03b 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -16,7 +16,7 @@ //! - an observation-only inspector changes nothing at all; //! - and removing gas is measured with the same machinery as adding it. -use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use crate::common::{base_db, CALLEE, CALLER, CONTRACT}; use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, @@ -189,13 +189,6 @@ fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { } } -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// A countdown loop of plain opcodes with no checkpoint anywhere in the body, so the whole run is /// one settlement segment and the gas clamp is the only thing enforcing the compute limit inside /// it. diff --git a/crates/mega-evm/tests/rex7/parity_shapes.rs b/crates/mega-evm/tests/rex7/parity_shapes.rs index 73a3b146..373032ba 100644 --- a/crates/mega-evm/tests/rex7/parity_shapes.rs +++ b/crates/mega-evm/tests/rex7/parity_shapes.rs @@ -23,8 +23,8 @@ use std::vec::Vec; use crate::common::{ - assert_outcomes_identical, default_envs, transact_tx, Outcome, CALLEE, CALLER, CONTRACT, - DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, + assert_outcomes_identical, base_db, default_envs, plain_filler, transact_tx, Outcome, CALLEE, + CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, }; use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization}; use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; @@ -53,22 +53,6 @@ const NEW_AUTHORITY: Address = address!("000000000000000000000000000000000033000 const KEYLESS_RELAYER: Address = address!("0000000000000000000000000000000000330004"); -fn base_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - -/// `pairs` PUSH1/POP pairs — plain opcodes that record nothing of their own. -fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBuilder { - let mut builder = builder; - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder -} - /// A mixed body: plain opcodes around one of every checkpoint family that does not need operands /// from the caller — a storage read, a storage write, a log, and a volatile opcode. fn mixed_checkpoint_body() -> Bytes { From b68c38a5a6c19872cc8f2776da91dcee210fe66f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:47:24 +0800 Subject: [PATCH 182/208] test(rex7): give the inspected fixtures one home `inspector_common.rs` holds what a rewriting-inspector test needs on top of the shared driver: the REX7 limits, the bytecode shapes an inspector reaches into (a call it can widen, a creation it can revive, a checkpoint-free loop it can inject into), the one-lane ledgers a test asserts against, and the two ways a refused rewrite surfaces. `measured_inspector.rs` is the first to use it: 856 -> 584 lines. Assertions 60 -> 53 (0 removed, 7 deduped): its `Reading`, `read` and `assert_identity` are the shared `Outcome`, `drive` and terminal identity, its halt-reason panic is `Outcome::halt_reason`, and the two halves of its refusal check are `assert_refused`. --- crates/mega-evm/tests/rex7/common.rs | 2 +- .../mega-evm/tests/rex7/inspector_common.rs | 195 +++++++++ crates/mega-evm/tests/rex7/main.rs | 1 + .../mega-evm/tests/rex7/measured_inspector.rs | 397 +++--------------- 4 files changed, 263 insertions(+), 332 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/inspector_common.rs diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 6d1c4c05..e0b86abd 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -150,7 +150,7 @@ impl Outcome { /// The transaction every helper here runs unless a test supplies its own: a plain call from /// [`CALLER`] into [`CONTRACT`]. -fn call_contract_tx(gas_limit: u64) -> MegaTransaction { +pub(crate) fn call_contract_tx(gas_limit: u64) -> MegaTransaction { let tx = TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(gas_limit).build_fill(); let mut tx = MegaTransaction::new(tx); diff --git a/crates/mega-evm/tests/rex7/inspector_common.rs b/crates/mega-evm/tests/rex7/inspector_common.rs new file mode 100644 index 00000000..d0a2a4ee --- /dev/null +++ b/crates/mega-evm/tests/rex7/inspector_common.rs @@ -0,0 +1,195 @@ +//! Shared fixtures for the tests that attach an inspector. +//! +//! [`crate::common`] drives the transaction and checks the conservation law on every run. What is +//! left over — the REX7 limits, the bytecode shapes an inspector needs something to reach into, the +//! one-lane ledgers a test asserts against, and the two ways a refused rewrite surfaces — lives +//! here, because a rewrite is only ever pinned by comparing an inspected run against the +//! uninspected one over the same fixture. + +use alloy_primitives::{Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaContext, MegaEvm, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, DUP1, JUMPDEST, JUMPI, MSTORE8, POP, STOP, SUB, SWAP1}, + Inspector, +}; +use std::{boxed::Box, string::String, vec::Vec}; + +use crate::common::{call_contract_tx, context, plain_filler, CALLEE, DEFAULT_TX_GAS_LIMIT}; + +/// The spec every fixture here runs under, and its default runtime limits. +pub(crate) fn limits() -> EvmTxRuntimeLimits { + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) +} + +/// [`limits`] with the per-transaction compute budget lowered to `limit`. +pub(crate) fn limits_with_compute(limit: u64) -> EvmTxRuntimeLimits { + limits().with_tx_compute_gas_limit(limit) +} + +// --- bytecode ------------------------------------------------------------------------------ + +/// A `CALL` to `target` forwarding `gas` and `value`, with empty argument and return ranges. +pub(crate) fn append_call( + builder: BytecodeBuilder, + target: Address, + gas: u64, + value: u64, +) -> BytecodeBuilder { + builder + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(value) + .push_address(target) + .push_number(gas) + .append(CALL) +} + +/// The minimal frame that makes one inner call and ignores what it returned. +pub(crate) fn call_then_stop(target: Address, gas: u64) -> Bytes { + append_call(BytecodeBuilder::default(), target, gas, 0).append(POP).append(STOP).build() +} + +/// A straight run of `pairs` plain opcodes that always succeeds. +pub(crate) fn plain_run_code(pairs: usize) -> Bytes { + plain_filler(BytecodeBuilder::default(), pairs).append(STOP).build() +} + +/// A countdown loop of plain opcodes with no checkpoint anywhere in the body, so the whole run is +/// one settlement segment and the gas clamp is the only thing enforcing the compute limit inside +/// it. +pub(crate) fn countdown_loop_code(iterations: u16) -> Bytes { + let mut code = Vec::new(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// Writes `init_code` into memory a byte at a time and `CREATE`s from it, so a test can choose the +/// constructor without a second account. +pub(crate) fn deploy_then_stop(init_code: &[u8]) -> Bytes { + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init_code.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + builder + .push_number(init_code.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append(STOP) + .build() +} + +/// [`crate::common::base_db`] with `callee` installed at [`CALLEE`]. +pub(crate) fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { + crate::common::base_db(code).account_code(CALLEE, callee) +} + +// --- ledgers ------------------------------------------------------------------------------- + +/// The ledger of a rewrite that moved gas on exactly one lane. +/// +/// Separate constructors rather than one, because which lane a shape moves is exactly what decides +/// whether the conservation law can see it: only the gas, envelope, result and reservoir lanes are +/// terms of it. +pub(crate) fn ledger_gas(gas: i128) -> InspectorLedger { + InspectorLedger { gas: Lane::once(gas), ..InspectorLedger::default() } +} + +pub(crate) fn ledger_env(env: i128) -> InspectorLedger { + InspectorLedger { env: Lane::once(env), ..InspectorLedger::default() } +} + +pub(crate) fn ledger_result(result: i128) -> InspectorLedger { + InspectorLedger { result: Lane::once(result), ..InspectorLedger::default() } +} + +pub(crate) fn ledger_refund(refund: i128) -> InspectorLedger { + InspectorLedger { refund: Lane::once(refund), ..InspectorLedger::default() } +} + +pub(crate) fn ledger_reservoir(reservoir: i128) -> InspectorLedger { + InspectorLedger { reservoir: Lane::once(reservoir), ..InspectorLedger::default() } +} + +pub(crate) fn ledger_state_gas(state_gas: i128) -> InspectorLedger { + InspectorLedger { state_gas: Lane::once(state_gas), ..InspectorLedger::default() } +} + +/// The ledger of a rewrite that moves no gas: the shim saw the argument it was handed come back +/// changed, and that is the whole of what it books. +/// +/// These are the cells that would otherwise be indistinguishable from an observation-only run, and +/// the reason the canonical block path could not tell them apart before this lane existed. +pub(crate) fn ledger_intervention() -> InspectorLedger { + InspectorLedger { interventions: 1, ..InspectorLedger::default() } +} + +// --- refusals ------------------------------------------------------------------------------ + +/// [`crate::common::transact_inspected`] surfacing the `EVMError` instead of panicking on it. +pub(crate) fn try_transact_inspected( + db: MemoryDatabase, + limits: EvmTxRuntimeLimits, + inspector: &mut I, +) -> Result<(), String> +where + I: for<'a> Inspector>, +{ + let mut db = db; + let mut evm = + MegaEvm::new(context(&mut db, MegaSpecId::REX7, limits)).with_inspector(inspector); + evm.execute_transaction(call_contract_tx(DEFAULT_TX_GAS_LIMIT)) + .map(|_| ()) + .map_err(|e| std::format!("{e:?}")) +} + +/// Drives `run` and asserts the shim refused the rewrite, however this build surfaces a refusal: +/// a debug build asserts (the shape is a detector, and a corpus that produces it should stop), a +/// release build fails the transaction with the same message. +pub(crate) fn assert_refused(message: &str, run: impl Fn() -> Result<(), String>) { + if cfg!(debug_assertions) { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)); + std::panic::set_hook(previous); + let payload = panicked.expect_err("the detector must fire in debug builds"); + let caught = payload + .downcast_ref::() + .map(String::as_str) + .or_else(|| payload.downcast_ref::<&str>().copied()) + .unwrap_or_default(); + assert!(caught.contains(message), "the assertion must name the shape; got {caught:?}"); + } else { + let error = run().expect_err("the refusal must surface as an EVMError in release builds"); + assert!(error.contains(message), "the error must name the shape; got {error:?}"); + } +} + +/// The message the shim refuses a resurrected creation with, on both of the paths that can catch +/// it. +pub(crate) const REVIVED_CREATION: &str = + "inspector rewrote a failed contract creation into a successful one"; + +/// Init code that reverts immediately, so the creation it is handed to fails. +pub(crate) const REVERTING_INIT_CODE: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; + +/// A slot as the produced state has it, taking the slot as the small integer the fixtures use. +pub(crate) fn slot_of(outcome: &crate::common::Outcome, address: Address, slot: u64) -> U256 { + outcome.storage_value(address, U256::from(slot)) +} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index a114c2a6..f683f54a 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -116,6 +116,7 @@ mod gas_leakage; mod gas_surface; mod guard_pass_static_gas; mod inspector_cheat_matrix; +mod inspector_common; mod inspector_settlement_window; mod interception_gas; mod interceptor_resume; diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs index 8ac8b03b..58aa4533 100644 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ b/crates/mega-evm/tests/rex7/measured_inspector.rs @@ -16,215 +16,25 @@ //! - an observation-only inspector changes nothing at all; //! - and removing gas is measured with the same machinery as adding it. -use crate::common::{base_db, CALLEE, CALLER, CONTRACT}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, - MegaTransactionNew as _, MegaTransactionOutcome, +use crate::{ + common::{base_db, transact, transact_inspected, CALLEE, CONTRACT}, + inspector_common::{ + assert_refused, call_then_stop, countdown_loop_code, db_with_callee, deploy_then_stop, + limits, limits_with_compute, plain_run_code, try_transact_inspected, REVERTING_INIT_CODE, + REVIVED_CREATION, + }, }; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{test_utils::BytecodeBuilder, InspectorLedger, Lane, MegaHaltReason, MegaSpecId}; use revm::{ - bytecode::opcode::{ - CALL, CREATE, DUP1, JUMPDEST, JUMPI, MSTORE, POP, RETURN, STOP, SUB, SWAP1, - }, - context::{result::ExecutionResult, tx::TxEnvBuilder}, - handler::EvmTr, + bytecode::opcode::{CALL, MSTORE, POP, RETURN, STOP}, interpreter::{ CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, InterpreterResult, InterpreterTypes, }, - state::EvmState, Inspector, }; -/// Transaction gas limit used throughout: high enough that EVM gas is never what binds. -const TX_GAS_LIMIT: u64 = 100_000_000; - -/// Everything one transaction reports, plus what the shim booked for it. -struct Reading { - result: ExecutionResult, - compute_gas: u64, - enforced: u64, - destroyed: u64, - data_size: u64, - kv_updates: u64, - state_growth: u64, - gas_used: u64, - total_gas_spent: u64, - terms: ConservationTerms, - ledger: InspectorLedger, - state: EvmState, -} - -impl Reading { - fn halt_reason(&self) -> &MegaHaltReason { - match &self.result { - ExecutionResult::Halt { reason, .. } => reason, - other => panic!("expected a halt, got {other:?}"), - } - } -} - -/// The conservation identity, stated with the term the measurement shim contributes. -/// -/// Uninspected, this is the identity `common::assert_terminal_identity` checks: the reported -/// compute total plus `MegaETH` storage gas, less the `CALL_STIPEND` the EVM minted into child -/// frames, is exactly the envelope the receipt reports. An inspector that conjures gas — by writing -/// into an interpreter's counter or into a frame's gas limit — makes the transaction spend less -/// than its frames recorded, by exactly what it conjured, so the identity only closes once the -/// ledger's term is taken out of the accounted side. -/// -/// This is what goes red when a lane the shim is supposed to book goes unbooked: the two sides -/// disagree by precisely the unbooked amount. -fn assert_identity(label: &str, r: &Reading) { - assert_eq!( - r.compute_gas, - r.enforced + r.destroyed, - "{label}: reported compute must split into enforced + destroyed", - ); - assert_eq!( - r.terms.inspector_conjured_gas, - r.ledger.conjured_gas(), - "{label}: the law's `I` term is the ledger's net, and nothing else", - ); - assert_eq!( - r.terms.envelope_for(r.destroyed), - i128::from(r.total_gas_spent), - "{label}: the law must close against the envelope the receipt reports; \ - reported compute={} destroyed={} envelope={} ({})", - r.compute_gas, - r.destroyed, - r.total_gas_spent, - r.terms, - ); -} - -fn tx() -> MegaTransaction { - let mut tx = MegaTransaction::new( - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), - ); - tx.enveloped_tx = Some(Bytes::new()); - tx -} - -/// The context every run in this module uses: REX7, no external environment, no operator fee. -fn context( - db: &mut MemoryDatabase, - limits: EvmTxRuntimeLimits, -) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = MegaContext::new(db, MegaSpecId::REX7).with_tx_runtime_limits(limits); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context -} - -/// Runs the transaction with no inspector at all — the reference every inspected run is compared -/// against. -fn transact_plain(mut db: MemoryDatabase, limits: EvmTxRuntimeLimits) -> Reading { - let mut evm = MegaEvm::new(context(&mut db, limits)); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading -} - -/// Runs the transaction with `inspector` attached, borrowed so the caller can read it back -/// afterwards. -fn transact_inspected( - mut db: MemoryDatabase, - limits: EvmTxRuntimeLimits, - inspector: &mut I, -) -> Reading -where - I: for<'a> Inspector>, -{ - let mut evm = MegaEvm::new(context(&mut db, limits)).with_inspector(inspector); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading -} - -/// Like [`transact_inspected`], but surfaces the `EVMError` instead of panicking on it. -fn try_transact_inspected( - mut db: MemoryDatabase, - limits: EvmTxRuntimeLimits, - inspector: &mut I, -) -> Result<(), String> -where - I: for<'a> Inspector>, -{ - let mut evm = MegaEvm::new(context(&mut db, limits)).with_inspector(inspector); - evm.execute_transaction(tx()).map(|_| ()).map_err(|e| format!("{e:?}")) -} - -/// Reads one transaction's outcome, and pins the outcome's own ledger field against the tracker's -/// on every shape this module runs. -/// -/// The outcome is what a consumer sees; the tracker is where the shim booked. Checking them here -/// means every test below asserts the outcome API carries the measurement, not just the two that -/// look at it on purpose. -fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { - assert_eq!( - outcome.inspector_ledger, - limit.inspector_ledger(), - "the outcome must report the ledger the shim booked, unchanged", - ); - let gas_used = outcome.result_and_state.result.tx_gas_used(); - let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); - Reading { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - enforced: outcome.compute_gas_enforced, - destroyed: outcome.compute_gas_destroyed, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - total_gas_spent, - terms: limit.conservation_terms(), - ledger: outcome.inspector_ledger, - state: outcome.result_and_state.state, - } -} - -/// A countdown loop of plain opcodes with no checkpoint anywhere in the body, so the whole run is -/// one settlement segment and the gas clamp is the only thing enforcing the compute limit inside -/// it. -fn countdown_loop_code(iterations: u16) -> Bytes { - let mut code = Vec::new(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); - code.push(JUMPDEST); - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(SWAP1); - code.push(SUB); - code.push(DUP1); - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(JUMPI); - code.push(STOP); - Bytes::from(code) -} - -/// A straight run of plain opcodes that always succeeds. -fn plain_run_code(pairs: usize) -> Bytes { - let mut builder = BytecodeBuilder::default(); - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder.append(STOP).build() -} - -fn limits_with_compute(limit: u64) -> EvmTxRuntimeLimits { - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) -} - -fn default_limits() -> EvmTxRuntimeLimits { - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) -} - /// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. /// /// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact @@ -363,44 +173,47 @@ fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { const INJECTED: u64 = 20_000; let code = countdown_loop_code(10_000); // Far below what the loop needs, so the clamp binds for the whole run. - let intrinsic = transact_plain(base_db(plain_run_code(0)), default_limits()).compute_gas; + let intrinsic = transact(MegaSpecId::REX7, base_db(plain_run_code(0)), limits()).compute_gas; let limits = limits_with_compute(intrinsic + 5_000); - let plain = transact_plain(base_db(code.clone()), limits); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); let mut inspector = GasEditor::new(20, INJECTED as i64); - let inspected = transact_inspected(base_db(code), limits, &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); assert!(inspector.applied, "the fixture must reach the injection point"); assert!( - matches!(plain.halt_reason(), MegaHaltReason::ComputeGasLimitExceeded { .. }), + matches!(plain.halt_reason("plain"), MegaHaltReason::ComputeGasLimitExceeded { .. }), "fixture check: the uninspected run must stop on the compute limit, got {:?}", - plain.halt_reason(), + plain.halt_reason("plain"), ); assert_eq!( - inspected.enforced, plain.enforced, + inspected.enforced(), + plain.enforced(), "the injection must be neither counted as work nor deducted from it, and the re-derived \ clamp must stop the loop at the same opcode the uninspected run stopped at; \ inspected result {:?}", inspected.result, ); assert!( - matches!(inspected.halt_reason(), MegaHaltReason::ComputeGasLimitExceeded { .. }), + matches!( + inspected.halt_reason("inspected"), + MegaHaltReason::ComputeGasLimitExceeded { .. } + ), "injected gas must not turn a compute-limit halt into something else, got {:?}", - inspected.halt_reason(), + inspected.halt_reason("inspected"), ); assert_eq!( - inspected.ledger.gas, + inspected.inspector_ledger.gas, Lane::once(i128::from(INJECTED)), "the ledger must hold exactly what was injected", ); - assert_eq!(inspected.ledger.env, Lane::default(), "no frame envelope was touched"); + assert_eq!(inspected.inspector_ledger.env, Lane::default(), "no frame envelope was touched"); assert_eq!( i128::from(inspected.total_gas_spent) + i128::from(INJECTED), i128::from(plain.total_gas_spent), "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ spends exactly that much less than the uninspected run", ); - assert_identity("injected", &inspected); } /// (v) The same machinery, in the other direction: gas removed from a running interpreter is @@ -415,20 +228,21 @@ fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { const REMOVED: u64 = 1_000; let code = plain_run_code(200); - let plain = transact_plain(base_db(code.clone()), default_limits()); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); let mut inspector = GasEditor::new(20, -(REMOVED as i64)); - let inspected = transact_inspected(base_db(code), default_limits(), &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); assert!(inspector.applied, "the fixture must reach the removal point"); assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); assert_eq!( - inspected.ledger.gas, + inspected.inspector_ledger.gas, Lane::once(-i128::from(REMOVED)), "the ledger must hold the removal as a negative entry", ); assert_eq!( - inspected.enforced, plain.enforced, + inspected.enforced(), + plain.enforced(), "gas the inspector destroyed is not work the EVM performed", ); assert_eq!( @@ -436,7 +250,6 @@ fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { plain.total_gas_spent + REMOVED, "the removed gas never comes back, so the envelope is exactly that much larger", ); - assert_identity("removed", &inspected); } /// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the @@ -467,32 +280,36 @@ fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { .push_number(0u64) .append(RETURN) .build(); - let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let build_db = || db_with_callee(code.clone(), callee.clone()); - let plain = transact_plain(build_db(), default_limits()); + let plain = transact(MegaSpecId::REX7, build_db(), limits()); let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; - let inspected = transact_inspected(build_db(), default_limits(), &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); assert!(inspected.result.is_success(), "the inner call must still succeed"); assert_eq!( - inspected.ledger.env, + inspected.inspector_ledger.env, Lane::once(i128::from(BONUS)), "the ledger must hold exactly the gas the inspector added to the child's envelope", ); - assert_eq!(inspected.ledger.gas, Lane::default(), "no interpreter counter was touched"); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::default(), + "no interpreter counter was touched" + ); assert_eq!( inspected.total_gas_spent + BONUS, plain.total_gas_spent, "the child returns the conjured gas to its caller, so the transaction spends that much less", ); assert_eq!( - inspected.enforced, plain.enforced, + inspected.enforced(), + plain.enforced(), "a wider envelope is not more work: the child's compute budget comes from the compute \ tracker, not from its gas limit", ); - assert_identity("raised child gas limit", &inspected); } /// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. @@ -534,33 +351,21 @@ fn test_an_intercepting_callback_books_no_envelope_adjustment() { } let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let db = base_db(code).account_code(CALLEE, callee); + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, callee); let mut inspector = Interceptor::default(); - let inspected = transact_inspected(db, default_limits(), &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); assert_eq!( - inspected.ledger, + inspected.inspector_ledger, InspectorLedger { interventions: 1, ..InspectorLedger::default() }, "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ itself a rewrite", ); - assert_eq!(inspected.ledger.conjured_gas(), 0, "no gas lane may move on this shape"); - assert_identity("intercepted", &inspected); + assert_eq!(inspected.inspector_ledger.conjured_gas(), 0, "no gas lane may move on this shape"); } /// (iii) A `create_end` that turns a failed contract creation into a successful one is refused, @@ -572,53 +377,12 @@ fn test_an_intercepting_callback_books_no_envelope_adjustment() { /// all: debug builds assert, release builds surface the refusal as an `EVMError`. #[test] fn test_reviving_a_failed_creation_is_refused() { - // Init code that reverts immediately: PUSH1 0, PUSH1 0, REVERT. - let init_code: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init_code.iter().enumerate() { - builder = builder - .push_number(u64::from(*byte)) - .push_number(offset as u64) - .append(revm::bytecode::opcode::MSTORE8); - } - let code = builder - .push_number(init_code.len() as u64) // size - .push_number(0u64) // offset - .push_number(0u64) // value - .append(CREATE) - .append(POP) - .append(STOP) - .build(); - let db = base_db(code); + let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); - let run = || { + assert_refused(REVIVED_CREATION, || { let mut inspector = CreateReviver; - try_transact_inspected(db.clone(), default_limits(), &mut inspector) - }; - - if cfg!(debug_assertions) { - let previous = std::panic::take_hook(); - std::panic::set_hook(Box::new(|_| {})); - let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)); - std::panic::set_hook(previous); - let payload = panicked.expect_err("the detector must fire in debug builds"); - let message = payload - .downcast_ref::() - .map(String::as_str) - .or_else(|| payload.downcast_ref::<&str>().copied()) - .unwrap_or_default() - .to_string(); - assert!( - message.contains("inspector rewrote a failed contract creation into a successful one"), - "the assertion must name the shape it caught; got {message:?}", - ); - } else { - let error = run().expect_err("the refusal must surface as an EVMError in release builds"); - assert!( - error.contains("inspector rewrote a failed contract creation into a successful one"), - "the error must name the shape it caught; got {error:?}", - ); - } + try_transact_inspected(db.clone(), limits(), &mut inspector) + }); } /// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. @@ -655,22 +419,11 @@ fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { } } - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let db = base_db(code).account_code(CALLEE, plain_run_code(20)); + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, plain_run_code(20)); let mut inspector = HaltingInterceptor::default(); - let inspected = transact_inspected(db, default_limits(), &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); @@ -680,10 +433,9 @@ fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { ); assert_eq!( inspected.compute_gas, - inspected.enforced + inspected.destroyed, + inspected.enforced() + inspected.destroyed, "and it is reported without being enforced", ); - assert_identity("intercepted halt", &inspected); } /// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the @@ -707,27 +459,14 @@ fn test_killing_a_successful_creation_rolls_its_state_back() { .build() .to_vec(); - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init_code.iter().enumerate() { - builder = builder - .push_number(u64::from(*byte)) - .push_number(offset as u64) - .append(revm::bytecode::opcode::MSTORE8); - } - let code = builder - .push_number(init_code.len() as u64) // size - .push_number(0u64) // offset - .push_number(0u64) // value - .append(CREATE) - .append(POP) - .append(STOP) - .build(); + let code = deploy_then_stop(&init_code); let deployed = CONTRACT.create(0); // The uninspected run deploys, so the rewrite has something to undo. let mut observer = Observer::default(); - let plain = transact_inspected(base_db(code.clone()), default_limits(), &mut observer); + let plain = + transact_inspected(MegaSpecId::REX7, base_db(code.clone()), limits(), &mut observer); assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); assert!( @@ -736,7 +475,7 @@ fn test_killing_a_successful_creation_rolls_its_state_back() { ); let mut killer = CreateKiller::default(); - let killed = transact_inspected(base_db(code), default_limits(), &mut killer); + let killed = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut killer); assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); assert!( @@ -753,7 +492,6 @@ fn test_killing_a_successful_creation_rolls_its_state_back() { U256::ZERO, "and none of the constructor's storage writes", ); - assert_identity("killed creation", &killed); } /// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. @@ -777,24 +515,24 @@ fn test_an_observing_inspector_changes_nothing() { .append(POP) .append(STOP) .build(); - let build_db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + let build_db = || db_with_callee(code.clone(), callee.clone()); - let plain = transact_plain(build_db(), default_limits()); + let plain = transact(MegaSpecId::REX7, build_db(), limits()); let mut inspector = Observer::default(); - let inspected = transact_inspected(build_db(), default_limits(), &mut inspector); + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); assert_eq!(inspector.call_ends, 2, "every call must be paired"); assert!( - inspected.ledger.is_zero(), + inspected.inspector_ledger.is_zero(), "an observation-only inspector must leave an empty ledger; got {:?}", - inspected.ledger, + inspected.inspector_ledger, ); assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); assert_eq!(inspected.compute_gas, plain.compute_gas); - assert_eq!(inspected.enforced, plain.enforced); + assert_eq!(inspected.enforced(), plain.enforced()); assert_eq!(inspected.destroyed, plain.destroyed); assert_eq!(inspected.data_size, plain.data_size); assert_eq!(inspected.kv_updates, plain.kv_updates); @@ -803,8 +541,6 @@ fn test_an_observing_inspector_changes_nothing() { assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); assert_eq!(inspected.terms, plain.terms); assert_eq!(inspected.state, plain.state, "the produced state must be identical"); - assert_identity("observed", &inspected); - assert_identity("plain", &plain); } /// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term @@ -830,9 +566,9 @@ fn test_an_uninspected_transaction_reports_an_empty_ledger() { .append(POP) .append(STOP) .build(); - let db = base_db(code).account_code(CALLEE, callee); + let db = db_with_callee(code, callee); - let plain = transact_plain(db, default_limits()); + let plain = transact(MegaSpecId::REX7, db, limits()); assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); assert!( @@ -840,10 +576,9 @@ fn test_an_uninspected_transaction_reports_an_empty_ledger() { "fixture check: the transaction must have moved a lane other than compute", ); assert_eq!( - plain.ledger, + plain.inspector_ledger, InspectorLedger::default(), "no inspector ran, so every lane must be untouched", ); assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); - assert_identity("uninspected", &plain); } From 8d164bd76cab5a69aaa7c10add4c8b8d0f33b055 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:51:14 +0800 Subject: [PATCH 183/208] test(rex7): put the other four measurement files on the shared fixtures Each of `interception_gas`, `refund_and_state_gas`, `inspector_settlement_window` and `ledger_blind_spots` carried its own reading type, driver and copy of the conservation identity, plus its own spelling of the two bytecode shapes every one of them needs: a call an inspector can widen, and a creation it can rewrite. They now read the shared `Outcome` and drive through `common`, and the plain-versus-cheated pair every rewrite is pinned as is one helper. Lines 2951 -> 2649. Assertions 194 -> 187 (0 removed, 7 deduped): three copies of the identity's three checks and four of the ledger equality, all of which the driver now runs on every transaction rather than on the ones that asked. --- .../mega-evm/tests/rex7/inspector_common.rs | 21 +- .../tests/rex7/inspector_settlement_window.rs | 87 ++---- .../mega-evm/tests/rex7/interception_gas.rs | 216 +++----------- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 69 ++--- .../tests/rex7/refund_and_state_gas.rs | 277 +++++------------- 5 files changed, 193 insertions(+), 477 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_common.rs b/crates/mega-evm/tests/rex7/inspector_common.rs index d0a2a4ee..64272001 100644 --- a/crates/mega-evm/tests/rex7/inspector_common.rs +++ b/crates/mega-evm/tests/rex7/inspector_common.rs @@ -17,7 +17,10 @@ use revm::{ }; use std::{boxed::Box, string::String, vec::Vec}; -use crate::common::{call_contract_tx, context, plain_filler, CALLEE, DEFAULT_TX_GAS_LIMIT}; +use crate::common::{ + call_contract_tx, context, plain_filler, transact, transact_inspected, Outcome, CALLEE, + DEFAULT_TX_GAS_LIMIT, +}; /// The spec every fixture here runs under, and its default runtime limits. pub(crate) fn limits() -> EvmTxRuntimeLimits { @@ -100,6 +103,22 @@ pub(crate) fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { crate::common::base_db(code).account_code(CALLEE, callee) } +/// Runs one fixture twice: with no inspector, then with `inspector` attached. +/// +/// Every rewrite in this suite is pinned as a difference between those two runs, so the fixture +/// has to be built twice from the same recipe — `db` is a closure for that reason. +pub(crate) fn plain_and_cheated( + db: impl Fn() -> MemoryDatabase, + inspector: &mut I, +) -> (Outcome, Outcome) +where + I: for<'a> Inspector>, +{ + let plain = transact(MegaSpecId::REX7, db(), limits()); + let cheated = transact_inspected(MegaSpecId::REX7, db(), limits(), inspector); + (plain, cheated) +} + // --- ledgers ------------------------------------------------------------------------------- /// The ledger of a rewrite that moved gas on exactly one lane. diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs index a9ec7ab8..d60f7b38 100644 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs @@ -27,15 +27,16 @@ //! Every case here is checked by the identity `common::finish` runs on every transaction: the //! tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. -use crate::common::{ - transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, CALLEE, CALLER, - CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, +use crate::{ + common::{ + base_db, transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, + CALLEE, DEFAULT_TX_GAS_LIMIT, + }, + inspector_common::{call_then_stop, db_with_callee, limits}, }; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ - kzg_point_evaluation, - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, + kzg_point_evaluation, test_utils::BytecodeBuilder, InspectorLedger, Lane, MegaSpecId, }; use revm::{ bytecode::opcode::{CALL, INVALID, POP, STOP}, @@ -67,17 +68,6 @@ const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); /// KZG point evaluation. const KZG: Address = address!("000000000000000000000000000000000000000a"); -fn limits() -> EvmTxRuntimeLimits { - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7) -} - -fn db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - // --- A: the window a terminating opcode's `step_end` sits in --------------------------------- /// Which of the three `step_end` windows an edit is aimed at, told apart by the action the @@ -136,24 +126,13 @@ fn straight_line_code() -> Bytes { /// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame /// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. fn suspending_code() -> Bytes { - BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(IDENTITY) - .push_number(FORWARDED) - .append(CALL) - .append(POP) - .append(STOP) - .build() + call_then_stop(IDENTITY, FORWARDED) } fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { - let plain = transact(MegaSpecId::REX7, db(code.clone()), limits()); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); let mut inspector = CounterEditor::new(window); - let edited = transact_inspected(MegaSpecId::REX7, db(code), limits(), &mut inspector); + let edited = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); (plain, edited, inspector.fired) } @@ -303,9 +282,10 @@ fn kzg_verification_failure() -> Vec { /// uninspected run's split therefore is. fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { let code = call_precompile(target, calldata); - let plain = transact(MegaSpecId::REX7, db(code.clone()), limits()); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); let mut inspector = Reclassifier::new(target, to); - let refusal = transact_inspected_refused(MegaSpecId::REX7, db(code), limits(), &mut inspector); + let refusal = + transact_inspected_refused(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); assert!( @@ -444,22 +424,7 @@ impl Inspector for ActionEditor { /// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries /// is decided by the callee the fixture installs. fn call_callee_code() -> Bytes { - BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(FORWARDED) - .append(CALL) - .append(POP) - .append(STOP) - .build() -} - -fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { - db(code).account_code(CALLEE, callee) + call_then_stop(CALLEE, FORWARDED) } /// Gas written into a returning frame's pending action is gas the caller really reclaims, so it @@ -467,10 +432,14 @@ fn db_with_callee(code: Bytes, callee: Bytes) -> MemoryDatabase { /// known at the frame's settlement point. #[test] fn test_raising_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); let mut inspector = ActionEditor::raise(Window::Terminating); - let edited = - transact_inspected(MegaSpecId::REX7, db(straight_line_code()), limits(), &mut inspector); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( @@ -495,10 +464,14 @@ fn test_raising_a_returning_frames_pending_action_is_booked() { /// The same edit in the other direction. #[test] fn test_lowering_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, db(straight_line_code()), limits()); + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); let mut inspector = ActionEditor::lower(Window::Terminating); - let edited = - transact_inspected(MegaSpecId::REX7, db(straight_line_code()), limits(), &mut inspector); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); assert_eq!( @@ -566,10 +539,10 @@ fn test_editing_a_halting_frames_pending_action_moves_nothing() { /// frame is about to be built with, which the caller was never debited for. #[test] fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { - let plain = transact(MegaSpecId::REX7, db(suspending_code()), limits()); + let plain = transact(MegaSpecId::REX7, base_db(suspending_code()), limits()); let mut inspector = ActionEditor::raise(Window::Suspending); let edited = - transact_inspected(MegaSpecId::REX7, db(suspending_code()), limits(), &mut inspector); + transact_inspected(MegaSpecId::REX7, base_db(suspending_code()), limits(), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); assert_eq!( diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs index ca20c406..1cfdbd28 100644 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ b/crates/mega-evm/tests/rex7/interception_gas.rs @@ -19,18 +19,18 @@ //! inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed //! remainder is settled against the envelope instead. -use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use crate::{ + common::{base_db, transact_inspected, CALLEE}, + inspector_common::{call_then_stop, db_with_callee, deploy_then_stop, limits, plain_run_code}, +}; use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, - MegaTransactionNew as _, MegaTransactionOutcome, + EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, }; use revm::{ - bytecode::opcode::{CALL, CREATE, MSTORE, MSTORE8, POP, RETURN, STOP}, - context::{result::ExecutionResult, tx::TxEnvBuilder}, - handler::{EvmTr, FrameResult}, + bytecode::opcode::{MSTORE, RETURN}, + handler::FrameResult, interpreter::{ CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, InterpreterResult, InterpreterTypes, @@ -39,130 +39,12 @@ use revm::{ }; use std::vec::Vec; -/// High enough that EVM gas is never what binds. -const TX_GAS_LIMIT: u64 = 100_000_000; /// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. const FORWARDED: u64 = 50_000; -/// Everything one transaction reports, plus what the shim booked for it. -struct Reading { - result: ExecutionResult, - compute_gas: u64, - enforced: u64, - destroyed: u64, - total_gas_spent: u64, - terms: ConservationTerms, - ledger: InspectorLedger, -} - -/// The conservation identity, stated with the term the measurement shim contributes. -fn assert_identity(label: &str, r: &Reading) { - assert_eq!( - r.compute_gas, - r.enforced + r.destroyed, - "{label}: reported compute must split into enforced + destroyed", - ); - assert_eq!( - r.terms.inspector_conjured_gas, - r.ledger.conjured_gas(), - "{label}: the law's `I` term is the ledger's net, and nothing else", - ); - assert_eq!( - r.terms.envelope_for(r.destroyed), - i128::from(r.total_gas_spent), - "{label}: the law must close against the envelope the receipt reports; \ - reported compute={} destroyed={} envelope={} ({})", - r.compute_gas, - r.destroyed, - r.total_gas_spent, - r.terms, - ); -} - -fn tx() -> MegaTransaction { - let mut tx = MegaTransaction::new( - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), - ); - tx.enveloped_tx = Some(Bytes::new()); - tx -} - -fn context_for( - db: &mut MemoryDatabase, - spec: MegaSpecId, -) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = - MegaContext::new(db, spec).with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec)); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context -} - -fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { - assert_eq!( - outcome.inspector_ledger, - limit.inspector_ledger(), - "the outcome must report the ledger the shim booked, unchanged", - ); - let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); - Reading { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - enforced: outcome.compute_gas_enforced, - destroyed: outcome.compute_gas_destroyed, - total_gas_spent, - terms: limit.conservation_terms(), - ledger: outcome.inspector_ledger, - } -} - -fn transact_on(spec: MegaSpecId, mut db: MemoryDatabase, inspector: &mut I) -> Reading -where - I: for<'a> Inspector>, -{ - let mut evm = MegaEvm::new(context_for(&mut db, spec)).with_inspector(inspector); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading -} - -fn transact(db: MemoryDatabase, inspector: &mut I) -> Reading -where - I: for<'a> Inspector>, -{ - transact_on(MegaSpecId::REX7, db, inspector) -} - -/// A straight run of plain opcodes that always succeeds. -fn plain_run_code(pairs: usize) -> Bytes { - let mut builder = BytecodeBuilder::default(); - for _ in 0..pairs { - builder = builder.push_number(1u64).append(POP); - } - builder.append(STOP).build() -} - /// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. fn call_fixture() -> MemoryDatabase { - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(u128::from(FORWARDED)) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, plain_run_code(20)) + db_with_callee(call_then_stop(CALLEE, FORWARDED), plain_run_code(20)) } /// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. @@ -230,13 +112,13 @@ impl Inspector for CallInterceptor { #[test] fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let reading = transact(call_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, @@ -244,18 +126,17 @@ fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { }, "the half the outcome withheld is gas the inspector destroyed", ); - assert_identity("half-gas interception", &reading); } /// The extreme of the same direction: the outcome hands back nothing at all. #[test] fn test_a_zero_gas_interception_books_the_whole_envelope() { let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); - let reading = transact(call_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), interventions: 1, @@ -263,7 +144,6 @@ fn test_a_zero_gas_interception_books_the_whole_envelope() { }, "an outcome that returns nothing consumed the whole envelope", ); - assert_identity("zero-gas interception", &reading); } /// The other direction: an outcome that hands back more than it was given conjures the difference. @@ -271,11 +151,11 @@ fn test_a_zero_gas_interception_books_the_whole_envelope() { fn test_an_over_funded_interception_books_the_gas_it_conjured() { const EXTRA: u64 = 7_000; let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); - let reading = transact(call_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), interventions: 1, @@ -283,7 +163,6 @@ fn test_an_over_funded_interception_books_the_gas_it_conjured() { }, "gas the transaction never funded is gas the inspector conjured", ); - assert_identity("over-funded interception", &reading); } /// The echo convention moves nothing, and must book nothing. @@ -296,16 +175,16 @@ fn test_an_over_funded_interception_books_the_gas_it_conjured() { fn test_an_echoing_interception_books_no_gas_at_all() { for classification in [InstructionResult::Stop, InstructionResult::Revert] { let mut inspector = CallInterceptor::new(Sizing::Echo, classification); - let reading = transact(call_fixture(), &mut inspector); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { interventions: 1, ..InspectorLedger::default() }, "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", ); - assert_eq!(reading.ledger.conjured_gas(), 0, "{classification:?}"); - assert_identity("echoing interception", &reading); + assert_eq!(reading.inspector_ledger.conjured_gas(), 0, "{classification:?}"); } } @@ -319,17 +198,18 @@ fn test_an_echoing_interception_books_no_gas_at_all() { fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); - let reading = transact(call_fixture(), &mut inspector); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); assert_eq!( - reading.ledger.conjured_gas(), + reading.inspector_ledger.conjured_gas(), 0, "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", ); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { interventions: 1, result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), @@ -341,7 +221,6 @@ fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { reading.destroyed, FORWARDED, "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", ); - assert_identity("halting interception", &reading); } } @@ -380,11 +259,11 @@ fn test_the_generic_frame_start_interception_is_measured_too() { } let mut inspector = GenericInterceptor::default(); - let reading = transact(call_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, @@ -392,7 +271,6 @@ fn test_the_generic_frame_start_interception_is_measured_too() { }, "the generic callback's interception books on the same lane as the variant one's", ); - assert_identity("frame_start interception", &reading); } /// Init code that writes one slot and returns two bytes of runtime code. @@ -411,23 +289,7 @@ fn init_code() -> Vec { /// The entry contract: one `CREATE`, then `STOP`. fn create_fixture() -> MemoryDatabase { - let init = init_code(); - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) // size - .push_number(0u64) // offset - .push_number(0u64) // value - .append(CREATE) - .append(POP) - .append(STOP) - .build(); - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) + base_db(deploy_then_stop(&init_code())) } /// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. @@ -463,12 +325,12 @@ fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() } let mut inspector = CreateInterceptor::default(); - let reading = transact(create_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, create_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), interventions: 1, @@ -476,7 +338,6 @@ fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() }, "a creation's interception is measured against the envelope its CREATE forwarded", ); - assert_identity("intercepted creation", &reading); } /// The envelope an interception is measured against is the one the callback *received*. @@ -514,11 +375,11 @@ fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { } let mut inspector = RaisingInterceptor::default(); - let reading = transact(call_fixture(), &mut inspector); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); assert_eq!( - reading.ledger, + reading.inspector_ledger, InspectorLedger { result: Lane::once(i128::from(BONUS)), interventions: 1, @@ -527,7 +388,6 @@ fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { "the bonus reaches the caller through the outcome, so it is booked once, on the result \ lane — the env lane stays empty because no frame was ever built from those inputs", ); - assert_identity("raised then intercepted", &reading); } /// The lane reports on a frozen spec too, and reporting it settles nothing there. @@ -544,19 +404,29 @@ fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { #[test] fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); - let echo = transact_on(MegaSpecId::REX6, call_fixture(), &mut echoing); + let echo = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut echoing, + ); let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let half = transact_on(MegaSpecId::REX6, call_fixture(), &mut halving); + let half = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut halving, + ); assert_eq!(echoing.intercepted, 1, "fixture check"); assert_eq!(halving.intercepted, 1, "fixture check"); assert_eq!( - echo.ledger, + echo.inspector_ledger, InspectorLedger { interventions: 1, ..InspectorLedger::default() }, "REX6: an echoed envelope moves no gas here either", ); assert_eq!( - half.ledger, + half.inspector_ledger, InspectorLedger { result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), interventions: 1, diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs index a38dd4e2..8eef5f15 100644 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs @@ -32,12 +32,12 @@ //! What the shim takes now is every constant-time reading of the interpreter, and what pins that is //! the `Interpreter` row of `gas_surface.rs`'s closed table. -use crate::common::{transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET, ONE_ETH}; -use alloy_primitives::{address, Address, Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, MegaSpecId, +use crate::{ + common::{base_db, transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET}, + inspector_common::plain_and_cheated, }; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaSpecId}; use revm::{ bytecode::opcode::{ CALL, CALLER, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, @@ -68,13 +68,6 @@ const RESULT_SLOT: u64 = 0x11; /// to the receipt is the whole of the surviving half rather than whatever the cap left of it. const REFUND: i64 = 2_000; -fn db_with(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(crate::common::CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// The mainnet memory expansion cost of a memory `words` words long. const fn memory_cost(words: u64) -> u64 { 3 * words + words * words / 512 @@ -134,10 +127,8 @@ fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = FreeExpansion::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); @@ -213,12 +204,10 @@ fn test_a_moved_return_range_is_booked() { .push_number(0u64) .append(RETURN) .build(); - let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db(), limits); let mut inspector = MoveReturnData::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); assert_eq!( @@ -292,12 +281,10 @@ fn test_a_forged_call_output_is_booked() { .push_number(0u64) .append(RETURN) .build(); - let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db(), limits); let mut inspector = ForgeCallOutput::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); assert_eq!( @@ -361,10 +348,8 @@ fn test_a_rewritten_deployment_address_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = MoveDeploymentAddress::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); @@ -442,10 +427,8 @@ fn test_a_drained_construction_action_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = DrainConstructionAction::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); assert_ne!( @@ -532,12 +515,10 @@ fn test_cancelling_action_and_result_edits_are_booked() { .append(STOP) .build(); let callee = BytecodeBuilder::default().append(STOP).build(); - let db = || db_with(code.clone()).account_code(CALLEE, callee.clone()); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db(), limits); let mut inspector = CancellingActionAndResultEdits::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); assert_eq!( @@ -607,9 +588,9 @@ fn test_cancelling_counter_edits_are_booked() { // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of // its remaining gas is the counter the injection moved. let limits = EvmTxRuntimeLimits::no_limits(); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); let mut inspector = CancellingCounterEdits::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let cheated = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); assert_eq!( @@ -690,15 +671,13 @@ fn test_cancelling_refunds_across_frames_are_booked() { let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); let reverting = clearing(BytecodeBuilder::default()).revert().build(); let db = || { - db_with(code.clone()) + base_db(code.clone()) .account_code(CALLEE, returning.clone()) .account_code(REVERTER, reverting.clone()) }; - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db(), limits); let mut inspector = CancellingRefundsAcrossFrames::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db(), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); assert!( @@ -770,10 +749,8 @@ fn test_a_skipped_opcode_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = SkipTheStore::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); @@ -834,10 +811,8 @@ fn test_a_forged_return_buffer_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = ForgeReturnData::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); @@ -923,10 +898,8 @@ fn test_a_frame_invariant_moved_and_moved_back_is_booked() { .append(STOP) .build(); - let limits = EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7); - let plain = transact(MegaSpecId::REX7, db_with(code.clone()), limits); let mut inspector = BorrowTheCaller::default(); - let cheated = transact_inspected(MegaSpecId::REX7, db_with(code), limits, &mut inspector); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); assert_eq!( diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs index 58b66473..f5f3eaff 100644 --- a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs +++ b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs @@ -19,18 +19,17 @@ //! goes on to erase. The lane is settled once, from the number the transaction ends with, which //! is exactly the surviving part and is the inspector's in whole. -use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use crate::{ + common::{transact, transact_inspected, Outcome, CALLEE}, + inspector_common::{append_call, db_with_callee, limits}, +}; use alloy_primitives::{Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, - MegaTransactionNew as _, MegaTransactionOutcome, + ConservationTerms, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, }; use revm::{ - bytecode::opcode::{CALL, POP, STOP}, - context::{result::ExecutionResult, tx::TxEnvBuilder}, - handler::EvmTr, + bytecode::opcode::{POP, STOP}, interpreter::{ interpreter_types::LoopControl, CallInputs, CallOutcome, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, @@ -38,103 +37,25 @@ use revm::{ Inspector, }; -/// High enough that EVM gas is never what binds. -const TX_GAS_LIMIT: u64 = 100_000_000; /// Gas the fixture's inner `CALL` forwards. const INNER_CALL_GAS: u64 = 200_000; -/// Refund an edit writes, small enough that the EIP-3529 cap does not clip it. +/// The refund an edit writes, small enough to stay under the EIP-3529 cap. const REFUND: i64 = 2_000; -/// Refund the cap test writes, chosen to exceed a fifth of anything the fixture can burn. +/// A refund large enough that the cap keeps part of it out of the receipt. const OVERSIZED_REFUND: i64 = 60_000; -/// The EIP-8037 pool a reservoir edit fills. +/// The EIP-8037 pool an edit fills. const RESERVOIR: u64 = 10_000; -/// The EIP-8037 spend counter a state-gas edit writes. +/// The EIP-8037 spend an edit writes. const STATE_GAS: i64 = 5_000; -/// Slot the caller writes. +/// Slot the top frame writes. const TOP_SLOT: u64 = 0x10; -/// Slot the callee writes and keeps. +/// Slot the callee writes. const CALLEE_SLOT: u64 = 0x20; -/// Slot the callee sets and clears, so its frame carries a refund the EVM produced. +/// Slot the callee sets and clears, so the frame ends holding a refund of the EVM's own making. const CLEARED_SLOT: u64 = 0x30; -// --- what one run reports ---------------------------------------------------------------------- - -struct Reading { - result: ExecutionResult, - /// Receipt `gas_used`: the envelope less the refund, floored by EIP-7623. - gas_used: u64, - /// Receipt envelope, which is what the conservation law is stated over. - total_gas_spent: u64, - /// Receipt refund, after the EIP-3529 cap. - refunded: u64, - /// Receipt EIP-8037 state gas. - state_gas_spent: u64, - destroyed: u64, - terms: ConservationTerms, - ledger: InspectorLedger, -} - -/// The conservation identity, over the envelope the receipt reports. -fn assert_identity(label: &str, r: &Reading) { - assert_eq!( - r.terms.inspector_conjured_gas, - r.ledger.conjured_gas(), - "{label}: the law's `I` term is the ledger's net, and nothing else", - ); - assert_eq!( - r.terms.envelope_for(r.destroyed), - i128::from(r.total_gas_spent), - "{label}: the law must close against the envelope the receipt reports ({})", - r.terms, - ); -} - -fn tx() -> MegaTransaction { - let mut tx = MegaTransaction::new( - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), - ); - tx.enveloped_tx = Some(Bytes::new()); - tx -} - -fn context_on( - db: &mut MemoryDatabase, - spec: MegaSpecId, -) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = - MegaContext::new(db, spec).with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec)); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context -} - -fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - context_on(db, MegaSpecId::REX7) -} - -fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { - assert_eq!( - outcome.inspector_ledger, - limit.inspector_ledger(), - "the outcome must report the ledger the shim booked, unchanged", - ); - let gas = *outcome.result_and_state.result.gas(); - Reading { - result: outcome.result_and_state.result, - gas_used: gas.tx_gas_used(), - total_gas_spent: gas.total_gas_spent(), - refunded: gas.inner_refunded(), - state_gas_spent: gas.state_gas_spent_final(), - destroyed: outcome.compute_gas_destroyed, - terms: limit.conservation_terms(), - ledger: outcome.inspector_ledger, - } -} - // --- the fixture ------------------------------------------------------------------------------- /// How the fixture's callee ends, which is what decides whether its refund travels. @@ -147,15 +68,7 @@ enum Callee { } fn caller_code() -> Bytes { - BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(u128::from(INNER_CALL_GAS)) - .append(CALL) + append_call(BytecodeBuilder::default(), CALLEE, INNER_CALL_GAS, 0) .append(POP) .sstore(U256::from(TOP_SLOT), U256::from(1u64)) .append(STOP) @@ -175,11 +88,7 @@ fn callee_code(callee: Callee) -> Bytes { } fn db_for(callee: Callee) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, caller_code()) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, callee_code(callee)) + db_with_callee(caller_code(), callee_code(callee)) } // --- the edit ---------------------------------------------------------------------------------- @@ -308,27 +217,19 @@ impl Inspector for Editor { } /// Runs the fixture with no inspector at all. -fn transact_plain(callee: Callee) -> Reading { - let mut db = db_for(callee); - let mut evm = MegaEvm::new(context(&mut db)); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading +fn transact_plain(callee: Callee) -> Outcome { + transact(MegaSpecId::REX7, db_for(callee), limits()) } /// Runs it with one edit applied, asserting the edit landed exactly once. -fn transact_edited(callee: Callee, edit: Edit) -> Reading { - let mut db = db_for(callee); +fn transact_edited(callee: Callee, edit: Edit) -> Outcome { let mut editor = Editor::new(edit); - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut editor); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); + let outcome = transact_inspected(MegaSpecId::REX7, db_for(callee), limits(), &mut editor); assert_eq!( - alloy_evm::Evm::inspector(&evm).fired, - 1, + editor.fired, 1, "{edit:?}: the fixture must reach the edit's callback exactly once", ); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading + outcome } // --- the fixture's own assumptions --------------------------------------------------------------- @@ -340,17 +241,16 @@ fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { let plain = transact_plain(Callee::Returning); assert!(plain.result.is_success(), "{:?}", plain.result); assert!( - plain.refunded > 0, + plain.refunded() > 0, "the callee's cleared slot must leave a refund for the lowering cell to take from", ); assert_eq!( plain.gas_used, - plain.total_gas_spent - plain.refunded, + plain.total_gas_spent - plain.refunded(), "the receipt's two gas numbers differ by exactly the refund", ); - assert_eq!(plain.state_gas_spent, 0, "EIP-8037 is off on every MegaETH path"); - assert!(plain.ledger.is_zero(), "no inspector ran: {:?}", plain.ledger); - assert_identity("plain", &plain); + assert_eq!(plain.state_gas_spent(), 0, "EIP-8037 is off on every MegaETH path"); + assert!(plain.inspector_ledger.is_zero(), "no inspector ran: {:?}", plain.inspector_ledger); } // --- the refund lane @@ -364,7 +264,7 @@ fn test_a_refund_written_into_a_live_interpreter_is_booked() { let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, "the shim must book the refund and nothing else", ); @@ -373,8 +273,8 @@ fn test_a_refund_written_into_a_live_interpreter_is_booked() { "a refund does not move the envelope, which is why the law cannot see it", ); assert_eq!( - edited.refunded, - plain.refunded + u64::try_from(REFUND).unwrap(), + edited.refunded(), + plain.refunded() + u64::try_from(REFUND).unwrap(), "but it does move the receipt's refund", ); assert_eq!( @@ -386,8 +286,7 @@ fn test_a_refund_written_into_a_live_interpreter_is_booked() { edited.terms.inspector_conjured_gas, 0, "the refund lane is deliberately not a term of the law", ); - assert!(!edited.ledger.is_zero(), "and the block guard has to see it"); - assert_identity("refund at step", &edited); + assert!(!edited.inspector_ledger.is_zero(), "and the block guard has to see it"); } /// The same edit made at the last callback that holds the finished frame's result. @@ -397,12 +296,11 @@ fn test_a_refund_written_into_a_finished_frame_result_is_booked() { let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, ); - assert_eq!(edited.refunded, plain.refunded + u64::try_from(REFUND).unwrap()); + assert_eq!(edited.refunded(), plain.refunded() + u64::try_from(REFUND).unwrap()); assert_eq!(edited.total_gas_spent, plain.total_gas_spent); - assert_identity("refund at call_end", &edited); } /// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction @@ -411,23 +309,22 @@ fn test_a_refund_written_into_a_finished_frame_result_is_booked() { fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { let plain = transact_plain(Callee::Returning); assert!( - plain.refunded >= u64::try_from(REFUND).unwrap(), + plain.refunded() >= u64::try_from(REFUND).unwrap(), "fixture check: there must be a refund to take from, got {}", - plain.refunded, + plain.refunded(), ); let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, ); - assert_eq!(edited.refunded, plain.refunded - u64::try_from(REFUND).unwrap()); + assert_eq!(edited.refunded(), plain.refunded() - u64::try_from(REFUND).unwrap()); assert_eq!( edited.gas_used, plain.gas_used + u64::try_from(REFUND).unwrap(), "the sender pays more, by exactly what was taken", ); - assert_identity("refund lowered", &edited); } /// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. @@ -442,7 +339,7 @@ fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { refund: Lane::once(i128::from(OVERSIZED_REFUND)), ..InspectorLedger::default() @@ -450,16 +347,15 @@ fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() "the lane carries the nominal edit", ); assert_eq!( - edited.refunded, + edited.refunded(), edited.total_gas_spent / 5, "while the receipt carries the EIP-3529 cap", ); assert!( - edited.refunded < plain.refunded + u64::try_from(OVERSIZED_REFUND).unwrap(), + edited.refunded() < plain.refunded() + u64::try_from(OVERSIZED_REFUND).unwrap(), "fixture check: the cap must actually bind, or this cell asserts nothing", ); assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); - assert_identity("oversized refund", &edited); } /// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. @@ -475,16 +371,16 @@ fn test_a_refund_the_frame_chain_discards_is_still_booked() { let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, "the lane books the edit", ); assert_eq!( - edited.refunded, plain.refunded, + edited.refunded(), + plain.refunded(), "the receipt is unmoved: a reverting frame hands its caller no refund", ); assert_eq!(edited.gas_used, plain.gas_used); - assert_identity("refund on a reverting frame", &edited); } // --- the EIP-8037 state-gas dimension ------------------------------------------------------------ @@ -497,7 +393,7 @@ fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { reservoir: Lane::once(i128::from(RESERVOIR)), ..InspectorLedger::default() @@ -513,7 +409,6 @@ fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes i128::from(RESERVOIR), "which is why this lane, unlike the refund one, is a term of the law", ); - assert_identity("reservoir at step", &edited); } /// The same, written into the pool a call's inputs seed the child frame with. @@ -523,7 +418,7 @@ fn test_a_reservoir_written_into_a_frame_input_is_booked() { let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { reservoir: Lane::once(i128::from(RESERVOIR)), // The inputs came back changed in a field the envelope lane does not cover, which the @@ -533,7 +428,6 @@ fn test_a_reservoir_written_into_a_frame_input_is_booked() { }, ); assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); - assert_identity("reservoir on inputs", &edited); } /// And into the finished frame's own pool, which its caller takes whatever the classification. @@ -543,14 +437,13 @@ fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { reservoir: Lane::once(i128::from(RESERVOIR)), ..InspectorLedger::default() }, ); assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); - assert_identity("reservoir at call_end", &edited); } /// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the @@ -565,14 +458,13 @@ fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); assert!( - edited.ledger.is_zero(), + edited.inspector_ledger.is_zero(), "an edit the child frame's own pool replaces moved nothing: {:?}", - edited.ledger, + edited.inspector_ledger, ); assert_eq!(edited.total_gas_spent, plain.total_gas_spent); assert_eq!(edited.gas_used, plain.gas_used); - assert_eq!(edited.refunded, plain.refunded); - assert_identity("reservoir in the dead window", &edited); + assert_eq!(edited.refunded(), plain.refunded()); } /// The spend counter's own effect on the receipt: a successful transaction reports it, whether or @@ -582,14 +474,14 @@ fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_boo let plain = transact_plain(Callee::Returning); let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); - assert_eq!(plain.state_gas_spent, 0, "fixture check"); + assert_eq!(plain.state_gas_spent(), 0, "fixture check"); assert_eq!( - edited.state_gas_spent, + edited.state_gas_spent(), u64::try_from(STATE_GAS).unwrap(), "the receipt reports what was written", ); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { state_gas: Lane::once(i128::from(STATE_GAS)), ..InspectorLedger::default() @@ -600,7 +492,6 @@ fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_boo "the envelope is untouched, so this lane is not a term of the law either", ); assert_eq!(edited.terms.inspector_conjured_gas, 0); - assert_identity("state gas at step", &edited); } /// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend @@ -615,7 +506,7 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); assert_eq!( - edited.ledger, + edited.inspector_ledger, InspectorLedger { reservoir: Lane::once(i128::from(STATE_GAS)), ..InspectorLedger::default() @@ -623,7 +514,8 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { "the spend counter of a reverting frame arrives in its caller as a pool", ); assert_eq!( - edited.state_gas_spent, 0, + edited.state_gas_spent(), + 0, "and not as a spend: a failing frame's counter is not accumulated", ); assert_eq!( @@ -631,7 +523,6 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), "so the envelope moves, and the law's term has to move with it", ); - assert_identity("state gas on a reverting frame", &edited); } // --- a frame the inspector answers itself @@ -648,15 +539,14 @@ fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { fn test_a_synthetic_outcome_carries_its_own_figures() { let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); assert_eq!( - echo.ledger, + echo.inspector_ledger, InspectorLedger { interventions: 1, ..InspectorLedger::default() }, "an echoing interception moves no figure at all", ); - assert_identity("interception, echo", &echo); let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); assert_eq!( - refunding.ledger, + refunding.inspector_ledger, InspectorLedger { refund: Lane::once(i128::from(REFUND)), interventions: 1, @@ -665,16 +555,15 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { "the refund a frame that never ran hands back is the inspector's in whole", ); assert_eq!( - refunding.refunded, - echo.refunded + u64::try_from(REFUND).unwrap(), + refunding.refunded(), + echo.refunded() + u64::try_from(REFUND).unwrap(), "and it reaches the receipt: the outcome succeeded, so its caller records it", ); assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); - assert_identity("interception, refunding", &refunding); let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); assert_eq!( - pooled.ledger, + pooled.inspector_ledger, InspectorLedger { reservoir: Lane::once(i128::from(RESERVOIR)), interventions: 1, @@ -686,7 +575,6 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { echo.total_gas_spent - RESERVOIR, "a pool does move the envelope, wherever it came from", ); - assert_identity("interception, pooled", &pooled); } // --- the frozen specs @@ -700,33 +588,23 @@ fn test_a_synthetic_outcome_carries_its_own_figures() { /// edited run against an unedited one on the same spec. #[test] fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { - fn run(edit: Option) -> (Reading, u64, u64) { - let mut db = db_for(Callee::Returning); - let mut editor = edit.map(Editor::new); - match &mut editor { - Some(editor) => { - let mut evm = - MegaEvm::new(context_on(&mut db, MegaSpecId::REX6)).with_inspector(editor); - let outcome = evm.execute_transaction(tx()).expect("no EVMError"); - assert_eq!(alloy_evm::Evm::inspector(&evm).fired, 1, "{edit:?} must land"); - let compute = outcome.compute_gas_used; - let destroyed = outcome.compute_gas_destroyed; - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - (reading, compute, destroyed) - } - None => { - let mut evm = MegaEvm::new(context_on(&mut db, MegaSpecId::REX6)); - let outcome = evm.execute_transaction(tx()).expect("no EVMError"); - let compute = outcome.compute_gas_used; - let destroyed = outcome.compute_gas_destroyed; - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - (reading, compute, destroyed) + const REX6: MegaSpecId = MegaSpecId::REX6; + fn run(edit: Option) -> Outcome { + let db = db_for(Callee::Returning); + let limits = EvmTxRuntimeLimits::from_spec(REX6); + match edit { + Some(edit) => { + let mut editor = Editor::new(edit); + let outcome = transact_inspected(REX6, db, limits, &mut editor); + assert_eq!(editor.fired, 1, "{edit:?} must land"); + outcome } + None => transact(REX6, db, limits), } } - let (plain, plain_compute, plain_destroyed) = run(None); - assert!(plain.ledger.is_zero()); + let plain = run(None); + assert!(plain.inspector_ledger.is_zero()); for (edit, expected) in [ ( @@ -751,10 +629,13 @@ fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { }, ), ] { - let (edited, compute, destroyed) = run(Some(edit)); - assert_eq!(edited.ledger, expected, "{edit:?}: the lane reports on every spec"); - assert_eq!(compute, plain_compute, "{edit:?}: a frozen spec's compute total must not move",); - assert_eq!(destroyed, plain_destroyed, "{edit:?}: nor its destroyed lane"); + let edited = run(Some(edit)); + assert_eq!(edited.inspector_ledger, expected, "{edit:?}: the lane reports on every spec"); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "{edit:?}: a frozen spec's compute total must not move", + ); + assert_eq!(edited.destroyed, plain.destroyed, "{edit:?}: nor its destroyed lane"); // `inspector_conjured_gas` is a reading of the ledger rather than something the // transaction recorded, so it moves with the lane on every spec. Every other term is what // a frozen spec must leave alone. @@ -765,7 +646,7 @@ fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { ); assert_eq!( edited.terms.inspector_conjured_gas, - edited.ledger.conjured_gas(), + edited.inspector_ledger.conjured_gas(), "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", ); } From 0166cd4a4c75b4de310524fcf925c5d15be3bcab Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:53:42 +0800 Subject: [PATCH 184/208] test(rex7): route the two refusal files through the shared driver `try_drive` is `drive` for a run the shim may refuse: it reads the tracker before deciding, so a refused run reports the refusals counted and a surviving one still checks the terminal identity. `drive` and `transact_inspected_refused` are now both stated over it. `trusted_observer` keeps its own field-by-field comparison (it compares raw state, which the shared one does not) and loses everything else; 300 -> 215. `frame_init_result_rewrite` keeps its own reading (it is the one file whose runs may produce no receipt) and loses its context, its EVM and its ledger read; 435 -> 413. Assertions 33 -> 33. --- crates/mega-evm/tests/rex7/common.rs | 44 ++++-- .../tests/rex7/frame_init_result_rewrite.rs | 108 ++++++-------- .../mega-evm/tests/rex7/inspector_common.rs | 13 ++ .../mega-evm/tests/rex7/trusted_observer.rs | 135 ++++-------------- 4 files changed, 114 insertions(+), 186 deletions(-) diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index e0b86abd..81814dde 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -194,7 +194,24 @@ where INSP: Inspector>, EXT: ExternalEnvTypes, { - let outcome = evm.execute_transaction(tx).expect("tx should not surface EVMError"); + try_drive(spec, evm, tx) + .unwrap_or_else(|refusal| panic!("tx should not surface EVMError, got {}", refusal.error)) +} + +/// [`drive`] for a run the shim may refuse. +/// +/// A refusal produces no receipt at all, so there is no [`Outcome`] to read and the two numbers a +/// [`Refusal`] carries are the whole of what such a run leaves behind. +pub(crate) fn try_drive<'db, INSP, EXT>( + spec: MegaSpecId, + evm: &mut MegaEvm<&'db mut MemoryDatabase, INSP, EXT>, + tx: MegaTransaction, +) -> Result +where + INSP: Inspector>, + EXT: ExternalEnvTypes, +{ + let executed = evm.execute_transaction(tx); let (detained_compute_gas_limit, terms, tracker_ledger) = { let additional_limit = EvmTr::ctx_ref(evm).additional_limit.borrow(); ( @@ -203,6 +220,15 @@ where additional_limit.inspector_ledger(), ) }; + let outcome = match executed { + Ok(outcome) => outcome, + Err(e) => { + return Err(Refusal { + error: std::format!("{e:?}"), + rejected_rewrites: tracker_ledger.rejected_rewrites, + }) + } + }; assert_eq!( outcome.inspector_ledger, tracker_ledger, "the outcome must report the ledger the shim booked, unchanged", @@ -225,7 +251,7 @@ where state: outcome.result_and_state.state, }; assert_terminal_identity(spec, &outcome); - outcome + Ok(outcome) } /// Runs a single transaction that calls [`CONTRACT`] under `spec` with the given DB and runtime @@ -298,15 +324,11 @@ where I: for<'a> Inspector>, { let mut evm = MegaEvm::new(context(&mut db, spec, limits)).with_inspector(inspector); - let outcome = evm.execute_transaction(call_contract_tx(DEFAULT_TX_GAS_LIMIT)); - let rejected_rewrites = - EvmTr::ctx_ref(&evm).additional_limit.borrow().inspector_ledger().rejected_rewrites; - match outcome { - Ok(outcome) => panic!( - "the run was expected to be refused, but produced {:?}", - outcome.result_and_state.result, - ), - Err(e) => Refusal { error: std::format!("{e:?}"), rejected_rewrites }, + match try_drive(spec, &mut evm, call_contract_tx(DEFAULT_TX_GAS_LIMIT)) { + Ok(outcome) => { + panic!("the run was expected to be refused, but produced {:?}", outcome.result) + } + Err(refusal) => refusal, } } diff --git a/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs b/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs index 51c12d90..8c67d295 100644 --- a/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs +++ b/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs @@ -17,20 +17,21 @@ //! that honours the rewrite reports the two halves that disagree rather than only the missing //! counter. -use crate::common::{CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}; +use crate::{ + common::{base_db, context, try_drive, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}, + inspector_common::append_call, +}; use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall as _; use mega_evm::{ alloy_consensus::{Signed, TxLegacy}, test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, - MegaSpecId, MegaTransaction, MegaTransactionNew as _, MegaTransactionOutcome, TestExternalEnvs, - KEYLESS_DEPLOY_ADDRESS, + MegaSpecId, MegaTransaction, MegaTransactionNew as _, KEYLESS_DEPLOY_ADDRESS, }; use revm::{ - bytecode::opcode::{CALL, RETURN, SSTORE, STOP}, + bytecode::opcode::{RETURN, SSTORE, STOP}, context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr}, - handler::EvmTr, interpreter::{ CallInputs, CallOutcome, Gas, InstructionResult, InterpreterResult, InterpreterTypes, }, @@ -52,7 +53,7 @@ const RELAYER: Address = address!("0000000000000000000000000000000000340009"); const FLAG_SLOT: U256 = U256::from_limbs([7, 0, 0, 0]); /// The wei the value-transferring cases send. -const SENT: u128 = 1; +const SENT: u64 = 1; /// What one run produced, in the shape the splits are asserted over. struct Reading { @@ -119,35 +120,40 @@ where } } -/// Runs `tx` under REX7 with `inspector` attached. -fn run(mut db: MemoryDatabase, tx: MegaTransaction, inspector: &mut I) -> Reading +/// Runs `tx` under `spec` with `inspector` attached. +fn run_on( + spec: MegaSpecId, + mut db: MemoryDatabase, + tx: MegaTransaction, + inspector: &mut I, +) -> Reading where I: for<'a> Inspector>, { - let mut context = MegaContext::new(&mut db, MegaSpecId::REX7) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); - 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(inspector); - let outcome: Result = evm.execute_transaction(tx); - let rejected_rewrites = - evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites; - match outcome { + let limits = EvmTxRuntimeLimits::from_spec(spec); + let mut evm = MegaEvm::new(context(&mut db, spec, limits)).with_inspector(inspector); + match try_drive(spec, &mut evm, tx) { Ok(outcome) => Reading { - result: Ok(outcome.result_and_state.result), - rejected_rewrites, - state: outcome.result_and_state.state, + result: Ok(outcome.result), + rejected_rewrites: outcome.inspector_ledger.rejected_rewrites, + state: outcome.state, }, - Err(e) => Reading { - result: Err(std::format!("{e:?}")), - rejected_rewrites, + Err(refusal) => Reading { + result: Err(refusal.error), + rejected_rewrites: refusal.rejected_rewrites, state: EvmState::default(), }, } } +/// [`run_on`] under REX7, which is where every rewrite below is refused. +fn run(db: MemoryDatabase, tx: MegaTransaction, inspector: &mut I) -> Reading +where + I: for<'a> Inspector>, +{ + run_on(MegaSpecId::REX7, db, tx, inspector) +} + /// The two facts every case here pins: the rewrite was counted as refused, and the transaction /// failed with an error rather than reporting a receipt built on it. fn assert_refused(reading: &Reading) { @@ -172,29 +178,14 @@ fn call_tx(to: Address) -> MegaTransaction { /// /// The recorded flag is what makes the split visible: it is the answer the *caller* was given, /// which the state the call left behind has to agree with. -fn calls_and_records(target: Address, gas: u64, value: u128) -> Bytes { - BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(value) - .push_address(target) - .push_number(u128::from(gas)) - .append(CALL) +fn calls_and_records(target: Address, gas: u64, value: u64) -> Bytes { + append_call(BytecodeBuilder::default(), target, gas, value) .push_u256(FLAG_SLOT) .append(SSTORE) .append(STOP) .build() } -fn caller_db(code: Bytes) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) -} - /// A value-transferring `CALL` into an empty-code account, rewritten from its `Stop` into a /// revert. /// @@ -206,7 +197,7 @@ fn caller_db(code: Bytes) -> MemoryDatabase { fn test_rewriting_an_empty_code_call_into_a_revert_is_refused() { let mut inspector = RewriteInitResult::new(EMPTY_TARGET, InstructionResult::Revert); let reading = run( - caller_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)), + base_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)), call_tx(CONTRACT), &mut inspector, ); @@ -233,7 +224,7 @@ fn test_reviving_a_failed_precompile_call_is_refused() { // the precompile is reached and cannot pay. let mut inspector = RewriteInitResult::new(ECRECOVER, InstructionResult::Stop); let reading = - run(caller_db(calls_and_records(ECRECOVER, 0, SENT)), call_tx(CONTRACT), &mut inspector); + run(base_db(calls_and_records(ECRECOVER, 0, SENT)), call_tx(CONTRACT), &mut inspector); assert_eq!(inspector.fired, 1, "the fixture must reach the callback exactly once"); assert!( !reading.succeeded(), @@ -380,7 +371,7 @@ where fn test_rewriting_an_inspector_s_own_synthetic_outcome_is_supported() { let mut inspector = AnswerThenRewrite { target: CALLEE, answered: 0, rewrote: 0 }; let reading = run( - caller_db(calls_and_records(CALLEE, 200_000, 0)).account_code( + base_db(calls_and_records(CALLEE, 200_000, 0)).account_code( CALLEE, BytecodeBuilder::default().sstore(U256::from(1), U256::from(7)).stop().build(), ), @@ -410,26 +401,13 @@ fn test_rewriting_an_inspector_s_own_synthetic_outcome_is_supported() { #[test] fn test_the_frozen_spec_refuses_nothing() { let mut inspector = RewriteInitResult::new(EMPTY_TARGET, InstructionResult::Revert); - let mut db = caller_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)); - let mut context = MegaContext::new(&mut db, MegaSpecId::REX6) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6)); - 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(&mut inspector); - let outcome = evm.execute_transaction(call_tx(CONTRACT)).expect("REX6 must not refuse"); - assert_eq!( - evm.ctx_ref().additional_limit.borrow().inspector_ledger().rejected_rewrites, - 0, - "a frozen spec refuses nothing", + let reading = run_on( + MegaSpecId::REX6, + base_db(calls_and_records(EMPTY_TARGET, 200_000, SENT)), + call_tx(CONTRACT), + &mut inspector, ); - assert!(outcome.result_and_state.result.is_success(), "the frozen run must still succeed"); -} -/// Silences the unused-import warning the external-env type would otherwise carry when only the -/// empty environment is used above. -#[allow(dead_code)] -fn _envs() -> TestExternalEnvs { - TestExternalEnvs::default() + assert_eq!(reading.rejected_rewrites, 0, "a frozen spec refuses nothing"); + assert!(reading.succeeded(), "the frozen run must still succeed, got {:?}", reading.result); } diff --git a/crates/mega-evm/tests/rex7/inspector_common.rs b/crates/mega-evm/tests/rex7/inspector_common.rs index 64272001..366287ce 100644 --- a/crates/mega-evm/tests/rex7/inspector_common.rs +++ b/crates/mega-evm/tests/rex7/inspector_common.rs @@ -119,6 +119,19 @@ where (plain, cheated) } +/// [`crate::common::transact_inspected`] with the shim delegating on the strength of a +/// `TrustedObserver` declaration instead of measuring. +pub(crate) fn transact_trusted(db: MemoryDatabase, inspector: &mut I) -> Outcome +where + I: for<'a> Inspector> + + mega_evm::TrustedObserver, +{ + let mut db = db; + let mut evm = MegaEvm::new(context(&mut db, MegaSpecId::REX7, limits())) + .with_trusted_inspector(inspector); + crate::common::drive(MegaSpecId::REX7, &mut evm, call_contract_tx(DEFAULT_TX_GAS_LIMIT)) +} + // --- ledgers ------------------------------------------------------------------------------- /// The ledger of a rewrite that moved gas on exactly one lane. diff --git a/crates/mega-evm/tests/rex7/trusted_observer.rs b/crates/mega-evm/tests/rex7/trusted_observer.rs index 76bb1144..94012286 100644 --- a/crates/mega-evm/tests/rex7/trusted_observer.rs +++ b/crates/mega-evm/tests/rex7/trusted_observer.rs @@ -20,48 +20,29 @@ //! runs exercise. `cargo test -p mega-evm --release --test rex7` is where the comparison is //! actually against the fast path, and it is an acceptance gate for that reason. -use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; -use alloy_primitives::{Bytes, U256}; +use crate::{ + common::{transact, transact_inspected, Outcome, CALLEE}, + inspector_common::{append_call, db_with_callee, limits, transact_trusted}, +}; +use alloy_primitives::U256; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - EmptyExternalEnv, InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, - MegaTransaction, MegaTransactionNew as _, TrustedObserver, + MegaSpecId, TrustedObserver, }; use revm::{ - bytecode::opcode::{CALL, POP, STOP}, - context::{result::ExecutionResult, tx::TxEnvBuilder}, + bytecode::opcode::{POP, STOP}, interpreter::{CallInputs, CallOutcome, Interpreter, InterpreterTypes}, - state::EvmState, Inspector, }; -/// Transaction gas limit used throughout: high enough that EVM gas is never what binds. -const TX_GAS_LIMIT: u64 = 100_000_000; - -/// Everything one run of the fixture produces, for the three-way comparison. -#[derive(Debug)] -struct Reading { - result: ExecutionResult, - compute_gas: u64, - enforced: u64, - destroyed: u64, - data_size: u64, - kv_updates: u64, - state_growth: u64, - gas_used: u64, - total_gas_spent: u64, - ledger: InspectorLedger, - state: EvmState, -} - /// Asserts two runs of the fixture are the same run, field by field. /// /// Written out rather than derived from `PartialEq` on the whole struct so that the field that /// disagrees is the one the failure names. -fn assert_same(label: &str, left: &Reading, right: &Reading) { +fn assert_same(label: &str, left: &Outcome, right: &Outcome) { assert_eq!(format!("{:?}", left.result), format!("{:?}", right.result), "{label}: result"); assert_eq!(left.compute_gas, right.compute_gas, "{label}: compute gas"); - assert_eq!(left.enforced, right.enforced, "{label}: enforced compute gas"); + assert_eq!(left.enforced(), right.enforced(), "{label}: enforced compute gas"); assert_eq!(left.destroyed, right.destroyed, "{label}: destroyed compute gas"); assert_eq!(left.data_size, right.data_size, "{label}: data size"); assert_eq!(left.kv_updates, right.kv_updates, "{label}: kv updates"); @@ -71,65 +52,6 @@ fn assert_same(label: &str, left: &Reading, right: &Reading) { assert_eq!(left.state, right.state, "{label}: produced state"); } -fn tx() -> MegaTransaction { - let mut tx = MegaTransaction::new( - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), - ); - tx.enveloped_tx = Some(Bytes::new()); - tx -} - -fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = MegaContext::new(db, MegaSpecId::REX7); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context -} - -fn read(outcome: mega_evm::MegaTransactionOutcome) -> Reading { - let gas_used = outcome.result_and_state.result.tx_gas_used(); - let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); - Reading { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - enforced: outcome.compute_gas_enforced, - destroyed: outcome.compute_gas_destroyed, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - total_gas_spent, - ledger: outcome.inspector_ledger, - state: outcome.result_and_state.state, - } -} - -/// No inspector at all: revm's plain frame loops, which never call one. -fn transact_plain(mut db: MemoryDatabase) -> Reading { - let mut evm = MegaEvm::new(context(&mut db)); - read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) -} - -/// The inspected loop with the shim measuring, which is what every undeclared inspector gets. -fn transact_measured(mut db: MemoryDatabase, inspector: &mut I) -> Reading -where - I: for<'a> Inspector>, -{ - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(inspector); - read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) -} - -/// The inspected loop with the shim delegating on the strength of the declaration. -fn transact_trusted(mut db: MemoryDatabase, inspector: &mut I) -> Reading -where - I: for<'a> Inspector> + TrustedObserver, -{ - let mut evm = MegaEvm::new(context(&mut db)).with_trusted_inspector(inspector); - read(evm.execute_transaction(tx()).expect("tx should not surface EVMError")) -} - /// Counts the callbacks it is handed and changes nothing — a declaration that holds. #[derive(Default, Debug, PartialEq, Eq)] struct Observer { @@ -170,24 +92,16 @@ impl Inspector for Observer { fn fixture_db() -> MemoryDatabase { let callee = BytecodeBuilder::default().sstore(U256::from(0x11), U256::from(0x22)).append(STOP).build(); - let code = BytecodeBuilder::default() - .sstore(U256::from(0x20), U256::from(0x99)) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, callee) + let code = append_call( + BytecodeBuilder::default().sstore(U256::from(0x20), U256::from(0x99)), + CALLEE, + 100_000, + 0, + ) + .append(POP) + .append(STOP) + .build(); + db_with_callee(code, callee) } /// ★ Declaring an observer read-only changes what the measurement costs and nothing it says. @@ -201,10 +115,11 @@ fn fixture_db() -> MemoryDatabase { /// inspector": a fast path that delegated less would pass every other assertion here. #[test] fn test_a_declared_observer_runs_the_transaction_the_other_two_runs_produce() { - let plain = transact_plain(fixture_db()); + let plain = transact(MegaSpecId::REX7, fixture_db(), limits()); let mut measured_observer = Observer::default(); - let measured = transact_measured(fixture_db(), &mut measured_observer); + let measured = + transact_inspected(MegaSpecId::REX7, fixture_db(), limits(), &mut measured_observer); let mut trusted_observer = Observer::default(); let trusted = transact_trusted(fixture_db(), &mut trusted_observer); @@ -216,11 +131,11 @@ fn test_a_declared_observer_runs_the_transaction_the_other_two_runs_produce() { "the declared run must be handed the same callbacks as the measured one", ); - assert!(measured.ledger.is_zero(), "measured: {:?}", measured.ledger); + assert!(measured.inspector_ledger.is_zero(), "measured: {:?}", measured.inspector_ledger); assert!( - trusted.ledger.is_zero(), + trusted.inspector_ledger.is_zero(), "the fast path books nothing by construction: {:?}", - trusted.ledger, + trusted.inspector_ledger, ); assert_same("declared against uninspected", &trusted, &plain); From 0a073615464c306490b608da87a2a53ece7f2132 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 20:58:37 +0800 Subject: [PATCH 185/208] test(rex7): make the cheat matrix's grid data and its axes derived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things were written out that a machine can hold instead. The two axes listed their own variants a second time in an `ALL` array, so a variant added without touching the array would have shrunk the exhaustive sweep silently; `axis!` derives the list from the declaration. Every cell was a five-argument `push` spread over seven lines, of which two arguments were the same value in all but eleven cells; `cell!` takes the three that vary and one line per cell. The reading, driver and identity are the shared ones, and the state comparison is now `state_view` rather than the file's own rendering — strictly more fields, including the deployed code and the status flags. 1851 -> 1448 lines, 66 -> 60 assertions (0 removed, 6 deduped). The grid is unchanged and provably so: 91 covered pairs and 197 excused ones still sum to the 288 the two axes span, with no pair both covered and excused. --- .../tests/rex7/inspector_cheat_matrix.rs | 737 ++++-------------- 1 file changed, 167 insertions(+), 570 deletions(-) diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index 338f1694..d90d18a5 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -33,24 +33,31 @@ //! transaction, and no callback — every sampled cell must then be bit-identical to a run with no //! inspector attached, which is what says the shim itself contributes nothing. -use crate::common::{CALLEE, CALLER, CONTRACT, ONE_ETH}; +use crate::{ + common::{ + base_db, call_contract_tx, context, drive, state_view, transact, Outcome, CALLEE, CALLER, + CONTRACT, ONE_ETH, + }, + inspector_common::{ + assert_refused, call_then_stop, db_with_callee, deploy_then_stop, ledger_env, ledger_gas, + ledger_intervention, ledger_refund, ledger_reservoir, ledger_result, ledger_state_gas, + limits, plain_and_cheated, try_transact_inspected, REVERTING_INIT_CODE, REVIVED_CREATION, + }, +}; use alloy_primitives::{Address, Bytes, Log, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - AdditionalLimit, ConservationTerms, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, - Lane, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, - MegaTransactionNew as _, MegaTransactionOutcome, + InspectorLedger, Lane, MegaEvm, MegaSpecId, MegaTransactionNew as _, }; use revm::{ bytecode::opcode::{CALL, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, SSTORE, STOP}, - context::{result::ExecutionResult, tx::TxEnvBuilder, Cfg, ContextTr, JournalTr}, - handler::{EvmTr, FrameResult}, + context::{Cfg, ContextTr, JournalTr}, + handler::FrameResult, interpreter::{ interpreter_types::{Jumps, LoopControl, MemoryTr, StackTr}, CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, }, - state::EvmState, Inspector, }; use std::{collections::BTreeMap, string::String, vec::Vec}; @@ -102,6 +109,25 @@ fn deployed_address() -> Address { CONTRACT.create(0) } +/// Declares one axis of the matrix, with the list of its members derived from the declaration. +/// +/// The two axes are swept exhaustively by `test_the_matrix_leaves_no_cell_unaccounted`, so a +/// variant added without a corresponding entry in `ALL` would silently shrink the sweep instead of +/// failing. Deriving the list removes that possibility. +macro_rules! axis { + ( + $(#[$meta:meta])* + enum $name:ident { $($(#[$vmeta:meta])* $variant:ident,)* } + ) => { + $(#[$meta])* + enum $name { $($(#[$vmeta])* $variant,)* } + + impl $name { + const ALL: &'static [Self] = &[$(Self::$variant,)*]; + } + }; +} + // --- rows and columns ----------------------------------------------------------------------- /// One row of the matrix: a callback on the `Inspector` trait. @@ -110,6 +136,7 @@ fn deployed_address() -> Address { /// reached only when a precompile's logs are forwarded, which no wired `MegaETH` precompile /// produces — and [`inapplicable`] carries that, together with the reason its every column is /// empty anyway. +axis! { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum At { InitializeInterp, @@ -125,22 +152,6 @@ enum At { CreateEnd, Selfdestruct, } - -impl At { - const ALL: [Self; 12] = [ - Self::InitializeInterp, - Self::Step, - Self::StepEnd, - Self::Log, - Self::LogFull, - Self::FrameStart, - Self::FrameEnd, - Self::Call, - Self::CallEnd, - Self::Create, - Self::CreateEnd, - Self::Selfdestruct, - ]; } /// One column of the matrix: a shape a rewrite can take. @@ -153,6 +164,7 @@ impl At { /// The EIP-8037 dimensions are the one place that pairing does not apply: `MegaETH` runs with the /// EIP off, so a `Gas` reaches every callback with both of its state-gas figures at zero and there /// is nothing to lower. +axis! { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum Shape { /// Write gas into a live interpreter's counter. @@ -215,35 +227,9 @@ enum Shape { /// Write to the journal directly, behind the EVM's back. JournalWrite, } +} impl Shape { - const ALL: [Self; 24] = [ - Self::InjectGas, - Self::DrainGas, - Self::RaiseEnvelope, - Self::LowerEnvelope, - Self::EditInput, - Self::Intercept, - Self::RaiseInterceptionGas, - Self::LowerInterceptionGas, - Self::RaiseResultGas, - Self::LowerResultGas, - Self::FailResult, - Self::ReviveResult, - Self::RaiseActionResultGas, - Self::LowerActionResultGas, - Self::RaiseActionEnvelope, - Self::LowerActionEnvelope, - Self::RaiseRefund, - Self::LowerRefund, - Self::WriteReservoir, - Self::WriteStateGas, - Self::EditStackOrMemory, - Self::GrowMemoryFree, - Self::EditOutcomeMetadata, - Self::JournalWrite, - ]; - /// Whether this shape answers the frame with a synthetic outcome instead of letting the EVM /// build it. const fn is_interception(self) -> bool { @@ -991,162 +977,35 @@ fn callee_code(fixture: Fixture) -> Bytes { } fn db_for(fixture: Fixture) -> MemoryDatabase { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, caller_code()) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, callee_code(fixture)) + db_with_callee(caller_code(), callee_code(fixture)) } // --- running one cell ----------------------------------------------------------------------- -/// Everything one transaction reports, plus what the shim booked for it. -struct Reading { - result: ExecutionResult, - compute_gas: u64, - enforced: u64, - destroyed: u64, - data_size: u64, - kv_updates: u64, - state_growth: u64, - gas_used: u64, - total_gas_spent: u64, - terms: ConservationTerms, - ledger: InspectorLedger, - state: EvmState, -} - -impl Reading { - /// A storage slot as the produced state has it, zero when the transaction never wrote it. - fn slot(&self, address: Address, slot: u64) -> U256 { - self.state - .get(&address) - .and_then(|account| account.storage.get(&U256::from(slot))) - .map(|value| value.present_value()) - .unwrap_or_default() - } - - /// Whether the fixture's `CREATE` left code behind. - fn deployed(&self) -> bool { - self.state - .get(&deployed_address()) - .is_some_and(|account| !account.info.is_empty_code_hash()) - } - - /// The produced state, rendered order-independently for a bit-for-bit comparison. - fn render_state(&self) -> String { - let canonical: BTreeMap = self - .state - .iter() - .map(|(address, account)| { - let storage: BTreeMap = account - .storage - .iter() - .map(|(slot, value)| (*slot, value.present_value())) - .collect(); - ( - *address, - std::format!( - "{:?}/{}/{:?}/{storage:?}", - account.info.balance, - account.info.nonce, - account.info.code_hash - ), - ) - }) - .collect(); - std::format!("{canonical:?}") - } -} - -/// The conservation identity, stated with the term the measurement shim contributes. -/// -/// This is the assertion that goes red when a lane the shim should have booked went unbooked: the -/// two sides then disagree by precisely the unbooked amount. -fn assert_identity(label: &str, r: &Reading) { - assert_eq!( - r.compute_gas, - r.enforced + r.destroyed, - "{label}: reported compute must split into enforced + destroyed", - ); - assert_eq!( - r.terms.inspector_conjured_gas, - r.ledger.conjured_gas(), - "{label}: the law's `I` term is the ledger's net, and nothing else", - ); - assert_eq!( - r.terms.envelope_for(r.destroyed), - i128::from(r.total_gas_spent), - "{label}: the law must close against the envelope the receipt reports; \ - reported compute={} destroyed={} envelope={} ({})", - r.compute_gas, - r.destroyed, - r.total_gas_spent, - r.terms, - ); -} - -fn tx() -> MegaTransaction { - let mut tx = MegaTransaction::new( - TxEnvBuilder::default().caller(CALLER).call(CONTRACT).gas_limit(TX_GAS_LIMIT).build_fill(), - ); - tx.enveloped_tx = Some(Bytes::new()); - tx +/// A storage slot as the produced state has it, taking the slot as the small integer the fixtures +/// use. +fn slot(outcome: &Outcome, address: Address, slot: u64) -> U256 { + outcome.storage_value(address, U256::from(slot)) } -fn context(db: &mut MemoryDatabase) -> MegaContext<&mut MemoryDatabase, EmptyExternalEnv> { - let mut context = MegaContext::new(db, MegaSpecId::REX7) - .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)); - context.modify_chain(|chain| { - chain.operator_fee_scalar = Some(U256::ZERO); - chain.operator_fee_constant = Some(U256::ZERO); - }); - context -} - -fn read(limit: &AdditionalLimit, outcome: MegaTransactionOutcome) -> Reading { - assert_eq!( - outcome.inspector_ledger, - limit.inspector_ledger(), - "the outcome must report the ledger the shim booked, unchanged", - ); - let gas_used = outcome.result_and_state.result.tx_gas_used(); - let total_gas_spent = outcome.result_and_state.result.gas().total_gas_spent(); - Reading { - result: outcome.result_and_state.result, - compute_gas: outcome.compute_gas_used, - enforced: outcome.compute_gas_enforced, - destroyed: outcome.compute_gas_destroyed, - data_size: outcome.data_size, - kv_updates: outcome.kv_updates, - state_growth: outcome.state_growth_used, - gas_used, - total_gas_spent, - terms: limit.conservation_terms(), - ledger: outcome.inspector_ledger, - state: outcome.result_and_state.state, - } +/// Whether the fixture's `CREATE` left code behind. +fn deployed(outcome: &Outcome) -> bool { + outcome.state.get(&deployed_address()).is_some_and(|account| !account.info.is_empty_code_hash()) } /// Runs the fixture with no inspector at all. -fn transact_plain(fixture: Fixture) -> Reading { - let mut db = db_for(fixture); - let mut evm = MegaEvm::new(context(&mut db)); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading +fn transact_plain(fixture: Fixture) -> Outcome { + transact(MegaSpecId::REX7, db_for(fixture), limits()) } /// Runs the fixture with `cheat` attached, on the inspected loops or with them switched off. -fn transact_cheating(fixture: Fixture, cheat: &mut Cheat, inspected: bool) -> Reading { +fn transact_cheating(fixture: Fixture, cheat: &mut Cheat, inspected: bool) -> Outcome { let mut db = db_for(fixture); - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(cheat); + let mut evm = MegaEvm::new(context(&mut db, MegaSpecId::REX7, limits())).with_inspector(cheat); if !inspected { alloy_evm::Evm::set_inspector_enabled(&mut evm, false); } - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading + drive(MegaSpecId::REX7, &mut evm, call_contract_tx(TX_GAS_LIMIT)) } // --- the matrix ----------------------------------------------------------------------------- @@ -1159,100 +1018,73 @@ struct Cell { /// The ledger the shim must have booked, exactly. ledger: InspectorLedger, /// What the produced state must show, given that the cheat landed. - state: fn(&Reading, &str), -} - -fn ledger_gas(gas: i128) -> InspectorLedger { - InspectorLedger { gas: Lane::once(gas), ..InspectorLedger::default() } -} - -fn ledger_env(env: i128) -> InspectorLedger { - InspectorLedger { env: Lane::once(env), ..InspectorLedger::default() } -} - -fn ledger_result(result: i128) -> InspectorLedger { - InspectorLedger { result: Lane::once(result), ..InspectorLedger::default() } -} - -/// The ledger a rewrite of one of the receipt's other two numbers books. -/// -/// Separate helpers rather than one, because which of the three figures a shape moves is exactly -/// what decides whether the conservation law can see it: only the pool is a term of it. -fn ledger_refund(refund: i128) -> InspectorLedger { - InspectorLedger { refund: Lane::once(refund), ..InspectorLedger::default() } + state: fn(&Outcome, &str), } -fn ledger_reservoir(reservoir: i128) -> InspectorLedger { - InspectorLedger { reservoir: Lane::once(reservoir), ..InspectorLedger::default() } -} - -fn ledger_state_gas(state_gas: i128) -> InspectorLedger { - InspectorLedger { state_gas: Lane::once(state_gas), ..InspectorLedger::default() } -} - -/// The ledger of a rewrite that moves no gas: the shim saw the argument it was handed come back -/// changed, and that is the whole of what it books. -/// -/// These are the cells that would otherwise be indistinguishable from an observation-only run, and -/// the reason the canonical block path could not tell them apart before this lane existed. -fn ledger_intervention() -> InspectorLedger { - InspectorLedger { interventions: 1, ..InspectorLedger::default() } +/// A rewrite that moved gas *and* came back changed in something the shim compares. +fn plus_intervention(mut ledger: InspectorLedger) -> InspectorLedger { + ledger.interventions += 1; + ledger } /// The fixture ran to its end and every frame committed: the callee's write, the deployment, and /// the top frame's own write are all there. -fn state_all_committed(r: &Reading, label: &str) { +fn state_all_committed(r: &Outcome, label: &str) { assert!( r.result.is_success(), "{label}: expected a successful transaction, got {:?}", r.result ); - assert_eq!(r.slot(CALLEE, CALLEE_SLOT), U256::from(STORED), "{label}: the callee's write"); - assert!(r.deployed(), "{label}: the fixture's CREATE must have deployed code"); - assert_eq!(r.slot(CONTRACT, TOP_SLOT), U256::from(STORED), "{label}: the top frame's write"); + assert_eq!(slot(r, CALLEE, CALLEE_SLOT), U256::from(STORED), "{label}: the callee's write"); + assert!(deployed(r), "{label}: the fixture's CREATE must have deployed code"); + assert_eq!(slot(r, CONTRACT, TOP_SLOT), U256::from(STORED), "{label}: the top frame's write"); } /// The callee's frame did not commit: whatever ended it, its write is gone. -fn state_callee_write_rolled_back(r: &Reading, label: &str) { +fn state_callee_write_rolled_back(r: &Outcome, label: &str) { assert!( r.result.is_success(), "{label}: the caller absorbs the inner failure, got {:?}", r.result ); assert_eq!( - r.slot(CALLEE, CALLEE_SLOT), + slot(r, CALLEE, CALLEE_SLOT), U256::ZERO, "{label}: a frame the caller was told failed must leave no write behind", ); - assert!(r.deployed(), "{label}: the rest of the transaction must be unaffected"); + assert!(deployed(r), "{label}: the rest of the transaction must be unaffected"); } /// The callee's frame committed a write the EVM had decided to roll back — the journal followed /// the rewritten result rather than the classification. -fn state_callee_write_revived(r: &Reading, label: &str) { +fn state_callee_write_revived(r: &Outcome, label: &str) { assert!(r.result.is_success(), "{label}: {:?}", r.result); assert_eq!( - r.slot(CALLEE, CALLEE_SLOT), + slot(r, CALLEE, CALLEE_SLOT), U256::from(STORED), "{label}: a reverted frame rewritten into a success must have its state committed with it", ); } /// The stack edit landed on the callee's `SSTORE` operand. -fn state_callee_write_bumped(r: &Reading, label: &str) { +fn state_callee_write_bumped(r: &Outcome, label: &str) { assert!(r.result.is_success(), "{label}: {:?}", r.result); assert_eq!( - r.slot(CALLEE, CALLEE_SLOT), + slot(r, CALLEE, CALLEE_SLOT), U256::from(STORED + 1), "{label}: the value the inspector put on the stack is the value the EVM wrote", ); } /// The creation did not happen. -fn state_no_deployment(r: &Reading, label: &str) { +fn state_no_deployment(r: &Outcome, label: &str) { assert!(r.result.is_success(), "{label}: the caller absorbs it, got {:?}", r.result); - assert!(!r.deployed(), "{label}: no code may be deployed"); - assert_eq!(r.slot(deployed_address(), INIT_SLOT), U256::ZERO, "{label}: nor its storage write"); + assert!(!deployed(r), "{label}: no code may be deployed"); + assert_eq!( + slot(r, deployed_address(), INIT_SLOT), + U256::ZERO, + "{label}: nor its storage write" + ); } /// Every cell the matrix covers. @@ -1261,240 +1093,112 @@ fn matrix() -> Vec { use Shape::*; let mut cells = Vec::new(); - let mut push = |at, shape, fixture, ledger, state: fn(&Reading, &str)| { - cells.push(Cell { at, shape, fixture, ledger, state }); - }; + /// One cell of the matrix, as a data row. + /// + /// The fixture defaults to the returning callee and the state check to "every frame + /// committed", because that is what a cell wants unless the rewrite it makes is one that + /// changes what the transaction leaves behind. + macro_rules! cell { + ($at:expr, $shape:expr, $ledger:expr $(, $fixture:expr, $state:expr)?) => {{ + #[allow(unused_mut, unused_assignments)] + let mut fixture = Fixture::ReturningCallee; + #[allow(unused_mut, unused_assignments)] + let mut state: fn(&Outcome, &str) = state_all_committed; + $( + fixture = $fixture; + state = $state; + )? + cells.push(Cell { at: $at, shape: $shape, fixture, ledger: $ledger, state }); + }}; + } // The four callbacks that are handed a live interpreter. for at in [InitializeInterp, Step, StepEnd, LogFull] { - push( - at, - InjectGas, - Fixture::ReturningCallee, - ledger_gas(i128::from(INJECT)), - state_all_committed, - ); - push( - at, - DrainGas, - Fixture::ReturningCallee, - ledger_gas(-i128::from(DRAIN)), - state_all_committed, - ); + cell!(at, InjectGas, ledger_gas(i128::from(INJECT))); + cell!(at, DrainGas, ledger_gas(-i128::from(DRAIN))); // The two halves of the working-state column. `step` fires on an `SSTORE`'s operands and // pops and pushes the same two words, so both sizes come back where they were and nothing // is booked — the contents rewrite that has no lane. The other three rows run where there // is no operand to swap, so the cheat leaves a word on the stack instead, and a stack that // came back one word longer is a constant-time reading the shim takes. - push( + cell!( at, EditStackOrMemory, + if at == InitializeInterp { ledger_intervention() } else { InspectorLedger::default() }, Fixture::ReturningCallee, - if at == At::InitializeInterp { - ledger_intervention() - } else { - InspectorLedger::default() - }, - if at == Step { state_callee_write_bumped } else { state_all_committed }, + if at == Step { state_callee_write_bumped } else { state_all_committed } ); // Growing the memory and its memo together leaves every interpreter invariant intact and // still skips the next expansion's charge, so no gas lane sees it and the intervention // counter must. - push( - at, - GrowMemoryFree, - Fixture::ReturningCallee, - ledger_intervention(), - state_all_committed, - ); - push( - at, - JournalWrite, - Fixture::ReturningCallee, - InspectorLedger::default(), - state_all_committed, - ); + cell!(at, GrowMemoryFree, ledger_intervention()); + cell!(at, JournalWrite, InspectorLedger::default()); // The receipt's other two numbers, reached through the same `Gas` as the counter above. - push( - at, - RaiseRefund, - Fixture::ReturningCallee, - ledger_refund(i128::from(REFUND)), - state_all_committed, - ); + cell!(at, RaiseRefund, ledger_refund(i128::from(REFUND))); if at != InitializeInterp { - push( - at, - LowerRefund, - Fixture::ReturningCallee, - ledger_refund(-i128::from(REFUND)), - state_all_committed, - ); + cell!(at, LowerRefund, ledger_refund(-i128::from(REFUND))); } - push( - at, - WriteReservoir, - Fixture::ReturningCallee, - ledger_reservoir(i128::from(RESERVOIR)), - state_all_committed, - ); - push( - at, - WriteStateGas, - Fixture::ReturningCallee, - ledger_state_gas(i128::from(STATE_GAS)), - state_all_committed, - ); + cell!(at, WriteReservoir, ledger_reservoir(i128::from(RESERVOIR))); + cell!(at, WriteStateGas, ledger_state_gas(i128::from(STATE_GAS))); } // The pending action, which only `step_end` ever sees: revm's inspected loop breaks out the // moment one is set, so it is the one callback that runs with an instruction's action already // in place. Which lane the edit lands on is decided by the action's own variant — a `Return` // action is what the frame hands back, a `NewFrame` action is what the child is built with. - push( - StepEnd, - RaiseActionResultGas, - Fixture::ReturningCallee, - ledger_result(i128::from(ACTION)), - state_all_committed, - ); - push( - StepEnd, - LowerActionResultGas, - Fixture::ReturningCallee, - ledger_result(-i128::from(ACTION)), - state_all_committed, - ); - push( - StepEnd, - RaiseActionEnvelope, - Fixture::ReturningCallee, - ledger_env(i128::from(ACTION)), - state_all_committed, - ); - push( - StepEnd, - LowerActionEnvelope, - Fixture::ReturningCallee, - ledger_env(-i128::from(ACTION)), - state_all_committed, - ); + cell!(StepEnd, RaiseActionResultGas, ledger_result(i128::from(ACTION))); + cell!(StepEnd, LowerActionResultGas, ledger_result(-i128::from(ACTION))); + cell!(StepEnd, RaiseActionEnvelope, ledger_env(i128::from(ACTION))); + cell!(StepEnd, LowerActionEnvelope, ledger_env(-i128::from(ACTION))); // The three callbacks that are handed a frame's inputs before the frame is built. `create` // reaches the fixture's `CREATE`; the other two reach its inner `CALL`. for at in [FrameStart, Call, Create] { - let deployment_side = at == Create; - push( - at, - RaiseEnvelope, - Fixture::ReturningCallee, - ledger_env(i128::from(ENVELOPE)), - state_all_committed, - ); - push( - at, - LowerEnvelope, - Fixture::ReturningCallee, - ledger_env(-i128::from(ENVELOPE)), - state_all_committed, - ); - push( - at, - EditInput, - Fixture::ReturningCallee, - ledger_intervention(), - if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, - ); - push( - at, - Intercept, - Fixture::ReturningCallee, - ledger_intervention(), - if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, - ); + // A rewrite of what the frame will *do* is the one that changes what it leaves behind, and + // the creation side of the sweep leaves no deployment where the call side leaves no write. + let undone: fn(&Outcome, &str) = + if at == Create { state_no_deployment } else { state_callee_write_rolled_back }; + cell!(at, RaiseEnvelope, ledger_env(i128::from(ENVELOPE))); + cell!(at, LowerEnvelope, ledger_env(-i128::from(ENVELOPE))); + cell!(at, EditInput, ledger_intervention(), Fixture::ReturningCallee, undone); + cell!(at, Intercept, ledger_intervention(), Fixture::ReturningCallee, undone); // The same interception, sized against the envelope rather than echoing it. No frame is // built, so the whole of what the outcome hands back is the inspector's number, and the // difference from what the caller forwarded is what the ledger has to carry. - push( - at, - RaiseInterceptionGas, - Fixture::ReturningCallee, - InspectorLedger { - result: Lane::once(i128::from(INTERCEPTION)), - interventions: 1, - ..InspectorLedger::default() - }, - if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, - ); - push( - at, - LowerInterceptionGas, - Fixture::ReturningCallee, - InspectorLedger { - result: Lane::once(-i128::from(INTERCEPTION)), - interventions: 1, - ..InspectorLedger::default() - }, - if deployment_side { state_no_deployment } else { state_callee_write_rolled_back }, - ); - push( - at, - JournalWrite, - Fixture::ReturningCallee, - InspectorLedger::default(), - state_all_committed, - ); - if !deployment_side { - // The pool a call's inputs seed the child with. It travels to the child and back, so - // it is booked as gas — and the inputs came back changed in a field the envelope lane - // does not cover, which the rewrite comparison books separately. - push( + for (shape, sign) in [(RaiseInterceptionGas, 1i128), (LowerInterceptionGas, -1)] { + cell!( at, - WriteReservoir, + shape, + plus_intervention(ledger_result(sign * i128::from(INTERCEPTION))), Fixture::ReturningCallee, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - interventions: 1, - ..InspectorLedger::default() - }, - state_all_committed, + undone ); } + cell!(at, JournalWrite, InspectorLedger::default()); + if at != Create { + // The pool a call's inputs seed the child with. It travels to the child and back, so + // it is booked as gas — and the inputs came back changed in a field the envelope lane + // does not cover, which the rewrite comparison books separately. + cell!(at, WriteReservoir, plus_intervention(ledger_reservoir(i128::from(RESERVOIR)))); + } } // The three callbacks that are handed a finished frame's result. for at in [FrameEnd, CallEnd, CreateEnd] { - let creation = at == CreateEnd; - push( - at, - RaiseResultGas, - Fixture::ReturningCallee, - ledger_result(i128::from(RESULT)), - state_all_committed, - ); - push( - at, - LowerResultGas, - Fixture::ReturningCallee, - ledger_result(-i128::from(RESULT)), - state_all_committed, - ); - push( - at, - FailResult, - Fixture::ReturningCallee, - ledger_intervention(), - if creation { state_no_deployment } else { state_callee_write_rolled_back }, - ); - if !creation { + let undone: fn(&Outcome, &str) = + if at == CreateEnd { state_no_deployment } else { state_callee_write_rolled_back }; + cell!(at, RaiseResultGas, ledger_result(i128::from(RESULT))); + cell!(at, LowerResultGas, ledger_result(-i128::from(RESULT))); + cell!(at, FailResult, ledger_intervention(), Fixture::ReturningCallee, undone); + if at != CreateEnd { // Reviving a reverted *call* is honoured; the creation form is refused, and the two // tests that pin the refusal are named in `inapplicable`. - push( + cell!( at, ReviveResult, - Fixture::RevertingCallee, ledger_intervention(), - state_callee_write_revived, + Fixture::RevertingCallee, + state_callee_write_revived ); } // The half of a finished outcome that sits outside the `InterpreterResult`: where a call's @@ -1502,48 +1206,12 @@ fn matrix() -> Vec { // fixture discards both — the caller asks for a range the callee never fills and pops the // address — so what the cell pins is the booking. `ledger_blind_spots.rs` pins the forms // that change the produced state. - push( - at, - EditOutcomeMetadata, - Fixture::ReturningCallee, - ledger_intervention(), - state_all_committed, - ); - push( - at, - JournalWrite, - Fixture::ReturningCallee, - InspectorLedger::default(), - state_all_committed, - ); - push( - at, - RaiseRefund, - Fixture::ReturningCallee, - ledger_refund(i128::from(REFUND)), - state_all_committed, - ); - push( - at, - LowerRefund, - Fixture::ReturningCallee, - ledger_refund(-i128::from(REFUND)), - state_all_committed, - ); - push( - at, - WriteReservoir, - Fixture::ReturningCallee, - ledger_reservoir(i128::from(RESERVOIR)), - state_all_committed, - ); - push( - at, - WriteStateGas, - Fixture::ReturningCallee, - ledger_state_gas(i128::from(STATE_GAS)), - state_all_committed, - ); + cell!(at, EditOutcomeMetadata, ledger_intervention()); + cell!(at, JournalWrite, InspectorLedger::default()); + cell!(at, RaiseRefund, ledger_refund(i128::from(REFUND))); + cell!(at, LowerRefund, ledger_refund(-i128::from(REFUND))); + cell!(at, WriteReservoir, ledger_reservoir(i128::from(RESERVOIR))); + cell!(at, WriteStateGas, ledger_state_gas(i128::from(STATE_GAS))); } cells @@ -1566,19 +1234,18 @@ fn test_every_cheat_shape_is_booked_and_the_law_still_closes() { assert_eq!(cheat.fired, 1, "{label}: the fixture must reach this callback exactly once"); assert_eq!( - reading.ledger, cell.ledger, + reading.inspector_ledger, cell.ledger, "{label}: the shim must book exactly what the cheat did, and nothing else", ); // The interpreter lane the cheat measured for itself and the lane the shim booked are two // independent readings of the same edit. if cheat.moved_gas != 0 { assert_eq!( - reading.ledger.gas.net(), + reading.inspector_ledger.gas.net(), cheat.moved_gas, "{label}: the shim's reading of the counter edit must match the cheat's own", ); } - assert_identity(&label, &reading); (cell.state)(&reading, &label); } } @@ -1594,8 +1261,8 @@ fn test_the_matrix_leaves_no_cell_unaccounted() { let mut holes = Vec::new(); let (mut tested, mut excused) = (0usize, 0usize); - for at in At::ALL { - for shape in Shape::ALL { + for &at in At::ALL { + for &shape in Shape::ALL { match (covered.contains(&(at, shape)), inapplicable(at, shape)) { (true, None) => tested += 1, (false, Some(_)) => excused += 1, @@ -1647,7 +1314,7 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { (At::CreateEnd, Shape::EditOutcomeMetadata, Fixture::ReturningCallee), ]; - let mut plain: BTreeMap = BTreeMap::new(); + let mut plain: BTreeMap = BTreeMap::new(); for fixture in [Fixture::ReturningCallee, Fixture::RevertingCallee] { plain.insert(fixture, transact_plain(fixture)); } @@ -1660,9 +1327,9 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { assert_eq!(cheat.fired, 0, "{label}: no callback may run with the inspected loops off"); assert!( - off.ledger.is_zero(), + off.inspector_ledger.is_zero(), "{label}: an inert inspector books nothing: {:?}", - off.ledger + off.inspector_ledger ); assert_eq!( std::format!("{:?}", off.result), @@ -1670,7 +1337,7 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { "{label}" ); assert_eq!(off.compute_gas, reference.compute_gas, "{label}"); - assert_eq!(off.enforced, reference.enforced, "{label}"); + assert_eq!(off.enforced(), reference.enforced(), "{label}"); assert_eq!(off.destroyed, reference.destroyed, "{label}"); assert_eq!(off.data_size, reference.data_size, "{label}"); assert_eq!(off.kv_updates, reference.kv_updates, "{label}"); @@ -1678,8 +1345,11 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { assert_eq!(off.gas_used, reference.gas_used, "{label}"); assert_eq!(off.total_gas_spent, reference.total_gas_spent, "{label}"); assert_eq!(off.terms, reference.terms, "{label}"); - assert_eq!(off.render_state(), reference.render_state(), "{label}: the produced state"); - assert_identity(&label, &off); + assert_eq!( + state_view(&off.state), + state_view(&reference.state), + "{label}: the produced state" + ); } } @@ -1692,16 +1362,14 @@ fn test_the_matrix_is_inert_with_the_inspected_loops_switched_off() { fn test_the_fixtures_behave_as_the_cells_assume() { let returning = transact_plain(Fixture::ReturningCallee); state_all_committed(&returning, "returning callee"); - assert_identity("returning callee", &returning); let reverting = transact_plain(Fixture::RevertingCallee); assert!(reverting.result.is_success(), "the caller absorbs the revert: {:?}", reverting.result); assert_eq!( - reverting.slot(CALLEE, CALLEE_SLOT), + slot(&reverting, CALLEE, CALLEE_SLOT), U256::ZERO, "the reverting callee's write must be rolled back without an inspector", ); - assert_identity("reverting callee", &reverting); } /// `frame_end` is the last callback that can rewrite a creation's classification, and the refusal @@ -1729,55 +1397,11 @@ fn test_reviving_a_failed_creation_is_refused_at_frame_end() { } } - // Init code that reverts immediately: PUSH1 0, PUSH1 0, REVERT. - let init: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) // size - .push_number(0u64) // offset - .push_number(0u64) // value - .append(CREATE) - .append(POP) - .append(STOP) - .build(); - - let run = move || { - let mut db = MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code.clone()) - .account_balance(CONTRACT, U256::from(ONE_ETH)); - let mut inspector = LateReviver; - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut inspector); - evm.execute_transaction(tx()).map(|_| ()).map_err(|e| std::format!("{e:?}")) - }; + let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); - assert_refused(run); -} - -/// Drives `run` and asserts the shim refused the rewrite, however this build surfaces a refusal: -/// a debug build asserts (the shape is a detector, and a corpus that produces it should stop), a -/// release build fails the transaction with the same message. -fn assert_refused(run: impl Fn() -> Result<(), String>) { - const MESSAGE: &str = "inspector rewrote a failed contract creation into a successful one"; - if cfg!(debug_assertions) { - let previous = std::panic::take_hook(); - std::panic::set_hook(std::boxed::Box::new(|_| {})); - let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)); - std::panic::set_hook(previous); - let payload = panicked.expect_err("the detector must fire in debug builds"); - let message = payload - .downcast_ref::() - .map(String::as_str) - .or_else(|| payload.downcast_ref::<&str>().copied()) - .unwrap_or_default(); - assert!(message.contains(MESSAGE), "the assertion must name the shape; got {message:?}"); - } else { - let error = run().expect_err("the refusal must surface as an EVMError in release builds"); - assert!(error.contains(MESSAGE), "the error must name the shape; got {error:?}"); - } + assert_refused(REVIVED_CREATION, || { + try_transact_inspected(db.clone(), limits(), &mut LateReviver) + }); } /// An inspector that implements only `selfdestruct` moves nothing. @@ -1802,50 +1426,23 @@ fn test_a_selfdestruct_only_inspector_moves_nothing() { } let callee = BytecodeBuilder::default().push_address(CALLER).append(SELFDESTRUCT).build(); - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(u128::from(INNER_CALL_GAS)) - .append(CALL) - .append(POP) - .append(STOP) - .build(); + let code = call_then_stop(CALLEE, INNER_CALL_GAS); let build_db = || { - MemoryDatabase::default() - .account_balance(CALLER, U256::from(10 * ONE_ETH)) - .account_code(CONTRACT, code.clone()) - .account_balance(CONTRACT, U256::from(ONE_ETH)) - .account_code(CALLEE, callee.clone()) - .account_balance(CALLEE, U256::from(ONE_ETH)) - }; - - let plain = { - let mut db = build_db(); - let mut evm = MegaEvm::new(context(&mut db)); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading + db_with_callee(code.clone(), callee.clone()).account_balance(CALLEE, U256::from(ONE_ETH)) }; let mut watcher = Watcher::default(); - let watched = { - let mut db = build_db(); - let mut evm = MegaEvm::new(context(&mut db)).with_inspector(&mut watcher); - let outcome = evm.execute_transaction(tx()).expect("tx should not surface EVMError"); - let reading = read(&evm.ctx_ref().additional_limit.borrow(), outcome); - reading - }; + let (plain, watched) = plain_and_cheated(build_db, &mut watcher); assert_eq!(watcher.seen.len(), 1, "the shim must forward the callback: {:?}", watcher.seen); assert_eq!(watcher.seen[0].0, CALLEE, "the self-destructing contract"); assert_eq!(watcher.seen[0].1, CALLER, "the beneficiary"); - assert!(watched.ledger.is_zero(), "nothing to measure: {:?}", watched.ledger); + assert!( + watched.inspector_ledger.is_zero(), + "nothing to measure: {:?}", + watched.inspector_ledger + ); assert_eq!(watched.compute_gas, plain.compute_gas); assert_eq!(watched.total_gas_spent, plain.total_gas_spent); - assert_eq!(watched.render_state(), plain.render_state()); - assert_identity("selfdestruct", &watched); + assert_eq!(state_view(&watched.state), state_view(&plain.state)); } From 6c8d9f8700a033c9e5bc1ad49a661f8ca12289a4 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:03:37 +0800 Subject: [PATCH 186/208] test(rex7): collect the shim's tests into two files, by mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine files became five. `shim_measurement.rs` is the five that pin what the shim *books* — the counter and envelope edits, the two settlement windows, the blind spots an all-zero ledger used to admit, the receipt's refund and EIP-8037 dimensions, and the gas a synthetic outcome carries — in that order, each behind its own banner and keeping its own header prose. `shim_refusals.rs` is the two that pin what the shim *refuses*, which had been split across three files by which callback catches the rewrite rather than by what the rewrite is. Three constants collided. `ACTION_DELTA` and `REFUND` held the same value in the two files that declared each, so one declaration survives; the two `FORWARDED`s did not, so the settlement window's is `PROBE_GAS`. Assertions 1051 -> 1051, and the suite still runs the same 1830 test names. --- crates/mega-evm/src/evm/AGENTS.md | 2 +- crates/mega-evm/tests/rex7/gas_surface.rs | 4 +- .../tests/rex7/inspector_cheat_matrix.rs | 55 +- .../mega-evm/tests/rex7/inspector_common.rs | 7 +- .../tests/rex7/inspector_settlement_window.rs | 615 ---- .../mega-evm/tests/rex7/interception_gas.rs | 451 --- .../mega-evm/tests/rex7/ledger_blind_spots.rs | 930 ----- crates/mega-evm/tests/rex7/main.rs | 36 +- .../mega-evm/tests/rex7/measured_inspector.rs | 584 --- .../tests/rex7/refund_and_state_gas.rs | 653 ---- .../mega-evm/tests/rex7/shim_measurement.rs | 3173 +++++++++++++++++ ...nit_result_rewrite.rs => shim_refusals.rs} | 121 +- crates/mega-state-test/src/chaos.rs | 4 +- 13 files changed, 3300 insertions(+), 3335 deletions(-) delete mode 100644 crates/mega-evm/tests/rex7/inspector_settlement_window.rs delete mode 100644 crates/mega-evm/tests/rex7/interception_gas.rs delete mode 100644 crates/mega-evm/tests/rex7/ledger_blind_spots.rs delete mode 100644 crates/mega-evm/tests/rex7/measured_inspector.rs delete mode 100644 crates/mega-evm/tests/rex7/refund_and_state_gas.rs create mode 100644 crates/mega-evm/tests/rex7/shim_measurement.rs rename crates/mega-evm/tests/rex7/{frame_init_result_rewrite.rs => shim_refusals.rs} (76%) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 8618b0f0..5bfe0e99 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -218,7 +218,7 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Compare every reading at every callback, not once per frame.** The four fields a frame's identity is made of — its target, the address of the code it runs, its caller and its value — together with its calldata identity, its static flag and its spec id, cannot change while it runs, which makes them the readings a cheaper shim would compare once per frame instead of twice per opcode. They can change — an inspector writes them — and the shape that exploits a per-frame comparison is an edit made in `step` and undone in `step_end`, which leaves the frame's identity equal to the EVM's at every point outside those two callbacks while the instruction in between reads something else. - `tests/rex7/ledger_blind_spots.rs::test_a_frame_invariant_moved_and_moved_back_is_booked` is that shape, and it costs the transaction nothing, so no gas lane can stand in for the comparison. + `tests/rex7/shim_measurement.rs::test_a_frame_invariant_moved_and_moved_back_is_booked` is that shape, and it costs the transaction nothing, so no gas lane can stand in for the comparison. Making a reading cheaper is free to do; taking it less often needs an argument that this test survives. - **Book a lane through `Lane::book`, never by writing its net.** The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index 156b879d..11649383 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -469,8 +469,8 @@ fn sample_interpreter() -> Interpreter { /// Every field of every gas-carrying object an inspector is handed has a verdict. /// /// This is the closure the completeness table rests on. It is not a claim that the verdicts are -/// right — the tests in `measured_inspector.rs`, `inspector_cheat_matrix.rs`, -/// `interception_gas.rs` and `refund_and_state_gas.rs` are — it is the claim that there is no +/// right — the tests in `shim_measurement.rs` and `inspector_cheat_matrix.rs` are — it is the +/// claim that there is no /// field without one. #[test] fn test_every_field_of_every_gas_carrier_has_a_verdict() { diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index d90d18a5..e3d19a73 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -1,6 +1,6 @@ //! Every rewrite shape, at every callback that can carry it. //! -//! `tests/rex7/measured_inspector.rs` pins one mechanism per test, chosen because each is a +//! `tests/rex7/shim_measurement.rs` pins one mechanism per test, chosen because each is a //! different half of the measurement shim. This module asks the complementary question: not "does //! each mechanism work" but "is there a callback on the `Inspector` trait, or a rewrite shape a //! callback admits, that nothing measures". So the cases here are laid out as a matrix over the @@ -35,19 +35,18 @@ use crate::{ common::{ - base_db, call_contract_tx, context, drive, state_view, transact, Outcome, CALLEE, CALLER, - CONTRACT, ONE_ETH, + call_contract_tx, context, drive, state_view, transact, Outcome, CALLEE, CALLER, CONTRACT, + ONE_ETH, }, inspector_common::{ - assert_refused, call_then_stop, db_with_callee, deploy_then_stop, ledger_env, ledger_gas, - ledger_intervention, ledger_refund, ledger_reservoir, ledger_result, ledger_state_gas, - limits, plain_and_cheated, try_transact_inspected, REVERTING_INIT_CODE, REVIVED_CREATION, + call_then_stop, db_with_callee, ledger_env, ledger_gas, ledger_intervention, ledger_refund, + ledger_reservoir, ledger_result, ledger_state_gas, limits, plain_and_cheated, }, }; use alloy_primitives::{Address, Bytes, Log, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, - InspectorLedger, Lane, MegaEvm, MegaSpecId, MegaTransactionNew as _, + InspectorLedger, MegaEvm, MegaSpecId, }; use revm::{ bytecode::opcode::{CALL, CREATE, LOG1, MSTORE, MSTORE8, POP, RETURN, SSTORE, STOP}, @@ -130,13 +129,13 @@ macro_rules! axis { // --- rows and columns ----------------------------------------------------------------------- +axis! { /// One row of the matrix: a callback on the `Inspector` trait. /// /// Every method of the trait is here. `log` is the one that never fires in these fixtures — it is /// reached only when a precompile's logs are forwarded, which no wired `MegaETH` precompile /// produces — and [`inapplicable`] carries that, together with the reason its every column is /// empty anyway. -axis! { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum At { InitializeInterp, @@ -154,6 +153,7 @@ enum At { } } +axis! { /// One column of the matrix: a shape a rewrite can take. /// /// The columns are the rewrite's *mechanism*, not its purpose: what argument it reaches through @@ -164,7 +164,6 @@ enum At { /// The EIP-8037 dimensions are the one place that pairing does not apply: `MegaETH` runs with the /// EIP off, so a `Gas` reaches every callback with both of its state-gas figures at zero and there /// is nothing to lower. -axis! { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] enum Shape { /// Write gas into a live interpreter's counter. @@ -348,7 +347,7 @@ fn inapplicable(at: At, shape: Shape) -> Option<&'static str> { } ReviveResult if at == CreateEnd => Some( "refused outright rather than measured — `test_reviving_a_failed_creation_is_refused` \ - in `measured_inspector.rs` pins the refusal at `create_end`, and \ + in `shim_refusals.rs` pins the refusal at `create_end`, and \ `test_reviving_a_failed_creation_is_refused_at_frame_end` pins it one callback later", ), _ => None, @@ -626,7 +625,7 @@ impl Cheat { /// A call's return range is shrunk to nothing rather than moved, because moving it past the /// caller's allocated memory is a panic in revm and this fixture's caller holds one word. The /// visible-effect form, where the caller then reads a word the callee never wrote, is pinned - /// in `ledger_blind_spots.rs`. + /// in `shim_measurement.rs`. fn hit_outcome_metadata(&mut self, result: &mut FrameResult) { match result { FrameResult::Call(outcome) => { @@ -1204,7 +1203,7 @@ fn matrix() -> Vec { // The half of a finished outcome that sits outside the `InterpreterResult`: where a call's // return data lands, and which address a creation reports. Neither moves gas, and this // fixture discards both — the caller asks for a range the callee never fills and pops the - // address — so what the cell pins is the booking. `ledger_blind_spots.rs` pins the forms + // address — so what the cell pins is the booking. `shim_measurement.rs` pins the forms // that change the produced state. cell!(at, EditOutcomeMetadata, ledger_intervention()); cell!(at, JournalWrite, InspectorLedger::default()); @@ -1372,38 +1371,6 @@ fn test_the_fixtures_behave_as_the_cells_assume() { ); } -/// `frame_end` is the last callback that can rewrite a creation's classification, and the refusal -/// covers it too. -/// -/// `measured_inspector.rs` pins the `create_end` form. This is the one callback later: revm calls -/// `create_end` first and `frame_end` after it, so an inspector that leaves `create_end` alone and -/// rewrites in `frame_end` would slip past a refusal wired only to the earlier one. -#[test] -fn test_reviving_a_failed_creation_is_refused_at_frame_end() { - /// Rewrites a failed creation into a success, from `frame_end` only. - struct LateReviver; - - impl Inspector for LateReviver { - fn frame_end( - &mut self, - _context: &mut CTX, - _frame_input: &FrameInput, - frame_result: &mut FrameResult, - ) { - let FrameResult::Create(outcome) = frame_result else { return }; - if !outcome.result.result.is_ok() { - outcome.result.result = InstructionResult::Stop; - } - } - } - - let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); - - assert_refused(REVIVED_CREATION, || { - try_transact_inspected(db.clone(), limits(), &mut LateReviver) - }); -} - /// An inspector that implements only `selfdestruct` moves nothing. /// /// The callback takes every argument by value and is handed no context, so there is nothing for diff --git a/crates/mega-evm/tests/rex7/inspector_common.rs b/crates/mega-evm/tests/rex7/inspector_common.rs index 366287ce..362623a1 100644 --- a/crates/mega-evm/tests/rex7/inspector_common.rs +++ b/crates/mega-evm/tests/rex7/inspector_common.rs @@ -6,7 +6,7 @@ //! here, because a rewrite is only ever pinned by comparing an inspected run against the //! uninspected one over the same fixture. -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{Address, Bytes}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaContext, MegaEvm, MegaSpecId, @@ -220,8 +220,3 @@ pub(crate) const REVIVED_CREATION: &str = /// Init code that reverts immediately, so the creation it is handed to fails. pub(crate) const REVERTING_INIT_CODE: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xfd]; - -/// A slot as the produced state has it, taking the slot as the small integer the fixtures use. -pub(crate) fn slot_of(outcome: &crate::common::Outcome, address: Address, slot: u64) -> U256 { - outcome.storage_value(address, U256::from(slot)) -} diff --git a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs b/crates/mega-evm/tests/rex7/inspector_settlement_window.rs deleted file mode 100644 index d60f7b38..00000000 --- a/crates/mega-evm/tests/rex7/inspector_settlement_window.rs +++ /dev/null @@ -1,615 +0,0 @@ -//! The two windows in which a rewrite lands after the accounting that should have read it. -//! -//! Both halves of the measurement shim rest on the same claim: what the shim books is what the -//! transaction's envelope actually moved by. There are two places where the number the shim reads -//! and the number the envelope carries are not the same object, and each of them is a fixture -//! here. -//! -//! - **A terminating opcode's `step_end`.** revm's inspected loop runs `step_end` *after* the -//! instruction that produced the frame's action, and that action carries its own copy of the gas -//! counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail -//! settlement measures work against and nothing the caller will ever see, so it must move the -//! settlement baseline and must not move the ledger. The two neighbouring windows — a `step_end` -//! in mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of -//! that rule: the frame resumes on the edited counter in both, so both are booked. -//! -//! - **A precompile's classification.** A precompile is answered inside the frame init and never -//! becomes a child frame, so its recording site is the only place that knows the forwarded -//! envelope and the work performed. The split is nonetheless settled at the frame's settlement -//! point, from what that site staged, exactly as an ordinary frame's is — because a callback runs -//! in between, and the classification is what decides whether the caller reclaims the remainder. -//! What that callback may do to the classification is bounded: the journal decision behind a -//! result frame init produced was taken before any callback ran and is not reachable from one, so -//! a rewrite that moves such a result across the success / revert / halt boundary is refused and -//! the settlement reads the classification the EVM produced. The cases below pin the uninspected -//! split each precompile arm produces, and the refusal that keeps it the one the settlement sees. -//! -//! Every case here is checked by the identity `common::finish` runs on every transaction: the -//! tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. - -use crate::{ - common::{ - base_db, transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, - CALLEE, DEFAULT_TX_GAS_LIMIT, - }, - inspector_common::{call_then_stop, db_with_callee, limits}, -}; -use alloy_primitives::{address, Address, Bytes, U256}; -use mega_evm::{ - kzg_point_evaluation, test_utils::BytecodeBuilder, InspectorLedger, Lane, MegaSpecId, -}; -use revm::{ - bytecode::opcode::{CALL, INVALID, POP, STOP}, - context::ContextTr, - interpreter::{ - interpreter_types::LoopControl, CallInputs, CallOutcome, FrameInput, InstructionResult, - Interpreter, InterpreterAction, InterpreterTypes, - }, - Inspector, -}; -use sha2::{Digest, Sha256}; - -/// Gas the edit-once inspector writes into a live interpreter's counter. -const INJECT: u64 = 1_000; - -/// Gas every probed CALL forwards. Well inside the 63/64 rule at the default transaction gas -/// limit and well inside the default compute budget, so the forwarded envelope is exactly this. -const FORWARDED: u64 = 1_000_000; - -/// The transaction gas limit is not what binds any fixture here — pinned at compile time, so a -/// change to the shared limit cannot silently turn a destroyed-remainder case into an -/// out-of-gas one. -const _: () = assert!(DEFAULT_TX_GAS_LIMIT > 10 * FORWARDED); - -/// The identity precompile. -const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); -/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. -const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); -/// KZG point evaluation. -const KZG: Address = address!("000000000000000000000000000000000000000a"); - -// --- A: the window a terminating opcode's `step_end` sits in --------------------------------- - -/// Which of the three `step_end` windows an edit is aimed at, told apart by the action the -/// instruction that just ran left behind. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Window { - /// No action yet: the frame carries on, and the edited counter is what it carries on with. - MidFrame, - /// A `NewFrame` action: the frame suspends into a child and then resumes on this counter. - Suspending, - /// A `Return` action: the frame is over, and the gas it hands back was copied into the action - /// before this callback ran. - Terminating, -} - -impl Window { - fn of(interp: &mut Interpreter) -> Self { - match interp.bytecode.action() { - None => Self::MidFrame, - Some(InterpreterAction::NewFrame(_)) => Self::Suspending, - Some(InterpreterAction::Return(_)) => Self::Terminating, - } - } -} - -/// Writes [`INJECT`] into the interpreter's counter once, at the first `step_end` that sits in -/// `window`. -#[derive(Debug)] -struct CounterEditor { - window: Window, - fired: u32, -} - -impl CounterEditor { - fn new(window: Window) -> Self { - Self { window, fired: 0 } - } -} - -impl Inspector for CounterEditor { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || Window::of(interp) != self.window { - return; - } - self.fired += 1; - interp.gas.erase_cost(INJECT); - } -} - -/// `PUSH1 1; POP; STOP` — three opcodes, so a mid-frame `step_end` and a terminating one are both -/// reached, and nothing else happens in between. -fn straight_line_code() -> Bytes { - BytecodeBuilder::default().push_number(1u64).append(POP).append(STOP).build() -} - -/// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame -/// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. -fn suspending_code() -> Bytes { - call_then_stop(IDENTITY, FORWARDED) -} - -fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = CounterEditor::new(window); - let edited = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - (plain, edited, inspector.fired) -} - -/// An edit made in the terminating window reaches nobody, so nothing is booked for it — and the -/// transaction is the one the EVM would have produced alone. -/// -/// The action the terminating instruction set already holds its own copy of the counter, so the -/// caller is handed a number this edit never touched. Booking it would tell the conservation law -/// that the transaction spent [`INJECT`] less than it did. -/// -/// `compute_gas` being unmoved is the other half of the rule, and the one that would break if the -/// fix were written as "leave the counter alone" rather than "book nothing for it": the tail -/// settlement measures work as a drop in this very counter, so without the baseline shift the -/// injection would read as [`INJECT`] gas of work the frame never performed. -#[test] -fn test_an_edit_in_the_terminating_window_is_not_booked() { - let (plain, edited, fired) = run_counter_edit(straight_line_code(), Window::Terminating); - - assert_eq!(fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger::default(), - "an edit that cannot reach the envelope must leave the ledger untouched", - ); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "the settlement baseline must absorb the edit, so it counts as no work at all", - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "the envelope must be the one the uninspected run produced", - ); -} - -/// The near boundary: a mid-frame edit is booked, because the frame carries on spending the -/// counter the callback left behind. -#[test] -fn test_an_edit_in_mid_frame_is_still_booked() { - let (_, edited, fired) = run_counter_edit(straight_line_code(), Window::MidFrame); - - assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); - assert_eq!( - edited.inspector_ledger.gas, - Lane::once(i128::from(INJECT)), - "gas written into a counter the frame will keep spending is conjured gas", - ); -} - -/// The far boundary, and the one a coarser rule would get wrong: a `CALL` has set an action too, -/// but it is a `NewFrame` action — the frame suspends, the child runs, and then the frame resumes -/// on exactly this counter. So the edit reaches the envelope and must be booked, even though the -/// interpreter is "at the end of its loop" in precisely the same sense as the terminating case. -#[test] -fn test_an_edit_in_the_suspending_window_is_still_booked() { - let (_, edited, fired) = run_counter_edit(suspending_code(), Window::Suspending); - - assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); - assert_eq!( - edited.inspector_ledger.gas, - Lane::once(i128::from(INJECT)), - "a suspended frame resumes on the edited counter, so the edit reaches the envelope", - ); -} - -// --- B: a precompile's classification, rewritten after its recording site --------------------- - -/// Rewrites the result of the call to `target` into `to`, once. -#[derive(Debug)] -struct Reclassifier { - target: Address, - to: InstructionResult, - fired: u32, -} - -impl Reclassifier { - fn new(target: Address, to: InstructionResult) -> Self { - Self { target, to, fired: 0 } - } -} - -impl Inspector for Reclassifier { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.fired > 0 || inputs.target_address != self.target { - return; - } - self.fired += 1; - outcome.result.result = self.to; - } -} - -/// A `CALL` forwarding [`FORWARDED`] gas to `target` with `calldata` at `mem[0..]`, its success -/// flag popped so the caller survives either classification. -fn call_precompile(target: Address, calldata: &[u8]) -> Bytes { - BytecodeBuilder::default() - .mstore(0, calldata) - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(calldata.len() as u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(target) - .push_number(FORWARDED) - .append(CALL) - .append(POP) - .append(STOP) - .build() -} - -/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped: 192 bytes -/// with a matching versioned hash, so KZG clears the length doorway and fails inside verification -/// — the one halt shape `MegaETH` prices as work performed. -fn kzg_verification_failure() -> Vec { - let commitment = hex::decode( - "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ - 5f26936857bc3a7c2539ea8ec3a952b7", - ) - .unwrap(); - let mut versioned_hash = Sha256::digest(&commitment).to_vec(); - versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG - let z = - hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); - let y = - hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); - let proof = hex::decode( - "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ - 4faf0070725e00b60ad9a026a15b1a8c", - ) - .unwrap(); - - let mut input = Vec::new(); - input.extend_from_slice(&versioned_hash); - input.extend_from_slice(&z); - input.extend_from_slice(&y); - input.extend_from_slice(&commitment); - input.extend_from_slice(&proof); - assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); - let last = input.len() - 1; - input[last] ^= 0x01; - input -} - -/// Runs the fixture twice: once uninspected, and once with the classification rewritten across -/// the boundary the shim refuses. -/// -/// The refusal is asserted here rather than in each case, so every case below is left stating the -/// one thing that differs between them — which arm of the precompile it reaches, and what the -/// uninspected run's split therefore is. -fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { - let code = call_precompile(target, calldata); - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = Reclassifier::new(target, to); - let refusal = - transact_inspected_refused(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); - assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); - assert!( - refusal.error.contains("classification of a result frame init produced"), - "the transaction must fail with the refusal's own reason, got {}", - refusal.error, - ); - (plain, refusal) -} - -/// A successful precompile rewritten into a halt is refused, and the uninspected run destroys -/// nothing. -/// -/// The rewrite is the direction with state behind it: `make_call_frame` commits the checkpoint -/// before it returns a successful precompile's result, so a caller told the call halted would be -/// told so with the transfer that funded it standing. -#[test] -fn test_rewriting_a_successful_precompile_into_a_halt_is_refused() { - let (plain, _) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); - - assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); - assert_eq!( - plain.compute_gas, - plain.enforced(), - "with nothing destroyed the reported total is the work performed", - ); -} - -/// A rejected precompile rewritten into a success is refused, and the uninspected run destroys the -/// whole envelope. -/// -/// The other direction, and the other half of the split: `blake2f` rejects the input before any -/// work, so `make_call_frame` reverted the checkpoint and nothing was performed. -#[test] -fn test_reviving_a_rejected_precompile_is_refused() { - let (plain, _) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); - - assert_eq!( - plain.destroyed, FORWARDED, - "blake2f rejects the input before any work, so the uninspected run destroys all of it", - ); - assert_eq!( - plain.enforced(), - plain.compute_gas - plain.destroyed, - "nothing was performed, so nothing enforces", - ); -} - -/// The third arm, and the only one whose failure `MegaETH` prices as work: a KZG verification that -/// ran and rejected. -/// -/// The refusal matters most here. A halting precompile's gas object carries the whole forwarded -/// envelope as remaining — it is reset rather than spent down — so a caller told such a call -/// succeeded would reclaim all of it, the fixed fee included. That fee is gas the execution priced -/// and the envelope never paid, which is exactly the shape the refusal keeps out. -#[test] -fn test_reviving_a_priced_precompile_failure_is_refused() { - let calldata = kzg_verification_failure(); - let (plain, _) = run_reclassified(KZG, &calldata, InstructionResult::Stop); - - assert_eq!( - plain.destroyed, - FORWARDED - kzg_point_evaluation::GAS_COST, - "verification ran, so the uninspected run destroys the envelope less the fixed fee", - ); - assert_eq!( - plain.compute_gas - plain.destroyed, - plain.enforced(), - "the fee is the work performed, and it is what enforces", - ); -} - -// --- C: the pending action itself --------------------------------------------------------------- - -/// Gas an action edit moves. -const ACTION_DELTA: u64 = 700; - -/// Reaches past the interpreter's gas counter and into the action the interpreter is holding, once. -/// -/// The counter and the action are two different objects at exactly one moment — after a -/// terminating or suspending instruction has run and before the loop hands the action on — and -/// this is the inspector that edits the second one. -#[derive(Debug)] -struct ActionEditor { - window: Window, - /// Positive raises the gas the action carries, negative lowers it. - delta: i64, - /// Fire only on an action whose classification is (or is not) an exceptional halt. - halting: bool, - fired: u32, -} - -impl ActionEditor { - fn raise(window: Window) -> Self { - Self { window, delta: ACTION_DELTA as i64, halting: false, fired: 0 } - } - - fn lower(window: Window) -> Self { - Self { window, delta: -(ACTION_DELTA as i64), halting: false, fired: 0 } - } - - fn on_halt() -> Self { - Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } - } -} - -impl Inspector for ActionEditor { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || Window::of(interp) != self.window { - return; - } - match interp.bytecode.action() { - Some(InterpreterAction::Return(result)) => { - if result.result.is_ok_or_revert() == self.halting { - return; - } - if self.delta >= 0 { - result.gas.erase_cost(self.delta.unsigned_abs()); - } else { - assert!( - result.gas.record_regular_cost(self.delta.unsigned_abs()), - "the fixture must leave the action enough gas for the removal to land", - ); - } - } - Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { - inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); - } - _ => return, - } - self.fired += 1; - } -} - -/// A `CALL` into [`CALLEE`], its result flag popped, then `STOP` — so the first terminating -/// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries -/// is decided by the callee the fixture installs. -fn call_callee_code() -> Bytes { - call_then_stop(CALLEE, FORWARDED) -} - -/// Gas written into a returning frame's pending action is gas the caller really reclaims, so it -/// has to be booked — the frame's classification is what says so, and the classification is only -/// known at the frame's settlement point. -#[test] -fn test_raising_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); - let mut inspector = ActionEditor::raise(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - base_db(straight_line_code()), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::once(i128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "an edit to the action a returning frame hands back is an edit to the envelope", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - ACTION_DELTA, - "the transaction really did spend less, which is why the ledger has to carry it", - ); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "the edit is not work: the frame performed exactly what it performed uninspected", - ); -} - -/// The same edit in the other direction. -#[test] -fn test_lowering_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); - let mut inspector = ActionEditor::lower(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - base_db(straight_line_code()), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::once(-i128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "gas taken out of the action is gas the caller never gets back", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent + ACTION_DELTA, - "the transaction really did spend more", - ); -} - -/// The classification branch: a halting frame hands nothing back, so an edit to the gas its action -/// carries moves nothing and must not reach the lane's *net* — and the remainder it destroys is -/// the EVM's own number, not the edited one. -/// -/// The lane's gross carries the edit all the same. Whether it moved the envelope is what the -/// classification decides; whether the inspector made it is not, and the block guard asks the -/// second question. -#[test] -fn test_editing_a_halting_frames_pending_action_moves_nothing() { - let callee = BytecodeBuilder::default().append(INVALID).build(); - let plain = - transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); - let mut inspector = ActionEditor::on_halt(); - let edited = transact_inspected( - MegaSpecId::REX7, - db_with_callee(call_callee_code(), callee), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::of(0, u128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "a halting frame hands its remainder to nobody, so the edit moves the envelope by nothing \ - — and the lane still has to show it was made", - ); - assert_eq!( - edited.inspector_ledger.conjured_gas(), - 0, - "the conservation law reads the net, which is what stays zero", - ); - assert!( - !edited.inspector_ledger.is_zero(), - "and the block guard reads the gross, which is what does not", - ); - assert_eq!( - edited.destroyed, plain.destroyed, - "the destroyed remainder is the EVM's own, not the one the inspector wrote", - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "and the envelope is unmoved"); -} - -/// The other action variant: gas written into a pending `NewFrame` action is the envelope a child -/// frame is about to be built with, which the caller was never debited for. -#[test] -fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { - let plain = transact(MegaSpecId::REX7, base_db(suspending_code()), limits()); - let mut inspector = ActionEditor::raise(Window::Suspending); - let edited = - transact_inspected(MegaSpecId::REX7, base_db(suspending_code()), limits(), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { env: Lane::once(i128::from(ACTION_DELTA)), ..InspectorLedger::default() }, - "the child's budget grew by gas the caller's CALL never forwarded", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - ACTION_DELTA, - "the child hands the extra budget straight back, so the transaction spends less", - ); -} - -/// Rewrites the classification inside a pending `Return` action, once, at the terminating -/// `step_end` of the frame that set it. -#[derive(Debug)] -struct ActionReclassifier { - to: InstructionResult, - fired: u32, -} - -impl Inspector for ActionReclassifier { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { return }; - result.result = self.to; - self.fired += 1; - } -} - -/// An edit to a pending action that is not to its gas moves nothing and is booked as an -/// intervention — but it still decides what the frame did, so the frame's state follows it. -/// -/// The action is what `classify_frame_action` builds the frame's result from, so a classification -/// written here is the one the caller sees and the one the journal decision is taken on. Nothing -/// on any gas lane can see that, which is what the intervention counter is for. -#[test] -fn test_rewriting_a_pending_actions_classification_is_an_intervention() { - let callee = - BytecodeBuilder::default().sstore(U256::from(1u64), U256::from(1u64)).append(STOP).build(); - let plain = - transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); - let mut inspector = ActionReclassifier { to: InstructionResult::Revert, fired: 0 }; - let edited = transact_inspected( - MegaSpecId::REX7, - db_with_callee(call_callee_code(), callee), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - plain.storage_value(CALLEE, U256::from(1u64)), - U256::from(1u64), - "uninspected, the callee's write is committed", - ); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "no gas moved, and the only thing left to say is that the transaction was not left alone", - ); - assert_eq!( - edited.storage_value(CALLEE, U256::from(1u64)), - U256::ZERO, - "a frame the caller was told reverted must leave no write behind", - ); -} diff --git a/crates/mega-evm/tests/rex7/interception_gas.rs b/crates/mega-evm/tests/rex7/interception_gas.rs deleted file mode 100644 index 1cfdbd28..00000000 --- a/crates/mega-evm/tests/rex7/interception_gas.rs +++ /dev/null @@ -1,451 +0,0 @@ -//! The gas a synthetic outcome carries. -//! -//! A `frame_start` / `call` / `create` callback that returns `Some(outcome)` answers the frame -//! itself: no frame is built, `frame_init` never runs, and the number the caller reclaims is -//! whatever `Gas` the inspector put in that outcome. Nothing about it is derived from the -//! execution — the inspector chooses it outright — so it is a gas figure the transaction's -//! accounting has to be told about, exactly like an edit to a result the EVM did produce. -//! -//! The tests here are laid out over the sign of that choice, because the two directions settle -//! differently and a lane that books one and drops the other is a real failure mode: -//! -//! - an outcome that hands back **less** than the envelope makes the caller spend gas no frame ever -//! performed work for; -//! - an outcome that hands back **more** conjures gas the transaction never funded; -//! - an outcome that hands back **exactly** the envelope — the echo convention every tracer that -//! intercepts follows — moves nothing, and must book nothing. -//! -//! The halt direction is the asymmetry: a halting outcome hands nothing back at all, so what the -//! inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed -//! remainder is settled against the envelope instead. - -use crate::{ - common::{base_db, transact_inspected, CALLEE}, - inspector_common::{call_then_stop, db_with_callee, deploy_then_stop, limits, plain_run_code}, -}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, -}; -use revm::{ - bytecode::opcode::{MSTORE, RETURN}, - handler::FrameResult, - interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - InterpreterResult, InterpreterTypes, - }, - Inspector, -}; -use std::vec::Vec; - -/// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. -const FORWARDED: u64 = 50_000; - -/// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. -fn call_fixture() -> MemoryDatabase { - db_with_callee(call_then_stop(CALLEE, FORWARDED), plain_run_code(20)) -} - -/// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. -#[derive(Clone, Copy, Debug)] -enum Sizing { - /// The echo convention: exactly the envelope. - Echo, - /// Half of it — the caller spends the other half for work no frame performed. - Half, - /// None of it. - Zero, - /// More than it — gas the transaction never funded. - Excess(u64), -} - -impl Sizing { - fn gas(self, envelope: u64) -> u64 { - match self { - Self::Echo => envelope, - Self::Half => envelope / 2, - Self::Zero => 0, - Self::Excess(extra) => envelope + extra, - } - } - - /// What the ledger must carry for this sizing, as a signed movement from the envelope. - fn expected_delta(self, envelope: u64) -> i128 { - i128::from(self.gas(envelope)) - i128::from(envelope) - } -} - -/// Intercepts the call to [`CALLEE`], sizing the outcome's gas by [`Sizing`]. -struct CallInterceptor { - sizing: Sizing, - classification: InstructionResult, - intercepted: u64, - envelope: u64, -} - -impl CallInterceptor { - fn new(sizing: Sizing, classification: InstructionResult) -> Self { - Self { sizing, classification, intercepted: 0, envelope: 0 } - } -} - -impl Inspector for CallInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - self.envelope = inputs.gas_limit; - Some(CallOutcome::new( - InterpreterResult::new( - self.classification, - Bytes::new(), - Gas::new(self.sizing.gas(inputs.gas_limit)), - ), - inputs.return_memory_offset.clone(), - )) - } -} - -/// An outcome that hands back less than the envelope makes the caller spend gas nothing performed. -#[test] -fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { - let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); - assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "the half the outcome withheld is gas the inspector destroyed", - ); -} - -/// The extreme of the same direction: the outcome hands back nothing at all. -#[test] -fn test_a_zero_gas_interception_books_the_whole_envelope() { - let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "an outcome that returns nothing consumed the whole envelope", - ); -} - -/// The other direction: an outcome that hands back more than it was given conjures the difference. -#[test] -fn test_an_over_funded_interception_books_the_gas_it_conjured() { - const EXTRA: u64 = 7_000; - let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "gas the transaction never funded is gas the inspector conjured", - ); -} - -/// The echo convention moves nothing, and must book nothing. -/// -/// This is the shape every tool that intercepts actually uses, and the reason the lane could go -/// missing for as long as it did: with the envelope echoed back the accounting closes whether or -/// not anything measures it. Pinning the zero is what says the lane is measuring rather than -/// coincidentally agreeing. -#[test] -fn test_an_echoing_interception_books_no_gas_at_all() { - for classification in [InstructionResult::Stop, InstructionResult::Revert] { - let mut inspector = CallInterceptor::new(Sizing::Echo, classification); - let reading = - transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", - ); - assert_eq!(reading.inspector_ledger.conjured_gas(), 0, "{classification:?}"); - } -} - -/// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes -/// nothing the transaction spends — and the envelope is destroyed whole. -/// -/// What the outcome claimed is still traffic on the result lane: the sizings below differ from the -/// envelope by different amounts, and each one is an edit the inspector made whether or not the -/// classification let it reach anybody. -#[test] -fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { - for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { - let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); - let reading = - transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); - assert_eq!( - reading.inspector_ledger.conjured_gas(), - 0, - "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", - ); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - interventions: 1, - result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), - ..InspectorLedger::default() - }, - "{sizing:?}: and the traffic is what the outcome claimed, off the envelope", - ); - assert_eq!( - reading.destroyed, FORWARDED, - "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", - ); - } -} - -/// The generic callback intercepts too, and is measured by the same rule. -/// -/// revm runs `frame_start` before the variant-specific `call` / `create`, and an outcome returned -/// there skips both. A lane wired only to the variant hooks would leave this one unmeasured. -#[test] -fn test_the_generic_frame_start_interception_is_measured_too() { - /// Intercepts the call to [`CALLEE`] from the generic callback, handing back half. - #[derive(Default)] - struct GenericInterceptor { - intercepted: u64, - } - - impl Inspector for GenericInterceptor { - fn frame_start( - &mut self, - _context: &mut CTX, - frame_input: &mut FrameInput, - ) -> Option { - let FrameInput::Call(inputs) = frame_input else { return None }; - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - Some(FrameResult::Call(CallOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit / 2), - ), - inputs.return_memory_offset.clone(), - ))) - } - } - - let mut inspector = GenericInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "the generic callback's interception books on the same lane as the variant one's", - ); -} - -/// Init code that writes one slot and returns two bytes of runtime code. -fn init_code() -> Vec { - BytecodeBuilder::default() - .sstore(U256::from(0x30), U256::from(1)) - .push_number(0x6000u64) - .push_number(0u64) - .append(MSTORE) - .push_number(2u64) // size - .push_number(30u64) // offset - .append(RETURN) - .build() - .to_vec() -} - -/// The entry contract: one `CREATE`, then `STOP`. -fn create_fixture() -> MemoryDatabase { - base_db(deploy_then_stop(&init_code())) -} - -/// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. -/// -/// The envelope is not a constant here — `CREATE` forwards all but a sixty-fourth of what the -/// caller holds — so the test reads it back from the callback rather than asserting a figure. -#[test] -fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() { - /// Intercepts the creation, handing back half of what it was given. - #[derive(Default)] - struct CreateInterceptor { - intercepted: u64, - envelope: u64, - } - - impl Inspector for CreateInterceptor { - fn create( - &mut self, - _context: &mut CTX, - inputs: &mut CreateInputs, - ) -> Option { - self.intercepted += 1; - self.envelope = inputs.gas_limit(); - Some(CreateOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit() / 2), - ), - None, - )) - } - } - - let mut inspector = CreateInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, create_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); - assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), - interventions: 1, - ..InspectorLedger::default() - }, - "a creation's interception is measured against the envelope its CREATE forwarded", - ); -} - -/// The envelope an interception is measured against is the one the callback *received*. -/// -/// A callback is free to edit the inputs and then answer the frame itself. The edit reaches no -/// frame — nothing is built from those inputs — so the envelope the caller actually funded is the -/// one the callback was handed, and an outcome echoing the *edited* limit hands back more than -/// that. Measuring against the post-edit number instead would read this run as conjuring nothing. -#[test] -fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { - const BONUS: u64 = 9_000; - - /// Raises the child's gas limit and then intercepts, echoing the raised figure. - #[derive(Default)] - struct RaisingInterceptor { - intercepted: u64, - } - - impl Inspector for RaisingInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - inputs.gas_limit += BONUS; - Some(CallOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit), - ), - inputs.return_memory_offset.clone(), - )) - } - } - - let mut inspector = RaisingInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(i128::from(BONUS)), - interventions: 1, - ..InspectorLedger::default() - }, - "the bonus reaches the caller through the outcome, so it is booked once, on the result \ - lane — the env lane stays empty because no frame was ever built from those inputs", - ); -} - -/// The lane reports on a frozen spec too, and reporting it settles nothing there. -/// -/// The measurement is not REX7-gated, and neither are the two lanes it joins: `InspectorLedger` is -/// what the canonical block path's guard reads, so a frame an inspector answered has to be visible -/// on it whatever spec is executing. What is REX7's alone is the settlement the lane feeds — the -/// envelope a refused frame init decides the fate of. REX6 derives nothing from the envelope and -/// books no destroyed remainder, so what it reports is what it always reported. -/// -/// The transaction's own gas does follow the figure the inspector wrote, on both specs. That is -/// the EVM handing the caller back what the result carries, which is upstream's arithmetic rather -/// than `MegaETH`'s, and it is the movement the lane exists to account for rather than to prevent. -#[test] -fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { - let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); - let echo = transact_inspected( - MegaSpecId::REX6, - call_fixture(), - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), - &mut echoing, - ); - let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let half = transact_inspected( - MegaSpecId::REX6, - call_fixture(), - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), - &mut halving, - ); - - assert_eq!(echoing.intercepted, 1, "fixture check"); - assert_eq!(halving.intercepted, 1, "fixture check"); - assert_eq!( - echo.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "REX6: an echoed envelope moves no gas here either", - ); - assert_eq!( - half.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "REX6: the lane reports, because the block guard has to see this frame on every spec", - ); - assert_eq!( - (echo.destroyed, half.destroyed), - (0, 0), - "REX6 has no destroyed remainder to book, on either sizing", - ); - assert_eq!( - echo.compute_gas, half.compute_gas, - "and its compute total does not follow the figure the inspector wrote", - ); - assert_eq!( - half.total_gas_spent - echo.total_gas_spent, - FORWARDED / 2, - "the caller really did lose the half the outcome withheld — that is the EVM's arithmetic", - ); -} diff --git a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs b/crates/mega-evm/tests/rex7/ledger_blind_spots.rs deleted file mode 100644 index 8eef5f15..00000000 --- a/crates/mega-evm/tests/rex7/ledger_blind_spots.rs +++ /dev/null @@ -1,930 +0,0 @@ -//! The rewrite shapes an all-zero ledger used to admit. -//! -//! The measurement shim's contract is that a transaction an inspector rewrote never reaches a -//! block: the canonical path refuses one whose `InspectorLedger` is non-zero, so every rewrite has -//! to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that per -//! mechanism and per callback × shape pair. This module pins the shapes that slipped *between* -//! those two questions — each one a rewrite the shim was handed, that changes what the transaction -//! produces, and that every lane read as nothing: -//! -//! - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it -//! has been paid for in the same step, so that neither goes out of bounds and the next expanding -//! opcode charges nothing; -//! - a `CallOutcome` / `CreateOutcome` metadata field — where the callee's return data lands, and -//! which address a creation reports — rewritten without touching the `InterpreterResult` inside -//! it, which is the only part the rewrite comparison used to read; -//! - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels -//! to zero; -//! - the same cancellation spread across two frames, where only one of the two survives to the -//! receipt, so the net is zero and the effect is not; -//! - an instruction deleted from a frame, by stepping the program counter past it, so the work is -//! never performed and there is nothing for any counter to meter; -//! - a return buffer put in front of a frame that made no call, so `RETURNDATASIZE` reads a length -//! no call produced. -//! -//! Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use -//! to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test -//! here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no -//! longer changes anything is a shape that stopped testing the guard.//! -//! The last two are also why the snapshot the first shape needed is now a *rule* rather than a -//! list. A snapshot of four chosen readings caught the memory pair and let the program counter -//! through, because `Interpreter::bytecode` was not among the things anyone had thought to name. -//! What the shim takes now is every constant-time reading of the interpreter, and what pins that is -//! the `Interpreter` row of `gas_surface.rs`'s closed table. - -use crate::{ - common::{base_db, transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET}, - inspector_common::plain_and_cheated, -}; -use alloy_primitives::{address, Address, Bytes, U256}; -use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaSpecId}; -use revm::{ - bytecode::opcode::{ - CALL, CALLER, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, - STOP, - }, - context::{Cfg, ContextTr}, - interpreter::{ - interpreter::EthInterpreter, - interpreter_types::{InputsTr, Jumps, LoopControl, MemoryTr, ReturnData}, - CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterAction, - InterpreterTypes, - }, - Inspector, -}; - -/// A second callee, whose frame reverts. -const REVERTER: Address = EMPTY_TARGET; - -/// The address a rewritten `CreateOutcome` reports instead of the one the code was deployed at. -const FAKE_DEPLOYMENT: Address = address!("00000000000000000000000000000000000f00d0"); - -/// Slot the fixtures write their observable result to. -const RESULT_SLOT: u64 = 0x11; - -/// Refund a cancelling pair of refund edits moves, in each direction. -/// -/// Small enough to stay well under the EIP-3529 cap on every fixture here, so that what survives -/// to the receipt is the whole of the surviving half rather than whatever the cap left of it. -const REFUND: i64 = 2_000; - -/// The mainnet memory expansion cost of a memory `words` words long. -const fn memory_cost(words: u64) -> u64 { - 3 * words + words * words / 512 -} - -// --- a frame's memory, grown for free ------------------------------------------------------------ - -/// How far the free-expansion inspector grows the frame's memory, in words. -/// -/// The fixture's own `MSTORE` lands inside it, so the expansion the EVM would have charged for is -/// exactly the one the inspector already did for nothing. -const STOLEN_WORDS: u64 = 129; - -/// Grows the frame's memory and tells the EVM it is already paid for. -/// -/// Both halves are needed and neither is a rewrite on its own. Moving the memory alone leaves the -/// memo behind, and the next expanding opcode charges for an expansion that already happened; -/// moving the memo alone leaves the memory behind, and the EVM reads out of bounds. Moving both -/// keeps every invariant the interpreter has and skips the charge, which is why the pair was the -/// hole and neither half was. -#[derive(Default)] -struct FreeExpansion { - fired: u32, -} - -impl Inspector for FreeExpansion { - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != MSTORE { - return; - } - let words = STOLEN_WORDS as usize; - assert!(interp.memory.resize(words * 32), "the fixture must allow the memory to be grown",); - // Priced through revm's own table, so the memo is exactly what the EVM would have written - // had the frame paid; the assertion below restates the formula independently, which is - // what makes the two a check rather than one number written twice. - let cost = context.cfg().gas_params().memory_cost(words); - interp.gas.memory_mut().set_words_num(words, cost); - self.fired += 1; - } -} - -/// ★ A frame whose memory was grown for free is not an all-zero ledger. -/// -/// The rewrite reaches through no argument the shim used to compare: the interpreter's gas counter -/// is untouched, no action is pending, no frame input and no frame result exists yet. What it -/// moves is the interpreter's memory and the memo beside it, and the transaction then pays less -/// than it would have — which is the one thing the guard exists to keep out of a block. -#[test] -fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { - // MSTORE(offset = STOLEN_WORDS * 32 - 32, value = 0xAA), which expands memory to exactly the - // size the inspector already grew it to. - let offset = (STOLEN_WORDS - 1) * 32; - let code = BytecodeBuilder::default() - .push_number(0xAAu64) - .push_number(offset) - .append(MSTORE) - .append(STOP) - .build(); - - let mut inspector = FreeExpansion::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.total_gas_spent - cheated.total_gas_spent, - memory_cost(STOLEN_WORDS), - "the expansion the inspector performed is the charge the EVM then skipped", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction that paid less because an inspector moved its memory must not read as \ - untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- a call outcome's metadata ------------------------------------------------------------------- - -/// Where the fixture's `CALL` asks for its return data, and where the inspector moves it to. -const RETURN_AT: usize = 0; -const MOVED_TO: usize = 32; - -/// Moves a finished call's return data somewhere else in the caller's memory. -/// -/// The `InterpreterResult` inside the outcome — its classification, its output bytes, its gas — -/// comes back exactly as the EVM produced it. Only the range the caller will copy the output into -/// changes, which is not a field the result carries. -#[derive(Default)] -struct MoveReturnData { - fired: u32, -} - -impl Inspector for MoveReturnData { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address != CALLEE || self.fired > 0 { - return; - } - outcome.memory_offset = MOVED_TO..MOVED_TO + 32; - self.fired += 1; - } -} - -/// ★ A call outcome whose return range was moved is not an all-zero ledger. -#[test] -fn test_a_moved_return_range_is_booked() { - // Size the caller's memory to two words, call the callee for one word of output at offset 0, - // then store what landed there. - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(32u64) - .append(MSTORE) - .push_number(32u64) // retSize - .push_number(u64::try_from(RETURN_AT).unwrap()) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .push_number(u64::try_from(RETURN_AT).unwrap()) - .append(MLOAD) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - // The callee returns one word of 0x11s. - let callee = BytecodeBuilder::default() - .push_u256(U256::from(0x11u64)) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = MoveReturnData::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(0x11u64), - "without the rewrite the return data lands where the caller asked for it", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it, the caller reads a word the callee never wrote", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a rewritten return range changed must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -// --- a frame's returned output ------------------------------------------------------------------ - -/// The word a rewritten output buffer feeds the caller instead of the one the callee returned. -const FORGED_OUTPUT: u64 = 0xdead; - -/// Replaces the output buffer a finished call hands back, leaving its classification alone. -/// -/// The classification and the remaining gas are what every other lane reads. The output is -/// neither, and it is what the caller copies into its own memory. -#[derive(Default)] -struct ForgeCallOutput { - fired: u32, -} - -impl Inspector for ForgeCallOutput { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address != CALLEE || self.fired > 0 { - return; - } - outcome.result.output = Bytes::from(U256::from(FORGED_OUTPUT).to_be_bytes::<32>().to_vec()); - self.fired += 1; - } -} - -/// ★ A call outcome whose returned output was replaced is not an all-zero ledger. -#[test] -fn test_a_forged_call_output_is_booked() { - // Call the callee for one word of output, then store what landed there. - let code = BytecodeBuilder::default() - .push_number(32u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .push_number(0u64) - .append(MLOAD) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - // The callee returns one word of 0x11s. - let callee = BytecodeBuilder::default() - .push_u256(U256::from(0x11u64)) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = ForgeCallOutput::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(0x11u64), - "without the rewrite the caller reads what the callee returned", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(FORGED_OUTPUT), - "with it, the caller reads a word no frame produced", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a replaced output buffer changed must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -/// Reports a different address than the one the creation deployed to. -#[derive(Default)] -struct MoveDeploymentAddress { - fired: u32, -} - -impl Inspector for MoveDeploymentAddress { - fn create_end( - &mut self, - _context: &mut CTX, - _inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - if self.fired > 0 || outcome.address.is_none() { - return; - } - outcome.address = Some(FAKE_DEPLOYMENT); - self.fired += 1; - } -} - -/// ★ A creation outcome whose reported address was rewritten is not an all-zero ledger. -/// -/// The code is still deployed where the EVM put it; only the address the caller's stack receives -/// changes, so the caller goes on to talk to an account that holds nothing. -#[test] -fn test_a_rewritten_deployment_address_is_booked() { - // Init code that returns two bytes of runtime code. - let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) - .push_number(0u64) - .push_number(0u64) - .append(CREATE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = MoveDeploymentAddress::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); - let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); - assert_ne!(deployed, U256::ZERO, "the fixture's CREATE must succeed"); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(FAKE_DEPLOYMENT.as_slice()), - "the caller must have been handed the address the inspector wrote", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction told a contract lives somewhere it does not must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -// --- a construction frame's pending action ------------------------------------------------------- - -/// Drains the gas a construction frame's pending `Return` action carries. -/// -/// The contract this module's other cases rest on — that an action is the frame's result a moment -/// later, so an edit to it settles with that result — does not hold for a creation. Between the -/// two, `classify_frame_action` charges the code deposit out of the gas *this action* carries, and -/// a creation that cannot pay it becomes an `OutOfGas` that deploys nothing. So this edit changes -/// what the transaction produces, and it does it by a route that leaves the classification and the -/// output the boundary compares exactly where they were. -#[derive(Default)] -struct DrainConstructionAction { - fired: u32, -} - -impl Inspector for DrainConstructionAction { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - // A construction frame runs no deployed code, so it has no bytecode address. - if self.fired > 0 || interp.input.bytecode_address().is_some() { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { - return; - }; - if !result.result.is_ok() { - return; - } - let remaining = result.gas.remaining(); - assert!( - result.gas.record_regular_cost(remaining), - "the fixture must be able to drain the action it found", - ); - self.fired += 1; - } -} - -/// ★ A construction frame whose pending action was drained is not an all-zero ledger. -/// -/// Every lane the boundary reads stays put: the action's classification and output are untouched, -/// so nothing is an intervention; the gas edit is staged for the frame's settlement point, and -/// that point declines to book it because the result it finally sees is a swallowed one. The -/// deposit the drained action could no longer pay is what turned it into one. -#[test] -fn test_a_drained_construction_action_is_booked() { - // Init code that returns two bytes of runtime code. - let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) - .push_number(0u64) - .push_number(0u64) - .append(CREATE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = DrainConstructionAction::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); - assert_ne!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "without the edit the creation must succeed", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it the creation cannot pay its code deposit and deploys nothing", - ); - assert_ne!( - plain.gas_used, cheated.gas_used, - "and the receipt the sender is billed on moves with it", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose contract an inspector deleted must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -/// Gas a cancelling pair moves through the result lane's two windows. -const ACTION_DELTA: u64 = 700; - -/// Raises the gas an inner call frame's pending `Return` action carries, then takes the same -/// amount back out of the result that action became. -/// -/// The two windows are one lane and one frame, and the pair nets to zero. They are still two -/// edits, made in two different callbacks, and the lane's traffic is what says so — the sum alone -/// reads as an inspector that did nothing. -#[derive(Default)] -struct CancellingActionAndResultEdits { - raised: u32, - lowered: u32, -} - -impl Inspector for CancellingActionAndResultEdits { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.raised > 0 || interp.input.bytecode_address() != Some(&CALLEE) { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { - return; - }; - if !result.result.is_ok() { - return; - } - result.gas.erase_cost(ACTION_DELTA); - self.raised += 1; - } - - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.lowered > 0 || inputs.target_address != CALLEE { - return; - } - assert!( - outcome.result.gas.record_regular_cost(ACTION_DELTA), - "the fixture must leave the result enough gas for the removal to land", - ); - self.lowered += 1; - } -} - -/// ★ An edit staged at one callback and undone at the next is two edits, not none. -/// -/// Nothing about this transaction changes: a call frame's remaining gas is read by nobody between -/// the two windows, so the pair really is invisible in what the transaction produces. That is the -/// point — the lane's traffic is the only thing that separates it from an inspector that never -/// ran, and on a *creation* frame the same pair is the shape that deletes a contract. -#[test] -fn test_cancelling_action_and_result_edits_are_booked() { - let code = BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let callee = BytecodeBuilder::default().append(STOP).build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = CancellingActionAndResultEdits::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); - assert_eq!( - cheated.gas_used, plain.gas_used, - "the pair cancels, so the receipt really is the one the EVM would have produced", - ); - assert_eq!( - cheated.inspector_ledger.conjured_gas(), - 0, - "and the conservation law must read the net, which is zero", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "but the guard must still see that the lane carried two edits: {:?}", - cheated.inspector_ledger, - ); - assert_eq!( - cheated.inspector_ledger.result.gross(), - 2 * u128::from(ACTION_DELTA), - "one edit in each window, counted where each was made", - ); -} - -// --- two edits to one lane, in opposite directions ----------------------------------------------- - -/// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. -/// -/// Both edits land on the interpreter counter, which is one signed lane. Their net is zero and -/// the transaction's envelope is unmoved — and in between them the frame read a number one higher -/// than the EVM would have given it, and wrote that number to storage. -#[derive(Default)] -struct CancellingCounterEdits { - /// 0 before the injection, 1 between the two edits, 2 once both have landed. - phase: u8, -} - -impl Inspector for CancellingCounterEdits { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - match self.phase { - 0 if interp.bytecode.opcode() == GAS => { - interp.gas.erase_cost(1); - self.phase = 1; - } - 1 => { - assert!(interp.gas.record_regular_cost(1), "the frame must afford the give-back"); - self.phase = 2; - } - _ => {} - } - } -} - -/// ★ Two edits to the same lane that cancel are not an all-zero ledger. -/// -/// The net of the gas lane really is zero — the transaction spent exactly what it would have — so -/// nothing the conservation law reads has moved. What moved is the number the frame read in -/// between, and a guard that asks the net cannot see it. The gross activity counter is what does. -#[test] -fn test_cancelling_counter_edits_are_booked() { - let code = BytecodeBuilder::default() - .append(GAS) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of - // its remaining gas is the counter the injection moved. - let limits = EvmTxRuntimeLimits::no_limits(); - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); - let mut inspector = CancellingCounterEdits::default(); - let cheated = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); - - assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)) + U256::from(1), - "the frame must have read one gas more than the EVM would have given it", - ); - assert_eq!( - cheated.total_gas_spent, plain.total_gas_spent, - "the two edits cancel, so the envelope the receipt reports is unmoved", - ); - assert_eq!( - cheated.inspector_conjured_gas(), - 0, - "and so is the law's term: this is exactly the shape a net-only reading cannot see", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "but the transaction was rewritten, and the guard has to see that: {:?}", - cheated.inspector_ledger, - ); -} - -/// Adds a refund to one child frame's result and takes the same amount out of another's. -/// -/// The frame that gets the addition returns, so its refund reaches the receipt. The frame that -/// gets the subtraction reverts, so revm discards its whole refund counter — the subtraction never -/// reaches anything. Net zero on the lane, one refund's worth of difference on the receipt. -#[derive(Default)] -struct CancellingRefundsAcrossFrames { - added: u32, - removed: u32, -} - -impl Inspector for CancellingRefundsAcrossFrames { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address == CALLEE && self.added == 0 { - outcome.result.gas.record_refund(REFUND); - self.added += 1; - } else if inputs.target_address == REVERTER && self.removed == 0 { - assert!( - outcome.result.gas.refunded() >= REFUND, - "the reverting callee must hold a refund of its own to take from, got {}", - outcome.result.gas.refunded(), - ); - outcome.result.gas.record_refund(-REFUND); - self.removed += 1; - } - } -} - -/// ★ A cancellation split across a surviving frame and a discarded one is not an all-zero ledger. -/// -/// This is the previous shape with the asymmetry made explicit: the two halves are equal and -/// opposite where the ledger books them, and only one of them is still standing by the time the -/// receipt is built. -#[test] -fn test_cancelling_refunds_across_frames_are_booked() { - let call_to = |builder: BytecodeBuilder, target: Address| { - builder - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(target) - .push_number(200_000u64) - .append(CALL) - .append(POP) - }; - let code = call_to(call_to(BytecodeBuilder::default(), CALLEE), REVERTER).append(STOP).build(); - // Both callees set a slot and clear it again, so each ends holding a refund the EVM produced. - let clearing = |builder: BytecodeBuilder| { - builder - .sstore(U256::from(RESULT_SLOT), U256::from(1u64)) - .sstore(U256::from(RESULT_SLOT), U256::ZERO) - }; - let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); - let reverting = clearing(BytecodeBuilder::default()).revert().build(); - let db = || { - base_db(code.clone()) - .account_code(CALLEE, returning.clone()) - .account_code(REVERTER, reverting.clone()) - }; - - let mut inspector = CancellingRefundsAcrossFrames::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); - assert!( - plain.total_gas_spent >= 5 * u64::try_from(REFUND).unwrap(), - "the fixture must burn enough that the EIP-3529 cap does not hide the difference", - ); - assert_eq!( - plain.gas_used - cheated.gas_used, - u64::try_from(REFUND).unwrap(), - "only the surviving frame's half reaches the receipt, so the sender pays that much less", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a receipt an inspector moved must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- an opcode skipped, and a return buffer conjured -// ---------------------------------------------- - -/// What the fixture's `SSTORE` writes when it runs. -const STORED: u64 = 0x99; - -/// The gas a cold `SSTORE` into a zero slot costs, which is what skipping it saves. -const COLD_SSTORE_SET: u64 = 22_100; - -/// How many bytes of return data the forging inspector conjures. -/// -/// Non-zero and a whole number of words, so that the `SSTORE` that stores it turns a zero slot -/// into a non-zero one — which is a different charge as well as a different value. -const CONJURED_RETURN_DATA: u64 = 96; - -/// Advances the program counter past the frame's `SSTORE`, so the EVM never executes it. -/// -/// revm's inspected loop runs this callback *before* the instruction, and the interpreter reads -/// the opcode it is about to execute from the very pointer this moves. Stepping the pointer on by -/// one byte therefore deletes one instruction from the frame: the two operands the `SSTORE` would -/// have consumed stay on the stack, the `STOP` after it runs instead, and the frame ends where it -/// was going to end. -/// -/// Nothing about this reaches a gas counter. The work is not performed, so there is nothing for -/// the EVM to meter and nothing for a gas lane to see. -#[derive(Default)] -struct SkipTheStore { - fired: u32, -} - -impl Inspector for SkipTheStore { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != SSTORE { - return; - } - interp.bytecode.relative_jump(1); - self.fired += 1; - } -} - -/// ★ A frame with an opcode skipped out from under it is not an all-zero ledger. -/// -/// The rewrite is the free-expansion shape's twin and is strictly worse: it does not merely make -/// the frame's next charge cheaper, it deletes an instruction from the frame. The transaction ends -/// with different storage *and* a smaller bill, and every gas lane reads zero because the gas that -/// went missing was never spent by anybody. -#[test] -fn test_a_skipped_opcode_is_booked() { - let code = BytecodeBuilder::default() - .sstore(U256::from(RESULT_SLOT), U256::from(STORED)) - .append(STOP) - .build(); - - let mut inspector = SkipTheStore::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(STORED), - "without the rewrite the frame stores what its bytecode says", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it, the store never happens", - ); - assert_eq!( - plain.total_gas_spent - cheated.total_gas_spent, - COLD_SSTORE_SET, - "the deleted instruction is the charge the transaction then did not pay", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction an inspector deleted an instruction from must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -/// Puts return data in front of a frame that has made no call. -/// -/// `RETURNDATASIZE` reads the buffer's length, so the frame goes on to store a number no call -/// produced. The buffer is the interpreter's own, reachable through `ReturnData` on any live -/// interpreter, and its length is a constant-time reading exactly like the memory's size. -#[derive(Default)] -struct ForgeReturnData { - fired: u32, -} - -impl Inspector for ForgeReturnData { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != RETURNDATASIZE { - return; - } - interp.return_data.set_buffer(Bytes::from(vec![0u8; CONJURED_RETURN_DATA as usize])); - self.fired += 1; - } -} - -/// ★ A frame handed return data it never received is not an all-zero ledger. -/// -/// The frame made no call, so the EVM's own buffer is empty and the store is a zero-to-zero -/// no-op. With the rewrite the same store turns a zero slot into a non-zero one, which changes the -/// post-state and costs the transaction more — in the opposite direction to every other shape -/// here, and just as invisible to a lane that only watches gas counters. -#[test] -fn test_a_forged_return_buffer_is_booked() { - let code = BytecodeBuilder::default() - .append(RETURNDATASIZE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = ForgeReturnData::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "a frame that made no call has no return data", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(CONJURED_RETURN_DATA), - "with the rewrite it reads the length of a buffer no call produced", - ); - assert!( - cheated.total_gas_spent > plain.total_gas_spent, - "and pays for the non-zero store the rewrite turned it into: {} vs {}", - cheated.total_gas_spent, - plain.total_gas_spent, - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a forged buffer changed must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- a frame invariant moved and moved back -------------------------------------------------- - -/// The caller the rewriting inspector shows the frame instead of the one that called it. -const IMPOSTOR: Address = address!("00000000000000000000000000000000000ca11e"); - -/// Moves the frame's caller for the length of one instruction, and puts it back. -/// -/// `CALLER` reads `input.caller_address`, so the frame pushes an address nobody called it from and -/// goes on to store that. The rewrite is undone in the very next callback, which is what makes the -/// shape worth pinning: the frame's identity is the one the EVM gave it at every point a *frame* -/// could be inspected — at its start, at its end, and at every callback but the two this touches. -/// -/// Nothing about it reaches a gas counter. Both runs execute the same instructions and pay the -/// same cold `SSTORE`; only the value written differs. -#[derive(Default)] -struct BorrowTheCaller { - /// The caller the EVM gave the frame, kept so it can be handed back. - original: Option
, - /// How many times each half of the rewrite ran. - moved: u32, - restored: u32, -} - -impl Inspector for BorrowTheCaller { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.moved > 0 || interp.bytecode.opcode() != CALLER { - return; - } - self.original = Some(interp.input.caller_address); - interp.input.caller_address = IMPOSTOR; - self.moved += 1; - } - - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - let Some(original) = self.original.filter(|_| self.restored == 0) else { - return; - }; - interp.input.caller_address = original; - self.restored += 1; - } -} - -/// ★ A frame invariant moved in `step` and moved back in `step_end` is not an all-zero ledger. -/// -/// The four addresses and the value a frame is identified by cannot change while it runs, which -/// makes them the readings a cheaper shim would be tempted to compare once per frame rather than -/// once per callback. This is the shape that answers that: an inspector borrows one of them for -/// exactly as long as it takes the frame to read it, and gives it back before anything outside the -/// two callbacks could look. A per-frame comparison sees the address it started with; a per-opcode -/// one sees it move twice. -#[test] -fn test_a_frame_invariant_moved_and_moved_back_is_booked() { - let code = BytecodeBuilder::default() - .append(CALLER) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = BorrowTheCaller::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); - assert_eq!( - inspector.original, - Some(crate::common::CALLER), - "and the half that gives the address back must have the one the EVM gave the frame", - ); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(crate::common::CALLER.as_slice()), - "without the rewrite the frame stores the address that called it", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(IMPOSTOR.as_slice()), - "with it, the frame stores one nobody called it from", - ); - assert_eq!( - plain.total_gas_spent, cheated.total_gas_spent, - "the two runs cost the same, so no gas lane can tell them apart", - ); - assert!( - cheated.inspector_ledger.interventions >= 2, - "each half of the rewrite is a rewrite: {:?}", - cheated.inspector_ledger, - ); -} diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index f683f54a..6bb42eb5 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -14,28 +14,20 @@ //! - `create_code_deposit_charge` — a CREATE's canonical code-deposit compute gas is weighed before //! it is recorded, so a creation that fails at its frame exit is charged nothing for a deposit //! the EVM never makes. -//! - `inspector_settlement_window` — the two windows where a rewrite lands after the accounting -//! that should have read it: a terminating opcode's `step_end`, whose counter edit reaches -//! nobody, and a precompile's classification, whose split has to follow the callback rather than -//! the recording site. -//! - `interception_gas` — the gas an inspector puts into a synthetic outcome, over the four sizings -//! it can choose relative to the envelope it was handed, and the halt direction where the choice -//! reaches nothing. //! - `interceptor_resume` — the two ways a CALL returns without a child frame ever running: a //! system contract interceptor's synthetic result, and a precompile. //! - `keyless_synthetic_halt` — the `KeylessDeploy` interceptor's synthetic halts: the two that //! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. -//! - `ledger_blind_spots` — the six rewrite shapes an all-zero ledger used to admit: a frame's -//! memory grown for free, a call or create outcome's metadata rewritten around the result inside -//! it, two edits to one signed lane that cancel — within a frame and across two of them — an -//! instruction deleted from a frame by stepping its program counter past it, and a return buffer -//! conjured in front of a frame that made no call. -//! - `measured_inspector` — the shim every inspector is wrapped in: gas an inspector writes into an -//! interpreter counter or a frame's gas limit is measured at the callback boundary, booked, and -//! kept out of enforcement, with the clamp re-derived on the spot; reviving a failed creation is -//! refused; an observation-only inspector is bit-identical to no inspector at all. +//! - `shim_measurement` — what the measurement shim books, over every shape that can move a number +//! it reports: gas written into an interpreter counter or a frame's gas limit, the two windows +//! where a rewrite lands after the accounting that should have read it, the shapes an all-zero +//! ledger used to admit, the receipt's refund and EIP-8037 dimensions, and the gas a synthetic +//! outcome carries. +//! - `shim_refusals` — the rewrites the shim refuses outright: a failed creation revived (at both +//! callbacks that can), and the classification of a result frame init produced; with the near +//! boundary, a frame the inspector answered itself, which is supported. //! - `gas_surface` — the closed enumeration one level below the cheat matrix: every field of every //! gas-carrying object an inspector callback is handed, each with a verdict, pinned against what //! upstream's own `Debug` renders. @@ -79,10 +71,6 @@ //! - `pre_execution_intrinsic_reject` — the one envelope-keeping synthetic halt REX7 cannot reach: //! for an ordinary transaction an intrinsic overrun is a validation error from REX5 on, and a //! validation error produces no receipt for any lane to account for. -//! - `refund_and_state_gas` — the two numbers on a receipt the conservation law cannot see: the -//! EIP-3529 refund, measured at the callback boundary because the EVM produces refunds too, and -//! the EIP-8037 state-gas dimension, settled from the transaction's final figures because -//! `MegaETH` produces none of it and revm propagates it by replacement. //! - `deposit_receipt_rewrite` — the transactions that break that last step. A failed OP deposit //! does get a receipt, rebuilt to report its whole gas limit after every settlement has run; the //! boundary that rebuilds it books the difference as destroyed without moving what enforces. @@ -109,7 +97,6 @@ mod detention_window; mod double_exceed_corner; mod exceptional_halt; mod frame_init_reject_burn; -mod frame_init_result_rewrite; mod frame_loop_parity; mod gas_clamp; mod gas_leakage; @@ -117,19 +104,16 @@ mod gas_surface; mod guard_pass_static_gas; mod inspector_cheat_matrix; mod inspector_common; -mod inspector_settlement_window; -mod interception_gas; mod interceptor_resume; mod keyless_synthetic_halt; mod latch_surfacing; mod late_frame_local; -mod ledger_blind_spots; -mod measured_inspector; mod modexp_gas; mod opcode_set_parity; mod parity_shapes; mod pre_execution_intrinsic_reject; mod precompile_halt; -mod refund_and_state_gas; mod result_space_tripwire; +mod shim_measurement; +mod shim_refusals; mod trusted_observer; diff --git a/crates/mega-evm/tests/rex7/measured_inspector.rs b/crates/mega-evm/tests/rex7/measured_inspector.rs deleted file mode 100644 index 58aa4533..00000000 --- a/crates/mega-evm/tests/rex7/measured_inspector.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! The measurement shim: what an inspector does to gas is measured, booked, and kept out of -//! enforcement. -//! -//! `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector -//! callback, so anything that changes across one is the inspector's doing by construction — which -//! is what makes the callback boundary a sound place to measure from. -//! -//! Each test here is one shape a rewriting inspector can take, and each pins a different half of -//! the mechanism: -//! -//! - injecting gas into a running interpreter must not buy compute headroom, and the gas clamp must -//! tighten again immediately rather than at the next checkpoint; -//! - raising a child frame's gas limit conjures gas the transaction never funded, which the ledger -//! has to account for or the conservation law breaks; -//! - resurrecting a failed contract creation is refused outright; -//! - an observation-only inspector changes nothing at all; -//! - and removing gas is measured with the same machinery as adding it. - -use crate::{ - common::{base_db, transact, transact_inspected, CALLEE, CONTRACT}, - inspector_common::{ - assert_refused, call_then_stop, countdown_loop_code, db_with_callee, deploy_then_stop, - limits, limits_with_compute, plain_run_code, try_transact_inspected, REVERTING_INIT_CODE, - REVIVED_CREATION, - }, -}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{test_utils::BytecodeBuilder, InspectorLedger, Lane, MegaHaltReason, MegaSpecId}; -use revm::{ - bytecode::opcode::{CALL, MSTORE, POP, RETURN, STOP}, - interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, - InterpreterResult, InterpreterTypes, - }, - Inspector, -}; - -/// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. -/// -/// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact -/// number a test can assert on, and so the edit lands well inside the plain segment rather than at -/// its boundary. -#[derive(Default)] -struct GasEditor { - at: u64, - delta: i64, - steps: u64, - applied: bool, -} - -impl GasEditor { - fn new(at: u64, delta: i64) -> Self { - Self { at, delta, steps: 0, applied: false } - } -} - -impl Inspector for GasEditor { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - if self.steps != self.at || self.applied { - return; - } - self.applied = true; - if self.delta >= 0 { - interp.gas.erase_cost(self.delta.unsigned_abs()); - } else { - assert!( - interp.gas.record_regular_cost(self.delta.unsigned_abs()), - "the fixture must leave enough gas for the removal to land", - ); - } - } -} - -/// Raises the gas limit of every call to [`CALLEE`] by a fixed amount. -#[derive(Default)] -struct CallGasLimitRaiser { - bonus: u64, - raises: u64, -} - -impl Inspector for CallGasLimitRaiser { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address == CALLEE { - inputs.gas_limit += self.bonus; - self.raises += 1; - } - None - } -} - -/// Rewrites every successful contract creation into a revert — the shape the frame loop has to -/// carry through to the journal. -#[derive(Default)] -struct CreateKiller { - killed: u64, -} - -impl Inspector for CreateKiller { - fn create_end( - &mut self, - _context: &mut CTX, - _inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - if outcome.result.result.is_ok() { - outcome.result.result = InstructionResult::Revert; - self.killed += 1; - } - } -} - -/// Rewrites every failed contract creation into a successful one — the shape the shim refuses. -#[derive(Default)] -struct CreateReviver; - -impl Inspector for CreateReviver { - fn create_end( - &mut self, - _context: &mut CTX, - _inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - if !outcome.result.result.is_ok() { - outcome.result.result = InstructionResult::Return; - } - } -} - -/// Counts callbacks and changes nothing. -#[derive(Default)] -struct Observer { - steps: u64, - calls: u64, - call_ends: u64, -} - -impl Inspector for Observer { - fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - } - - fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { - self.calls += 1; - None - } - - fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { - self.call_ends += 1; - } -} - -/// (i) Gas injected into a running interpreter buys no compute headroom, is booked, and the clamp -/// tightens again on the spot. -/// -/// The fixture is a checkpoint-free loop under a compute limit far below what the loop needs, so -/// the gas clamp is the only thing that can stop it: the visible counter is pinned to the compute -/// headroom and revm's own gas check rejects the crossing opcode. An inspector then writes four -/// times that headroom into the counter, mid-loop. -/// -/// Three separate mechanisms are pinned: -/// -/// - **Enforcement does not eat the injection.** The recorded compute total is identical to the -/// uninspected run's, to the gas. Without the baseline shift, the injection reads as negative -/// work and the loop is handed free headroom. -/// - **The clamp is re-derived immediately.** Usage still stops exactly at the limit. Without the -/// re-clamp the loop runs on the injected gas until the frame ends, and the frame-exit settlement -/// then records the whole overshoot — the halt still lands, but hundreds of thousands of gas -/// late. -/// - **The ledger records it.** Exactly what was injected, no more. -#[test] -fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { - const INJECTED: u64 = 20_000; - let code = countdown_loop_code(10_000); - // Far below what the loop needs, so the clamp binds for the whole run. - let intrinsic = transact(MegaSpecId::REX7, base_db(plain_run_code(0)), limits()).compute_gas; - let limits = limits_with_compute(intrinsic + 5_000); - - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); - let mut inspector = GasEditor::new(20, INJECTED as i64); - let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); - - assert!(inspector.applied, "the fixture must reach the injection point"); - assert!( - matches!(plain.halt_reason("plain"), MegaHaltReason::ComputeGasLimitExceeded { .. }), - "fixture check: the uninspected run must stop on the compute limit, got {:?}", - plain.halt_reason("plain"), - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "the injection must be neither counted as work nor deducted from it, and the re-derived \ - clamp must stop the loop at the same opcode the uninspected run stopped at; \ - inspected result {:?}", - inspected.result, - ); - assert!( - matches!( - inspected.halt_reason("inspected"), - MegaHaltReason::ComputeGasLimitExceeded { .. } - ), - "injected gas must not turn a compute-limit halt into something else, got {:?}", - inspected.halt_reason("inspected"), - ); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::once(i128::from(INJECTED)), - "the ledger must hold exactly what was injected", - ); - assert_eq!(inspected.inspector_ledger.env, Lane::default(), "no frame envelope was touched"); - assert_eq!( - i128::from(inspected.total_gas_spent) + i128::from(INJECTED), - i128::from(plain.total_gas_spent), - "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ - spends exactly that much less than the uninspected run", - ); -} - -/// (v) The same machinery, in the other direction: gas removed from a running interpreter is -/// booked as a negative entry and is not charged as work. -/// -/// Under an active clamp the removal comes out of the hidden remainder rather than the visible -/// counter — the frame has more EVM gas than compute headroom, and destroying EVM gas does not -/// shrink the headroom — so the transaction runs to the same successful end while spending exactly -/// the removed amount more. -#[test] -fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { - const REMOVED: u64 = 1_000; - let code = plain_run_code(200); - - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = GasEditor::new(20, -(REMOVED as i64)); - let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - - assert!(inspector.applied, "the fixture must reach the removal point"); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::once(-i128::from(REMOVED)), - "the ledger must hold the removal as a negative entry", - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "gas the inspector destroyed is not work the EVM performed", - ); - assert_eq!( - inspected.total_gas_spent, - plain.total_gas_spent + REMOVED, - "the removed gas never comes back, so the envelope is exactly that much larger", - ); -} - -/// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the -/// envelope only balances once the ledger accounts for it. -/// -/// The caller's `CALL` opcode debited the gas it forwards before any inspector callback ran, so the -/// bonus the inspector adds is paid for by nobody. The child hands it straight back on return, and -/// the transaction ends up spending exactly that much less than the uninspected run. -/// -/// Without the `env` lane the conservation law derives a destroyed total that is short by the -/// bonus, and the envelope assertion inside `execute_transaction` fails on the spot. -#[test] -fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { - const BONUS: u64 = 10_000; - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(50_000u64) // gas - .append(CALL) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let build_db = || db_with_callee(code.clone(), callee.clone()); - - let plain = transact(MegaSpecId::REX7, build_db(), limits()); - let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; - let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); - - assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!(inspected.result.is_success(), "the inner call must still succeed"); - assert_eq!( - inspected.inspector_ledger.env, - Lane::once(i128::from(BONUS)), - "the ledger must hold exactly the gas the inspector added to the child's envelope", - ); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::default(), - "no interpreter counter was touched" - ); - assert_eq!( - inspected.total_gas_spent + BONUS, - plain.total_gas_spent, - "the child returns the conjured gas to its caller, so the transaction spends that much less", - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "a wider envelope is not more work: the child's compute budget comes from the compute \ - tracker, not from its gas limit", - ); -} - -/// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. -/// -/// A callback that returns a synthetic outcome has intercepted the frame: no frame is built from -/// the inputs, so no edit of theirs can widen an envelope. The gas that outcome carries is the -/// inspector's own choice and has nothing to do with the edit — here it is deliberately the -/// original forwarded amount, so the transaction really does conjure nothing and the identity has -/// to close at zero. -/// -/// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the -/// conservation law would come out over by the bonus — the same failure as not booking a real one, -/// with the sign flipped. -/// -/// The interception itself is booked, on the lane that carries rewrites rather than gas: answering -/// a frame the EVM was about to build changes what the transaction did, whatever it costs. -#[test] -fn test_an_intercepting_callback_books_no_envelope_adjustment() { - /// Raises the child's gas limit and then intercepts the call, handing back an outcome built - /// from the amount the caller actually forwarded. - #[derive(Default)] - struct Interceptor { - intercepted: u64, - } - - impl Inspector for Interceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - let forwarded = inputs.gas_limit; - inputs.gas_limit += 10_000; - self.intercepted += 1; - Some(CallOutcome::new( - InterpreterResult::new(InstructionResult::Stop, Bytes::new(), Gas::new(forwarded)), - inputs.return_memory_offset.clone(), - )) - } - } - - let callee = plain_run_code(20); - let code = call_then_stop(CALLEE, 50_000); - let db = db_with_callee(code, callee); - - let mut inspector = Interceptor::default(); - let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); - assert_eq!( - inspected.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ - itself a rewrite", - ); - assert_eq!(inspected.inspector_ledger.conjured_gas(), 0, "no gas lane may move on this shape"); -} - -/// (iii) A `create_end` that turns a failed contract creation into a successful one is refused, -/// loudly. -/// -/// By that point revm has already reverted the frame's journal checkpoint and already declined to -/// deposit any code, so the rewrite would report a deployment that did not happen. The shim -/// restores the original classification and refuses to let the transaction produce a receipt at -/// all: debug builds assert, release builds surface the refusal as an `EVMError`. -#[test] -fn test_reviving_a_failed_creation_is_refused() { - let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); - - assert_refused(REVIVED_CREATION, || { - let mut inspector = CreateReviver; - try_transact_inspected(db.clone(), limits(), &mut inspector) - }); -} - -/// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. -/// -/// A callback that returns a synthetic outcome skips the frame init entirely: no frame is built, -/// and the settlement that books what a refused frame init destroys never used to run on this -/// path. A halting outcome hands nothing back to the caller, so the transaction spends that -/// envelope with no compute total to show for it — which is exactly what the conservation law is -/// stated over, and what it goes red on. -#[test] -fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { - /// Intercepts the call to [`CALLEE`] with an exceptional halt, keeping the forwarded gas. - #[derive(Default)] - struct HaltingInterceptor { - intercepted: u64, - forwarded: u64, - } - - impl Inspector for HaltingInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - self.forwarded = inputs.gas_limit; - Some(CallOutcome::new( - InterpreterResult::new( - InstructionResult::OutOfGas, - Bytes::new(), - Gas::new(inputs.gas_limit), - ), - inputs.return_memory_offset.clone(), - )) - } - } - - let code = call_then_stop(CALLEE, 50_000); - let db = db_with_callee(code, plain_run_code(20)); - - let mut inspector = HaltingInterceptor::default(); - let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); - assert_eq!( - inspected.destroyed, inspector.forwarded, - "the whole intercepted envelope is destroyed — nothing hands it back", - ); - assert_eq!( - inspected.compute_gas, - inspected.enforced() + inspected.destroyed, - "and it is reported without being enforced", - ); -} - -/// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the -/// state has to follow it. -/// -/// This is the rewrite direction there is something behind: the constructor ran, the deposit -/// predicates passed, and the inspector is telling the caller the frame failed. If the journal -/// decision were taken before the callback, the caller would be handed a failure over a deployed -/// contract, with the constructor's storage writes committed underneath it. -#[test] -fn test_killing_a_successful_creation_rolls_its_state_back() { - // Init code that stores to slot 1 and returns a two-byte runtime code. - let init_code: Vec = BytecodeBuilder::default() - .sstore(U256::from(1), U256::from(7)) - .push_number(0x6000u64) - .push_number(0u64) - .append(MSTORE) - .push_number(2u64) // size - .push_number(30u64) // offset: the last two bytes of the word just stored - .append(RETURN) - .build() - .to_vec(); - - let code = deploy_then_stop(&init_code); - - let deployed = CONTRACT.create(0); - - // The uninspected run deploys, so the rewrite has something to undo. - let mut observer = Observer::default(); - let plain = - transact_inspected(MegaSpecId::REX7, base_db(code.clone()), limits(), &mut observer); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); - assert!( - !deployed_account.info.is_empty_code_hash(), - "the fixture must deploy code for the rewrite to have something to undo", - ); - - let mut killer = CreateKiller::default(); - let killed = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut killer); - - assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); - assert!( - killed.state.get(&deployed).is_none_or(|account| account.info.is_empty_code_hash()), - "a creation the inspector failed must leave no code at {deployed}", - ); - assert_eq!( - killed - .state - .get(&deployed) - .and_then(|account| account.storage.get(&U256::from(1))) - .map(|slot| slot.present_value()) - .unwrap_or_default(), - U256::ZERO, - "and none of the constructor's storage writes", - ); -} - -/// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. -/// -/// This is the property every tracer in production depends on. The comparison is against a run with -/// no inspector attached at all, across every number the transaction reports and the state it -/// produced — not just the ones the ledger touches. -#[test] -fn test_an_observing_inspector_changes_nothing() { - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .sstore(U256::from(0x20), U256::from(0x99)) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let build_db = || db_with_callee(code.clone(), callee.clone()); - - let plain = transact(MegaSpecId::REX7, build_db(), limits()); - let mut inspector = Observer::default(); - let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); - - assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); - assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); - assert_eq!(inspector.call_ends, 2, "every call must be paired"); - - assert!( - inspected.inspector_ledger.is_zero(), - "an observation-only inspector must leave an empty ledger; got {:?}", - inspected.inspector_ledger, - ); - assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); - assert_eq!(inspected.compute_gas, plain.compute_gas); - assert_eq!(inspected.enforced(), plain.enforced()); - assert_eq!(inspected.destroyed, plain.destroyed); - assert_eq!(inspected.data_size, plain.data_size); - assert_eq!(inspected.kv_updates, plain.kv_updates); - assert_eq!(inspected.state_growth, plain.state_growth); - assert_eq!(inspected.gas_used, plain.gas_used); - assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); - assert_eq!(inspected.terms, plain.terms); - assert_eq!(inspected.state, plain.state, "the produced state must be identical"); -} - -/// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term -/// is zero — the shape every consumer of this API sees in practice. -/// -/// The stronger property is what the field is *for*: an all-zero ledger is a consumer's guarantee -/// that the gas numbers next to it are the EVM's own, so it has to be exactly zero rather than -/// merely small. A fixture that makes an inner call and writes storage is used, so the assertion -/// covers a transaction with something for a lane to have picked up. -#[test] -fn test_an_uninspected_transaction_reports_an_empty_ledger() { - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .sstore(U256::from(1), U256::from(9)) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let db = db_with_callee(code, callee); - - let plain = transact(MegaSpecId::REX7, db, limits()); - - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!( - plain.terms.non_compute_gas > 0, - "fixture check: the transaction must have moved a lane other than compute", - ); - assert_eq!( - plain.inspector_ledger, - InspectorLedger::default(), - "no inspector ran, so every lane must be untouched", - ); - assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); -} diff --git a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs b/crates/mega-evm/tests/rex7/refund_and_state_gas.rs deleted file mode 100644 index f5f3eaff..00000000 --- a/crates/mega-evm/tests/rex7/refund_and_state_gas.rs +++ /dev/null @@ -1,653 +0,0 @@ -//! The two numbers on a receipt that the conservation law cannot see, and the lanes that do. -//! -//! The law is stated over `total_gas_spent`, which is `limit - remaining`. A transaction's receipt -//! carries two more figures that arithmetic does not reach: the EIP-3529 refund, which decides what -//! the sender actually pays, and the EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and its -//! `state_gas_spent` counter — which decides how much of the envelope the receipt counts as spent -//! at all. -//! -//! Both are reachable from every callback that is handed a `Gas`, and both were unmeasured. The -//! shapes here are what the two lanes now book, and each pins the *reason* its lane is measured -//! where it is: -//! -//! - a **refund** is a quantity the EVM also produces, so only a difference across a callback -//! isolates the inspector's share — the lane is measured at the boundary, and is nominal in both -//! the senses that can make it differ from what reaches the receipt (the EIP-3529 cap, and the -//! chain of successful frame returns an edit has to survive); -//! - a **reservoir** is a quantity `MegaETH` never produces at all, and one revm propagates by -//! replacement rather than by accumulation, so a boundary difference would book edits the EVM -//! goes on to erase. The lane is settled once, from the number the transaction ends with, which -//! is exactly the surviving part and is the inspector's in whole. - -use crate::{ - common::{transact, transact_inspected, Outcome, CALLEE}, - inspector_common::{append_call, db_with_callee, limits}, -}; -use alloy_primitives::{Bytes, U256}; -use mega_evm::{ - test_utils::{BytecodeBuilder, MemoryDatabase}, - ConservationTerms, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, -}; -use revm::{ - bytecode::opcode::{POP, STOP}, - interpreter::{ - interpreter_types::LoopControl, CallInputs, CallOutcome, Gas, InstructionResult, - Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, - }, - Inspector, -}; - -/// Gas the fixture's inner `CALL` forwards. -const INNER_CALL_GAS: u64 = 200_000; - -/// The refund an edit writes, small enough to stay under the EIP-3529 cap. -const REFUND: i64 = 2_000; -/// A refund large enough that the cap keeps part of it out of the receipt. -const OVERSIZED_REFUND: i64 = 60_000; -/// The EIP-8037 pool an edit fills. -const RESERVOIR: u64 = 10_000; -/// The EIP-8037 spend an edit writes. -const STATE_GAS: i64 = 5_000; - -/// Slot the top frame writes. -const TOP_SLOT: u64 = 0x10; -/// Slot the callee writes. -const CALLEE_SLOT: u64 = 0x20; -/// Slot the callee sets and clears, so the frame ends holding a refund of the EVM's own making. -const CLEARED_SLOT: u64 = 0x30; - -// --- the fixture ------------------------------------------------------------------------------- - -/// How the fixture's callee ends, which is what decides whether its refund travels. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Callee { - /// Writes storage, produces a refund by clearing a slot it just set, and returns. - Returning, - /// Writes storage and reverts, so the EVM discards everything the frame held. - Reverting, -} - -fn caller_code() -> Bytes { - append_call(BytecodeBuilder::default(), CALLEE, INNER_CALL_GAS, 0) - .append(POP) - .sstore(U256::from(TOP_SLOT), U256::from(1u64)) - .append(STOP) - .build() -} - -fn callee_code(callee: Callee) -> Bytes { - let builder = BytecodeBuilder::default() - .sstore(U256::from(CALLEE_SLOT), U256::from(1u64)) - // Set and clear, so the frame ends holding a refund the EVM itself produced. - .sstore(U256::from(CLEARED_SLOT), U256::from(1u64)) - .sstore(U256::from(CLEARED_SLOT), U256::ZERO); - match callee { - Callee::Returning => builder.append(STOP).build(), - Callee::Reverting => builder.revert().build(), - } -} - -fn db_for(callee: Callee) -> MemoryDatabase { - db_with_callee(caller_code(), callee_code(callee)) -} - -// --- the edit ---------------------------------------------------------------------------------- - -/// One edit, applied once, to one of the `Gas` objects a callback is handed. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Edit { - /// Add to the running interpreter's refund counter. - RefundAtStep(i64), - /// Add to the finished inner call's refund counter. - RefundAtCallEnd(i64), - /// Fill the running interpreter's EIP-8037 pool. - ReservoirAtStep, - /// Fill it at the one moment the frame is holding a `NewFrame` action, whose child overwrites - /// the pool on the way back. - ReservoirAtSuspension, - /// Fill the pool the inner call's inputs seed the child frame with. - ReservoirOnInputs, - /// Fill the finished inner call's pool. - ReservoirAtCallEnd, - /// Write the running interpreter's EIP-8037 spend counter. - StateGasAtStep, - /// Write the finished inner call's spend counter. - StateGasAtCallEnd, - /// Answer the inner call with a synthetic outcome that echoes the envelope and carries - /// neither figure — the control the two below are read against. - InterceptEcho, - /// The same, carrying a refund the frame never earned. - InterceptWithRefund, - /// The same, carrying an EIP-8037 pool. - InterceptWithReservoir, -} - -impl Edit { - /// Whether this edit answers the frame itself instead of letting the EVM build it. - const fn intercepts(self) -> bool { - matches!( - self, - Self::InterceptEcho | Self::InterceptWithRefund | Self::InterceptWithReservoir - ) - } -} - -/// Applies one [`Edit`], once, and records that it landed. -#[derive(Debug)] -struct Editor { - edit: Edit, - fired: u32, - steps: u64, -} - -impl Editor { - const fn new(edit: Edit) -> Self { - Self { edit, fired: 0, steps: 0 } - } -} - -impl Inspector for Editor { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - if self.fired > 0 || self.steps != 4 { - return; - } - match self.edit { - Edit::RefundAtStep(amount) => interp.gas.record_refund(amount), - Edit::ReservoirAtStep => interp.gas.set_reservoir(RESERVOIR), - Edit::StateGasAtStep => interp.gas.set_state_gas_spent(STATE_GAS), - _ => return, - } - self.fired += 1; - } - - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || self.edit != Edit::ReservoirAtSuspension { - return; - } - // The one window where the pool the frame holds is not the pool that travels: the child - // this action builds was already sized from the pre-edit value, and its own pool - // overwrites this one when it returns. - if !matches!(interp.bytecode.action(), Some(InterpreterAction::NewFrame(_))) { - return; - } - interp.gas.set_reservoir(RESERVOIR); - self.fired += 1; - } - - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if self.fired > 0 || inputs.target_address != CALLEE { - return None; - } - if self.edit == Edit::ReservoirOnInputs { - inputs.reservoir += RESERVOIR; - self.fired += 1; - return None; - } - if !self.edit.intercepts() { - return None; - } - // The echo convention every tool that intercepts follows: hand back exactly what was - // forwarded, so the gas lanes see nothing and only the figures under test move. - let mut gas = Gas::new(inputs.gas_limit); - match self.edit { - Edit::InterceptWithRefund => gas.record_refund(REFUND), - Edit::InterceptWithReservoir => gas.set_reservoir(RESERVOIR), - _ => {} - } - self.fired += 1; - Some(CallOutcome::new( - InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas), - inputs.return_memory_offset.clone(), - )) - } - - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.fired > 0 || inputs.target_address != CALLEE { - return; - } - match self.edit { - Edit::RefundAtCallEnd(amount) => outcome.result.gas.record_refund(amount), - Edit::ReservoirAtCallEnd => outcome.result.gas.set_reservoir(RESERVOIR), - Edit::StateGasAtCallEnd => outcome.result.gas.set_state_gas_spent(STATE_GAS), - _ => return, - } - self.fired += 1; - } -} - -/// Runs the fixture with no inspector at all. -fn transact_plain(callee: Callee) -> Outcome { - transact(MegaSpecId::REX7, db_for(callee), limits()) -} - -/// Runs it with one edit applied, asserting the edit landed exactly once. -fn transact_edited(callee: Callee, edit: Edit) -> Outcome { - let mut editor = Editor::new(edit); - let outcome = transact_inspected(MegaSpecId::REX7, db_for(callee), limits(), &mut editor); - assert_eq!( - editor.fired, 1, - "{edit:?}: the fixture must reach the edit's callback exactly once", - ); - outcome -} - -// --- the fixture's own assumptions --------------------------------------------------------------- - -/// The uninspected run is what the cells below assume it is: it succeeds, it produces a refund of -/// its own, and it reports no EIP-8037 dimension at all. -#[test] -fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { - let plain = transact_plain(Callee::Returning); - assert!(plain.result.is_success(), "{:?}", plain.result); - assert!( - plain.refunded() > 0, - "the callee's cleared slot must leave a refund for the lowering cell to take from", - ); - assert_eq!( - plain.gas_used, - plain.total_gas_spent - plain.refunded(), - "the receipt's two gas numbers differ by exactly the refund", - ); - assert_eq!(plain.state_gas_spent(), 0, "EIP-8037 is off on every MegaETH path"); - assert!(plain.inspector_ledger.is_zero(), "no inspector ran: {:?}", plain.inspector_ledger); -} - -// --- the refund lane -// ------------------------------------------------------------------------------ - -/// A refund written into a running interpreter's counter is booked, and moves what the sender pays -/// without moving the envelope. -#[test] -fn test_a_refund_written_into_a_live_interpreter_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - "the shim must book the refund and nothing else", - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "a refund does not move the envelope, which is why the law cannot see it", - ); - assert_eq!( - edited.refunded(), - plain.refunded() + u64::try_from(REFUND).unwrap(), - "but it does move the receipt's refund", - ); - assert_eq!( - edited.gas_used, - plain.gas_used - u64::try_from(REFUND).unwrap(), - "and through it what the sender pays", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, 0, - "the refund lane is deliberately not a term of the law", - ); - assert!(!edited.inspector_ledger.is_zero(), "and the block guard has to see it"); -} - -/// The same edit made at the last callback that holds the finished frame's result. -#[test] -fn test_a_refund_written_into_a_finished_frame_result_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - ); - assert_eq!(edited.refunded(), plain.refunded() + u64::try_from(REFUND).unwrap()); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent); -} - -/// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction -/// would report an inspector that raised the sender's bill as having done nothing. -#[test] -fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { - let plain = transact_plain(Callee::Returning); - assert!( - plain.refunded() >= u64::try_from(REFUND).unwrap(), - "fixture check: there must be a refund to take from, got {}", - plain.refunded(), - ); - - let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, - ); - assert_eq!(edited.refunded(), plain.refunded() - u64::try_from(REFUND).unwrap()); - assert_eq!( - edited.gas_used, - plain.gas_used + u64::try_from(REFUND).unwrap(), - "the sender pays more, by exactly what was taken", - ); -} - -/// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. -/// -/// The cap applies to the transaction's whole refund at once, over a sum in which the EVM's own -/// refunds and an inspector's are indistinguishable, at a point past every callback. Splitting it -/// between them needs a priority rule the protocol does not have, so the lane states the edit and -/// the receipt states the effect — and the two are allowed to differ. -#[test] -fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - refund: Lane::once(i128::from(OVERSIZED_REFUND)), - ..InspectorLedger::default() - }, - "the lane carries the nominal edit", - ); - assert_eq!( - edited.refunded(), - edited.total_gas_spent / 5, - "while the receipt carries the EIP-3529 cap", - ); - assert!( - edited.refunded() < plain.refunded() + u64::try_from(OVERSIZED_REFUND).unwrap(), - "fixture check: the cap must actually bind, or this cell asserts nothing", - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); -} - -/// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. -/// -/// revm hands a frame's refund to its caller only on success, so this edit dies with the frame. -/// The lane books it anyway, because the alternative is a rule that has to track every frame -/// between the edit and the top — and because a lane that under-reports lets exactly the shape -/// this module exists to catch into a block, while over-reporting costs nothing: the law has no -/// term for it. -#[test] -fn test_a_refund_the_frame_chain_discards_is_still_booked() { - let plain = transact_plain(Callee::Reverting); - let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - "the lane books the edit", - ); - assert_eq!( - edited.refunded(), - plain.refunded(), - "the receipt is unmoved: a reverting frame hands its caller no refund", - ); - assert_eq!(edited.gas_used, plain.gas_used); -} - -// --- the EIP-8037 state-gas dimension ------------------------------------------------------------ - -/// A reservoir an inspector fills is gas the transaction never funded: the receipt reports that -/// much less spent, and the law needs it back. -#[test] -fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - RESERVOIR, - "the receipt counts the pool as unspent, so the envelope shrinks by exactly it", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, - i128::from(RESERVOIR), - "which is why this lane, unlike the refund one, is a term of the law", - ); -} - -/// The same, written into the pool a call's inputs seed the child frame with. -#[test] -fn test_a_reservoir_written_into_a_frame_input_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - // The inputs came back changed in a field the envelope lane does not cover, which the - // rewrite comparison books on its own. - interventions: 1, - ..InspectorLedger::default() - }, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); -} - -/// And into the finished frame's own pool, which its caller takes whatever the classification. -#[test] -fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); -} - -/// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the -/// run it produces is the run the EVM would have produced alone. -/// -/// This is the window that decides where the lane is measured. A difference taken across this -/// callback would say `RESERVOIR` was conjured; the transaction says otherwise, and the settlement -/// point is the only reading that agrees with it. -#[test] -fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); - - assert!( - edited.inspector_ledger.is_zero(), - "an edit the child frame's own pool replaces moved nothing: {:?}", - edited.inspector_ledger, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent); - assert_eq!(edited.gas_used, plain.gas_used); - assert_eq!(edited.refunded(), plain.refunded()); -} - -/// The spend counter's own effect on the receipt: a successful transaction reports it, whether or -/// not EIP-8037 is enabled. -#[test] -fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); - - assert_eq!(plain.state_gas_spent(), 0, "fixture check"); - assert_eq!( - edited.state_gas_spent(), - u64::try_from(STATE_GAS).unwrap(), - "the receipt reports what was written", - ); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - state_gas: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "the envelope is untouched, so this lane is not a term of the law either", - ); - assert_eq!(edited.terms.inspector_conjured_gas, 0); -} - -/// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend -/// counter back into its caller's pool, which turns a state-gas edit into an envelope-moving one. -/// -/// The lane that catches it is the reservoir's, not the state-gas one, because the fold has -/// already happened by the time either is read. That is the second reason the two are settled from -/// the transaction's final figures rather than differenced across a callback. -#[test] -fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { - let plain = transact_plain(Callee::Reverting); - let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - "the spend counter of a reverting frame arrives in its caller as a pool", - ); - assert_eq!( - edited.state_gas_spent(), - 0, - "and not as a spend: a failing frame's counter is not accumulated", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), - "so the envelope moves, and the law's term has to move with it", - ); -} - -// --- a frame the inspector answers itself -// --------------------------------------------------------- - -/// A synthetic outcome carries figures of its own, and there is no EVM-produced number on the -/// other side of the callback to difference against — so the whole of what it carries is the -/// inspector's, measured against nothing rather than against a baseline. -/// -/// The echo control is what makes the two cells below readings of the figures rather than of the -/// interception: it moves the gas lanes not at all, which is the convention every tool that -/// intercepts follows. -#[test] -fn test_a_synthetic_outcome_carries_its_own_figures() { - let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); - assert_eq!( - echo.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "an echoing interception moves no figure at all", - ); - - let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); - assert_eq!( - refunding.inspector_ledger, - InspectorLedger { - refund: Lane::once(i128::from(REFUND)), - interventions: 1, - ..InspectorLedger::default() - }, - "the refund a frame that never ran hands back is the inspector's in whole", - ); - assert_eq!( - refunding.refunded(), - echo.refunded() + u64::try_from(REFUND).unwrap(), - "and it reaches the receipt: the outcome succeeded, so its caller records it", - ); - assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); - - let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); - assert_eq!( - pooled.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - interventions: 1, - ..InspectorLedger::default() - }, - ); - assert_eq!( - pooled.total_gas_spent, - echo.total_gas_spent - RESERVOIR, - "a pool does move the envelope, wherever it came from", - ); -} - -// --- the frozen specs -// ----------------------------------------------------------------------------- - -/// On a frozen spec the two lanes report and settle nothing. -/// -/// The shim is not spec-gated, and must not be: the block guard has to see a rewritten receipt -/// whichever spec produced it. What is gated is the accounting the lanes feed, so a frozen spec's -/// own numbers have to be exactly what they were — which is what this reads, by comparing an -/// edited run against an unedited one on the same spec. -#[test] -fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { - const REX6: MegaSpecId = MegaSpecId::REX6; - fn run(edit: Option) -> Outcome { - let db = db_for(Callee::Returning); - let limits = EvmTxRuntimeLimits::from_spec(REX6); - match edit { - Some(edit) => { - let mut editor = Editor::new(edit); - let outcome = transact_inspected(REX6, db, limits, &mut editor); - assert_eq!(editor.fired, 1, "{edit:?} must land"); - outcome - } - None => transact(REX6, db, limits), - } - } - - let plain = run(None); - assert!(plain.inspector_ledger.is_zero()); - - for (edit, expected) in [ - ( - Edit::RefundAtStep(REFUND), - InspectorLedger { - refund: Lane::once(i128::from(REFUND)), - ..InspectorLedger::default() - }, - ), - ( - Edit::ReservoirAtStep, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ), - ( - Edit::StateGasAtStep, - InspectorLedger { - state_gas: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - ), - ] { - let edited = run(Some(edit)); - assert_eq!(edited.inspector_ledger, expected, "{edit:?}: the lane reports on every spec"); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "{edit:?}: a frozen spec's compute total must not move", - ); - assert_eq!(edited.destroyed, plain.destroyed, "{edit:?}: nor its destroyed lane"); - // `inspector_conjured_gas` is a reading of the ledger rather than something the - // transaction recorded, so it moves with the lane on every spec. Every other term is what - // a frozen spec must leave alone. - assert_eq!( - ConservationTerms { inspector_conjured_gas: 0, ..edited.terms }, - plain.terms, - "{edit:?}: nothing a frozen spec records may move", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, - edited.inspector_ledger.conjured_gas(), - "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", - ); - } -} diff --git a/crates/mega-evm/tests/rex7/shim_measurement.rs b/crates/mega-evm/tests/rex7/shim_measurement.rs new file mode 100644 index 00000000..ca7dff98 --- /dev/null +++ b/crates/mega-evm/tests/rex7/shim_measurement.rs @@ -0,0 +1,3173 @@ +//! What the measurement shim books, over every shape that can move a number it reports. +//! +//! `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector +//! callback, so anything that changes across one is the inspector's doing by construction — which +//! is what makes the callback boundary a sound place to measure from. Every fixture here is the +//! same comparison: one run with an inspector against one without, over the same fixture, with the +//! conservation law checked on both by the shared driver. +//! +//! The sections, in the order they appear: +//! +//! 1. **The shim itself** — gas written into an interpreter's counter or a frame's gas limit is +//! measured, booked, and kept out of enforcement, with the clamp re-derived on the spot; an +//! observation-only inspector is bit-identical to no inspector at all. +//! 2. **The two settlement windows** — a terminating opcode's `step_end`, whose counter edit +//! reaches nobody, and a precompile's classification, whose split has to follow the callback +//! rather than the recording site. +//! 3. **The blind spots** — the rewrite shapes an all-zero ledger used to admit: a frame's memory +//! grown for free, an outcome's metadata rewritten around the result inside it, two edits to one +//! signed lane that cancel, an instruction deleted by stepping the program counter past it, and +//! a return buffer conjured in front of a frame that made no call. +//! 4. **The receipt's other two numbers** — the EIP-3529 refund, measured at the callback boundary +//! because the EVM produces refunds too, and the EIP-8037 state-gas dimension, settled from the +//! transaction's final figures because `MegaETH` produces none of it and revm propagates it by +//! replacement. +//! 5. **Interception** — the gas an inspector puts into a synthetic outcome, over the four sizings +//! it can choose relative to the envelope it was handed, and the halt direction where the choice +//! reaches nothing. +//! +//! The rewrites the shim *refuses* are in `shim_refusals.rs`; the exhaustive callback × shape +//! sweep is in `inspector_cheat_matrix.rs`. + +use crate::{ + common::{ + base_db, transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, + CALLEE, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, + }, + inspector_common::{ + append_call, call_then_stop, countdown_loop_code, db_with_callee, deploy_then_stop, limits, + limits_with_compute, plain_and_cheated, plain_run_code, + }, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + kzg_point_evaluation, + test_utils::{BytecodeBuilder, MemoryDatabase}, + ConservationTerms, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaHaltReason, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{ + CALL, CALLER, CREATE, GAS, INVALID, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, + SSTORE, STOP, + }, + context::{Cfg, ContextTr}, + handler::FrameResult, + interpreter::{ + interpreter::EthInterpreter, + interpreter_types::{InputsTr, Jumps, LoopControl, MemoryTr, ReturnData}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, + }, + Inspector, +}; +use sha2::{Digest, Sha256}; +use std::vec::Vec; + +// === 1. the shim itself ======================================================================= +// +// The measurement shim: what an inspector does to gas is measured, booked, and kept out of +// enforcement. +// +// `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector +// callback, so anything that changes across one is the inspector's doing by construction — which +// is what makes the callback boundary a sound place to measure from. +// +// Each test here is one shape a rewriting inspector can take, and each pins a different half of +// the mechanism: +// +// - injecting gas into a running interpreter must not buy compute headroom, and the gas clamp must +// tighten again immediately rather than at the next checkpoint; +// - raising a child frame's gas limit conjures gas the transaction never funded, which the ledger +// has to account for or the conservation law breaks; +// - an observation-only inspector changes nothing at all; +// - and removing gas is measured with the same machinery as adding it. + +/// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. +/// +/// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact +/// number a test can assert on, and so the edit lands well inside the plain segment rather than at +/// its boundary. +#[derive(Default)] +struct GasEditor { + at: u64, + delta: i64, + steps: u64, + applied: bool, +} + +impl GasEditor { + fn new(at: u64, delta: i64) -> Self { + Self { at, delta, steps: 0, applied: false } + } +} + +impl Inspector for GasEditor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.steps != self.at || self.applied { + return; + } + self.applied = true; + if self.delta >= 0 { + interp.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + interp.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave enough gas for the removal to land", + ); + } + } +} + +/// Raises the gas limit of every call to [`CALLEE`] by a fixed amount. +#[derive(Default)] +struct CallGasLimitRaiser { + bonus: u64, + raises: u64, +} + +impl Inspector for CallGasLimitRaiser { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address == CALLEE { + inputs.gas_limit += self.bonus; + self.raises += 1; + } + None + } +} + +/// Rewrites every successful contract creation into a revert — the shape the frame loop has to +/// carry through to the journal. +#[derive(Default)] +struct CreateKiller { + killed: u64, +} + +impl Inspector for CreateKiller { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Revert; + self.killed += 1; + } + } +} + +/// Counts callbacks and changes nothing. +#[derive(Default)] +struct Observer { + steps: u64, + calls: u64, + call_ends: u64, +} + +impl Inspector for Observer { + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.calls += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.call_ends += 1; + } +} + +/// (i) Gas injected into a running interpreter buys no compute headroom, is booked, and the clamp +/// tightens again on the spot. +/// +/// The fixture is a checkpoint-free loop under a compute limit far below what the loop needs, so +/// the gas clamp is the only thing that can stop it: the visible counter is pinned to the compute +/// headroom and revm's own gas check rejects the crossing opcode. An inspector then writes four +/// times that headroom into the counter, mid-loop. +/// +/// Three separate mechanisms are pinned: +/// +/// - **Enforcement does not eat the injection.** The recorded compute total is identical to the +/// uninspected run's, to the gas. Without the baseline shift, the injection reads as negative +/// work and the loop is handed free headroom. +/// - **The clamp is re-derived immediately.** Usage still stops exactly at the limit. Without the +/// re-clamp the loop runs on the injected gas until the frame ends, and the frame-exit settlement +/// then records the whole overshoot — the halt still lands, but hundreds of thousands of gas +/// late. +/// - **The ledger records it.** Exactly what was injected, no more. +#[test] +fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { + const INJECTED: u64 = 20_000; + let code = countdown_loop_code(10_000); + // Far below what the loop needs, so the clamp binds for the whole run. + let intrinsic = transact(MegaSpecId::REX7, base_db(plain_run_code(0)), limits()).compute_gas; + let limits = limits_with_compute(intrinsic + 5_000); + + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); + let mut inspector = GasEditor::new(20, INJECTED as i64); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); + + assert!(inspector.applied, "the fixture must reach the injection point"); + assert!( + matches!(plain.halt_reason("plain"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "fixture check: the uninspected run must stop on the compute limit, got {:?}", + plain.halt_reason("plain"), + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "the injection must be neither counted as work nor deducted from it, and the re-derived \ + clamp must stop the loop at the same opcode the uninspected run stopped at; \ + inspected result {:?}", + inspected.result, + ); + assert!( + matches!( + inspected.halt_reason("inspected"), + MegaHaltReason::ComputeGasLimitExceeded { .. } + ), + "injected gas must not turn a compute-limit halt into something else, got {:?}", + inspected.halt_reason("inspected"), + ); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::once(i128::from(INJECTED)), + "the ledger must hold exactly what was injected", + ); + assert_eq!(inspected.inspector_ledger.env, Lane::default(), "no frame envelope was touched"); + assert_eq!( + i128::from(inspected.total_gas_spent) + i128::from(INJECTED), + i128::from(plain.total_gas_spent), + "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ + spends exactly that much less than the uninspected run", + ); +} + +/// (v) The same machinery, in the other direction: gas removed from a running interpreter is +/// booked as a negative entry and is not charged as work. +/// +/// Under an active clamp the removal comes out of the hidden remainder rather than the visible +/// counter — the frame has more EVM gas than compute headroom, and destroying EVM gas does not +/// shrink the headroom — so the transaction runs to the same successful end while spending exactly +/// the removed amount more. +#[test] +fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { + const REMOVED: u64 = 1_000; + let code = plain_run_code(200); + + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = GasEditor::new(20, -(REMOVED as i64)); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + + assert!(inspector.applied, "the fixture must reach the removal point"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::once(-i128::from(REMOVED)), + "the ledger must hold the removal as a negative entry", + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "gas the inspector destroyed is not work the EVM performed", + ); + assert_eq!( + inspected.total_gas_spent, + plain.total_gas_spent + REMOVED, + "the removed gas never comes back, so the envelope is exactly that much larger", + ); +} + +/// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the +/// envelope only balances once the ledger accounts for it. +/// +/// The caller's `CALL` opcode debited the gas it forwards before any inspector callback ran, so the +/// bonus the inspector adds is paid for by nobody. The child hands it straight back on return, and +/// the transaction ends up spending exactly that much less than the uninspected run. +/// +/// Without the `env` lane the conservation law derives a destroyed total that is short by the +/// bonus, and the envelope assertion inside `execute_transaction` fails on the spot. +#[test] +fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { + const BONUS: u64 = 10_000; + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000u64) // gas + .append(CALL) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let build_db = || db_with_callee(code.clone(), callee.clone()); + + let plain = transact(MegaSpecId::REX7, build_db(), limits()); + let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); + + assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "the inner call must still succeed"); + assert_eq!( + inspected.inspector_ledger.env, + Lane::once(i128::from(BONUS)), + "the ledger must hold exactly the gas the inspector added to the child's envelope", + ); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::default(), + "no interpreter counter was touched" + ); + assert_eq!( + inspected.total_gas_spent + BONUS, + plain.total_gas_spent, + "the child returns the conjured gas to its caller, so the transaction spends that much less", + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "a wider envelope is not more work: the child's compute budget comes from the compute \ + tracker, not from its gas limit", + ); +} + +/// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. +/// +/// A callback that returns a synthetic outcome has intercepted the frame: no frame is built from +/// the inputs, so no edit of theirs can widen an envelope. The gas that outcome carries is the +/// inspector's own choice and has nothing to do with the edit — here it is deliberately the +/// original forwarded amount, so the transaction really does conjure nothing and the identity has +/// to close at zero. +/// +/// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the +/// conservation law would come out over by the bonus — the same failure as not booking a real one, +/// with the sign flipped. +/// +/// The interception itself is booked, on the lane that carries rewrites rather than gas: answering +/// a frame the EVM was about to build changes what the transaction did, whatever it costs. +#[test] +fn test_an_intercepting_callback_books_no_envelope_adjustment() { + /// Raises the child's gas limit and then intercepts the call, handing back an outcome built + /// from the amount the caller actually forwarded. + #[derive(Default)] + struct Interceptor { + intercepted: u64, + } + + impl Inspector for Interceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + let forwarded = inputs.gas_limit; + inputs.gas_limit += 10_000; + self.intercepted += 1; + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), Gas::new(forwarded)), + inputs.return_memory_offset.clone(), + )) + } + } + + let callee = plain_run_code(20); + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, callee); + + let mut inspector = Interceptor::default(); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); + assert_eq!( + inspected.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ + itself a rewrite", + ); + assert_eq!(inspected.inspector_ledger.conjured_gas(), 0, "no gas lane may move on this shape"); +} + +/// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. +/// +/// A callback that returns a synthetic outcome skips the frame init entirely: no frame is built, +/// and the settlement that books what a refused frame init destroys never used to run on this +/// path. A halting outcome hands nothing back to the caller, so the transaction spends that +/// envelope with no compute total to show for it — which is exactly what the conservation law is +/// stated over, and what it goes red on. +#[test] +fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { + /// Intercepts the call to [`CALLEE`] with an exceptional halt, keeping the forwarded gas. + #[derive(Default)] + struct HaltingInterceptor { + intercepted: u64, + forwarded: u64, + } + + impl Inspector for HaltingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.forwarded = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::OutOfGas, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, plain_run_code(20)); + + let mut inspector = HaltingInterceptor::default(); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); + assert_eq!( + inspected.destroyed, inspector.forwarded, + "the whole intercepted envelope is destroyed — nothing hands it back", + ); + assert_eq!( + inspected.compute_gas, + inspected.enforced() + inspected.destroyed, + "and it is reported without being enforced", + ); +} + +/// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the +/// state has to follow it. +/// +/// This is the rewrite direction there is something behind: the constructor ran, the deposit +/// predicates passed, and the inspector is telling the caller the frame failed. If the journal +/// decision were taken before the callback, the caller would be handed a failure over a deployed +/// contract, with the constructor's storage writes committed underneath it. +#[test] +fn test_killing_a_successful_creation_rolls_its_state_back() { + // Init code that stores to slot 1 and returns a two-byte runtime code. + let init_code: Vec = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(7)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset: the last two bytes of the word just stored + .append(RETURN) + .build() + .to_vec(); + + let code = deploy_then_stop(&init_code); + + let deployed = CONTRACT.create(0); + + // The uninspected run deploys, so the rewrite has something to undo. + let mut observer = Observer::default(); + let plain = + transact_inspected(MegaSpecId::REX7, base_db(code.clone()), limits(), &mut observer); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); + assert!( + !deployed_account.info.is_empty_code_hash(), + "the fixture must deploy code for the rewrite to have something to undo", + ); + + let mut killer = CreateKiller::default(); + let killed = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut killer); + + assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); + assert!( + killed.state.get(&deployed).is_none_or(|account| account.info.is_empty_code_hash()), + "a creation the inspector failed must leave no code at {deployed}", + ); + assert_eq!( + killed + .state + .get(&deployed) + .and_then(|account| account.storage.get(&U256::from(1))) + .map(|slot| slot.present_value()) + .unwrap_or_default(), + U256::ZERO, + "and none of the constructor's storage writes", + ); +} + +/// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. +/// +/// This is the property every tracer in production depends on. The comparison is against a run with +/// no inspector attached at all, across every number the transaction reports and the state it +/// produced — not just the ones the ledger touches. +#[test] +fn test_an_observing_inspector_changes_nothing() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(0x20), U256::from(0x99)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || db_with_callee(code.clone(), callee.clone()); + + let plain = transact(MegaSpecId::REX7, build_db(), limits()); + let mut inspector = Observer::default(); + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); + + assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); + assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); + assert_eq!(inspector.call_ends, 2, "every call must be paired"); + + assert!( + inspected.inspector_ledger.is_zero(), + "an observation-only inspector must leave an empty ledger; got {:?}", + inspected.inspector_ledger, + ); + assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); + assert_eq!(inspected.compute_gas, plain.compute_gas); + assert_eq!(inspected.enforced(), plain.enforced()); + assert_eq!(inspected.destroyed, plain.destroyed); + assert_eq!(inspected.data_size, plain.data_size); + assert_eq!(inspected.kv_updates, plain.kv_updates); + assert_eq!(inspected.state_growth, plain.state_growth); + assert_eq!(inspected.gas_used, plain.gas_used); + assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); + assert_eq!(inspected.terms, plain.terms); + assert_eq!(inspected.state, plain.state, "the produced state must be identical"); +} + +/// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term +/// is zero — the shape every consumer of this API sees in practice. +/// +/// The stronger property is what the field is *for*: an all-zero ledger is a consumer's guarantee +/// that the gas numbers next to it are the EVM's own, so it has to be exactly zero rather than +/// merely small. A fixture that makes an inner call and writes storage is used, so the assertion +/// covers a transaction with something for a lane to have picked up. +#[test] +fn test_an_uninspected_transaction_reports_an_empty_ledger() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(9)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = db_with_callee(code, callee); + + let plain = transact(MegaSpecId::REX7, db, limits()); + + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!( + plain.terms.non_compute_gas > 0, + "fixture check: the transaction must have moved a lane other than compute", + ); + assert_eq!( + plain.inspector_ledger, + InspectorLedger::default(), + "no inspector ran, so every lane must be untouched", + ); + assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); +} + +// === 2. the two settlement windows ============================================================ +// +// The two windows in which a rewrite lands after the accounting that should have read it. +// +// Both halves of the measurement shim rest on the same claim: what the shim books is what the +// transaction's envelope actually moved by. There are two places where the number the shim reads +// and the number the envelope carries are not the same object, and each of them is a fixture +// here. +// +// - **A terminating opcode's `step_end`.** revm's inspected loop runs `step_end` *after* the +// instruction that produced the frame's action, and that action carries its own copy of the gas +// counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail settlement +// measures work against and nothing the caller will ever see, so it must move the settlement +// baseline and must not move the ledger. The two neighbouring windows — a `step_end` in +// mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of that +// rule: the frame resumes on the edited counter in both, so both are booked. +// +// - **A precompile's classification.** A precompile is answered inside the frame init and never +// becomes a child frame, so its recording site is the only place that knows the forwarded +// envelope and the work performed. The split is nonetheless settled at the frame's settlement +// point, from what that site staged, exactly as an ordinary frame's is — because a callback runs +// in between, and the classification is what decides whether the caller reclaims the remainder. +// What that callback may do to the classification is bounded: the journal decision behind a +// result frame init produced was taken before any callback ran and is not reachable from one, so +// a rewrite that moves such a result across the success / revert / halt boundary is refused and +// the settlement reads the classification the EVM produced. The cases below pin the uninspected +// split each precompile arm produces, and the refusal that keeps it the one the settlement sees. +// +// Every case here is checked by the identity `common::finish` runs on every transaction: the +// tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. + +/// Gas the edit-once inspector writes into a live interpreter's counter. +const INJECT: u64 = 1_000; + +/// Gas every probed CALL forwards. Well inside the 63/64 rule at the default transaction gas +/// limit and well inside the default compute budget, so the forwarded envelope is exactly this. +const PROBE_GAS: u64 = 1_000_000; + +/// The transaction gas limit is not what binds any fixture here — pinned at compile time, so a +/// change to the shared limit cannot silently turn a destroyed-remainder case into an +/// out-of-gas one. +const _: () = assert!(DEFAULT_TX_GAS_LIMIT > 10 * PROBE_GAS); + +/// The identity precompile. +const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); +/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. +const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); +/// KZG point evaluation. +const KZG: Address = address!("000000000000000000000000000000000000000a"); + +// --- A: the window a terminating opcode's `step_end` sits in --------------------------------- + +/// Which of the three `step_end` windows an edit is aimed at, told apart by the action the +/// instruction that just ran left behind. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Window { + /// No action yet: the frame carries on, and the edited counter is what it carries on with. + MidFrame, + /// A `NewFrame` action: the frame suspends into a child and then resumes on this counter. + Suspending, + /// A `Return` action: the frame is over, and the gas it hands back was copied into the action + /// before this callback ran. + Terminating, +} + +impl Window { + fn of(interp: &mut Interpreter) -> Self { + match interp.bytecode.action() { + None => Self::MidFrame, + Some(InterpreterAction::NewFrame(_)) => Self::Suspending, + Some(InterpreterAction::Return(_)) => Self::Terminating, + } + } +} + +/// Writes [`INJECT`] into the interpreter's counter once, at the first `step_end` that sits in +/// `window`. +#[derive(Debug)] +struct CounterEditor { + window: Window, + fired: u32, +} + +impl CounterEditor { + fn new(window: Window) -> Self { + Self { window, fired: 0 } + } +} + +impl Inspector for CounterEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + self.fired += 1; + interp.gas.erase_cost(INJECT); + } +} + +/// `PUSH1 1; POP; STOP` — three opcodes, so a mid-frame `step_end` and a terminating one are both +/// reached, and nothing else happens in between. +fn straight_line_code() -> Bytes { + BytecodeBuilder::default().push_number(1u64).append(POP).append(STOP).build() +} + +/// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame +/// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. +fn suspending_code() -> Bytes { + call_then_stop(IDENTITY, PROBE_GAS) +} + +fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = CounterEditor::new(window); + let edited = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + (plain, edited, inspector.fired) +} + +/// An edit made in the terminating window reaches nobody, so nothing is booked for it — and the +/// transaction is the one the EVM would have produced alone. +/// +/// The action the terminating instruction set already holds its own copy of the counter, so the +/// caller is handed a number this edit never touched. Booking it would tell the conservation law +/// that the transaction spent [`INJECT`] less than it did. +/// +/// `compute_gas` being unmoved is the other half of the rule, and the one that would break if the +/// fix were written as "leave the counter alone" rather than "book nothing for it": the tail +/// settlement measures work as a drop in this very counter, so without the baseline shift the +/// injection would read as [`INJECT`] gas of work the frame never performed. +#[test] +fn test_an_edit_in_the_terminating_window_is_not_booked() { + let (plain, edited, fired) = run_counter_edit(straight_line_code(), Window::Terminating); + + assert_eq!(fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger::default(), + "an edit that cannot reach the envelope must leave the ledger untouched", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the settlement baseline must absorb the edit, so it counts as no work at all", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope must be the one the uninspected run produced", + ); +} + +/// The near boundary: a mid-frame edit is booked, because the frame carries on spending the +/// counter the callback left behind. +#[test] +fn test_an_edit_in_mid_frame_is_still_booked() { + let (_, edited, fired) = run_counter_edit(straight_line_code(), Window::MidFrame); + + assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + Lane::once(i128::from(INJECT)), + "gas written into a counter the frame will keep spending is conjured gas", + ); +} + +/// The far boundary, and the one a coarser rule would get wrong: a `CALL` has set an action too, +/// but it is a `NewFrame` action — the frame suspends, the child runs, and then the frame resumes +/// on exactly this counter. So the edit reaches the envelope and must be booked, even though the +/// interpreter is "at the end of its loop" in precisely the same sense as the terminating case. +#[test] +fn test_an_edit_in_the_suspending_window_is_still_booked() { + let (_, edited, fired) = run_counter_edit(suspending_code(), Window::Suspending); + + assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + Lane::once(i128::from(INJECT)), + "a suspended frame resumes on the edited counter, so the edit reaches the envelope", + ); +} + +// --- B: a precompile's classification, rewritten after its recording site --------------------- + +/// Rewrites the result of the call to `target` into `to`, once. +#[derive(Debug)] +struct Reclassifier { + target: Address, + to: InstructionResult, + fired: u32, +} + +impl Reclassifier { + fn new(target: Address, to: InstructionResult) -> Self { + Self { target, to, fired: 0 } + } +} + +impl Inspector for Reclassifier { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != self.target { + return; + } + self.fired += 1; + outcome.result.result = self.to; + } +} + +/// A `CALL` forwarding [`PROBE_GAS`] gas to `target` with `calldata` at `mem[0..]`, its success +/// flag popped so the caller survives either classification. +fn call_precompile(target: Address, calldata: &[u8]) -> Bytes { + BytecodeBuilder::default() + .mstore(0, calldata) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(calldata.len() as u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(PROBE_GAS) + .append(CALL) + .append(POP) + .append(STOP) + .build() +} + +/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped: 192 bytes +/// with a matching versioned hash, so KZG clears the length doorway and fails inside verification +/// — the one halt shape `MegaETH` prices as work performed. +fn kzg_verification_failure() -> Vec { + let commitment = hex::decode( + "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ + 5f26936857bc3a7c2539ea8ec3a952b7", + ) + .unwrap(); + let mut versioned_hash = Sha256::digest(&commitment).to_vec(); + versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG + let z = + hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); + let y = + hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); + let proof = hex::decode( + "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ + 4faf0070725e00b60ad9a026a15b1a8c", + ) + .unwrap(); + + let mut input = Vec::new(); + input.extend_from_slice(&versioned_hash); + input.extend_from_slice(&z); + input.extend_from_slice(&y); + input.extend_from_slice(&commitment); + input.extend_from_slice(&proof); + assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); + let last = input.len() - 1; + input[last] ^= 0x01; + input +} + +/// Runs the fixture twice: once uninspected, and once with the classification rewritten across +/// the boundary the shim refuses. +/// +/// The refusal is asserted here rather than in each case, so every case below is left stating the +/// one thing that differs between them — which arm of the precompile it reaches, and what the +/// uninspected run's split therefore is. +fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { + let code = call_precompile(target, calldata); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = Reclassifier::new(target, to); + let refusal = + transact_inspected_refused(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); + assert!( + refusal.error.contains("classification of a result frame init produced"), + "the transaction must fail with the refusal's own reason, got {}", + refusal.error, + ); + (plain, refusal) +} + +/// A successful precompile rewritten into a halt is refused, and the uninspected run destroys +/// nothing. +/// +/// The rewrite is the direction with state behind it: `make_call_frame` commits the checkpoint +/// before it returns a successful precompile's result, so a caller told the call halted would be +/// told so with the transfer that funded it standing. +#[test] +fn test_rewriting_a_successful_precompile_into_a_halt_is_refused() { + let (plain, _) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); + + assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); + assert_eq!( + plain.compute_gas, + plain.enforced(), + "with nothing destroyed the reported total is the work performed", + ); +} + +/// A rejected precompile rewritten into a success is refused, and the uninspected run destroys the +/// whole envelope. +/// +/// The other direction, and the other half of the split: `blake2f` rejects the input before any +/// work, so `make_call_frame` reverted the checkpoint and nothing was performed. +#[test] +fn test_reviving_a_rejected_precompile_is_refused() { + let (plain, _) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); + + assert_eq!( + plain.destroyed, PROBE_GAS, + "blake2f rejects the input before any work, so the uninspected run destroys all of it", + ); + assert_eq!( + plain.enforced(), + plain.compute_gas - plain.destroyed, + "nothing was performed, so nothing enforces", + ); +} + +/// The third arm, and the only one whose failure `MegaETH` prices as work: a KZG verification that +/// ran and rejected. +/// +/// The refusal matters most here. A halting precompile's gas object carries the whole forwarded +/// envelope as remaining — it is reset rather than spent down — so a caller told such a call +/// succeeded would reclaim all of it, the fixed fee included. That fee is gas the execution priced +/// and the envelope never paid, which is exactly the shape the refusal keeps out. +#[test] +fn test_reviving_a_priced_precompile_failure_is_refused() { + let calldata = kzg_verification_failure(); + let (plain, _) = run_reclassified(KZG, &calldata, InstructionResult::Stop); + + assert_eq!( + plain.destroyed, + PROBE_GAS - kzg_point_evaluation::GAS_COST, + "verification ran, so the uninspected run destroys the envelope less the fixed fee", + ); + assert_eq!( + plain.compute_gas - plain.destroyed, + plain.enforced(), + "the fee is the work performed, and it is what enforces", + ); +} + +// --- C: the pending action itself --------------------------------------------------------------- + +/// Gas an action edit moves, and gas a cancelling pair moves through the result lane's two +/// windows. +const ACTION_DELTA: u64 = 700; + +/// Reaches past the interpreter's gas counter and into the action the interpreter is holding, once. +/// +/// The counter and the action are two different objects at exactly one moment — after a +/// terminating or suspending instruction has run and before the loop hands the action on — and +/// this is the inspector that edits the second one. +#[derive(Debug)] +struct ActionEditor { + window: Window, + /// Positive raises the gas the action carries, negative lowers it. + delta: i64, + /// Fire only on an action whose classification is (or is not) an exceptional halt. + halting: bool, + fired: u32, +} + +impl ActionEditor { + fn raise(window: Window) -> Self { + Self { window, delta: ACTION_DELTA as i64, halting: false, fired: 0 } + } + + fn lower(window: Window) -> Self { + Self { window, delta: -(ACTION_DELTA as i64), halting: false, fired: 0 } + } + + fn on_halt() -> Self { + Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } + } +} + +impl Inspector for ActionEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + match interp.bytecode.action() { + Some(InterpreterAction::Return(result)) => { + if result.result.is_ok_or_revert() == self.halting { + return; + } + if self.delta >= 0 { + result.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + result.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave the action enough gas for the removal to land", + ); + } + } + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { + inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); + } + _ => return, + } + self.fired += 1; + } +} + +/// A `CALL` into [`CALLEE`], its result flag popped, then `STOP` — so the first terminating +/// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries +/// is decided by the callee the fixture installs. +fn call_callee_code() -> Bytes { + call_then_stop(CALLEE, PROBE_GAS) +} + +/// Gas written into a returning frame's pending action is gas the caller really reclaims, so it +/// has to be booked — the frame's classification is what says so, and the classification is only +/// known at the frame's settlement point. +#[test] +fn test_raising_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::once(i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "an edit to the action a returning frame hands back is an edit to the envelope", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the transaction really did spend less, which is why the ledger has to carry it", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the edit is not work: the frame performed exactly what it performed uninspected", + ); +} + +/// The same edit in the other direction. +#[test] +fn test_lowering_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); + let mut inspector = ActionEditor::lower(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::once(-i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "gas taken out of the action is gas the caller never gets back", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent + ACTION_DELTA, + "the transaction really did spend more", + ); +} + +/// The classification branch: a halting frame hands nothing back, so an edit to the gas its action +/// carries moves nothing and must not reach the lane's *net* — and the remainder it destroys is +/// the EVM's own number, not the edited one. +/// +/// The lane's gross carries the edit all the same. Whether it moved the envelope is what the +/// classification decides; whether the inspector made it is not, and the block guard asks the +/// second question. +#[test] +fn test_editing_a_halting_frames_pending_action_moves_nothing() { + let callee = BytecodeBuilder::default().append(INVALID).build(); + let plain = + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); + let mut inspector = ActionEditor::on_halt(); + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(call_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::of(0, u128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "a halting frame hands its remainder to nobody, so the edit moves the envelope by nothing \ + — and the lane still has to show it was made", + ); + assert_eq!( + edited.inspector_ledger.conjured_gas(), + 0, + "the conservation law reads the net, which is what stays zero", + ); + assert!( + !edited.inspector_ledger.is_zero(), + "and the block guard reads the gross, which is what does not", + ); + assert_eq!( + edited.destroyed, plain.destroyed, + "the destroyed remainder is the EVM's own, not the one the inspector wrote", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "and the envelope is unmoved"); +} + +/// The other action variant: gas written into a pending `NewFrame` action is the envelope a child +/// frame is about to be built with, which the caller was never debited for. +#[test] +fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { + let plain = transact(MegaSpecId::REX7, base_db(suspending_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Suspending); + let edited = + transact_inspected(MegaSpecId::REX7, base_db(suspending_code()), limits(), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { env: Lane::once(i128::from(ACTION_DELTA)), ..InspectorLedger::default() }, + "the child's budget grew by gas the caller's CALL never forwarded", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the child hands the extra budget straight back, so the transaction spends less", + ); +} + +/// Rewrites the classification inside a pending `Return` action, once, at the terminating +/// `step_end` of the frame that set it. +#[derive(Debug)] +struct ActionReclassifier { + to: InstructionResult, + fired: u32, +} + +impl Inspector for ActionReclassifier { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { return }; + result.result = self.to; + self.fired += 1; + } +} + +/// An edit to a pending action that is not to its gas moves nothing and is booked as an +/// intervention — but it still decides what the frame did, so the frame's state follows it. +/// +/// The action is what `classify_frame_action` builds the frame's result from, so a classification +/// written here is the one the caller sees and the one the journal decision is taken on. Nothing +/// on any gas lane can see that, which is what the intervention counter is for. +#[test] +fn test_rewriting_a_pending_actions_classification_is_an_intervention() { + let callee = + BytecodeBuilder::default().sstore(U256::from(1u64), U256::from(1u64)).append(STOP).build(); + let plain = + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); + let mut inspector = ActionReclassifier { to: InstructionResult::Revert, fired: 0 }; + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(call_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + plain.storage_value(CALLEE, U256::from(1u64)), + U256::from(1u64), + "uninspected, the callee's write is committed", + ); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "no gas moved, and the only thing left to say is that the transaction was not left alone", + ); + assert_eq!( + edited.storage_value(CALLEE, U256::from(1u64)), + U256::ZERO, + "a frame the caller was told reverted must leave no write behind", + ); +} + +// === 3. the blind spots ======================================================================= +// +// The rewrite shapes an all-zero ledger used to admit. +// +// The measurement shim's contract is that a transaction an inspector rewrote never reaches a +// block: the canonical path refuses one whose `InspectorLedger` is non-zero, so every rewrite has +// to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that per +// mechanism and per callback × shape pair. This module pins the shapes that slipped *between* +// those two questions — each one a rewrite the shim was handed, that changes what the transaction +// produces, and that every lane read as nothing: +// +// - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it +// has been paid for in the same step, so that neither goes out of bounds and the next expanding +// opcode charges nothing; +// - a `CallOutcome` / `CreateOutcome` metadata field — where the callee's return data lands, and +// which address a creation reports — rewritten without touching the `InterpreterResult` inside +// it, which is the only part the rewrite comparison used to read; +// - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels to +// zero; +// - the same cancellation spread across two frames, where only one of the two survives to the +// receipt, so the net is zero and the effect is not; +// - an instruction deleted from a frame, by stepping the program counter past it, so the work is +// never performed and there is nothing for any counter to meter; +// - a return buffer put in front of a frame that made no call, so `RETURNDATASIZE` reads a length +// no call produced. +// +// Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use +// to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test +// here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no +// longer changes anything is a shape that stopped testing the guard.//! +// The last two are also why the snapshot the first shape needed is now a *rule* rather than a +// list. A snapshot of four chosen readings caught the memory pair and let the program counter +// through, because `Interpreter::bytecode` was not among the things anyone had thought to name. +// What the shim takes now is every constant-time reading of the interpreter, and what pins that is +// the `Interpreter` row of `gas_surface.rs`'s closed table. + +/// A second callee, whose frame reverts. +const REVERTER: Address = EMPTY_TARGET; + +/// The address a rewritten `CreateOutcome` reports instead of the one the code was deployed at. +const FAKE_DEPLOYMENT: Address = address!("00000000000000000000000000000000000f00d0"); + +/// Slot the fixtures write their observable result to. +const RESULT_SLOT: u64 = 0x11; + +/// Refund a cancelling pair of refund edits moves, in each direction. +/// +/// Small enough to stay well under the EIP-3529 cap on every fixture here, so that what survives +/// to the receipt is the whole of the surviving half rather than whatever the cap left of it. +const REFUND: i64 = 2_000; + +/// The mainnet memory expansion cost of a memory `words` words long. +const fn memory_cost(words: u64) -> u64 { + 3 * words + words * words / 512 +} + +// --- a frame's memory, grown for free ------------------------------------------------------------ + +/// How far the free-expansion inspector grows the frame's memory, in words. +/// +/// The fixture's own `MSTORE` lands inside it, so the expansion the EVM would have charged for is +/// exactly the one the inspector already did for nothing. +const STOLEN_WORDS: u64 = 129; + +/// Grows the frame's memory and tells the EVM it is already paid for. +/// +/// Both halves are needed and neither is a rewrite on its own. Moving the memory alone leaves the +/// memo behind, and the next expanding opcode charges for an expansion that already happened; +/// moving the memo alone leaves the memory behind, and the EVM reads out of bounds. Moving both +/// keeps every invariant the interpreter has and skips the charge, which is why the pair was the +/// hole and neither half was. +#[derive(Default)] +struct FreeExpansion { + fired: u32, +} + +impl Inspector for FreeExpansion { + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != MSTORE { + return; + } + let words = STOLEN_WORDS as usize; + assert!(interp.memory.resize(words * 32), "the fixture must allow the memory to be grown",); + // Priced through revm's own table, so the memo is exactly what the EVM would have written + // had the frame paid; the assertion below restates the formula independently, which is + // what makes the two a check rather than one number written twice. + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); + self.fired += 1; + } +} + +/// ★ A frame whose memory was grown for free is not an all-zero ledger. +/// +/// The rewrite reaches through no argument the shim used to compare: the interpreter's gas counter +/// is untouched, no action is pending, no frame input and no frame result exists yet. What it +/// moves is the interpreter's memory and the memo beside it, and the transaction then pays less +/// than it would have — which is the one thing the guard exists to keep out of a block. +#[test] +fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { + // MSTORE(offset = STOLEN_WORDS * 32 - 32, value = 0xAA), which expands memory to exactly the + // size the inspector already grew it to. + let offset = (STOLEN_WORDS - 1) * 32; + let code = BytecodeBuilder::default() + .push_number(0xAAu64) + .push_number(offset) + .append(MSTORE) + .append(STOP) + .build(); + + let mut inspector = FreeExpansion::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + memory_cost(STOLEN_WORDS), + "the expansion the inspector performed is the charge the EVM then skipped", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction that paid less because an inspector moved its memory must not read as \ + untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- a call outcome's metadata ------------------------------------------------------------------- + +/// Where the fixture's `CALL` asks for its return data, and where the inspector moves it to. +const RETURN_AT: usize = 0; +const MOVED_TO: usize = 32; + +/// Moves a finished call's return data somewhere else in the caller's memory. +/// +/// The `InterpreterResult` inside the outcome — its classification, its output bytes, its gas — +/// comes back exactly as the EVM produced it. Only the range the caller will copy the output into +/// changes, which is not a field the result carries. +#[derive(Default)] +struct MoveReturnData { + fired: u32, +} + +impl Inspector for MoveReturnData { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.memory_offset = MOVED_TO..MOVED_TO + 32; + self.fired += 1; + } +} + +/// ★ A call outcome whose return range was moved is not an all-zero ledger. +#[test] +fn test_a_moved_return_range_is_booked() { + // Size the caller's memory to two words, call the callee for one word of output at offset 0, + // then store what landed there. + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(32u64) + .append(MSTORE) + .push_number(32u64) // retSize + .push_number(u64::try_from(RETURN_AT).unwrap()) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(u64::try_from(RETURN_AT).unwrap()) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = MoveReturnData::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the return data lands where the caller asked for it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the caller reads a word the callee never wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a rewritten return range changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +// --- a frame's returned output ------------------------------------------------------------------ + +/// The word a rewritten output buffer feeds the caller instead of the one the callee returned. +const FORGED_OUTPUT: u64 = 0xdead; + +/// Replaces the output buffer a finished call hands back, leaving its classification alone. +/// +/// The classification and the remaining gas are what every other lane reads. The output is +/// neither, and it is what the caller copies into its own memory. +#[derive(Default)] +struct ForgeCallOutput { + fired: u32, +} + +impl Inspector for ForgeCallOutput { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.result.output = Bytes::from(U256::from(FORGED_OUTPUT).to_be_bytes::<32>().to_vec()); + self.fired += 1; + } +} + +/// ★ A call outcome whose returned output was replaced is not an all-zero ledger. +#[test] +fn test_a_forged_call_output_is_booked() { + // Call the callee for one word of output, then store what landed there. + let code = BytecodeBuilder::default() + .push_number(32u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(0u64) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = ForgeCallOutput::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the caller reads what the callee returned", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(FORGED_OUTPUT), + "with it, the caller reads a word no frame produced", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a replaced output buffer changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +/// Reports a different address than the one the creation deployed to. +#[derive(Default)] +struct MoveDeploymentAddress { + fired: u32, +} + +impl Inspector for MoveDeploymentAddress { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if self.fired > 0 || outcome.address.is_none() { + return; + } + outcome.address = Some(FAKE_DEPLOYMENT); + self.fired += 1; + } +} + +/// ★ A creation outcome whose reported address was rewritten is not an all-zero ledger. +/// +/// The code is still deployed where the EVM put it; only the address the caller's stack receives +/// changes, so the caller goes on to talk to an account that holds nothing. +#[test] +fn test_a_rewritten_deployment_address_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = MoveDeploymentAddress::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); + let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); + assert_ne!(deployed, U256::ZERO, "the fixture's CREATE must succeed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(FAKE_DEPLOYMENT.as_slice()), + "the caller must have been handed the address the inspector wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction told a contract lives somewhere it does not must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +// --- a construction frame's pending action ------------------------------------------------------- + +/// Drains the gas a construction frame's pending `Return` action carries. +/// +/// The contract this module's other cases rest on — that an action is the frame's result a moment +/// later, so an edit to it settles with that result — does not hold for a creation. Between the +/// two, `classify_frame_action` charges the code deposit out of the gas *this action* carries, and +/// a creation that cannot pay it becomes an `OutOfGas` that deploys nothing. So this edit changes +/// what the transaction produces, and it does it by a route that leaves the classification and the +/// output the boundary compares exactly where they were. +#[derive(Default)] +struct DrainConstructionAction { + fired: u32, +} + +impl Inspector for DrainConstructionAction { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + // A construction frame runs no deployed code, so it has no bytecode address. + if self.fired > 0 || interp.input.bytecode_address().is_some() { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + let remaining = result.gas.remaining(); + assert!( + result.gas.record_regular_cost(remaining), + "the fixture must be able to drain the action it found", + ); + self.fired += 1; + } +} + +/// ★ A construction frame whose pending action was drained is not an all-zero ledger. +/// +/// Every lane the boundary reads stays put: the action's classification and output are untouched, +/// so nothing is an intervention; the gas edit is staged for the frame's settlement point, and +/// that point declines to book it because the result it finally sees is a swallowed one. The +/// deposit the drained action could no longer pay is what turned it into one. +#[test] +fn test_a_drained_construction_action_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = DrainConstructionAction::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); + assert_ne!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "without the edit the creation must succeed", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it the creation cannot pay its code deposit and deploys nothing", + ); + assert_ne!( + plain.gas_used, cheated.gas_used, + "and the receipt the sender is billed on moves with it", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose contract an inspector deleted must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Raises the gas an inner call frame's pending `Return` action carries, then takes the same +/// amount back out of the result that action became. +/// +/// The two windows are one lane and one frame, and the pair nets to zero. They are still two +/// edits, made in two different callbacks, and the lane's traffic is what says so — the sum alone +/// reads as an inspector that did nothing. +#[derive(Default)] +struct CancellingActionAndResultEdits { + raised: u32, + lowered: u32, +} + +impl Inspector for CancellingActionAndResultEdits { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.raised > 0 || interp.input.bytecode_address() != Some(&CALLEE) { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + result.gas.erase_cost(ACTION_DELTA); + self.raised += 1; + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.lowered > 0 || inputs.target_address != CALLEE { + return; + } + assert!( + outcome.result.gas.record_regular_cost(ACTION_DELTA), + "the fixture must leave the result enough gas for the removal to land", + ); + self.lowered += 1; + } +} + +/// ★ An edit staged at one callback and undone at the next is two edits, not none. +/// +/// Nothing about this transaction changes: a call frame's remaining gas is read by nobody between +/// the two windows, so the pair really is invisible in what the transaction produces. That is the +/// point — the lane's traffic is the only thing that separates it from an inspector that never +/// ran, and on a *creation* frame the same pair is the shape that deletes a contract. +#[test] +fn test_cancelling_action_and_result_edits_are_booked() { + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let callee = BytecodeBuilder::default().append(STOP).build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = CancellingActionAndResultEdits::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); + assert_eq!( + cheated.gas_used, plain.gas_used, + "the pair cancels, so the receipt really is the one the EVM would have produced", + ); + assert_eq!( + cheated.inspector_ledger.conjured_gas(), + 0, + "and the conservation law must read the net, which is zero", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the guard must still see that the lane carried two edits: {:?}", + cheated.inspector_ledger, + ); + assert_eq!( + cheated.inspector_ledger.result.gross(), + 2 * u128::from(ACTION_DELTA), + "one edit in each window, counted where each was made", + ); +} + +// --- two edits to one lane, in opposite directions ----------------------------------------------- + +/// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. +/// +/// Both edits land on the interpreter counter, which is one signed lane. Their net is zero and +/// the transaction's envelope is unmoved — and in between them the frame read a number one higher +/// than the EVM would have given it, and wrote that number to storage. +#[derive(Default)] +struct CancellingCounterEdits { + /// 0 before the injection, 1 between the two edits, 2 once both have landed. + phase: u8, +} + +impl Inspector for CancellingCounterEdits { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + match self.phase { + 0 if interp.bytecode.opcode() == GAS => { + interp.gas.erase_cost(1); + self.phase = 1; + } + 1 => { + assert!(interp.gas.record_regular_cost(1), "the frame must afford the give-back"); + self.phase = 2; + } + _ => {} + } + } +} + +/// ★ Two edits to the same lane that cancel are not an all-zero ledger. +/// +/// The net of the gas lane really is zero — the transaction spent exactly what it would have — so +/// nothing the conservation law reads has moved. What moved is the number the frame read in +/// between, and a guard that asks the net cannot see it. The gross activity counter is what does. +#[test] +fn test_cancelling_counter_edits_are_booked() { + let code = BytecodeBuilder::default() + .append(GAS) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of + // its remaining gas is the counter the injection moved. + let limits = EvmTxRuntimeLimits::no_limits(); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); + let mut inspector = CancellingCounterEdits::default(); + let cheated = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); + + assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)) + U256::from(1), + "the frame must have read one gas more than the EVM would have given it", + ); + assert_eq!( + cheated.total_gas_spent, plain.total_gas_spent, + "the two edits cancel, so the envelope the receipt reports is unmoved", + ); + assert_eq!( + cheated.inspector_conjured_gas(), + 0, + "and so is the law's term: this is exactly the shape a net-only reading cannot see", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the transaction was rewritten, and the guard has to see that: {:?}", + cheated.inspector_ledger, + ); +} + +/// Adds a refund to one child frame's result and takes the same amount out of another's. +/// +/// The frame that gets the addition returns, so its refund reaches the receipt. The frame that +/// gets the subtraction reverts, so revm discards its whole refund counter — the subtraction never +/// reaches anything. Net zero on the lane, one refund's worth of difference on the receipt. +#[derive(Default)] +struct CancellingRefundsAcrossFrames { + added: u32, + removed: u32, +} + +impl Inspector for CancellingRefundsAcrossFrames { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address == CALLEE && self.added == 0 { + outcome.result.gas.record_refund(REFUND); + self.added += 1; + } else if inputs.target_address == REVERTER && self.removed == 0 { + assert!( + outcome.result.gas.refunded() >= REFUND, + "the reverting callee must hold a refund of its own to take from, got {}", + outcome.result.gas.refunded(), + ); + outcome.result.gas.record_refund(-REFUND); + self.removed += 1; + } + } +} + +/// ★ A cancellation split across a surviving frame and a discarded one is not an all-zero ledger. +/// +/// This is the previous shape with the asymmetry made explicit: the two halves are equal and +/// opposite where the ledger books them, and only one of them is still standing by the time the +/// receipt is built. +#[test] +fn test_cancelling_refunds_across_frames_are_booked() { + let call_to = |builder: BytecodeBuilder, target: Address| { + builder + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(target) + .push_number(200_000u64) + .append(CALL) + .append(POP) + }; + let code = call_to(call_to(BytecodeBuilder::default(), CALLEE), REVERTER).append(STOP).build(); + // Both callees set a slot and clear it again, so each ends holding a refund the EVM produced. + let clearing = |builder: BytecodeBuilder| { + builder + .sstore(U256::from(RESULT_SLOT), U256::from(1u64)) + .sstore(U256::from(RESULT_SLOT), U256::ZERO) + }; + let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); + let reverting = clearing(BytecodeBuilder::default()).revert().build(); + let db = || { + base_db(code.clone()) + .account_code(CALLEE, returning.clone()) + .account_code(REVERTER, reverting.clone()) + }; + + let mut inspector = CancellingRefundsAcrossFrames::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); + assert!( + plain.total_gas_spent >= 5 * u64::try_from(REFUND).unwrap(), + "the fixture must burn enough that the EIP-3529 cap does not hide the difference", + ); + assert_eq!( + plain.gas_used - cheated.gas_used, + u64::try_from(REFUND).unwrap(), + "only the surviving frame's half reaches the receipt, so the sender pays that much less", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a receipt an inspector moved must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- an opcode skipped, and a return buffer conjured +// ---------------------------------------------- + +/// What the fixture's `SSTORE` writes when it runs. +const STORED: u64 = 0x99; + +/// The gas a cold `SSTORE` into a zero slot costs, which is what skipping it saves. +const COLD_SSTORE_SET: u64 = 22_100; + +/// How many bytes of return data the forging inspector conjures. +/// +/// Non-zero and a whole number of words, so that the `SSTORE` that stores it turns a zero slot +/// into a non-zero one — which is a different charge as well as a different value. +const CONJURED_RETURN_DATA: u64 = 96; + +/// Advances the program counter past the frame's `SSTORE`, so the EVM never executes it. +/// +/// revm's inspected loop runs this callback *before* the instruction, and the interpreter reads +/// the opcode it is about to execute from the very pointer this moves. Stepping the pointer on by +/// one byte therefore deletes one instruction from the frame: the two operands the `SSTORE` would +/// have consumed stay on the stack, the `STOP` after it runs instead, and the frame ends where it +/// was going to end. +/// +/// Nothing about this reaches a gas counter. The work is not performed, so there is nothing for +/// the EVM to meter and nothing for a gas lane to see. +#[derive(Default)] +struct SkipTheStore { + fired: u32, +} + +impl Inspector for SkipTheStore { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != SSTORE { + return; + } + interp.bytecode.relative_jump(1); + self.fired += 1; + } +} + +/// ★ A frame with an opcode skipped out from under it is not an all-zero ledger. +/// +/// The rewrite is the free-expansion shape's twin and is strictly worse: it does not merely make +/// the frame's next charge cheaper, it deletes an instruction from the frame. The transaction ends +/// with different storage *and* a smaller bill, and every gas lane reads zero because the gas that +/// went missing was never spent by anybody. +#[test] +fn test_a_skipped_opcode_is_booked() { + let code = BytecodeBuilder::default() + .sstore(U256::from(RESULT_SLOT), U256::from(STORED)) + .append(STOP) + .build(); + + let mut inspector = SkipTheStore::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(STORED), + "without the rewrite the frame stores what its bytecode says", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the store never happens", + ); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + COLD_SSTORE_SET, + "the deleted instruction is the charge the transaction then did not pay", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction an inspector deleted an instruction from must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Puts return data in front of a frame that has made no call. +/// +/// `RETURNDATASIZE` reads the buffer's length, so the frame goes on to store a number no call +/// produced. The buffer is the interpreter's own, reachable through `ReturnData` on any live +/// interpreter, and its length is a constant-time reading exactly like the memory's size. +#[derive(Default)] +struct ForgeReturnData { + fired: u32, +} + +impl Inspector for ForgeReturnData { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != RETURNDATASIZE { + return; + } + interp.return_data.set_buffer(Bytes::from(vec![0u8; CONJURED_RETURN_DATA as usize])); + self.fired += 1; + } +} + +/// ★ A frame handed return data it never received is not an all-zero ledger. +/// +/// The frame made no call, so the EVM's own buffer is empty and the store is a zero-to-zero +/// no-op. With the rewrite the same store turns a zero slot into a non-zero one, which changes the +/// post-state and costs the transaction more — in the opposite direction to every other shape +/// here, and just as invisible to a lane that only watches gas counters. +#[test] +fn test_a_forged_return_buffer_is_booked() { + let code = BytecodeBuilder::default() + .append(RETURNDATASIZE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = ForgeReturnData::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "a frame that made no call has no return data", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(CONJURED_RETURN_DATA), + "with the rewrite it reads the length of a buffer no call produced", + ); + assert!( + cheated.total_gas_spent > plain.total_gas_spent, + "and pays for the non-zero store the rewrite turned it into: {} vs {}", + cheated.total_gas_spent, + plain.total_gas_spent, + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a forged buffer changed must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- a frame invariant moved and moved back -------------------------------------------------- + +/// The caller the rewriting inspector shows the frame instead of the one that called it. +const IMPOSTOR: Address = address!("00000000000000000000000000000000000ca11e"); + +/// Moves the frame's caller for the length of one instruction, and puts it back. +/// +/// `CALLER` reads `input.caller_address`, so the frame pushes an address nobody called it from and +/// goes on to store that. The rewrite is undone in the very next callback, which is what makes the +/// shape worth pinning: the frame's identity is the one the EVM gave it at every point a *frame* +/// could be inspected — at its start, at its end, and at every callback but the two this touches. +/// +/// Nothing about it reaches a gas counter. Both runs execute the same instructions and pay the +/// same cold `SSTORE`; only the value written differs. +#[derive(Default)] +struct BorrowTheCaller { + /// The caller the EVM gave the frame, kept so it can be handed back. + original: Option
, + /// How many times each half of the rewrite ran. + moved: u32, + restored: u32, +} + +impl Inspector for BorrowTheCaller { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.moved > 0 || interp.bytecode.opcode() != CALLER { + return; + } + self.original = Some(interp.input.caller_address); + interp.input.caller_address = IMPOSTOR; + self.moved += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + let Some(original) = self.original.filter(|_| self.restored == 0) else { + return; + }; + interp.input.caller_address = original; + self.restored += 1; + } +} + +/// ★ A frame invariant moved in `step` and moved back in `step_end` is not an all-zero ledger. +/// +/// The four addresses and the value a frame is identified by cannot change while it runs, which +/// makes them the readings a cheaper shim would be tempted to compare once per frame rather than +/// once per callback. This is the shape that answers that: an inspector borrows one of them for +/// exactly as long as it takes the frame to read it, and gives it back before anything outside the +/// two callbacks could look. A per-frame comparison sees the address it started with; a per-opcode +/// one sees it move twice. +#[test] +fn test_a_frame_invariant_moved_and_moved_back_is_booked() { + let code = BytecodeBuilder::default() + .append(CALLER) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = BorrowTheCaller::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); + assert_eq!( + inspector.original, + Some(crate::common::CALLER), + "and the half that gives the address back must have the one the EVM gave the frame", + ); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(crate::common::CALLER.as_slice()), + "without the rewrite the frame stores the address that called it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(IMPOSTOR.as_slice()), + "with it, the frame stores one nobody called it from", + ); + assert_eq!( + plain.total_gas_spent, cheated.total_gas_spent, + "the two runs cost the same, so no gas lane can tell them apart", + ); + assert!( + cheated.inspector_ledger.interventions >= 2, + "each half of the rewrite is a rewrite: {:?}", + cheated.inspector_ledger, + ); +} + +// === 4. the receipt's other two numbers ======================================================= +// +// The two numbers on a receipt that the conservation law cannot see, and the lanes that do. +// +// The law is stated over `total_gas_spent`, which is `limit - remaining`. A transaction's receipt +// carries two more figures that arithmetic does not reach: the EIP-3529 refund, which decides what +// the sender actually pays, and the EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and its +// `state_gas_spent` counter — which decides how much of the envelope the receipt counts as spent +// at all. +// +// Both are reachable from every callback that is handed a `Gas`, and both were unmeasured. The +// shapes here are what the two lanes now book, and each pins the *reason* its lane is measured +// where it is: +// +// - a **refund** is a quantity the EVM also produces, so only a difference across a callback +// isolates the inspector's share — the lane is measured at the boundary, and is nominal in both +// the senses that can make it differ from what reaches the receipt (the EIP-3529 cap, and the +// chain of successful frame returns an edit has to survive); +// - a **reservoir** is a quantity `MegaETH` never produces at all, and one revm propagates by +// replacement rather than by accumulation, so a boundary difference would book edits the EVM goes +// on to erase. The lane is settled once, from the number the transaction ends with, which is +// exactly the surviving part and is the inspector's in whole. + +/// Gas the fixture's inner `CALL` forwards. +const INNER_CALL_GAS: u64 = 200_000; + +/// A refund large enough that the cap keeps part of it out of the receipt. +const OVERSIZED_REFUND: i64 = 60_000; +/// The EIP-8037 pool an edit fills. +const RESERVOIR: u64 = 10_000; +/// The EIP-8037 spend an edit writes. +const STATE_GAS: i64 = 5_000; + +/// Slot the top frame writes. +const TOP_SLOT: u64 = 0x10; +/// Slot the callee writes. +const CALLEE_SLOT: u64 = 0x20; +/// Slot the callee sets and clears, so the frame ends holding a refund of the EVM's own making. +const CLEARED_SLOT: u64 = 0x30; + +// --- the fixture ------------------------------------------------------------------------------- + +/// How the fixture's callee ends, which is what decides whether its refund travels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Callee { + /// Writes storage, produces a refund by clearing a slot it just set, and returns. + Returning, + /// Writes storage and reverts, so the EVM discards everything the frame held. + Reverting, +} + +fn caller_code() -> Bytes { + append_call(BytecodeBuilder::default(), CALLEE, INNER_CALL_GAS, 0) + .append(POP) + .sstore(U256::from(TOP_SLOT), U256::from(1u64)) + .append(STOP) + .build() +} + +fn callee_code(callee: Callee) -> Bytes { + let builder = BytecodeBuilder::default() + .sstore(U256::from(CALLEE_SLOT), U256::from(1u64)) + // Set and clear, so the frame ends holding a refund the EVM itself produced. + .sstore(U256::from(CLEARED_SLOT), U256::from(1u64)) + .sstore(U256::from(CLEARED_SLOT), U256::ZERO); + match callee { + Callee::Returning => builder.append(STOP).build(), + Callee::Reverting => builder.revert().build(), + } +} + +fn db_for(callee: Callee) -> MemoryDatabase { + db_with_callee(caller_code(), callee_code(callee)) +} + +// --- the edit ---------------------------------------------------------------------------------- + +/// One edit, applied once, to one of the `Gas` objects a callback is handed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Edit { + /// Add to the running interpreter's refund counter. + RefundAtStep(i64), + /// Add to the finished inner call's refund counter. + RefundAtCallEnd(i64), + /// Fill the running interpreter's EIP-8037 pool. + ReservoirAtStep, + /// Fill it at the one moment the frame is holding a `NewFrame` action, whose child overwrites + /// the pool on the way back. + ReservoirAtSuspension, + /// Fill the pool the inner call's inputs seed the child frame with. + ReservoirOnInputs, + /// Fill the finished inner call's pool. + ReservoirAtCallEnd, + /// Write the running interpreter's EIP-8037 spend counter. + StateGasAtStep, + /// Write the finished inner call's spend counter. + StateGasAtCallEnd, + /// Answer the inner call with a synthetic outcome that echoes the envelope and carries + /// neither figure — the control the two below are read against. + InterceptEcho, + /// The same, carrying a refund the frame never earned. + InterceptWithRefund, + /// The same, carrying an EIP-8037 pool. + InterceptWithReservoir, +} + +impl Edit { + /// Whether this edit answers the frame itself instead of letting the EVM build it. + const fn intercepts(self) -> bool { + matches!( + self, + Self::InterceptEcho | Self::InterceptWithRefund | Self::InterceptWithReservoir + ) + } +} + +/// Applies one [`Edit`], once, and records that it landed. +#[derive(Debug)] +struct Editor { + edit: Edit, + fired: u32, + steps: u64, +} + +impl Editor { + const fn new(edit: Edit) -> Self { + Self { edit, fired: 0, steps: 0 } + } +} + +impl Inspector for Editor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.fired > 0 || self.steps != 4 { + return; + } + match self.edit { + Edit::RefundAtStep(amount) => interp.gas.record_refund(amount), + Edit::ReservoirAtStep => interp.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtStep => interp.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || self.edit != Edit::ReservoirAtSuspension { + return; + } + // The one window where the pool the frame holds is not the pool that travels: the child + // this action builds was already sized from the pre-edit value, and its own pool + // overwrites this one when it returns. + if !matches!(interp.bytecode.action(), Some(InterpreterAction::NewFrame(_))) { + return; + } + interp.gas.set_reservoir(RESERVOIR); + self.fired += 1; + } + + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if self.fired > 0 || inputs.target_address != CALLEE { + return None; + } + if self.edit == Edit::ReservoirOnInputs { + inputs.reservoir += RESERVOIR; + self.fired += 1; + return None; + } + if !self.edit.intercepts() { + return None; + } + // The echo convention every tool that intercepts follows: hand back exactly what was + // forwarded, so the gas lanes see nothing and only the figures under test move. + let mut gas = Gas::new(inputs.gas_limit); + match self.edit { + Edit::InterceptWithRefund => gas.record_refund(REFUND), + Edit::InterceptWithReservoir => gas.set_reservoir(RESERVOIR), + _ => {} + } + self.fired += 1; + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas), + inputs.return_memory_offset.clone(), + )) + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != CALLEE { + return; + } + match self.edit { + Edit::RefundAtCallEnd(amount) => outcome.result.gas.record_refund(amount), + Edit::ReservoirAtCallEnd => outcome.result.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtCallEnd => outcome.result.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } +} + +/// Runs the fixture with no inspector at all. +fn transact_plain(callee: Callee) -> Outcome { + transact(MegaSpecId::REX7, db_for(callee), limits()) +} + +/// Runs it with one edit applied, asserting the edit landed exactly once. +fn transact_edited(callee: Callee, edit: Edit) -> Outcome { + let mut editor = Editor::new(edit); + let outcome = transact_inspected(MegaSpecId::REX7, db_for(callee), limits(), &mut editor); + assert_eq!( + editor.fired, 1, + "{edit:?}: the fixture must reach the edit's callback exactly once", + ); + outcome +} + +// --- the fixture's own assumptions --------------------------------------------------------------- + +/// The uninspected run is what the cells below assume it is: it succeeds, it produces a refund of +/// its own, and it reports no EIP-8037 dimension at all. +#[test] +fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { + let plain = transact_plain(Callee::Returning); + assert!(plain.result.is_success(), "{:?}", plain.result); + assert!( + plain.refunded() > 0, + "the callee's cleared slot must leave a refund for the lowering cell to take from", + ); + assert_eq!( + plain.gas_used, + plain.total_gas_spent - plain.refunded(), + "the receipt's two gas numbers differ by exactly the refund", + ); + assert_eq!(plain.state_gas_spent(), 0, "EIP-8037 is off on every MegaETH path"); + assert!(plain.inspector_ledger.is_zero(), "no inspector ran: {:?}", plain.inspector_ledger); +} + +// --- the refund lane +// ------------------------------------------------------------------------------ + +/// A refund written into a running interpreter's counter is booked, and moves what the sender pays +/// without moving the envelope. +#[test] +fn test_a_refund_written_into_a_live_interpreter_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + "the shim must book the refund and nothing else", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "a refund does not move the envelope, which is why the law cannot see it", + ); + assert_eq!( + edited.refunded(), + plain.refunded() + u64::try_from(REFUND).unwrap(), + "but it does move the receipt's refund", + ); + assert_eq!( + edited.gas_used, + plain.gas_used - u64::try_from(REFUND).unwrap(), + "and through it what the sender pays", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, 0, + "the refund lane is deliberately not a term of the law", + ); + assert!(!edited.inspector_ledger.is_zero(), "and the block guard has to see it"); +} + +/// The same edit made at the last callback that holds the finished frame's result. +#[test] +fn test_a_refund_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded(), plain.refunded() + u64::try_from(REFUND).unwrap()); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); +} + +/// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction +/// would report an inspector that raised the sender's bill as having done nothing. +#[test] +fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { + let plain = transact_plain(Callee::Returning); + assert!( + plain.refunded() >= u64::try_from(REFUND).unwrap(), + "fixture check: there must be a refund to take from, got {}", + plain.refunded(), + ); + + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded(), plain.refunded() - u64::try_from(REFUND).unwrap()); + assert_eq!( + edited.gas_used, + plain.gas_used + u64::try_from(REFUND).unwrap(), + "the sender pays more, by exactly what was taken", + ); +} + +/// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. +/// +/// The cap applies to the transaction's whole refund at once, over a sum in which the EVM's own +/// refunds and an inspector's are indistinguishable, at a point past every callback. Splitting it +/// between them needs a priority rule the protocol does not have, so the lane states the edit and +/// the receipt states the effect — and the two are allowed to differ. +#[test] +fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + refund: Lane::once(i128::from(OVERSIZED_REFUND)), + ..InspectorLedger::default() + }, + "the lane carries the nominal edit", + ); + assert_eq!( + edited.refunded(), + edited.total_gas_spent / 5, + "while the receipt carries the EIP-3529 cap", + ); + assert!( + edited.refunded() < plain.refunded() + u64::try_from(OVERSIZED_REFUND).unwrap(), + "fixture check: the cap must actually bind, or this cell asserts nothing", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); +} + +/// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. +/// +/// revm hands a frame's refund to its caller only on success, so this edit dies with the frame. +/// The lane books it anyway, because the alternative is a rule that has to track every frame +/// between the edit and the top — and because a lane that under-reports lets exactly the shape +/// this module exists to catch into a block, while over-reporting costs nothing: the law has no +/// term for it. +#[test] +fn test_a_refund_the_frame_chain_discards_is_still_booked() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + "the lane books the edit", + ); + assert_eq!( + edited.refunded(), + plain.refunded(), + "the receipt is unmoved: a reverting frame hands its caller no refund", + ); + assert_eq!(edited.gas_used, plain.gas_used); +} + +// --- the EIP-8037 state-gas dimension ------------------------------------------------------------ + +/// A reservoir an inspector fills is gas the transaction never funded: the receipt reports that +/// much less spent, and the law needs it back. +#[test] +fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - RESERVOIR, + "the receipt counts the pool as unspent, so the envelope shrinks by exactly it", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + i128::from(RESERVOIR), + "which is why this lane, unlike the refund one, is a term of the law", + ); +} + +/// The same, written into the pool a call's inputs seed the child frame with. +#[test] +fn test_a_reservoir_written_into_a_frame_input_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + // The inputs came back changed in a field the envelope lane does not cover, which the + // rewrite comparison books on its own. + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); +} + +/// And into the finished frame's own pool, which its caller takes whatever the classification. +#[test] +fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); +} + +/// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the +/// run it produces is the run the EVM would have produced alone. +/// +/// This is the window that decides where the lane is measured. A difference taken across this +/// callback would say `RESERVOIR` was conjured; the transaction says otherwise, and the settlement +/// point is the only reading that agrees with it. +#[test] +fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); + + assert!( + edited.inspector_ledger.is_zero(), + "an edit the child frame's own pool replaces moved nothing: {:?}", + edited.inspector_ledger, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); + assert_eq!(edited.gas_used, plain.gas_used); + assert_eq!(edited.refunded(), plain.refunded()); +} + +/// The spend counter's own effect on the receipt: a successful transaction reports it, whether or +/// not EIP-8037 is enabled. +#[test] +fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); + + assert_eq!(plain.state_gas_spent(), 0, "fixture check"); + assert_eq!( + edited.state_gas_spent(), + u64::try_from(STATE_GAS).unwrap(), + "the receipt reports what was written", + ); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope is untouched, so this lane is not a term of the law either", + ); + assert_eq!(edited.terms.inspector_conjured_gas, 0); +} + +/// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend +/// counter back into its caller's pool, which turns a state-gas edit into an envelope-moving one. +/// +/// The lane that catches it is the reservoir's, not the state-gas one, because the fold has +/// already happened by the time either is read. That is the second reason the two are settled from +/// the transaction's final figures rather than differenced across a callback. +#[test] +fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + "the spend counter of a reverting frame arrives in its caller as a pool", + ); + assert_eq!( + edited.state_gas_spent(), + 0, + "and not as a spend: a failing frame's counter is not accumulated", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), + "so the envelope moves, and the law's term has to move with it", + ); +} + +// --- a frame the inspector answers itself +// --------------------------------------------------------- + +/// A synthetic outcome carries figures of its own, and there is no EVM-produced number on the +/// other side of the callback to difference against — so the whole of what it carries is the +/// inspector's, measured against nothing rather than against a baseline. +/// +/// The echo control is what makes the two cells below readings of the figures rather than of the +/// interception: it moves the gas lanes not at all, which is the convention every tool that +/// intercepts follows. +#[test] +fn test_a_synthetic_outcome_carries_its_own_figures() { + let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); + assert_eq!( + echo.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an echoing interception moves no figure at all", + ); + + let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); + assert_eq!( + refunding.inspector_ledger, + InspectorLedger { + refund: Lane::once(i128::from(REFUND)), + interventions: 1, + ..InspectorLedger::default() + }, + "the refund a frame that never ran hands back is the inspector's in whole", + ); + assert_eq!( + refunding.refunded(), + echo.refunded() + u64::try_from(REFUND).unwrap(), + "and it reaches the receipt: the outcome succeeded, so its caller records it", + ); + assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); + + let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); + assert_eq!( + pooled.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!( + pooled.total_gas_spent, + echo.total_gas_spent - RESERVOIR, + "a pool does move the envelope, wherever it came from", + ); +} + +// --- the frozen specs +// ----------------------------------------------------------------------------- + +/// On a frozen spec the two lanes report and settle nothing. +/// +/// The shim is not spec-gated, and must not be: the block guard has to see a rewritten receipt +/// whichever spec produced it. What is gated is the accounting the lanes feed, so a frozen spec's +/// own numbers have to be exactly what they were — which is what this reads, by comparing an +/// edited run against an unedited one on the same spec. +#[test] +fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { + const REX6: MegaSpecId = MegaSpecId::REX6; + fn run(edit: Option) -> Outcome { + let db = db_for(Callee::Returning); + let limits = EvmTxRuntimeLimits::from_spec(REX6); + match edit { + Some(edit) => { + let mut editor = Editor::new(edit); + let outcome = transact_inspected(REX6, db, limits, &mut editor); + assert_eq!(editor.fired, 1, "{edit:?} must land"); + outcome + } + None => transact(REX6, db, limits), + } + } + + let plain = run(None); + assert!(plain.inspector_ledger.is_zero()); + + for (edit, expected) in [ + ( + Edit::RefundAtStep(REFUND), + InspectorLedger { + refund: Lane::once(i128::from(REFUND)), + ..InspectorLedger::default() + }, + ), + ( + Edit::ReservoirAtStep, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ), + ( + Edit::StateGasAtStep, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + ), + ] { + let edited = run(Some(edit)); + assert_eq!(edited.inspector_ledger, expected, "{edit:?}: the lane reports on every spec"); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "{edit:?}: a frozen spec's compute total must not move", + ); + assert_eq!(edited.destroyed, plain.destroyed, "{edit:?}: nor its destroyed lane"); + // `inspector_conjured_gas` is a reading of the ledger rather than something the + // transaction recorded, so it moves with the lane on every spec. Every other term is what + // a frozen spec must leave alone. + assert_eq!( + ConservationTerms { inspector_conjured_gas: 0, ..edited.terms }, + plain.terms, + "{edit:?}: nothing a frozen spec records may move", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + edited.inspector_ledger.conjured_gas(), + "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", + ); + } +} + +// === 5. interception ========================================================================== +// +// The gas a synthetic outcome carries. +// +// A `frame_start` / `call` / `create` callback that returns `Some(outcome)` answers the frame +// itself: no frame is built, `frame_init` never runs, and the number the caller reclaims is +// whatever `Gas` the inspector put in that outcome. Nothing about it is derived from the +// execution — the inspector chooses it outright — so it is a gas figure the transaction's +// accounting has to be told about, exactly like an edit to a result the EVM did produce. +// +// The tests here are laid out over the sign of that choice, because the two directions settle +// differently and a lane that books one and drops the other is a real failure mode: +// +// - an outcome that hands back **less** than the envelope makes the caller spend gas no frame ever +// performed work for; +// - an outcome that hands back **more** conjures gas the transaction never funded; +// - an outcome that hands back **exactly** the envelope — the echo convention every tracer that +// intercepts follows — moves nothing, and must book nothing. +// +// The halt direction is the asymmetry: a halting outcome hands nothing back at all, so what the +// inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed +// remainder is settled against the envelope instead. + +/// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. +const FORWARDED: u64 = 50_000; + +/// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. +fn call_fixture() -> MemoryDatabase { + db_with_callee(call_then_stop(CALLEE, FORWARDED), plain_run_code(20)) +} + +/// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. +#[derive(Clone, Copy, Debug)] +enum Sizing { + /// The echo convention: exactly the envelope. + Echo, + /// Half of it — the caller spends the other half for work no frame performed. + Half, + /// None of it. + Zero, + /// More than it — gas the transaction never funded. + Excess(u64), +} + +impl Sizing { + fn gas(self, envelope: u64) -> u64 { + match self { + Self::Echo => envelope, + Self::Half => envelope / 2, + Self::Zero => 0, + Self::Excess(extra) => envelope + extra, + } + } + + /// What the ledger must carry for this sizing, as a signed movement from the envelope. + fn expected_delta(self, envelope: u64) -> i128 { + i128::from(self.gas(envelope)) - i128::from(envelope) + } +} + +/// Intercepts the call to [`CALLEE`], sizing the outcome's gas by [`Sizing`]. +struct CallInterceptor { + sizing: Sizing, + classification: InstructionResult, + intercepted: u64, + envelope: u64, +} + +impl CallInterceptor { + fn new(sizing: Sizing, classification: InstructionResult) -> Self { + Self { sizing, classification, intercepted: 0, envelope: 0 } + } +} + +impl Inspector for CallInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.envelope = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + self.classification, + Bytes::new(), + Gas::new(self.sizing.gas(inputs.gas_limit)), + ), + inputs.return_memory_offset.clone(), + )) + } +} + +/// An outcome that hands back less than the envelope makes the caller spend gas nothing performed. +#[test] +fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { + let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); + assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "the half the outcome withheld is gas the inspector destroyed", + ); +} + +/// The extreme of the same direction: the outcome hands back nothing at all. +#[test] +fn test_a_zero_gas_interception_books_the_whole_envelope() { + let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "an outcome that returns nothing consumed the whole envelope", + ); +} + +/// The other direction: an outcome that hands back more than it was given conjures the difference. +#[test] +fn test_an_over_funded_interception_books_the_gas_it_conjured() { + const EXTRA: u64 = 7_000; + let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "gas the transaction never funded is gas the inspector conjured", + ); +} + +/// The echo convention moves nothing, and must book nothing. +/// +/// This is the shape every tool that intercepts actually uses, and the reason the lane could go +/// missing for as long as it did: with the envelope echoed back the accounting closes whether or +/// not anything measures it. Pinning the zero is what says the lane is measuring rather than +/// coincidentally agreeing. +#[test] +fn test_an_echoing_interception_books_no_gas_at_all() { + for classification in [InstructionResult::Stop, InstructionResult::Revert] { + let mut inspector = CallInterceptor::new(Sizing::Echo, classification); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", + ); + assert_eq!(reading.inspector_ledger.conjured_gas(), 0, "{classification:?}"); + } +} + +/// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes +/// nothing the transaction spends — and the envelope is destroyed whole. +/// +/// What the outcome claimed is still traffic on the result lane: the sizings below differ from the +/// envelope by different amounts, and each one is an edit the inspector made whether or not the +/// classification let it reach anybody. +#[test] +fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { + for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { + let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); + assert_eq!( + reading.inspector_ledger.conjured_gas(), + 0, + "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", + ); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + interventions: 1, + result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), + ..InspectorLedger::default() + }, + "{sizing:?}: and the traffic is what the outcome claimed, off the envelope", + ); + assert_eq!( + reading.destroyed, FORWARDED, + "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", + ); + } +} + +/// The generic callback intercepts too, and is measured by the same rule. +/// +/// revm runs `frame_start` before the variant-specific `call` / `create`, and an outcome returned +/// there skips both. A lane wired only to the variant hooks would leave this one unmeasured. +#[test] +fn test_the_generic_frame_start_interception_is_measured_too() { + /// Intercepts the call to [`CALLEE`] from the generic callback, handing back half. + #[derive(Default)] + struct GenericInterceptor { + intercepted: u64, + } + + impl Inspector for GenericInterceptor { + fn frame_start( + &mut self, + _context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + let FrameInput::Call(inputs) = frame_input else { return None }; + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + Some(FrameResult::Call(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit / 2), + ), + inputs.return_memory_offset.clone(), + ))) + } + } + + let mut inspector = GenericInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "the generic callback's interception books on the same lane as the variant one's", + ); +} + +/// Init code that writes one slot and returns two bytes of runtime code. +fn init_code() -> Vec { + BytecodeBuilder::default() + .sstore(U256::from(0x30), U256::from(1)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset + .append(RETURN) + .build() + .to_vec() +} + +/// The entry contract: one `CREATE`, then `STOP`. +fn create_fixture() -> MemoryDatabase { + base_db(deploy_then_stop(&init_code())) +} + +/// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. +/// +/// The envelope is not a constant here — `CREATE` forwards all but a sixty-fourth of what the +/// caller holds — so the test reads it back from the callback rather than asserting a figure. +#[test] +fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() { + /// Intercepts the creation, handing back half of what it was given. + #[derive(Default)] + struct CreateInterceptor { + intercepted: u64, + envelope: u64, + } + + impl Inspector for CreateInterceptor { + fn create( + &mut self, + _context: &mut CTX, + inputs: &mut CreateInputs, + ) -> Option { + self.intercepted += 1; + self.envelope = inputs.gas_limit(); + Some(CreateOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit() / 2), + ), + None, + )) + } + } + + let mut inspector = CreateInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, create_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); + assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), + interventions: 1, + ..InspectorLedger::default() + }, + "a creation's interception is measured against the envelope its CREATE forwarded", + ); +} + +/// The envelope an interception is measured against is the one the callback *received*. +/// +/// A callback is free to edit the inputs and then answer the frame itself. The edit reaches no +/// frame — nothing is built from those inputs — so the envelope the caller actually funded is the +/// one the callback was handed, and an outcome echoing the *edited* limit hands back more than +/// that. Measuring against the post-edit number instead would read this run as conjuring nothing. +#[test] +fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { + const BONUS: u64 = 9_000; + + /// Raises the child's gas limit and then intercepts, echoing the raised figure. + #[derive(Default)] + struct RaisingInterceptor { + intercepted: u64, + } + + impl Inspector for RaisingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + inputs.gas_limit += BONUS; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let mut inspector = RaisingInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(i128::from(BONUS)), + interventions: 1, + ..InspectorLedger::default() + }, + "the bonus reaches the caller through the outcome, so it is booked once, on the result \ + lane — the env lane stays empty because no frame was ever built from those inputs", + ); +} + +/// The lane reports on a frozen spec too, and reporting it settles nothing there. +/// +/// The measurement is not REX7-gated, and neither are the two lanes it joins: `InspectorLedger` is +/// what the canonical block path's guard reads, so a frame an inspector answered has to be visible +/// on it whatever spec is executing. What is REX7's alone is the settlement the lane feeds — the +/// envelope a refused frame init decides the fate of. REX6 derives nothing from the envelope and +/// books no destroyed remainder, so what it reports is what it always reported. +/// +/// The transaction's own gas does follow the figure the inspector wrote, on both specs. That is +/// the EVM handing the caller back what the result carries, which is upstream's arithmetic rather +/// than `MegaETH`'s, and it is the movement the lane exists to account for rather than to prevent. +#[test] +fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { + let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); + let echo = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut echoing, + ); + let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let half = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut halving, + ); + + assert_eq!(echoing.intercepted, 1, "fixture check"); + assert_eq!(halving.intercepted, 1, "fixture check"); + assert_eq!( + echo.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "REX6: an echoed envelope moves no gas here either", + ); + assert_eq!( + half.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "REX6: the lane reports, because the block guard has to see this frame on every spec", + ); + assert_eq!( + (echo.destroyed, half.destroyed), + (0, 0), + "REX6 has no destroyed remainder to book, on either sizing", + ); + assert_eq!( + echo.compute_gas, half.compute_gas, + "and its compute total does not follow the figure the inspector wrote", + ); + assert_eq!( + half.total_gas_spent - echo.total_gas_spent, + FORWARDED / 2, + "the caller really did lose the half the outcome withheld — that is the EVM's arithmetic", + ); +} diff --git a/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs b/crates/mega-evm/tests/rex7/shim_refusals.rs similarity index 76% rename from crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs rename to crates/mega-evm/tests/rex7/shim_refusals.rs index 8c67d295..8ecef937 100644 --- a/crates/mega-evm/tests/rex7/frame_init_result_rewrite.rs +++ b/crates/mega-evm/tests/rex7/shim_refusals.rs @@ -1,25 +1,33 @@ -//! A result frame init produced itself cannot have its classification rewritten. +//! The rewrites the shim refuses outright, and the near boundary of each refusal. //! -//! Every other rewrite of a frame's result is supported, because REX7 withholds the journal -//! decision until the result is final: the frame loops park it and `frame_return_result` carries -//! it out, so a frame rewritten into a revert has its state rolled back with it. +//! Almost everything an inspector does is measured and booked. Two shapes are not, because +//! honouring them would produce a receipt that contradicts state the EVM had already decided on +//! before any callback ran: //! -//! A result that comes out of frame *init* has no such window. Upstream decides the journal inside -//! `make_call_frame` — a value-transferring call into an empty-code account commits the transfer -//! and returns `Stop`, a failing precompile reverts the transfer and returns its own failure — and -//! `MegaETH`'s interceptors decide it before they return, the `KeylessDeploy` one by merging a -//! whole sandbox's state. All of that has happened by the time any callback sees the result, and -//! none of it is reachable from one. +//! - **A failed contract creation rewritten into a successful one.** By the time `create_end` or +//! `frame_end` runs, revm has reverted the frame's checkpoint and declined to deposit any code, +//! so the rewrite reports a deployment that did not happen. Both callbacks are covered, because a +//! refusal wired only to the earlier one is one an inspector can step past. +//! - **The classification of a result frame init produced.** A precompile, an empty-code call, and +//! the `KeylessDeploy` interceptor's synthetic result all come back out of frame init with the +//! journal decision behind them already taken and no frame checkpoint left to unwind. What each +//! case below pins is the state that decision left behind, and that the caller is not told +//! something else about it. //! -//! So a rewrite that moves such a result across the success / revert / halt boundary hands the -//! caller an answer the state behind it contradicts. Each test below reaches that split by a -//! different door, and asserts the absence of the split before it asserts the refusal — so a run -//! that honours the rewrite reports the two halves that disagree rather than only the missing -//! counter. +//! The near boundary is a frame the inspector answered *itself*. That result comes out of frame +//! init too, and it is not refused — nothing in the EVM decided anything for it, so there is +//! nothing for the rewrite to contradict. +//! +//! Both halves surface the same way in both builds: a debug build asserts (the shape is a +//! detector, and a corpus that produces it should stop), a release build fails the transaction +//! with the same message. use crate::{ common::{base_db, context, try_drive, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}, - inspector_common::append_call, + inspector_common::{ + append_call, assert_refused, deploy_then_stop, limits, try_transact_inspected, + REVERTING_INIT_CODE, REVIVED_CREATION, + }, }; use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; use alloy_sol_types::SolCall as _; @@ -32,14 +40,85 @@ use mega_evm::{ use revm::{ bytecode::opcode::{RETURN, SSTORE, STOP}, context::{result::ExecutionResult, tx::TxEnvBuilder, ContextTr}, + handler::FrameResult, interpreter::{ - CallInputs, CallOutcome, Gas, InstructionResult, InterpreterResult, InterpreterTypes, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, + InterpreterResult, InterpreterTypes, }, state::EvmState, Inspector, }; use std::{string::String, vec::Vec}; +// === a failed creation, revived =============================================================== + +/// Rewrites every failed contract creation into a successful one, from `create_end`. +#[derive(Default)] +struct CreateReviver; + +impl Inspector for CreateReviver { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if !outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Return; + } + } +} + +/// A `create_end` that turns a failed contract creation into a successful one is refused, loudly. +/// +/// By that point revm has already reverted the frame's journal checkpoint and already declined to +/// deposit any code, so the rewrite would report a deployment that did not happen. The shim +/// restores the original classification and refuses to let the transaction produce a receipt at +/// all. +#[test] +fn test_reviving_a_failed_creation_is_refused() { + let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); + + assert_refused(REVIVED_CREATION, || { + let mut inspector = CreateReviver; + try_transact_inspected(db.clone(), limits(), &mut inspector) + }); +} + +/// `frame_end` is the last callback that can rewrite a creation's classification, and the refusal +/// covers it too. +/// +/// `measured_inspector.rs` pins the `create_end` form. This is the one callback later: revm calls +/// `create_end` first and `frame_end` after it, so an inspector that leaves `create_end` alone and +/// rewrites in `frame_end` would slip past a refusal wired only to the earlier one. +#[test] +fn test_reviving_a_failed_creation_is_refused_at_frame_end() { + /// Rewrites a failed creation into a success, from `frame_end` only. + struct LateReviver; + + impl Inspector for LateReviver { + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + let FrameResult::Create(outcome) = frame_result else { return }; + if !outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Stop; + } + } + } + + let db = base_db(deploy_then_stop(&REVERTING_INIT_CODE)); + + assert_refused(REVIVED_CREATION, || { + try_transact_inspected(db.clone(), limits(), &mut LateReviver) + }); +} + +// === the classification of a result frame init produced ======================================= + /// Transaction gas limit: high enough that EVM gas never binds. const TX_GAS_LIMIT: u64 = 30_000_000; @@ -156,7 +235,7 @@ where /// The two facts every case here pins: the rewrite was counted as refused, and the transaction /// failed with an error rather than reporting a receipt built on it. -fn assert_refused(reading: &Reading) { +fn assert_reading_refused(reading: &Reading) { assert_eq!(reading.rejected_rewrites, 1, "the shim must count the refusal"); assert!( reading.result.is_err(), @@ -209,7 +288,7 @@ fn test_rewriting_an_empty_code_call_into_a_revert_is_refused() { reading.storage(CONTRACT, FLAG_SLOT), reading.balance(EMPTY_TARGET), ); - assert_refused(&reading); + assert_reading_refused(&reading); } /// A value-transferring `CALL` into a precompile that cannot afford its own fee, rewritten from @@ -232,7 +311,7 @@ fn test_reviving_a_failed_precompile_call_is_refused() { reading.storage(CONTRACT, FLAG_SLOT), reading.balance(ECRECOVER), ); - assert_refused(&reading); + assert_reading_refused(&reading); } /// A deterministic pre-EIP-155 keyless deployment transaction whose init code returns one byte of @@ -316,7 +395,7 @@ fn test_rewriting_the_keyless_deploy_synthetic_result_is_refused() { deployed code anyway", reading.result, ); - assert_refused(&reading); + assert_reading_refused(&reading); } /// Answers the frame itself and then moves the classification of its own answer. diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 7ba1d87b..a5a6b472 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -3,7 +3,7 @@ //! # What this is for //! //! `MegaETH` supports rewriting inspectors in full: the measurement shim books what one does to a -//! transaction's gas, and the conservation law accounts for it. `tests/rex7/measured_inspector.rs` +//! transaction's gas, and the conservation law accounts for it. `tests/rex7/shim_measurement.rs` //! and `tests/rex7/inspector_cheat_matrix.rs` pin that mechanism shape by shape, on fixtures built //! to reach each shape. What neither can do is put a rewriting inspector on top of *arbitrary* //! execution — the corner of the state space where a rewrite meets a detained frame, a latched @@ -716,7 +716,7 @@ impl ChaosInspector { // correctly booked nowhere, and would then look to the ledger gate like a rewrite // the shim missed. Leave the counter alone and spend no budget; the same edit // reaches the live object at the next callback, and the dead window itself is - // pinned by `tests/rex7/inspector_settlement_window.rs`. + // pinned by `tests/rex7/shim_measurement.rs`. if matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) { return; } From fc6671960a054eb49be63bcd6503d460353af8a3 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:09:09 +0800 Subject: [PATCH 187/208] refactor(evm): book the shim's lanes through one borrow, and state the two refusals once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five `book_*` helpers each took a whole `MegaContext` and reached back through it for the tracker, so a live-interpreter callback took four `RefCell` borrows where the readings it books are one boundary, and each helper was instantiated once per `(DB, ExtEnvs)` pair for no reason. They take the tracker directly now: one borrow per callback, one machine copy each, and one hop less between the boundary that measures and the lane that records. The two refusals were the same body twice — the REX7 gate, the restore, the count, the error slot — differing in which rewrite they recognise and how loud a debug build is about it. `Forbidden` names those two differences and `reject_forbidden_rewrite` is the body. Both debug assertions are preserved, each on its own arm. --- crates/mega-evm/src/evm/inspector.rs | 259 +++++++++++++-------------- 1 file changed, 129 insertions(+), 130 deletions(-) diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 0e5436c1..042d445e 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -48,7 +48,7 @@ use revm::{ Inspector, }; -use crate::{ExternalEnvTypes, MegaContext, MegaSpecId}; +use crate::{AdditionalLimit, ExternalEnvTypes, MegaContext, MegaSpecId}; /// The message a refused `create_end` rewrite surfaces as `EVMError::Custom`. /// @@ -299,31 +299,12 @@ fn held_refund(action: Option<&InterpreterAction>, counter: i64) -> i64 { /// the chain of frame returns between here and the receipt. Neither of those is a quantity a /// boundary can measure, and over-stating is the safe direction for the lane's one consumer. #[inline] -fn book_refund( - context: &MegaContext, - before: i64, - after: i64, -) { +fn book_refund(limit: &mut AdditionalLimit, before: i64, after: i64) { if before != after { - context - .additional_limit - .borrow_mut() - .record_inspector_refund_adjustment(i128::from(after) - i128::from(before)); + limit.record_inspector_refund_adjustment(i128::from(after) - i128::from(before)); } } -/// The refund a synthetic outcome carries, for a callback that answered a frame itself. -/// -/// There is no "before" to difference against, so the whole of it is the inspector's. The baseline -/// is zero rather than the envelope because a frame that never ran has refunded nothing. -#[inline] -fn book_synthetic_refund( - context: &MegaContext, - refunded: i64, -) { - book_refund(context, 0, refunded); -} - /// What a live-interpreter callback did to the interpreter's pending action. #[derive(Clone, Copy, Debug)] struct ActionChange { @@ -404,19 +385,15 @@ impl ActionSnapshot { /// rather than a shape the API offers — `reset_action` leaves the action in place, so emptying /// the slot means writing `None` and desynchronising the two. #[inline] -fn book_pending_action( - context: &MegaContext, - change: ActionChange, -) { +fn book_pending_action(limit: &mut AdditionalLimit, change: ActionChange) { if change.gas != 0 { - let mut limit = context.additional_limit.borrow_mut(); match change.lane { ActionLane::Result => limit.stage_inspector_action_result_adjustment(change.gas), ActionLane::Envelope => limit.stage_inspector_action_env_adjustment(change.gas), ActionLane::Counter => limit.record_inspector_action_counter_adjustment(change.gas), } } - book_intervention(context, change.rewritten); + book_intervention(limit, change.rewritten); } /// The gas limit a frame input carries, for the two variants that have one. @@ -445,28 +422,28 @@ fn frame_input_gas_limit(frame_input: &FrameInput) -> Option { /// inspector wrote from nothing. What the transaction funded is the envelope on the way in, and /// only the frame init that asked can take that difference. #[inline] -fn book_env_adjustment( - context: &MegaContext, +fn book_env_adjustment( + limit: &mut AdditionalLimit, before: Option, after: Option, intercepted: bool, ) { - let staged = context.additional_limit.borrow_mut().take_inspector_action_env_adjustment(); + let staged = limit.take_inspector_action_env_adjustment(); let callback = match (intercepted, before, after) { (false, Some(before), Some(after)) => i128::from(after) - i128::from(before), _ => 0, }; if let (true, Some(before)) = (intercepted, before) { - context.additional_limit.borrow_mut().stage_inspector_interception_envelope(before); + limit.stage_inspector_interception_envelope(before); } // Booked separately rather than summed first: the two were written in different callbacks, so // an envelope raised in one and lowered back in the other is two edits, not none. The staged // half already counted its own traffic where it was measured, so only its movement lands here. if staged != 0 { - context.additional_limit.borrow_mut().record_staged_inspector_env_movement(staged); + limit.record_staged_inspector_env_movement(staged); } if callback != 0 { - context.additional_limit.borrow_mut().record_inspector_env_adjustment(callback); + limit.record_inspector_env_adjustment(callback); } } @@ -480,12 +457,9 @@ fn book_env_adjustment( /// telling whether those came back changed needs a snapshot of unbounded state that no per-opcode /// boundary can take. Their sizes are constant-time readings and are covered. #[inline] -fn book_intervention( - context: &MegaContext, - changed: bool, -) { +fn book_intervention(limit: &mut AdditionalLimit, changed: bool) { if changed { - context.additional_limit.borrow_mut().record_inspector_intervention(); + limit.record_inspector_intervention(); } } @@ -822,89 +796,113 @@ impl ResultClass { } } -/// Refuses a rewrite that moves a frame-init result across the success / revert / halt boundary. +/// One shape of rewrite the shim refuses. /// -/// Every other classification rewrite is supported because REX7 withholds the journal decision -/// until the result is final, so a frame rewritten into a revert has its state rolled back with -/// it. A result out of frame *init* has no such window and cannot be given one from here: upstream -/// decides inside `make_call_frame` — an empty-code call commits its transfer and returns `Stop`, -/// a failing precompile reverts and returns its own failure — and `MegaETH`'s interceptors decide -/// before they return, the `KeylessDeploy` one by merging a whole sandbox's state. Honouring a -/// rewrite would hand the caller an answer the state behind it contradicts. +/// Both are detection only: nothing here compensates the journal. The classification is restored, +/// the ledger counts the refusal, and the error slot carries the reason so the transaction fails +/// rather than producing a receipt built on the rewrite. What differs is how loud a debug build +/// is, which is [`Self::note_in_debug`]. /// -/// A result an inspector answered the frame with itself is deliberately outside the refusal: -/// nothing in the EVM decided anything for it, so its classification is the inspector's to state. -/// What separates the two is which callback site in `inspect_frame_init` ran, which is where the -/// window is opened. -/// -/// Detection only; nothing here compensates the journal. The classification is restored, the -/// ledger counts the refusal, and the error slot carries the reason so the transaction fails -/// rather than producing a receipt built on the rewrite. Loud but not fatal, unlike -/// [`reject_forbidden_create_rewrite`]: this is the most ordinary rewrite a tool makes — failing a -/// call — landing on the one frame kind it cannot be applied to, so a corpus should be able to -/// report it rather than die on it. -/// -/// Gated to REX7+: on a frozen spec a rewrite reaches no accounting lane it can make unsound, and -/// those specs' behaviour is closed. -#[inline] -fn reject_forbidden_frame_init_rewrite( - context: &mut MegaContext, - before: InstructionResult, - result: &mut InterpreterResult, -) { - if !context.spec.is_enabled(MegaSpecId::REX7) || - ResultClass::of(before) == ResultClass::of(result.result) || - !context.additional_limit.borrow().is_settling_frame_init_result() - { - return; +/// Both are gated to REX7+: on a frozen spec a rewrite reaches no accounting lane it can make +/// unsound, and those specs' behaviour is closed. +#[derive(Clone, Copy, Debug)] +enum Forbidden { + /// A result frame init produced, moved across the success / revert / halt boundary. + /// + /// Every other classification rewrite is supported because REX7 withholds the journal decision + /// until the result is final, so a frame rewritten into a revert has its state rolled back + /// with it. A result out of frame *init* has no such window and cannot be given one from + /// here: upstream decides inside `make_call_frame` — an empty-code call commits its + /// transfer and returns `Stop`, a failing precompile reverts and returns its own failure — + /// and `MegaETH`'s interceptors decide before they return, the `KeylessDeploy` one by + /// merging a whole sandbox's state. Honouring a rewrite would hand the caller an answer + /// the state behind it contradicts. + /// + /// A result an inspector answered the frame with itself is deliberately outside the refusal: + /// nothing in the EVM decided anything for it, so its classification is the inspector's to + /// state. What separates the two is which callback site in `inspect_frame_init` ran, which is + /// where the window is opened. + FrameInitRewrite, + /// A non-successful contract creation turned into a successful one. + /// + /// Forbidden rather than supported because there is no state behind it: by the time + /// `create_end` runs, revm has reverted the frame's checkpoint and declined to deposit the + /// code — the size limit, the `0xEF` prefix rule and the code-deposit charge are all evaluated + /// before the callback. Honouring it would report a deployment at an address holding no code. + CreateRevival, +} + +impl Forbidden { + /// The reason the error slot carries, which is also what a test matches on. + const fn message(self) -> &'static str { + match self { + Self::FrameInitRewrite => FORBIDDEN_FRAME_INIT_REWRITE, + Self::CreateRevival => FORBIDDEN_CREATE_REVIVAL, + } } - result.result = before; - context.additional_limit.borrow_mut().record_inspector_rejected_rewrite(); - let slot = context.error(); - if slot.is_ok() { - *slot = Err(ContextError::Custom(String::from(FORBIDDEN_FRAME_INIT_REWRITE))); + + /// Whether this rewrite is the one that happened. + #[inline] + fn applies( + self, + context: &MegaContext, + before: InstructionResult, + after: InstructionResult, + ) -> bool { + match self { + Self::FrameInitRewrite => { + ResultClass::of(before) != ResultClass::of(after) && + context.additional_limit.borrow().is_settling_frame_init_result() + } + Self::CreateRevival => !before.is_ok() && after.is_ok(), + } + } + + /// What a debug build does about it, once the refusal has been carried out. + /// + /// A frame-init rewrite is the most ordinary rewrite a tool makes — failing a call — landing + /// on the one frame kind it cannot be applied to, so a corpus should be able to report it + /// rather than die on it; all that is asserted is that the refusal did restore the + /// classification. A revived creation has no reading behind it at all, so a corpus that + /// produces one should stop. + #[inline] + fn note_in_debug(self, before: InstructionResult, after: InstructionResult) { + match self { + Self::FrameInitRewrite => debug_assert_eq!( + ResultClass::of(after), + ResultClass::of(before), + "{}: the refusal must leave the caller holding the classification the EVM \ + produced", + self.message(), + ), + Self::CreateRevival => debug_assert!( + false, + "{}: {before:?} was rewritten to a success, which no journal entry and no \ + deposited code stands behind", + self.message(), + ), + } } - debug_assert_eq!( - ResultClass::of(result.result), - ResultClass::of(before), - "{FORBIDDEN_FRAME_INIT_REWRITE}: the refusal must leave the caller holding the \ - classification the EVM produced", - ); } -/// Refuses a rewrite that turns a non-successful contract creation into a successful one, and says -/// so loudly. -/// -/// Forbidden rather than supported because there is no state behind it: by the time `create_end` -/// runs, revm has reverted the frame's checkpoint and declined to deposit the code — the size -/// limit, the `0xEF` prefix rule and the code-deposit charge are all evaluated before the -/// callback. Honouring it would report a deployment at an address holding no code. -/// -/// Detection only, on the same terms as [`reject_forbidden_frame_init_rewrite`], except that debug -/// builds assert: this shape is a mistake with no reading behind it, and a corpus that produces it -/// should stop rather than quietly take the rejection path. -/// -/// Gated to REX7+ for the same reason as that one. +/// Restores the classification a forbidden rewrite moved, and fails the transaction over it. #[inline] -fn reject_forbidden_create_rewrite( +fn reject_forbidden_rewrite( context: &mut MegaContext, + what: Forbidden, before: InstructionResult, result: &mut InterpreterResult, ) { - if !context.spec.is_enabled(MegaSpecId::REX7) || before.is_ok() || !result.result.is_ok() { + if !context.spec.is_enabled(MegaSpecId::REX7) || !what.applies(context, before, result.result) { return; } result.result = before; context.additional_limit.borrow_mut().record_inspector_rejected_rewrite(); let slot = context.error(); if slot.is_ok() { - *slot = Err(ContextError::Custom(String::from(FORBIDDEN_CREATE_REVIVAL))); + *slot = Err(ContextError::Custom(String::from(what.message()))); } - debug_assert!( - false, - "{FORBIDDEN_CREATE_REVIVAL}: {before:?} was rewritten to a success, which no journal \ - entry and no deposited code stands behind", - ); + what.note_in_debug(before, result.result); } /// What the shim reads off a live interpreter on the way into a callback, and settles on the way @@ -973,20 +971,16 @@ impl LiveReading { }; let refund = held_refund(action, refunded); - book_intervention(context, moved); - book_pending_action(context, change); - book_refund(context, self.refund, refund); - // `record_inspector_gas_adjustment` returns on its own when the counter did not move, and - // the borrow it would take to find that out is not free on a path taken twice per opcode. + let mut limit = context.additional_limit.borrow_mut(); + book_intervention(&mut limit, moved); + book_pending_action(&mut limit, change); + book_refund(&mut limit, self.refund, refund); if gas != self.gas { - context - .additional_limit - .borrow_mut() - .record_inspector_gas_adjustment::( - &mut interp.gas, - self.gas, - lane.counter_reaches_envelope(), - ); + limit.record_inspector_gas_adjustment::( + &mut interp.gas, + self.gas, + lane.counter_reaches_envelope(), + ); } } } @@ -1084,19 +1078,21 @@ impl MeasuredInspector { /// when the callback answered the frame itself — the refund its synthetic outcome carries. /// `intercepted_refund` is `Some` exactly when it did. #[inline] -fn book_frame_entry( - context: &MegaContext, +fn book_frame_entry( + limit: &mut AdditionalLimit, before: Option, after: Option, intercepted_refund: Option, rewritten: bool, ) { let intercepted = intercepted_refund.is_some(); - book_env_adjustment(context, before, after, intercepted); + book_env_adjustment(limit, before, after, intercepted); if let Some(refund) = intercepted_refund { - book_synthetic_refund(context, refund); + // A synthetic outcome has no "before" to difference against, so the whole of its refund is + // the inspector's; the baseline is zero because a frame that never ran refunded nothing. + book_refund(limit, 0, refund); } - book_intervention(context, intercepted || rewritten); + book_intervention(limit, intercepted || rewritten); } /// What a finished frame reads as on the way into an `*_end` callback. @@ -1126,12 +1122,15 @@ impl FrameEnding { metadata: M, is_create: bool, ) { - book_refund(context, self.refund, result.gas.refunded()); - book_intervention(context, result_rewritten((self.result, &self.output), result)); - book_intervention(context, metadata != self.metadata); - reject_forbidden_frame_init_rewrite(context, self.result, result); + { + let mut limit = context.additional_limit.borrow_mut(); + book_refund(&mut limit, self.refund, result.gas.refunded()); + book_intervention(&mut limit, result_rewritten((self.result, &self.output), result)); + book_intervention(&mut limit, metadata != self.metadata); + } + reject_forbidden_rewrite(context, Forbidden::FrameInitRewrite, self.result, result); if is_create { - reject_forbidden_create_rewrite(context, self.result, result); + reject_forbidden_rewrite(context, Forbidden::CreateRevival, self.result, result); } } } @@ -1212,7 +1211,7 @@ where let before = frame_input.clone(); let outcome = self.inner.frame_start(context, frame_input); book_frame_entry( - context, + &mut context.additional_limit.borrow_mut(), frame_input_gas_limit(&before), frame_input_gas_limit(frame_input), outcome.as_ref().map(|outcome| outcome.gas().refunded()), @@ -1257,7 +1256,7 @@ where let before = inputs.clone(); let outcome = self.inner.call(context, inputs); book_frame_entry( - context, + &mut context.additional_limit.borrow_mut(), Some(before.gas_limit), Some(inputs.gas_limit), outcome.as_ref().map(|outcome| outcome.result.gas.refunded()), @@ -1304,7 +1303,7 @@ where let before = inputs.clone(); let outcome = self.inner.create(context, inputs); book_frame_entry( - context, + &mut context.additional_limit.borrow_mut(), Some(before.gas_limit()), Some(inputs.gas_limit()), outcome.as_ref().map(|outcome| outcome.result.gas.refunded()), From 8021578efc70888eaebba1bc9cd35a3277c5ee9c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:13:10 +0800 Subject: [PATCH 188/208] refactor(state-test): make the chaos shape pool one row per shape The enum, the list every sweep iterates and the label reports print were three lists of the same twenty-eight shapes, kept in step by hand. `shapes!` takes one row per shape and derives all three, so a shape added to the declaration cannot shrink the sweep or print a wrong label. The three callback pools stay written out: they are membership facts rather than views of a row, and their order is what the draw sequence depends on. The four live-interpreter callbacks shared a body verbatim; it is `hit_live`. --- crates/mega-state-test/src/chaos.rs | 191 ++++++++++++---------------- 1 file changed, 78 insertions(+), 113 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index a5a6b472..be3e4597 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -167,120 +167,118 @@ pub fn vector_seed(global: u64, path: &str, name: &str, indexes: TxPartIndices) // --- the shapes --------------------------------------------------------------------------------- -/// A rewrite shape the chaos pool draws from — one legal column of the cheat-shape matrix. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum ChaosShape { +/// Declares the shape pool as one row per shape. +/// +/// The enum, the list every sweep iterates and the label a report and `--chaos-shapes` use are +/// three views of one row. Declared separately, a shape added to the enum and missed in the list +/// shrinks the sweep silently, and one missed in the labels prints the wrong name in a report. +macro_rules! shapes { + ($( $(#[$meta:meta])* $variant:ident = $label:literal; )*) => { + /// A rewrite shape the chaos pool draws from — one legal column of the cheat-shape matrix. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + pub enum ChaosShape { + $($(#[$meta])* $variant,)* + } + + impl ChaosShape { + /// Every shape, in the order the labels are listed by `--chaos-shapes`, which is also + /// the order [`ShapeFilter`]'s bitmask indexes through the discriminant. + pub const ALL: [Self; [$(shapes!(@unit $variant)),*].len()] = + [$(Self::$variant,)*]; + + /// Stable label, for reports. + pub const fn label(self) -> &'static str { + match self { + $(Self::$variant => $label,)* + } + } + } + }; + // One `()` per row, so the array's length is counted from the declaration rather than + // written out beside it. + (@unit $variant:ident) => { () }; +} + +shapes! { /// Gas written into a live interpreter's counter. - InjectGas, + InjectGas = "inject_gas"; /// Gas taken out of one. - DrainGas, + DrainGas = "drain_gas"; /// The interpreter's own working state — a memory word, or the operand an `SSTORE` is about /// to consume. - EditFrameState, + EditFrameState = "edit_frame_state"; /// A transient-storage write made behind the EVM's back. - JournalWrite, + JournalWrite = "journal_write"; /// A raised `gas_limit` on a frame about to be built. - RaiseEnvelope, + RaiseEnvelope = "raise_envelope"; /// A lowered one. - LowerEnvelope, + LowerEnvelope = "lower_envelope"; /// A call turned static, so what the frame is allowed to do changes rather than what it costs. - MakeStatic, + MakeStatic = "make_static"; /// A synthetic outcome, so no frame is built at all. Its gas echoes the envelope the callback /// was handed, which is what every tool that intercepts does. - Intercept, + Intercept = "intercept"; /// The same, sized above the envelope, so the outcome hands the caller back gas the /// transaction never funded. - InterceptOverGas, + InterceptOverGas = "intercept_over_gas"; /// Sized below it, so the caller spends the difference on a frame that never ran. - InterceptUnderGas, + InterceptUnderGas = "intercept_under_gas"; /// Sized at nothing, the extreme of the same direction: the whole envelope is consumed. - InterceptNoGas, + InterceptNoGas = "intercept_no_gas"; /// A raised remaining-gas figure on a finished frame's result. - RaiseResultGas, + RaiseResultGas = "raise_result_gas"; /// A lowered one. - LowerResultGas, + LowerResultGas = "lower_result_gas"; /// A successful frame result rewritten into a revert or an exceptional halt. - FailFrame, + FailFrame = "fail_frame"; /// A failed *call* frame rewritten into a success. The creation form of this shape is refused /// by the shim and is deliberately not in the pool — see the module docs. - ReviveCall, + ReviveCall = "revive_call"; /// Gas written into the action the interpreter is already holding — the object a terminating /// or suspending instruction left behind, which carries its own copy of what the frame is /// handing on. - RaiseActionGas, + RaiseActionGas = "raise_action_gas"; /// Gas taken out of one. - LowerActionGas, + LowerActionGas = "lower_action_gas"; /// A refund added to a `Gas`'s refund counter — what the sender is billed, which the envelope /// the conservation law is stated over does not reach. - RaiseRefund, + RaiseRefund = "raise_refund"; /// A refund taken out of one. Skipped when the `Gas` has none, rather than driving the counter /// negative — a state revm documents as invalid at the end of a transaction. - LowerRefund, + LowerRefund = "lower_refund"; /// An EIP-8037 state-gas pool written into a `Gas` or a call's inputs. `MegaETH` runs with the /// EIP off and fills no pool, so anything found in one is gas the transaction never funded. - WriteReservoir, + WriteReservoir = "write_reservoir"; /// An EIP-8037 spend counter written into a `Gas`. Structurally zero for the same reason, and /// reachable through two different receipt figures depending on how the frame ends. - WriteStateGas, + WriteStateGas = "write_state_gas"; /// The frame's memory grown, together with the memo of how far it has been paid for, so that /// the interpreter stays consistent and the next expanding opcode is charged nothing. - GrowMemoryFree, + GrowMemoryFree = "grow_memory_free"; /// A finished outcome's metadata rewritten around the `InterpreterResult` inside it: the range /// a call's return data lands in, shrunk to nothing, or the address a creation reports. - MoveOutcomeMetadata, + MoveOutcomeMetadata = "move_outcome_metadata"; /// Gas injected into an interpreter counter and taken straight back out at the next /// live-interpreter callback, so the lane's net is zero and the frame saw a number in between /// that the EVM would never have produced. - CancelGasEdit, + CancelGasEdit = "cancel_gas_edit"; /// A refund added to one finished frame's result and taken out of the next one's, so the /// lane's net is zero and — whenever the two frames end differently — the receipt's is /// not. - CancelRefundEdit, + CancelRefundEdit = "cancel_refund_edit"; /// The program counter stepped past the instruction the frame was about to execute, deleting /// it from the frame. The work is never performed, so no counter falls and no lane moves. - SkipOpcode, + SkipOpcode = "skip_opcode"; /// A return buffer put in front of the frame, so its `RETURNDATASIZE` and `RETURNDATACOPY` /// read data no call produced. - RewriteReturnData, + RewriteReturnData = "rewrite_return_data"; /// The classification of a result *frame init* produced, moved across the success / revert / /// halt boundary. The shim refuses this one, so the run it lands in is declined rather than /// executed — which is the verdict [`ChaosClass::Refused`] names. - MoveInitResultClass, + MoveInitResultClass = "move_init_result_class"; } impl ChaosShape { - /// Every shape, in the order the labels are listed by `--chaos-shapes`. - pub const ALL: [Self; 28] = [ - Self::InjectGas, - Self::DrainGas, - Self::EditFrameState, - Self::JournalWrite, - Self::RaiseEnvelope, - Self::LowerEnvelope, - Self::MakeStatic, - Self::Intercept, - Self::InterceptOverGas, - Self::InterceptUnderGas, - Self::InterceptNoGas, - Self::RaiseResultGas, - Self::LowerResultGas, - Self::FailFrame, - Self::ReviveCall, - Self::RaiseActionGas, - Self::LowerActionGas, - Self::RaiseRefund, - Self::LowerRefund, - Self::WriteReservoir, - Self::WriteStateGas, - Self::GrowMemoryFree, - Self::MoveOutcomeMetadata, - Self::CancelGasEdit, - Self::CancelRefundEdit, - Self::SkipOpcode, - Self::RewriteReturnData, - Self::MoveInitResultClass, - ]; - /// The shape a label names. /// /// # Errors @@ -295,40 +293,6 @@ impl ChaosShape { }) } - /// Stable label, for reports. - pub const fn label(self) -> &'static str { - match self { - Self::InjectGas => "inject_gas", - Self::DrainGas => "drain_gas", - Self::EditFrameState => "edit_frame_state", - Self::JournalWrite => "journal_write", - Self::RaiseEnvelope => "raise_envelope", - Self::LowerEnvelope => "lower_envelope", - Self::MakeStatic => "make_static", - Self::Intercept => "intercept", - Self::InterceptOverGas => "intercept_over_gas", - Self::InterceptUnderGas => "intercept_under_gas", - Self::InterceptNoGas => "intercept_no_gas", - Self::RaiseResultGas => "raise_result_gas", - Self::LowerResultGas => "lower_result_gas", - Self::FailFrame => "fail_frame", - Self::ReviveCall => "revive_call", - Self::RaiseActionGas => "raise_action_gas", - Self::LowerActionGas => "lower_action_gas", - Self::RaiseRefund => "raise_refund", - Self::LowerRefund => "lower_refund", - Self::WriteReservoir => "write_reservoir", - Self::WriteStateGas => "write_state_gas", - Self::GrowMemoryFree => "grow_memory_free", - Self::MoveOutcomeMetadata => "move_outcome_metadata", - Self::CancelGasEdit => "cancel_gas_edit", - Self::CancelRefundEdit => "cancel_refund_edit", - Self::SkipOpcode => "skip_opcode", - Self::RewriteReturnData => "rewrite_return_data", - Self::MoveInitResultClass => "move_init_result_class", - } - } - /// Whether the shim is contracted to book a mutation of this shape unconditionally. /// /// Most shapes are booked *when the thing they moved still reaches something*: gas written into @@ -775,6 +739,19 @@ impl ChaosInspector { } } + /// The body all four live-interpreter callbacks share: settle whatever the previous one left + /// pending, then draw one interpreter-facing shape. + fn hit_live( + &mut self, + interp: &mut Interpreter, + context: &mut CTX, + ) { + self.settle_pending_gas(interp); + if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { + self.hit_interpreter(interp, context, shape, entropy); + } + } + /// Applies an input-facing shape to a call's inputs, or intercepts the frame. fn hit_call_inputs( &mut self, @@ -1169,31 +1146,19 @@ impl Inspector, context: &mut CTX) { - self.settle_pending_gas(interp); - if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { - self.hit_interpreter(interp, context, shape, entropy); - } + self.hit_live(interp, context); } fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.settle_pending_gas(interp); - if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { - self.hit_interpreter(interp, context, shape, entropy); - } + self.hit_live(interp, context); } fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.settle_pending_gas(interp); - if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { - self.hit_interpreter(interp, context, shape, entropy); - } + self.hit_live(interp, context); } fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, _log: Log) { - self.settle_pending_gas(interp); - if let Some((shape, entropy)) = self.pick(&INTERPRETER_SHAPES) { - self.hit_interpreter(interp, context, shape, entropy); - } + self.hit_live(interp, context); } fn frame_start( From 4873aa53beb94732987ff1b63baf8dc5d3546961 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:20:42 +0800 Subject: [PATCH 189/208] test(rex7): hoist the six fixture builders the suite still wrote out per file Six helpers were copied verbatim across the suite, one per file that needed them: the Nick's-Method keyless transaction (six copies, differing only in gas limit and init code), the checkpoint-free countdown loop (five), the two runtime limits a compute or detention case runs under (four and three), and the do-nothing contract with the storage-overhead reading taken off it (two each). `common.rs` holds all six now, and `Outcome::storage_overhead` is the reading. Assertions 1051 -> 1051. Lines 17755 -> 17540 across the suite. --- crates/mega-evm/tests/rex7/burn_split.rs | 27 +------ .../tests/rex7/checkpoint_settlement.rs | 32 +------- .../tests/rex7/checkpoint_static_fee_edges.rs | 28 ++----- .../tests/rex7/clamp_classification.rs | 34 +------- crates/mega-evm/tests/rex7/common.rs | 79 ++++++++++++++++++- .../mega-evm/tests/rex7/conservation_terms.rs | 28 +------ .../tests/rex7/deposit_receipt_rewrite.rs | 31 ++------ crates/mega-evm/tests/rex7/gas_clamp.rs | 45 +---------- crates/mega-evm/tests/rex7/gas_leakage.rs | 40 +--------- .../tests/rex7/guard_pass_static_gas.rs | 35 +++----- .../mega-evm/tests/rex7/interceptor_resume.rs | 18 +---- .../tests/rex7/keyless_synthetic_halt.rs | 32 ++------ crates/mega-evm/tests/rex7/parity_shapes.rs | 31 ++------ crates/mega-evm/tests/rex7/shim_refusals.rs | 52 +++++------- 14 files changed, 153 insertions(+), 359 deletions(-) diff --git a/crates/mega-evm/tests/rex7/burn_split.rs b/crates/mega-evm/tests/rex7/burn_split.rs index 2aa92d91..1550cbaa 100644 --- a/crates/mega-evm/tests/rex7/burn_split.rs +++ b/crates/mega-evm/tests/rex7/burn_split.rs @@ -25,14 +25,13 @@ //! which side of the enforcing boundary each part lands on. use crate::common::{ - base_db, drive, plain_filler, transact, transact_default, transact_tx, + base_db, drive, keyless_tx_bytes, plain_filler, transact, transact_default, transact_tx, transact_with_bucket_capacity, transact_with_gas_limit, zero_operator_fee, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, ONE_ETH, }; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, U256}; use alloy_sol_types::SolCall as _; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, alloy_op_evm::OpTxError, constants::mini_rex::{CODEDEPOSIT_STORAGE_GAS, LOG_DATA_STORAGE_GAS, MAX_CONTRACT_SIZE}, test_utils::{BytecodeBuilder, MemoryDatabase}, @@ -602,26 +601,6 @@ fn test_the_split_is_the_same_under_an_inspector() { ); } -/// Builds a deterministic pre-EIP-155 keyless deployment transaction. -fn keyless_tx_bytes(init_code: Bytes) -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: 200_000, - to: TxKind::Create, - value: U256::ZERO, - input: init_code, - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// The `KeylessDeploy` sandbox runs a whole nested transaction with its own tracker and merges the /// usage back, so the executed / destroyed split has to survive that boundary. A sandbox whose /// constructor halts exceptionally reports its destroyed remainder like any other frame; if the @@ -636,7 +615,7 @@ fn test_sandbox_destroyed_remainder_stays_non_enforcing_across_the_merge() { // envelope is destroyed rather than performed. let init_code = BytecodeBuilder::default().append(ADD).append(STOP).build(); let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_tx_bytes(init_code), + keylessDeploymentTransaction: keyless_tx_bytes(init_code, 200_000), gasLimitOverride: U256::from(1_000_000u64), } .abi_encode(); diff --git a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs index 1285e0fa..4730887d 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_settlement.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_settlement.rs @@ -16,8 +16,8 @@ //! enforcement mechanism behind the first — the gas clamp — has its own suite in `gas_clamp`. use crate::common::{ - base_db, plain_filler, transact, transact_default, transact_with_bucket_capacity, Outcome, - CALLEE, CONTRACT, EMPTY_TARGET, + base_db, countdown_loop_code, plain_filler, transact, transact_default, + transact_with_bucket_capacity, Outcome, CALLEE, CONTRACT, EMPTY_TARGET, }; use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ @@ -25,9 +25,8 @@ use mega_evm::{ EvmTxRuntimeLimits, MegaSpecId, }; use revm::bytecode::opcode::{ - ADD, BALANCE, CALL, CALLCODE, CREATE, CREATE2, DELEGATECALL, DUP1, EXTCODEHASH, EXTCODESIZE, - GAS, JUMPDEST, JUMPI, LOG1, MUL, POP, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, SUB, - SWAP1, TIMESTAMP, + ADD, BALANCE, CALL, CALLCODE, CREATE, CREATE2, DELEGATECALL, EXTCODEHASH, EXTCODESIZE, GAS, + LOG1, MUL, POP, SELFDESTRUCT, SLOAD, SSTORE, STATICCALL, STOP, TIMESTAMP, }; /// A third contract, so a CALL chain can reach depth 2. @@ -122,29 +121,6 @@ fn assert_outcomes_match(label: &str, expect_success: bool, r6: &Outcome, r7: &O ); } -/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body: -/// -/// ```text -/// PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP -/// ``` -/// -/// `prefix` is prepended verbatim and participates in the jump-target offset. -fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { - let mut code = prefix.to_vec(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); - code.push(JUMPDEST); - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(SWAP1); - code.push(SUB); - code.push(DUP1); - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(JUMPI); - code.push(STOP); - Bytes::from(code) -} - /// A pure arithmetic loop settles only at the frame-exit checkpoint, and that single settlement /// must equal the sum the per-opcode wrappers would have recorded opcode by opcode. #[test] diff --git a/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs index 40b00d0e..111c39f4 100644 --- a/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs +++ b/crates/mega-evm/tests/rex7/checkpoint_static_fee_edges.rs @@ -15,7 +15,7 @@ //! Each family has two top-frame edges, calibrated so the named headroom is the remaining compute //! at the opcode itself (prefix `PUSH` opcodes are measured out first). -use crate::common::{base_db, transact, Outcome, CONTRACT}; +use crate::common::{base_db, rex7_compute_limit, stop_only, transact, Outcome, CONTRACT}; use alloy_primitives::{Address, Bytes}; use alloy_sol_types::SolError; use mega_evm::{ @@ -39,26 +39,14 @@ const LOG1_BODY_COMPUTE: u64 = 750; /// `CREATE` body fee. The REX7 table entry is 0; revm charges this inside the body. const CREATE_BODY_GAS: u64 = 32_000; -fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) -} - fn run(code: Bytes, limit: u64) -> Outcome { - transact(MegaSpecId::REX7, base_db(code), compute_limit(limit)) + transact(MegaSpecId::REX7, base_db(code), rex7_compute_limit(limit)) } fn unconstrained(code: Bytes) -> Outcome { transact(MegaSpecId::REX7, base_db(code), EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)) } -fn stop_only() -> Bytes { - BytecodeBuilder::default().append(STOP).build() -} - -fn storage_overhead(intrinsic: &Outcome) -> u64 { - intrinsic.gas_used - intrinsic.compute_gas -} - fn account_nonce(outcome: &Outcome, address: Address) -> u64 { outcome.state.get(&address).map(|account| account.info.nonce).unwrap_or(0) } @@ -117,7 +105,7 @@ fn test_gas_one_below_static_is_a_plain_segment_crossing() { "GAS headroom=static-1", &outcome, limit, - storage_overhead(&intrinsic), + intrinsic.storage_overhead(), ); } @@ -136,7 +124,7 @@ fn test_gas_at_static_executes_the_body() { outcome.result ); assert_eq!(outcome.compute_gas, intrinsic.compute_gas + GAS_STATIC_GAS); - assert_eq!(outcome.gas_used, outcome.compute_gas + storage_overhead(&intrinsic)); + assert_eq!(outcome.gas_used, outcome.compute_gas + intrinsic.storage_overhead()); } // --------------------------------------------------------------------------------------------- @@ -165,7 +153,7 @@ fn test_log1_one_below_static_is_a_plain_segment_crossing() { "LOG1 headroom=static-1", &outcome, limit, - storage_overhead(&intrinsic), + intrinsic.storage_overhead(), ); assert!( outcome.result.logs().is_empty(), @@ -203,7 +191,7 @@ fn test_log1_at_full_body_cost_emits_the_log() { assert_eq!(outcome.result.logs().len(), 1, "LOG1 body must emit exactly one log"); assert_eq!( outcome.gas_used, - outcome.compute_gas + storage_overhead(&intrinsic) + LOG_TOPIC_STORAGE_GAS, + outcome.compute_gas + intrinsic.storage_overhead() + LOG_TOPIC_STORAGE_GAS, "receipt gas includes the LOG topic storage component" ); } @@ -265,7 +253,7 @@ fn test_create_one_below_body_fee_runs_then_reverts() { assert_eq!(account_nonce(&outcome, CONTRACT), 0, "the creator nonce must not advance"); assert_eq!( outcome.gas_used, - outcome.compute_gas + storage_overhead(&intrinsic), + outcome.compute_gas + intrinsic.storage_overhead(), "empty-initcode CREATE adds no storage gas at minimum bucket capacity" ); } @@ -294,5 +282,5 @@ fn test_create_at_body_fee_creates_the_account() { "CREATE must leave one created account; created={:?}", created_addresses(&outcome) ); - assert_eq!(outcome.gas_used, outcome.compute_gas + storage_overhead(&intrinsic)); + assert_eq!(outcome.gas_used, outcome.compute_gas + intrinsic.storage_overhead()); } diff --git a/crates/mega-evm/tests/rex7/clamp_classification.rs b/crates/mega-evm/tests/rex7/clamp_classification.rs index e2e2c5d9..156f80ae 100644 --- a/crates/mega-evm/tests/rex7/clamp_classification.rs +++ b/crates/mega-evm/tests/rex7/clamp_classification.rs @@ -15,28 +15,19 @@ //! settlement that runs after the exceed is latched. use crate::common::{ - base_db, plain_filler as common_plain_filler, transact, transact_default, - transact_with_gas_limit, Outcome, CALLEE, + base_db, compute_limit, countdown_loop_code, plain_filler as common_plain_filler, transact, + transact_default, transact_with_gas_limit, Outcome, CALLEE, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::SolError; use mega_evm::{ - test_utils::BytecodeBuilder, EvmTxRuntimeLimits, LimitKind, MegaHaltReason, MegaLimitExceeded, - MegaSpecId, + test_utils::BytecodeBuilder, LimitKind, MegaHaltReason, MegaLimitExceeded, MegaSpecId, }; use revm::{ - bytecode::opcode::{ - CALL, DUP1, EXP, JUMPDEST, JUMPI, MSTORE, POP, RETURN, RETURNDATACOPY, RETURNDATASIZE, - STOP, SUB, SWAP1, - }, + bytecode::opcode::{CALL, EXP, MSTORE, POP, RETURN, RETURNDATACOPY, RETURNDATASIZE, STOP}, context::result::ExecutionResult, }; -/// Per-spec runtime limits with the TX compute gas limit replaced. -fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) -} - fn plain_filler(pairs: usize) -> Vec { common_plain_filler(BytecodeBuilder::default(), pairs).build_vec() } @@ -268,23 +259,6 @@ fn test_equal_value_clamp_on_a_spend_all_out_of_gas_matches_rex6_gas_used() { // The payload a clamp-induced exceed reports. // --------------------------------------------------------------------------------------------- -/// A countdown loop of cheap plain opcodes, prefixed verbatim. -fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { - let mut code = prefix.to_vec(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); - code.push(JUMPDEST); - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(SWAP1); - code.push(SUB); - code.push(DUP1); - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(JUMPI); - code.push(STOP); - Bytes::from(code) -} - /// A caller that CALLs [`CALLEE`] and returns the sub-frame's return data verbatim, so the /// `MegaLimitExceeded` payload the sub-frame reverted with is observable from the receipt. fn call_and_return_revert_data(gas: u64) -> Bytes { diff --git a/crates/mega-evm/tests/rex7/common.rs b/crates/mega-evm/tests/rex7/common.rs index 81814dde..d1c6dd17 100644 --- a/crates/mega-evm/tests/rex7/common.rs +++ b/crates/mega-evm/tests/rex7/common.rs @@ -1,19 +1,20 @@ //! Shared helpers for the REX7 test suite. -use alloy_primitives::{address, Address, Bytes, B256, U256}; +use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; use mega_evm::{ + alloy_consensus::{Signed, TxLegacy}, test_utils::{BytecodeBuilder, MemoryDatabase}, ConservationTerms, EvmTxRuntimeLimits, ExternalEnvTypes, InspectorLedger, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, TestExternalEnvs, }; use revm::{ - bytecode::opcode::POP, + bytecode::opcode::{DUP1, JUMPDEST, JUMPI, POP, STOP, SUB, SWAP1}, context::{result::ExecutionResult, tx::TxEnvBuilder, TxEnv}, handler::EvmTr, state::EvmState, Inspector, }; -use std::{collections::BTreeMap, string::String}; +use std::{collections::BTreeMap, string::String, vec::Vec}; /// Transaction sender. pub(crate) const CALLER: Address = address!("0000000000000000000000000000000000300000"); @@ -50,6 +51,72 @@ pub(crate) fn plain_filler(builder: BytecodeBuilder, pairs: usize) -> BytecodeBu builder } +/// A countdown loop of plain opcodes with no checkpoint anywhere in the body, after `prefix`, so +/// the run is one settlement segment and the gas clamp is the only thing enforcing the compute +/// limit inside it. +pub(crate) fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { + let mut code = prefix.to_vec(); + code.push(0x61); // PUSH2 + code.extend_from_slice(&iterations.to_be_bytes()); + let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); + code.push(JUMPDEST); + code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 + code.push(SWAP1); + code.push(SUB); + code.push(DUP1); + code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop + code.push(JUMPI); + code.push(STOP); + Bytes::from(code) +} + +/// A contract that does nothing, for measuring what a transaction costs before its code runs. +pub(crate) fn stop_only() -> Bytes { + BytecodeBuilder::default().append(STOP).build() +} + +/// The spec's default runtime limits with the per-transaction compute budget lowered to `limit`. +pub(crate) fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) +} + +/// [`compute_limit`] under REX7, for the files that never run a second spec. +pub(crate) fn rex7_compute_limit(limit: u64) -> EvmTxRuntimeLimits { + compute_limit(limit)(MegaSpecId::REX7) +} + +/// The spec's default runtime limits with the block-env detention cap lowered to `cap`. +pub(crate) fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { + move |spec| { + let mut limits = EvmTxRuntimeLimits::from_spec(spec); + limits.block_env_access_compute_gas_limit = cap; + limits + } +} + +/// A deterministic pre-EIP-155 keyless deployment transaction, RLP-encoded. +/// +/// The signature is Nick's Method's: an unrecoverable `r == s` that no key produced, which is what +/// makes the sender deterministic and the deployment address the same on every chain. +pub(crate) fn keyless_tx_bytes(init_code: Bytes, gas_limit: u64) -> Bytes { + let tx = TxLegacy { + nonce: 0, + gas_price: 100_000_000_000, + gas_limit, + to: TxKind::Create, + value: U256::ZERO, + input: init_code, + chain_id: None, + }; + let word = U256::from_be_bytes(hex!( + "3333333333333333333333333333333333333333333333333333333333333333" + )); + let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); + let mut buf = Vec::new(); + signed.rlp_encode(&mut buf); + Bytes::from(buf) +} + /// The post-transaction readings compared across specs. pub(crate) struct Outcome { pub(crate) result: ExecutionResult, @@ -137,6 +204,12 @@ impl Outcome { self.result.gas().state_gas_spent_final() } + /// The non-compute part of what this transaction's receipt reports — the `MegaETH` storage + /// gas and intrinsic share a compute-gas figure does not cover. + pub(crate) fn storage_overhead(&self) -> u64 { + self.gas_used - self.compute_gas + } + /// Reads a storage slot out of the produced state, defaulting to zero when the transaction /// never touched it. pub(crate) fn storage_value(&self, address: Address, slot: U256) -> U256 { diff --git a/crates/mega-evm/tests/rex7/conservation_terms.rs b/crates/mega-evm/tests/rex7/conservation_terms.rs index 66e1e99f..90a96742 100644 --- a/crates/mega-evm/tests/rex7/conservation_terms.rs +++ b/crates/mega-evm/tests/rex7/conservation_terms.rs @@ -26,13 +26,12 @@ //! `test_negative_derivation_is_clamped_to_zero`. use crate::common::{ - default_envs, transact_default, transact_tx, Outcome, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, - ONE_ETH, + default_envs, keyless_tx_bytes, transact_default, transact_tx, Outcome, CALLEE, CALLER, + CONTRACT, EMPTY_TARGET, ONE_ETH, }; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, U256}; use alloy_sol_types::SolCall as _; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, test_utils::{BytecodeBuilder, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, MegaSpecId, KEYLESS_DEPLOY_ADDRESS, }; @@ -40,7 +39,6 @@ use revm::{ bytecode::opcode::{ADD, CALL, MSTORE8, POP, STOP}, context::tx::TxEnvBuilder, }; -use std::vec::Vec; /// Empty accounts the value-transferring calls pay into. Each one is fresh, so every call really /// transfers value and really mints a stipend. @@ -268,26 +266,6 @@ fn test_stipend_is_minted_by_a_value_call_whose_child_frame_never_runs() { ); } -/// Builds a deterministic pre-EIP-155 keyless deployment transaction. -fn keyless_tx_bytes(init_code: Bytes, gas_limit: u64) -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit, - to: TxKind::Create, - value: U256::ZERO, - input: init_code, - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// Runs one `KeylessDeploy` whose sandbox executes `init_code` with `gas_limit`. /// /// [`CALLEE`] is preloaded with a bare `ADD` so a constructor can open a sub-frame that halts diff --git a/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs index b7d6c514..0589a7d5 100644 --- a/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs +++ b/crates/mega-evm/tests/rex7/deposit_receipt_rewrite.rs @@ -21,11 +21,10 @@ //! transactions never settle a derivation, because the law is stated over an outer transaction's //! final envelope. -use crate::common::{transact_mega_tx, transact_tx, Outcome, ONE_ETH}; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use crate::common::{keyless_tx_bytes, transact_mega_tx, transact_tx, Outcome, ONE_ETH}; +use alloy_primitives::{address, Address, Bytes, B256, U256}; use alloy_sol_types::{SolCall as _, SolError as _}; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, constants::rex::TX_INTRINSIC_STORAGE_GAS, test_utils::{BytecodeBuilder, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, @@ -356,27 +355,6 @@ fn test_exempt_deposit_reject_still_accounts_for_the_envelope() { /// validation rather than running. const SANDBOX_REJECT_INNER_GAS_LIMIT: u64 = INTRINSIC_REQUIREMENT; -/// Builds a deterministic pre-EIP-155 keyless deployment transaction whose gas limit cannot cover -/// its own `MegaETH` intrinsic requirement. -fn underfunded_keyless_tx_bytes() -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: SANDBOX_REJECT_INNER_GAS_LIMIT, - to: TxKind::Create, - value: U256::ZERO, - input: BytecodeBuilder::default().append(INVALID).build(), - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// A keyless-deploy sandbox transaction that fails validation is rewritten into a failed deposit /// too — inside the sandbox, where no settlement of its own belongs. Its usage is discarded and /// the interceptor hands the whole reservation back, so the outer transaction sees only the @@ -386,7 +364,10 @@ fn underfunded_keyless_tx_bytes() -> Bytes { fn test_keyless_sandbox_reject_leaves_the_outer_transaction_alone() { const OUTER_GAS_LIMIT: u64 = 1_000_000; let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: underfunded_keyless_tx_bytes(), + keylessDeploymentTransaction: keyless_tx_bytes( + BytecodeBuilder::default().append(INVALID).build(), + SANDBOX_REJECT_INNER_GAS_LIMIT, + ), gasLimitOverride: U256::from(SANDBOX_REJECT_INNER_GAS_LIMIT), } .abi_encode(); diff --git a/crates/mega-evm/tests/rex7/gas_clamp.rs b/crates/mega-evm/tests/rex7/gas_clamp.rs index 3b795438..422271e8 100644 --- a/crates/mega-evm/tests/rex7/gas_clamp.rs +++ b/crates/mega-evm/tests/rex7/gas_clamp.rs @@ -21,14 +21,13 @@ //! `VolatileDataAccessOutOfGas`. use crate::common::{ - base_db, plain_filler, transact, transact_default, transact_with_gas_limit, Outcome, CALLEE, - CONTRACT, + base_db, compute_limit, countdown_loop_code, detention_cap, plain_filler, transact, + transact_default, transact_with_gas_limit, Outcome, CALLEE, CONTRACT, }; use alloy_primitives::{Address, Bytes, U256}; use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaHaltReason, MegaSpecId}; use revm::bytecode::opcode::{ - CALL, DUP1, EXTCODECOPY, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, - TIMESTAMP, + CALL, EXTCODECOPY, GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP, }; /// Slot the outer contract stores the CALL success flag into. @@ -36,44 +35,6 @@ const CALL_RESULT_SLOT: u64 = 0x10; /// Slot a callee writes to, so a reverted sub-frame can be told from a committed one. const CALLEE_SLOT: u64 = 0x11; -/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body: -/// -/// ```text -/// [prefix] PUSH2 iterations; loop: JUMPDEST; PUSH1 1; SWAP1; SUB; DUP1; PUSH1 loop; JUMPI; STOP -/// ``` -/// -/// Each iteration runs seven plain opcodes for 26 gas. `prefix` is prepended verbatim and -/// participates in the jump-target offset. -fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { - let mut code = prefix.to_vec(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); - code.push(JUMPDEST); - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(SWAP1); - code.push(SUB); - code.push(DUP1); - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(JUMPI); - code.push(STOP); - Bytes::from(code) -} - -/// Per-spec runtime limits with the TX compute gas limit replaced. -fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) -} - -/// Per-spec runtime limits with the block-environment detention cap replaced. -fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| { - let mut limits = EvmTxRuntimeLimits::from_spec(spec); - limits.block_env_access_compute_gas_limit = cap; - limits - } -} - /// Per-spec runtime limits with both the TX compute gas limit and the block-environment /// detention cap replaced. fn compute_and_detention(compute: u64, cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { diff --git a/crates/mega-evm/tests/rex7/gas_leakage.rs b/crates/mega-evm/tests/rex7/gas_leakage.rs index a68c3eb7..31ff733a 100644 --- a/crates/mega-evm/tests/rex7/gas_leakage.rs +++ b/crates/mega-evm/tests/rex7/gas_leakage.rs @@ -21,8 +21,8 @@ //! all. use crate::common::{ - assert_outcomes_identical, base_db as common_base_db, plain_filler, transact, - transact_with_gas_limit, Outcome, CALLEE, CONTRACT, + assert_outcomes_identical, base_db as common_base_db, compute_limit, countdown_loop_code, + detention_cap, plain_filler, transact, transact_with_gas_limit, Outcome, CALLEE, CONTRACT, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::SolCall as _; @@ -31,10 +31,7 @@ use mega_evm::{ EvmTxRuntimeLimits, IMegaLimitControl, MegaHaltReason, MegaSpecId, LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE, }; -use revm::bytecode::opcode::{ - CALL, CREATE, DUP1, GAS, JUMPDEST, JUMPI, MSTORE, POP, RETURN, SSTORE, STOP, SUB, SWAP1, - TIMESTAMP, -}; +use revm::bytecode::opcode::{CALL, CREATE, GAS, MSTORE, POP, RETURN, SSTORE, STOP, TIMESTAMP}; /// Slot the caller stores its post-return `GAS` reading into. const GAS_READING_SLOT: u64 = 0x40; @@ -45,37 +42,6 @@ fn base_db(code: Bytes) -> MemoryDatabase { common_base_db(code).account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) } -/// A countdown loop of cheap opcodes with no checkpoint anywhere inside the loop body. -fn countdown_loop_code(prefix: &[u8], iterations: u16) -> Bytes { - let mut code = prefix.to_vec(); - code.push(0x61); // PUSH2 - code.extend_from_slice(&iterations.to_be_bytes()); - let loop_target = u8::try_from(code.len()).expect("loop target must fit in a PUSH1"); - code.push(JUMPDEST); - code.extend_from_slice(&[0x60, 0x01]); // PUSH1 1 - code.push(SWAP1); - code.push(SUB); - code.push(DUP1); - code.extend_from_slice(&[0x60, loop_target]); // PUSH1 loop - code.push(JUMPI); - code.push(STOP); - Bytes::from(code) -} - -/// Per-spec runtime limits with the TX compute gas limit replaced. -fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) -} - -/// Per-spec runtime limits with the block-environment detention cap replaced. -fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| { - let mut limits = EvmTxRuntimeLimits::from_spec(spec); - limits.block_env_access_compute_gas_limit = cap; - limits - } -} - /// A CALL to `target` forwarding `gas`, with no arguments and no return data, followed by the /// caller reading `GAS` and storing it. /// diff --git a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs index 13a5e658..1dc04727 100644 --- a/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs +++ b/crates/mega-evm/tests/rex7/guard_pass_static_gas.rs @@ -14,7 +14,8 @@ //! remaining, which is what the interceptor reads once the frames have been popped. use crate::common::{ - base_db, context, drive, transact, Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, + base_db, context, drive, rex7_compute_limit, stop_only, transact, Outcome, CALLEE, CALLER, + CONTRACT, DEFAULT_TX_GAS_LIMIT, }; use alloy_primitives::{Bytes, U256}; use alloy_sol_types::{SolCall as _, SolError}; @@ -38,10 +39,6 @@ const SELFBALANCE_STATIC_GAS: u64 = 5; /// Slot the nested caller stores its `remainingComputeGas` reading into. const REMAINING_SLOT: u64 = 0xc0; -fn compute_limit(limit: u64) -> EvmTxRuntimeLimits { - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7).with_tx_compute_gas_limit(limit) -} - /// Codex / knife-edge program: `TIMESTAMP; POP; STOP`. fn timestamp_pop_stop() -> Bytes { BytecodeBuilder::default().append(TIMESTAMP).append(POP).stop().build() @@ -62,10 +59,6 @@ fn selfbalance_stop() -> Bytes { BytecodeBuilder::default().append(SELFBALANCE).stop().build() } -fn stop_only() -> Bytes { - BytecodeBuilder::default().append(STOP).build() -} - struct GuardPassRun { outcome: Outcome, accessed: VolatileDataAccess, @@ -101,10 +94,6 @@ fn intrinsic_stop() -> Outcome { ) } -fn storage_overhead(intrinsic: &Outcome) -> u64 { - intrinsic.gas_used - intrinsic.compute_gas -} - fn assert_timestamp_marked(label: &str, run: &GuardPassRun) { assert!( run.accessed.contains(VolatileDataAccess::TIMESTAMP), @@ -190,7 +179,7 @@ fn test_top_frame_timestamp_one_below_static_gas_reverts_after_the_body() { let limit = intrinsic.compute_gas + TIMESTAMP_STATIC_GAS - 1; assert_eq!(limit, 21_001); - let run = run(timestamp_pop_stop(), compute_limit(limit)); + let run = run(timestamp_pop_stop(), rex7_compute_limit(limit)); let decoded = decode_top_revert("headroom=static-1", &run.outcome); assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); @@ -210,7 +199,7 @@ fn test_top_frame_timestamp_one_below_static_gas_reverts_after_the_body() { assert_eq!(run.outcome.compute_gas, 21_002); assert_eq!( run.outcome.gas_used, - run.outcome.compute_gas + storage_overhead(&intrinsic), + run.outcome.compute_gas + intrinsic.storage_overhead(), "receipt gas is compute plus the intrinsic storage component" ); assert_eq!(run.outcome.gas_used, 60_002); @@ -231,7 +220,7 @@ fn test_top_frame_timestamp_at_static_gas_continues() { let limit = intrinsic.compute_gas + TIMESTAMP_STATIC_GAS; assert_eq!(limit, 21_002); - let run = run(timestamp_stop(), compute_limit(limit)); + let run = run(timestamp_stop(), rex7_compute_limit(limit)); assert!( run.outcome.is_success(), @@ -239,7 +228,7 @@ fn test_top_frame_timestamp_at_static_gas_continues() { run.outcome.result ); assert_eq!(run.outcome.compute_gas, intrinsic.compute_gas + TIMESTAMP_STATIC_GAS); - assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + storage_overhead(&intrinsic),); + assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + intrinsic.storage_overhead(),); assert_timestamp_marked("headroom=static_gas", &run); assert_eq!(run.remaining_compute_gas, 0); } @@ -257,7 +246,7 @@ fn test_nested_timestamp_one_below_static_gas_reverts_after_the_body() { let before = empty.outcome.compute_gas; let limit = before + TIMESTAMP_STATIC_GAS - 1; - let run = run_db(nested_db(timestamp_stop()), compute_limit(limit)); + let run = run_db(nested_db(timestamp_stop()), rex7_compute_limit(limit)); assert_timestamp_marked("nested headroom=static-1", &run); assert_eq!( @@ -298,7 +287,7 @@ fn test_nested_timestamp_at_static_gas_continues() { let before = empty.outcome.compute_gas; let limit = before + TIMESTAMP_STATIC_GAS; - let run = run_db(nested_db(timestamp_stop()), compute_limit(limit)); + let run = run_db(nested_db(timestamp_stop()), rex7_compute_limit(limit)); assert_timestamp_marked("nested headroom=static_gas", &run); assert_eq!(run.outcome.compute_gas, before + TIMESTAMP_STATIC_GAS); @@ -363,7 +352,7 @@ fn test_top_frame_selfbalance_one_below_static_gas_reverts_after_the_body() { let limit = intrinsic.compute_gas + SELFBALANCE_STATIC_GAS - 1; assert_eq!(limit, 21_004); - let run = run(selfbalance_pop_stop(), compute_limit(limit)); + let run = run(selfbalance_pop_stop(), rex7_compute_limit(limit)); let decoded = decode_top_revert("SELFBALANCE headroom=static-1", &run.outcome); assert_eq!(decoded.kind, LimitKind::ComputeGas.as_u8()); @@ -383,7 +372,7 @@ fn test_top_frame_selfbalance_one_below_static_gas_reverts_after_the_body() { assert_eq!(run.outcome.compute_gas, 21_005); assert_eq!( run.outcome.gas_used, - run.outcome.compute_gas + storage_overhead(&intrinsic), + run.outcome.compute_gas + intrinsic.storage_overhead(), "receipt gas is compute plus the intrinsic storage component" ); assert_eq!(run.outcome.gas_used, 60_005); @@ -403,7 +392,7 @@ fn test_top_frame_selfbalance_at_static_gas_continues() { let limit = intrinsic.compute_gas + SELFBALANCE_STATIC_GAS; assert_eq!(limit, 21_005); - let run = run(selfbalance_stop(), compute_limit(limit)); + let run = run(selfbalance_stop(), rex7_compute_limit(limit)); assert!( run.outcome.is_success(), @@ -411,6 +400,6 @@ fn test_top_frame_selfbalance_at_static_gas_continues() { run.outcome.result ); assert_eq!(run.outcome.compute_gas, intrinsic.compute_gas + SELFBALANCE_STATIC_GAS); - assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + storage_overhead(&intrinsic),); + assert_eq!(run.outcome.gas_used, run.outcome.compute_gas + intrinsic.storage_overhead(),); assert_eq!(run.remaining_compute_gas, 0); } diff --git a/crates/mega-evm/tests/rex7/interceptor_resume.rs b/crates/mega-evm/tests/rex7/interceptor_resume.rs index 627e4b85..1fb385ef 100644 --- a/crates/mega-evm/tests/rex7/interceptor_resume.rs +++ b/crates/mega-evm/tests/rex7/interceptor_resume.rs @@ -23,8 +23,8 @@ //! is still stopped at the clamp boundary rather than overshooting to the next checkpoint. use crate::common::{ - assert_outcomes_identical, base_db as common_base_db, plain_filler, transact, transact_default, - Outcome, CALLEE, CONTRACT, + assert_outcomes_identical, base_db as common_base_db, compute_limit, detention_cap, + plain_filler, transact, transact_default, Outcome, CALLEE, CONTRACT, }; use alloy_primitives::{address, Address, Bytes, U256}; use alloy_sol_types::SolCall as _; @@ -53,20 +53,6 @@ fn base_db(code: Bytes) -> MemoryDatabase { common_base_db(code).account_code(LIMIT_CONTROL_ADDRESS, LIMIT_CONTROL_CODE) } -/// Per-spec runtime limits with the TX compute gas limit replaced. -fn compute_limit(limit: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| EvmTxRuntimeLimits::from_spec(spec).with_tx_compute_gas_limit(limit) -} - -/// Per-spec runtime limits with the block-environment detention cap replaced. -fn detention_cap(cap: u64) -> impl Fn(MegaSpecId) -> EvmTxRuntimeLimits { - move |spec| { - let mut limits = EvmTxRuntimeLimits::from_spec(spec); - limits.block_env_access_compute_gas_limit = cap; - limits - } -} - /// A CALL to `target` forwarding `forwarded_gas`, with `args_size` bytes of calldata taken from /// `mem[0..]` and 32 bytes of return data written to `mem[RET_OFFSET..]`. fn call_with_return_data( diff --git a/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs b/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs index 5b22e208..d813550e 100644 --- a/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs +++ b/crates/mega-evm/tests/rex7/keyless_synthetic_halt.rs @@ -20,11 +20,10 @@ //! envelope is measured through the `GasLimitTooLow` revert rather than read back out of the lane //! under test. -use crate::common::{transact_tx, Outcome, ONE_ETH}; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use crate::common::{keyless_tx_bytes, transact_tx, Outcome, ONE_ETH}; +use alloy_primitives::{address, Address, Bytes, U256}; use alloy_sol_types::{SolCall as _, SolError as _}; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, constants::{rex::NEW_ACCOUNT_STORAGE_GAS_BASE, rex2::KEYLESS_DEPLOY_OVERHEAD_GAS}, test_utils::{BytecodeBuilder, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, MegaSpecId, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, @@ -34,7 +33,6 @@ use revm::{ bytecode::opcode::STOP, context::{result::ExecutionResult, tx::TxEnvBuilder}, }; -use std::vec::Vec; /// Relayer that sends the keyless-deploy transactions. const RELAYER: Address = address!("0000000000000000000000000000000000340009"); @@ -54,27 +52,6 @@ const MATERIALIZATION_GAS: u64 = NEW_ACCOUNT_STORAGE_GAS_BASE; /// the pre-cap check clears; the post-cap re-check at step 4b is what the calibration below reads. const INNER_TX_GAS_LIMIT: u64 = 200_000; -/// Builds a deterministic pre-EIP-155 keyless deployment transaction. Its init code never runs on -/// any path exercised here — every probe fails before the sandbox is built. -fn keyless_tx_bytes() -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: INNER_TX_GAS_LIMIT, - to: TxKind::Create, - value: U256::ZERO, - input: BytecodeBuilder::default().append(STOP).build(), - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// Runs a top-level keyless-deploy call. Depth zero is the only depth the interceptor fires at, so /// every probe here has to be a direct transaction. fn run( @@ -84,7 +61,10 @@ fn run( tx_compute_gas_limit: Option, ) -> Outcome { let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_tx_bytes(), + keylessDeploymentTransaction: keyless_tx_bytes( + BytecodeBuilder::default().append(STOP).build(), + INNER_TX_GAS_LIMIT, + ), gasLimitOverride: U256::from(1_000_000u64), } .abi_encode(); diff --git a/crates/mega-evm/tests/rex7/parity_shapes.rs b/crates/mega-evm/tests/rex7/parity_shapes.rs index 373032ba..8d7f406e 100644 --- a/crates/mega-evm/tests/rex7/parity_shapes.rs +++ b/crates/mega-evm/tests/rex7/parity_shapes.rs @@ -23,14 +23,13 @@ use std::vec::Vec; use crate::common::{ - assert_outcomes_identical, base_db, default_envs, plain_filler, transact_tx, Outcome, CALLEE, - CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, + assert_outcomes_identical, base_db, default_envs, keyless_tx_bytes, plain_filler, transact_tx, + Outcome, CALLEE, CALLER, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, ONE_ETH, }; use alloy_eips::eip7702::{Authorization, RecoveredAuthority, RecoveredAuthorization}; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, B256, U256}; use alloy_sol_types::SolCall as _; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, test_utils::{BytecodeBuilder, MemoryDatabase}, EvmTxRuntimeLimits, IKeylessDeploy, IOracle, MegaSpecId, TestExternalEnvs, KEYLESS_DEPLOY_ADDRESS, ORACLE_CONTRACT_ADDRESS, ORACLE_CONTRACT_CODE_REX2, @@ -163,26 +162,6 @@ fn test_eip7702_authorization_under_detention_matches_per_opcode() { assert!(r7.is_success(), "the authorized transaction must succeed: {:?}", r7.result); } -/// Builds a deterministic pre-EIP-155 keyless deployment transaction. -fn keyless_tx_bytes(init_code: Bytes) -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: 200_000, - to: TxKind::Create, - value: U256::ZERO, - input: init_code, - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// `KeylessDeploy` is intercepted at depth 0, so the interception happens before any frame — and /// therefore before any checkpoint — has been created. Its sandbox then runs a whole nested /// transaction under the same spec, with its own tracker, and merges the usage back. @@ -198,7 +177,7 @@ fn test_keyless_deploy_sandbox_accounting_matches_per_opcode() { .return_with_data(&runtime) .build(); let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_tx_bytes(init_code), + keylessDeploymentTransaction: keyless_tx_bytes(init_code, 200_000), gasLimitOverride: U256::from(1_000_000u64), } .abi_encode(); @@ -240,7 +219,7 @@ fn test_keyless_deploy_sandbox_under_detention_matches_per_opcode() { .return_with_data(&runtime) .build(); let call_data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_tx_bytes(init_code), + keylessDeploymentTransaction: keyless_tx_bytes(init_code, 200_000), gasLimitOverride: U256::from(1_000_000u64), } .abi_encode(); diff --git a/crates/mega-evm/tests/rex7/shim_refusals.rs b/crates/mega-evm/tests/rex7/shim_refusals.rs index 8ecef937..e5f9694c 100644 --- a/crates/mega-evm/tests/rex7/shim_refusals.rs +++ b/crates/mega-evm/tests/rex7/shim_refusals.rs @@ -23,16 +23,18 @@ //! with the same message. use crate::{ - common::{base_db, context, try_drive, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, ONE_ETH}, + common::{ + base_db, context, keyless_tx_bytes, try_drive, CALLEE, CALLER, CONTRACT, EMPTY_TARGET, + ONE_ETH, + }, inspector_common::{ append_call, assert_refused, deploy_then_stop, limits, try_transact_inspected, REVERTING_INIT_CODE, REVIVED_CREATION, }, }; -use alloy_primitives::{address, hex, Address, Bytes, Signature, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, B256, U256}; use alloy_sol_types::SolCall as _; use mega_evm::{ - alloy_consensus::{Signed, TxLegacy}, test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, EvmTxRuntimeLimits, IKeylessDeploy, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, MegaTransactionNew as _, KEYLESS_DEPLOY_ADDRESS, @@ -48,7 +50,7 @@ use revm::{ state::EvmState, Inspector, }; -use std::{string::String, vec::Vec}; +use std::string::String; // === a failed creation, revived =============================================================== @@ -314,35 +316,6 @@ fn test_reviving_a_failed_precompile_call_is_refused() { assert_reading_refused(&reading); } -/// A deterministic pre-EIP-155 keyless deployment transaction whose init code returns one byte of -/// runtime code, so the deployment it makes is visible in the produced state. -fn keyless_tx_bytes() -> Bytes { - let tx = TxLegacy { - nonce: 0, - gas_price: 100_000_000_000, - gas_limit: 200_000, - to: TxKind::Create, - value: U256::ZERO, - // MSTORE8 a STOP at offset 0, then return that one byte as the runtime code. - input: BytecodeBuilder::default() - .push_number(u128::from(STOP)) - .push_number(0u64) - .append(0x53) // MSTORE8 - .push_number(1u64) - .push_number(0u64) - .append(RETURN) - .build(), - chain_id: None, - }; - let word = U256::from_be_bytes(hex!( - "3333333333333333333333333333333333333333333333333333333333333333" - )); - let signed = Signed::new_unchecked(tx, Signature::new(word, word, false), B256::ZERO); - let mut buf = Vec::new(); - signed.rlp_encode(&mut buf); - Bytes::from(buf) -} - /// The address the keyless transaction above deploys to, recovered from the receipt of an /// unrewritten run. fn deployed_address(reading: &Reading) -> Option
{ @@ -361,7 +334,18 @@ fn deployed_address(reading: &Reading) -> Option
{ fn test_rewriting_the_keyless_deploy_synthetic_result_is_refused() { let deploy_tx = || { let data = IKeylessDeploy::keylessDeployCall { - keylessDeploymentTransaction: keyless_tx_bytes(), + keylessDeploymentTransaction: keyless_tx_bytes( + // MSTORE8 a STOP at offset 0, then return that one byte as the runtime code. + BytecodeBuilder::default() + .push_number(u128::from(STOP)) + .push_number(0u64) + .append(0x53) // MSTORE8 + .push_number(1u64) + .push_number(0u64) + .append(RETURN) + .build(), + 200_000, + ), gasLimitOverride: U256::from(1_000_000u64), } .abi_encode(); From 48907a02f2922b823c370dff21228585f1bf960f Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:34:26 +0800 Subject: [PATCH 190/208] test(rex7): split the shim suite back into one file per mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `shim_measurement.rs` had grown to 3,173 lines across five banner-separated sections, which is more than a reviewer can hold at once. Split it by mechanism, with no test body changed: - `shim_lanes.rs` — what each lane books: gas written into an interpreter's counter or a frame's gas limit, and the receipt's two other numbers. - `shim_settlement.rs` — where a rewrite is settled when the shim's reading and the envelope's number are not the same object: the two settlement windows, and interception. - `shim_blind_spots.rs` — the rewrite shapes an all-zero ledger used to admit. The two magnitudes that two of the three modules edit by, `ACTION_DELTA` and `REFUND`, move to `inspector_common.rs` rather than being copied. --- .../mega-evm/tests/rex7/inspector_common.rs | 19 +- crates/mega-evm/tests/rex7/main.rs | 19 +- .../mega-evm/tests/rex7/shim_blind_spots.rs | 922 +++++ crates/mega-evm/tests/rex7/shim_lanes.rs | 1213 +++++++ .../mega-evm/tests/rex7/shim_measurement.rs | 3173 ----------------- crates/mega-evm/tests/rex7/shim_settlement.rs | 1073 ++++++ 6 files changed, 3237 insertions(+), 3182 deletions(-) create mode 100644 crates/mega-evm/tests/rex7/shim_blind_spots.rs create mode 100644 crates/mega-evm/tests/rex7/shim_lanes.rs delete mode 100644 crates/mega-evm/tests/rex7/shim_measurement.rs create mode 100644 crates/mega-evm/tests/rex7/shim_settlement.rs diff --git a/crates/mega-evm/tests/rex7/inspector_common.rs b/crates/mega-evm/tests/rex7/inspector_common.rs index 362623a1..23e55b27 100644 --- a/crates/mega-evm/tests/rex7/inspector_common.rs +++ b/crates/mega-evm/tests/rex7/inspector_common.rs @@ -2,9 +2,9 @@ //! //! [`crate::common`] drives the transaction and checks the conservation law on every run. What is //! left over — the REX7 limits, the bytecode shapes an inspector needs something to reach into, the -//! one-lane ledgers a test asserts against, and the two ways a refused rewrite surfaces — lives -//! here, because a rewrite is only ever pinned by comparing an inspected run against the -//! uninspected one over the same fixture. +//! magnitudes more than one module edits by, the one-lane ledgers a test asserts against, and the +//! two ways a refused rewrite surfaces — lives here, because a rewrite is only ever pinned by +//! comparing an inspected run against the uninspected one over the same fixture. use alloy_primitives::{Address, Bytes}; use mega_evm::{ @@ -132,6 +132,19 @@ where crate::common::drive(MegaSpecId::REX7, &mut evm, call_contract_tx(DEFAULT_TX_GAS_LIMIT)) } +// --- magnitudes ------------------------------------------------------------------------------ + +/// Gas an action edit moves, and gas a cancelling pair moves through the result lane's two +/// windows. +pub(crate) const ACTION_DELTA: u64 = 700; + +/// Refund a refund edit moves, and the magnitude a cancelling pair moves in each direction. +/// +/// Small enough to stay well under the EIP-3529 cap on every fixture that uses it, so that what +/// survives to the receipt is the whole of the surviving half rather than whatever the cap left of +/// it. +pub(crate) const REFUND: i64 = 2_000; + // --- ledgers ------------------------------------------------------------------------------- /// The ledger of a rewrite that moved gas on exactly one lane. diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 6bb42eb5..4f65fc33 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -20,11 +20,16 @@ //! keep the envelope book the unperformed part as destroyed, the rescued one books nothing. //! - `latch_surfacing` — where a latched data-size / KV-update / state-growth exceed becomes a //! stop. -//! - `shim_measurement` — what the measurement shim books, over every shape that can move a number -//! it reports: gas written into an interpreter counter or a frame's gas limit, the two windows -//! where a rewrite lands after the accounting that should have read it, the shapes an all-zero -//! ledger used to admit, the receipt's refund and EIP-8037 dimensions, and the gas a synthetic -//! outcome carries. +//! - `shim_lanes` — what each lane of the measurement shim books: gas written into an interpreter +//! counter or a frame's gas limit, and the receipt's two other numbers, the EIP-3529 refund and +//! the EIP-8037 state-gas dimension. +//! - `shim_settlement` — where a rewrite is settled when the number the shim reads and the number +//! the envelope carries are not the same object: the two windows a rewrite can land in after the +//! accounting that should have read it, and the gas a synthetic outcome carries. +//! - `shim_blind_spots` — the rewrite shapes an all-zero ledger used to admit: a frame's memory +//! grown for free, an outcome's metadata rewritten around the result inside it, two edits to one +//! signed lane that cancel, an instruction deleted by stepping the program counter past it, and a +//! return buffer conjured in front of a frame that made no call. //! - `shim_refusals` — the rewrites the shim refuses outright: a failed creation revived (at both //! callbacks that can), and the classification of a result frame init produced; with the near //! boundary, a frame the inspector answered itself, which is supported. @@ -114,6 +119,8 @@ mod parity_shapes; mod pre_execution_intrinsic_reject; mod precompile_halt; mod result_space_tripwire; -mod shim_measurement; +mod shim_blind_spots; +mod shim_lanes; mod shim_refusals; +mod shim_settlement; mod trusted_observer; diff --git a/crates/mega-evm/tests/rex7/shim_blind_spots.rs b/crates/mega-evm/tests/rex7/shim_blind_spots.rs new file mode 100644 index 00000000..e9176c7a --- /dev/null +++ b/crates/mega-evm/tests/rex7/shim_blind_spots.rs @@ -0,0 +1,922 @@ +//! The rewrite shapes an all-zero ledger used to admit. +//! +//! The measurement shim's contract is that a transaction an inspector rewrote never reaches a +//! block: the canonical path refuses one whose `InspectorLedger` is non-zero, so every rewrite has +//! to leave a mark on it. `shim_lanes.rs`, `shim_settlement.rs` and `inspector_cheat_matrix.rs` +//! pin that per mechanism and per callback × shape pair. This module pins the shapes that slipped +//! *between* those two questions — each one a rewrite the shim was handed, that changes what the +//! transaction produces, and that every lane read as nothing: +//! +//! - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it +//! has been paid for in the same step, so that neither goes out of bounds and the next expanding +//! opcode charges nothing; +//! - a `CallOutcome` / `CreateOutcome` metadata field — where the callee's return data lands, and +//! which address a creation reports — rewritten without touching the `InterpreterResult` inside +//! it, which is the only part the rewrite comparison used to read; +//! - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels +//! to zero; +//! - the same cancellation spread across two frames, where only one of the two survives to the +//! receipt, so the net is zero and the effect is not; +//! - an instruction deleted from a frame, by stepping the program counter past it, so the work is +//! never performed and there is nothing for any counter to meter; +//! - a return buffer put in front of a frame that made no call, so `RETURNDATASIZE` reads a length +//! no call produced. +//! +//! Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use +//! to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test +//! here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no +//! longer changes anything is a shape that stopped testing the guard. +//! +//! The last two are also why the snapshot the first shape needed is now a *rule* rather than a +//! list. A snapshot of four chosen readings caught the memory pair and let the program counter +//! through, because `Interpreter::bytecode` was not among the things anyone had thought to name. +//! What the shim takes now is every constant-time reading of the interpreter, and what pins that is +//! the `Interpreter` row of `gas_surface.rs`'s closed table. + +use crate::{ + common::{base_db, transact, transact_inspected, CALLEE, CONTRACT, EMPTY_TARGET}, + inspector_common::{plain_and_cheated, ACTION_DELTA, REFUND}, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{test_utils::BytecodeBuilder, EvmTxRuntimeLimits, MegaSpecId}; +use revm::{ + bytecode::opcode::{ + CALL, CALLER, CREATE, GAS, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, SSTORE, + STOP, + }, + context::{Cfg, ContextTr}, + interpreter::{ + interpreter::EthInterpreter, + interpreter_types::{InputsTr, Jumps, LoopControl, MemoryTr, ReturnData}, + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Interpreter, InterpreterAction, + InterpreterTypes, + }, + Inspector, +}; + +/// A second callee, whose frame reverts. +const REVERTER: Address = EMPTY_TARGET; + +/// The address a rewritten `CreateOutcome` reports instead of the one the code was deployed at. +const FAKE_DEPLOYMENT: Address = address!("00000000000000000000000000000000000f00d0"); + +/// Slot the fixtures write their observable result to. +const RESULT_SLOT: u64 = 0x11; + +/// The mainnet memory expansion cost of a memory `words` words long. +const fn memory_cost(words: u64) -> u64 { + 3 * words + words * words / 512 +} + +// --- a frame's memory, grown for free ------------------------------------------------------------ + +/// How far the free-expansion inspector grows the frame's memory, in words. +/// +/// The fixture's own `MSTORE` lands inside it, so the expansion the EVM would have charged for is +/// exactly the one the inspector already did for nothing. +const STOLEN_WORDS: u64 = 129; + +/// Grows the frame's memory and tells the EVM it is already paid for. +/// +/// Both halves are needed and neither is a rewrite on its own. Moving the memory alone leaves the +/// memo behind, and the next expanding opcode charges for an expansion that already happened; +/// moving the memo alone leaves the memory behind, and the EVM reads out of bounds. Moving both +/// keeps every invariant the interpreter has and skips the charge, which is why the pair was the +/// hole and neither half was. +#[derive(Default)] +struct FreeExpansion { + fired: u32, +} + +impl Inspector for FreeExpansion { + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != MSTORE { + return; + } + let words = STOLEN_WORDS as usize; + assert!(interp.memory.resize(words * 32), "the fixture must allow the memory to be grown",); + // Priced through revm's own table, so the memo is exactly what the EVM would have written + // had the frame paid; the assertion below restates the formula independently, which is + // what makes the two a check rather than one number written twice. + let cost = context.cfg().gas_params().memory_cost(words); + interp.gas.memory_mut().set_words_num(words, cost); + self.fired += 1; + } +} + +/// ★ A frame whose memory was grown for free is not an all-zero ledger. +/// +/// The rewrite reaches through no argument the shim used to compare: the interpreter's gas counter +/// is untouched, no action is pending, no frame input and no frame result exists yet. What it +/// moves is the interpreter's memory and the memo beside it, and the transaction then pays less +/// than it would have — which is the one thing the guard exists to keep out of a block. +#[test] +fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { + // MSTORE(offset = STOLEN_WORDS * 32 - 32, value = 0xAA), which expands memory to exactly the + // size the inspector already grew it to. + let offset = (STOLEN_WORDS - 1) * 32; + let code = BytecodeBuilder::default() + .push_number(0xAAu64) + .push_number(offset) + .append(MSTORE) + .append(STOP) + .build(); + + let mut inspector = FreeExpansion::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + memory_cost(STOLEN_WORDS), + "the expansion the inspector performed is the charge the EVM then skipped", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction that paid less because an inspector moved its memory must not read as \ + untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- a call outcome's metadata ------------------------------------------------------------------- + +/// Where the fixture's `CALL` asks for its return data, and where the inspector moves it to. +const RETURN_AT: usize = 0; +const MOVED_TO: usize = 32; + +/// Moves a finished call's return data somewhere else in the caller's memory. +/// +/// The `InterpreterResult` inside the outcome — its classification, its output bytes, its gas — +/// comes back exactly as the EVM produced it. Only the range the caller will copy the output into +/// changes, which is not a field the result carries. +#[derive(Default)] +struct MoveReturnData { + fired: u32, +} + +impl Inspector for MoveReturnData { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.memory_offset = MOVED_TO..MOVED_TO + 32; + self.fired += 1; + } +} + +/// ★ A call outcome whose return range was moved is not an all-zero ledger. +#[test] +fn test_a_moved_return_range_is_booked() { + // Size the caller's memory to two words, call the callee for one word of output at offset 0, + // then store what landed there. + let code = BytecodeBuilder::default() + .push_number(0u64) + .push_number(32u64) + .append(MSTORE) + .push_number(32u64) // retSize + .push_number(u64::try_from(RETURN_AT).unwrap()) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(u64::try_from(RETURN_AT).unwrap()) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = MoveReturnData::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the return data lands where the caller asked for it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the caller reads a word the callee never wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a rewritten return range changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +// --- a frame's returned output ------------------------------------------------------------------ + +/// The word a rewritten output buffer feeds the caller instead of the one the callee returned. +const FORGED_OUTPUT: u64 = 0xdead; + +/// Replaces the output buffer a finished call hands back, leaving its classification alone. +/// +/// The classification and the remaining gas are what every other lane reads. The output is +/// neither, and it is what the caller copies into its own memory. +#[derive(Default)] +struct ForgeCallOutput { + fired: u32, +} + +impl Inspector for ForgeCallOutput { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address != CALLEE || self.fired > 0 { + return; + } + outcome.result.output = Bytes::from(U256::from(FORGED_OUTPUT).to_be_bytes::<32>().to_vec()); + self.fired += 1; + } +} + +/// ★ A call outcome whose returned output was replaced is not an all-zero ledger. +#[test] +fn test_a_forged_call_output_is_booked() { + // Call the callee for one word of output, then store what landed there. + let code = BytecodeBuilder::default() + .push_number(32u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .push_number(0u64) + .append(MLOAD) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + // The callee returns one word of 0x11s. + let callee = BytecodeBuilder::default() + .push_u256(U256::from(0x11u64)) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = ForgeCallOutput::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(0x11u64), + "without the rewrite the caller reads what the callee returned", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(FORGED_OUTPUT), + "with it, the caller reads a word no frame produced", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a replaced output buffer changed must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +/// Reports a different address than the one the creation deployed to. +#[derive(Default)] +struct MoveDeploymentAddress { + fired: u32, +} + +impl Inspector for MoveDeploymentAddress { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if self.fired > 0 || outcome.address.is_none() { + return; + } + outcome.address = Some(FAKE_DEPLOYMENT); + self.fired += 1; + } +} + +/// ★ A creation outcome whose reported address was rewritten is not an all-zero ledger. +/// +/// The code is still deployed where the EVM put it; only the address the caller's stack receives +/// changes, so the caller goes on to talk to an account that holds nothing. +#[test] +fn test_a_rewritten_deployment_address_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = MoveDeploymentAddress::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); + let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); + assert_ne!(deployed, U256::ZERO, "the fixture's CREATE must succeed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(FAKE_DEPLOYMENT.as_slice()), + "the caller must have been handed the address the inspector wrote", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction told a contract lives somewhere it does not must not read as untouched: \ + {:?}", + cheated.inspector_ledger, + ); +} + +// --- a construction frame's pending action ------------------------------------------------------- + +/// Drains the gas a construction frame's pending `Return` action carries. +/// +/// The contract this module's other cases rest on — that an action is the frame's result a moment +/// later, so an edit to it settles with that result — does not hold for a creation. Between the +/// two, `classify_frame_action` charges the code deposit out of the gas *this action* carries, and +/// a creation that cannot pay it becomes an `OutOfGas` that deploys nothing. So this edit changes +/// what the transaction produces, and it does it by a route that leaves the classification and the +/// output the boundary compares exactly where they were. +#[derive(Default)] +struct DrainConstructionAction { + fired: u32, +} + +impl Inspector for DrainConstructionAction { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + // A construction frame runs no deployed code, so it has no bytecode address. + if self.fired > 0 || interp.input.bytecode_address().is_some() { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + let remaining = result.gas.remaining(); + assert!( + result.gas.record_regular_cost(remaining), + "the fixture must be able to drain the action it found", + ); + self.fired += 1; + } +} + +/// ★ A construction frame whose pending action was drained is not an all-zero ledger. +/// +/// Every lane the boundary reads stays put: the action's classification and output are untouched, +/// so nothing is an intervention; the gas edit is staged for the frame's settlement point, and +/// that point declines to book it because the result it finally sees is a swallowed one. The +/// deposit the drained action could no longer pay is what turned it into one. +#[test] +fn test_a_drained_construction_action_is_booked() { + // Init code that returns two bytes of runtime code. + let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; + let mut builder = BytecodeBuilder::default(); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + let code = builder + .push_number(init.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = DrainConstructionAction::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); + assert_ne!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "without the edit the creation must succeed", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it the creation cannot pay its code deposit and deploys nothing", + ); + assert_ne!( + plain.gas_used, cheated.gas_used, + "and the receipt the sender is billed on moves with it", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose contract an inspector deleted must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Raises the gas an inner call frame's pending `Return` action carries, then takes the same +/// amount back out of the result that action became. +/// +/// The two windows are one lane and one frame, and the pair nets to zero. They are still two +/// edits, made in two different callbacks, and the lane's traffic is what says so — the sum alone +/// reads as an inspector that did nothing. +#[derive(Default)] +struct CancellingActionAndResultEdits { + raised: u32, + lowered: u32, +} + +impl Inspector for CancellingActionAndResultEdits { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.raised > 0 || interp.input.bytecode_address() != Some(&CALLEE) { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { + return; + }; + if !result.result.is_ok() { + return; + } + result.gas.erase_cost(ACTION_DELTA); + self.raised += 1; + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.lowered > 0 || inputs.target_address != CALLEE { + return; + } + assert!( + outcome.result.gas.record_regular_cost(ACTION_DELTA), + "the fixture must leave the result enough gas for the removal to land", + ); + self.lowered += 1; + } +} + +/// ★ An edit staged at one callback and undone at the next is two edits, not none. +/// +/// Nothing about this transaction changes: a call frame's remaining gas is read by nobody between +/// the two windows, so the pair really is invisible in what the transaction produces. That is the +/// point — the lane's traffic is the only thing that separates it from an inspector that never +/// ran, and on a *creation* frame the same pair is the shape that deletes a contract. +#[test] +fn test_cancelling_action_and_result_edits_are_booked() { + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let callee = BytecodeBuilder::default().append(STOP).build(); + let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); + + let mut inspector = CancellingActionAndResultEdits::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); + assert_eq!( + cheated.gas_used, plain.gas_used, + "the pair cancels, so the receipt really is the one the EVM would have produced", + ); + assert_eq!( + cheated.inspector_ledger.conjured_gas(), + 0, + "and the conservation law must read the net, which is zero", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the guard must still see that the lane carried two edits: {:?}", + cheated.inspector_ledger, + ); + assert_eq!( + cheated.inspector_ledger.result.gross(), + 2 * u128::from(ACTION_DELTA), + "one edit in each window, counted where each was made", + ); +} + +// --- two edits to one lane, in opposite directions ----------------------------------------------- + +/// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. +/// +/// Both edits land on the interpreter counter, which is one signed lane. Their net is zero and +/// the transaction's envelope is unmoved — and in between them the frame read a number one higher +/// than the EVM would have given it, and wrote that number to storage. +#[derive(Default)] +struct CancellingCounterEdits { + /// 0 before the injection, 1 between the two edits, 2 once both have landed. + phase: u8, +} + +impl Inspector for CancellingCounterEdits { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + match self.phase { + 0 if interp.bytecode.opcode() == GAS => { + interp.gas.erase_cost(1); + self.phase = 1; + } + 1 => { + assert!(interp.gas.record_regular_cost(1), "the frame must afford the give-back"); + self.phase = 2; + } + _ => {} + } + } +} + +/// ★ Two edits to the same lane that cancel are not an all-zero ledger. +/// +/// The net of the gas lane really is zero — the transaction spent exactly what it would have — so +/// nothing the conservation law reads has moved. What moved is the number the frame read in +/// between, and a guard that asks the net cannot see it. The gross activity counter is what does. +#[test] +fn test_cancelling_counter_edits_are_booked() { + let code = BytecodeBuilder::default() + .append(GAS) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of + // its remaining gas is the counter the injection moved. + let limits = EvmTxRuntimeLimits::no_limits(); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); + let mut inspector = CancellingCounterEdits::default(); + let cheated = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); + + assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)) + U256::from(1), + "the frame must have read one gas more than the EVM would have given it", + ); + assert_eq!( + cheated.total_gas_spent, plain.total_gas_spent, + "the two edits cancel, so the envelope the receipt reports is unmoved", + ); + assert_eq!( + cheated.inspector_conjured_gas(), + 0, + "and so is the law's term: this is exactly the shape a net-only reading cannot see", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "but the transaction was rewritten, and the guard has to see that: {:?}", + cheated.inspector_ledger, + ); +} + +/// Adds a refund to one child frame's result and takes the same amount out of another's. +/// +/// The frame that gets the addition returns, so its refund reaches the receipt. The frame that +/// gets the subtraction reverts, so revm discards its whole refund counter — the subtraction never +/// reaches anything. Net zero on the lane, one refund's worth of difference on the receipt. +#[derive(Default)] +struct CancellingRefundsAcrossFrames { + added: u32, + removed: u32, +} + +impl Inspector for CancellingRefundsAcrossFrames { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if inputs.target_address == CALLEE && self.added == 0 { + outcome.result.gas.record_refund(REFUND); + self.added += 1; + } else if inputs.target_address == REVERTER && self.removed == 0 { + assert!( + outcome.result.gas.refunded() >= REFUND, + "the reverting callee must hold a refund of its own to take from, got {}", + outcome.result.gas.refunded(), + ); + outcome.result.gas.record_refund(-REFUND); + self.removed += 1; + } + } +} + +/// ★ A cancellation split across a surviving frame and a discarded one is not an all-zero ledger. +/// +/// This is the previous shape with the asymmetry made explicit: the two halves are equal and +/// opposite where the ledger books them, and only one of them is still standing by the time the +/// receipt is built. +#[test] +fn test_cancelling_refunds_across_frames_are_booked() { + let call_to = |builder: BytecodeBuilder, target: Address| { + builder + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(target) + .push_number(200_000u64) + .append(CALL) + .append(POP) + }; + let code = call_to(call_to(BytecodeBuilder::default(), CALLEE), REVERTER).append(STOP).build(); + // Both callees set a slot and clear it again, so each ends holding a refund the EVM produced. + let clearing = |builder: BytecodeBuilder| { + builder + .sstore(U256::from(RESULT_SLOT), U256::from(1u64)) + .sstore(U256::from(RESULT_SLOT), U256::ZERO) + }; + let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); + let reverting = clearing(BytecodeBuilder::default()).revert().build(); + let db = || { + base_db(code.clone()) + .account_code(CALLEE, returning.clone()) + .account_code(REVERTER, reverting.clone()) + }; + + let mut inspector = CancellingRefundsAcrossFrames::default(); + let (plain, cheated) = plain_and_cheated(db, &mut inspector); + + assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); + assert!( + plain.total_gas_spent >= 5 * u64::try_from(REFUND).unwrap(), + "the fixture must burn enough that the EIP-3529 cap does not hide the difference", + ); + assert_eq!( + plain.gas_used - cheated.gas_used, + u64::try_from(REFUND).unwrap(), + "only the surviving frame's half reaches the receipt, so the sender pays that much less", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a receipt an inspector moved must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- an opcode skipped, and a return buffer conjured +// ---------------------------------------------- + +/// What the fixture's `SSTORE` writes when it runs. +const STORED: u64 = 0x99; + +/// The gas a cold `SSTORE` into a zero slot costs, which is what skipping it saves. +const COLD_SSTORE_SET: u64 = 22_100; + +/// How many bytes of return data the forging inspector conjures. +/// +/// Non-zero and a whole number of words, so that the `SSTORE` that stores it turns a zero slot +/// into a non-zero one — which is a different charge as well as a different value. +const CONJURED_RETURN_DATA: u64 = 96; + +/// Advances the program counter past the frame's `SSTORE`, so the EVM never executes it. +/// +/// revm's inspected loop runs this callback *before* the instruction, and the interpreter reads +/// the opcode it is about to execute from the very pointer this moves. Stepping the pointer on by +/// one byte therefore deletes one instruction from the frame: the two operands the `SSTORE` would +/// have consumed stay on the stack, the `STOP` after it runs instead, and the frame ends where it +/// was going to end. +/// +/// Nothing about this reaches a gas counter. The work is not performed, so there is nothing for +/// the EVM to meter and nothing for a gas lane to see. +#[derive(Default)] +struct SkipTheStore { + fired: u32, +} + +impl Inspector for SkipTheStore { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != SSTORE { + return; + } + interp.bytecode.relative_jump(1); + self.fired += 1; + } +} + +/// ★ A frame with an opcode skipped out from under it is not an all-zero ledger. +/// +/// The rewrite is the free-expansion shape's twin and is strictly worse: it does not merely make +/// the frame's next charge cheaper, it deletes an instruction from the frame. The transaction ends +/// with different storage *and* a smaller bill, and every gas lane reads zero because the gas that +/// went missing was never spent by anybody. +#[test] +fn test_a_skipped_opcode_is_booked() { + let code = BytecodeBuilder::default() + .sstore(U256::from(RESULT_SLOT), U256::from(STORED)) + .append(STOP) + .build(); + + let mut inspector = SkipTheStore::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(STORED), + "without the rewrite the frame stores what its bytecode says", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "with it, the store never happens", + ); + assert_eq!( + plain.total_gas_spent - cheated.total_gas_spent, + COLD_SSTORE_SET, + "the deleted instruction is the charge the transaction then did not pay", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction an inspector deleted an instruction from must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +/// Puts return data in front of a frame that has made no call. +/// +/// `RETURNDATASIZE` reads the buffer's length, so the frame goes on to store a number no call +/// produced. The buffer is the interpreter's own, reachable through `ReturnData` on any live +/// interpreter, and its length is a constant-time reading exactly like the memory's size. +#[derive(Default)] +struct ForgeReturnData { + fired: u32, +} + +impl Inspector for ForgeReturnData { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || interp.bytecode.opcode() != RETURNDATASIZE { + return; + } + interp.return_data.set_buffer(Bytes::from(vec![0u8; CONJURED_RETURN_DATA as usize])); + self.fired += 1; + } +} + +/// ★ A frame handed return data it never received is not an all-zero ledger. +/// +/// The frame made no call, so the EVM's own buffer is empty and the store is a zero-to-zero +/// no-op. With the rewrite the same store turns a zero slot into a non-zero one, which changes the +/// post-state and costs the transaction more — in the opposite direction to every other shape +/// here, and just as invisible to a lane that only watches gas counters. +#[test] +fn test_a_forged_return_buffer_is_booked() { + let code = BytecodeBuilder::default() + .append(RETURNDATASIZE) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = ForgeReturnData::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::ZERO, + "a frame that made no call has no return data", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from(CONJURED_RETURN_DATA), + "with the rewrite it reads the length of a buffer no call produced", + ); + assert!( + cheated.total_gas_spent > plain.total_gas_spent, + "and pays for the non-zero store the rewrite turned it into: {} vs {}", + cheated.total_gas_spent, + plain.total_gas_spent, + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "a transaction whose state a forged buffer changed must not read as untouched: {:?}", + cheated.inspector_ledger, + ); +} + +// --- a frame invariant moved and moved back -------------------------------------------------- + +/// The caller the rewriting inspector shows the frame instead of the one that called it. +const IMPOSTOR: Address = address!("00000000000000000000000000000000000ca11e"); + +/// Moves the frame's caller for the length of one instruction, and puts it back. +/// +/// `CALLER` reads `input.caller_address`, so the frame pushes an address nobody called it from and +/// goes on to store that. The rewrite is undone in the very next callback, which is what makes the +/// shape worth pinning: the frame's identity is the one the EVM gave it at every point a *frame* +/// could be inspected — at its start, at its end, and at every callback but the two this touches. +/// +/// Nothing about it reaches a gas counter. Both runs execute the same instructions and pay the +/// same cold `SSTORE`; only the value written differs. +#[derive(Default)] +struct BorrowTheCaller { + /// The caller the EVM gave the frame, kept so it can be handed back. + original: Option
, + /// How many times each half of the rewrite ran. + moved: u32, + restored: u32, +} + +impl Inspector for BorrowTheCaller { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.moved > 0 || interp.bytecode.opcode() != CALLER { + return; + } + self.original = Some(interp.input.caller_address); + interp.input.caller_address = IMPOSTOR; + self.moved += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + let Some(original) = self.original.filter(|_| self.restored == 0) else { + return; + }; + interp.input.caller_address = original; + self.restored += 1; + } +} + +/// ★ A frame invariant moved in `step` and moved back in `step_end` is not an all-zero ledger. +/// +/// The four addresses and the value a frame is identified by cannot change while it runs, which +/// makes them the readings a cheaper shim would be tempted to compare once per frame rather than +/// once per callback. This is the shape that answers that: an inspector borrows one of them for +/// exactly as long as it takes the frame to read it, and gives it back before anything outside the +/// two callbacks could look. A per-frame comparison sees the address it started with; a per-opcode +/// one sees it move twice. +#[test] +fn test_a_frame_invariant_moved_and_moved_back_is_booked() { + let code = BytecodeBuilder::default() + .append(CALLER) + .push_number(RESULT_SLOT) + .append(SSTORE) + .append(STOP) + .build(); + + let mut inspector = BorrowTheCaller::default(); + let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); + + assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); + assert_eq!( + inspector.original, + Some(crate::common::CALLER), + "and the half that gives the address back must have the one the EVM gave the frame", + ); + assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); + assert_eq!( + plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(crate::common::CALLER.as_slice()), + "without the rewrite the frame stores the address that called it", + ); + assert_eq!( + cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), + U256::from_be_slice(IMPOSTOR.as_slice()), + "with it, the frame stores one nobody called it from", + ); + assert_eq!( + plain.total_gas_spent, cheated.total_gas_spent, + "the two runs cost the same, so no gas lane can tell them apart", + ); + assert!( + cheated.inspector_ledger.interventions >= 2, + "each half of the rewrite is a rewrite: {:?}", + cheated.inspector_ledger, + ); +} diff --git a/crates/mega-evm/tests/rex7/shim_lanes.rs b/crates/mega-evm/tests/rex7/shim_lanes.rs new file mode 100644 index 00000000..5b4711a4 --- /dev/null +++ b/crates/mega-evm/tests/rex7/shim_lanes.rs @@ -0,0 +1,1213 @@ +//! The lanes the measurement shim books a rewrite on, and what each one is measured against. +//! +//! `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector +//! callback, so anything that changes across one is the inspector's doing by construction — which +//! is what makes the callback boundary a sound place to measure from. Every fixture here is the +//! same comparison: one run with an inspector against one without, over the same fixture, with the +//! conservation law checked on both by the shared driver. +//! +//! The two groups, in the order they appear: +//! +//! 1. **The shim itself** — gas written into an interpreter's counter or a frame's gas limit is +//! measured, booked, and kept out of enforcement, with the clamp re-derived on the spot; an +//! observation-only inspector is bit-identical to no inspector at all. +//! 2. **The receipt's other two numbers** — the EIP-3529 refund, measured at the callback boundary +//! because the EVM produces refunds too, and the EIP-8037 state-gas dimension, settled from the +//! transaction's final figures because `MegaETH` produces none of it and revm propagates it by +//! replacement. +//! +//! The two windows in which a rewrite lands after the accounting that should have read it are in +//! `shim_settlement.rs`, and the shapes an all-zero ledger used to admit are in +//! `shim_blind_spots.rs`. The rewrites the shim *refuses* are in `shim_refusals.rs`; the exhaustive +//! callback × shape sweep is in `inspector_cheat_matrix.rs`. + +use crate::{ + common::{base_db, transact, transact_inspected, Outcome, CALLEE, CONTRACT}, + inspector_common::{ + append_call, call_then_stop, countdown_loop_code, db_with_callee, deploy_then_stop, limits, + limits_with_compute, plain_run_code, REFUND, + }, +}; +use alloy_primitives::{Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + ConservationTerms, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaHaltReason, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CALL, MSTORE, POP, RETURN, STOP}, + interpreter::{ + interpreter_types::LoopControl, CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, + InstructionResult, Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, + }, + Inspector, +}; +use std::vec::Vec; + +// === the shim itself ========================================================================= +// +// The measurement shim: what an inspector does to gas is measured, booked, and kept out of +// enforcement. +// +// `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector +// callback, so anything that changes across one is the inspector's doing by construction — which +// is what makes the callback boundary a sound place to measure from. +// +// Each test here is one shape a rewriting inspector can take, and each pins a different half of +// the mechanism: +// +// - injecting gas into a running interpreter must not buy compute headroom, and the gas clamp must +// tighten again immediately rather than at the next checkpoint; +// - raising a child frame's gas limit conjures gas the transaction never funded, which the ledger +// has to account for or the conservation law breaks; +// - an observation-only inspector changes nothing at all; +// - and removing gas is measured with the same machinery as adding it. + +/// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. +/// +/// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact +/// number a test can assert on, and so the edit lands well inside the plain segment rather than at +/// its boundary. +#[derive(Default)] +struct GasEditor { + at: u64, + delta: i64, + steps: u64, + applied: bool, +} + +impl GasEditor { + fn new(at: u64, delta: i64) -> Self { + Self { at, delta, steps: 0, applied: false } + } +} + +impl Inspector for GasEditor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.steps != self.at || self.applied { + return; + } + self.applied = true; + if self.delta >= 0 { + interp.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + interp.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave enough gas for the removal to land", + ); + } + } +} + +/// Raises the gas limit of every call to [`CALLEE`] by a fixed amount. +#[derive(Default)] +struct CallGasLimitRaiser { + bonus: u64, + raises: u64, +} + +impl Inspector for CallGasLimitRaiser { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address == CALLEE { + inputs.gas_limit += self.bonus; + self.raises += 1; + } + None + } +} + +/// Rewrites every successful contract creation into a revert — the shape the frame loop has to +/// carry through to the journal. +#[derive(Default)] +struct CreateKiller { + killed: u64, +} + +impl Inspector for CreateKiller { + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + if outcome.result.result.is_ok() { + outcome.result.result = InstructionResult::Revert; + self.killed += 1; + } + } +} + +/// Counts callbacks and changes nothing. +#[derive(Default)] +struct Observer { + steps: u64, + calls: u64, + call_ends: u64, +} + +impl Inspector for Observer { + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.calls += 1; + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.call_ends += 1; + } +} + +/// (i) Gas injected into a running interpreter buys no compute headroom, is booked, and the clamp +/// tightens again on the spot. +/// +/// The fixture is a checkpoint-free loop under a compute limit far below what the loop needs, so +/// the gas clamp is the only thing that can stop it: the visible counter is pinned to the compute +/// headroom and revm's own gas check rejects the crossing opcode. An inspector then writes four +/// times that headroom into the counter, mid-loop. +/// +/// Three separate mechanisms are pinned: +/// +/// - **Enforcement does not eat the injection.** The recorded compute total is identical to the +/// uninspected run's, to the gas. Without the baseline shift, the injection reads as negative +/// work and the loop is handed free headroom. +/// - **The clamp is re-derived immediately.** Usage still stops exactly at the limit. Without the +/// re-clamp the loop runs on the injected gas until the frame ends, and the frame-exit settlement +/// then records the whole overshoot — the halt still lands, but hundreds of thousands of gas +/// late. +/// - **The ledger records it.** Exactly what was injected, no more. +#[test] +fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { + const INJECTED: u64 = 20_000; + let code = countdown_loop_code(10_000); + // Far below what the loop needs, so the clamp binds for the whole run. + let intrinsic = transact(MegaSpecId::REX7, base_db(plain_run_code(0)), limits()).compute_gas; + let limits = limits_with_compute(intrinsic + 5_000); + + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); + let mut inspector = GasEditor::new(20, INJECTED as i64); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); + + assert!(inspector.applied, "the fixture must reach the injection point"); + assert!( + matches!(plain.halt_reason("plain"), MegaHaltReason::ComputeGasLimitExceeded { .. }), + "fixture check: the uninspected run must stop on the compute limit, got {:?}", + plain.halt_reason("plain"), + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "the injection must be neither counted as work nor deducted from it, and the re-derived \ + clamp must stop the loop at the same opcode the uninspected run stopped at; \ + inspected result {:?}", + inspected.result, + ); + assert!( + matches!( + inspected.halt_reason("inspected"), + MegaHaltReason::ComputeGasLimitExceeded { .. } + ), + "injected gas must not turn a compute-limit halt into something else, got {:?}", + inspected.halt_reason("inspected"), + ); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::once(i128::from(INJECTED)), + "the ledger must hold exactly what was injected", + ); + assert_eq!(inspected.inspector_ledger.env, Lane::default(), "no frame envelope was touched"); + assert_eq!( + i128::from(inspected.total_gas_spent) + i128::from(INJECTED), + i128::from(plain.total_gas_spent), + "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ + spends exactly that much less than the uninspected run", + ); +} + +/// (v) The same machinery, in the other direction: gas removed from a running interpreter is +/// booked as a negative entry and is not charged as work. +/// +/// Under an active clamp the removal comes out of the hidden remainder rather than the visible +/// counter — the frame has more EVM gas than compute headroom, and destroying EVM gas does not +/// shrink the headroom — so the transaction runs to the same successful end while spending exactly +/// the removed amount more. +#[test] +fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { + const REMOVED: u64 = 1_000; + let code = plain_run_code(200); + + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = GasEditor::new(20, -(REMOVED as i64)); + let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + + assert!(inspector.applied, "the fixture must reach the removal point"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::once(-i128::from(REMOVED)), + "the ledger must hold the removal as a negative entry", + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "gas the inspector destroyed is not work the EVM performed", + ); + assert_eq!( + inspected.total_gas_spent, + plain.total_gas_spent + REMOVED, + "the removed gas never comes back, so the envelope is exactly that much larger", + ); +} + +/// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the +/// envelope only balances once the ledger accounts for it. +/// +/// The caller's `CALL` opcode debited the gas it forwards before any inspector callback ran, so the +/// bonus the inspector adds is paid for by nobody. The child hands it straight back on return, and +/// the transaction ends up spending exactly that much less than the uninspected run. +/// +/// Without the `env` lane the conservation law derives a destroyed total that is short by the +/// bonus, and the envelope assertion inside `execute_transaction` fails on the spot. +#[test] +fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { + const BONUS: u64 = 10_000; + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CALLEE) + .push_number(50_000u64) // gas + .append(CALL) + .push_number(0u64) + .append(MSTORE) + .push_number(32u64) + .push_number(0u64) + .append(RETURN) + .build(); + let build_db = || db_with_callee(code.clone(), callee.clone()); + + let plain = transact(MegaSpecId::REX7, build_db(), limits()); + let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); + + assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!(inspected.result.is_success(), "the inner call must still succeed"); + assert_eq!( + inspected.inspector_ledger.env, + Lane::once(i128::from(BONUS)), + "the ledger must hold exactly the gas the inspector added to the child's envelope", + ); + assert_eq!( + inspected.inspector_ledger.gas, + Lane::default(), + "no interpreter counter was touched" + ); + assert_eq!( + inspected.total_gas_spent + BONUS, + plain.total_gas_spent, + "the child returns the conjured gas to its caller, so the transaction spends that much less", + ); + assert_eq!( + inspected.enforced(), + plain.enforced(), + "a wider envelope is not more work: the child's compute budget comes from the compute \ + tracker, not from its gas limit", + ); +} + +/// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. +/// +/// A callback that returns a synthetic outcome has intercepted the frame: no frame is built from +/// the inputs, so no edit of theirs can widen an envelope. The gas that outcome carries is the +/// inspector's own choice and has nothing to do with the edit — here it is deliberately the +/// original forwarded amount, so the transaction really does conjure nothing and the identity has +/// to close at zero. +/// +/// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the +/// conservation law would come out over by the bonus — the same failure as not booking a real one, +/// with the sign flipped. +/// +/// The interception itself is booked, on the lane that carries rewrites rather than gas: answering +/// a frame the EVM was about to build changes what the transaction did, whatever it costs. +#[test] +fn test_an_intercepting_callback_books_no_envelope_adjustment() { + /// Raises the child's gas limit and then intercepts the call, handing back an outcome built + /// from the amount the caller actually forwarded. + #[derive(Default)] + struct Interceptor { + intercepted: u64, + } + + impl Inspector for Interceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + let forwarded = inputs.gas_limit; + inputs.gas_limit += 10_000; + self.intercepted += 1; + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), Gas::new(forwarded)), + inputs.return_memory_offset.clone(), + )) + } + } + + let callee = plain_run_code(20); + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, callee); + + let mut inspector = Interceptor::default(); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); + assert_eq!( + inspected.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ + itself a rewrite", + ); + assert_eq!(inspected.inspector_ledger.conjured_gas(), 0, "no gas lane may move on this shape"); +} + +/// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. +/// +/// A callback that returns a synthetic outcome skips the frame init entirely: no frame is built, +/// and the settlement that books what a refused frame init destroys never used to run on this +/// path. A halting outcome hands nothing back to the caller, so the transaction spends that +/// envelope with no compute total to show for it — which is exactly what the conservation law is +/// stated over, and what it goes red on. +#[test] +fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { + /// Intercepts the call to [`CALLEE`] with an exceptional halt, keeping the forwarded gas. + #[derive(Default)] + struct HaltingInterceptor { + intercepted: u64, + forwarded: u64, + } + + impl Inspector for HaltingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.forwarded = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::OutOfGas, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let code = call_then_stop(CALLEE, 50_000); + let db = db_with_callee(code, plain_run_code(20)); + + let mut inspector = HaltingInterceptor::default(); + let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); + assert_eq!( + inspected.destroyed, inspector.forwarded, + "the whole intercepted envelope is destroyed — nothing hands it back", + ); + assert_eq!( + inspected.compute_gas, + inspected.enforced() + inspected.destroyed, + "and it is reported without being enforced", + ); +} + +/// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the +/// state has to follow it. +/// +/// This is the rewrite direction there is something behind: the constructor ran, the deposit +/// predicates passed, and the inspector is telling the caller the frame failed. If the journal +/// decision were taken before the callback, the caller would be handed a failure over a deployed +/// contract, with the constructor's storage writes committed underneath it. +#[test] +fn test_killing_a_successful_creation_rolls_its_state_back() { + // Init code that stores to slot 1 and returns a two-byte runtime code. + let init_code: Vec = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(7)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset: the last two bytes of the word just stored + .append(RETURN) + .build() + .to_vec(); + + let code = deploy_then_stop(&init_code); + + let deployed = CONTRACT.create(0); + + // The uninspected run deploys, so the rewrite has something to undo. + let mut observer = Observer::default(); + let plain = + transact_inspected(MegaSpecId::REX7, base_db(code.clone()), limits(), &mut observer); + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); + assert!( + !deployed_account.info.is_empty_code_hash(), + "the fixture must deploy code for the rewrite to have something to undo", + ); + + let mut killer = CreateKiller::default(); + let killed = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut killer); + + assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); + assert!( + killed.state.get(&deployed).is_none_or(|account| account.info.is_empty_code_hash()), + "a creation the inspector failed must leave no code at {deployed}", + ); + assert_eq!( + killed + .state + .get(&deployed) + .and_then(|account| account.storage.get(&U256::from(1))) + .map(|slot| slot.present_value()) + .unwrap_or_default(), + U256::ZERO, + "and none of the constructor's storage writes", + ); +} + +/// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. +/// +/// This is the property every tracer in production depends on. The comparison is against a run with +/// no inspector attached at all, across every number the transaction reports and the state it +/// produced — not just the ones the ledger touches. +#[test] +fn test_an_observing_inspector_changes_nothing() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(0x20), U256::from(0x99)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let build_db = || db_with_callee(code.clone(), callee.clone()); + + let plain = transact(MegaSpecId::REX7, build_db(), limits()); + let mut inspector = Observer::default(); + let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); + + assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); + assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); + assert_eq!(inspector.call_ends, 2, "every call must be paired"); + + assert!( + inspected.inspector_ledger.is_zero(), + "an observation-only inspector must leave an empty ledger; got {:?}", + inspected.inspector_ledger, + ); + assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); + assert_eq!(inspected.compute_gas, plain.compute_gas); + assert_eq!(inspected.enforced(), plain.enforced()); + assert_eq!(inspected.destroyed, plain.destroyed); + assert_eq!(inspected.data_size, plain.data_size); + assert_eq!(inspected.kv_updates, plain.kv_updates); + assert_eq!(inspected.state_growth, plain.state_growth); + assert_eq!(inspected.gas_used, plain.gas_used); + assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); + assert_eq!(inspected.terms, plain.terms); + assert_eq!(inspected.state, plain.state, "the produced state must be identical"); +} + +/// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term +/// is zero — the shape every consumer of this API sees in practice. +/// +/// The stronger property is what the field is *for*: an all-zero ledger is a consumer's guarantee +/// that the gas numbers next to it are the EVM's own, so it has to be exactly zero rather than +/// merely small. A fixture that makes an inner call and writes storage is used, so the assertion +/// covers a transaction with something for a lane to have picked up. +#[test] +fn test_an_uninspected_transaction_reports_an_empty_ledger() { + let callee = plain_run_code(20); + let code = BytecodeBuilder::default() + .sstore(U256::from(1), U256::from(9)) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(50_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + let db = db_with_callee(code, callee); + + let plain = transact(MegaSpecId::REX7, db, limits()); + + assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); + assert!( + plain.terms.non_compute_gas > 0, + "fixture check: the transaction must have moved a lane other than compute", + ); + assert_eq!( + plain.inspector_ledger, + InspectorLedger::default(), + "no inspector ran, so every lane must be untouched", + ); + assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); +} + +// === the receipt's other two numbers ========================================================= +// +// The two numbers on a receipt that the conservation law cannot see, and the lanes that do. +// +// The law is stated over `total_gas_spent`, which is `limit - remaining`. A transaction's receipt +// carries two more figures that arithmetic does not reach: the EIP-3529 refund, which decides what +// the sender actually pays, and the EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and its +// `state_gas_spent` counter — which decides how much of the envelope the receipt counts as spent +// at all. +// +// Both are reachable from every callback that is handed a `Gas`, and both were unmeasured. The +// shapes here are what the two lanes now book, and each pins the *reason* its lane is measured +// where it is: +// +// - a **refund** is a quantity the EVM also produces, so only a difference across a callback +// isolates the inspector's share — the lane is measured at the boundary, and is nominal in both +// the senses that can make it differ from what reaches the receipt (the EIP-3529 cap, and the +// chain of successful frame returns an edit has to survive); +// - a **reservoir** is a quantity `MegaETH` never produces at all, and one revm propagates by +// replacement rather than by accumulation, so a boundary difference would book edits the EVM goes +// on to erase. The lane is settled once, from the number the transaction ends with, which is +// exactly the surviving part and is the inspector's in whole. + +/// Gas the fixture's inner `CALL` forwards. +const INNER_CALL_GAS: u64 = 200_000; + +/// A refund large enough that the cap keeps part of it out of the receipt. +const OVERSIZED_REFUND: i64 = 60_000; +/// The EIP-8037 pool an edit fills. +const RESERVOIR: u64 = 10_000; +/// The EIP-8037 spend an edit writes. +const STATE_GAS: i64 = 5_000; + +/// Slot the top frame writes. +const TOP_SLOT: u64 = 0x10; +/// Slot the callee writes. +const CALLEE_SLOT: u64 = 0x20; +/// Slot the callee sets and clears, so the frame ends holding a refund of the EVM's own making. +const CLEARED_SLOT: u64 = 0x30; + +// --- the fixture ------------------------------------------------------------------------------- + +/// How the fixture's callee ends, which is what decides whether its refund travels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Callee { + /// Writes storage, produces a refund by clearing a slot it just set, and returns. + Returning, + /// Writes storage and reverts, so the EVM discards everything the frame held. + Reverting, +} + +fn caller_code() -> Bytes { + append_call(BytecodeBuilder::default(), CALLEE, INNER_CALL_GAS, 0) + .append(POP) + .sstore(U256::from(TOP_SLOT), U256::from(1u64)) + .append(STOP) + .build() +} + +fn callee_code(callee: Callee) -> Bytes { + let builder = BytecodeBuilder::default() + .sstore(U256::from(CALLEE_SLOT), U256::from(1u64)) + // Set and clear, so the frame ends holding a refund the EVM itself produced. + .sstore(U256::from(CLEARED_SLOT), U256::from(1u64)) + .sstore(U256::from(CLEARED_SLOT), U256::ZERO); + match callee { + Callee::Returning => builder.append(STOP).build(), + Callee::Reverting => builder.revert().build(), + } +} + +fn db_for(callee: Callee) -> MemoryDatabase { + db_with_callee(caller_code(), callee_code(callee)) +} + +// --- the edit ---------------------------------------------------------------------------------- + +/// One edit, applied once, to one of the `Gas` objects a callback is handed. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Edit { + /// Add to the running interpreter's refund counter. + RefundAtStep(i64), + /// Add to the finished inner call's refund counter. + RefundAtCallEnd(i64), + /// Fill the running interpreter's EIP-8037 pool. + ReservoirAtStep, + /// Fill it at the one moment the frame is holding a `NewFrame` action, whose child overwrites + /// the pool on the way back. + ReservoirAtSuspension, + /// Fill the pool the inner call's inputs seed the child frame with. + ReservoirOnInputs, + /// Fill the finished inner call's pool. + ReservoirAtCallEnd, + /// Write the running interpreter's EIP-8037 spend counter. + StateGasAtStep, + /// Write the finished inner call's spend counter. + StateGasAtCallEnd, + /// Answer the inner call with a synthetic outcome that echoes the envelope and carries + /// neither figure — the control the two below are read against. + InterceptEcho, + /// The same, carrying a refund the frame never earned. + InterceptWithRefund, + /// The same, carrying an EIP-8037 pool. + InterceptWithReservoir, +} + +impl Edit { + /// Whether this edit answers the frame itself instead of letting the EVM build it. + const fn intercepts(self) -> bool { + matches!( + self, + Self::InterceptEcho | Self::InterceptWithRefund | Self::InterceptWithReservoir + ) + } +} + +/// Applies one [`Edit`], once, and records that it landed. +#[derive(Debug)] +struct Editor { + edit: Edit, + fired: u32, + steps: u64, +} + +impl Editor { + const fn new(edit: Edit) -> Self { + Self { edit, fired: 0, steps: 0 } + } +} + +impl Inspector for Editor { + fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + self.steps += 1; + if self.fired > 0 || self.steps != 4 { + return; + } + match self.edit { + Edit::RefundAtStep(amount) => interp.gas.record_refund(amount), + Edit::ReservoirAtStep => interp.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtStep => interp.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } + + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || self.edit != Edit::ReservoirAtSuspension { + return; + } + // The one window where the pool the frame holds is not the pool that travels: the child + // this action builds was already sized from the pre-edit value, and its own pool + // overwrites this one when it returns. + if !matches!(interp.bytecode.action(), Some(InterpreterAction::NewFrame(_))) { + return; + } + interp.gas.set_reservoir(RESERVOIR); + self.fired += 1; + } + + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if self.fired > 0 || inputs.target_address != CALLEE { + return None; + } + if self.edit == Edit::ReservoirOnInputs { + inputs.reservoir += RESERVOIR; + self.fired += 1; + return None; + } + if !self.edit.intercepts() { + return None; + } + // The echo convention every tool that intercepts follows: hand back exactly what was + // forwarded, so the gas lanes see nothing and only the figures under test move. + let mut gas = Gas::new(inputs.gas_limit); + match self.edit { + Edit::InterceptWithRefund => gas.record_refund(REFUND), + Edit::InterceptWithReservoir => gas.set_reservoir(RESERVOIR), + _ => {} + } + self.fired += 1; + Some(CallOutcome::new( + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas), + inputs.return_memory_offset.clone(), + )) + } + + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != CALLEE { + return; + } + match self.edit { + Edit::RefundAtCallEnd(amount) => outcome.result.gas.record_refund(amount), + Edit::ReservoirAtCallEnd => outcome.result.gas.set_reservoir(RESERVOIR), + Edit::StateGasAtCallEnd => outcome.result.gas.set_state_gas_spent(STATE_GAS), + _ => return, + } + self.fired += 1; + } +} + +/// Runs the fixture with no inspector at all. +fn transact_plain(callee: Callee) -> Outcome { + transact(MegaSpecId::REX7, db_for(callee), limits()) +} + +/// Runs it with one edit applied, asserting the edit landed exactly once. +fn transact_edited(callee: Callee, edit: Edit) -> Outcome { + let mut editor = Editor::new(edit); + let outcome = transact_inspected(MegaSpecId::REX7, db_for(callee), limits(), &mut editor); + assert_eq!( + editor.fired, 1, + "{edit:?}: the fixture must reach the edit's callback exactly once", + ); + outcome +} + +// --- the fixture's own assumptions --------------------------------------------------------------- + +/// The uninspected run is what the cells below assume it is: it succeeds, it produces a refund of +/// its own, and it reports no EIP-8037 dimension at all. +#[test] +fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { + let plain = transact_plain(Callee::Returning); + assert!(plain.result.is_success(), "{:?}", plain.result); + assert!( + plain.refunded() > 0, + "the callee's cleared slot must leave a refund for the lowering cell to take from", + ); + assert_eq!( + plain.gas_used, + plain.total_gas_spent - plain.refunded(), + "the receipt's two gas numbers differ by exactly the refund", + ); + assert_eq!(plain.state_gas_spent(), 0, "EIP-8037 is off on every MegaETH path"); + assert!(plain.inspector_ledger.is_zero(), "no inspector ran: {:?}", plain.inspector_ledger); +} + +// --- the refund lane +// ------------------------------------------------------------------------------ + +/// A refund written into a running interpreter's counter is booked, and moves what the sender pays +/// without moving the envelope. +#[test] +fn test_a_refund_written_into_a_live_interpreter_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + "the shim must book the refund and nothing else", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "a refund does not move the envelope, which is why the law cannot see it", + ); + assert_eq!( + edited.refunded(), + plain.refunded() + u64::try_from(REFUND).unwrap(), + "but it does move the receipt's refund", + ); + assert_eq!( + edited.gas_used, + plain.gas_used - u64::try_from(REFUND).unwrap(), + "and through it what the sender pays", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, 0, + "the refund lane is deliberately not a term of the law", + ); + assert!(!edited.inspector_ledger.is_zero(), "and the block guard has to see it"); +} + +/// The same edit made at the last callback that holds the finished frame's result. +#[test] +fn test_a_refund_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded(), plain.refunded() + u64::try_from(REFUND).unwrap()); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); +} + +/// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction +/// would report an inspector that raised the sender's bill as having done nothing. +#[test] +fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { + let plain = transact_plain(Callee::Returning); + assert!( + plain.refunded() >= u64::try_from(REFUND).unwrap(), + "fixture check: there must be a refund to take from, got {}", + plain.refunded(), + ); + + let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, + ); + assert_eq!(edited.refunded(), plain.refunded() - u64::try_from(REFUND).unwrap()); + assert_eq!( + edited.gas_used, + plain.gas_used + u64::try_from(REFUND).unwrap(), + "the sender pays more, by exactly what was taken", + ); +} + +/// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. +/// +/// The cap applies to the transaction's whole refund at once, over a sum in which the EVM's own +/// refunds and an inspector's are indistinguishable, at a point past every callback. Splitting it +/// between them needs a priority rule the protocol does not have, so the lane states the edit and +/// the receipt states the effect — and the two are allowed to differ. +#[test] +fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + refund: Lane::once(i128::from(OVERSIZED_REFUND)), + ..InspectorLedger::default() + }, + "the lane carries the nominal edit", + ); + assert_eq!( + edited.refunded(), + edited.total_gas_spent / 5, + "while the receipt carries the EIP-3529 cap", + ); + assert!( + edited.refunded() < plain.refunded() + u64::try_from(OVERSIZED_REFUND).unwrap(), + "fixture check: the cap must actually bind, or this cell asserts nothing", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); +} + +/// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. +/// +/// revm hands a frame's refund to its caller only on success, so this edit dies with the frame. +/// The lane books it anyway, because the alternative is a rule that has to track every frame +/// between the edit and the top — and because a lane that under-reports lets exactly the shape +/// this module exists to catch into a block, while over-reporting costs nothing: the law has no +/// term for it. +#[test] +fn test_a_refund_the_frame_chain_discards_is_still_booked() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, + "the lane books the edit", + ); + assert_eq!( + edited.refunded(), + plain.refunded(), + "the receipt is unmoved: a reverting frame hands its caller no refund", + ); + assert_eq!(edited.gas_used, plain.gas_used); +} + +// --- the EIP-8037 state-gas dimension ------------------------------------------------------------ + +/// A reservoir an inspector fills is gas the transaction never funded: the receipt reports that +/// much less spent, and the law needs it back. +#[test] +fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - RESERVOIR, + "the receipt counts the pool as unspent, so the envelope shrinks by exactly it", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + i128::from(RESERVOIR), + "which is why this lane, unlike the refund one, is a term of the law", + ); +} + +/// The same, written into the pool a call's inputs seed the child frame with. +#[test] +fn test_a_reservoir_written_into_a_frame_input_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + // The inputs came back changed in a field the envelope lane does not cover, which the + // rewrite comparison books on its own. + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); +} + +/// And into the finished frame's own pool, which its caller takes whatever the classification. +#[test] +fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); +} + +/// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the +/// run it produces is the run the EVM would have produced alone. +/// +/// This is the window that decides where the lane is measured. A difference taken across this +/// callback would say `RESERVOIR` was conjured; the transaction says otherwise, and the settlement +/// point is the only reading that agrees with it. +#[test] +fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); + + assert!( + edited.inspector_ledger.is_zero(), + "an edit the child frame's own pool replaces moved nothing: {:?}", + edited.inspector_ledger, + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent); + assert_eq!(edited.gas_used, plain.gas_used); + assert_eq!(edited.refunded(), plain.refunded()); +} + +/// The spend counter's own effect on the receipt: a successful transaction reports it, whether or +/// not EIP-8037 is enabled. +#[test] +fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_booked() { + let plain = transact_plain(Callee::Returning); + let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); + + assert_eq!(plain.state_gas_spent(), 0, "fixture check"); + assert_eq!( + edited.state_gas_spent(), + u64::try_from(STATE_GAS).unwrap(), + "the receipt reports what was written", + ); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope is untouched, so this lane is not a term of the law either", + ); + assert_eq!(edited.terms.inspector_conjured_gas, 0); +} + +/// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend +/// counter back into its caller's pool, which turns a state-gas edit into an envelope-moving one. +/// +/// The lane that catches it is the reservoir's, not the state-gas one, because the fold has +/// already happened by the time either is read. That is the second reason the two are settled from +/// the transaction's final figures rather than differenced across a callback. +#[test] +fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { + let plain = transact_plain(Callee::Reverting); + let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); + + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + "the spend counter of a reverting frame arrives in its caller as a pool", + ); + assert_eq!( + edited.state_gas_spent(), + 0, + "and not as a spend: a failing frame's counter is not accumulated", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), + "so the envelope moves, and the law's term has to move with it", + ); +} + +// --- a frame the inspector answers itself +// --------------------------------------------------------- + +/// A synthetic outcome carries figures of its own, and there is no EVM-produced number on the +/// other side of the callback to difference against — so the whole of what it carries is the +/// inspector's, measured against nothing rather than against a baseline. +/// +/// The echo control is what makes the two cells below readings of the figures rather than of the +/// interception: it moves the gas lanes not at all, which is the convention every tool that +/// intercepts follows. +#[test] +fn test_a_synthetic_outcome_carries_its_own_figures() { + let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); + assert_eq!( + echo.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "an echoing interception moves no figure at all", + ); + + let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); + assert_eq!( + refunding.inspector_ledger, + InspectorLedger { + refund: Lane::once(i128::from(REFUND)), + interventions: 1, + ..InspectorLedger::default() + }, + "the refund a frame that never ran hands back is the inspector's in whole", + ); + assert_eq!( + refunding.refunded(), + echo.refunded() + u64::try_from(REFUND).unwrap(), + "and it reaches the receipt: the outcome succeeded, so its caller records it", + ); + assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); + + let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); + assert_eq!( + pooled.inspector_ledger, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + interventions: 1, + ..InspectorLedger::default() + }, + ); + assert_eq!( + pooled.total_gas_spent, + echo.total_gas_spent - RESERVOIR, + "a pool does move the envelope, wherever it came from", + ); +} + +// --- the frozen specs +// ----------------------------------------------------------------------------- + +/// On a frozen spec the two lanes report and settle nothing. +/// +/// The shim is not spec-gated, and must not be: the block guard has to see a rewritten receipt +/// whichever spec produced it. What is gated is the accounting the lanes feed, so a frozen spec's +/// own numbers have to be exactly what they were — which is what this reads, by comparing an +/// edited run against an unedited one on the same spec. +#[test] +fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { + const REX6: MegaSpecId = MegaSpecId::REX6; + fn run(edit: Option) -> Outcome { + let db = db_for(Callee::Returning); + let limits = EvmTxRuntimeLimits::from_spec(REX6); + match edit { + Some(edit) => { + let mut editor = Editor::new(edit); + let outcome = transact_inspected(REX6, db, limits, &mut editor); + assert_eq!(editor.fired, 1, "{edit:?} must land"); + outcome + } + None => transact(REX6, db, limits), + } + } + + let plain = run(None); + assert!(plain.inspector_ledger.is_zero()); + + for (edit, expected) in [ + ( + Edit::RefundAtStep(REFUND), + InspectorLedger { + refund: Lane::once(i128::from(REFUND)), + ..InspectorLedger::default() + }, + ), + ( + Edit::ReservoirAtStep, + InspectorLedger { + reservoir: Lane::once(i128::from(RESERVOIR)), + ..InspectorLedger::default() + }, + ), + ( + Edit::StateGasAtStep, + InspectorLedger { + state_gas: Lane::once(i128::from(STATE_GAS)), + ..InspectorLedger::default() + }, + ), + ] { + let edited = run(Some(edit)); + assert_eq!(edited.inspector_ledger, expected, "{edit:?}: the lane reports on every spec"); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "{edit:?}: a frozen spec's compute total must not move", + ); + assert_eq!(edited.destroyed, plain.destroyed, "{edit:?}: nor its destroyed lane"); + // `inspector_conjured_gas` is a reading of the ledger rather than something the + // transaction recorded, so it moves with the lane on every spec. Every other term is what + // a frozen spec must leave alone. + assert_eq!( + ConservationTerms { inspector_conjured_gas: 0, ..edited.terms }, + plain.terms, + "{edit:?}: nothing a frozen spec records may move", + ); + assert_eq!( + edited.terms.inspector_conjured_gas, + edited.inspector_ledger.conjured_gas(), + "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", + ); + } +} diff --git a/crates/mega-evm/tests/rex7/shim_measurement.rs b/crates/mega-evm/tests/rex7/shim_measurement.rs deleted file mode 100644 index ca7dff98..00000000 --- a/crates/mega-evm/tests/rex7/shim_measurement.rs +++ /dev/null @@ -1,3173 +0,0 @@ -//! What the measurement shim books, over every shape that can move a number it reports. -//! -//! `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector -//! callback, so anything that changes across one is the inspector's doing by construction — which -//! is what makes the callback boundary a sound place to measure from. Every fixture here is the -//! same comparison: one run with an inspector against one without, over the same fixture, with the -//! conservation law checked on both by the shared driver. -//! -//! The sections, in the order they appear: -//! -//! 1. **The shim itself** — gas written into an interpreter's counter or a frame's gas limit is -//! measured, booked, and kept out of enforcement, with the clamp re-derived on the spot; an -//! observation-only inspector is bit-identical to no inspector at all. -//! 2. **The two settlement windows** — a terminating opcode's `step_end`, whose counter edit -//! reaches nobody, and a precompile's classification, whose split has to follow the callback -//! rather than the recording site. -//! 3. **The blind spots** — the rewrite shapes an all-zero ledger used to admit: a frame's memory -//! grown for free, an outcome's metadata rewritten around the result inside it, two edits to one -//! signed lane that cancel, an instruction deleted by stepping the program counter past it, and -//! a return buffer conjured in front of a frame that made no call. -//! 4. **The receipt's other two numbers** — the EIP-3529 refund, measured at the callback boundary -//! because the EVM produces refunds too, and the EIP-8037 state-gas dimension, settled from the -//! transaction's final figures because `MegaETH` produces none of it and revm propagates it by -//! replacement. -//! 5. **Interception** — the gas an inspector puts into a synthetic outcome, over the four sizings -//! it can choose relative to the envelope it was handed, and the halt direction where the choice -//! reaches nothing. -//! -//! The rewrites the shim *refuses* are in `shim_refusals.rs`; the exhaustive callback × shape -//! sweep is in `inspector_cheat_matrix.rs`. - -use crate::{ - common::{ - base_db, transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, - CALLEE, CONTRACT, DEFAULT_TX_GAS_LIMIT, EMPTY_TARGET, - }, - inspector_common::{ - append_call, call_then_stop, countdown_loop_code, db_with_callee, deploy_then_stop, limits, - limits_with_compute, plain_and_cheated, plain_run_code, - }, -}; -use alloy_primitives::{address, Address, Bytes, U256}; -use mega_evm::{ - kzg_point_evaluation, - test_utils::{BytecodeBuilder, MemoryDatabase}, - ConservationTerms, EvmTxRuntimeLimits, InspectorLedger, Lane, MegaHaltReason, MegaSpecId, -}; -use revm::{ - bytecode::opcode::{ - CALL, CALLER, CREATE, GAS, INVALID, MLOAD, MSTORE, MSTORE8, POP, RETURN, RETURNDATASIZE, - SSTORE, STOP, - }, - context::{Cfg, ContextTr}, - handler::FrameResult, - interpreter::{ - interpreter::EthInterpreter, - interpreter_types::{InputsTr, Jumps, LoopControl, MemoryTr, ReturnData}, - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Gas, InstructionResult, - Interpreter, InterpreterAction, InterpreterResult, InterpreterTypes, - }, - Inspector, -}; -use sha2::{Digest, Sha256}; -use std::vec::Vec; - -// === 1. the shim itself ======================================================================= -// -// The measurement shim: what an inspector does to gas is measured, booked, and kept out of -// enforcement. -// -// `MegaETH` wraps every inspector it is handed. The EVM does not execute inside an inspector -// callback, so anything that changes across one is the inspector's doing by construction — which -// is what makes the callback boundary a sound place to measure from. -// -// Each test here is one shape a rewriting inspector can take, and each pins a different half of -// the mechanism: -// -// - injecting gas into a running interpreter must not buy compute headroom, and the gas clamp must -// tighten again immediately rather than at the next checkpoint; -// - raising a child frame's gas limit conjures gas the transaction never funded, which the ledger -// has to account for or the conservation law breaks; -// - an observation-only inspector changes nothing at all; -// - and removing gas is measured with the same machinery as adding it. - -/// Edits the interpreter's gas counter once, at the `at`-th step, by `delta` gas. -/// -/// One edit rather than a per-step trickle so that the amount conjured (or destroyed) is an exact -/// number a test can assert on, and so the edit lands well inside the plain segment rather than at -/// its boundary. -#[derive(Default)] -struct GasEditor { - at: u64, - delta: i64, - steps: u64, - applied: bool, -} - -impl GasEditor { - fn new(at: u64, delta: i64) -> Self { - Self { at, delta, steps: 0, applied: false } - } -} - -impl Inspector for GasEditor { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - if self.steps != self.at || self.applied { - return; - } - self.applied = true; - if self.delta >= 0 { - interp.gas.erase_cost(self.delta.unsigned_abs()); - } else { - assert!( - interp.gas.record_regular_cost(self.delta.unsigned_abs()), - "the fixture must leave enough gas for the removal to land", - ); - } - } -} - -/// Raises the gas limit of every call to [`CALLEE`] by a fixed amount. -#[derive(Default)] -struct CallGasLimitRaiser { - bonus: u64, - raises: u64, -} - -impl Inspector for CallGasLimitRaiser { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address == CALLEE { - inputs.gas_limit += self.bonus; - self.raises += 1; - } - None - } -} - -/// Rewrites every successful contract creation into a revert — the shape the frame loop has to -/// carry through to the journal. -#[derive(Default)] -struct CreateKiller { - killed: u64, -} - -impl Inspector for CreateKiller { - fn create_end( - &mut self, - _context: &mut CTX, - _inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - if outcome.result.result.is_ok() { - outcome.result.result = InstructionResult::Revert; - self.killed += 1; - } - } -} - -/// Counts callbacks and changes nothing. -#[derive(Default)] -struct Observer { - steps: u64, - calls: u64, - call_ends: u64, -} - -impl Inspector for Observer { - fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - } - - fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { - self.calls += 1; - None - } - - fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { - self.call_ends += 1; - } -} - -/// (i) Gas injected into a running interpreter buys no compute headroom, is booked, and the clamp -/// tightens again on the spot. -/// -/// The fixture is a checkpoint-free loop under a compute limit far below what the loop needs, so -/// the gas clamp is the only thing that can stop it: the visible counter is pinned to the compute -/// headroom and revm's own gas check rejects the crossing opcode. An inspector then writes four -/// times that headroom into the counter, mid-loop. -/// -/// Three separate mechanisms are pinned: -/// -/// - **Enforcement does not eat the injection.** The recorded compute total is identical to the -/// uninspected run's, to the gas. Without the baseline shift, the injection reads as negative -/// work and the loop is handed free headroom. -/// - **The clamp is re-derived immediately.** Usage still stops exactly at the limit. Without the -/// re-clamp the loop runs on the injected gas until the frame ends, and the frame-exit settlement -/// then records the whole overshoot — the halt still lands, but hundreds of thousands of gas -/// late. -/// - **The ledger records it.** Exactly what was injected, no more. -#[test] -fn test_injected_gas_is_booked_and_never_becomes_compute_headroom() { - const INJECTED: u64 = 20_000; - let code = countdown_loop_code(10_000); - // Far below what the loop needs, so the clamp binds for the whole run. - let intrinsic = transact(MegaSpecId::REX7, base_db(plain_run_code(0)), limits()).compute_gas; - let limits = limits_with_compute(intrinsic + 5_000); - - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); - let mut inspector = GasEditor::new(20, INJECTED as i64); - let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); - - assert!(inspector.applied, "the fixture must reach the injection point"); - assert!( - matches!(plain.halt_reason("plain"), MegaHaltReason::ComputeGasLimitExceeded { .. }), - "fixture check: the uninspected run must stop on the compute limit, got {:?}", - plain.halt_reason("plain"), - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "the injection must be neither counted as work nor deducted from it, and the re-derived \ - clamp must stop the loop at the same opcode the uninspected run stopped at; \ - inspected result {:?}", - inspected.result, - ); - assert!( - matches!( - inspected.halt_reason("inspected"), - MegaHaltReason::ComputeGasLimitExceeded { .. } - ), - "injected gas must not turn a compute-limit halt into something else, got {:?}", - inspected.halt_reason("inspected"), - ); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::once(i128::from(INJECTED)), - "the ledger must hold exactly what was injected", - ); - assert_eq!(inspected.inspector_ledger.env, Lane::default(), "no frame envelope was touched"); - assert_eq!( - i128::from(inspected.total_gas_spent) + i128::from(INJECTED), - i128::from(plain.total_gas_spent), - "the injected gas is refunded with the rest of the rescued remainder, so the transaction \ - spends exactly that much less than the uninspected run", - ); -} - -/// (v) The same machinery, in the other direction: gas removed from a running interpreter is -/// booked as a negative entry and is not charged as work. -/// -/// Under an active clamp the removal comes out of the hidden remainder rather than the visible -/// counter — the frame has more EVM gas than compute headroom, and destroying EVM gas does not -/// shrink the headroom — so the transaction runs to the same successful end while spending exactly -/// the removed amount more. -#[test] -fn test_removed_gas_is_booked_as_a_negative_entry_and_is_not_charged_as_work() { - const REMOVED: u64 = 1_000; - let code = plain_run_code(200); - - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = GasEditor::new(20, -(REMOVED as i64)); - let inspected = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - - assert!(inspector.applied, "the fixture must reach the removal point"); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!(inspected.result.is_success(), "removing gas must not fail the transaction"); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::once(-i128::from(REMOVED)), - "the ledger must hold the removal as a negative entry", - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "gas the inspector destroyed is not work the EVM performed", - ); - assert_eq!( - inspected.total_gas_spent, - plain.total_gas_spent + REMOVED, - "the removed gas never comes back, so the envelope is exactly that much larger", - ); -} - -/// (ii) Raising a child frame's gas limit conjures gas the transaction never funded, and the -/// envelope only balances once the ledger accounts for it. -/// -/// The caller's `CALL` opcode debited the gas it forwards before any inspector callback ran, so the -/// bonus the inspector adds is paid for by nobody. The child hands it straight back on return, and -/// the transaction ends up spending exactly that much less than the uninspected run. -/// -/// Without the `env` lane the conservation law derives a destroyed total that is short by the -/// bonus, and the envelope assertion inside `execute_transaction` fails on the spot. -#[test] -fn test_a_raised_child_gas_limit_is_booked_as_conjured_gas() { - const BONUS: u64 = 10_000; - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(50_000u64) // gas - .append(CALL) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let build_db = || db_with_callee(code.clone(), callee.clone()); - - let plain = transact(MegaSpecId::REX7, build_db(), limits()); - let mut inspector = CallGasLimitRaiser { bonus: BONUS, raises: 0 }; - let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); - - assert_eq!(inspector.raises, 1, "the fixture must make exactly one inner call"); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!(inspected.result.is_success(), "the inner call must still succeed"); - assert_eq!( - inspected.inspector_ledger.env, - Lane::once(i128::from(BONUS)), - "the ledger must hold exactly the gas the inspector added to the child's envelope", - ); - assert_eq!( - inspected.inspector_ledger.gas, - Lane::default(), - "no interpreter counter was touched" - ); - assert_eq!( - inspected.total_gas_spent + BONUS, - plain.total_gas_spent, - "the child returns the conjured gas to its caller, so the transaction spends that much less", - ); - assert_eq!( - inspected.enforced(), - plain.enforced(), - "a wider envelope is not more work: the child's compute budget comes from the compute \ - tracker, not from its gas limit", - ); -} - -/// (ii, mirror) An edit to inputs the EVM never reads conjures nothing, so nothing is booked. -/// -/// A callback that returns a synthetic outcome has intercepted the frame: no frame is built from -/// the inputs, so no edit of theirs can widen an envelope. The gas that outcome carries is the -/// inspector's own choice and has nothing to do with the edit — here it is deliberately the -/// original forwarded amount, so the transaction really does conjure nothing and the identity has -/// to close at zero. -/// -/// Booking the edit anyway would claim gas was conjured for a frame that never existed, and the -/// conservation law would come out over by the bonus — the same failure as not booking a real one, -/// with the sign flipped. -/// -/// The interception itself is booked, on the lane that carries rewrites rather than gas: answering -/// a frame the EVM was about to build changes what the transaction did, whatever it costs. -#[test] -fn test_an_intercepting_callback_books_no_envelope_adjustment() { - /// Raises the child's gas limit and then intercepts the call, handing back an outcome built - /// from the amount the caller actually forwarded. - #[derive(Default)] - struct Interceptor { - intercepted: u64, - } - - impl Inspector for Interceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - let forwarded = inputs.gas_limit; - inputs.gas_limit += 10_000; - self.intercepted += 1; - Some(CallOutcome::new( - InterpreterResult::new(InstructionResult::Stop, Bytes::new(), Gas::new(forwarded)), - inputs.return_memory_offset.clone(), - )) - } - } - - let callee = plain_run_code(20); - let code = call_then_stop(CALLEE, 50_000); - let db = db_with_callee(code, callee); - - let mut inspector = Interceptor::default(); - let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(inspected.result.is_success(), "fixture check: {:?}", inspected.result); - assert_eq!( - inspected.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "an edit to inputs that never reach a frame conjures nothing, but answering the frame is \ - itself a rewrite", - ); - assert_eq!(inspected.inspector_ledger.conjured_gas(), 0, "no gas lane may move on this shape"); -} - -/// An intercepted frame that halts destroys the envelope it was handed, and that has to be booked. -/// -/// A callback that returns a synthetic outcome skips the frame init entirely: no frame is built, -/// and the settlement that books what a refused frame init destroys never used to run on this -/// path. A halting outcome hands nothing back to the caller, so the transaction spends that -/// envelope with no compute total to show for it — which is exactly what the conservation law is -/// stated over, and what it goes red on. -#[test] -fn test_an_intercepted_frame_that_halts_books_the_envelope_it_destroys() { - /// Intercepts the call to [`CALLEE`] with an exceptional halt, keeping the forwarded gas. - #[derive(Default)] - struct HaltingInterceptor { - intercepted: u64, - forwarded: u64, - } - - impl Inspector for HaltingInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - self.forwarded = inputs.gas_limit; - Some(CallOutcome::new( - InterpreterResult::new( - InstructionResult::OutOfGas, - Bytes::new(), - Gas::new(inputs.gas_limit), - ), - inputs.return_memory_offset.clone(), - )) - } - } - - let code = call_then_stop(CALLEE, 50_000); - let db = db_with_callee(code, plain_run_code(20)); - - let mut inspector = HaltingInterceptor::default(); - let inspected = transact_inspected(MegaSpecId::REX7, db, limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(inspected.result.is_success(), "the caller absorbs the halt: {:?}", inspected.result); - assert_eq!( - inspected.destroyed, inspector.forwarded, - "the whole intercepted envelope is destroyed — nothing hands it back", - ); - assert_eq!( - inspected.compute_gas, - inspected.enforced() + inspected.destroyed, - "and it is reported without being enforced", - ); -} - -/// A `create_end` that turns a *successful* contract creation into a failure is honoured — and the -/// state has to follow it. -/// -/// This is the rewrite direction there is something behind: the constructor ran, the deposit -/// predicates passed, and the inspector is telling the caller the frame failed. If the journal -/// decision were taken before the callback, the caller would be handed a failure over a deployed -/// contract, with the constructor's storage writes committed underneath it. -#[test] -fn test_killing_a_successful_creation_rolls_its_state_back() { - // Init code that stores to slot 1 and returns a two-byte runtime code. - let init_code: Vec = BytecodeBuilder::default() - .sstore(U256::from(1), U256::from(7)) - .push_number(0x6000u64) - .push_number(0u64) - .append(MSTORE) - .push_number(2u64) // size - .push_number(30u64) // offset: the last two bytes of the word just stored - .append(RETURN) - .build() - .to_vec(); - - let code = deploy_then_stop(&init_code); - - let deployed = CONTRACT.create(0); - - // The uninspected run deploys, so the rewrite has something to undo. - let mut observer = Observer::default(); - let plain = - transact_inspected(MegaSpecId::REX7, base_db(code.clone()), limits(), &mut observer); - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - let deployed_account = plain.state.get(&deployed).expect("the fixture must deploy a contract"); - assert!( - !deployed_account.info.is_empty_code_hash(), - "the fixture must deploy code for the rewrite to have something to undo", - ); - - let mut killer = CreateKiller::default(); - let killed = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut killer); - - assert_eq!(killer.killed, 1, "the fixture must rewrite exactly one creation"); - assert!( - killed.state.get(&deployed).is_none_or(|account| account.info.is_empty_code_hash()), - "a creation the inspector failed must leave no code at {deployed}", - ); - assert_eq!( - killed - .state - .get(&deployed) - .and_then(|account| account.storage.get(&U256::from(1))) - .map(|slot| slot.present_value()) - .unwrap_or_default(), - U256::ZERO, - "and none of the constructor's storage writes", - ); -} - -/// (iv) An observation-only inspector leaves an empty ledger and a bit-identical transaction. -/// -/// This is the property every tracer in production depends on. The comparison is against a run with -/// no inspector attached at all, across every number the transaction reports and the state it -/// produced — not just the ones the ledger touches. -#[test] -fn test_an_observing_inspector_changes_nothing() { - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .sstore(U256::from(0x20), U256::from(0x99)) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let build_db = || db_with_callee(code.clone(), callee.clone()); - - let plain = transact(MegaSpecId::REX7, build_db(), limits()); - let mut inspector = Observer::default(); - let inspected = transact_inspected(MegaSpecId::REX7, build_db(), limits(), &mut inspector); - - assert!(inspector.steps > 0, "the fixture must actually run opcodes under the inspector"); - assert_eq!(inspector.calls, 2, "one top-level frame plus one inner call"); - assert_eq!(inspector.call_ends, 2, "every call must be paired"); - - assert!( - inspected.inspector_ledger.is_zero(), - "an observation-only inspector must leave an empty ledger; got {:?}", - inspected.inspector_ledger, - ); - assert_eq!(format!("{:?}", inspected.result), format!("{:?}", plain.result)); - assert_eq!(inspected.compute_gas, plain.compute_gas); - assert_eq!(inspected.enforced(), plain.enforced()); - assert_eq!(inspected.destroyed, plain.destroyed); - assert_eq!(inspected.data_size, plain.data_size); - assert_eq!(inspected.kv_updates, plain.kv_updates); - assert_eq!(inspected.state_growth, plain.state_growth); - assert_eq!(inspected.gas_used, plain.gas_used); - assert_eq!(inspected.total_gas_spent, plain.total_gas_spent); - assert_eq!(inspected.terms, plain.terms); - assert_eq!(inspected.state, plain.state, "the produced state must be identical"); -} - -/// A transaction that ran with no inspector at all reports an empty ledger, and the law's `I` term -/// is zero — the shape every consumer of this API sees in practice. -/// -/// The stronger property is what the field is *for*: an all-zero ledger is a consumer's guarantee -/// that the gas numbers next to it are the EVM's own, so it has to be exactly zero rather than -/// merely small. A fixture that makes an inner call and writes storage is used, so the assertion -/// covers a transaction with something for a lane to have picked up. -#[test] -fn test_an_uninspected_transaction_reports_an_empty_ledger() { - let callee = plain_run_code(20); - let code = BytecodeBuilder::default() - .sstore(U256::from(1), U256::from(9)) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(CALLEE) - .push_number(50_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let db = db_with_callee(code, callee); - - let plain = transact(MegaSpecId::REX7, db, limits()); - - assert!(plain.result.is_success(), "fixture check: {:?}", plain.result); - assert!( - plain.terms.non_compute_gas > 0, - "fixture check: the transaction must have moved a lane other than compute", - ); - assert_eq!( - plain.inspector_ledger, - InspectorLedger::default(), - "no inspector ran, so every lane must be untouched", - ); - assert_eq!(plain.terms.inspector_conjured_gas, 0, "and the law's inspector term must be zero"); -} - -// === 2. the two settlement windows ============================================================ -// -// The two windows in which a rewrite lands after the accounting that should have read it. -// -// Both halves of the measurement shim rest on the same claim: what the shim books is what the -// transaction's envelope actually moved by. There are two places where the number the shim reads -// and the number the envelope carries are not the same object, and each of them is a fixture -// here. -// -// - **A terminating opcode's `step_end`.** revm's inspected loop runs `step_end` *after* the -// instruction that produced the frame's action, and that action carries its own copy of the gas -// counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail settlement -// measures work against and nothing the caller will ever see, so it must move the settlement -// baseline and must not move the ledger. The two neighbouring windows — a `step_end` in -// mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of that -// rule: the frame resumes on the edited counter in both, so both are booked. -// -// - **A precompile's classification.** A precompile is answered inside the frame init and never -// becomes a child frame, so its recording site is the only place that knows the forwarded -// envelope and the work performed. The split is nonetheless settled at the frame's settlement -// point, from what that site staged, exactly as an ordinary frame's is — because a callback runs -// in between, and the classification is what decides whether the caller reclaims the remainder. -// What that callback may do to the classification is bounded: the journal decision behind a -// result frame init produced was taken before any callback ran and is not reachable from one, so -// a rewrite that moves such a result across the success / revert / halt boundary is refused and -// the settlement reads the classification the EVM produced. The cases below pin the uninspected -// split each precompile arm produces, and the refusal that keeps it the one the settlement sees. -// -// Every case here is checked by the identity `common::finish` runs on every transaction: the -// tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. - -/// Gas the edit-once inspector writes into a live interpreter's counter. -const INJECT: u64 = 1_000; - -/// Gas every probed CALL forwards. Well inside the 63/64 rule at the default transaction gas -/// limit and well inside the default compute budget, so the forwarded envelope is exactly this. -const PROBE_GAS: u64 = 1_000_000; - -/// The transaction gas limit is not what binds any fixture here — pinned at compile time, so a -/// change to the shared limit cannot silently turn a destroyed-remainder case into an -/// out-of-gas one. -const _: () = assert!(DEFAULT_TX_GAS_LIMIT > 10 * PROBE_GAS); - -/// The identity precompile. -const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); -/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. -const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); -/// KZG point evaluation. -const KZG: Address = address!("000000000000000000000000000000000000000a"); - -// --- A: the window a terminating opcode's `step_end` sits in --------------------------------- - -/// Which of the three `step_end` windows an edit is aimed at, told apart by the action the -/// instruction that just ran left behind. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Window { - /// No action yet: the frame carries on, and the edited counter is what it carries on with. - MidFrame, - /// A `NewFrame` action: the frame suspends into a child and then resumes on this counter. - Suspending, - /// A `Return` action: the frame is over, and the gas it hands back was copied into the action - /// before this callback ran. - Terminating, -} - -impl Window { - fn of(interp: &mut Interpreter) -> Self { - match interp.bytecode.action() { - None => Self::MidFrame, - Some(InterpreterAction::NewFrame(_)) => Self::Suspending, - Some(InterpreterAction::Return(_)) => Self::Terminating, - } - } -} - -/// Writes [`INJECT`] into the interpreter's counter once, at the first `step_end` that sits in -/// `window`. -#[derive(Debug)] -struct CounterEditor { - window: Window, - fired: u32, -} - -impl CounterEditor { - fn new(window: Window) -> Self { - Self { window, fired: 0 } - } -} - -impl Inspector for CounterEditor { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || Window::of(interp) != self.window { - return; - } - self.fired += 1; - interp.gas.erase_cost(INJECT); - } -} - -/// `PUSH1 1; POP; STOP` — three opcodes, so a mid-frame `step_end` and a terminating one are both -/// reached, and nothing else happens in between. -fn straight_line_code() -> Bytes { - BytecodeBuilder::default().push_number(1u64).append(POP).append(STOP).build() -} - -/// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame -/// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. -fn suspending_code() -> Bytes { - call_then_stop(IDENTITY, PROBE_GAS) -} - -fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = CounterEditor::new(window); - let edited = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - (plain, edited, inspector.fired) -} - -/// An edit made in the terminating window reaches nobody, so nothing is booked for it — and the -/// transaction is the one the EVM would have produced alone. -/// -/// The action the terminating instruction set already holds its own copy of the counter, so the -/// caller is handed a number this edit never touched. Booking it would tell the conservation law -/// that the transaction spent [`INJECT`] less than it did. -/// -/// `compute_gas` being unmoved is the other half of the rule, and the one that would break if the -/// fix were written as "leave the counter alone" rather than "book nothing for it": the tail -/// settlement measures work as a drop in this very counter, so without the baseline shift the -/// injection would read as [`INJECT`] gas of work the frame never performed. -#[test] -fn test_an_edit_in_the_terminating_window_is_not_booked() { - let (plain, edited, fired) = run_counter_edit(straight_line_code(), Window::Terminating); - - assert_eq!(fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger::default(), - "an edit that cannot reach the envelope must leave the ledger untouched", - ); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "the settlement baseline must absorb the edit, so it counts as no work at all", - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "the envelope must be the one the uninspected run produced", - ); -} - -/// The near boundary: a mid-frame edit is booked, because the frame carries on spending the -/// counter the callback left behind. -#[test] -fn test_an_edit_in_mid_frame_is_still_booked() { - let (_, edited, fired) = run_counter_edit(straight_line_code(), Window::MidFrame); - - assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); - assert_eq!( - edited.inspector_ledger.gas, - Lane::once(i128::from(INJECT)), - "gas written into a counter the frame will keep spending is conjured gas", - ); -} - -/// The far boundary, and the one a coarser rule would get wrong: a `CALL` has set an action too, -/// but it is a `NewFrame` action — the frame suspends, the child runs, and then the frame resumes -/// on exactly this counter. So the edit reaches the envelope and must be booked, even though the -/// interpreter is "at the end of its loop" in precisely the same sense as the terminating case. -#[test] -fn test_an_edit_in_the_suspending_window_is_still_booked() { - let (_, edited, fired) = run_counter_edit(suspending_code(), Window::Suspending); - - assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); - assert_eq!( - edited.inspector_ledger.gas, - Lane::once(i128::from(INJECT)), - "a suspended frame resumes on the edited counter, so the edit reaches the envelope", - ); -} - -// --- B: a precompile's classification, rewritten after its recording site --------------------- - -/// Rewrites the result of the call to `target` into `to`, once. -#[derive(Debug)] -struct Reclassifier { - target: Address, - to: InstructionResult, - fired: u32, -} - -impl Reclassifier { - fn new(target: Address, to: InstructionResult) -> Self { - Self { target, to, fired: 0 } - } -} - -impl Inspector for Reclassifier { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.fired > 0 || inputs.target_address != self.target { - return; - } - self.fired += 1; - outcome.result.result = self.to; - } -} - -/// A `CALL` forwarding [`PROBE_GAS`] gas to `target` with `calldata` at `mem[0..]`, its success -/// flag popped so the caller survives either classification. -fn call_precompile(target: Address, calldata: &[u8]) -> Bytes { - BytecodeBuilder::default() - .mstore(0, calldata) - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(calldata.len() as u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(target) - .push_number(PROBE_GAS) - .append(CALL) - .append(POP) - .append(STOP) - .build() -} - -/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped: 192 bytes -/// with a matching versioned hash, so KZG clears the length doorway and fails inside verification -/// — the one halt shape `MegaETH` prices as work performed. -fn kzg_verification_failure() -> Vec { - let commitment = hex::decode( - "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ - 5f26936857bc3a7c2539ea8ec3a952b7", - ) - .unwrap(); - let mut versioned_hash = Sha256::digest(&commitment).to_vec(); - versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG - let z = - hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); - let y = - hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); - let proof = hex::decode( - "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ - 4faf0070725e00b60ad9a026a15b1a8c", - ) - .unwrap(); - - let mut input = Vec::new(); - input.extend_from_slice(&versioned_hash); - input.extend_from_slice(&z); - input.extend_from_slice(&y); - input.extend_from_slice(&commitment); - input.extend_from_slice(&proof); - assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); - let last = input.len() - 1; - input[last] ^= 0x01; - input -} - -/// Runs the fixture twice: once uninspected, and once with the classification rewritten across -/// the boundary the shim refuses. -/// -/// The refusal is asserted here rather than in each case, so every case below is left stating the -/// one thing that differs between them — which arm of the precompile it reaches, and what the -/// uninspected run's split therefore is. -fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { - let code = call_precompile(target, calldata); - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); - let mut inspector = Reclassifier::new(target, to); - let refusal = - transact_inspected_refused(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); - assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); - assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); - assert!( - refusal.error.contains("classification of a result frame init produced"), - "the transaction must fail with the refusal's own reason, got {}", - refusal.error, - ); - (plain, refusal) -} - -/// A successful precompile rewritten into a halt is refused, and the uninspected run destroys -/// nothing. -/// -/// The rewrite is the direction with state behind it: `make_call_frame` commits the checkpoint -/// before it returns a successful precompile's result, so a caller told the call halted would be -/// told so with the transfer that funded it standing. -#[test] -fn test_rewriting_a_successful_precompile_into_a_halt_is_refused() { - let (plain, _) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); - - assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); - assert_eq!( - plain.compute_gas, - plain.enforced(), - "with nothing destroyed the reported total is the work performed", - ); -} - -/// A rejected precompile rewritten into a success is refused, and the uninspected run destroys the -/// whole envelope. -/// -/// The other direction, and the other half of the split: `blake2f` rejects the input before any -/// work, so `make_call_frame` reverted the checkpoint and nothing was performed. -#[test] -fn test_reviving_a_rejected_precompile_is_refused() { - let (plain, _) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); - - assert_eq!( - plain.destroyed, PROBE_GAS, - "blake2f rejects the input before any work, so the uninspected run destroys all of it", - ); - assert_eq!( - plain.enforced(), - plain.compute_gas - plain.destroyed, - "nothing was performed, so nothing enforces", - ); -} - -/// The third arm, and the only one whose failure `MegaETH` prices as work: a KZG verification that -/// ran and rejected. -/// -/// The refusal matters most here. A halting precompile's gas object carries the whole forwarded -/// envelope as remaining — it is reset rather than spent down — so a caller told such a call -/// succeeded would reclaim all of it, the fixed fee included. That fee is gas the execution priced -/// and the envelope never paid, which is exactly the shape the refusal keeps out. -#[test] -fn test_reviving_a_priced_precompile_failure_is_refused() { - let calldata = kzg_verification_failure(); - let (plain, _) = run_reclassified(KZG, &calldata, InstructionResult::Stop); - - assert_eq!( - plain.destroyed, - PROBE_GAS - kzg_point_evaluation::GAS_COST, - "verification ran, so the uninspected run destroys the envelope less the fixed fee", - ); - assert_eq!( - plain.compute_gas - plain.destroyed, - plain.enforced(), - "the fee is the work performed, and it is what enforces", - ); -} - -// --- C: the pending action itself --------------------------------------------------------------- - -/// Gas an action edit moves, and gas a cancelling pair moves through the result lane's two -/// windows. -const ACTION_DELTA: u64 = 700; - -/// Reaches past the interpreter's gas counter and into the action the interpreter is holding, once. -/// -/// The counter and the action are two different objects at exactly one moment — after a -/// terminating or suspending instruction has run and before the loop hands the action on — and -/// this is the inspector that edits the second one. -#[derive(Debug)] -struct ActionEditor { - window: Window, - /// Positive raises the gas the action carries, negative lowers it. - delta: i64, - /// Fire only on an action whose classification is (or is not) an exceptional halt. - halting: bool, - fired: u32, -} - -impl ActionEditor { - fn raise(window: Window) -> Self { - Self { window, delta: ACTION_DELTA as i64, halting: false, fired: 0 } - } - - fn lower(window: Window) -> Self { - Self { window, delta: -(ACTION_DELTA as i64), halting: false, fired: 0 } - } - - fn on_halt() -> Self { - Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } - } -} - -impl Inspector for ActionEditor { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || Window::of(interp) != self.window { - return; - } - match interp.bytecode.action() { - Some(InterpreterAction::Return(result)) => { - if result.result.is_ok_or_revert() == self.halting { - return; - } - if self.delta >= 0 { - result.gas.erase_cost(self.delta.unsigned_abs()); - } else { - assert!( - result.gas.record_regular_cost(self.delta.unsigned_abs()), - "the fixture must leave the action enough gas for the removal to land", - ); - } - } - Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { - inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); - } - _ => return, - } - self.fired += 1; - } -} - -/// A `CALL` into [`CALLEE`], its result flag popped, then `STOP` — so the first terminating -/// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries -/// is decided by the callee the fixture installs. -fn call_callee_code() -> Bytes { - call_then_stop(CALLEE, PROBE_GAS) -} - -/// Gas written into a returning frame's pending action is gas the caller really reclaims, so it -/// has to be booked — the frame's classification is what says so, and the classification is only -/// known at the frame's settlement point. -#[test] -fn test_raising_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); - let mut inspector = ActionEditor::raise(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - base_db(straight_line_code()), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::once(i128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "an edit to the action a returning frame hands back is an edit to the envelope", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - ACTION_DELTA, - "the transaction really did spend less, which is why the ledger has to carry it", - ); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "the edit is not work: the frame performed exactly what it performed uninspected", - ); -} - -/// The same edit in the other direction. -#[test] -fn test_lowering_a_returning_frames_pending_action_is_booked() { - let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); - let mut inspector = ActionEditor::lower(Window::Terminating); - let edited = transact_inspected( - MegaSpecId::REX7, - base_db(straight_line_code()), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::once(-i128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "gas taken out of the action is gas the caller never gets back", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent + ACTION_DELTA, - "the transaction really did spend more", - ); -} - -/// The classification branch: a halting frame hands nothing back, so an edit to the gas its action -/// carries moves nothing and must not reach the lane's *net* — and the remainder it destroys is -/// the EVM's own number, not the edited one. -/// -/// The lane's gross carries the edit all the same. Whether it moved the envelope is what the -/// classification decides; whether the inspector made it is not, and the block guard asks the -/// second question. -#[test] -fn test_editing_a_halting_frames_pending_action_moves_nothing() { - let callee = BytecodeBuilder::default().append(INVALID).build(); - let plain = - transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); - let mut inspector = ActionEditor::on_halt(); - let edited = transact_inspected( - MegaSpecId::REX7, - db_with_callee(call_callee_code(), callee), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - result: Lane::of(0, u128::from(ACTION_DELTA)), - ..InspectorLedger::default() - }, - "a halting frame hands its remainder to nobody, so the edit moves the envelope by nothing \ - — and the lane still has to show it was made", - ); - assert_eq!( - edited.inspector_ledger.conjured_gas(), - 0, - "the conservation law reads the net, which is what stays zero", - ); - assert!( - !edited.inspector_ledger.is_zero(), - "and the block guard reads the gross, which is what does not", - ); - assert_eq!( - edited.destroyed, plain.destroyed, - "the destroyed remainder is the EVM's own, not the one the inspector wrote", - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "and the envelope is unmoved"); -} - -/// The other action variant: gas written into a pending `NewFrame` action is the envelope a child -/// frame is about to be built with, which the caller was never debited for. -#[test] -fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { - let plain = transact(MegaSpecId::REX7, base_db(suspending_code()), limits()); - let mut inspector = ActionEditor::raise(Window::Suspending); - let edited = - transact_inspected(MegaSpecId::REX7, base_db(suspending_code()), limits(), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { env: Lane::once(i128::from(ACTION_DELTA)), ..InspectorLedger::default() }, - "the child's budget grew by gas the caller's CALL never forwarded", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - ACTION_DELTA, - "the child hands the extra budget straight back, so the transaction spends less", - ); -} - -/// Rewrites the classification inside a pending `Return` action, once, at the terminating -/// `step_end` of the frame that set it. -#[derive(Debug)] -struct ActionReclassifier { - to: InstructionResult, - fired: u32, -} - -impl Inspector for ActionReclassifier { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { return }; - result.result = self.to; - self.fired += 1; - } -} - -/// An edit to a pending action that is not to its gas moves nothing and is booked as an -/// intervention — but it still decides what the frame did, so the frame's state follows it. -/// -/// The action is what `classify_frame_action` builds the frame's result from, so a classification -/// written here is the one the caller sees and the one the journal decision is taken on. Nothing -/// on any gas lane can see that, which is what the intervention counter is for. -#[test] -fn test_rewriting_a_pending_actions_classification_is_an_intervention() { - let callee = - BytecodeBuilder::default().sstore(U256::from(1u64), U256::from(1u64)).append(STOP).build(); - let plain = - transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); - let mut inspector = ActionReclassifier { to: InstructionResult::Revert, fired: 0 }; - let edited = transact_inspected( - MegaSpecId::REX7, - db_with_callee(call_callee_code(), callee), - limits(), - &mut inspector, - ); - - assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); - assert_eq!( - plain.storage_value(CALLEE, U256::from(1u64)), - U256::from(1u64), - "uninspected, the callee's write is committed", - ); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "no gas moved, and the only thing left to say is that the transaction was not left alone", - ); - assert_eq!( - edited.storage_value(CALLEE, U256::from(1u64)), - U256::ZERO, - "a frame the caller was told reverted must leave no write behind", - ); -} - -// === 3. the blind spots ======================================================================= -// -// The rewrite shapes an all-zero ledger used to admit. -// -// The measurement shim's contract is that a transaction an inspector rewrote never reaches a -// block: the canonical path refuses one whose `InspectorLedger` is non-zero, so every rewrite has -// to leave a mark on it. `measured_inspector.rs` and `inspector_cheat_matrix.rs` pin that per -// mechanism and per callback × shape pair. This module pins the shapes that slipped *between* -// those two questions — each one a rewrite the shim was handed, that changes what the transaction -// produces, and that every lane read as nothing: -// -// - a frame's memory grown for free, by moving the interpreter's memory and the memo of how far it -// has been paid for in the same step, so that neither goes out of bounds and the next expanding -// opcode charges nothing; -// - a `CallOutcome` / `CreateOutcome` metadata field — where the callee's return data lands, and -// which address a creation reports — rewritten without touching the `InterpreterResult` inside -// it, which is the only part the rewrite comparison used to read; -// - two edits to the *same* signed lane in opposite directions, which a net-only reading cancels to -// zero; -// - the same cancellation spread across two frames, where only one of the two survives to the -// receipt, so the net is zero and the effect is not; -// - an instruction deleted from a frame, by stepping the program counter past it, so the work is -// never performed and there is nothing for any counter to meter; -// - a return buffer put in front of a frame that made no call, so `RETURNDATASIZE` reads a length -// no call produced. -// -// Four of them are booked on `InspectorLedger::interventions`, from readings the shim did not use -// to take; the cancelling pair are what the per-lane gross activity counters exist for. Every test -// here asserts the ledger the shim books *and* the effect the rewrite had, because a shape that no -// longer changes anything is a shape that stopped testing the guard.//! -// The last two are also why the snapshot the first shape needed is now a *rule* rather than a -// list. A snapshot of four chosen readings caught the memory pair and let the program counter -// through, because `Interpreter::bytecode` was not among the things anyone had thought to name. -// What the shim takes now is every constant-time reading of the interpreter, and what pins that is -// the `Interpreter` row of `gas_surface.rs`'s closed table. - -/// A second callee, whose frame reverts. -const REVERTER: Address = EMPTY_TARGET; - -/// The address a rewritten `CreateOutcome` reports instead of the one the code was deployed at. -const FAKE_DEPLOYMENT: Address = address!("00000000000000000000000000000000000f00d0"); - -/// Slot the fixtures write their observable result to. -const RESULT_SLOT: u64 = 0x11; - -/// Refund a cancelling pair of refund edits moves, in each direction. -/// -/// Small enough to stay well under the EIP-3529 cap on every fixture here, so that what survives -/// to the receipt is the whole of the surviving half rather than whatever the cap left of it. -const REFUND: i64 = 2_000; - -/// The mainnet memory expansion cost of a memory `words` words long. -const fn memory_cost(words: u64) -> u64 { - 3 * words + words * words / 512 -} - -// --- a frame's memory, grown for free ------------------------------------------------------------ - -/// How far the free-expansion inspector grows the frame's memory, in words. -/// -/// The fixture's own `MSTORE` lands inside it, so the expansion the EVM would have charged for is -/// exactly the one the inspector already did for nothing. -const STOLEN_WORDS: u64 = 129; - -/// Grows the frame's memory and tells the EVM it is already paid for. -/// -/// Both halves are needed and neither is a rewrite on its own. Moving the memory alone leaves the -/// memo behind, and the next expanding opcode charges for an expansion that already happened; -/// moving the memo alone leaves the memory behind, and the EVM reads out of bounds. Moving both -/// keeps every invariant the interpreter has and skips the charge, which is why the pair was the -/// hole and neither half was. -#[derive(Default)] -struct FreeExpansion { - fired: u32, -} - -impl Inspector for FreeExpansion { - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != MSTORE { - return; - } - let words = STOLEN_WORDS as usize; - assert!(interp.memory.resize(words * 32), "the fixture must allow the memory to be grown",); - // Priced through revm's own table, so the memo is exactly what the EVM would have written - // had the frame paid; the assertion below restates the formula independently, which is - // what makes the two a check rather than one number written twice. - let cost = context.cfg().gas_params().memory_cost(words); - interp.gas.memory_mut().set_words_num(words, cost); - self.fired += 1; - } -} - -/// ★ A frame whose memory was grown for free is not an all-zero ledger. -/// -/// The rewrite reaches through no argument the shim used to compare: the interpreter's gas counter -/// is untouched, no action is pending, no frame input and no frame result exists yet. What it -/// moves is the interpreter's memory and the memo beside it, and the transaction then pays less -/// than it would have — which is the one thing the guard exists to keep out of a block. -#[test] -fn test_a_frame_whose_memory_was_grown_for_free_is_booked() { - // MSTORE(offset = STOLEN_WORDS * 32 - 32, value = 0xAA), which expands memory to exactly the - // size the inspector already grew it to. - let offset = (STOLEN_WORDS - 1) * 32; - let code = BytecodeBuilder::default() - .push_number(0xAAu64) - .push_number(offset) - .append(MSTORE) - .append(STOP) - .build(); - - let mut inspector = FreeExpansion::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the expanding opcode exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.total_gas_spent - cheated.total_gas_spent, - memory_cost(STOLEN_WORDS), - "the expansion the inspector performed is the charge the EVM then skipped", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction that paid less because an inspector moved its memory must not read as \ - untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- a call outcome's metadata ------------------------------------------------------------------- - -/// Where the fixture's `CALL` asks for its return data, and where the inspector moves it to. -const RETURN_AT: usize = 0; -const MOVED_TO: usize = 32; - -/// Moves a finished call's return data somewhere else in the caller's memory. -/// -/// The `InterpreterResult` inside the outcome — its classification, its output bytes, its gas — -/// comes back exactly as the EVM produced it. Only the range the caller will copy the output into -/// changes, which is not a field the result carries. -#[derive(Default)] -struct MoveReturnData { - fired: u32, -} - -impl Inspector for MoveReturnData { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address != CALLEE || self.fired > 0 { - return; - } - outcome.memory_offset = MOVED_TO..MOVED_TO + 32; - self.fired += 1; - } -} - -/// ★ A call outcome whose return range was moved is not an all-zero ledger. -#[test] -fn test_a_moved_return_range_is_booked() { - // Size the caller's memory to two words, call the callee for one word of output at offset 0, - // then store what landed there. - let code = BytecodeBuilder::default() - .push_number(0u64) - .push_number(32u64) - .append(MSTORE) - .push_number(32u64) // retSize - .push_number(u64::try_from(RETURN_AT).unwrap()) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .push_number(u64::try_from(RETURN_AT).unwrap()) - .append(MLOAD) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - // The callee returns one word of 0x11s. - let callee = BytecodeBuilder::default() - .push_u256(U256::from(0x11u64)) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = MoveReturnData::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(0x11u64), - "without the rewrite the return data lands where the caller asked for it", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it, the caller reads a word the callee never wrote", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a rewritten return range changed must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -// --- a frame's returned output ------------------------------------------------------------------ - -/// The word a rewritten output buffer feeds the caller instead of the one the callee returned. -const FORGED_OUTPUT: u64 = 0xdead; - -/// Replaces the output buffer a finished call hands back, leaving its classification alone. -/// -/// The classification and the remaining gas are what every other lane reads. The output is -/// neither, and it is what the caller copies into its own memory. -#[derive(Default)] -struct ForgeCallOutput { - fired: u32, -} - -impl Inspector for ForgeCallOutput { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address != CALLEE || self.fired > 0 { - return; - } - outcome.result.output = Bytes::from(U256::from(FORGED_OUTPUT).to_be_bytes::<32>().to_vec()); - self.fired += 1; - } -} - -/// ★ A call outcome whose returned output was replaced is not an all-zero ledger. -#[test] -fn test_a_forged_call_output_is_booked() { - // Call the callee for one word of output, then store what landed there. - let code = BytecodeBuilder::default() - .push_number(32u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .push_number(0u64) - .append(MLOAD) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - // The callee returns one word of 0x11s. - let callee = BytecodeBuilder::default() - .push_u256(U256::from(0x11u64)) - .push_number(0u64) - .append(MSTORE) - .push_number(32u64) - .push_number(0u64) - .append(RETURN) - .build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = ForgeCallOutput::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `call_end` for the callee once"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(0x11u64), - "without the rewrite the caller reads what the callee returned", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(FORGED_OUTPUT), - "with it, the caller reads a word no frame produced", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a replaced output buffer changed must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -/// Reports a different address than the one the creation deployed to. -#[derive(Default)] -struct MoveDeploymentAddress { - fired: u32, -} - -impl Inspector for MoveDeploymentAddress { - fn create_end( - &mut self, - _context: &mut CTX, - _inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - if self.fired > 0 || outcome.address.is_none() { - return; - } - outcome.address = Some(FAKE_DEPLOYMENT); - self.fired += 1; - } -} - -/// ★ A creation outcome whose reported address was rewritten is not an all-zero ledger. -/// -/// The code is still deployed where the EVM put it; only the address the caller's stack receives -/// changes, so the caller goes on to talk to an account that holds nothing. -#[test] -fn test_a_rewritten_deployment_address_is_booked() { - // Init code that returns two bytes of runtime code. - let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) - .push_number(0u64) - .push_number(0u64) - .append(CREATE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = MoveDeploymentAddress::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach `create_end` once"); - let deployed = plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)); - assert_ne!(deployed, U256::ZERO, "the fixture's CREATE must succeed"); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(FAKE_DEPLOYMENT.as_slice()), - "the caller must have been handed the address the inspector wrote", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction told a contract lives somewhere it does not must not read as untouched: \ - {:?}", - cheated.inspector_ledger, - ); -} - -// --- a construction frame's pending action ------------------------------------------------------- - -/// Drains the gas a construction frame's pending `Return` action carries. -/// -/// The contract this module's other cases rest on — that an action is the frame's result a moment -/// later, so an edit to it settles with that result — does not hold for a creation. Between the -/// two, `classify_frame_action` charges the code deposit out of the gas *this action* carries, and -/// a creation that cannot pay it becomes an `OutOfGas` that deploys nothing. So this edit changes -/// what the transaction produces, and it does it by a route that leaves the classification and the -/// output the boundary compares exactly where they were. -#[derive(Default)] -struct DrainConstructionAction { - fired: u32, -} - -impl Inspector for DrainConstructionAction { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - // A construction frame runs no deployed code, so it has no bytecode address. - if self.fired > 0 || interp.input.bytecode_address().is_some() { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { - return; - }; - if !result.result.is_ok() { - return; - } - let remaining = result.gas.remaining(); - assert!( - result.gas.record_regular_cost(remaining), - "the fixture must be able to drain the action it found", - ); - self.fired += 1; - } -} - -/// ★ A construction frame whose pending action was drained is not an all-zero ledger. -/// -/// Every lane the boundary reads stays put: the action's classification and output are untouched, -/// so nothing is an intervention; the gas edit is staged for the frame's settlement point, and -/// that point declines to book it because the result it finally sees is a swallowed one. The -/// deposit the drained action could no longer pay is what turned it into one. -#[test] -fn test_a_drained_construction_action_is_booked() { - // Init code that returns two bytes of runtime code. - let init: [u8; 11] = [0x60, 0x00, 0x60, 0x00, 0x52, 0x60, 0x02, 0x60, 0x1e, 0xf3, 0x00]; - let mut builder = BytecodeBuilder::default(); - for (offset, byte) in init.iter().enumerate() { - builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); - } - let code = builder - .push_number(init.len() as u64) - .push_number(0u64) - .push_number(0u64) - .append(CREATE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = DrainConstructionAction::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the construction frame's step_end once"); - assert_ne!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "without the edit the creation must succeed", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it the creation cannot pay its code deposit and deploys nothing", - ); - assert_ne!( - plain.gas_used, cheated.gas_used, - "and the receipt the sender is billed on moves with it", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose contract an inspector deleted must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -/// Raises the gas an inner call frame's pending `Return` action carries, then takes the same -/// amount back out of the result that action became. -/// -/// The two windows are one lane and one frame, and the pair nets to zero. They are still two -/// edits, made in two different callbacks, and the lane's traffic is what says so — the sum alone -/// reads as an inspector that did nothing. -#[derive(Default)] -struct CancellingActionAndResultEdits { - raised: u32, - lowered: u32, -} - -impl Inspector for CancellingActionAndResultEdits { - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.raised > 0 || interp.input.bytecode_address() != Some(&CALLEE) { - return; - } - let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { - return; - }; - if !result.result.is_ok() { - return; - } - result.gas.erase_cost(ACTION_DELTA); - self.raised += 1; - } - - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.lowered > 0 || inputs.target_address != CALLEE { - return; - } - assert!( - outcome.result.gas.record_regular_cost(ACTION_DELTA), - "the fixture must leave the result enough gas for the removal to land", - ); - self.lowered += 1; - } -} - -/// ★ An edit staged at one callback and undone at the next is two edits, not none. -/// -/// Nothing about this transaction changes: a call frame's remaining gas is read by nobody between -/// the two windows, so the pair really is invisible in what the transaction produces. That is the -/// point — the lane's traffic is the only thing that separates it from an inspector that never -/// ran, and on a *creation* frame the same pair is the shape that deletes a contract. -#[test] -fn test_cancelling_action_and_result_edits_are_booked() { - let code = BytecodeBuilder::default() - .push_number(0u64) // retSize - .push_number(0u64) // retOffset - .push_number(0u64) // argsSize - .push_number(0u64) // argsOffset - .push_number(0u64) // value - .push_address(CALLEE) - .push_number(100_000u64) - .append(CALL) - .append(POP) - .append(STOP) - .build(); - let callee = BytecodeBuilder::default().append(STOP).build(); - let db = || base_db(code.clone()).account_code(CALLEE, callee.clone()); - - let mut inspector = CancellingActionAndResultEdits::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!((inspector.raised, inspector.lowered), (1, 1), "both windows must be reached"); - assert_eq!( - cheated.gas_used, plain.gas_used, - "the pair cancels, so the receipt really is the one the EVM would have produced", - ); - assert_eq!( - cheated.inspector_ledger.conjured_gas(), - 0, - "and the conservation law must read the net, which is zero", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "but the guard must still see that the lane carried two edits: {:?}", - cheated.inspector_ledger, - ); - assert_eq!( - cheated.inspector_ledger.result.gross(), - 2 * u128::from(ACTION_DELTA), - "one edit in each window, counted where each was made", - ); -} - -// --- two edits to one lane, in opposite directions ----------------------------------------------- - -/// Injects one gas before the frame reads its own remaining gas, and takes it back afterwards. -/// -/// Both edits land on the interpreter counter, which is one signed lane. Their net is zero and -/// the transaction's envelope is unmoved — and in between them the frame read a number one higher -/// than the EVM would have given it, and wrote that number to storage. -#[derive(Default)] -struct CancellingCounterEdits { - /// 0 before the injection, 1 between the two edits, 2 once both have landed. - phase: u8, -} - -impl Inspector for CancellingCounterEdits { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - match self.phase { - 0 if interp.bytecode.opcode() == GAS => { - interp.gas.erase_cost(1); - self.phase = 1; - } - 1 => { - assert!(interp.gas.record_regular_cost(1), "the frame must afford the give-back"); - self.phase = 2; - } - _ => {} - } - } -} - -/// ★ Two edits to the same lane that cancel are not an all-zero ledger. -/// -/// The net of the gas lane really is zero — the transaction spent exactly what it would have — so -/// nothing the conservation law reads has moved. What moved is the number the frame read in -/// between, and a guard that asks the net cannot see it. The gross activity counter is what does. -#[test] -fn test_cancelling_counter_edits_are_booked() { - let code = BytecodeBuilder::default() - .append(GAS) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - // No compute-gas limit, so the REX7 gas clamp hides nothing and the frame's own reading of - // its remaining gas is the counter the injection moved. - let limits = EvmTxRuntimeLimits::no_limits(); - let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits); - let mut inspector = CancellingCounterEdits::default(); - let cheated = transact_inspected(MegaSpecId::REX7, base_db(code), limits, &mut inspector); - - assert_eq!(inspector.phase, 2, "both halves of the cancellation must have landed"); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)) + U256::from(1), - "the frame must have read one gas more than the EVM would have given it", - ); - assert_eq!( - cheated.total_gas_spent, plain.total_gas_spent, - "the two edits cancel, so the envelope the receipt reports is unmoved", - ); - assert_eq!( - cheated.inspector_conjured_gas(), - 0, - "and so is the law's term: this is exactly the shape a net-only reading cannot see", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "but the transaction was rewritten, and the guard has to see that: {:?}", - cheated.inspector_ledger, - ); -} - -/// Adds a refund to one child frame's result and takes the same amount out of another's. -/// -/// The frame that gets the addition returns, so its refund reaches the receipt. The frame that -/// gets the subtraction reverts, so revm discards its whole refund counter — the subtraction never -/// reaches anything. Net zero on the lane, one refund's worth of difference on the receipt. -#[derive(Default)] -struct CancellingRefundsAcrossFrames { - added: u32, - removed: u32, -} - -impl Inspector for CancellingRefundsAcrossFrames { - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if inputs.target_address == CALLEE && self.added == 0 { - outcome.result.gas.record_refund(REFUND); - self.added += 1; - } else if inputs.target_address == REVERTER && self.removed == 0 { - assert!( - outcome.result.gas.refunded() >= REFUND, - "the reverting callee must hold a refund of its own to take from, got {}", - outcome.result.gas.refunded(), - ); - outcome.result.gas.record_refund(-REFUND); - self.removed += 1; - } - } -} - -/// ★ A cancellation split across a surviving frame and a discarded one is not an all-zero ledger. -/// -/// This is the previous shape with the asymmetry made explicit: the two halves are equal and -/// opposite where the ledger books them, and only one of them is still standing by the time the -/// receipt is built. -#[test] -fn test_cancelling_refunds_across_frames_are_booked() { - let call_to = |builder: BytecodeBuilder, target: Address| { - builder - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_number(0u64) - .push_address(target) - .push_number(200_000u64) - .append(CALL) - .append(POP) - }; - let code = call_to(call_to(BytecodeBuilder::default(), CALLEE), REVERTER).append(STOP).build(); - // Both callees set a slot and clear it again, so each ends holding a refund the EVM produced. - let clearing = |builder: BytecodeBuilder| { - builder - .sstore(U256::from(RESULT_SLOT), U256::from(1u64)) - .sstore(U256::from(RESULT_SLOT), U256::ZERO) - }; - let returning = clearing(BytecodeBuilder::default()).append(STOP).build(); - let reverting = clearing(BytecodeBuilder::default()).revert().build(); - let db = || { - base_db(code.clone()) - .account_code(CALLEE, returning.clone()) - .account_code(REVERTER, reverting.clone()) - }; - - let mut inspector = CancellingRefundsAcrossFrames::default(); - let (plain, cheated) = plain_and_cheated(db, &mut inspector); - - assert_eq!((inspector.added, inspector.removed), (1, 1), "both halves must have landed"); - assert!( - plain.total_gas_spent >= 5 * u64::try_from(REFUND).unwrap(), - "the fixture must burn enough that the EIP-3529 cap does not hide the difference", - ); - assert_eq!( - plain.gas_used - cheated.gas_used, - u64::try_from(REFUND).unwrap(), - "only the surviving frame's half reaches the receipt, so the sender pays that much less", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a receipt an inspector moved must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- an opcode skipped, and a return buffer conjured -// ---------------------------------------------- - -/// What the fixture's `SSTORE` writes when it runs. -const STORED: u64 = 0x99; - -/// The gas a cold `SSTORE` into a zero slot costs, which is what skipping it saves. -const COLD_SSTORE_SET: u64 = 22_100; - -/// How many bytes of return data the forging inspector conjures. -/// -/// Non-zero and a whole number of words, so that the `SSTORE` that stores it turns a zero slot -/// into a non-zero one — which is a different charge as well as a different value. -const CONJURED_RETURN_DATA: u64 = 96; - -/// Advances the program counter past the frame's `SSTORE`, so the EVM never executes it. -/// -/// revm's inspected loop runs this callback *before* the instruction, and the interpreter reads -/// the opcode it is about to execute from the very pointer this moves. Stepping the pointer on by -/// one byte therefore deletes one instruction from the frame: the two operands the `SSTORE` would -/// have consumed stay on the stack, the `STOP` after it runs instead, and the frame ends where it -/// was going to end. -/// -/// Nothing about this reaches a gas counter. The work is not performed, so there is nothing for -/// the EVM to meter and nothing for a gas lane to see. -#[derive(Default)] -struct SkipTheStore { - fired: u32, -} - -impl Inspector for SkipTheStore { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != SSTORE { - return; - } - interp.bytecode.relative_jump(1); - self.fired += 1; - } -} - -/// ★ A frame with an opcode skipped out from under it is not an all-zero ledger. -/// -/// The rewrite is the free-expansion shape's twin and is strictly worse: it does not merely make -/// the frame's next charge cheaper, it deletes an instruction from the frame. The transaction ends -/// with different storage *and* a smaller bill, and every gas lane reads zero because the gas that -/// went missing was never spent by anybody. -#[test] -fn test_a_skipped_opcode_is_booked() { - let code = BytecodeBuilder::default() - .sstore(U256::from(RESULT_SLOT), U256::from(STORED)) - .append(STOP) - .build(); - - let mut inspector = SkipTheStore::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the store exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(STORED), - "without the rewrite the frame stores what its bytecode says", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "with it, the store never happens", - ); - assert_eq!( - plain.total_gas_spent - cheated.total_gas_spent, - COLD_SSTORE_SET, - "the deleted instruction is the charge the transaction then did not pay", - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction an inspector deleted an instruction from must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -/// Puts return data in front of a frame that has made no call. -/// -/// `RETURNDATASIZE` reads the buffer's length, so the frame goes on to store a number no call -/// produced. The buffer is the interpreter's own, reachable through `ReturnData` on any live -/// interpreter, and its length is a constant-time reading exactly like the memory's size. -#[derive(Default)] -struct ForgeReturnData { - fired: u32, -} - -impl Inspector for ForgeReturnData { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || interp.bytecode.opcode() != RETURNDATASIZE { - return; - } - interp.return_data.set_buffer(Bytes::from(vec![0u8; CONJURED_RETURN_DATA as usize])); - self.fired += 1; - } -} - -/// ★ A frame handed return data it never received is not an all-zero ledger. -/// -/// The frame made no call, so the EVM's own buffer is empty and the store is a zero-to-zero -/// no-op. With the rewrite the same store turns a zero slot into a non-zero one, which changes the -/// post-state and costs the transaction more — in the opposite direction to every other shape -/// here, and just as invisible to a lane that only watches gas counters. -#[test] -fn test_a_forged_return_buffer_is_booked() { - let code = BytecodeBuilder::default() - .append(RETURNDATASIZE) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = ForgeReturnData::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!(inspector.fired, 1, "the fixture must reach the read exactly once"); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::ZERO, - "a frame that made no call has no return data", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from(CONJURED_RETURN_DATA), - "with the rewrite it reads the length of a buffer no call produced", - ); - assert!( - cheated.total_gas_spent > plain.total_gas_spent, - "and pays for the non-zero store the rewrite turned it into: {} vs {}", - cheated.total_gas_spent, - plain.total_gas_spent, - ); - assert!( - !cheated.inspector_ledger.is_zero(), - "a transaction whose state a forged buffer changed must not read as untouched: {:?}", - cheated.inspector_ledger, - ); -} - -// --- a frame invariant moved and moved back -------------------------------------------------- - -/// The caller the rewriting inspector shows the frame instead of the one that called it. -const IMPOSTOR: Address = address!("00000000000000000000000000000000000ca11e"); - -/// Moves the frame's caller for the length of one instruction, and puts it back. -/// -/// `CALLER` reads `input.caller_address`, so the frame pushes an address nobody called it from and -/// goes on to store that. The rewrite is undone in the very next callback, which is what makes the -/// shape worth pinning: the frame's identity is the one the EVM gave it at every point a *frame* -/// could be inspected — at its start, at its end, and at every callback but the two this touches. -/// -/// Nothing about it reaches a gas counter. Both runs execute the same instructions and pay the -/// same cold `SSTORE`; only the value written differs. -#[derive(Default)] -struct BorrowTheCaller { - /// The caller the EVM gave the frame, kept so it can be handed back. - original: Option
, - /// How many times each half of the rewrite ran. - moved: u32, - restored: u32, -} - -impl Inspector for BorrowTheCaller { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.moved > 0 || interp.bytecode.opcode() != CALLER { - return; - } - self.original = Some(interp.input.caller_address); - interp.input.caller_address = IMPOSTOR; - self.moved += 1; - } - - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - let Some(original) = self.original.filter(|_| self.restored == 0) else { - return; - }; - interp.input.caller_address = original; - self.restored += 1; - } -} - -/// ★ A frame invariant moved in `step` and moved back in `step_end` is not an all-zero ledger. -/// -/// The four addresses and the value a frame is identified by cannot change while it runs, which -/// makes them the readings a cheaper shim would be tempted to compare once per frame rather than -/// once per callback. This is the shape that answers that: an inspector borrows one of them for -/// exactly as long as it takes the frame to read it, and gives it back before anything outside the -/// two callbacks could look. A per-frame comparison sees the address it started with; a per-opcode -/// one sees it move twice. -#[test] -fn test_a_frame_invariant_moved_and_moved_back_is_booked() { - let code = BytecodeBuilder::default() - .append(CALLER) - .push_number(RESULT_SLOT) - .append(SSTORE) - .append(STOP) - .build(); - - let mut inspector = BorrowTheCaller::default(); - let (plain, cheated) = plain_and_cheated(|| base_db(code.clone()), &mut inspector); - - assert_eq!((inspector.moved, inspector.restored), (1, 1), "both halves must run once"); - assert_eq!( - inspector.original, - Some(crate::common::CALLER), - "and the half that gives the address back must have the one the EVM gave the frame", - ); - assert!(plain.is_success() && cheated.is_success(), "both runs must succeed"); - assert_eq!( - plain.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(crate::common::CALLER.as_slice()), - "without the rewrite the frame stores the address that called it", - ); - assert_eq!( - cheated.storage_value(CONTRACT, U256::from(RESULT_SLOT)), - U256::from_be_slice(IMPOSTOR.as_slice()), - "with it, the frame stores one nobody called it from", - ); - assert_eq!( - plain.total_gas_spent, cheated.total_gas_spent, - "the two runs cost the same, so no gas lane can tell them apart", - ); - assert!( - cheated.inspector_ledger.interventions >= 2, - "each half of the rewrite is a rewrite: {:?}", - cheated.inspector_ledger, - ); -} - -// === 4. the receipt's other two numbers ======================================================= -// -// The two numbers on a receipt that the conservation law cannot see, and the lanes that do. -// -// The law is stated over `total_gas_spent`, which is `limit - remaining`. A transaction's receipt -// carries two more figures that arithmetic does not reach: the EIP-3529 refund, which decides what -// the sender actually pays, and the EIP-8037 state-gas dimension — a `Gas`'s `reservoir` and its -// `state_gas_spent` counter — which decides how much of the envelope the receipt counts as spent -// at all. -// -// Both are reachable from every callback that is handed a `Gas`, and both were unmeasured. The -// shapes here are what the two lanes now book, and each pins the *reason* its lane is measured -// where it is: -// -// - a **refund** is a quantity the EVM also produces, so only a difference across a callback -// isolates the inspector's share — the lane is measured at the boundary, and is nominal in both -// the senses that can make it differ from what reaches the receipt (the EIP-3529 cap, and the -// chain of successful frame returns an edit has to survive); -// - a **reservoir** is a quantity `MegaETH` never produces at all, and one revm propagates by -// replacement rather than by accumulation, so a boundary difference would book edits the EVM goes -// on to erase. The lane is settled once, from the number the transaction ends with, which is -// exactly the surviving part and is the inspector's in whole. - -/// Gas the fixture's inner `CALL` forwards. -const INNER_CALL_GAS: u64 = 200_000; - -/// A refund large enough that the cap keeps part of it out of the receipt. -const OVERSIZED_REFUND: i64 = 60_000; -/// The EIP-8037 pool an edit fills. -const RESERVOIR: u64 = 10_000; -/// The EIP-8037 spend an edit writes. -const STATE_GAS: i64 = 5_000; - -/// Slot the top frame writes. -const TOP_SLOT: u64 = 0x10; -/// Slot the callee writes. -const CALLEE_SLOT: u64 = 0x20; -/// Slot the callee sets and clears, so the frame ends holding a refund of the EVM's own making. -const CLEARED_SLOT: u64 = 0x30; - -// --- the fixture ------------------------------------------------------------------------------- - -/// How the fixture's callee ends, which is what decides whether its refund travels. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Callee { - /// Writes storage, produces a refund by clearing a slot it just set, and returns. - Returning, - /// Writes storage and reverts, so the EVM discards everything the frame held. - Reverting, -} - -fn caller_code() -> Bytes { - append_call(BytecodeBuilder::default(), CALLEE, INNER_CALL_GAS, 0) - .append(POP) - .sstore(U256::from(TOP_SLOT), U256::from(1u64)) - .append(STOP) - .build() -} - -fn callee_code(callee: Callee) -> Bytes { - let builder = BytecodeBuilder::default() - .sstore(U256::from(CALLEE_SLOT), U256::from(1u64)) - // Set and clear, so the frame ends holding a refund the EVM itself produced. - .sstore(U256::from(CLEARED_SLOT), U256::from(1u64)) - .sstore(U256::from(CLEARED_SLOT), U256::ZERO); - match callee { - Callee::Returning => builder.append(STOP).build(), - Callee::Reverting => builder.revert().build(), - } -} - -fn db_for(callee: Callee) -> MemoryDatabase { - db_with_callee(caller_code(), callee_code(callee)) -} - -// --- the edit ---------------------------------------------------------------------------------- - -/// One edit, applied once, to one of the `Gas` objects a callback is handed. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Edit { - /// Add to the running interpreter's refund counter. - RefundAtStep(i64), - /// Add to the finished inner call's refund counter. - RefundAtCallEnd(i64), - /// Fill the running interpreter's EIP-8037 pool. - ReservoirAtStep, - /// Fill it at the one moment the frame is holding a `NewFrame` action, whose child overwrites - /// the pool on the way back. - ReservoirAtSuspension, - /// Fill the pool the inner call's inputs seed the child frame with. - ReservoirOnInputs, - /// Fill the finished inner call's pool. - ReservoirAtCallEnd, - /// Write the running interpreter's EIP-8037 spend counter. - StateGasAtStep, - /// Write the finished inner call's spend counter. - StateGasAtCallEnd, - /// Answer the inner call with a synthetic outcome that echoes the envelope and carries - /// neither figure — the control the two below are read against. - InterceptEcho, - /// The same, carrying a refund the frame never earned. - InterceptWithRefund, - /// The same, carrying an EIP-8037 pool. - InterceptWithReservoir, -} - -impl Edit { - /// Whether this edit answers the frame itself instead of letting the EVM build it. - const fn intercepts(self) -> bool { - matches!( - self, - Self::InterceptEcho | Self::InterceptWithRefund | Self::InterceptWithReservoir - ) - } -} - -/// Applies one [`Edit`], once, and records that it landed. -#[derive(Debug)] -struct Editor { - edit: Edit, - fired: u32, - steps: u64, -} - -impl Editor { - const fn new(edit: Edit) -> Self { - Self { edit, fired: 0, steps: 0 } - } -} - -impl Inspector for Editor { - fn step(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - self.steps += 1; - if self.fired > 0 || self.steps != 4 { - return; - } - match self.edit { - Edit::RefundAtStep(amount) => interp.gas.record_refund(amount), - Edit::ReservoirAtStep => interp.gas.set_reservoir(RESERVOIR), - Edit::StateGasAtStep => interp.gas.set_state_gas_spent(STATE_GAS), - _ => return, - } - self.fired += 1; - } - - fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { - if self.fired > 0 || self.edit != Edit::ReservoirAtSuspension { - return; - } - // The one window where the pool the frame holds is not the pool that travels: the child - // this action builds was already sized from the pre-edit value, and its own pool - // overwrites this one when it returns. - if !matches!(interp.bytecode.action(), Some(InterpreterAction::NewFrame(_))) { - return; - } - interp.gas.set_reservoir(RESERVOIR); - self.fired += 1; - } - - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if self.fired > 0 || inputs.target_address != CALLEE { - return None; - } - if self.edit == Edit::ReservoirOnInputs { - inputs.reservoir += RESERVOIR; - self.fired += 1; - return None; - } - if !self.edit.intercepts() { - return None; - } - // The echo convention every tool that intercepts follows: hand back exactly what was - // forwarded, so the gas lanes see nothing and only the figures under test move. - let mut gas = Gas::new(inputs.gas_limit); - match self.edit { - Edit::InterceptWithRefund => gas.record_refund(REFUND), - Edit::InterceptWithReservoir => gas.set_reservoir(RESERVOIR), - _ => {} - } - self.fired += 1; - Some(CallOutcome::new( - InterpreterResult::new(InstructionResult::Stop, Bytes::new(), gas), - inputs.return_memory_offset.clone(), - )) - } - - fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - if self.fired > 0 || inputs.target_address != CALLEE { - return; - } - match self.edit { - Edit::RefundAtCallEnd(amount) => outcome.result.gas.record_refund(amount), - Edit::ReservoirAtCallEnd => outcome.result.gas.set_reservoir(RESERVOIR), - Edit::StateGasAtCallEnd => outcome.result.gas.set_state_gas_spent(STATE_GAS), - _ => return, - } - self.fired += 1; - } -} - -/// Runs the fixture with no inspector at all. -fn transact_plain(callee: Callee) -> Outcome { - transact(MegaSpecId::REX7, db_for(callee), limits()) -} - -/// Runs it with one edit applied, asserting the edit landed exactly once. -fn transact_edited(callee: Callee, edit: Edit) -> Outcome { - let mut editor = Editor::new(edit); - let outcome = transact_inspected(MegaSpecId::REX7, db_for(callee), limits(), &mut editor); - assert_eq!( - editor.fired, 1, - "{edit:?}: the fixture must reach the edit's callback exactly once", - ); - outcome -} - -// --- the fixture's own assumptions --------------------------------------------------------------- - -/// The uninspected run is what the cells below assume it is: it succeeds, it produces a refund of -/// its own, and it reports no EIP-8037 dimension at all. -#[test] -fn test_the_fixture_refunds_on_its_own_and_holds_no_state_gas() { - let plain = transact_plain(Callee::Returning); - assert!(plain.result.is_success(), "{:?}", plain.result); - assert!( - plain.refunded() > 0, - "the callee's cleared slot must leave a refund for the lowering cell to take from", - ); - assert_eq!( - plain.gas_used, - plain.total_gas_spent - plain.refunded(), - "the receipt's two gas numbers differ by exactly the refund", - ); - assert_eq!(plain.state_gas_spent(), 0, "EIP-8037 is off on every MegaETH path"); - assert!(plain.inspector_ledger.is_zero(), "no inspector ran: {:?}", plain.inspector_ledger); -} - -// --- the refund lane -// ------------------------------------------------------------------------------ - -/// A refund written into a running interpreter's counter is booked, and moves what the sender pays -/// without moving the envelope. -#[test] -fn test_a_refund_written_into_a_live_interpreter_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - "the shim must book the refund and nothing else", - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "a refund does not move the envelope, which is why the law cannot see it", - ); - assert_eq!( - edited.refunded(), - plain.refunded() + u64::try_from(REFUND).unwrap(), - "but it does move the receipt's refund", - ); - assert_eq!( - edited.gas_used, - plain.gas_used - u64::try_from(REFUND).unwrap(), - "and through it what the sender pays", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, 0, - "the refund lane is deliberately not a term of the law", - ); - assert!(!edited.inspector_ledger.is_zero(), "and the block guard has to see it"); -} - -/// The same edit made at the last callback that holds the finished frame's result. -#[test] -fn test_a_refund_written_into_a_finished_frame_result_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - ); - assert_eq!(edited.refunded(), plain.refunded() + u64::try_from(REFUND).unwrap()); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent); -} - -/// A refund taken *out* is booked with the sign that says so — a lane that only saw one direction -/// would report an inspector that raised the sender's bill as having done nothing. -#[test] -fn test_a_refund_taken_out_of_a_frame_is_booked_with_the_sign_that_says_so() { - let plain = transact_plain(Callee::Returning); - assert!( - plain.refunded() >= u64::try_from(REFUND).unwrap(), - "fixture check: there must be a refund to take from, got {}", - plain.refunded(), - ); - - let edited = transact_edited(Callee::Returning, Edit::RefundAtCallEnd(-REFUND)); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(-i128::from(REFUND)), ..InspectorLedger::default() }, - ); - assert_eq!(edited.refunded(), plain.refunded() - u64::try_from(REFUND).unwrap()); - assert_eq!( - edited.gas_used, - plain.gas_used + u64::try_from(REFUND).unwrap(), - "the sender pays more, by exactly what was taken", - ); -} - -/// The lane reports what the inspector wrote, not what the EIP-3529 cap let through. -/// -/// The cap applies to the transaction's whole refund at once, over a sum in which the EVM's own -/// refunds and an inspector's are indistinguishable, at a point past every callback. Splitting it -/// between them needs a priority rule the protocol does not have, so the lane states the edit and -/// the receipt states the effect — and the two are allowed to differ. -#[test] -fn test_the_refund_lane_reports_what_was_written_not_what_the_cap_let_through() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::RefundAtStep(OVERSIZED_REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - refund: Lane::once(i128::from(OVERSIZED_REFUND)), - ..InspectorLedger::default() - }, - "the lane carries the nominal edit", - ); - assert_eq!( - edited.refunded(), - edited.total_gas_spent / 5, - "while the receipt carries the EIP-3529 cap", - ); - assert!( - edited.refunded() < plain.refunded() + u64::try_from(OVERSIZED_REFUND).unwrap(), - "fixture check: the cap must actually bind, or this cell asserts nothing", - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "the envelope is untouched"); -} - -/// A refund written into a frame the EVM then fails is booked too, even though it reaches nothing. -/// -/// revm hands a frame's refund to its caller only on success, so this edit dies with the frame. -/// The lane books it anyway, because the alternative is a rule that has to track every frame -/// between the edit and the top — and because a lane that under-reports lets exactly the shape -/// this module exists to catch into a block, while over-reporting costs nothing: the law has no -/// term for it. -#[test] -fn test_a_refund_the_frame_chain_discards_is_still_booked() { - let plain = transact_plain(Callee::Reverting); - let edited = transact_edited(Callee::Reverting, Edit::RefundAtCallEnd(REFUND)); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { refund: Lane::once(i128::from(REFUND)), ..InspectorLedger::default() }, - "the lane books the edit", - ); - assert_eq!( - edited.refunded(), - plain.refunded(), - "the receipt is unmoved: a reverting frame hands its caller no refund", - ); - assert_eq!(edited.gas_used, plain.gas_used); -} - -// --- the EIP-8037 state-gas dimension ------------------------------------------------------------ - -/// A reservoir an inspector fills is gas the transaction never funded: the receipt reports that -/// much less spent, and the law needs it back. -#[test] -fn test_a_reservoir_written_into_a_live_interpreter_is_booked_and_the_law_closes() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtStep); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - RESERVOIR, - "the receipt counts the pool as unspent, so the envelope shrinks by exactly it", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, - i128::from(RESERVOIR), - "which is why this lane, unlike the refund one, is a term of the law", - ); -} - -/// The same, written into the pool a call's inputs seed the child frame with. -#[test] -fn test_a_reservoir_written_into_a_frame_input_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirOnInputs); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - // The inputs came back changed in a field the envelope lane does not cover, which the - // rewrite comparison books on its own. - interventions: 1, - ..InspectorLedger::default() - }, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); -} - -/// And into the finished frame's own pool, which its caller takes whatever the classification. -#[test] -fn test_a_reservoir_written_into_a_finished_frame_result_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtCallEnd); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent - RESERVOIR); -} - -/// A reservoir edit the EVM overwrites books nothing — and there is nothing to book, because the -/// run it produces is the run the EVM would have produced alone. -/// -/// This is the window that decides where the lane is measured. A difference taken across this -/// callback would say `RESERVOIR` was conjured; the transaction says otherwise, and the settlement -/// point is the only reading that agrees with it. -#[test] -fn test_a_reservoir_edit_the_evm_overwrites_books_nothing() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::ReservoirAtSuspension); - - assert!( - edited.inspector_ledger.is_zero(), - "an edit the child frame's own pool replaces moved nothing: {:?}", - edited.inspector_ledger, - ); - assert_eq!(edited.total_gas_spent, plain.total_gas_spent); - assert_eq!(edited.gas_used, plain.gas_used); - assert_eq!(edited.refunded(), plain.refunded()); -} - -/// The spend counter's own effect on the receipt: a successful transaction reports it, whether or -/// not EIP-8037 is enabled. -#[test] -fn test_state_gas_written_into_a_live_interpreter_reaches_the_receipt_and_is_booked() { - let plain = transact_plain(Callee::Returning); - let edited = transact_edited(Callee::Returning, Edit::StateGasAtStep); - - assert_eq!(plain.state_gas_spent(), 0, "fixture check"); - assert_eq!( - edited.state_gas_spent(), - u64::try_from(STATE_GAS).unwrap(), - "the receipt reports what was written", - ); - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - state_gas: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - ); - assert_eq!( - edited.total_gas_spent, plain.total_gas_spent, - "the envelope is untouched, so this lane is not a term of the law either", - ); - assert_eq!(edited.terms.inspector_conjured_gas, 0); -} - -/// The counter's *other* effect, at a site no callback sees: a frame that fails folds its spend -/// counter back into its caller's pool, which turns a state-gas edit into an envelope-moving one. -/// -/// The lane that catches it is the reservoir's, not the state-gas one, because the fold has -/// already happened by the time either is read. That is the second reason the two are settled from -/// the transaction's final figures rather than differenced across a callback. -#[test] -fn test_state_gas_on_a_failing_frame_becomes_its_callers_reservoir() { - let plain = transact_plain(Callee::Reverting); - let edited = transact_edited(Callee::Reverting, Edit::StateGasAtCallEnd); - - assert_eq!( - edited.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - "the spend counter of a reverting frame arrives in its caller as a pool", - ); - assert_eq!( - edited.state_gas_spent(), - 0, - "and not as a spend: a failing frame's counter is not accumulated", - ); - assert_eq!( - edited.total_gas_spent, - plain.total_gas_spent - u64::try_from(STATE_GAS).unwrap(), - "so the envelope moves, and the law's term has to move with it", - ); -} - -// --- a frame the inspector answers itself -// --------------------------------------------------------- - -/// A synthetic outcome carries figures of its own, and there is no EVM-produced number on the -/// other side of the callback to difference against — so the whole of what it carries is the -/// inspector's, measured against nothing rather than against a baseline. -/// -/// The echo control is what makes the two cells below readings of the figures rather than of the -/// interception: it moves the gas lanes not at all, which is the convention every tool that -/// intercepts follows. -#[test] -fn test_a_synthetic_outcome_carries_its_own_figures() { - let echo = transact_edited(Callee::Returning, Edit::InterceptEcho); - assert_eq!( - echo.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "an echoing interception moves no figure at all", - ); - - let refunding = transact_edited(Callee::Returning, Edit::InterceptWithRefund); - assert_eq!( - refunding.inspector_ledger, - InspectorLedger { - refund: Lane::once(i128::from(REFUND)), - interventions: 1, - ..InspectorLedger::default() - }, - "the refund a frame that never ran hands back is the inspector's in whole", - ); - assert_eq!( - refunding.refunded(), - echo.refunded() + u64::try_from(REFUND).unwrap(), - "and it reaches the receipt: the outcome succeeded, so its caller records it", - ); - assert_eq!(refunding.total_gas_spent, echo.total_gas_spent, "the envelope is unmoved"); - - let pooled = transact_edited(Callee::Returning, Edit::InterceptWithReservoir); - assert_eq!( - pooled.inspector_ledger, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - interventions: 1, - ..InspectorLedger::default() - }, - ); - assert_eq!( - pooled.total_gas_spent, - echo.total_gas_spent - RESERVOIR, - "a pool does move the envelope, wherever it came from", - ); -} - -// --- the frozen specs -// ----------------------------------------------------------------------------- - -/// On a frozen spec the two lanes report and settle nothing. -/// -/// The shim is not spec-gated, and must not be: the block guard has to see a rewritten receipt -/// whichever spec produced it. What is gated is the accounting the lanes feed, so a frozen spec's -/// own numbers have to be exactly what they were — which is what this reads, by comparing an -/// edited run against an unedited one on the same spec. -#[test] -fn test_a_frozen_spec_reports_the_lanes_without_settling_anything() { - const REX6: MegaSpecId = MegaSpecId::REX6; - fn run(edit: Option) -> Outcome { - let db = db_for(Callee::Returning); - let limits = EvmTxRuntimeLimits::from_spec(REX6); - match edit { - Some(edit) => { - let mut editor = Editor::new(edit); - let outcome = transact_inspected(REX6, db, limits, &mut editor); - assert_eq!(editor.fired, 1, "{edit:?} must land"); - outcome - } - None => transact(REX6, db, limits), - } - } - - let plain = run(None); - assert!(plain.inspector_ledger.is_zero()); - - for (edit, expected) in [ - ( - Edit::RefundAtStep(REFUND), - InspectorLedger { - refund: Lane::once(i128::from(REFUND)), - ..InspectorLedger::default() - }, - ), - ( - Edit::ReservoirAtStep, - InspectorLedger { - reservoir: Lane::once(i128::from(RESERVOIR)), - ..InspectorLedger::default() - }, - ), - ( - Edit::StateGasAtStep, - InspectorLedger { - state_gas: Lane::once(i128::from(STATE_GAS)), - ..InspectorLedger::default() - }, - ), - ] { - let edited = run(Some(edit)); - assert_eq!(edited.inspector_ledger, expected, "{edit:?}: the lane reports on every spec"); - assert_eq!( - edited.compute_gas, plain.compute_gas, - "{edit:?}: a frozen spec's compute total must not move", - ); - assert_eq!(edited.destroyed, plain.destroyed, "{edit:?}: nor its destroyed lane"); - // `inspector_conjured_gas` is a reading of the ledger rather than something the - // transaction recorded, so it moves with the lane on every spec. Every other term is what - // a frozen spec must leave alone. - assert_eq!( - ConservationTerms { inspector_conjured_gas: 0, ..edited.terms }, - plain.terms, - "{edit:?}: nothing a frozen spec records may move", - ); - assert_eq!( - edited.terms.inspector_conjured_gas, - edited.inspector_ledger.conjured_gas(), - "{edit:?}: and the term is the ledger's net, exactly as it is under REX7", - ); - } -} - -// === 5. interception ========================================================================== -// -// The gas a synthetic outcome carries. -// -// A `frame_start` / `call` / `create` callback that returns `Some(outcome)` answers the frame -// itself: no frame is built, `frame_init` never runs, and the number the caller reclaims is -// whatever `Gas` the inspector put in that outcome. Nothing about it is derived from the -// execution — the inspector chooses it outright — so it is a gas figure the transaction's -// accounting has to be told about, exactly like an edit to a result the EVM did produce. -// -// The tests here are laid out over the sign of that choice, because the two directions settle -// differently and a lane that books one and drops the other is a real failure mode: -// -// - an outcome that hands back **less** than the envelope makes the caller spend gas no frame ever -// performed work for; -// - an outcome that hands back **more** conjures gas the transaction never funded; -// - an outcome that hands back **exactly** the envelope — the echo convention every tracer that -// intercepts follows — moves nothing, and must book nothing. -// -// The halt direction is the asymmetry: a halting outcome hands nothing back at all, so what the -// inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed -// remainder is settled against the envelope instead. - -/// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. -const FORWARDED: u64 = 50_000; - -/// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. -fn call_fixture() -> MemoryDatabase { - db_with_callee(call_then_stop(CALLEE, FORWARDED), plain_run_code(20)) -} - -/// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. -#[derive(Clone, Copy, Debug)] -enum Sizing { - /// The echo convention: exactly the envelope. - Echo, - /// Half of it — the caller spends the other half for work no frame performed. - Half, - /// None of it. - Zero, - /// More than it — gas the transaction never funded. - Excess(u64), -} - -impl Sizing { - fn gas(self, envelope: u64) -> u64 { - match self { - Self::Echo => envelope, - Self::Half => envelope / 2, - Self::Zero => 0, - Self::Excess(extra) => envelope + extra, - } - } - - /// What the ledger must carry for this sizing, as a signed movement from the envelope. - fn expected_delta(self, envelope: u64) -> i128 { - i128::from(self.gas(envelope)) - i128::from(envelope) - } -} - -/// Intercepts the call to [`CALLEE`], sizing the outcome's gas by [`Sizing`]. -struct CallInterceptor { - sizing: Sizing, - classification: InstructionResult, - intercepted: u64, - envelope: u64, -} - -impl CallInterceptor { - fn new(sizing: Sizing, classification: InstructionResult) -> Self { - Self { sizing, classification, intercepted: 0, envelope: 0 } - } -} - -impl Inspector for CallInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - self.envelope = inputs.gas_limit; - Some(CallOutcome::new( - InterpreterResult::new( - self.classification, - Bytes::new(), - Gas::new(self.sizing.gas(inputs.gas_limit)), - ), - inputs.return_memory_offset.clone(), - )) - } -} - -/// An outcome that hands back less than the envelope makes the caller spend gas nothing performed. -#[test] -fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { - let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); - assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "the half the outcome withheld is gas the inspector destroyed", - ); -} - -/// The extreme of the same direction: the outcome hands back nothing at all. -#[test] -fn test_a_zero_gas_interception_books_the_whole_envelope() { - let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "an outcome that returns nothing consumed the whole envelope", - ); -} - -/// The other direction: an outcome that hands back more than it was given conjures the difference. -#[test] -fn test_an_over_funded_interception_books_the_gas_it_conjured() { - const EXTRA: u64 = 7_000; - let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "gas the transaction never funded is gas the inspector conjured", - ); -} - -/// The echo convention moves nothing, and must book nothing. -/// -/// This is the shape every tool that intercepts actually uses, and the reason the lane could go -/// missing for as long as it did: with the envelope echoed back the accounting closes whether or -/// not anything measures it. Pinning the zero is what says the lane is measuring rather than -/// coincidentally agreeing. -#[test] -fn test_an_echoing_interception_books_no_gas_at_all() { - for classification in [InstructionResult::Stop, InstructionResult::Revert] { - let mut inspector = CallInterceptor::new(Sizing::Echo, classification); - let reading = - transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", - ); - assert_eq!(reading.inspector_ledger.conjured_gas(), 0, "{classification:?}"); - } -} - -/// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes -/// nothing the transaction spends — and the envelope is destroyed whole. -/// -/// What the outcome claimed is still traffic on the result lane: the sizings below differ from the -/// envelope by different amounts, and each one is an edit the inspector made whether or not the -/// classification let it reach anybody. -#[test] -fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { - for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { - let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); - let reading = - transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); - assert_eq!( - reading.inspector_ledger.conjured_gas(), - 0, - "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", - ); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - interventions: 1, - result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), - ..InspectorLedger::default() - }, - "{sizing:?}: and the traffic is what the outcome claimed, off the envelope", - ); - assert_eq!( - reading.destroyed, FORWARDED, - "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", - ); - } -} - -/// The generic callback intercepts too, and is measured by the same rule. -/// -/// revm runs `frame_start` before the variant-specific `call` / `create`, and an outcome returned -/// there skips both. A lane wired only to the variant hooks would leave this one unmeasured. -#[test] -fn test_the_generic_frame_start_interception_is_measured_too() { - /// Intercepts the call to [`CALLEE`] from the generic callback, handing back half. - #[derive(Default)] - struct GenericInterceptor { - intercepted: u64, - } - - impl Inspector for GenericInterceptor { - fn frame_start( - &mut self, - _context: &mut CTX, - frame_input: &mut FrameInput, - ) -> Option { - let FrameInput::Call(inputs) = frame_input else { return None }; - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - Some(FrameResult::Call(CallOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit / 2), - ), - inputs.return_memory_offset.clone(), - ))) - } - } - - let mut inspector = GenericInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "the generic callback's interception books on the same lane as the variant one's", - ); -} - -/// Init code that writes one slot and returns two bytes of runtime code. -fn init_code() -> Vec { - BytecodeBuilder::default() - .sstore(U256::from(0x30), U256::from(1)) - .push_number(0x6000u64) - .push_number(0u64) - .append(MSTORE) - .push_number(2u64) // size - .push_number(30u64) // offset - .append(RETURN) - .build() - .to_vec() -} - -/// The entry contract: one `CREATE`, then `STOP`. -fn create_fixture() -> MemoryDatabase { - base_db(deploy_then_stop(&init_code())) -} - -/// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. -/// -/// The envelope is not a constant here — `CREATE` forwards all but a sixty-fourth of what the -/// caller holds — so the test reads it back from the callback rather than asserting a figure. -#[test] -fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() { - /// Intercepts the creation, handing back half of what it was given. - #[derive(Default)] - struct CreateInterceptor { - intercepted: u64, - envelope: u64, - } - - impl Inspector for CreateInterceptor { - fn create( - &mut self, - _context: &mut CTX, - inputs: &mut CreateInputs, - ) -> Option { - self.intercepted += 1; - self.envelope = inputs.gas_limit(); - Some(CreateOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit() / 2), - ), - None, - )) - } - } - - let mut inspector = CreateInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, create_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); - assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), - interventions: 1, - ..InspectorLedger::default() - }, - "a creation's interception is measured against the envelope its CREATE forwarded", - ); -} - -/// The envelope an interception is measured against is the one the callback *received*. -/// -/// A callback is free to edit the inputs and then answer the frame itself. The edit reaches no -/// frame — nothing is built from those inputs — so the envelope the caller actually funded is the -/// one the callback was handed, and an outcome echoing the *edited* limit hands back more than -/// that. Measuring against the post-edit number instead would read this run as conjuring nothing. -#[test] -fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { - const BONUS: u64 = 9_000; - - /// Raises the child's gas limit and then intercepts, echoing the raised figure. - #[derive(Default)] - struct RaisingInterceptor { - intercepted: u64, - } - - impl Inspector for RaisingInterceptor { - fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { - if inputs.target_address != CALLEE { - return None; - } - self.intercepted += 1; - inputs.gas_limit += BONUS; - Some(CallOutcome::new( - InterpreterResult::new( - InstructionResult::Stop, - Bytes::new(), - Gas::new(inputs.gas_limit), - ), - inputs.return_memory_offset.clone(), - )) - } - } - - let mut inspector = RaisingInterceptor::default(); - let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); - - assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); - assert_eq!( - reading.inspector_ledger, - InspectorLedger { - result: Lane::once(i128::from(BONUS)), - interventions: 1, - ..InspectorLedger::default() - }, - "the bonus reaches the caller through the outcome, so it is booked once, on the result \ - lane — the env lane stays empty because no frame was ever built from those inputs", - ); -} - -/// The lane reports on a frozen spec too, and reporting it settles nothing there. -/// -/// The measurement is not REX7-gated, and neither are the two lanes it joins: `InspectorLedger` is -/// what the canonical block path's guard reads, so a frame an inspector answered has to be visible -/// on it whatever spec is executing. What is REX7's alone is the settlement the lane feeds — the -/// envelope a refused frame init decides the fate of. REX6 derives nothing from the envelope and -/// books no destroyed remainder, so what it reports is what it always reported. -/// -/// The transaction's own gas does follow the figure the inspector wrote, on both specs. That is -/// the EVM handing the caller back what the result carries, which is upstream's arithmetic rather -/// than `MegaETH`'s, and it is the movement the lane exists to account for rather than to prevent. -#[test] -fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { - let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); - let echo = transact_inspected( - MegaSpecId::REX6, - call_fixture(), - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), - &mut echoing, - ); - let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); - let half = transact_inspected( - MegaSpecId::REX6, - call_fixture(), - EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), - &mut halving, - ); - - assert_eq!(echoing.intercepted, 1, "fixture check"); - assert_eq!(halving.intercepted, 1, "fixture check"); - assert_eq!( - echo.inspector_ledger, - InspectorLedger { interventions: 1, ..InspectorLedger::default() }, - "REX6: an echoed envelope moves no gas here either", - ); - assert_eq!( - half.inspector_ledger, - InspectorLedger { - result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), - interventions: 1, - ..InspectorLedger::default() - }, - "REX6: the lane reports, because the block guard has to see this frame on every spec", - ); - assert_eq!( - (echo.destroyed, half.destroyed), - (0, 0), - "REX6 has no destroyed remainder to book, on either sizing", - ); - assert_eq!( - echo.compute_gas, half.compute_gas, - "and its compute total does not follow the figure the inspector wrote", - ); - assert_eq!( - half.total_gas_spent - echo.total_gas_spent, - FORWARDED / 2, - "the caller really did lose the half the outcome withheld — that is the EVM's arithmetic", - ); -} diff --git a/crates/mega-evm/tests/rex7/shim_settlement.rs b/crates/mega-evm/tests/rex7/shim_settlement.rs new file mode 100644 index 00000000..1030df44 --- /dev/null +++ b/crates/mega-evm/tests/rex7/shim_settlement.rs @@ -0,0 +1,1073 @@ +//! Where a rewrite is settled, in the two places the shim's reading and the envelope's number are +//! not the same object. +//! +//! Both halves of the measurement shim rest on the same claim: what the shim books is what the +//! transaction's envelope actually moved by. Two groups of fixture stand behind it, in the order +//! they appear: +//! +//! 1. **The two settlement windows** — a terminating opcode's `step_end`, whose counter edit +//! reaches nobody, and a precompile's classification, whose split has to follow the callback +//! rather than the recording site. +//! 2. **Interception** — the gas an inspector puts into a synthetic outcome, over the four sizings +//! it can choose relative to the envelope it was handed, and the halt direction where the choice +//! reaches nothing. +//! +//! What each lane books is in `shim_lanes.rs`, and the shapes an all-zero ledger used to admit are +//! in `shim_blind_spots.rs`. The rewrites the shim *refuses* are in `shim_refusals.rs`; the +//! exhaustive callback × shape sweep is in `inspector_cheat_matrix.rs`. + +use crate::{ + common::{ + base_db, transact, transact_inspected, transact_inspected_refused, Outcome, Refusal, + CALLEE, DEFAULT_TX_GAS_LIMIT, + }, + inspector_common::{ + call_then_stop, db_with_callee, deploy_then_stop, limits, plain_run_code, ACTION_DELTA, + }, +}; +use alloy_primitives::{address, Address, Bytes, U256}; +use mega_evm::{ + kzg_point_evaluation, + test_utils::{BytecodeBuilder, MemoryDatabase}, + EvmTxRuntimeLimits, InspectorLedger, Lane, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CALL, INVALID, MSTORE, POP, RETURN, STOP}, + context::ContextTr, + handler::FrameResult, + interpreter::{ + interpreter_types::LoopControl, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + FrameInput, Gas, InstructionResult, Interpreter, InterpreterAction, InterpreterResult, + InterpreterTypes, + }, + Inspector, +}; +use sha2::{Digest, Sha256}; +use std::vec::Vec; + +// === the two settlement windows ============================================================== +// +// The two windows in which a rewrite lands after the accounting that should have read it. +// +// Both halves of the measurement shim rest on the same claim: what the shim books is what the +// transaction's envelope actually moved by. There are two places where the number the shim reads +// and the number the envelope carries are not the same object, and each of them is a fixture +// here. +// +// - **A terminating opcode's `step_end`.** revm's inspected loop runs `step_end` *after* the +// instruction that produced the frame's action, and that action carries its own copy of the gas +// counter. An edit to `interp.gas` at that moment changes the counter `MegaETH`'s tail settlement +// measures work against and nothing the caller will ever see, so it must move the settlement +// baseline and must not move the ledger. The two neighbouring windows — a `step_end` in +// mid-frame, and the one after a `CALL` has set a `NewFrame` action — are the boundary of that +// rule: the frame resumes on the edited counter in both, so both are booked. +// +// - **A precompile's classification.** A precompile is answered inside the frame init and never +// becomes a child frame, so its recording site is the only place that knows the forwarded +// envelope and the work performed. The split is nonetheless settled at the frame's settlement +// point, from what that site staged, exactly as an ordinary frame's is — because a callback runs +// in between, and the classification is what decides whether the caller reclaims the remainder. +// What that callback may do to the classification is bounded: the journal decision behind a +// result frame init produced was taken before any callback ran and is not reachable from one, so +// a rewrite that moves such a result across the success / revert / halt boundary is refused and +// the settlement reads the classification the EVM produced. The cases below pin the uninspected +// split each precompile arm produces, and the refusal that keeps it the one the settlement sees. +// +// Every case here is checked by the identity `common::finish` runs on every transaction: the +// tracker lanes must account for the whole receipt envelope, with the inspector's own term in it. + +/// Gas the edit-once inspector writes into a live interpreter's counter. +const INJECT: u64 = 1_000; + +/// Gas every probed CALL forwards. Well inside the 63/64 rule at the default transaction gas +/// limit and well inside the default compute budget, so the forwarded envelope is exactly this. +const PROBE_GAS: u64 = 1_000_000; + +/// The transaction gas limit is not what binds any fixture here — pinned at compile time, so a +/// change to the shared limit cannot silently turn a destroyed-remainder case into an +/// out-of-gas one. +const _: () = assert!(DEFAULT_TX_GAS_LIMIT > 10 * PROBE_GAS); + +/// The identity precompile. +const IDENTITY: Address = address!("0000000000000000000000000000000000000004"); +/// blake2f. Rejects any input whose length is not 213 bytes, before charging anything. +const BLAKE2F: Address = address!("0000000000000000000000000000000000000009"); +/// KZG point evaluation. +const KZG: Address = address!("000000000000000000000000000000000000000a"); + +// --- A: the window a terminating opcode's `step_end` sits in --------------------------------- + +/// Which of the three `step_end` windows an edit is aimed at, told apart by the action the +/// instruction that just ran left behind. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Window { + /// No action yet: the frame carries on, and the edited counter is what it carries on with. + MidFrame, + /// A `NewFrame` action: the frame suspends into a child and then resumes on this counter. + Suspending, + /// A `Return` action: the frame is over, and the gas it hands back was copied into the action + /// before this callback ran. + Terminating, +} + +impl Window { + fn of(interp: &mut Interpreter) -> Self { + match interp.bytecode.action() { + None => Self::MidFrame, + Some(InterpreterAction::NewFrame(_)) => Self::Suspending, + Some(InterpreterAction::Return(_)) => Self::Terminating, + } + } +} + +/// Writes [`INJECT`] into the interpreter's counter once, at the first `step_end` that sits in +/// `window`. +#[derive(Debug)] +struct CounterEditor { + window: Window, + fired: u32, +} + +impl CounterEditor { + fn new(window: Window) -> Self { + Self { window, fired: 0 } + } +} + +impl Inspector for CounterEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + self.fired += 1; + interp.gas.erase_cost(INJECT); + } +} + +/// `PUSH1 1; POP; STOP` — three opcodes, so a mid-frame `step_end` and a terminating one are both +/// reached, and nothing else happens in between. +fn straight_line_code() -> Bytes { + BytecodeBuilder::default().push_number(1u64).append(POP).append(STOP).build() +} + +/// A `CALL` into the identity precompile, its success flag popped, then `STOP` — so the frame +/// suspends once and the `step_end` after the `CALL` opcode sits in [`Window::Suspending`]. +fn suspending_code() -> Bytes { + call_then_stop(IDENTITY, PROBE_GAS) +} + +fn run_counter_edit(code: Bytes, window: Window) -> (Outcome, Outcome, u32) { + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = CounterEditor::new(window); + let edited = transact_inspected(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + (plain, edited, inspector.fired) +} + +/// An edit made in the terminating window reaches nobody, so nothing is booked for it — and the +/// transaction is the one the EVM would have produced alone. +/// +/// The action the terminating instruction set already holds its own copy of the counter, so the +/// caller is handed a number this edit never touched. Booking it would tell the conservation law +/// that the transaction spent [`INJECT`] less than it did. +/// +/// `compute_gas` being unmoved is the other half of the rule, and the one that would break if the +/// fix were written as "leave the counter alone" rather than "book nothing for it": the tail +/// settlement measures work as a drop in this very counter, so without the baseline shift the +/// injection would read as [`INJECT`] gas of work the frame never performed. +#[test] +fn test_an_edit_in_the_terminating_window_is_not_booked() { + let (plain, edited, fired) = run_counter_edit(straight_line_code(), Window::Terminating); + + assert_eq!(fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger::default(), + "an edit that cannot reach the envelope must leave the ledger untouched", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the settlement baseline must absorb the edit, so it counts as no work at all", + ); + assert_eq!( + edited.total_gas_spent, plain.total_gas_spent, + "the envelope must be the one the uninspected run produced", + ); +} + +/// The near boundary: a mid-frame edit is booked, because the frame carries on spending the +/// counter the callback left behind. +#[test] +fn test_an_edit_in_mid_frame_is_still_booked() { + let (_, edited, fired) = run_counter_edit(straight_line_code(), Window::MidFrame); + + assert_eq!(fired, 1, "the fixture must reach a mid-frame step_end exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + Lane::once(i128::from(INJECT)), + "gas written into a counter the frame will keep spending is conjured gas", + ); +} + +/// The far boundary, and the one a coarser rule would get wrong: a `CALL` has set an action too, +/// but it is a `NewFrame` action — the frame suspends, the child runs, and then the frame resumes +/// on exactly this counter. So the edit reaches the envelope and must be booked, even though the +/// interpreter is "at the end of its loop" in precisely the same sense as the terminating case. +#[test] +fn test_an_edit_in_the_suspending_window_is_still_booked() { + let (_, edited, fired) = run_counter_edit(suspending_code(), Window::Suspending); + + assert_eq!(fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger.gas, + Lane::once(i128::from(INJECT)), + "a suspended frame resumes on the edited counter, so the edit reaches the envelope", + ); +} + +// --- B: a precompile's classification, rewritten after its recording site --------------------- + +/// Rewrites the result of the call to `target` into `to`, once. +#[derive(Debug)] +struct Reclassifier { + target: Address, + to: InstructionResult, + fired: u32, +} + +impl Reclassifier { + fn new(target: Address, to: InstructionResult) -> Self { + Self { target, to, fired: 0 } + } +} + +impl Inspector for Reclassifier { + fn call_end(&mut self, _context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + if self.fired > 0 || inputs.target_address != self.target { + return; + } + self.fired += 1; + outcome.result.result = self.to; + } +} + +/// A `CALL` forwarding [`PROBE_GAS`] gas to `target` with `calldata` at `mem[0..]`, its success +/// flag popped so the caller survives either classification. +fn call_precompile(target: Address, calldata: &[u8]) -> Bytes { + BytecodeBuilder::default() + .mstore(0, calldata) + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(calldata.len() as u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(target) + .push_number(PROBE_GAS) + .append(CALL) + .append(POP) + .append(STOP) + .build() +} + +/// The EIP-4844 point-evaluation test vector with the last byte of the proof flipped: 192 bytes +/// with a matching versioned hash, so KZG clears the length doorway and fails inside verification +/// — the one halt shape `MegaETH` prices as work performed. +fn kzg_verification_failure() -> Vec { + let commitment = hex::decode( + "8f59a8d2a1a625a17f3fea0fe5eb8c896db3764f3185481bc22f91b4aaffcca2\ + 5f26936857bc3a7c2539ea8ec3a952b7", + ) + .unwrap(); + let mut versioned_hash = Sha256::digest(&commitment).to_vec(); + versioned_hash[0] = 0x01; // VERSIONED_HASH_VERSION_KZG + let z = + hex::decode("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000").unwrap(); + let y = + hex::decode("1522a4a7f34e1ea350ae07c29c96c7e79655aa926122e95fe69fcbd932ca49e9").unwrap(); + let proof = hex::decode( + "a62ad71d14c5719385c0686f1871430475bf3a00f0aa3f7b8dd99a9abc216074\ + 4faf0070725e00b60ad9a026a15b1a8c", + ) + .unwrap(); + + let mut input = Vec::new(); + input.extend_from_slice(&versioned_hash); + input.extend_from_slice(&z); + input.extend_from_slice(&y); + input.extend_from_slice(&commitment); + input.extend_from_slice(&proof); + assert_eq!(input.len(), 192, "the priced probe must clear the 192-byte doorway"); + let last = input.len() - 1; + input[last] ^= 0x01; + input +} + +/// Runs the fixture twice: once uninspected, and once with the classification rewritten across +/// the boundary the shim refuses. +/// +/// The refusal is asserted here rather than in each case, so every case below is left stating the +/// one thing that differs between them — which arm of the precompile it reaches, and what the +/// uninspected run's split therefore is. +fn run_reclassified(target: Address, calldata: &[u8], to: InstructionResult) -> (Outcome, Refusal) { + let code = call_precompile(target, calldata); + let plain = transact(MegaSpecId::REX7, base_db(code.clone()), limits()); + let mut inspector = Reclassifier::new(target, to); + let refusal = + transact_inspected_refused(MegaSpecId::REX7, base_db(code), limits(), &mut inspector); + assert_eq!(inspector.fired, 1, "the fixture must reach the precompile's call_end exactly once"); + assert_eq!(refusal.rejected_rewrites, 1, "the shim must count the refusal"); + assert!( + refusal.error.contains("classification of a result frame init produced"), + "the transaction must fail with the refusal's own reason, got {}", + refusal.error, + ); + (plain, refusal) +} + +/// A successful precompile rewritten into a halt is refused, and the uninspected run destroys +/// nothing. +/// +/// The rewrite is the direction with state behind it: `make_call_frame` commits the checkpoint +/// before it returns a successful precompile's result, so a caller told the call halted would be +/// told so with the transfer that funded it standing. +#[test] +fn test_rewriting_a_successful_precompile_into_a_halt_is_refused() { + let (plain, _) = run_reclassified(IDENTITY, &[], InstructionResult::OutOfGas); + + assert_eq!(plain.destroyed, 0, "the uninspected run destroys nothing"); + assert_eq!( + plain.compute_gas, + plain.enforced(), + "with nothing destroyed the reported total is the work performed", + ); +} + +/// A rejected precompile rewritten into a success is refused, and the uninspected run destroys the +/// whole envelope. +/// +/// The other direction, and the other half of the split: `blake2f` rejects the input before any +/// work, so `make_call_frame` reverted the checkpoint and nothing was performed. +#[test] +fn test_reviving_a_rejected_precompile_is_refused() { + let (plain, _) = run_reclassified(BLAKE2F, &[], InstructionResult::Stop); + + assert_eq!( + plain.destroyed, PROBE_GAS, + "blake2f rejects the input before any work, so the uninspected run destroys all of it", + ); + assert_eq!( + plain.enforced(), + plain.compute_gas - plain.destroyed, + "nothing was performed, so nothing enforces", + ); +} + +/// The third arm, and the only one whose failure `MegaETH` prices as work: a KZG verification that +/// ran and rejected. +/// +/// The refusal matters most here. A halting precompile's gas object carries the whole forwarded +/// envelope as remaining — it is reset rather than spent down — so a caller told such a call +/// succeeded would reclaim all of it, the fixed fee included. That fee is gas the execution priced +/// and the envelope never paid, which is exactly the shape the refusal keeps out. +#[test] +fn test_reviving_a_priced_precompile_failure_is_refused() { + let calldata = kzg_verification_failure(); + let (plain, _) = run_reclassified(KZG, &calldata, InstructionResult::Stop); + + assert_eq!( + plain.destroyed, + PROBE_GAS - kzg_point_evaluation::GAS_COST, + "verification ran, so the uninspected run destroys the envelope less the fixed fee", + ); + assert_eq!( + plain.compute_gas - plain.destroyed, + plain.enforced(), + "the fee is the work performed, and it is what enforces", + ); +} + +// --- C: the pending action itself --------------------------------------------------------------- + +/// Reaches past the interpreter's gas counter and into the action the interpreter is holding, once. +/// +/// The counter and the action are two different objects at exactly one moment — after a +/// terminating or suspending instruction has run and before the loop hands the action on — and +/// this is the inspector that edits the second one. +#[derive(Debug)] +struct ActionEditor { + window: Window, + /// Positive raises the gas the action carries, negative lowers it. + delta: i64, + /// Fire only on an action whose classification is (or is not) an exceptional halt. + halting: bool, + fired: u32, +} + +impl ActionEditor { + fn raise(window: Window) -> Self { + Self { window, delta: ACTION_DELTA as i64, halting: false, fired: 0 } + } + + fn lower(window: Window) -> Self { + Self { window, delta: -(ACTION_DELTA as i64), halting: false, fired: 0 } + } + + fn on_halt() -> Self { + Self { window: Window::Terminating, delta: ACTION_DELTA as i64, halting: true, fired: 0 } + } +} + +impl Inspector for ActionEditor { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 || Window::of(interp) != self.window { + return; + } + match interp.bytecode.action() { + Some(InterpreterAction::Return(result)) => { + if result.result.is_ok_or_revert() == self.halting { + return; + } + if self.delta >= 0 { + result.gas.erase_cost(self.delta.unsigned_abs()); + } else { + assert!( + result.gas.record_regular_cost(self.delta.unsigned_abs()), + "the fixture must leave the action enough gas for the removal to land", + ); + } + } + Some(InterpreterAction::NewFrame(FrameInput::Call(inputs))) => { + inputs.gas_limit = inputs.gas_limit.saturating_add(self.delta.unsigned_abs()); + } + _ => return, + } + self.fired += 1; + } +} + +/// A `CALL` into [`CALLEE`], its result flag popped, then `STOP` — so the first terminating +/// `step_end` of the transaction belongs to an *inner* frame, and what that frame's action carries +/// is decided by the callee the fixture installs. +fn call_callee_code() -> Bytes { + call_then_stop(CALLEE, PROBE_GAS) +} + +/// Gas written into a returning frame's pending action is gas the caller really reclaims, so it +/// has to be booked — the frame's classification is what says so, and the classification is only +/// known at the frame's settlement point. +#[test] +fn test_raising_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::once(i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "an edit to the action a returning frame hands back is an edit to the envelope", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the transaction really did spend less, which is why the ledger has to carry it", + ); + assert_eq!( + edited.compute_gas, plain.compute_gas, + "the edit is not work: the frame performed exactly what it performed uninspected", + ); +} + +/// The same edit in the other direction. +#[test] +fn test_lowering_a_returning_frames_pending_action_is_booked() { + let plain = transact(MegaSpecId::REX7, base_db(straight_line_code()), limits()); + let mut inspector = ActionEditor::lower(Window::Terminating); + let edited = transact_inspected( + MegaSpecId::REX7, + base_db(straight_line_code()), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::once(-i128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "gas taken out of the action is gas the caller never gets back", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent + ACTION_DELTA, + "the transaction really did spend more", + ); +} + +/// The classification branch: a halting frame hands nothing back, so an edit to the gas its action +/// carries moves nothing and must not reach the lane's *net* — and the remainder it destroys is +/// the EVM's own number, not the edited one. +/// +/// The lane's gross carries the edit all the same. Whether it moved the envelope is what the +/// classification decides; whether the inspector made it is not, and the block guard asks the +/// second question. +#[test] +fn test_editing_a_halting_frames_pending_action_moves_nothing() { + let callee = BytecodeBuilder::default().append(INVALID).build(); + let plain = + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); + let mut inspector = ActionEditor::on_halt(); + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(call_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must halt an inner frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { + result: Lane::of(0, u128::from(ACTION_DELTA)), + ..InspectorLedger::default() + }, + "a halting frame hands its remainder to nobody, so the edit moves the envelope by nothing \ + — and the lane still has to show it was made", + ); + assert_eq!( + edited.inspector_ledger.conjured_gas(), + 0, + "the conservation law reads the net, which is what stays zero", + ); + assert!( + !edited.inspector_ledger.is_zero(), + "and the block guard reads the gross, which is what does not", + ); + assert_eq!( + edited.destroyed, plain.destroyed, + "the destroyed remainder is the EVM's own, not the one the inspector wrote", + ); + assert_eq!(edited.total_gas_spent, plain.total_gas_spent, "and the envelope is unmoved"); +} + +/// The other action variant: gas written into a pending `NewFrame` action is the envelope a child +/// frame is about to be built with, which the caller was never debited for. +#[test] +fn test_raising_a_pending_new_frame_action_is_booked_as_an_envelope() { + let plain = transact(MegaSpecId::REX7, base_db(suspending_code()), limits()); + let mut inspector = ActionEditor::raise(Window::Suspending); + let edited = + transact_inspected(MegaSpecId::REX7, base_db(suspending_code()), limits(), &mut inspector); + + assert_eq!(inspector.fired, 1, "the fixture must suspend into a child frame exactly once"); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { env: Lane::once(i128::from(ACTION_DELTA)), ..InspectorLedger::default() }, + "the child's budget grew by gas the caller's CALL never forwarded", + ); + assert_eq!( + edited.total_gas_spent, + plain.total_gas_spent - ACTION_DELTA, + "the child hands the extra budget straight back, so the transaction spends less", + ); +} + +/// Rewrites the classification inside a pending `Return` action, once, at the terminating +/// `step_end` of the frame that set it. +#[derive(Debug)] +struct ActionReclassifier { + to: InstructionResult, + fired: u32, +} + +impl Inspector for ActionReclassifier { + fn step_end(&mut self, interp: &mut Interpreter, _context: &mut CTX) { + if self.fired > 0 { + return; + } + let Some(InterpreterAction::Return(result)) = interp.bytecode.action() else { return }; + result.result = self.to; + self.fired += 1; + } +} + +/// An edit to a pending action that is not to its gas moves nothing and is booked as an +/// intervention — but it still decides what the frame did, so the frame's state follows it. +/// +/// The action is what `classify_frame_action` builds the frame's result from, so a classification +/// written here is the one the caller sees and the one the journal decision is taken on. Nothing +/// on any gas lane can see that, which is what the intervention counter is for. +#[test] +fn test_rewriting_a_pending_actions_classification_is_an_intervention() { + let callee = + BytecodeBuilder::default().sstore(U256::from(1u64), U256::from(1u64)).append(STOP).build(); + let plain = + transact(MegaSpecId::REX7, db_with_callee(call_callee_code(), callee.clone()), limits()); + let mut inspector = ActionReclassifier { to: InstructionResult::Revert, fired: 0 }; + let edited = transact_inspected( + MegaSpecId::REX7, + db_with_callee(call_callee_code(), callee), + limits(), + &mut inspector, + ); + + assert_eq!(inspector.fired, 1, "the fixture must reach a terminating step_end exactly once"); + assert_eq!( + plain.storage_value(CALLEE, U256::from(1u64)), + U256::from(1u64), + "uninspected, the callee's write is committed", + ); + assert_eq!( + edited.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "no gas moved, and the only thing left to say is that the transaction was not left alone", + ); + assert_eq!( + edited.storage_value(CALLEE, U256::from(1u64)), + U256::ZERO, + "a frame the caller was told reverted must leave no write behind", + ); +} + +// === interception ============================================================================ +// +// The gas a synthetic outcome carries. +// +// A `frame_start` / `call` / `create` callback that returns `Some(outcome)` answers the frame +// itself: no frame is built, `frame_init` never runs, and the number the caller reclaims is +// whatever `Gas` the inspector put in that outcome. Nothing about it is derived from the +// execution — the inspector chooses it outright — so it is a gas figure the transaction's +// accounting has to be told about, exactly like an edit to a result the EVM did produce. +// +// The tests here are laid out over the sign of that choice, because the two directions settle +// differently and a lane that books one and drops the other is a real failure mode: +// +// - an outcome that hands back **less** than the envelope makes the caller spend gas no frame ever +// performed work for; +// - an outcome that hands back **more** conjures gas the transaction never funded; +// - an outcome that hands back **exactly** the envelope — the echo convention every tracer that +// intercepts follows — moves nothing, and must book nothing. +// +// The halt direction is the asymmetry: a halting outcome hands nothing back at all, so what the +// inspector wrote in the gas figure changes nothing the transaction spends, and the destroyed +// remainder is settled against the envelope instead. + +/// Gas the fixture's `CALL` forwards, and the envelope every interception is measured against. +const FORWARDED: u64 = 50_000; + +/// The entry contract: one `CALL` to [`CALLEE`] forwarding [`FORWARDED`], then `STOP`. +fn call_fixture() -> MemoryDatabase { + db_with_callee(call_then_stop(CALLEE, FORWARDED), plain_run_code(20)) +} + +/// How an interception sizes the `Gas` it hands back, relative to the envelope it was given. +#[derive(Clone, Copy, Debug)] +enum Sizing { + /// The echo convention: exactly the envelope. + Echo, + /// Half of it — the caller spends the other half for work no frame performed. + Half, + /// None of it. + Zero, + /// More than it — gas the transaction never funded. + Excess(u64), +} + +impl Sizing { + fn gas(self, envelope: u64) -> u64 { + match self { + Self::Echo => envelope, + Self::Half => envelope / 2, + Self::Zero => 0, + Self::Excess(extra) => envelope + extra, + } + } + + /// What the ledger must carry for this sizing, as a signed movement from the envelope. + fn expected_delta(self, envelope: u64) -> i128 { + i128::from(self.gas(envelope)) - i128::from(envelope) + } +} + +/// Intercepts the call to [`CALLEE`], sizing the outcome's gas by [`Sizing`]. +struct CallInterceptor { + sizing: Sizing, + classification: InstructionResult, + intercepted: u64, + envelope: u64, +} + +impl CallInterceptor { + fn new(sizing: Sizing, classification: InstructionResult) -> Self { + Self { sizing, classification, intercepted: 0, envelope: 0 } + } +} + +impl Inspector for CallInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + self.envelope = inputs.gas_limit; + Some(CallOutcome::new( + InterpreterResult::new( + self.classification, + Bytes::new(), + Gas::new(self.sizing.gas(inputs.gas_limit)), + ), + inputs.return_memory_offset.clone(), + )) + } +} + +/// An outcome that hands back less than the envelope makes the caller spend gas nothing performed. +#[test] +fn test_a_half_gas_interception_books_the_gas_it_took_from_the_caller() { + let mut inspector = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!(inspector.envelope, FORWARDED, "fixture check: the forwarded envelope"); + assert!(reading.result.is_success(), "fixture check: {:?}", reading.result); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "the half the outcome withheld is gas the inspector destroyed", + ); +} + +/// The extreme of the same direction: the outcome hands back nothing at all. +#[test] +fn test_a_zero_gas_interception_books_the_whole_envelope() { + let mut inspector = CallInterceptor::new(Sizing::Zero, InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Zero.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "an outcome that returns nothing consumed the whole envelope", + ); +} + +/// The other direction: an outcome that hands back more than it was given conjures the difference. +#[test] +fn test_an_over_funded_interception_books_the_gas_it_conjured() { + const EXTRA: u64 = 7_000; + let mut inspector = CallInterceptor::new(Sizing::Excess(EXTRA), InstructionResult::Stop); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Excess(EXTRA).expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "gas the transaction never funded is gas the inspector conjured", + ); +} + +/// The echo convention moves nothing, and must book nothing. +/// +/// This is the shape every tool that intercepts actually uses, and the reason the lane could go +/// missing for as long as it did: with the envelope echoed back the accounting closes whether or +/// not anything measures it. Pinning the zero is what says the lane is measuring rather than +/// coincidentally agreeing. +#[test] +fn test_an_echoing_interception_books_no_gas_at_all() { + for classification in [InstructionResult::Stop, InstructionResult::Revert] { + let mut inspector = CallInterceptor::new(Sizing::Echo, classification); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "{classification:?}: an echoed envelope moves no gas, so no gas lane may move", + ); + assert_eq!(reading.inspector_ledger.conjured_gas(), 0, "{classification:?}"); + } +} + +/// A halting outcome hands nothing back, so what the inspector wrote in its gas figure changes +/// nothing the transaction spends — and the envelope is destroyed whole. +/// +/// What the outcome claimed is still traffic on the result lane: the sizings below differ from the +/// envelope by different amounts, and each one is an edit the inspector made whether or not the +/// classification let it reach anybody. +#[test] +fn test_a_halting_interception_destroys_the_envelope_whatever_gas_it_reports() { + for sizing in [Sizing::Echo, Sizing::Half, Sizing::Zero, Sizing::Excess(7_000)] { + let mut inspector = CallInterceptor::new(sizing, InstructionResult::OutOfGas); + let reading = + transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert!(reading.result.is_success(), "the caller absorbs the halt: {:?}", reading.result); + assert_eq!( + reading.inspector_ledger.conjured_gas(), + 0, + "{sizing:?}: a halting frame hands nothing back, so no gas lane's net may move", + ); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + interventions: 1, + result: Lane::of(0, sizing.expected_delta(FORWARDED).unsigned_abs()), + ..InspectorLedger::default() + }, + "{sizing:?}: and the traffic is what the outcome claimed, off the envelope", + ); + assert_eq!( + reading.destroyed, FORWARDED, + "{sizing:?}: the whole envelope is destroyed, whatever the outcome claimed", + ); + } +} + +/// The generic callback intercepts too, and is measured by the same rule. +/// +/// revm runs `frame_start` before the variant-specific `call` / `create`, and an outcome returned +/// there skips both. A lane wired only to the variant hooks would leave this one unmeasured. +#[test] +fn test_the_generic_frame_start_interception_is_measured_too() { + /// Intercepts the call to [`CALLEE`] from the generic callback, handing back half. + #[derive(Default)] + struct GenericInterceptor { + intercepted: u64, + } + + impl Inspector for GenericInterceptor { + fn frame_start( + &mut self, + _context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + let FrameInput::Call(inputs) = frame_input else { return None }; + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + Some(FrameResult::Call(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit / 2), + ), + inputs.return_memory_offset.clone(), + ))) + } + } + + let mut inspector = GenericInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "the generic callback's interception books on the same lane as the variant one's", + ); +} + +/// Init code that writes one slot and returns two bytes of runtime code. +fn init_code() -> Vec { + BytecodeBuilder::default() + .sstore(U256::from(0x30), U256::from(1)) + .push_number(0x6000u64) + .push_number(0u64) + .append(MSTORE) + .push_number(2u64) // size + .push_number(30u64) // offset + .append(RETURN) + .build() + .to_vec() +} + +/// The entry contract: one `CREATE`, then `STOP`. +fn create_fixture() -> MemoryDatabase { + base_db(deploy_then_stop(&init_code())) +} + +/// A creation answered by the inspector is measured against the envelope its `CREATE` forwarded. +/// +/// The envelope is not a constant here — `CREATE` forwards all but a sixty-fourth of what the +/// caller holds — so the test reads it back from the callback rather than asserting a figure. +#[test] +fn test_an_intercepted_creation_is_measured_against_the_envelope_it_was_handed() { + /// Intercepts the creation, handing back half of what it was given. + #[derive(Default)] + struct CreateInterceptor { + intercepted: u64, + envelope: u64, + } + + impl Inspector for CreateInterceptor { + fn create( + &mut self, + _context: &mut CTX, + inputs: &mut CreateInputs, + ) -> Option { + self.intercepted += 1; + self.envelope = inputs.gas_limit(); + Some(CreateOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit() / 2), + ), + None, + )) + } + } + + let mut inspector = CreateInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, create_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one creation"); + assert!(inspector.envelope > 0, "fixture check: the creation must forward an envelope"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(inspector.envelope)), + interventions: 1, + ..InspectorLedger::default() + }, + "a creation's interception is measured against the envelope its CREATE forwarded", + ); +} + +/// The envelope an interception is measured against is the one the callback *received*. +/// +/// A callback is free to edit the inputs and then answer the frame itself. The edit reaches no +/// frame — nothing is built from those inputs — so the envelope the caller actually funded is the +/// one the callback was handed, and an outcome echoing the *edited* limit hands back more than +/// that. Measuring against the post-edit number instead would read this run as conjuring nothing. +#[test] +fn test_the_envelope_is_the_one_the_callback_received_not_the_one_it_left() { + const BONUS: u64 = 9_000; + + /// Raises the child's gas limit and then intercepts, echoing the raised figure. + #[derive(Default)] + struct RaisingInterceptor { + intercepted: u64, + } + + impl Inspector for RaisingInterceptor { + fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { + if inputs.target_address != CALLEE { + return None; + } + self.intercepted += 1; + inputs.gas_limit += BONUS; + Some(CallOutcome::new( + InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(inputs.gas_limit), + ), + inputs.return_memory_offset.clone(), + )) + } + } + + let mut inspector = RaisingInterceptor::default(); + let reading = transact_inspected(MegaSpecId::REX7, call_fixture(), limits(), &mut inspector); + + assert_eq!(inspector.intercepted, 1, "the fixture must intercept exactly one call"); + assert_eq!( + reading.inspector_ledger, + InspectorLedger { + result: Lane::once(i128::from(BONUS)), + interventions: 1, + ..InspectorLedger::default() + }, + "the bonus reaches the caller through the outcome, so it is booked once, on the result \ + lane — the env lane stays empty because no frame was ever built from those inputs", + ); +} + +/// The lane reports on a frozen spec too, and reporting it settles nothing there. +/// +/// The measurement is not REX7-gated, and neither are the two lanes it joins: `InspectorLedger` is +/// what the canonical block path's guard reads, so a frame an inspector answered has to be visible +/// on it whatever spec is executing. What is REX7's alone is the settlement the lane feeds — the +/// envelope a refused frame init decides the fate of. REX6 derives nothing from the envelope and +/// books no destroyed remainder, so what it reports is what it always reported. +/// +/// The transaction's own gas does follow the figure the inspector wrote, on both specs. That is +/// the EVM handing the caller back what the result carries, which is upstream's arithmetic rather +/// than `MegaETH`'s, and it is the movement the lane exists to account for rather than to prevent. +#[test] +fn test_a_frozen_spec_reports_the_lane_without_settling_anything() { + let mut echoing = CallInterceptor::new(Sizing::Echo, InstructionResult::Stop); + let echo = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut echoing, + ); + let mut halving = CallInterceptor::new(Sizing::Half, InstructionResult::Stop); + let half = transact_inspected( + MegaSpecId::REX6, + call_fixture(), + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX6), + &mut halving, + ); + + assert_eq!(echoing.intercepted, 1, "fixture check"); + assert_eq!(halving.intercepted, 1, "fixture check"); + assert_eq!( + echo.inspector_ledger, + InspectorLedger { interventions: 1, ..InspectorLedger::default() }, + "REX6: an echoed envelope moves no gas here either", + ); + assert_eq!( + half.inspector_ledger, + InspectorLedger { + result: Lane::once(Sizing::Half.expected_delta(FORWARDED)), + interventions: 1, + ..InspectorLedger::default() + }, + "REX6: the lane reports, because the block guard has to see this frame on every spec", + ); + assert_eq!( + (echo.destroyed, half.destroyed), + (0, 0), + "REX6 has no destroyed remainder to book, on either sizing", + ); + assert_eq!( + echo.compute_gas, half.compute_gas, + "and its compute total does not follow the figure the inspector wrote", + ); + assert_eq!( + half.total_gas_spent - echo.total_gas_spent, + FORWARDED / 2, + "the caller really did lose the half the outcome withheld — that is the EVM's arithmetic", + ); +} From 731d29170300dccadd0da6ff1ed7436194e67f7c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:48:38 +0800 Subject: [PATCH 191/208] style: format AGENTS.md emphasis for prettier Claude-Session: https://claude.ai/code/session_01VNYQWJ34yvw7EV74NEZZuP --- AGENTS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1aee3dc6..d4cb9ee3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,13 +130,13 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi `inspector_conjured_gas` is the same kind of correction for a producer outside the EVM: `MegaEvm` wraps every inspector it is handed in `MeasuredInspector`, which snapshots the interpreter's gas counter and a frame input's `gas_limit` across each callback and books the difference into `AdditionalLimit::inspector_ledger` — the EVM does not execute inside a callback, so anything that moves across one is the inspector's. Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. - An edit to a frame *result*'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. + An edit to a frame _result_'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome, and every constant-time reading it can take off a live interpreter — because a rewrite that costs nothing still produces different state and a different receipt. That last group is stated as a rule rather than as a list: every `O(1)` reading of the interpreter's working set enters the boundary snapshot, which is what makes a program counter stepped past an instruction, a memory grown together with its memo, or a return buffer conjured in front of a frame that made no call all visible on the same lane. Every gas lane carries a gross alongside its net, and it is the gross that `is_zero` — the guard's question — reads: two edits to one lane that cancel are two edits, whether they cancel inside one frame or across a surviving frame and a rolled-back one, and a net-only reading calls that pair untouched while the execution saw a number the EVM would never have produced. The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). A frame the inspector answers itself is the one place a difference across the callback is not the measurement, because no frame is built and the whole result is the inspector's: the shim stages the envelope the answering callback was handed, and `inspect_frame_init` settles the gas the result finally carries against it on the result lane — which also covers whatever of an edit to the inputs survives into a guard's replacement result, and which is zero for the echo convention every tool that intercepts follows. - What no callback boundary can see stays invisible (the *contents* of the interpreter's stack, memory, return buffer, calldata and code at unchanged identities, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed or could read in constant time come back changed, not that the transaction is the one the EVM would have produced alone. + What no callback boundary can see stays invisible (the _contents_ of the interpreter's stack, memory, return buffer, calldata and code at unchanged identities, direct journal writes), so an all-zero ledger says the shim saw no gas move and nothing it was handed or could read in constant time come back changed, not that the transaction is the one the EVM would have produced alone. The receipt's other two numbers have lanes of their own, measured at two different points because `MegaETH` produces one of the two quantities and none of the other: a refund is booked nominally across the callback boundary, since only a difference there separates an inspector's share from the EVM's own refunds, while the EIP-8037 state-gas dimension (`reservoir` and `state_gas_spent`, on a `Gas` or on a call's inputs) is settled once from the figures the transaction ends with — revm propagates it by replacement rather than accumulation, so a boundary difference would book edits the EVM goes on to erase. The reservoir is a term of the conservation law because it lowers the envelope the receipt reports; the refund and the spend counter are not, and are refused by the block guard rather than accounted for. `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration, pinned by `tests/rex7/gas_surface.rs`, which also fails on any row left saying a surface reaches the receipt and no lane books it. @@ -158,7 +158,7 @@ revm assembles a frame's result, decides its journal checkpoint and — for a co Between the two run the frozen post-action charge, the inspector's `frame_end`, and `AdditionalLimit::finalize_frame` — the single point a frame's outcome is settled (final classification, executed/destroyed split, frame-init refusal booking, gas rescue, and the REX7 frame-local absorb). Under REX7 the journal decision is taken later still — the frame loops park it on `MegaEvm::deferred_journal` and `frame_return_result` carries it out, after `AdditionalLimit::before_frame_return_result` (the last thing that can rewrite a frame's result) and before the caller resumes — so a frame's state agrees with the result its caller is handed and a creation's `set_code` still lands with no observation window. Frozen specs take it where revm does, right after the classification, because what they replay includes the state a frame leaves behind when a later rewrite fails it. -The rewrite that made the extra station necessary is the late frame-local exceed: a per-frame budget is the frame's usage weighed against its *caller's* budget after the merge, so a frame can overrun one with nothing having latched it. +The rewrite that made the extra station necessary is the late frame-local exceed: a per-frame budget is the frame's usage weighed against its _caller's_ budget after the merge, so a frame can overrun one with nothing having latched it. REX7 asks that question before the pop, through `AdditionalLimit::peek_check_limit_after_pop` over `FrameLimitTracker::view_after_pop`, and rewrites the frame to a revert first; the pop then discards the frame's usage the way it discards any reverting frame's, and the caller carries on. The pre-pop reading and the post-pop `check_limit()` are cross-checked against each other on every frame return in debug builds, on every spec — that assertion is what stands between the early decision and a drift in what counts as a frame-local exceed. Both frame loops (`frame_run` / `inspect_frame_run`) and both frame-init paths (`frame_init` / `inspect_frame_init`) run the same bodies; the inspected copies add exactly one thing, the callback that can rewrite a frame's classification. From edea4d14488ca82a4a4aaecafeef626ee36c3750 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 21:53:59 +0800 Subject: [PATCH 192/208] chore(mutants): re-anchor the state_growth spec-gate suppressions to their moved lines check_limit became check_limit_on at line 178 and current_call_remaining moved to line 205 when the frame-limit view was introduced; the three line-anchored entries stopped matching any generated mutant. Claude-Session: https://claude.ai/code/session_01VNYQWJ34yvw7EV74NEZZuP --- mutants/suppressions.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index 286409de..d6bfc9de 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -290,31 +290,31 @@ kind = "line" category = "equivalent" file = "crates/mega-evm/src/limit/state_growth.rs" mutant = "crates/mega-evm/src/limit/state_growth.rs:121:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX3) {" -justification = "Equivalent (coupled): push_frame shifted to REX3 derives per-frame state-growth limits at REX3, but check_limit (line 206, unmutated) skips the per-frame check pre-REX4 and uses TX-level net_usage only. The derived frame limits are never read, so no observable change." +justification = "Equivalent (coupled): push_frame shifted to REX3 derives per-frame state-growth limits at REX3, but check_limit_on (line 178, unmutated) skips the per-frame check pre-REX4 and uses TX-level net_usage only. The derived frame limits are never read, so no observable change." reviewer = "improve-mutation-score (William Aaron Cheung)" [[suppress]] kind = "line" category = "equivalent" file = "crates/mega-evm/src/limit/state_growth.rs" -mutant = "crates/mega-evm/src/limit/state_growth.rs:206:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX3) {" -justification = "Equivalent (coupled): check_limit shifted to REX3 runs the per-frame check at REX3, but push_frame (line 121, unmutated) pushes frames with u64::MAX limit pre-REX4, so exceeds_current_frame_limit never fires and control falls through to the identical TX-level check." +mutant = "crates/mega-evm/src/limit/state_growth.rs:178:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX3) {" +justification = "Equivalent (coupled): check_limit_on shifted to REX3 runs the per-frame check at REX3, but push_frame (line 121, unmutated) pushes frames with u64::MAX limit pre-REX4, so exceeds_current_frame_limit never fires and control falls through to the identical TX-level check." reviewer = "improve-mutation-score (William Aaron Cheung)" [[suppress]] kind = "line" category = "equivalent" file = "crates/mega-evm/src/limit/state_growth.rs" -mutant = "crates/mega-evm/src/limit/state_growth.rs:169:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX3) {" -justification = "Equivalent: current_call_remaining is only read when building a child sandbox budget (sandbox/execution.rs:1037, KeylessDeploy, REX5+); it is never called at REX3/REX4. And at REX3 frames carry u64::MAX limits (push_frame, line 121), so frame_remaining.min(tx_remaining) == tx_remaining regardless." +mutant = "crates/mega-evm/src/limit/state_growth.rs:205:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX3) {" +justification = "Equivalent: current_call_remaining is only read when building a child sandbox budget (sandbox/execution.rs, KeylessDeploy, REX5+); it is never called at REX3/REX4. And at REX3 frames carry u64::MAX limits (push_frame, line 121), so frame_remaining.min(tx_remaining) == tx_remaining regardless." reviewer = "improve-mutation-score (William Aaron Cheung)" [[suppress]] kind = "line" category = "equivalent" file = "crates/mega-evm/src/limit/state_growth.rs" -mutant = "crates/mega-evm/src/limit/state_growth.rs:169:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX5) {" -justification = "Equivalent: current_call_remaining is only read by the REX5+ KeylessDeploy sandbox (sandbox/execution.rs:1037). The REX4->REX5 shift differs only at exactly REX4, where the function is never called — no observable change." +mutant = "crates/mega-evm/src/limit/state_growth.rs:205:1: spec-gate if self.spec.is_enabled(MegaSpecId::REX4) { -> if self.spec.is_enabled(MegaSpecId::REX5) {" +justification = "Equivalent: current_call_remaining is only read by the REX5+ KeylessDeploy sandbox (sandbox/execution.rs). The REX4->REX5 shift differs only at exactly REX4, where the function is never called — no observable change." reviewer = "improve-mutation-score (William Aaron Cheung)" # --- precompiles.rs:135 legacy Osaka ModExp size check, first `||` (equivalent) --- From 56836fccc0ddca98a352f3256225a56638ccf494 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 22:55:43 +0800 Subject: [PATCH 193/208] test(equivalence): pin the frame settlement's EQUIVALENCE side, and suppress its two equivalent gate mutants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diff-scoped spec-gate run left three survivors on the shared frame-exit body, all of them the MINI_REX gate shifted to EQUIVALENCE. The `finalize_frame` gate is a real gap: under EQUIVALENCE the settlement does not run, so an inspector's edit to a frame result's gas reaches the receipt with the ledger's result lane — and the block guard behind it — reading it as untouched. Two tests pin that, together with its other half: what the measurement shim books at its own callback boundaries is unaffected, because the shim is not spec-gated. The other two are equivalent and recorded as such. The `gas_remaining_before` capture has one consumer, itself behind an unmutated MINI_REX gate. The uninspected `frame_init` reaches `finalize_frame` with a zero inspector delta, where every branch is either REX7-gated, structurally unreachable, or writes state no pre-MINI_REX reader can see. --- crates/mega-evm/src/evm/AGENTS.md | 5 + .../tests/equivalence/pre_mini_rex_gates.rs | 192 +++++++++++++++++- mutants/suppressions.toml | 52 +++++ 3 files changed, 246 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 5bfe0e99..85205c93 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -39,6 +39,11 @@ The shim's soundness rests on one fact: the EVM does not execute inside an inspe Anything that changes between the moment the shim delegates to the user's inspector and the moment control comes back is therefore the inspector's doing by construction, not by attribution — which is what makes the callback boundary a place a measurement can be taken at all. The shim snapshots what it cares about on the way in, compares on the way out, and books the difference on `InspectorLedger` (`../limit/inspector_ledger.rs`), which travels out on `MegaTransactionOutcome::inspector_ledger`. +Where a lane is booked also decides which specs book it. +A lane booked at a callback boundary is booked on every spec, because the shim itself is not spec-gated. +A lane booked at the frame's settlement point is booked from `MINI_REX` onwards, because `AdditionalLimit`'s frame settlement does not run before it — so under `EQUIVALENCE` a rewrite of a finished frame result's gas reaches the receipt with no lane recording it, and the block guard does not see it. +`tests/equivalence/pre_mini_rex_gates.rs` pins both halves. + Every gas lane is two numbers, because the ledger's two consumers ask different questions. The conservation law needs the **net**, since gas written into one object and taken back out of another really did leave the envelope where it was. The block guard needs the **gross**, since two edits that cancel are two edits: a `+1` before a frame reads its own remaining gas and a `−1` after it has read it net to nothing and leave the frame holding a number the EVM would never have produced, and the same pair split across a surviving frame and a rolled-back one moves what the sender pays. diff --git a/crates/mega-evm/tests/equivalence/pre_mini_rex_gates.rs b/crates/mega-evm/tests/equivalence/pre_mini_rex_gates.rs index 7da5f005..27d38ae2 100644 --- a/crates/mega-evm/tests/equivalence/pre_mini_rex_gates.rs +++ b/crates/mega-evm/tests/equivalence/pre_mini_rex_gates.rs @@ -1,6 +1,6 @@ //! Boundary coverage for the `EQUIVALENCE` side of the `MINI_REX` gates in the execution face. //! -//! Two properties are pinned here: +//! Three properties are pinned here: //! //! 1. `MegaHandler::before_run` promotes a transaction sent by the runtime system address into the //! OP deposit-style path — bypassing fee accounting — and rejects one whose callee is not @@ -9,20 +9,26 @@ //! 2. The whole `AdditionalLimit` subsystem is dormant before `MINI_REX`: no reset, no intrinsic //! accounting, and revm's stock instruction table, so every metered dimension stays at zero //! however much state a transaction touches. +//! 3. That dormancy reaches the frame settlement's inspector lanes too. An edit an inspector makes +//! to a frame result's gas is booked at `AdditionalLimit::finalize_frame`, which does not run +//! before `MINI_REX`, so under `EQUIVALENCE` the edit reaches the receipt with the ledger's +//! result lane — and therefore the block guard — reading it as untouched. What the measurement +//! shim books at its own callback boundaries is unaffected, because the shim is not spec-gated. use alloy_primitives::{address, Address, Bytes, U256}; use mega_evm::{ test_utils::{BytecodeBuilder, MemoryDatabase}, EmptyExternalEnv, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, MegaTransactionNew as _, - MEGA_SYSTEM_ADDRESS, ORACLE_CONTRACT_ADDRESS, + MegaTransactionOutcome, MEGA_SYSTEM_ADDRESS, ORACLE_CONTRACT_ADDRESS, }; use revm::{ bytecode::opcode::*, context::{BlockEnv, TxEnv}, handler::EvmTr, inspector::NoOpInspector, + interpreter::{CallInputs, CallOutcome, Interpreter}, primitives::TxKind, - Database as _, + Database as _, Inspector, }; /// A callee that is deliberately absent from `MEGA_SYSTEM_TX_WHITELIST`. @@ -171,3 +177,183 @@ fn test_equivalence_leaves_additional_limit_dormant() { assert_eq!(usage.kv_updates, 0, "pre-MINI_REX must not meter KV updates"); assert_eq!(usage.state_growth, 0, "pre-MINI_REX must not meter state growth"); } + +// --- the frame settlement's inspector lanes --------------------------------------------------- + +/// Sender of the two-frame transaction the inspector tests below rewrite. +const CHEAT_CALLER: Address = address!("0000000000000000000000000000000000200000"); +/// Outer contract: makes one inner call and stops. +const CHEAT_OUTER: Address = address!("0000000000000000000000000000000000200001"); +/// Inner contract: the frame whose result the inspector rewrites. +const CHEAT_INNER: Address = address!("0000000000000000000000000000000000200002"); + +/// How much gas each inspector below writes back into the EVM. +const CHEAT_AMOUNT: u64 = 1_000; + +/// Rewrites the gas of the inner frame's result at `call_end` — the last callback that can touch a +/// frame result, and the one whose edit `AdditionalLimit::finalize_frame` books. +#[derive(Default)] +struct ResultGasCheat { + rewrites: u32, +} + +impl Inspector> + for ResultGasCheat +{ + fn call_end( + &mut self, + _context: &mut MegaContext, + inputs: &CallInputs, + outcome: &mut CallOutcome, + ) { + if inputs.target_address == CHEAT_INNER { + self.rewrites += 1; + outcome.result.gas.erase_cost(CHEAT_AMOUNT); + } + } +} + +/// Writes the same amount into the interpreter's own gas counter instead, at the first `step_end`. +/// +/// The measurement shim books this one at its own callback boundary, with no help from the frame +/// settlement, which is what makes it the control for [`ResultGasCheat`]. +#[derive(Default)] +struct CounterGasCheat { + done: bool, +} + +impl Inspector> + for CounterGasCheat +{ + fn step_end( + &mut self, + interp: &mut Interpreter, + _context: &mut MegaContext, + ) { + if !self.done { + self.done = true; + interp.gas.erase_cost(CHEAT_AMOUNT); + } + } +} + +/// A database whose outer contract calls the inner one, so the transaction has a frame to settle +/// that is not the transaction's own. +fn cheat_db() -> MemoryDatabase { + let inner = BytecodeBuilder::default().stop().build(); + let outer = BytecodeBuilder::default() + .push_number(0u64) // retSize + .push_number(0u64) // retOffset + .push_number(0u64) // argsSize + .push_number(0u64) // argsOffset + .push_number(0u64) // value + .push_address(CHEAT_INNER) + .push_number(100_000u64) + .append(CALL) + .append(POP) + .append(STOP) + .build(); + MemoryDatabase::default() + .account_balance(CHEAT_CALLER, U256::from(INITIAL_BALANCE)) + .account_code(CHEAT_OUTER, outer) + .account_code(CHEAT_INNER, inner) +} + +fn cheat_tx() -> MegaTransaction { + let mut tx = MegaTransaction::new(TxEnv { + caller: CHEAT_CALLER, + kind: TxKind::Call(CHEAT_OUTER), + gas_limit: GAS_LIMIT, + gas_price: 0, + ..Default::default() + }); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// Runs [`cheat_tx`] over [`cheat_db`] under `spec`, optionally with an inspector attached. +fn transact_cheat(spec: MegaSpecId, inspector: Option<&mut I>) -> MegaTransactionOutcome +where + I: for<'a> Inspector>, +{ + let mut db = cheat_db(); + let mut context = MegaContext::new(&mut db, spec).with_block(BlockEnv { + beneficiary: BENEFICIARY, + number: U256::from(10), + basefee: 0, + ..Default::default() + }); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + let outcome = match inspector { + Some(inspector) => { + MegaEvm::new(context).with_inspector(inspector).execute_transaction(cheat_tx()) + } + None => MegaEvm::new(context).execute_transaction(cheat_tx()), + }; + outcome.expect("the cheat fixture must produce a receipt") +} + +/// `AdditionalLimit::finalize_frame` is where an edit an inspector makes to a frame result's gas is +/// booked onto the ledger's result lane, and that settlement point starts at `MINI_REX` along with +/// the rest of the subsystem. Under `EQUIVALENCE` the rewrite still reaches the receipt — the +/// settlement is not what hands gas back to a caller — but nothing books it, so the lane the block +/// guard reads stays empty. +/// +/// The `MINI_REX` half of the same fixture is the contrast that gives the assertion its meaning: +/// one spec later, the identical rewrite over the identical bytecode books its full amount. +#[test] +fn test_equivalence_does_not_book_a_frame_result_gas_rewrite() { + for (spec, expected_lane) in + [(MegaSpecId::EQUIVALENCE, 0i128), (MegaSpecId::MINI_REX, CHEAT_AMOUNT as i128)] + { + let plain = transact_cheat::(spec, None); + let mut cheat = ResultGasCheat::default(); + let cheated = transact_cheat(spec, Some(&mut cheat)); + + assert_eq!( + cheat.rewrites, 1, + "{spec:?}: the fixture must reach the inner frame's call_end" + ); + assert_eq!( + cheated.result_and_state.result.tx_gas_used() + CHEAT_AMOUNT, + plain.result_and_state.result.tx_gas_used(), + "{spec:?}: the rewrite must reach the receipt on both sides of the gate", + ); + + let lane = cheated.inspector_ledger.result; + assert_eq!(lane.net(), expected_lane, "{spec:?}: result lane net"); + assert_eq!(lane.gross(), expected_lane.unsigned_abs(), "{spec:?}: result lane gross"); + assert_eq!( + cheated.inspector_ledger.is_zero(), + expected_lane == 0, + "{spec:?}: the block guard reads the ledger through is_zero, got {:?}", + cheated.inspector_ledger, + ); + } +} + +/// The other half of the same statement: it is the frame settlement that is dormant before +/// `MINI_REX`, not the measurement shim. An edit written into the interpreter's own gas counter is +/// booked at the callback boundary that measured it, so it reads the same on both specs. +#[test] +fn test_equivalence_still_books_an_interpreter_gas_rewrite() { + for spec in [MegaSpecId::EQUIVALENCE, MegaSpecId::MINI_REX] { + let mut cheat = CounterGasCheat::default(); + let cheated = transact_cheat(spec, Some(&mut cheat)); + + assert!(cheat.done, "{spec:?}: the fixture must reach step_end at least once"); + assert_eq!( + cheated.inspector_ledger.gas.net(), + CHEAT_AMOUNT as i128, + "{spec:?}: an interpreter-counter edit is booked on every spec", + ); + assert!( + !cheated.inspector_ledger.is_zero(), + "{spec:?}: the block guard must see it, got {:?}", + cheated.inspector_ledger, + ); + } +} diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index d6bfc9de..92c92041 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -392,3 +392,55 @@ file = "crates/mega-evm/src/evm/instructions.rs" mutant = "replace += with *= in rex7::instruction_table" justification = "Dead/non-terminating: i *= 1 does not advance the INHERITED_FROM_REX6 copy loop, so instruction_table (a const fn) never finishes evaluating. The mutant times out at build rather than yielding a wrong table that a test could reject. Structurally unkillable; the += → -= and loop-bound mutants on the same loop are caught." reviewer = "RealiCZ (cz)" + +# --- execution.rs frame-settlement MINI_REX gates, EQUIVALENCE side (equivalent) -- +# +# Two of the three spec-gate survivors on the shared frame-exit body are equivalent; the +# third — the `finalize_frame` gate itself (execution.rs:613) — is a real gap and is killed +# by `tests/equivalence/pre_mini_rex_gates.rs` +# (`test_equivalence_does_not_book_a_frame_result_gas_rewrite`), so it has no entry here. +# +# `execution.rs` carries the same gate text at several sites (the identical +# `let is_mini_rex_enabled = self.ctx()...` line also appears in `init_frame_unsettled`), so +# both entries use the full `file:line:col:` form and suppress exactly one site each. + +# `gas_remaining_before` is read by exactly one consumer, `MegaEvm::settle_post_action_charge`, +# whose own `MINI_REX` gate is a separate (unmutated) site and returns before it looks at the +# argument. Capturing the value under EQUIVALENCE therefore reads a `u64` that is then thrown +# away, and no test can distinguish it. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "crates/mega-evm/src/evm/execution.rs:632:1: spec-gate let gas_remaining_before = match (&action, ctx.spec.is_enabled(MegaSpecId::MINI_REX)) { -> let gas_remaining_before = match (&action, ctx.spec.is_enabled(MegaSpecId::EQUIVALENCE)) {" +justification = "Equivalent: the captured value's only consumer is MegaEvm::settle_post_action_charge, whose own MINI_REX gate is a different site and is unmutated, so it returns before reading the argument. Nothing else reads the local. Under EQUIVALENCE the mutant only makes a u64 read whose value is discarded. Full mega-evm suite green with the mutant applied." +reviewer = "RealiCZ (cz) via S1 spec-gate triage" + +# The uninspected `frame_init`. Under EQUIVALENCE the mutant runs +# `AdditionalLimit::finalize_frame(result, exit, 0)`, and every branch of it is structurally +# inert there: +# - `absorb_frame_local_exceed`, `settle_exceptional_halt_burn` and +# `settle_frame_init_reject_burn` all return at `checkpoint.rex7_enabled()`; +# - `staged_precompile` is always `None`, because `stage_precompile_envelope` returns at the +# same REX7 gate; +# - the inspector delta is the literal `0` this call site passes (there is no inspector on +# this path, and only the measurement shim stages `staged_action_result_gas`), so +# `book_crossing` adds `0.unsigned_abs()` to the gross and `settle_inspector_result_gas` +# returns immediately without booking; +# - `try_rescue_gas` runs on a `Refused` exit, but `check_limit()` cannot latch: every +# `EvmTxRuntimeLimits::equivalence()` dimension is `u64::MAX`, every frame-local branch is +# `rex4_enabled`-gated off, and no usage is ever recorded because every recorder sits behind +# its own MINI_REX gate. Both of its effects are unreadable pre-MINI_REX anyway — +# `rescued_gas` is consumed only inside `last_frame_result`'s `is_mini_rex` branch, and every +# reader of `has_exceeded_limit` is behind an unmutated MINI_REX gate — so this holds even +# for a caller that installs finite limits on an EQUIVALENCE context via +# `with_tx_runtime_limits`. +# `FrameExit::Ran` is unreachable from frame init, so the `Ran` arm's debug_assert is not a +# `#[should_panic]` candidate. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/execution.rs" +mutant = "crates/mega-evm/src/evm/execution.rs:1673:1: spec-gate let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); -> let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::EQUIVALENCE);" +justification = "Equivalent: this is the uninspected frame_init, so the mutant runs AdditionalLimit::finalize_frame(result, exit, 0) under EQUIVALENCE and every branch is inert. absorb_frame_local_exceed / settle_exceptional_halt_burn / settle_frame_init_reject_burn return at checkpoint.rex7_enabled(); staged_precompile is always None because stage_precompile_envelope returns at the same REX7 gate; the inspector delta is the literal 0 this site passes, so book_crossing books nothing and settle_inspector_result_gas returns without booking; try_rescue_gas's check_limit() cannot latch under EQUIVALENCE (every EvmTxRuntimeLimits::equivalence() dimension is u64::MAX, every frame-local branch is rex4_enabled-gated off, and no usage is ever recorded), and both of its effects are unreadable pre-MINI_REX regardless — rescued_gas is consumed only in last_frame_result's is_mini_rex branch and every reader of has_exceeded_limit is behind an unmutated MINI_REX gate. FrameExit::Ran is unreachable from frame init. Full mega-evm suite green with the mutant applied." +reviewer = "RealiCZ (cz) via S1 spec-gate triage" From 866762b139fd3e73b429200f5a4dc7fd72c88d97 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:39:24 +0800 Subject: [PATCH 194/208] feat(block): admit an inspected transaction on a declaration, not on an empty ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty `InspectorLedger` was the canonical block path's admission criterion, and it cannot be: the measurement shim compares what it is handed across a callback boundary, so an inspector that edits the interpreter's stack or memory contents, or writes the journal directly, changes what the transaction produces and leaves every lane at zero. The path now takes an EVM running no inspector, or one whose type its author declared `TrustedObserver`, and refuses everything else with a new `MegaBlockExecutionError::UndeclaredInspector` — before the transaction runs, so an undeclared inspector's callbacks never reach the executor's state cache either. Live in release builds, and it fails the block rather than the process. The signal travels on `MegaTransactionOutcome::undeclared_inspector`, so the commit funnel can refuse a result produced by another executor instance or by an embedder driving the EVM itself. `InspectorAdjustedAccounting` stays as the backstop behind it, read at the same three entries: a declaration that did not hold, and a result arriving at the commit funnel already carrying a rewrite. `MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is the entry a node tracing block production takes; `create_executor`'s `debug_assert!` forbidding a declared observer is gone, since a declared observer is now what that path wants. `bin/mega-evme`'s replay command carries the forwarding newtype a foreign tracer needs. --- AGENTS.md | 6 +- bin/mega-evme/src/common/trace.rs | 99 ++- bin/mega-evme/src/replay/cmd.rs | 10 +- crates/mega-evm/src/block/executor.rs | 79 ++- crates/mega-evm/src/block/factory.rs | 70 ++- crates/mega-evm/src/block/result.rs | 66 +- crates/mega-evm/src/evm/AGENTS.md | 15 +- crates/mega-evm/src/evm/mod.rs | 43 +- crates/mega-evm/src/evm/result.rs | 20 +- crates/mega-evm/src/test_utils/inspectors.rs | 6 + .../tests/block_executor/inspector.rs | 50 +- .../tests/block_executor/inspector_guard.rs | 580 +++++++++++------- crates/mega-state-test/src/chaos.rs | 22 +- tools/eest-sweep/README.md | 2 +- 14 files changed, 764 insertions(+), 304 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d4cb9ee3..9681906b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -140,8 +140,10 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The receipt's other two numbers have lanes of their own, measured at two different points because `MegaETH` produces one of the two quantities and none of the other: a refund is booked nominally across the callback boundary, since only a difference there separates an inspector's share from the EVM's own refunds, while the EIP-8037 state-gas dimension (`reservoir` and `state_gas_spent`, on a `Gas` or on a call's inputs) is settled once from the figures the transaction ends with — revm propagates it by replacement rather than accumulation, so a boundary difference would book edits the EVM goes on to erase. The reservoir is a term of the conservation law because it lowers the envelope the receipt reports; the refund and the spend counter are not, and are refused by the block guard rather than accounted for. `crates/mega-evm/src/evm/AGENTS.md` carries the closed per-field enumeration, pinned by `tests/rex7/gas_surface.rs`, which also fails on any row left saying a surface reaches the receipt and no lane books it. - The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, and the canonical block path — `run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through — refuses a transaction whose ledger is non-zero with `MegaBlockExecutionError::InspectorAdjustedAccounting`, in release builds as well as debug. - Observation is untouched (a tracer's ledger is empty, which is what every inspector on that path is today); an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. + The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, but it is not what a block is admitted on: an inspector can edit the interpreter's stack or memory contents, or write the journal directly, and change the transaction while leaving every lane at zero. + Admission rests on a `TrustedObserver` declaration instead — a line written in source about one concrete type — and the canonical block path (`run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through) refuses a transaction from an EVM running an undeclared inspector with `MegaBlockExecutionError::UndeclaredInspector`, before running it, in release builds as well as debug; `MegaTransactionOutcome::undeclared_inspector` is what carries the answer to the commit funnel. + The ledger is the backstop behind that, read at the same entries as `MegaBlockExecutionError::InspectorAdjustedAccounting`, for a declaration that did not hold and for a result reaching the funnel from a producer this executor never saw. + A tracer keeps working by being declared — `MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is the entry, and `bin/mega-evme`'s replay command the worked example of the newtype a foreign tracer needs; an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index feba4a70..96b93faf 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; -use alloy_primitives::Bytes; +use alloy_primitives::{Address, Bytes, Log, U256}; use alloy_rpc_types_trace::geth::{ CallConfig, CallFrame, GethDefaultTracingOptions, PreStateConfig, }; @@ -14,16 +14,103 @@ use mega_evm::{ ContextTr, }, database::DatabaseRef, + handler::FrameResult, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, + InterpreterTypes, + }, state::EvmState, - ExecuteEvm, InspectEvm, + ExecuteEvm, InspectEvm, Inspector, }, - MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, + MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, TrustedObserver, }; use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; use tracing::{debug, info, trace}; use super::{EvmeError, EvmeExternalEnvs, EvmeState}; +/// A read-only declaration around `revm-inspectors`' tracer, for the block-execution path. +/// +/// `MegaBlockExecutor` refuses a transaction from an EVM running an inspector whose type carries +/// no [`TrustedObserver`] declaration, because its measurement shim cannot see an edit made to the +/// interpreter's stack contents or straight into the journal, and block production and block +/// validation have to agree on every node. [`TracingInspector`] writes nothing back to the EVM, +/// but the declaration cannot be made about it here: both it and the trait are foreign to this +/// crate, so it is made about a local newtype that forwards every callback unchanged. That is the +/// same shape a node keeping tracing on block production has to write. +#[derive(Debug)] +pub struct TrustedTracingInspector(pub TracingInspector); + +impl TrustedObserver for TrustedTracingInspector {} + +impl Inspector for TrustedTracingInspector +where + INTR: InterpreterTypes, + TracingInspector: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step_end(interp, context); + } + + fn log(&mut self, context: &mut CTX, log: Log) { + self.0.log(context, log); + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.log_full(interp, context, log); + } + + fn frame_start( + &mut self, + context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + self.0.frame_start(context, frame_input) + } + + fn frame_end( + &mut self, + context: &mut CTX, + frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + self.0.frame_end(context, frame_input, frame_result); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.0.selfdestruct(contract, target, value); + } +} + /// Tracer type for execution analysis #[derive(Debug, Clone, Copy, ValueEnum, Default)] #[non_exhaustive] @@ -103,6 +190,12 @@ impl TraceArgs { TracingInspector::new(config) } + /// The same tracer, wrapped in the declaration the canonical block-execution path admits an + /// inspected transaction on. + pub fn create_trusted_inspector(&self) -> TrustedTracingInspector { + TrustedTracingInspector(self.create_inspector()) + } + /// Creates [`GethDefaultTracingOptions`] from CLI arguments pub fn create_geth_options(&self) -> GethDefaultTracingOptions { GethDefaultTracingOptions { diff --git a/bin/mega-evme/src/replay/cmd.rs b/bin/mega-evme/src/replay/cmd.rs index 571d7714..77e3ce8a 100644 --- a/bin/mega-evme/src/replay/cmd.rs +++ b/bin/mega-evme/src/replay/cmd.rs @@ -470,10 +470,12 @@ impl Cmd { ); let start = Instant::now(); - let mut inspector = self.trace_args.create_inspector(); + // The tracer reaches the canonical block path through its read-only declaration: the + // executor refuses a transaction from an EVM running an undeclared inspector. + let mut inspector = self.trace_args.create_trusted_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( + let mut block_executor = block_executor_factory.create_executor_with_trusted_inspector( &mut state, block_ctx, evm_env, @@ -520,7 +522,7 @@ impl Cmd { .map(|acc| acc.nonce) .unwrap_or(0); - block_executor.inspector_mut().fuse(); + block_executor.inspector_mut().0.fuse(); let outcome = block_executor .run_transaction(wrapped_tx) .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?; @@ -547,7 +549,7 @@ impl Cmd { let trace_data = self.trace_args.is_tracing_enabled().then(|| { self.trace_args.generate_trace( - block_executor.inspector(), + &block_executor.inspector().0, &result_and_state, block_executor.evm().db_ref(), ) diff --git a/crates/mega-evm/src/block/executor.rs b/crates/mega-evm/src/block/executor.rs index 77fe067b..846ea3aa 100644 --- a/crates/mega-evm/src/block/executor.rs +++ b/crates/mega-evm/src/block/executor.rs @@ -78,21 +78,48 @@ impl core::fmt::Debug for MegaBlockExecutor } } -/// Refuses a transaction an inspector took part in, on the canonical path. +/// Refuses a transaction whose inspector was never declared read-only, on the canonical path. /// /// Block production and block validation are the two places where what the executor reports has to -/// be what the EVM did, reproducibly, on every node. An inspector's edits reach the receipt, the -/// block's counters and the transaction's state, but live in one node's configuration, so a -/// transaction carrying any is not something this executor may run or admit — see -/// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]. +/// be what the EVM did, reproducibly, on every node. An inspector lives in one node's +/// configuration and its edits reach the receipt, the block's counters and the transaction's +/// state, so the canonical path runs one only on the strength of a +/// [`TrustedObserver`](crate::TrustedObserver) declaration — see +/// [`MegaBlockExecutionError::UndeclaredInspector`]. /// -/// The criterion is the whole ledger, not its gas lanes. A rewrite of a frame's classification or -/// output, or a frame the inspector answered itself, moves no gas anywhere and would pass a -/// gas-only check while producing different state and a different receipt. +/// The criterion is the declaration and not the measurement, because the measurement cannot answer +/// the question. The shim compares what it is handed across a callback boundary; an inspector that +/// edits the interpreter's stack or memory contents, or writes the journal directly, changes the +/// transaction and leaves every lane at zero. A declaration is what someone asserts in source about +/// a concrete type, which is the only thing that reaches inside a callback. /// /// Enforced in release builds, deliberately. This is a boundary the canonical path holds against /// its embedder rather than an invariant `MegaETH` maintains internally, so it has to hold in the /// binaries that build and validate blocks, and it fails the block rather than the process. +#[inline] +fn reject_undeclared_inspector( + tx_hash: B256, + undeclared_inspector: bool, +) -> Result<(), BlockExecutionError> { + if undeclared_inspector { + return Err(BlockExecutionError::other( + crate::MegaBlockExecutionError::UndeclaredInspector { tx_hash }, + )); + } + Ok(()) +} + +/// Refuses a result whose gas accounting an inspector is measured to have moved. +/// +/// The backstop behind [`reject_undeclared_inspector`], for what a declaration does not cover: a +/// declared type that did not keep its promise, and a result that reaches the commit funnel from +/// somewhere this executor cannot see — another executor instance, an embedder driving +/// [`crate::MegaEvm::execute_transaction`] itself, or a value built by hand. The result's own +/// ledger is the only thing at that funnel that knows anything about how it was produced. +/// +/// The criterion is the whole ledger, not its gas lanes. A rewrite of a frame's classification or +/// output, or a frame the inspector answered itself, moves no gas anywhere and would pass a +/// gas-only check while producing different state and a different receipt. /// /// The check is free on every path that passes it: the ledger is a `Copy` struct already on the /// outcome, and this reads its fields once per transaction. @@ -106,7 +133,7 @@ fn reject_inspector_adjusted_accounting( } Err(BlockExecutionError::other(crate::MegaBlockExecutionError::InspectorAdjustedAccounting { tx_hash, - ledger, + ledger: std::boxed::Box::new(ledger), })) } @@ -505,11 +532,15 @@ where /// /// # Contract /// - /// A transaction whose gas accounting an inspector adjusted is refused with + /// An EVM running an inspector whose type carries no + /// [`TrustedObserver`](crate::TrustedObserver) declaration refuses the transaction with + /// [`MegaBlockExecutionError::UndeclaredInspector`]( + /// crate::MegaBlockExecutionError::UndeclaredInspector) before executing it, and a result + /// whose gas accounting an inspector is measured to have moved is refused with /// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]( - /// crate::MegaBlockExecutionError::InspectorAdjustedAccounting) rather than returned. An - /// observation-only inspector — which is every tracer — is unaffected; an embedder that wants - /// a rewriting one drives [`crate::MegaEvm::execute_transaction`] directly. + /// crate::MegaBlockExecutionError::InspectorAdjustedAccounting) after. A declared tracer is + /// unaffected; an embedder that wants a rewriting inspector drives + /// [`crate::MegaEvm::execute_transaction`] directly. pub fn run_transaction_with_sizes( &mut self, tx: Tx, @@ -519,6 +550,11 @@ where where Tx: IntoTxEnv + RecoveredTx + Copy, { + // Before anything else, including execution: an undeclared inspector does not run on this + // path at all. Refusing after the fact would leave its callbacks a window in which to + // reach the executor's own state cache through `db_mut()`. + reject_undeclared_inspector(tx.tx().tx_hash(), self.evm.has_undeclared_inspector())?; + let is_deposit = tx.tx().ty() == DEPOSIT_TRANSACTION_TYPE; // Check transaction-level and block-level limits before transaction execution @@ -568,6 +604,10 @@ where where Rec: RecoveredTx, { + // Same order as `run_transaction_with_sizes`: the inspector's declaration is settled + // before the transaction runs. + reject_undeclared_inspector(recovered.tx().tx_hash(), self.evm.has_undeclared_inspector())?; + let is_deposit = recovered.tx().ty() == DEPOSIT_TRANSACTION_TYPE; self.block_limiter.pre_execution_check( @@ -623,8 +663,11 @@ where /// /// This is also where a result an inspector took part in is refused, ahead of admission and /// of any other reading: the producers guard their own outputs, but a result reaching this - /// funnel may have been produced by another executor instance or built by hand, and the - /// outcome's own ledger is the only thing here that knows. See + /// funnel may have been produced by another executor instance, by an embedder driving + /// [`crate::MegaEvm::execute_transaction`] itself, or built by hand. What the outcome carries + /// is the only thing here that knows how it was produced — the inspector's declaration, and + /// then the ledger. See [`MegaBlockExecutionError::UndeclaredInspector`]( + /// crate::MegaBlockExecutionError::UndeclaredInspector) and /// [`MegaBlockExecutionError::InspectorAdjustedAccounting`]( /// crate::MegaBlockExecutionError::InspectorAdjustedAccounting). pub fn commit_tx_result( @@ -655,11 +698,15 @@ where compute_gas_enforced, state_growth_used, inspector_ledger, + undeclared_inspector, }, } = result; // Before anything else, including admission: a result an inspector took part in is not - // one this block may contain at all, whether or not it would still fit. + // one this block may contain at all, whether or not it would still fit. The declaration + // is asked first, because it is the admission rule and the ledger is the backstop behind + // it — an undeclared inspector is refused whether or not anything it did was measurable. + reject_undeclared_inspector(tx_hash, undeclared_inspector)?; reject_inspector_adjusted_accounting(tx_hash, inspector_ledger)?; // Re-validate limits at commit time to handle parallel execution race conditions. diff --git a/crates/mega-evm/src/block/factory.rs b/crates/mega-evm/src/block/factory.rs index cab44e62..c0d0bab5 100644 --- a/crates/mega-evm/src/block/factory.rs +++ b/crates/mega-evm/src/block/factory.rs @@ -100,7 +100,61 @@ where MegaBlockExecutor::new(evm, block_ctx, self.hardforks.clone(), self.receipt_builder.clone()) } - /// Create a new block executor with an inspector. + /// Create a new block executor with a read-only inspector its type's author has declared + /// [`TrustedObserver`](crate::TrustedObserver). + /// + /// The declaration is what the canonical block-execution path admits an inspected transaction + /// on, so this is the entry a node tracing block production or validation takes. + /// [`create_executor_with_inspector`](Self::create_executor_with_inspector) builds an executor + /// that refuses every transaction it is given. + /// + /// A `revm-inspectors` tracer cannot be declared where both it and the trait are foreign, so a + /// node writes a forwarding newtype of its own and declares that; `bin/mega-evme`'s replay + /// command is the shape to copy. + /// + /// # Parameters + /// + /// - `db`: The database to use for EVM state. + /// - `evm_env`: The EVM environment, including block and config environments. + /// - `block_ctx`: The block execution context for tracking access patterns. + /// - `inspector`: The declared read-only inspector to observe execution with. + pub fn create_executor_with_trusted_inspector<'a, DB, I>( + &self, + db: &'a mut State, + block_ctx: MegaBlockExecutionCtx, + evm_env: EvmEnv, + inspector: I, + ) -> MegaBlockExecutor< + Hardforks, + MegaEvm<&'a mut State, I, ExtEnvFactory::EnvTypes>, + ReceiptBuilder, + > + where + DB: Database + 'a, + I: Inspector, ExtEnvFactory::EnvTypes>> + + crate::TrustedObserver + + 'a, + { + let runtime_limits = block_ctx.block_limits.to_evm_tx_runtime_limits(); + let evm = self + .evm_factory + .create_evm(db, evm_env) + .with_trusted_inspector(inspector) + .with_tx_runtime_limits(runtime_limits); + MegaBlockExecutor::new(evm, block_ctx, self.hardforks.clone(), self.receipt_builder.clone()) + } + + /// Create a new block executor with an inspector that carries no read-only declaration. + /// + /// The executor this builds refuses every transaction it is asked to run or admit, with + /// [`MegaBlockExecutionError::UndeclaredInspector`]( + /// crate::MegaBlockExecutionError::UndeclaredInspector) — the canonical path admits an + /// inspected transaction only on a [`TrustedObserver`](crate::TrustedObserver) declaration, + /// which this entry's bound does not ask for. It stays because the EVM underneath it is + /// reachable through [`MegaBlockExecutor::evm_mut`], which an embedder can drive itself. + /// + /// A tracer belongs on + /// [`create_executor_with_trusted_inspector`](Self::create_executor_with_trusted_inspector). /// /// # Parameters /// @@ -177,16 +231,10 @@ where DB: StateDB, I: Inspector<::Context>, { - // The canonical block path measures every inspector it runs, because the guard that - // refuses an inspector-adjusted transaction reads a ledger the measurement fills. A - // declared observer is delegated to unmeasured in release builds, so its ledger is empty - // by construction and the guard would be reading nothing. The two factory methods cannot - // build one; this entry point takes an EVM the caller built, so it says so here. - debug_assert!( - !evm.has_trusted_inspector(), - "a declared TrustedObserver must not be handed to the canonical block path: the \ - inspector guard reads a ledger that is empty by construction for one", - ); + // Nothing is checked about the inspector here. This entry takes an EVM the caller built, + // so its inspector may or may not carry a declaration, and the answer is a runtime one + // the executor's own entries ask per transaction — as an error that fails the block, not + // an assertion that stops the process. See `MegaBlockExecutionError::UndeclaredInspector`. // Synchronize EVM tx runtime limits with the block context's BlockLimits. // This mirrors the inherent factory paths above which apply this diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 8a51da96..1a21105b 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -1,3 +1,7 @@ +#[cfg(not(feature = "std"))] +use alloc as std; +use std::boxed::Box; + use alloy_evm::{block::TxResult, InvalidTxError}; use alloy_primitives::TxHash; use revm::{ @@ -258,6 +262,39 @@ impl InvalidTxError for MegaBlockLimitExceededError { /// transaction itself for a caller to fix. #[derive(Debug, Clone, thiserror::Error)] pub enum MegaBlockExecutionError { + /// A transaction reached the canonical block-execution path from an EVM running an inspector + /// whose type carries no [`TrustedObserver`](crate::TrustedObserver) declaration. + /// + /// This is the admission rule, and it is a rule about the *configuration* rather than about + /// what one run was observed to do. The measurement shim books what an inspector writes across + /// a callback boundary, but an inspector can reach past that boundary — editing the contents + /// of the interpreter's stack or memory, or writing the journal directly — and change what the + /// transaction produces while leaving every lane of + /// [`InspectorLedger`](crate::InspectorLedger) at zero. An empty ledger therefore cannot be + /// what a block is admitted on. + /// + /// What can is a declaration: a line written in source, about one concrete type, by someone + /// who had read it. So the canonical path takes an EVM running no inspector, or one whose + /// inspector was built through + /// [`MegaEvm::with_trusted_inspector`](crate::MegaEvm::with_trusted_inspector), and refuses + /// everything else — including an inspector that only observes, because the criterion is what + /// the type's author declared and not what this run happened to do. + /// + /// A tracer keeps working by being declared. `revm-inspectors`' tracers are foreign types and + /// the trait is local to this crate, so a node writes a forwarding newtype of its own and + /// declares that; `bin/mega-evme`'s replay command does exactly this. An embedder that wants a + /// rewriting inspector still has one — [`MegaEvm::execute_transaction`]( + /// crate::MegaEvm::execute_transaction) supports it in full — it just does not get to call the + /// result a block. + #[error( + "transaction {tx_hash} reached the canonical block-execution path from an EVM running an \ + inspector whose type carries no `TrustedObserver` declaration" + )] + UndeclaredInspector { + /// The transaction that was refused. + tx_hash: TxHash, + }, + /// A transaction an inspector took part in reached the canonical block-execution path. /// /// Block production and block validation must produce the same numbers for the same block, on @@ -275,13 +312,20 @@ pub enum MegaBlockExecutionError { /// Both are refused. The second is why the criterion is the whole ledger rather than its gas /// lanes: a rewrite that costs nothing is not a rewrite that changes nothing. /// - /// Observation is untouched: a tracer leaves an all-zero ledger, which is what every inspector - /// on this path today does. An embedder that genuinely wants a rewriting inspector still has - /// one — [`MegaEvm::execute_transaction`](crate::MegaEvm::execute_transaction) supports it in - /// full, with the ledger reported on the outcome — it just does not get to call the result a - /// block. That is also what leaves a simulation EVM an embedder drives off the canonical path - /// alone, however much its inspector rewrites: this guard sits on the block executor's - /// entries, not on the EVM. + /// This is the backstop behind [`UndeclaredInspector`](Self::UndeclaredInspector), not the + /// admission rule. An undeclared inspector is refused before it runs, so what is left for this + /// to catch is a declaration that did not hold — which debug builds measure and assert — and a + /// result that arrives at the commit funnel already carrying a non-zero ledger, produced by + /// another executor instance, by an embedder driving the EVM itself, or built by hand. The + /// commit funnel cannot see the EVM that produced such a result; the result's own ledger is + /// the only thing there that knows anything. + /// + /// An embedder that genuinely wants a rewriting inspector still has one — + /// [`MegaEvm::execute_transaction`](crate::MegaEvm::execute_transaction) supports it in full, + /// with the ledger reported on the outcome — it just does not get to call the result a block. + /// That is also what leaves a simulation EVM an embedder drives off the canonical path alone, + /// however much its inspector rewrites: this guard sits on the block executor's entries, not + /// on the EVM. #[error( "transaction {tx_hash} reached the canonical block-execution path after an inspector took \ part in it: {ledger:?}" @@ -290,7 +334,12 @@ pub enum MegaBlockExecutionError { /// The transaction the adjusted accounting belongs to. tx_hash: TxHash, /// What the measurement shim booked for that transaction. - ledger: crate::InspectorLedger, + /// + /// Boxed: the ledger is six signed lanes and two counters, which is most of this enum's + /// size, and every value of this type is boxed again into + /// [`BlockExecutionError::Internal`](alloy_evm::block::BlockExecutionError::Internal) the + /// moment it is built. + ledger: Box, }, } @@ -333,6 +382,7 @@ mod tests { compute_gas_enforced: 2, state_growth_used: 4, inspector_ledger: crate::InspectorLedger::default(), + undeclared_inspector: false, }; // One hop: MegaTransactionOutcome -> ResultAndState. diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 85205c93..da401f85 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -107,7 +107,7 @@ Booking is a *reported* quantity throughout. No resource limit is ever compared Measuring costs about a nanosecond per reading per opcode, and there are sixteen readings taken twice per opcode, which adds between a third and two thirds to a production tracer's run. An inspector type whose author has implemented `TrustedObserver` for it is delegated to without any of that: `MeasuredInspector::new_trusted`, reached through `MegaEvm::with_trusted_inspector`, builds a shim that forwards every callback and takes no reading. -The block guard is unchanged and needs no change — a declared type's ledger is empty by construction, which is the same answer measuring it would have given. +The declaration is also what the canonical block path admits an inspected transaction on, so the fast path and the block path are reached by the same statement about the type. **What the declaration promises.** Every callback of the type leaves the EVM exactly as it found it: nothing written to an interpreter's gas counter or its pending action, nothing to a frame's inputs, nothing to a frame result's classification, gas, output or metadata, nothing to a refund, and no frame answered with a synthetic outcome. It may read whatever it likes and write to its own state. @@ -141,11 +141,9 @@ Anything supplied by a request — a JavaScript tracer, an RPC-selected tracer c **How a node reaches it.** `EvmFactory::create_evm_with_inspector` cannot: its bound is `I: Inspector` and its return type is fixed, so it has no way to select the constructor. The route is `factory.create_evm(db, env).with_trusted_inspector(tracer)`, which keeps the factory's own dynamic precompiles and differs from the two-step untrusted form only in the method name. -The same limitation is most of a fence, and it is worth being exact about where it stops. -`MegaBlockExecutorFactory`'s own two factory methods cannot produce a declared EVM, so nothing a node reaches *through them* arrives on the canonical block path unmeasured. -But `create_executor` takes an EVM the caller already built, so `factory.create_evm(db, env).with_trusted_inspector(tracer)` handed to it does reach that path — the fence is a convention the node keeps, not something the types enforce. -`create_executor` therefore carries a `debug_assert!` on `MegaEvm::has_trusted_inspector`, which is what turns the convention into something a test build checks. -What a declaration is *for* is an EVM an embedder drives itself, which is what RPC tracing and off-band simulation are. +`MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is that route packaged for the block path, and it is the entry a node tracing block production or validation takes; `create_executor_with_inspector` builds an executor that refuses every transaction it is given, and stays only because the EVM under it is reachable through `evm_mut()`. +`create_executor` (the `BlockExecutorFactory` trait method) takes an EVM the caller already built and checks nothing about its inspector, because the question is a runtime one the executor's own entries ask per transaction — an error that fails the block rather than an assertion that stops the process. +`bin/mega-evme`'s replay command is the worked example of the whole shape: a `TrustedTracingInspector` newtype declared over `revm-inspectors`' tracer, handed to `create_executor_with_trusted_inspector`. ### The window a counter edit reaches nothing through @@ -228,8 +226,9 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Book a lane through `Lane::book`, never by writing its net.** The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. - **Keep every rewrite out of a block.** - Supporting a rewrite is not the same as admitting one: the canonical block-execution path refuses a transaction whose ledger is non-zero, in release builds as well as debug, because an inspector is one node's configuration and its edits reach the receipt. - That is why a rewrite which moves no gas still has to be booked — on `InspectorLedger::interventions` — or the guard admits it. + Supporting a rewrite is not the same as admitting one: the canonical block-execution path refuses a transaction from an EVM running an inspector its type never declared `TrustedObserver`, before running it, in release builds as well as debug — because an inspector is one node's configuration and its edits reach the receipt. + The criterion is the declaration rather than the ledger because the ledger cannot answer the question: an inspector that edits the interpreter's stack or memory contents, or writes the journal directly, changes the transaction and leaves every lane at zero. + The ledger stays as the backstop behind it, read at the same entries, for a declaration that did not hold and for a result reaching the commit funnel from a producer this executor never saw — which is why a rewrite that moves no gas still has to be booked, on `InspectorLedger::interventions`. An EVM an embedder drives itself is deliberately not covered: it produces no block, so there is nothing for two nodes to disagree about. See `tests/block_executor/inspector_guard.rs`. diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 739c521e..3e7f4985 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -209,6 +209,12 @@ impl MegaEvm { /// # Returns /// /// A new `Evm` instance with the specified inspector enabled. + /// + /// The inspector is measured, and the resulting EVM is one the canonical block-execution path + /// will not admit a transaction from: admission is on the strength of a + /// [`TrustedObserver`] declaration, which this constructor does not ask for. An inspector + /// whose type carries one reaches a block through + /// [`with_trusted_inspector`](Self::with_trusted_inspector) instead. pub fn with_inspector(self, inspector: I) -> MegaEvm { let mega_cfg = self.mega_cfg; let inner = revm::context::Evm::new_with_inspector( @@ -234,7 +240,13 @@ impl MegaEvm { /// [`EvmFactory::create_evm_with_inspector`](alloy_evm::EvmFactory::create_evm_with_inspector) /// cannot reach this — its bound is `I: Inspector` and its return type is fixed — so a node /// that builds through the factory takes `create_evm(..).with_trusted_inspector(..)`, which - /// keeps the factory's dynamic precompiles. + /// keeps the factory's dynamic precompiles. The block executor factory has its own entry, + /// [`MegaBlockExecutorFactory::create_executor_with_trusted_inspector`]( + /// crate::MegaBlockExecutorFactory::create_executor_with_trusted_inspector). + /// + /// The declaration is also what the canonical block-execution path admits an inspected + /// transaction on, so this is the constructor a node tracing block production or validation + /// has to reach. pub fn with_trusted_inspector( self, inspector: I, @@ -430,6 +442,7 @@ where ExecuteEvm::transact(self, tx)? }; let trusted_inspector = self.inner.inspector.is_trusted(); + let undeclared_inspector = self.has_undeclared_inspector(); let is_inside_sandbox = self.ctx().is_inside_sandbox(); let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); @@ -444,6 +457,7 @@ where compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, inspector_ledger: additional_limit.inspector_ledger(), + undeclared_inspector, }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); debug_assert_trusted_observer_kept_its_promise(trusted_inspector, &outcome); @@ -453,13 +467,32 @@ where /// Whether this EVM's inspector was built from a [`TrustedObserver`](crate::TrustedObserver) /// declaration, and so is delegated to unmeasured in release builds. /// - /// Read by a caller that must not be handed one. The block executor factory is the case that - /// matters: it takes an EVM its caller built, so nothing in its own signature keeps a declared - /// observer off the canonical block path. + /// A declaration is what the canonical block-execution path admits an inspected transaction + /// on, so this is the positive half of the question that path asks; the question itself is + /// [`has_undeclared_inspector`](Self::has_undeclared_inspector), which also accounts for an + /// EVM running no inspector at all. pub const fn has_trusted_inspector(&self) -> bool { self.inner.inspector.is_trusted() } + /// Whether this EVM runs an inspector whose type carries no + /// [`TrustedObserver`](crate::TrustedObserver) declaration. + /// + /// The canonical block-execution path refuses such a transaction outright, because what it + /// reports has to be what the EVM did on every node and the measurement shim cannot see an + /// edit made behind a callback boundary — the interpreter's stack or memory contents, or a + /// direct journal write. A declaration is a line someone wrote in source about a type they had + /// read, which is the only thing that answers that. + /// + /// False for an EVM with no inspector: revm's plain frame loop never calls one, so there is + /// nothing to declare. False for one built through + /// [`with_trusted_inspector`](Self::with_trusted_inspector). True for every other inspected + /// EVM, including one whose inspector only observes — the criterion is the declaration, not + /// the behaviour of one run. + pub const fn has_undeclared_inspector(&self) -> bool { + self.inspect && !self.inner.inspector.is_trusted() + } + /// Inspect a transaction and return the outcome. The inspector used is the one set up already /// in the EVM. Use [`MegaEvm::with_inspector`] to set up a custom inspector. /// @@ -480,6 +513,7 @@ where ) -> Result> { let result_and_state = InspectEvm::inspect_tx(self, tx)?; let trusted_inspector = self.inner.inspector.is_trusted(); + let undeclared_inspector = self.has_undeclared_inspector(); let is_inside_sandbox = self.ctx().is_inside_sandbox(); let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); @@ -494,6 +528,7 @@ where compute_gas_enforced: additional_limit.enforced_compute_gas(), state_growth_used: state_growth, inspector_ledger: additional_limit.inspector_ledger(), + undeclared_inspector, }; debug_assert_envelope_accounted(spec, is_inside_sandbox, &additional_limit, &outcome); debug_assert_trusted_observer_kept_its_promise(trusted_inspector, &outcome); diff --git a/crates/mega-evm/src/evm/result.rs b/crates/mega-evm/src/evm/result.rs index 50024b53..882f877c 100644 --- a/crates/mega-evm/src/evm/result.rs +++ b/crates/mega-evm/src/evm/result.rs @@ -113,7 +113,9 @@ pub struct MegaTransactionOutcome { /// is measured is what the shim can see at a callback boundary: gas that moved, and arguments /// that came back changed. An inspector that reaches past those — editing the interpreter's /// stack or memory, writing the journal directly, or editing the pending action — leaves this - /// empty while changing the state the transaction produces. + /// empty while changing the state the transaction produces. That is why block admission rests + /// on [`undeclared_inspector`](Self::undeclared_inspector) and this field is only the backstop + /// behind it. /// /// # What it is for /// @@ -125,6 +127,22 @@ pub struct MegaTransactionOutcome { /// of the conservation law — see [`ConservationTerms`](crate::ConservationTerms) — which is /// why an outcome carrying gas numbers is not fully described without it. pub inspector_ledger: crate::InspectorLedger, + + /// Whether an inspector whose type carries no + /// [`TrustedObserver`](crate::TrustedObserver) declaration took part in this transaction. + /// + /// This is the canonical block path's admission criterion, and it is deliberately *not* + /// [`inspector_ledger`](Self::inspector_ledger). The ledger reports what the measurement shim + /// could see; an inspector that reaches past a callback boundary — editing the interpreter's + /// stack or memory contents, or writing the journal directly — changes the transaction while + /// leaving every lane at zero. So an execution is admitted into a block on the strength of a + /// declaration made in source about the inspector's type, and refused without one. + /// + /// False for a transaction that ran with no inspector at all, and for one whose inspector was + /// built through [`MegaEvm::with_trusted_inspector`](crate::MegaEvm::with_trusted_inspector). + /// True for every other inspected run, including one whose inspector only observes: the + /// question is what the type's author declared, not what this particular run happened to do. + pub undeclared_inspector: bool, } /// Identifies which stage of block execution produced a state change. diff --git a/crates/mega-evm/src/test_utils/inspectors.rs b/crates/mega-evm/src/test_utils/inspectors.rs index b1e40a1d..2cd7a455 100644 --- a/crates/mega-evm/src/test_utils/inspectors.rs +++ b/crates/mega-evm/src/test_utils/inspectors.rs @@ -270,6 +270,12 @@ impl GasInspector { } } +/// Read-only: every callback records into this inspector's own trace tree and returns the EVM +/// exactly what it was handed — `None` from both frame-entry callbacks, and no write to any +/// interpreter, frame input or outcome it is shown. Declared so that tests exercising the +/// canonical block-execution path with an inspector can be admitted by it. +impl crate::TrustedObserver for GasInspector {} + impl Inspector for GasInspector { fn call(&mut self, _context: &mut CTX, inputs: &mut CallInputs) -> Option { // Create a new trace node for this call diff --git a/crates/mega-evm/tests/block_executor/inspector.rs b/crates/mega-evm/tests/block_executor/inspector.rs index 0a811b93..b275032c 100644 --- a/crates/mega-evm/tests/block_executor/inspector.rs +++ b/crates/mega-evm/tests/block_executor/inspector.rs @@ -6,7 +6,7 @@ use std::{cell::Cell, convert::Infallible}; use alloy_consensus::{Signed, TxLegacy}; -use alloy_evm::{block::BlockExecutor, EvmEnv}; +use alloy_evm::{block::BlockExecutor, EvmEnv, IntoTxEnv}; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use mega_evm::{ @@ -116,12 +116,13 @@ fn test_inspector_works_with_block_executor() { let block_ctx = MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits()); - // Create inspector + // Create inspector. `GasInspector` only records, and its type says so — which is what the + // canonical block path admits an inspected transaction on. let inspector = GasInspector::new(); // Create block executor with inspector let mut executor = block_executor_factory - .create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector); + .create_executor_with_trusted_inspector(&mut state, block_ctx, evm_env, inspector); // Execute transaction let tx = create_transaction(0, 1_000_000); @@ -286,16 +287,19 @@ fn test_inspector_early_return_with_additional_limits() { // Execute transaction - this triggers a nested CALL that the inspector intercepts let tx = create_transaction(0, 1_000_000); - // Before the fix, this would panic with "frame stack is empty". It runs to completion now, - // and the canonical path then declines to admit it — an inspector that answers a frame itself - // is a rewriting inspector, which `inspector_guard` covers. That refusal is the assertion the - // alignment rests on: reaching it at all means every push found its pop. - let err = executor - .execute_transaction(&tx) - .expect_err("the canonical path refuses an intercepting inspector"); - assert!( - format!("{err:?}").contains("interventions: 1"), - "the refusal must name the interception it saw: {err:?}", + // Driven through the executor's EVM rather than through the executor: an inspector that + // answers a frame itself is a rewriting inspector, and the canonical path admits an inspected + // transaction only on a read-only declaration, which this type cannot be given. The EVM + // supports the interception in full, which is what this test is about — before the fix it + // panicked with "frame stack is empty", so completing at all means every push found its pop. + let outcome = executor + .evm_mut() + .execute_transaction(tx.into_tx_env()) + .expect("the EVM supports the interception in full"); + assert_eq!( + outcome.inspector_ledger.interventions, 1, + "the interception must be measured: {:?}", + outcome.inspector_ledger, ); // Verify the inspector intercepted the nested call @@ -398,15 +402,17 @@ fn test_inspector_early_return_create_with_additional_limits() { let init_code = Bytes::from(vec![0x00]); let tx = create_deploy_transaction(0, 10_000_000, init_code); - // Before the fix, this would panic with "frame stack is empty". As above, the transaction now - // runs to completion and the canonical path declines to admit it; getting as far as the - // refusal is what says the frame stacks stayed aligned. - let err = executor - .execute_transaction(&tx) - .expect_err("the canonical path refuses an intercepting inspector"); - assert!( - format!("{err:?}").contains("interventions: 1"), - "the refusal must name the interception it saw: {err:?}", + // Driven through the EVM for the same reason as above: an intercepting inspector has no place + // on the canonical block path, and getting as far as a completed outcome is what says the + // frame stacks stayed aligned. + let outcome = executor + .evm_mut() + .execute_transaction(tx.into_tx_env()) + .expect("the EVM supports the interception in full"); + assert_eq!( + outcome.inspector_ledger.interventions, 1, + "the interception must be measured: {:?}", + outcome.inspector_ledger, ); // Verify the inspector intercepted the create operation diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 8dfcaec0..7f12a3e2 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -1,4 +1,4 @@ -//! The canonical block-execution path admits no transaction an inspector took part in. +//! The canonical block-execution path admits an inspected transaction only on a declaration. //! //! `MegaETH` supports rewriting inspectors in full — the measurement shim books what they do and //! the conservation law accounts for it — but supporting a rewrite is not the same as letting it @@ -7,40 +7,45 @@ //! gas counter reaches the receipt, the transaction's reported compute total, and through it the //! block's cumulative counters. //! -//! It can also rewrite what a frame *did* — its classification, its output, or the frame itself, -//! answered with a synthetic outcome — which moves no gas anywhere and reaches the transaction's -//! state and its receipt directly. A gas-only criterion would admit every one of those, so the -//! criterion is the whole ledger: no gas the EVM did not move, and nothing the shim was handed -//! coming back changed. +//! What it can also do is reach past every boundary the shim watches. Editing the contents of the +//! interpreter's stack or its memory, or writing the journal directly, changes what the +//! transaction produces and leaves every lane of the ledger at zero — so an empty ledger cannot be +//! what a block is admitted on. What can is a `TrustedObserver` declaration: a line written in +//! source, about one concrete type, by someone who had read it. //! //! So every entry on the canonical path — the two that run a transaction and the one funnel that -//! admits a result — refuses a non-zero ledger. The refusal is an error rather than an assertion, -//! because it is a boundary held against an embedder and has to hold in the binaries that build -//! and validate blocks; the tests here therefore pass identically in debug and release builds. +//! admits a result — refuses an inspector its type never declared, *before* running it. The ledger +//! is kept as the backstop behind that: it catches a declaration that did not hold, and a result +//! that reaches the commit funnel already carrying a rewrite from somewhere this executor cannot +//! see. Both refusals are errors rather than assertions, because they are boundaries held against +//! an embedder and have to hold in the binaries that build and validate blocks; the tests here +//! therefore pass identically in debug and release builds. //! //! The green half matters as much as the red: every inspector on this path today is a tracer, and -//! a tracer must keep working. That is what the observation tests pin. +//! a tracer must keep working. That is what the declared-observer tests pin. use std::convert::Infallible; use alloy_evm::{block::BlockExecutor, EvmEnv}; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; -use alloy_primitives::{address, Address, Bytes, Signature, TxHash, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, Log, Signature, TxHash, TxKind, B256, U256}; use mega_evm::{ alloy_consensus::{transaction::Recovered, Signed, TxLegacy}, alloy_evm::block::BlockExecutionError, test_utils::{BytecodeBuilder, MemoryDatabase}, BlockLimits, InspectorLedger, Lane, MegaBlockExecutionCtx, MegaBlockExecutorFactory, - MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, MegaTxEnvelope, - TestExternalEnvs, + MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, + MegaTransactionOutcome, MegaTxEnvelope, TestExternalEnvs, TrustedObserver, }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, context::{BlockEnv, Cfg, ContextTr}, database::State, + handler::FrameResult, + inspector::NoOpInspector, interpreter::{ - interpreter_types::MemoryTr, CallInputs, CallOutcome, InstructionResult, Interpreter, - InterpreterTypes, + interpreter_types::MemoryTr, CallInputs, CallOutcome, CreateInputs, CreateOutcome, + FrameInput, InstructionResult, Interpreter, InterpreterTypes, }, Inspector, }; @@ -81,7 +86,7 @@ impl Inspector for GasInjector { /// Every one of the ledger's gas lanes stays at zero under this inspector: the call's remaining /// gas, its envelope and every interpreter counter are exactly what the EVM left. What changes is /// what the transaction did — the callee's storage write is rolled back and the caller reads a -/// failure — which is why the guard cannot be a gas-only check. +/// failure — which is why the ledger cannot be a gas-only check. #[derive(Default)] struct CallFailer { applied: bool, @@ -150,7 +155,8 @@ impl Inspector for MemoryGrow } } -/// Counts callbacks and changes nothing — the shape every tracer in production has. +/// Counts callbacks and changes nothing — the shape every tracer in production has, with no +/// declaration about its type. #[derive(Default)] struct Observer { steps: u64, @@ -162,6 +168,102 @@ impl Inspector for Observer { } } +/// The same observer, with its author's declaration that it writes nothing back. +/// +/// A separate type rather than a declaration on [`Observer`], because the pair is the experiment: +/// the two behave identically and only one of them is admitted, which is what says the criterion +/// is the declaration and not the behaviour. +#[derive(Default)] +struct DeclaredObserver { + inner: Observer, +} + +impl TrustedObserver for DeclaredObserver {} + +impl Inspector for DeclaredObserver { + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.inner.step(interp, context); + } +} + +/// A read-only declaration around `revm-inspectors`' geth tracer. +/// +/// The orphan rule keeps `TrustedObserver` from being implemented for `TracingInspector` directly +/// — from a downstream node both are foreign — so the declaration is made about a local newtype +/// that forwards every callback unchanged. `bin/mega-evme`'s replay command carries the same shape +/// for the same reason, and it is the shape a node writes to keep tracing block production. +struct TrustedTracer(revm_inspectors::tracing::TracingInspector); + +impl TrustedObserver for TrustedTracer {} + +impl Inspector for TrustedTracer +where + INTR: InterpreterTypes, + revm_inspectors::tracing::TracingInspector: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step_end(interp, context); + } + + fn log(&mut self, context: &mut CTX, log: Log) { + self.0.log(context, log); + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.log_full(interp, context, log); + } + + fn frame_start( + &mut self, + context: &mut CTX, + frame_input: &mut FrameInput, + ) -> Option { + self.0.frame_start(context, frame_input) + } + + fn frame_end( + &mut self, + context: &mut CTX, + frame_input: &FrameInput, + frame_result: &mut FrameResult, + ) { + self.0.frame_end(context, frame_input, frame_result); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.0.selfdestruct(contract, target, value); + } +} + fn envelope(nonce: u64) -> MegaTxEnvelope { let tx = TxLegacy { chain_id: Some(8453), @@ -232,98 +334,102 @@ fn block_ctx() -> MegaBlockExecutionCtx { MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits()) } -/// Unwraps the refusal, checking it is the one this module is about and that it names the -/// transaction and the measurement it refused over. +/// Unwraps `MegaETH`'s own error out of the `alloy_evm` boxing. /// /// Reached by downcast rather than by matching the message: the error crosses the `alloy_evm` /// boundary as a boxed `dyn Error`, and a consumer that wants to react to it — a sequencer that /// would rather drop the transaction than fail the block — has to get the typed value back. #[track_caller] -fn expect_refusal(err: &BlockExecutionError, expected_hash: TxHash) -> InspectorLedger { +fn expect_mega_error(err: &BlockExecutionError) -> &mega_evm::MegaBlockExecutionError { let internal = err.as_internal().unwrap_or_else(|| { panic!("the refusal must be an internal error, not a verdict on the transaction: {err:?}") }); let other = internal .as_other() .unwrap_or_else(|| panic!("the refusal must carry MegaETH's own error: {internal:?}")); - let mega = other + other .downcast_ref::() - .unwrap_or_else(|| panic!("the refusal must survive the boxing as a typed value: {other}")); - let mega_evm::MegaBlockExecutionError::InspectorAdjustedAccounting { tx_hash, ledger } = mega; - assert_eq!(*tx_hash, expected_hash, "the refusal must name the transaction it refused"); - assert!(!ledger.is_zero(), "a refusal over an empty ledger is a refusal of nothing"); - *ledger + .unwrap_or_else(|| panic!("the refusal must survive the boxing as a typed value: {other}")) +} + +/// Asserts the refusal is the admission rule's, and that it names the transaction. +#[track_caller] +fn expect_undeclared(err: &BlockExecutionError, expected_hash: TxHash) { + match expect_mega_error(err) { + mega_evm::MegaBlockExecutionError::UndeclaredInspector { tx_hash } => { + assert_eq!(*tx_hash, expected_hash, "the refusal must name the transaction it refused"); + } + other => panic!("expected the undeclared-inspector refusal, got {other:?}"), + } } -/// The producer entry: a transaction an inspector adjusted never becomes an outcome the block -/// path will hand back. +/// Asserts the refusal is the ledger backstop's, and returns what it was refused over. +#[track_caller] +fn expect_adjusted(err: &BlockExecutionError, expected_hash: TxHash) -> InspectorLedger { + match expect_mega_error(err) { + mega_evm::MegaBlockExecutionError::InspectorAdjustedAccounting { tx_hash, ledger } => { + assert_eq!(*tx_hash, expected_hash, "the refusal must name the transaction it refused"); + assert!(!ledger.is_zero(), "a refusal over an empty ledger is a refusal of nothing"); + **ledger + } + other => panic!("expected the ledger backstop's refusal, got {other:?}"), + } +} + +/// Runs the fixture transaction on an EVM the test drives itself, with `inspector` attached. /// -/// The rewrite is booked, the transaction itself executes fine, and the refusal comes from the -/// executor rather than from the EVM — which is the whole point, since the EVM is required to keep -/// supporting the rewrite. -#[test] -fn test_run_transaction_refuses_an_inspector_adjusted_transaction() { +/// This is the path an embedder keeps: `MegaEvm` supports a rewriting inspector in full and +/// reports what it did. Every rewrite shape below is measured here and refused at the block +/// executor's entries, which is what says the boundary is where it is claimed to be. +fn run_off_path(inspector: I) -> MegaTransactionOutcome +where + I: for<'a> Inspector>, +{ let mut db = build_db(); - let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( - &mut state, - block_ctx(), - evm_env(MegaSpecId::REX7), - GasInjector::default(), - ); - - let tx = envelope(0); - let err = executor - .run_transaction(Recovered::new_unchecked(&tx, CALLER)) - .expect_err("the canonical path must refuse an inspector-adjusted transaction"); + let mut evm = mega_evm::MegaEvm::new( + mega_evm::MegaContext::new(&mut db, MegaSpecId::REX7) + .with_tx_runtime_limits(mega_evm::EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ) + .with_inspector(inspector); - assert!(executor.evm().inspector.applied, "the fixture must reach the injection point"); - let ledger = expect_refusal(&err, *tx.hash()); - assert_eq!( - ledger.gas, - Lane::once(i128::from(INJECTED)), - "the refusal must carry what was actually injected, so a caller can see the size of it", - ); - assert_eq!(ledger.env, Lane::default(), "no frame envelope was touched"); - assert_eq!( - executor.block_limiter.block_compute_gas_used, 0, - "a refused transaction must leave the block's counters where they were", + let mut tx = mega_evm::MegaTransaction::new( + revm::context::tx::TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(1_000_000) + .build_fill(), ); + tx.enveloped_tx = Some(Bytes::new()); + evm.execute_transaction(tx).expect("the EVM supports the rewrite in full") } -/// The refusal is over the whole ledger, not over the lanes the conservation law reads. +/// The producer entry: an inspector nobody declared read-only never runs on this path at all. /// -/// A refund rewrite is the shape that makes the distinction load-bearing: it leaves every gas lane -/// at zero, closes the law exactly as an uninspected run does, and still changes the number the -/// sender is billed. A node that ran this inspector and a node that did not would build the same -/// block with different receipts. +/// The inspector here only observes, and is refused anyway. That is the whole change of criterion: +/// what a transaction is admitted on is what the inspector's type promises, not what this +/// particular run was measured to have done — because the measurement cannot see an edit made to +/// the interpreter's stack contents or straight into the journal. #[test] -fn test_run_transaction_refuses_a_refund_rewrite() { +fn test_run_transaction_refuses_an_undeclared_inspector() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - RefundWriter::default(), + Observer::default(), ); let tx = envelope(0); let err = executor .run_transaction(Recovered::new_unchecked(&tx, CALLER)) - .expect_err("the canonical path must refuse a transaction whose receipt was rewritten"); + .expect_err("the canonical path must refuse an undeclared inspector"); - assert!(executor.evm().inspector.applied, "the fixture must reach the refund write"); - let ledger = expect_refusal(&err, *tx.hash()); + expect_undeclared(&err, *tx.hash()); assert_eq!( - ledger.refund, - Lane::once(i128::from(REFUNDED)), - "the refusal must carry the refund that was written", - ); - assert_eq!( - ledger.conjured_gas(), + executor.evm().inspector.steps, 0, - "no gas moved: a gas-only criterion would have admitted this transaction", + "the refusal must come before execution: an undeclared inspector does not get to run", ); assert_eq!( executor.block_limiter.block_compute_gas_used, 0, @@ -350,19 +456,21 @@ fn test_execute_transaction_without_commit_refuses_it_too() { .execute_transaction_without_commit(&Recovered::new_unchecked(&tx, CALLER)) .expect_err("the trait entry must refuse it as well"); - assert_eq!(expect_refusal(&err, *tx.hash()).gas, Lane::once(i128::from(INJECTED))); + expect_undeclared(&err, *tx.hash()); + assert!(!executor.evm().inspector.applied, "and must refuse before the inspector runs"); assert!(executor.receipts.is_empty(), "nothing may have been recorded"); } -/// The consumer entry: a result whose adjustment was not made by *this* executor is refused at -/// the commit funnel, before it can touch anything. +/// The consumer entry: a result whose producer this executor never saw is refused at the commit +/// funnel, before it can touch anything. /// /// This is the entry that has to hold. Execution and commit are separate steps — the parallel /// executor speculatively runs many transactions and commits the survivors one by one — so a -/// result arriving here may have been produced by a different executor instance, or built by -/// hand. The producer-side guards cannot see any of those; the outcome's own ledger can. +/// result arriving here may have been produced by a different executor instance, by an embedder +/// driving `MegaEvm` itself, or built by hand. What the outcome carries is the only thing the +/// funnel can read. #[test] -fn test_commit_refuses_a_result_an_inspector_took_part_in() { +fn test_commit_refuses_a_result_produced_under_an_undeclared_inspector() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); let mut executor = executor_factory(MegaSpecId::REX7).create_executor( @@ -376,18 +484,18 @@ fn test_commit_refuses_a_result_an_inspector_took_part_in() { .run_transaction(Recovered::new_unchecked(&tx, CALLER)) .expect("fixture check: an uninspected run must be admitted"); assert!( - outcome.inner.inspector_ledger.is_zero(), - "fixture check: an uninspected run reports an empty ledger", + !outcome.inner.undeclared_inspector, + "fixture check: an uninspected run carries no inspector to declare", ); - // The shape a result produced elsewhere arrives in: the numbers are execution's, the ledger - // says an inspector moved some of them. - outcome.inner.inspector_ledger = InspectorLedger { gas: Lane::once(1), ..Default::default() }; + // The shape a result produced elsewhere arrives in: the numbers are execution's, and the + // outcome says an inspector nobody declared took part in producing them. + outcome.inner.undeclared_inspector = true; let err = executor.commit_transaction_outcome(outcome).expect_err("the commit funnel must refuse it"); - assert_eq!(expect_refusal(&err, *tx.hash()).gas, Lane::once(1)); + expect_undeclared(&err, *tx.hash()); assert!(executor.receipts.is_empty(), "no receipt may have been pushed"); assert_eq!( executor.block_limiter.block_gas_used, 0, @@ -399,6 +507,43 @@ fn test_commit_refuses_a_result_an_inspector_took_part_in() { ); } +/// The backstop: a result that says nothing about its inspector, and carries a ledger that does. +/// +/// The declaration covers the executor's own producers; it cannot cover a result built by hand or +/// produced by a version of the pipeline that did not fill the field in. The ledger is what is +/// left, and it is read at the same funnel. +#[test] +fn test_commit_refuses_a_result_an_inspector_took_part_in() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + ); + + let tx = envelope(0); + let mut outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("fixture check: an uninspected run must be admitted"); + assert!( + outcome.inner.inspector_ledger.is_zero(), + "fixture check: an uninspected run reports an empty ledger", + ); + + outcome.inner.inspector_ledger = InspectorLedger { gas: Lane::once(1), ..Default::default() }; + + let err = + executor.commit_transaction_outcome(outcome).expect_err("the commit funnel must refuse it"); + + assert_eq!(expect_adjusted(&err, *tx.hash()).gas, Lane::once(1)); + assert!(executor.receipts.is_empty(), "no receipt may have been pushed"); + assert_eq!( + executor.block_limiter.block_gas_used, 0, + "and no limiter counter may have been advanced", + ); +} + /// The infallible commit hook has no way to report the refusal, so it latches it and the block /// fails at `finish` — the same contract it already holds for a late block-limit rejection. #[test] @@ -431,20 +576,16 @@ fn test_the_infallible_commit_hook_latches_the_refusal() { let latched = executor .pending_commit_error() .expect("the refusal must be latched where `finish` will find it"); - assert_eq!(expect_refusal(latched, *tx.hash()).env, Lane::once(-5)); + assert_eq!(expect_adjusted(latched, *tx.hash()).env, Lane::once(-5)); let err = executor.finish().expect_err("the block must not finish over a latched refusal"); - expect_refusal(&err, *tx.hash()); + expect_adjusted(&err, *tx.hash()); } -/// The guard governs the configuration a block is built with, which no historical block covers, so -/// it is not gated on a spec — the same rewrite is refused on a frozen one. -/// -/// The measurement it reads is spec-independent too, for its own reason: the shim books what an -/// inspector writes into a gas counter whether or not the spec has a lane the write could make -/// unsound. +/// The refusal governs the configuration a block is built with, which no historical block covers, +/// so it is not gated on a spec — the same inspector is refused on a frozen one. #[test] -fn test_the_guard_is_not_spec_gated() { +fn test_the_refusal_is_not_spec_gated() { for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX4, MegaSpecId::REX6, MegaSpecId::REX7] { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); @@ -459,40 +600,38 @@ fn test_the_guard_is_not_spec_gated() { let err = executor .run_transaction(Recovered::new_unchecked(&tx, CALLER)) .err() - .unwrap_or_else(|| panic!("{spec:?}: the rewrite must be refused on every spec")); - assert_eq!( - expect_refusal(&err, *tx.hash()).gas, - Lane::once(i128::from(INJECTED)), - "{spec:?}: and the measurement it is refused over must be the same one", - ); + .unwrap_or_else(|| panic!("{spec:?}: the inspector must be refused on every spec")); + expect_undeclared(&err, *tx.hash()); } } -/// The green half: an observation-only inspector is left alone, and the block it helps build is -/// bit-identical to the one built without it. +/// The green half: a declared observer is left alone, and the block it helps build is bit-identical +/// to the one built without it. /// -/// Every inspector on this path today is a tracer. If the guard could not tell one from a -/// rewriting inspector, it would take tracing off block production entirely. +/// [`Observer`] and [`DeclaredObserver`] do exactly the same thing, and only the declared one gets +/// here — so this and [`test_run_transaction_refuses_an_undeclared_inspector`] together say the +/// criterion really is the declaration. #[test] -fn test_an_observing_inspector_still_builds_a_block() { +fn test_a_declared_observer_still_builds_a_block() { let build = |observe: bool| { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); let tx = envelope(0); let factory = executor_factory(MegaSpecId::REX7); let (gas_used, steps) = if observe { - let mut executor = factory.create_executor_with_inspector( + let mut executor = factory.create_executor_with_trusted_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - Observer::default(), + DeclaredObserver::default(), ); let outcome = executor .run_transaction(Recovered::new_unchecked(&tx, CALLER)) - .expect("an observing inspector must not be refused"); + .expect("a declared observer must not be refused"); + assert!(!outcome.inner.undeclared_inspector, "and must report itself declared"); assert!(outcome.inner.inspector_ledger.is_zero(), "and must leave an empty ledger"); let gas = executor.commit_transaction_outcome(outcome).expect("nor at commit"); - let steps = executor.evm().inspector.steps; + let steps = executor.evm().inspector.inner.steps; let (_, result) = executor.finish().expect("the block must finish"); assert_eq!(result.receipts.len(), 1, "the observed block still has its receipt"); (gas, steps) @@ -516,56 +655,50 @@ fn test_an_observing_inspector_still_builds_a_block() { assert_eq!(observed_gas, plain_gas, "observation must not move a single unit of gas"); } -/// A pre- or post-block system call never runs the inspector, so it is not an entry the guard has -/// to cover. +/// The inspector that observes nothing at all is declared, so the trivial configuration passes. /// -/// Two independent reasons, and this pins the one that is not visible from the block executor's -/// own signatures. Structurally, a system call produces a `ResultAndState` rather than a -/// `MegaTransactionOutcome`, and the ledger is reset at the start of every transaction, so nothing -/// a system call booked could reach a transaction's outcome anyway. Underneath that, the system -/// call path takes revm's plain frame loop rather than the inspecting one — which is what this -/// runs to find out, rather than reading it off upstream's source. +/// `NoOpInspector` is the one inspector this crate can declare for itself, and it is the shape a +/// caller reaches for when a code path needs an inspector-typed EVM without wanting one. #[test] -fn test_a_system_call_does_not_run_the_inspector() { - use revm::SystemCallEvm as _; - +fn test_the_no_op_inspector_is_admitted() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_trusted_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - GasInjector::default(), + NoOpInspector, ); - let result = executor - .evm_mut() - .system_call(CONTRACT, Bytes::new()) - .expect("the system call must not surface an EVMError"); - - assert!(result.result.is_success(), "fixture check: the callee must have run, got {result:?}",); - assert!(!executor.evm().inspector.applied, "a system call must not reach the inspector at all",); + let tx = envelope(0); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("a declared inspector must not be refused"); + assert!(outcome.result.is_success(), "fixture check: {:?}", outcome.result); + executor.commit_transaction_outcome(outcome).expect("nor at commit"); + let (_, result) = executor.finish().expect("the block must finish"); + assert_eq!(result.receipts.len(), 1); } -/// The real tracer that `mega-evme replay` attaches to this exact path is admitted, and the block -/// it observes is the one built without it. +/// The real tracer that `mega-evme replay` attaches to this exact path is admitted through its +/// declaration, and the block it observes is the one built without it. /// -/// The `Observer` above is a fixture; this is the production shape. `TracingInspector` receives -/// every callback the shim measures — including the ones handed a live interpreter and the ones -/// handed a frame's inputs — so if observation could move a lane by accident, it would move one -/// here. Run rather than reasoned about: the guard's blast radius is only acceptable if the -/// inspectors that exist today pass it. +/// The `DeclaredObserver` above is a fixture; this is the production shape, newtype and all. +/// `TracingInspector` receives every callback the shim measures — including the ones handed a live +/// interpreter and the ones handed a frame's inputs — so if observation could move a lane by +/// accident, it would move one here. Run rather than reasoned about: the refusal's blast radius is +/// only acceptable if the inspectors that exist today have a way through it. #[test] fn test_the_production_tracer_is_admitted() { use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( + let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_trusted_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - TracingInspector::new(TracingInspectorConfig::all()), + TrustedTracer(TracingInspector::new(TracingInspectorConfig::all())), ); let tx = envelope(0); @@ -582,122 +715,141 @@ fn test_the_production_tracer_is_admitted() { executor.commit_transaction_outcome(outcome).expect("nor at commit"); assert!( - executor.evm().inspector.traces().nodes().len() >= 2, + executor.evm().inspector.0.traces().nodes().len() >= 2, "fixture check: the tracer must have recorded the nested frame it was given", ); let (_, result) = executor.finish().expect("the block must finish"); assert_eq!(result.receipts.len(), 1); } -/// The rewrite a gas-only guard could not see: a frame's classification, changed at `call_end`. -/// -/// Nothing moves. The call's remaining gas, its envelope and every interpreter counter are the -/// EVM's own, so all three gas lanes read zero — and yet the callee's write is rolled back and the -/// caller is handed a failure, which is a different transaction with a different state root. The -/// intervention counter is the only thing standing between this and admission. +/// The bare `TracingInspector`, without the newtype, is refused — which is what makes the newtype +/// load-bearing rather than decorative. #[test] -fn test_a_rewrite_that_moves_no_gas_is_refused_too() { +fn test_the_production_tracer_without_its_declaration_is_refused() { + use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; + let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - CallFailer::default(), + TracingInspector::new(TracingInspectorConfig::all()), ); let tx = envelope(0); let err = executor .run_transaction(Recovered::new_unchecked(&tx, CALLER)) - .expect_err("a classification rewrite must be refused like any other"); - - assert!(executor.evm().inspector.applied, "the fixture must reach the rewrite point"); - let ledger = expect_refusal(&err, *tx.hash()); - assert_eq!( - (ledger.gas, ledger.env, ledger.result), - (Lane::default(), Lane::default(), Lane::default()), - "the point of this shape is that no gas lane moves; got {ledger:?}", - ); - assert_eq!(ledger.interventions, 1, "the rewrite must be the thing the refusal names"); - assert_eq!( - executor.block_limiter.block_compute_gas_used, 0, - "a refused transaction must leave the block's counters where they were", - ); + .expect_err("an undeclared tracer is an undeclared inspector"); + expect_undeclared(&err, *tx.hash()); } -/// The other rewrite a gas-only guard could not see: a frame's memory, grown for free. +/// A pre- or post-block system call never runs the inspector, so it is not an entry the guard has +/// to cover. /// -/// This one reaches through nothing the shim is *handed* — not a frame input, not a frame result, -/// not the pending action, not any gas counter. What it moves is the interpreter's own working -/// state, and it moves the two halves of it that have to agree, so the EVM finds nothing wrong and -/// simply charges less. The transaction pays less than it would have, which is the one thing the -/// guard exists to keep out of a block. +/// Two independent reasons, and this pins the one that is not visible from the block executor's +/// own signatures. Structurally, a system call produces a `ResultAndState` rather than a +/// `MegaTransactionOutcome`, and the ledger is reset at the start of every transaction, so nothing +/// a system call booked could reach a transaction's outcome anyway. Underneath that, the system +/// call path takes revm's plain frame loop rather than the inspecting one — which is what this +/// runs to find out, rather than reading it off upstream's source. #[test] -fn test_a_frame_grown_for_free_is_refused_too() { +fn test_a_system_call_does_not_run_the_inspector() { + use revm::SystemCallEvm as _; + let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - MemoryGrower::default(), + GasInjector::default(), ); - let tx = envelope(0); - let err = executor - .run_transaction(Recovered::new_unchecked(&tx, CALLER)) - .expect_err("a frame grown for free must be refused like any other rewrite"); + let result = executor + .evm_mut() + .system_call(CONTRACT, Bytes::new()) + .expect("the system call must not surface an EVMError"); - assert!(executor.evm().inspector.applied, "the fixture must reach the growth point"); - let ledger = expect_refusal(&err, *tx.hash()); + assert!(result.result.is_success(), "fixture check: the callee must have run, got {result:?}",); + assert!(!executor.evm().inspector.applied, "a system call must not reach the inspector at all",); +} + +/// An EVM driven off the canonical path is not covered by the refusal, however much its inspector +/// rewrites — and every rewrite it makes is still measured and reported. +/// +/// The boundary sits on the block executor's entries, not on `MegaEvm`, and this is what makes +/// that a property rather than an accident of the current call graph. It is what leaves a +/// simulation EVM — the oracle set-slot preflight the node runs before it publishes a value, and +/// anything else an embedder drives itself — free to attach a rewriting inspector: such a run +/// never produces a block, so there is nothing for two nodes to disagree about. +/// +/// Each shape below is one the ledger can see, and the four together are why the ledger is worth +/// keeping as a backstop even though admission no longer rests on it. +#[test] +fn test_an_off_path_evm_runs_a_gas_injection_to_completion() { + let outcome = run_off_path(GasInjector::default()); assert_eq!( - (ledger.gas, ledger.env, ledger.result, ledger.refund), - (Lane::default(), Lane::default(), Lane::default(), Lane::default()), - "no lane can see this shape; got {ledger:?}", + outcome.inspector_ledger.gas, + Lane::once(i128::from(INJECTED)), + "the injection must be measured: {:?}", + outcome.inspector_ledger, + ); + assert!(outcome.undeclared_inspector, "and the outcome must carry what a block would refuse"); + assert!(outcome.result_and_state.result.is_success(), "and the transaction still completes"); +} + +/// A refund rewrite: every gas lane stays at zero, and the number the sender is billed moves. +#[test] +fn test_an_off_path_evm_runs_a_refund_rewrite_to_completion() { + let outcome = run_off_path(RefundWriter::default()); + assert_eq!( + outcome.inspector_ledger.refund, + Lane::once(i128::from(REFUNDED)), + "the refund must be measured: {:?}", + outcome.inspector_ledger, ); - assert_eq!(ledger.interventions, 1, "the growth must be the thing the refusal names"); assert_eq!( - executor.block_limiter.block_compute_gas_used, 0, - "a refused transaction must leave the block's counters where they were", + outcome.inspector_ledger.conjured_gas(), + 0, + "no gas moved: a gas-only criterion would not have seen this at all", ); + assert!(outcome.undeclared_inspector); } -/// An EVM driven off the canonical path is not covered by the guard, however much its inspector -/// rewrites. -/// -/// The guard sits on the block executor's entries, not on `MegaEvm`, and this is what makes that a -/// property rather than an accident of the current call graph. It is what leaves a simulation EVM -/// — the oracle set-slot preflight the node runs before it publishes a value, and anything else an -/// embedder drives itself — free to attach a rewriting inspector: such a run never produces a -/// block, so there is nothing for two nodes to disagree about. -/// -/// The same transaction is refused by the executor in -/// [`test_a_rewrite_that_moves_no_gas_is_refused_too`], so the two together say the boundary is -/// where it is claimed to be rather than nowhere. +/// A classification rewrite: nothing moves, and the transaction's state is different. #[test] -fn test_an_off_path_evm_runs_the_same_rewrite_to_completion() { - let mut db = build_db(); - let mut inspector = CallFailer::default(); - let mut evm = mega_evm::MegaEvm::new( - mega_evm::MegaContext::new(&mut db, MegaSpecId::REX7) - .with_tx_runtime_limits(mega_evm::EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), - ) - .with_inspector(&mut inspector); - - let mut tx = mega_evm::MegaTransaction::new( - revm::context::tx::TxEnvBuilder::default() - .caller(CALLER) - .call(CONTRACT) - .gas_limit(1_000_000) - .build_fill(), +fn test_an_off_path_evm_runs_a_classification_rewrite_to_completion() { + let outcome = run_off_path(CallFailer::default()); + assert_eq!( + ( + outcome.inspector_ledger.gas, + outcome.inspector_ledger.env, + outcome.inspector_ledger.result + ), + (Lane::default(), Lane::default(), Lane::default()), + "the point of this shape is that no gas lane moves; got {:?}", + outcome.inspector_ledger, ); - tx.enveloped_tx = Some(Bytes::new()); - let outcome = evm.execute_transaction(tx).expect("the EVM supports the rewrite in full"); + assert_eq!(outcome.inspector_ledger.interventions, 1, "the rewrite must still be booked"); + assert!(outcome.undeclared_inspector); +} - assert!(inspector.applied, "the fixture must reach the rewrite point"); +/// A frame grown for free: the rewrite that reaches through nothing the shim is handed. +#[test] +fn test_an_off_path_evm_runs_a_free_memory_growth_to_completion() { + let outcome = run_off_path(MemoryGrower::default()); assert_eq!( - outcome.inspector_ledger.interventions, 1, - "the rewrite is still measured and reported — it is simply not refused here", + ( + outcome.inspector_ledger.gas, + outcome.inspector_ledger.env, + outcome.inspector_ledger.result, + outcome.inspector_ledger.refund, + ), + (Lane::default(), Lane::default(), Lane::default(), Lane::default()), + "no gas lane can see this shape; got {:?}", + outcome.inspector_ledger, ); - assert!(outcome.result_and_state.result.is_success(), "and the transaction still completes"); + assert_eq!(outcome.inspector_ledger.interventions, 1, "the growth must still be booked"); + assert!(outcome.undeclared_inspector); } diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index be3e4597..5d91920b 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -21,11 +21,12 @@ //! inspector at all, on every quantity the differential classifier compares. That is the property //! every tracer in production depends on, and it is checked here against 44,000 transactions //! rather than against a handful of fixtures. -//! - **Can the guard still see it?** A rewriting run that applied a mutation the shim is contracted -//! to book unconditionally must not end with an all-zero ledger. The canonical block path admits -//! a transaction whose ledger is zero, so a rewrite that leaves it zero is a rewrite that reaches -//! a block — see [`ChaosClass::LedgerBlind`] and [`ChaosShape::is_always_booked`] for why the -//! gate is stated over a subset of the pool rather than over all of it. +//! - **Can the ledger still see it?** A rewriting run that applied a mutation the shim is +//! contracted to book unconditionally must not end with an all-zero ledger. The ledger is what +//! the conservation law reads as its inspector term, what the block executor's backstop refuses a +//! result over, and the only thing that tells a consumer an execution was inspector-influenced — +//! see [`ChaosClass::LedgerBlind`] and [`ChaosShape::is_always_booked`] for why the gate is +//! stated over a subset of the pool rather than over all of it. //! //! # Why the randomness is not random //! @@ -1304,11 +1305,12 @@ pub enum ChaosClass { /// The rewriting run applied a mutation the shim is contracted to book unconditionally — see /// [`ChaosShape::is_always_booked`] — and still ended with an all-zero ledger. /// - /// That is the one thing the ledger exists to prevent: the canonical block path admits a - /// transaction whose ledger is zero, so a rewrite that leaves it zero is a rewrite that - /// reaches a block. Unlike every other verdict here it is not about the transaction's numbers - /// being wrong — they may be exactly what a rewriting inspector should produce — but about the - /// guard being unable to tell that anything happened. + /// The ledger is the inspector term of the conservation law, the backstop the block executor + /// refuses a result over, and the only thing that tells a consumer an execution was + /// inspector-influenced. A rewrite that leaves it zero is invisible to all three. Unlike every + /// other verdict here it is not about the transaction's numbers being wrong — they may be + /// exactly what a rewriting inspector should produce — but about nothing being able to tell + /// that anything happened. LedgerBlind, /// Neither run executed the transaction, and the runner declined it identically. Skipped, diff --git a/tools/eest-sweep/README.md b/tools/eest-sweep/README.md index 4359fef4..d5f060f1 100644 --- a/tools/eest-sweep/README.md +++ b/tools/eest-sweep/README.md @@ -76,7 +76,7 @@ Every vector is executed three times under the target spec — with no inspector - **Observation is free.** The read-only run must be identical to the run with no inspector on every quantity the differential classifier compares, and must leave an empty inspector ledger. That is the property every tracer in production depends on, checked against the whole corpus rather than a handful of fixtures. - **Rewriting does not break the books.** Every gas-accounting cross-check MegaETH has is a debug assertion, so under the default `hivetests` profile a broken conservation law is a panic, and a panic is that vector's verdict rather than a lost worker thread. -- **The guard can still see the rewrite.** A run that applied a shape the shim is contracted to book unconditionally must not end with an all-zero inspector ledger, because the canonical block-execution path admits a transaction whose ledger is zero. The gate is stated over a subset of the pool on purpose: most shapes are booked only when what they moved still reaches something — gas written into a counter the interpreter is about to stop reading moves nothing, a result's remaining gas edited on a halting frame is never handed back — and a gate over those would fail on a working shim. `ChaosShape::is_always_booked` is the partition. +- **The ledger can still see the rewrite.** A run that applied a shape the shim is contracted to book unconditionally must not end with an all-zero inspector ledger, because the ledger is the conservation law's inspector term, the backstop the block executor refuses a result over, and the only thing that tells a consumer an execution was inspector-influenced. The gate is stated over a subset of the pool on purpose: most shapes are booked only when what they moved still reaches something — gas written into a counter the interpreter is about to stop reading moves nothing, a result's remaining gas edited on a halting frame is never handed back — and a gate over those would fail on a working shim. `ChaosShape::is_always_booked` is the partition. The rewriting inspector's decisions come from a hash of the global seed and the vector's own identity — its fixture path, unit name and transaction indexes. No clock, no address, no iteration order. From 0abf71f5f6e0752d4d528416d35258e55005a857 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:39:33 +0800 Subject: [PATCH 195/208] fix(chaos): derive a vector's seed from a corpus-relative identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-vector seed hashed the fixture's full path, so the same corpus checked out under a different root, reached through a different `--corpus-dir`, or copied into the private fallback directory produced different seeds for identical fixtures — and a fixture pulled out for triage on its own shares no path prefix with the one the sweep walked. A reported seed that stops reproducing its own failure is worse than no seed. The identity is now the fixture's file name, the unit name and the vector indexes, none of which depends on where the corpus sits. Two fixtures with the same file name in different directories share a seed prefix; that is not a defect, because a seed selects a mutation stream and uniqueness is not what the identity is for. Existing seeds change, so the per-class counts of a given global seed change with them. --- crates/mega-state-test/src/chaos.rs | 36 ++++++++-- crates/mega-state-test/tests/chaos_mode.rs | 79 ++++++++++++++++++---- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 5d91920b..8e66e8c7 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -149,14 +149,36 @@ fn fnv1a(bytes: &[u8], mut hash: u64) -> u64 { hash } +/// The part of a fixture's path a seed is allowed to depend on: its file name, and nothing above +/// it. +/// +/// A reported seed has to mean the same thing on the machine that reports it and the machine that +/// triages it, and the rest of the path does not: the same corpus sits under a different checkout +/// root on every machine, under whatever directory `--corpus-dir` names, and under the private +/// fallback directory a sweep falls back to. A fixture triaged on its own is reached by a path +/// that shares no prefix at all with the one the sweep walked. Every one of those would move the +/// seed while the fixture stayed the same, which is the one thing a reported seed must not do — +/// and the failure mode is the worst kind, a nightly failure that quietly stops reproducing. +/// +/// Two fixtures with the same file name in different directories therefore feed the same bytes +/// into the hash. That is not a defect: a seed selects a mutation stream, and two vectors drawing +/// the same stream is exactly as useful as two drawing different ones. What the identity is for is +/// stability, not uniqueness — and the unit name, which follows it into the hash, carries the test +/// id that distinguishes them anyway. +fn fixture_identity(path: &Path) -> std::borrow::Cow<'_, str> { + path.file_name().unwrap_or(path.as_os_str()).to_string_lossy() +} + /// The seed one vector's chaos run uses, derived from the global seed and the vector's identity. /// -/// The identity is everything that distinguishes this transaction from every other in the corpus: -/// which file it came from, which unit of that file, and which of that unit's transaction vectors. -/// Two runs of the same corpus with the same global seed therefore mutate the same vectors the -/// same way, whatever order the files are swept in and however many threads sweep them. -pub fn vector_seed(global: u64, path: &str, name: &str, indexes: TxPartIndices) -> u64 { - let mut hash = fnv1a(path.as_bytes(), 0xCBF2_9CE4_8422_2325); +/// The identity is everything that distinguishes this transaction from every other in the corpus +/// *and* means the same thing on every machine: the fixture's file name (see +/// [`fixture_identity`]), which unit of that file it is, and which of that unit's transaction +/// vectors. Two runs of the same corpus with the same global seed therefore mutate the same +/// vectors the same way, whatever order the files are swept in, however many threads sweep them, +/// and wherever the corpus is checked out. +pub fn vector_seed(global: u64, path: &Path, name: &str, indexes: TxPartIndices) -> u64 { + let mut hash = fnv1a(fixture_identity(path).as_bytes(), 0xCBF2_9CE4_8422_2325); hash = fnv1a(&[0], hash); hash = fnv1a(name.as_bytes(), hash); hash = fnv1a(&[0], hash); @@ -1596,7 +1618,7 @@ pub fn chaos_test_suite( let multi = vectors.len() > 1; for indexes in vectors { let label = if multi { vector_label(&name, indexes) } else { name.clone() }; - let seed = vector_seed(global_seed, &path_str, &label, indexes); + let seed = vector_seed(global_seed, path, &label, indexes); let verdict = match panic_capture::catch(|| chaos_unit(&unit, indexes, spec, seed, filter)) { Ok(verdict) => { diff --git a/crates/mega-state-test/tests/chaos_mode.rs b/crates/mega-state-test/tests/chaos_mode.rs index ab1eb37e..1e92cbfa 100644 --- a/crates/mega-state-test/tests/chaos_mode.rs +++ b/crates/mega-state-test/tests/chaos_mode.rs @@ -9,14 +9,14 @@ use mega_evm::FORBIDDEN_FRAME_INIT_REWRITE; use state_test::{ chaos::{ - chaos_unit, run_chaos, vector_seed, ChaosClass, ChaosRunConfig, ChaosShape, ChaosTally, - ShapeFilter, + chaos_test_suite, chaos_unit, run_chaos, vector_seed, ChaosClass, ChaosRunConfig, + ChaosShape, ChaosTally, ShapeFilter, }, diff::{execute_unit_in_mode, execute_unit_reporting_chaos, RunMode}, runner::FixtureScan, types::{SpecName, TestUnit, TxPartIndices}, }; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; const SENDER: &str = "0x1000000000000000000000000000000000000001"; const CALLEE: &str = "0x2000000000000000000000000000000000000002"; @@ -151,19 +151,68 @@ fn test_different_seeds_produce_different_runs() { /// A vector's seed depends on every part of its identity, and on the global seed. #[test] fn test_a_vector_seed_separates_every_part_of_the_identity() { - let base = vector_seed(7, "a.json", "unit", VECTOR_0); + let a = Path::new("a.json"); + let base = vector_seed(7, a, "unit", VECTOR_0); let others = [ - vector_seed(8, "a.json", "unit", VECTOR_0), - vector_seed(7, "b.json", "unit", VECTOR_0), - vector_seed(7, "a.json", "other", VECTOR_0), - vector_seed(7, "a.json", "unit", TxPartIndices { data: 1, gas: 0, value: 0 }), - vector_seed(7, "a.json", "unit", TxPartIndices { data: 0, gas: 1, value: 0 }), - vector_seed(7, "a.json", "unit", TxPartIndices { data: 0, gas: 0, value: 1 }), + vector_seed(8, a, "unit", VECTOR_0), + vector_seed(7, Path::new("b.json"), "unit", VECTOR_0), + vector_seed(7, a, "other", VECTOR_0), + vector_seed(7, a, "unit", TxPartIndices { data: 1, gas: 0, value: 0 }), + vector_seed(7, a, "unit", TxPartIndices { data: 0, gas: 1, value: 0 }), + vector_seed(7, a, "unit", TxPartIndices { data: 0, gas: 0, value: 1 }), ]; for (i, other) in others.iter().enumerate() { assert_ne!(base, *other, "identity component {i} does not reach the seed"); } - assert_eq!(base, vector_seed(7, "a.json", "unit", VECTOR_0), "and the seed is a function"); + assert_eq!(base, vector_seed(7, a, "unit", VECTOR_0), "and the seed is a function"); +} + +/// The directory a fixture is reached through does not reach the seed. +/// +/// A seed is only worth reporting if the machine that reads it can re-run what produced it. The +/// same corpus is checked out at a different root on every machine, `--corpus-dir` names whatever +/// the caller likes, a sweep can fall back to a private copy, and a fixture under triage is passed +/// on its own rather than walked to. Every one of those changes the path and none of them changes +/// the fixture. +#[test] +fn test_a_vector_seed_ignores_the_directory_the_fixture_was_reached_through() { + let roots = [ + Path::new("/checkout-a/tests/GeneralStateTests/x.json"), + Path::new("/somewhere/else/entirely/x.json"), + Path::new("/tmp/.private.4928/x.json"), + Path::new("x.json"), + ]; + let seeds: Vec = roots.iter().map(|p| vector_seed(7, p, "unit", VECTOR_0)).collect(); + for (root, seed) in roots.iter().zip(&seeds) { + assert_eq!( + *seed, + seeds[0], + "{}: the path above the file name reached the seed", + root.display() + ); + } +} + +/// The same, through the sweep that actually derives the seeds rather than through the function. +/// +/// `vector_seed` taking a stable identity is only half of it; the other half is that the sweep +/// hands it one. This writes one fixture into two different directories and runs the real entry +/// point over each. +#[test] +fn test_the_sweep_derives_the_same_seeds_from_two_different_roots() { + let seeds = |dir: &str| -> Vec { + let path = write_suite_under(dir, "chaos_mode_same_seed.json"); + let (verdicts, _) = chaos_test_suite(&path, &SpecName::Rex7, 11, ShapeFilter::default()) + .expect("the fixture must be readable"); + assert!(!verdicts.is_empty(), "the fixture must produce at least one vector"); + verdicts.iter().map(|v| v.seed).collect() + }; + + assert_eq!( + seeds("chaos_mode_root_a"), + seeds("chaos_mode_root_b/nested"), + "the same fixture under two roots must be mutated the same way", + ); } /// Narrowing the filter keeps every surviving mutation where the full run put it. @@ -446,9 +495,15 @@ fn test_a_sweep_that_mutated_passes() { /// Writes the fixture to a unique temp file and returns its path. fn write_suite(file_name: &str) -> PathBuf { + write_suite_under("mega_state_test_chaos_mode", file_name) +} + +/// Writes the fixture under a named directory of the temp root, so two copies of one fixture can +/// be reached through two different paths. +fn write_suite_under(dir: &str, file_name: &str) -> PathBuf { let suite: serde_json::Map = std::iter::once(("chaos_unit".to_string(), unit_json())).collect(); - let dir = std::env::temp_dir().join("mega_state_test_chaos_mode"); + let dir = std::env::temp_dir().join(dir); std::fs::create_dir_all(&dir).expect("mkdir"); let path = dir.join(file_name); std::fs::write(&path, serde_json::to_string_pretty(&suite).expect("serialize")) From c48303872b1e30d23722fe4ef06c6d7f329e74c8 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:40:20 +0800 Subject: [PATCH 196/208] docs(spec): exclude code deposit from Rex7's unchanged-recording-sites claim The Rex7 checkpoint section stated that every non-opcode recording site on the page is unchanged, which contradicts the Rex7 rules stated earlier on the same page: the code-deposit amount is weighed against the compute budgets before it is recorded, and nothing is recorded when it does not fit or when the frame had already failed. An implementation following the universal sentence would keep Rex6's unconditional recording and report compute gas Rex7 deliberately omits. The claim now names the three sites that really are unchanged and points the fourth at the section that governs it. --- docs/spec/evm/compute-gas.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 9d046cf8..40230010 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -500,7 +500,8 @@ At each checkpoint a node MUST: 2. Record that segment amount as compute gas and evaluate the compute-gas limit (and any latched non-compute resource-limit exceed) at that checkpoint — the latch-surface point is the next checkpoint rather than the next per-opcode recording site. 3. Record the checkpoint opcode's own body under the measurement-window rules for its metering class, then re-open the settlement window. -Non-opcode recording sites on this page (intrinsic gas, successful or reverting precompiles, code deposit, KeylessDeploy) are unchanged. +Of the non-opcode recording sites on this page, intrinsic gas, successful or reverting precompiles and KeylessDeploy are unchanged. +Code deposit is not: Rex7 weighs the amount against the frame-local and transaction-level compute budgets before recording it, and records nothing when it does not fit or when the frame had already failed, as specified under [Contract Creation Code Deposit](#contract-creation-code-deposit). A precompile that fails is split under the exceptional-halt carve-out below. For every transaction that stays within every runtime resource limit, in which no frame ends in an exceptional halt, and in which no `disableVolatileDataAccess` guard rejects an opcode, a node MUST produce the same recorded compute-gas total, the same four-dimension usage, the same receipt `gas_used`, the same execution result, and the same state as under Rex6. From 4957f7f63b67ec7d3e3cbefb0172e025f723d601 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Wed, 2 Sep 2026 23:41:10 +0800 Subject: [PATCH 197/208] docs(spec): scope the failed-deposit zero-capacity rule to the rebuilt envelope's difference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule said a deposit rejected before it ran anything consumes no compute capacity at all, contradicting the sentence above it and the implementation: validation records the standard-EVM share of intrinsic gas before it returns the error, so the transaction enforces that amount and the envelope rebuild adds only the remainder as destroyed gas. An implementation following the sentence would enforce zero. The zero-capacity statement now covers the difference the rebuild introduces, and the rule says what each shape does enforce — which on both shapes is exactly what Rex6 records for the same transaction. Verified against the reject and halt shapes in tests/rex7/deposit_receipt_rewrite.rs, which pin the enforced total to Rex6's recorded total on both. --- docs/spec/evm/compute-gas.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/spec/evm/compute-gas.md b/docs/spec/evm/compute-gas.md index 40230010..50d93cf5 100644 --- a/docs/spec/evm/compute-gas.md +++ b/docs/spec/evm/compute-gas.md @@ -633,7 +633,9 @@ A deposit a node would otherwise reject during validation, and a deposit that ha The rebuild runs after every recording and settlement site, so it is the last thing that decides the envelope: a node MUST derive the law against the rebuilt envelope rather than against the one the transaction reached on its own. The difference between the two is destroyed compute gas, because the receipt burns it and nothing was executed for it. The two shapes arrive from opposite positions and the law covers both without distinguishing them — a rejected deposit has recorded only the standard-EVM share of its intrinsic gas and settled nothing, while a halted deposit has already settled against the smaller envelope its resource-limit gas rescue left behind. -A node MUST NOT let the rebuild change `executed_compute`: nothing was executed for the difference, so a deposit rejected before it ran anything consumes no compute capacity at transaction or block level. +A node MUST NOT let the rebuild change `executed_compute`: nothing was executed for the difference between the rebuilt envelope and the one the transaction reached on its own, so that difference MUST NOT consume compute capacity at transaction or block level. +What each shape recorded before the rebuild stands, and is enforced. +A rejected deposit therefore enforces the standard-EVM share of its intrinsic gas — the amount recorded before validation returned the error — and a halted deposit enforces everything it had settled; on both shapes that is exactly what Rex6 records for the same transaction, and the destroyed remainder is an addition to the reported total rather than a change to the enforced one. The split MUST be driven by the halt classification rather than by the interpreter's own counter, which an inherited EVM zeroes for ordinary out-of-gas only. That zeroing has one consequence a node MUST accept: for an ordinary out-of-gas taken with no clamp in force, the counter is already zero when the frame exits, so the whole segment measures as executed and is enforced in full. From 0dc0a2fdfdfa9dd8155c2e95adae08eaf7f17f7a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 00:09:25 +0800 Subject: [PATCH 198/208] refactor(block): drop the executor constructor that refuses every transaction `create_executor_with_inspector` built an executor whose every transaction the canonical path then refused, and stayed only because the EVM under it was reachable through `evm_mut()`. A constructor whose product always fails is a trap rather than an entry, so it is gone. An undeclared inspector now reaches an executor the way a node already builds one: `evm_factory().create_evm(db, env).with_inspector(x)` handed to the `BlockExecutorFactory` trait entry. The tests that needed the refusal take that route, which is the shape `mega-reth` uses and so worth exercising directly; the ones that needed admission take `create_executor_with_trusted_inspector`. --- crates/mega-evm/src/block/factory.rs | 52 ++------------ crates/mega-evm/src/evm/AGENTS.md | 3 +- .../tests/block_executor/inspector.rs | 35 ++++++++-- .../tests/block_executor/inspector_guard.rs | 70 +++++++++++++------ .../trait_factory_runtime_limits.rs | 4 +- 5 files changed, 83 insertions(+), 81 deletions(-) diff --git a/crates/mega-evm/src/block/factory.rs b/crates/mega-evm/src/block/factory.rs index c0d0bab5..011e43c2 100644 --- a/crates/mega-evm/src/block/factory.rs +++ b/crates/mega-evm/src/block/factory.rs @@ -104,9 +104,10 @@ where /// [`TrustedObserver`](crate::TrustedObserver). /// /// The declaration is what the canonical block-execution path admits an inspected transaction - /// on, so this is the entry a node tracing block production or validation takes. - /// [`create_executor_with_inspector`](Self::create_executor_with_inspector) builds an executor - /// that refuses every transaction it is given. + /// on, so this is the entry a node tracing block production or validation takes. There is no + /// undeclared counterpart: an inspector without a declaration reaches an executor only through + /// the [`BlockExecutorFactory`](alloy_evm::block::BlockExecutorFactory) trait entry, which + /// takes an EVM the caller built and whose transactions are then refused one by one. /// /// A `revm-inspectors` tracer cannot be declared where both it and the trait are foreign, so a /// node writes a forwarding newtype of its own and declares that; `bin/mega-evme`'s replay @@ -143,51 +144,6 @@ where .with_tx_runtime_limits(runtime_limits); MegaBlockExecutor::new(evm, block_ctx, self.hardforks.clone(), self.receipt_builder.clone()) } - - /// Create a new block executor with an inspector that carries no read-only declaration. - /// - /// The executor this builds refuses every transaction it is asked to run or admit, with - /// [`MegaBlockExecutionError::UndeclaredInspector`]( - /// crate::MegaBlockExecutionError::UndeclaredInspector) — the canonical path admits an - /// inspected transaction only on a [`TrustedObserver`](crate::TrustedObserver) declaration, - /// which this entry's bound does not ask for. It stays because the EVM underneath it is - /// reachable through [`MegaBlockExecutor::evm_mut`], which an embedder can drive itself. - /// - /// A tracer belongs on - /// [`create_executor_with_trusted_inspector`](Self::create_executor_with_trusted_inspector). - /// - /// # Parameters - /// - /// - `db`: The database to use for EVM state. - /// - `evm_env`: The EVM environment, including block and config environments. - /// - `block_ctx`: The block execution context for tracking access patterns. - /// - `inspector`: The inspector to use for debugging and monitoring. - /// - /// # Returns - /// - /// A new `BlockExecutor` instance configured with the provided parameters. - pub fn create_executor_with_inspector<'a, DB, I>( - &self, - db: &'a mut State, - block_ctx: MegaBlockExecutionCtx, - evm_env: EvmEnv, - inspector: I, - ) -> MegaBlockExecutor< - Hardforks, - MegaEvm<&'a mut State, I, ExtEnvFactory::EnvTypes>, - ReceiptBuilder, - > - where - DB: Database + 'a, - I: Inspector, ExtEnvFactory::EnvTypes>> + 'a, - { - let runtime_limits = block_ctx.block_limits.to_evm_tx_runtime_limits(); - let evm = self - .evm_factory - .create_evm_with_inspector(db, evm_env, inspector) - .with_tx_runtime_limits(runtime_limits); - MegaBlockExecutor::new(evm, block_ctx, self.hardforks.clone(), self.receipt_builder.clone()) - } } impl alloy_evm::block::BlockExecutorFactory diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index da401f85..d85ef898 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -141,7 +141,8 @@ Anything supplied by a request — a JavaScript tracer, an RPC-selected tracer c **How a node reaches it.** `EvmFactory::create_evm_with_inspector` cannot: its bound is `I: Inspector` and its return type is fixed, so it has no way to select the constructor. The route is `factory.create_evm(db, env).with_trusted_inspector(tracer)`, which keeps the factory's own dynamic precompiles and differs from the two-step untrusted form only in the method name. -`MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is that route packaged for the block path, and it is the entry a node tracing block production or validation takes; `create_executor_with_inspector` builds an executor that refuses every transaction it is given, and stays only because the EVM under it is reachable through `evm_mut()`. +`MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is that route packaged for the block path, and it is the entry a node tracing block production or validation takes. +There is no undeclared counterpart on the factory: an inspector without a declaration reaches an executor only by building the EVM and passing it to the `BlockExecutorFactory` trait entry below, which is the shape a node already uses and which refuses the transactions rather than the construction. `create_executor` (the `BlockExecutorFactory` trait method) takes an EVM the caller already built and checks nothing about its inspector, because the question is a runtime one the executor's own entries ask per transaction — an error that fails the block rather than an assertion that stops the process. `bin/mega-evme`'s replay command is the worked example of the whole shape: a `TrustedTracingInspector` newtype declared over `revm-inspectors`' tracer, handed to `create_executor_with_trusted_inspector`. diff --git a/crates/mega-evm/tests/block_executor/inspector.rs b/crates/mega-evm/tests/block_executor/inspector.rs index b275032c..bc142244 100644 --- a/crates/mega-evm/tests/block_executor/inspector.rs +++ b/crates/mega-evm/tests/block_executor/inspector.rs @@ -6,7 +6,10 @@ use std::{cell::Cell, convert::Infallible}; use alloy_consensus::{Signed, TxLegacy}; -use alloy_evm::{block::BlockExecutor, EvmEnv, IntoTxEnv}; +use alloy_evm::{ + block::{BlockExecutor, BlockExecutorFactory}, + EvmEnv, EvmFactory, IntoTxEnv, +}; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256}; use mega_evm::{ @@ -280,9 +283,18 @@ fn test_inspector_early_return_with_additional_limits() { // Create inspector that skips nested calls let inspector = SkipNestedCallInspector::default(); - // Create block executor with inspector - let mut executor = block_executor_factory - .create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector); + // Built the way a node builds one: the EVM carries the inspector, and the executor is made + // from it through the `alloy_evm` trait entry. The factory has no undeclared-inspector + // constructor, because an executor that refuses every transaction is not an API. + let evm = block_executor_factory + .evm_factory() + .create_evm(&mut state, evm_env) + .with_inspector(inspector); + let mut executor = as BlockExecutorFactory>::create_executor( + &block_executor_factory, + evm, + block_ctx, + ); // Execute transaction - this triggers a nested CALL that the inspector intercepts let tx = create_transaction(0, 1_000_000); @@ -392,9 +404,18 @@ fn test_inspector_early_return_create_with_additional_limits() { // Create inspector that skips create operations let inspector = SkipCreateInspector::default(); - // Create block executor with inspector - let mut executor = block_executor_factory - .create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector); + // Built the way a node builds one: the EVM carries the inspector, and the executor is made + // from it through the `alloy_evm` trait entry. The factory has no undeclared-inspector + // constructor, because an executor that refuses every transaction is not an API. + let evm = block_executor_factory + .evm_factory() + .create_evm(&mut state, evm_env) + .with_inspector(inspector); + let mut executor = as BlockExecutorFactory>::create_executor( + &block_executor_factory, + evm, + block_ctx, + ); // Execute contract creation transaction - this triggers the CREATE that the inspector // intercepts Init code is just STOP (0x00) diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 7f12a3e2..3be05308 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -26,7 +26,10 @@ use std::convert::Infallible; -use alloy_evm::{block::BlockExecutor, EvmEnv}; +use alloy_evm::{ + block::{BlockExecutor, BlockExecutorFactory}, + EvmEnv, EvmFactory, +}; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; use alloy_primitives::{address, Address, Bytes, Log, Signature, TxHash, TxKind, B256, U256}; use mega_evm::{ @@ -413,11 +416,15 @@ where fn test_run_transaction_refuses_an_undeclared_inspector() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( - &mut state, + let factory = executor_factory(MegaSpecId::REX7); + let evm = factory + .evm_factory() + .create_evm(&mut state, evm_env(MegaSpecId::REX7)) + .with_inspector(Observer::default()); + let mut executor = as BlockExecutorFactory>::create_executor( + &factory, + evm, block_ctx(), - evm_env(MegaSpecId::REX7), - Observer::default(), ); let tx = envelope(0); @@ -444,11 +451,15 @@ fn test_run_transaction_refuses_an_undeclared_inspector() { fn test_execute_transaction_without_commit_refuses_it_too() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( - &mut state, + let factory = executor_factory(MegaSpecId::REX7); + let evm = factory + .evm_factory() + .create_evm(&mut state, evm_env(MegaSpecId::REX7)) + .with_inspector(GasInjector::default()); + let mut executor = as BlockExecutorFactory>::create_executor( + &factory, + evm, block_ctx(), - evm_env(MegaSpecId::REX7), - GasInjector::default(), ); let tx = envelope(0); @@ -589,12 +600,17 @@ fn test_the_refusal_is_not_spec_gated() { for spec in [MegaSpecId::MINI_REX, MegaSpecId::REX4, MegaSpecId::REX6, MegaSpecId::REX7] { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(spec).create_executor_with_inspector( - &mut state, - block_ctx(), - evm_env(spec), - GasInjector::default(), - ); + let factory = executor_factory(spec); + let evm = factory + .evm_factory() + .create_evm(&mut state, evm_env(spec)) + .with_inspector(GasInjector::default()); + let mut executor = + as BlockExecutorFactory>::create_executor( + &factory, + evm, + block_ctx(), + ); let tx = envelope(0); let err = executor @@ -730,11 +746,15 @@ fn test_the_production_tracer_without_its_declaration_is_refused() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( - &mut state, + let factory = executor_factory(MegaSpecId::REX7); + let evm = factory + .evm_factory() + .create_evm(&mut state, evm_env(MegaSpecId::REX7)) + .with_inspector(TracingInspector::new(TracingInspectorConfig::all())); + let mut executor = as BlockExecutorFactory>::create_executor( + &factory, + evm, block_ctx(), - evm_env(MegaSpecId::REX7), - TracingInspector::new(TracingInspectorConfig::all()), ); let tx = envelope(0); @@ -759,11 +779,15 @@ fn test_a_system_call_does_not_run_the_inspector() { let mut db = build_db(); let mut state = State::builder().with_database(&mut db).build(); - let mut executor = executor_factory(MegaSpecId::REX7).create_executor_with_inspector( - &mut state, + let factory = executor_factory(MegaSpecId::REX7); + let evm = factory + .evm_factory() + .create_evm(&mut state, evm_env(MegaSpecId::REX7)) + .with_inspector(GasInjector::default()); + let mut executor = as BlockExecutorFactory>::create_executor( + &factory, + evm, block_ctx(), - evm_env(MegaSpecId::REX7), - GasInjector::default(), ); let result = executor diff --git a/crates/mega-evm/tests/block_executor/trait_factory_runtime_limits.rs b/crates/mega-evm/tests/block_executor/trait_factory_runtime_limits.rs index ff775c36..f92b9878 100644 --- a/crates/mega-evm/tests/block_executor/trait_factory_runtime_limits.rs +++ b/crates/mega-evm/tests/block_executor/trait_factory_runtime_limits.rs @@ -3,8 +3,8 @@ //! //! The factory exposes two paths that produce a `MegaBlockExecutor`: //! -//! 1. The inherent `create_executor` / `create_executor_with_inspector` methods, which build the -//! EVM internally and apply `block_ctx.block_limits.to_evm_tx_runtime_limits()` before +//! 1. The inherent `create_executor` / `create_executor_with_trusted_inspector` methods, which +//! build the EVM internally and apply `block_ctx.block_limits.to_evm_tx_runtime_limits()` before //! constructing the executor. //! //! 2. The trait method ` Date: Thu, 3 Sep 2026 00:13:25 +0800 Subject: [PATCH 199/208] fix(evm): read the inspector declaration off the inspector, not off the runtime flag Two rows of the construction-by-entry table admitted a transaction an inspector had taken part in. `inspect_transaction` runs the inspecting loop whatever `inspect` says, but derived its answer from `has_undeclared_inspector`, which reads that flag. An EVM whose flag was turned off through `Evm::set_inspector_enabled` and then driven through this entry ran its inspector and reported none, and the commit funnel let the result through. It now reads the declaration off the inspector's type alone; `execute_transaction`, which picks its loop on the flag, keeps reading the flag. The shim `MegaEvm::new` and `without_inspector` build wrapped `NoOpInspector` undeclared. Since `Evm::set_inspector_enabled` is a public trait method, an EVM built with no inspector could have its shim switched on and then be refused for observing nothing. Both now build the declared shim, on `NoOpInspector`'s own declaration. `InspectEvm::set_inspector` still drops the declaration, which is the safe direction and is now pinned. --- crates/mega-evm/src/evm/AGENTS.md | 8 ++ crates/mega-evm/src/evm/mod.rs | 33 +++-- .../tests/block_executor/inspector_guard.rs | 118 ++++++++++++++++++ 3 files changed, 151 insertions(+), 8 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index d85ef898..d195cf71 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -146,6 +146,14 @@ There is no undeclared counterpart on the factory: an inspector without a declar `create_executor` (the `BlockExecutorFactory` trait method) takes an EVM the caller already built and checks nothing about its inspector, because the question is a runtime one the executor's own entries ask per transaction — an error that fails the block rather than an assertion that stops the process. `bin/mega-evme`'s replay command is the worked example of the whole shape: a `TrustedTracingInspector` newtype declared over `revm-inspectors`' tracer, handed to `create_executor_with_trusted_inspector`. +**Which shim an EVM ends up with.** `MegaEvm::new` and `without_inspector` build the declared shim over `NoOpInspector`, because `Evm::set_inspector_enabled` is a public trait method: an EVM built with no inspector can have its shim switched on without any constructor being reached, and everything that then runs is `NoOpInspector`. +`with_trusted_inspector` is the only other route to the declared shim; `with_inspector` and `InspectEvm::set_inspector` build the measured one, the latter even for a type that carries a declaration, since its bound is plain `Inspector`. +So a swap drops the declaration, which is the safe direction and is what keeps the default shim's declaration from spreading to whatever replaces it. + +**Which question each execution entry asks.** `execute_transaction` selects its frame loop on the runtime flag and reports `has_undeclared_inspector`, which reads the same flag — with the flag off the inspector does not run, and reporting no inspector is right. +The deprecated `inspect_transaction` runs the inspecting loop whatever the flag says, so it reports the declaration off the inspector's type alone. +Reading the flag there would report a transaction an undeclared inspector took part in as one that had none, and the commit funnel would admit it. + ### The window a counter edit reaches nothing through A gas-counter edit made while the interpreter is already holding a `Return` action is written into an object nobody reads again: revm's inspected loop runs `step_end` after the instruction that set the action, and the action carries its own snapshot of the gas, which is what becomes the frame's result. diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 3e7f4985..1081b97f 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -190,7 +190,11 @@ impl MegaEvm MegaEvm { /// Creates a new `MegaETH` EVM instance with the inspector disabled at runtime. /// + /// The caller's inspector is dropped and replaced by `NoOpInspector`, carrying that type's own + /// [`TrustedObserver`] declaration — so an EVM this produces is admitted by the canonical + /// block-execution path even if its inspector is switched back on through + /// [`Evm::set_inspector_enabled`](alloy_evm::Evm::set_inspector_enabled). + /// /// # Returns /// /// A new `Evm` instance with the inspector disabled. @@ -270,7 +279,7 @@ impl MegaEvm { let mega_cfg = self.mega_cfg; let inner = revm::context::Evm::new_with_inspector( self.inner.ctx, - MeasuredInspector::new(NoOpInspector), + MeasuredInspector::new_trusted(NoOpInspector), self.inner.instruction, self.inner.precompiles, ); @@ -484,11 +493,15 @@ where /// direct journal write. A declaration is a line someone wrote in source about a type they had /// read, which is the only thing that answers that. /// - /// False for an EVM with no inspector: revm's plain frame loop never calls one, so there is - /// nothing to declare. False for one built through - /// [`with_trusted_inspector`](Self::with_trusted_inspector). True for every other inspected - /// EVM, including one whose inspector only observes — the criterion is the declaration, not - /// the behaviour of one run. + /// False for an EVM with no inspector, for two independent reasons: revm's plain frame loop + /// never calls one, and the shim such an EVM carries wraps `NoOpInspector`, which is declared. + /// The second reason is the load-bearing one, because + /// [`Evm::set_inspector_enabled`](alloy_evm::Evm::set_inspector_enabled) is a public trait + /// method that turns the first one off without changing the inspector. + /// + /// False for an EVM built through [`with_trusted_inspector`](Self::with_trusted_inspector). + /// True for every other inspected EVM, including one whose inspector only observes — the + /// criterion is the declaration, not the behaviour of one run. pub const fn has_undeclared_inspector(&self) -> bool { self.inspect && !self.inner.inspector.is_trusted() } @@ -513,7 +526,11 @@ where ) -> Result> { let result_and_state = InspectEvm::inspect_tx(self, tx)?; let trusted_inspector = self.inner.inspector.is_trusted(); - let undeclared_inspector = self.has_undeclared_inspector(); + // Not `has_undeclared_inspector()`: that reads the `inspect` flag, and this entry runs the + // inspecting loop whatever the flag says. An inspector swapped in through + // `InspectEvm::set_inspector` leaves the flag alone, so asking the flag here would report + // a transaction an undeclared inspector took part in as one that had none. + let undeclared_inspector = !trusted_inspector; let is_inside_sandbox = self.ctx().is_inside_sandbox(); let spec = self.ctx().spec; let additional_limit = self.ctx().additional_limit.borrow(); diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 3be05308..76178481 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -877,3 +877,121 @@ fn test_an_off_path_evm_runs_a_free_memory_growth_to_completion() { assert_eq!(outcome.inspector_ledger.interventions, 1, "the growth must still be booked"); assert!(outcome.undeclared_inspector); } + +/// The default shim is `NoOpInspector`'s own declaration, not an undeclared wrapper around it. +/// +/// `Evm::set_inspector_enabled` is a public trait method, so an EVM built with no inspector at all +/// can have its shim switched on without any constructor being reached. Everything that runs then +/// is `NoOpInspector`, which this crate declares — so the block path must admit the transaction. +/// Building the shim undeclared would refuse an EVM for observing nothing. +#[test] +fn test_an_evm_with_no_inspector_is_admitted_after_its_shim_is_switched_on() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + ); + + alloy_evm::Evm::enable_inspector(executor.evm_mut()); + assert!( + !executor.evm().has_undeclared_inspector(), + "the inspector an uninspected EVM carries is the declared one", + ); + + let tx = envelope(0); + let outcome = executor + .run_transaction(Recovered::new_unchecked(&tx, CALLER)) + .expect("an EVM observing nothing must not be refused"); + assert!(outcome.result.is_success(), "fixture check: {:?}", outcome.result); + assert!(!outcome.inner.undeclared_inspector, "and must report itself declared"); + executor.commit_transaction_outcome(outcome).expect("nor at commit"); + let (_, result) = executor.finish().expect("the block must finish"); + assert_eq!(result.receipts.len(), 1); +} + +/// Swapping an inspector in through `InspectEvm::set_inspector` drops the declaration, whatever +/// the type being swapped in. +/// +/// The constructor whose bound is `TrustedObserver` is the only route to the declared shim, and +/// `set_inspector`'s bound is plain `Inspector` — so it builds a measured one even for a type that +/// carries a declaration. That is the safe direction and it is pinned here, because it is what +/// keeps the default shim's declaration from spreading to whatever replaces it. +#[test] +fn test_swapping_an_inspector_in_drops_the_declaration() { + let mut db = build_db(); + let mut evm = mega_evm::MegaEvm::new( + mega_evm::MegaContext::new(&mut db, MegaSpecId::REX7) + .with_tx_runtime_limits(mega_evm::EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ); + assert!(evm.has_trusted_inspector(), "the default shim carries `NoOpInspector`'s declaration"); + + revm::InspectEvm::set_inspector(&mut evm, NoOpInspector); + assert!( + !evm.has_trusted_inspector(), + "a swapped-in inspector is measured: the declaration belongs to the constructor, not the \ + type being handed over", + ); +} + +/// The deprecated `inspect_transaction` runs the inspecting loop whatever the runtime flag says, +/// so what it reports about the inspector cannot be read off that flag. +/// +/// `Evm::set_inspector_enabled(false)` turns off the flag `execute_transaction` picks its loop on +/// and leaves the inspector where it is. Driven through this entry the inspector still runs, so an +/// outcome saying no inspector took part would let the commit funnel admit a transaction one did. +#[test] +fn test_inspect_transaction_reports_an_inspector_the_runtime_flag_hides() { + let mut db = build_db(); + let mut state = State::builder().with_database(&mut db).build(); + let mut executor = executor_factory(MegaSpecId::REX7).create_executor( + &mut state, + block_ctx(), + evm_env(MegaSpecId::REX7), + ); + + let mut inspected_db = build_db(); + let mut evm = mega_evm::MegaEvm::new( + mega_evm::MegaContext::new(&mut inspected_db, MegaSpecId::REX7) + .with_tx_runtime_limits(mega_evm::EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ) + .with_inspector(GasInjector::default()); + alloy_evm::Evm::set_inspector_enabled(&mut evm, false); + assert!( + !evm.has_undeclared_inspector(), + "fixture check: with the flag off, the entry that honours it reports no inspector", + ); + + let mut tx_env = mega_evm::MegaTransaction::new( + revm::context::tx::TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(1_000_000) + .build_fill(), + ); + tx_env.enveloped_tx = Some(Bytes::new()); + #[expect(deprecated, reason = "the entry under test is the deprecated one")] + let outcome = evm.inspect_transaction(tx_env).expect("the EVM supports the rewrite in full"); + + assert!(evm.inspector.applied, "fixture check: the inspector really did run"); + assert!( + outcome.undeclared_inspector, + "an entry that always inspects must report the inspector it always runs", + ); + + let tx = envelope(0); + let err = executor + .commit_tx_result(mega_evm::MegaBlockTxResult { + tx_type: tx.tx_type(), + tx_hash: *tx.hash(), + gas_limit: 1_000_000, + tx_size: 0, + da_size: 0, + depositor: None, + inner: outcome, + }) + .expect_err("and the commit funnel must refuse it"); + expect_undeclared(&err, *tx.hash()); + assert!(executor.receipts.is_empty(), "no receipt may have been pushed"); +} From bc09357a18e57020aff3c6166a5e56e934e168db Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 00:23:43 +0800 Subject: [PATCH 200/208] feat(evm): supply the declaration wrapper once, instead of a forwarding newtype per embedder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declaring a foreign tracer read-only meant writing about a hundred lines of per-callback forwarding, and this repository already had three copies of it — `mega-evme`'s replay command, the tracer bench subject, and the block-executor guard tests. Every embedder would have written a fourth. The copies were also a quiet failure waiting: every `Inspector` method has a default body, so a callback revm adds and a forwarder misses is not a compile error but a callback the wrapped tracer stops receiving, and a trace short a frame does not announce itself. `DeclaredObserver` is that forwarder, supplied once next to `TrustedObserver` and forwarding all twelve of revm 40's callbacks. It does not weaken what a declaration means: `DeclaredObserver(tracer)` is still an assertion someone makes in source about one concrete inspector, moved from a newtype's definition to the line that wraps the value, and a false one still fails the debug verification at the callback that breaks it. The three copies are replaced by it. `tests/block_executor/declared_observer.rs` holds the completeness: each callback invoked directly and checked to arrive, the callback sequence a recorder sees compared wrapped against bare, and the same comparison against `revm-inspectors`' own tracer — the last being the one that goes red on an upgrade, since a tracer that grows a callback the wrapper has not grown produces a different trace. --- AGENTS.md | 2 +- bin/mega-evme/src/common/trace.rs | 102 +---- crates/mega-evm/benches/common/subject.rs | 93 +--- crates/mega-evm/src/block/factory.rs | 5 +- crates/mega-evm/src/block/result.rs | 7 +- crates/mega-evm/src/evm/AGENTS.md | 6 +- crates/mega-evm/src/evm/inspector.rs | 127 +++++- crates/mega-evm/src/evm/mod.rs | 2 +- .../tests/block_executor/declared_observer.rs | 414 ++++++++++++++++++ .../tests/block_executor/inspector_guard.rs | 125 +----- crates/mega-evm/tests/block_executor/main.rs | 1 + 11 files changed, 580 insertions(+), 304 deletions(-) create mode 100644 crates/mega-evm/tests/block_executor/declared_observer.rs diff --git a/AGENTS.md b/AGENTS.md index 9681906b..779c4f70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,7 +143,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi The whole ledger travels on `MegaTransactionOutcome::inspector_ledger`, but it is not what a block is admitted on: an inspector can edit the interpreter's stack or memory contents, or write the journal directly, and change the transaction while leaving every lane at zero. Admission rests on a `TrustedObserver` declaration instead — a line written in source about one concrete type — and the canonical block path (`run_transaction_with_sizes`, `run_tx_env_with_sizes`, and the `commit_tx_result` funnel every commit entry routes through) refuses a transaction from an EVM running an undeclared inspector with `MegaBlockExecutionError::UndeclaredInspector`, before running it, in release builds as well as debug; `MegaTransactionOutcome::undeclared_inspector` is what carries the answer to the commit funnel. The ledger is the backstop behind that, read at the same entries as `MegaBlockExecutionError::InspectorAdjustedAccounting`, for a declaration that did not hold and for a result reaching the funnel from a producer this executor never saw. - A tracer keeps working by being declared — `MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is the entry, and `bin/mega-evme`'s replay command the worked example of the newtype a foreign tracer needs; an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. + A tracer keeps working by being declared — `MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is the entry, and a tracer this crate cannot implement the trait for is wrapped in `DeclaredObserver`, which carries the declaration and forwards every callback, with `bin/mega-evme`'s replay command as the worked example; an embedder that wants a rewriting inspector drives `MegaEvm::execute_transaction` directly, which supports it in full and is not covered by the guard — that is what leaves an off-band simulation EVM free to rewrite. Pre- and post-block system calls and the keyless-deploy sandbox are not entries the guard has to cover: neither produces a `MegaTransactionOutcome`, the ledger is reset at the start of every transaction, and both run uninspected anyway (`Handler::run_system_call` takes the plain frame loop; the sandbox builds its own EVM with no inspector). Subject to a per-spec compute gas limit and further restricted by gas detention (see below). - **Storage gas**: Charges for persistent state modifications (SSTORE, account creation, contract deployment). diff --git a/bin/mega-evme/src/common/trace.rs b/bin/mega-evme/src/common/trace.rs index 96b93faf..8d7909dc 100644 --- a/bin/mega-evme/src/common/trace.rs +++ b/bin/mega-evme/src/common/trace.rs @@ -2,7 +2,7 @@ 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, }; @@ -14,103 +14,16 @@ use mega_evm::{ ContextTr, }, database::DatabaseRef, - handler::FrameResult, - interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, - InterpreterTypes, - }, state::EvmState, - ExecuteEvm, InspectEvm, Inspector, + ExecuteEvm, InspectEvm, }, - MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, TrustedObserver, + DeclaredObserver, MegaContext, MegaEvm, MegaHaltReason, MegaTransaction, }; use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; use tracing::{debug, info, trace}; use super::{EvmeError, EvmeExternalEnvs, EvmeState}; -/// A read-only declaration around `revm-inspectors`' tracer, for the block-execution path. -/// -/// `MegaBlockExecutor` refuses a transaction from an EVM running an inspector whose type carries -/// no [`TrustedObserver`] declaration, because its measurement shim cannot see an edit made to the -/// interpreter's stack contents or straight into the journal, and block production and block -/// validation have to agree on every node. [`TracingInspector`] writes nothing back to the EVM, -/// but the declaration cannot be made about it here: both it and the trait are foreign to this -/// crate, so it is made about a local newtype that forwards every callback unchanged. That is the -/// same shape a node keeping tracing on block production has to write. -#[derive(Debug)] -pub struct TrustedTracingInspector(pub TracingInspector); - -impl TrustedObserver for TrustedTracingInspector {} - -impl Inspector for TrustedTracingInspector -where - INTR: InterpreterTypes, - TracingInspector: Inspector, -{ - fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.initialize_interp(interp, context); - } - - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step(interp, context); - } - - fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step_end(interp, context); - } - - fn log(&mut self, context: &mut CTX, log: Log) { - self.0.log(context, log); - } - - fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { - self.0.log_full(interp, context, log); - } - - fn frame_start( - &mut self, - context: &mut CTX, - frame_input: &mut FrameInput, - ) -> Option { - self.0.frame_start(context, frame_input) - } - - fn frame_end( - &mut self, - context: &mut CTX, - frame_input: &FrameInput, - frame_result: &mut FrameResult, - ) { - self.0.frame_end(context, frame_input, frame_result); - } - - fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { - self.0.call(context, inputs) - } - - fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.0.call_end(context, inputs, outcome); - } - - fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { - self.0.create(context, inputs) - } - - fn create_end( - &mut self, - context: &mut CTX, - inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - self.0.create_end(context, inputs, outcome); - } - - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.0.selfdestruct(contract, target, value); - } -} - /// Tracer type for execution analysis #[derive(Debug, Clone, Copy, ValueEnum, Default)] #[non_exhaustive] @@ -192,8 +105,13 @@ impl TraceArgs { /// The same tracer, wrapped in the declaration the canonical block-execution path admits an /// inspected transaction on. - pub fn create_trusted_inspector(&self) -> TrustedTracingInspector { - TrustedTracingInspector(self.create_inspector()) + /// + /// [`TracingInspector`] writes nothing back to the EVM, but the declaration cannot be made + /// about it here — both it and the trait are foreign to this crate — so it is made at the + /// point of use, about this one value, by wrapping it. That is the whole of what a node + /// keeping tracing on block production has to write. + pub fn create_trusted_inspector(&self) -> DeclaredObserver { + DeclaredObserver(self.create_inspector()) } /// Creates [`GethDefaultTracingOptions`] from CLI arguments diff --git a/crates/mega-evm/benches/common/subject.rs b/crates/mega-evm/benches/common/subject.rs index 74f24d2b..5abe80c1 100644 --- a/crates/mega-evm/benches/common/subject.rs +++ b/crates/mega-evm/benches/common/subject.rs @@ -9,12 +9,12 @@ //! operator-fee zero-out — is defined once in the "Comparability baseline" //! section below, not repeated per stack. -use alloy_primitives::{Address, Bytes, Log, U256}; +use alloy_primitives::{Bytes, U256}; use core::convert::Infallible; use criterion::black_box; use mega_evm::{ - revm::inspector::NoOpInspector, test_utils::MemoryDatabase, EmptyExternalEnv, MegaContext, - MegaEvm, MegaSpecId, MegaTransaction, TestExternalEnvs, TrustedObserver, + revm::inspector::NoOpInspector, test_utils::MemoryDatabase, DeclaredObserver, EmptyExternalEnv, + MegaContext, MegaEvm, MegaSpecId, MegaTransaction, TestExternalEnvs, TrustedObserver, }; use op_revm::{ DefaultOp as _, OpBuilder as _, OpContext as OpContextPinned, OpSpecId as OpSpecIdPinned, @@ -23,11 +23,6 @@ use op_revm::{ use revm::{ context::{tx::TxEnvBuilder, TxEnv}, database::EmptyDB as EmptyDBPinned, - handler::FrameResult, - interpreter::{ - CallInputs, CallOutcome, CreateInputs, CreateOutcome, FrameInput, Interpreter, - InterpreterTypes, - }, primitives::hardfork::SpecId as SpecIdPinned, Context as ContextPinned, ExecuteEvm, InspectEvm, Inspector, MainBuilder as _, MainContext as _, @@ -213,86 +208,6 @@ pub enum InspectKind { GethTracerTrusted, } -/// A read-only declaration around `revm-inspectors`' geth tracer. -/// -/// The orphan rule keeps `TrustedObserver` from being implemented for -/// `TracingInspector` directly — from here both are foreign — so the -/// declaration is made about a local newtype that forwards every callback -/// unchanged. That is the same shape a downstream node has to write for the -/// same reason, which is why the row is worth running against it rather than -/// against a bare tracer that could not be declared at all. -pub struct TrustedGethTracer(TracingInspector); - -impl TrustedObserver for TrustedGethTracer {} - -impl Inspector for TrustedGethTracer -where - INTR: InterpreterTypes, - TracingInspector: Inspector, -{ - fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.initialize_interp(interp, context); - } - - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step(interp, context); - } - - fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step_end(interp, context); - } - - fn log(&mut self, context: &mut CTX, log: Log) { - self.0.log(context, log); - } - - fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { - self.0.log_full(interp, context, log); - } - - fn frame_start( - &mut self, - context: &mut CTX, - frame_input: &mut FrameInput, - ) -> Option { - self.0.frame_start(context, frame_input) - } - - fn frame_end( - &mut self, - context: &mut CTX, - frame_input: &FrameInput, - frame_result: &mut FrameResult, - ) { - self.0.frame_end(context, frame_input, frame_result); - } - - fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { - self.0.call(context, inputs) - } - - fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.0.call_end(context, inputs, outcome); - } - - fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { - self.0.create(context, inputs) - } - - fn create_end( - &mut self, - context: &mut CTX, - inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - self.0.create_end(context, inputs, outcome); - } - - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.0.selfdestruct(contract, target, value); - } -} - /// `MegaEvm` on the inspected frame loop, with the measurement shim live. /// /// The plain [`Mega`] row calls `ExecuteEvm::transact`, which never enters an @@ -325,7 +240,7 @@ impl Subject for MegaInspected { } InspectKind::GethTracerTrusted => { run_inspected_trusted(self.name, self.spec, workload, || { - TrustedGethTracer(TracingInspector::new(TracingInspectorConfig::default_geth())) + DeclaredObserver(TracingInspector::new(TracingInspectorConfig::default_geth())) }); } } diff --git a/crates/mega-evm/src/block/factory.rs b/crates/mega-evm/src/block/factory.rs index 011e43c2..517e2ec8 100644 --- a/crates/mega-evm/src/block/factory.rs +++ b/crates/mega-evm/src/block/factory.rs @@ -110,8 +110,9 @@ where /// takes an EVM the caller built and whose transactions are then refused one by one. /// /// A `revm-inspectors` tracer cannot be declared where both it and the trait are foreign, so a - /// node writes a forwarding newtype of its own and declares that; `bin/mega-evme`'s replay - /// command is the shape to copy. + /// node wraps it in [`DeclaredObserver`](crate::DeclaredObserver), which is local here, carries + /// the declaration and forwards every callback; `bin/mega-evme`'s replay command is the shape + /// to copy. /// /// # Parameters /// diff --git a/crates/mega-evm/src/block/result.rs b/crates/mega-evm/src/block/result.rs index 1a21105b..8760c92e 100644 --- a/crates/mega-evm/src/block/result.rs +++ b/crates/mega-evm/src/block/result.rs @@ -281,9 +281,10 @@ pub enum MegaBlockExecutionError { /// the type's author declared and not what this run happened to do. /// /// A tracer keeps working by being declared. `revm-inspectors`' tracers are foreign types and - /// the trait is local to this crate, so a node writes a forwarding newtype of its own and - /// declares that; `bin/mega-evme`'s replay command does exactly this. An embedder that wants a - /// rewriting inspector still has one — [`MegaEvm::execute_transaction`]( + /// the trait is local to this crate, so a node wraps one in + /// [`DeclaredObserver`](crate::DeclaredObserver), which carries the declaration and forwards + /// every callback; `bin/mega-evme`'s replay command does exactly this. An embedder that wants + /// a rewriting inspector still has one — [`MegaEvm::execute_transaction`]( /// crate::MegaEvm::execute_transaction) supports it in full — it just does not get to call the /// result a block. #[error( diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index d195cf71..1093054e 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -130,7 +130,9 @@ There is no behavioural fork between the two builds for a declaration that holds **What may be declared.** Read-only tracers: the `revm-inspectors` `TracingInspector` family (`debug_traceTransaction`, `trace_*`, the call and prestate tracers) and anything else that only records what it is shown. `NoOpInspector` is declared here, being the only inspector this crate can reach. -The rest cannot be declared from here or from `mega-reth`, because the orphan rule wants one of the two to be local and neither the trait nor `TracingInspector` is: a node declares a newtype of its own that forwards every callback, which `benches/common/subject.rs` does for the `inspect_tracer_trusted` rows and is the shape to copy. +The rest cannot be declared from here or from `mega-reth`, because the orphan rule wants one of the two to be local and neither the trait nor `TracingInspector` is: a node wraps the tracer in `DeclaredObserver`, which is local here, carries the declaration and forwards every callback, so the whole of what an embedder writes is `DeclaredObserver(tracer)`. +The declaration is still an assertion made in source about one concrete inspector — the wrapper moves where it is written, from a newtype's definition to the line that wraps the value, and is not a way around the rules above. +`benches/common/subject.rs` uses it for the `inspect_tracer_trusted` rows and is the shape to copy. A wrapper that does nothing but forward may lift a declaration — `&mut T` does — but only from a concrete declared type; a wrapper that adds behaviour of its own is a type in its own right and has to be read on its own terms. **What may not be declared.** Anything that intercepts or rewrites, however little. @@ -144,7 +146,7 @@ The route is `factory.create_evm(db, env).with_trusted_inspector(tracer)`, which `MegaBlockExecutorFactory::create_executor_with_trusted_inspector` is that route packaged for the block path, and it is the entry a node tracing block production or validation takes. There is no undeclared counterpart on the factory: an inspector without a declaration reaches an executor only by building the EVM and passing it to the `BlockExecutorFactory` trait entry below, which is the shape a node already uses and which refuses the transactions rather than the construction. `create_executor` (the `BlockExecutorFactory` trait method) takes an EVM the caller already built and checks nothing about its inspector, because the question is a runtime one the executor's own entries ask per transaction — an error that fails the block rather than an assertion that stops the process. -`bin/mega-evme`'s replay command is the worked example of the whole shape: a `TrustedTracingInspector` newtype declared over `revm-inspectors`' tracer, handed to `create_executor_with_trusted_inspector`. +`bin/mega-evme`'s replay command is the worked example of the whole shape: `DeclaredObserver(TracingInspector::new(..))`, handed to `create_executor_with_trusted_inspector`. **Which shim an EVM ends up with.** `MegaEvm::new` and `without_inspector` build the declared shim over `NoOpInspector`, because `Evm::set_inspector_enabled` is a public trait method: an EVM built with no inspector can have its shim switched on without any constructor being reached, and everything that then runs is `NoOpInspector`. `with_trusted_inspector` is the only other route to the declared shim; `with_inspector` and `InspectEvm::set_inspector` build the measured one, the latter even for a type that carries a declaration, since its bound is plain `Inspector`. diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 042d445e..d77befe4 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -88,9 +88,9 @@ pub const FORBIDDEN_FRAME_INIT_REWRITE: &str = /// - **Do not implement it for anything that intercepts.** An inspector that answers a frame /// itself, edits inputs, or rewrites a result is a rewriting inspector however little it /// rewrites; those are supported, measured, and must stay measured. -/// - **A foreign inspector needs a newtype.** The orphan rule wants one of the trait and the type -/// to be local, and for a `revm-inspectors` tracer neither is, so a node declares a forwarding -/// newtype of its own. +/// - **A foreign inspector needs a wrapper.** The orphan rule wants one of the trait and the type +/// to be local, and for a `revm-inspectors` tracer neither is, so a node wraps it in +/// [`DeclaredObserver`], which is local here and carries the declaration. /// /// Debug builds measure a declared type anyway and assert the ledger stayed empty after every /// callback, so a wrong declaration fails at the callback that broke it. @@ -106,6 +106,127 @@ impl TrustedObserver for revm::inspector::NoOpInspector {} /// is declared exactly when `T` is, so no type becomes trusted that was not trusted already. impl TrustedObserver for &mut T {} +/// Carries a [`TrustedObserver`] declaration for an inspector whose type cannot carry one. +/// +/// The orphan rule wants one of the trait and the type to be local, and for a `revm-inspectors` +/// tracer used from a node neither is. This is the local half, supplied once here so that every +/// embedder does not write it again: it forwards every callback of the `Inspector` trait to the +/// inspector inside it and adds nothing of its own. +/// +/// The declaration is still an assertion someone makes in source about one concrete inspector — +/// `DeclaredObserver` only moves where it is written, from a newtype's definition to the line that +/// wraps the value. `DeclaredObserver(tracer)` says "I have read this tracer and it writes nothing +/// back to the EVM", exactly as a hand-written forwarding newtype did, and it is subject to the +/// same rules: wrapping something that intercepts or rewrites is a false declaration, and a debug +/// build will fail at the callback that breaks it. It is a way of writing the promise, not a way +/// around it. +/// +/// ```ignore +/// let executor = factory.create_executor_with_trusted_inspector( +/// db, +/// block_ctx, +/// evm_env, +/// DeclaredObserver(TracingInspector::new(TracingInspectorConfig::all())), +/// ); +/// ``` +/// +/// Forwarding by hand is what this replaces, and the reason is that a hand-written forwarder fails +/// quietly: every `Inspector` method has a default body, so a callback revm adds and a forwarder +/// misses is not a compile error but a callback the wrapped inspector stops receiving. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DeclaredObserver(pub I); + +impl DeclaredObserver { + /// Declares `inner` read-only and wraps it. + pub const fn new(inner: I) -> Self { + Self(inner) + } + + /// The declared inspector. + pub const fn inner(&self) -> &I { + &self.0 + } + + /// The declared inspector, mutably. + pub const fn inner_mut(&mut self) -> &mut I { + &mut self.0 + } + + /// Unwraps the declaration, returning the inspector it was made about. + pub fn into_inner(self) -> I { + self.0 + } +} + +/// The whole of what the wrapper is for. +impl TrustedObserver for DeclaredObserver {} + +/// Every callback of revm 40's `Inspector`, forwarded unchanged. +/// +/// Written out in full rather than left to the trait's default bodies: a default body does not +/// forward, it does nothing, so an unlisted callback would be one the wrapped inspector silently +/// stops receiving. `tests/block_executor/declared_observer.rs` compares the callback sequence a +/// recording inspector sees wrapped against the one it sees bare, which is what turns an upstream +/// callback added here and not forwarded into a failing test. +impl Inspector for DeclaredObserver +where + INTR: InterpreterTypes, + I: Inspector, +{ + fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.initialize_interp(interp, context); + } + + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step(interp, context); + } + + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.0.step_end(interp, context); + } + + fn log(&mut self, context: &mut CTX, log: Log) { + self.0.log(context, log); + } + + fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.0.log_full(interp, context, log); + } + + fn frame_start(&mut self, context: &mut CTX, frame_input: &mut FI) -> Option { + self.0.frame_start(context, frame_input) + } + + fn frame_end(&mut self, context: &mut CTX, frame_input: &FI, frame_result: &mut FR) { + self.0.frame_end(context, frame_input, frame_result); + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.0.call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.0.call_end(context, inputs, outcome); + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.0.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.0.create_end(context, inputs, outcome); + } + + fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { + self.0.selfdestruct(contract, target, value); + } +} + /// Wraps a user inspector so that what it does to gas accounting is measured and booked. /// /// `MegaETH` applies this itself, so the wrapper is not something a caller opts into or can opt diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index 1081b97f..ed940db7 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -239,7 +239,7 @@ impl MegaEvm { /// builds measure anyway and assert that the declaration held. /// /// See [`TrustedObserver`] for what the declaration promises and what it may not be written - /// for. + /// for, and [`DeclaredObserver`] for how a tracer this crate cannot name gets one. /// /// [`EvmFactory::create_evm_with_inspector`](alloy_evm::EvmFactory::create_evm_with_inspector) /// cannot reach this — its bound is `I: Inspector` and its return type is fixed — so a node diff --git a/crates/mega-evm/tests/block_executor/declared_observer.rs b/crates/mega-evm/tests/block_executor/declared_observer.rs new file mode 100644 index 00000000..a61c5572 --- /dev/null +++ b/crates/mega-evm/tests/block_executor/declared_observer.rs @@ -0,0 +1,414 @@ +//! `DeclaredObserver` forwards the whole `Inspector` trait, and nothing else. +//! +//! The wrapper exists so that a node declaring a foreign tracer read-only writes one line instead +//! of a hundred, and the hundred it replaces were dangerous: every `Inspector` method has a default +//! body, so a callback revm adds and a hand-written forwarder misses is not a compile error but a +//! callback the wrapped tracer silently stops receiving. A tracer whose output is quietly short a +//! frame is worse than one that fails to build. +//! +//! Moving the forwarding into this crate does not remove that hazard, it concentrates it — there is +//! one forwarder now, and it is this crate's to keep complete. Three tests hold it: +//! +//! - [`test_every_callback_the_trait_declares_is_forwarded`] invokes each callback directly and +//! checks the inner inspector received it. This is the pin on the set as it stands, and the +//! overriding recorder makes a rename or a removal upstream a compile error. +//! - [`test_wrapping_changes_no_callback_a_recorder_sees`] runs one transaction twice, bare and +//! wrapped, and compares the callback sequences element for element. +//! - [`test_wrapping_changes_no_trace_the_production_tracer_produces`] does the same with +//! `revm-inspectors`' own tracer instead of a fixture recorder. That is the one that survives an +//! upgrade: a callback added to the trait is added to `TracingInspector` too, so a forwarder that +//! has not grown the new method produces a different trace here while a fixture recorder written +//! before the upgrade would notice nothing. + +use alloy_primitives::{address, Address, Bytes, Log, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + DeclaredObserver, EvmTxRuntimeLimits, MegaContext, MegaEvm, MegaSpecId, MegaTransaction, + MegaTransactionNew as _, +}; +use revm::{ + bytecode::opcode::{CALL, CREATE, MSTORE, MSTORE8, POP, SELFDESTRUCT, STOP}, + handler::FrameResult, + interpreter::{ + interpreter::EthInterpreter, CallInput, CallInputs, CallOutcome, CallScheme, CallValue, + CreateInputs, CreateOutcome, CreateScheme, FrameInput, Gas, InstructionResult, Interpreter, + InterpreterResult, InterpreterTypes, + }, + Inspector, +}; + +/// Sends the fixture transaction. +const CALLER: Address = address!("2000000000000000000000000000000000000002"); +/// The entry contract: logs, calls, creates and writes a slot. +const CONTRACT: Address = address!("1000000000000000000000000000000000000001"); +/// The callee, which self-destructs so that the last callback of the trait fires too. +const CALLEE: Address = address!("1000000000000000000000000000000000000002"); +/// Where the callee sends its balance. +const BENEFICIARY: Address = address!("1000000000000000000000000000000000000003"); + +/// Gas the entry contract forwards to its inner call. +const INNER_CALL_GAS: u64 = 60_000; + +// --- the recorder --------------------------------------------------------------------------- + +/// Every callback `Inspector` declares today, in the order the trait declares them. +/// +/// Restated as data so that [`test_every_callback_the_trait_declares_is_forwarded`] can compare a +/// set against it. The compile-time half of the same pin is [`Recorder`]'s impl below, which +/// overrides all of them: a callback upstream renames or removes stops this file building. +const CALLBACKS: [&str; 12] = [ + "initialize_interp", + "step", + "step_end", + "log", + "log_full", + "frame_start", + "frame_end", + "call", + "call_end", + "create", + "create_end", + "selfdestruct", +]; + +/// Writes down the name of every callback it is handed, in order, and changes nothing. +#[derive(Default)] +struct Recorder { + seen: Vec<&'static str>, +} + +impl Recorder { + /// The distinct callbacks this recorder was handed, so a comparison names the missing one + /// rather than an index into a thousand-element sequence. + fn distinct(&self) -> Vec<&'static str> { + let mut seen: Vec<&'static str> = self.seen.clone(); + seen.sort_unstable(); + seen.dedup(); + seen + } +} + +impl Inspector for Recorder { + fn initialize_interp(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("initialize_interp"); + } + + fn step(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("step"); + } + + fn step_end(&mut self, _interp: &mut Interpreter, _context: &mut CTX) { + self.seen.push("step_end"); + } + + fn log(&mut self, _context: &mut CTX, _log: Log) { + self.seen.push("log"); + } + + fn log_full(&mut self, _interp: &mut Interpreter, _context: &mut CTX, _log: Log) { + self.seen.push("log_full"); + } + + fn frame_start( + &mut self, + _context: &mut CTX, + _frame_input: &mut FrameInput, + ) -> Option { + self.seen.push("frame_start"); + None + } + + fn frame_end( + &mut self, + _context: &mut CTX, + _frame_input: &FrameInput, + _frame_result: &mut FrameResult, + ) { + self.seen.push("frame_end"); + } + + fn call(&mut self, _context: &mut CTX, _inputs: &mut CallInputs) -> Option { + self.seen.push("call"); + None + } + + fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) { + self.seen.push("call_end"); + } + + fn create(&mut self, _context: &mut CTX, _inputs: &mut CreateInputs) -> Option { + self.seen.push("create"); + None + } + + fn create_end( + &mut self, + _context: &mut CTX, + _inputs: &CreateInputs, + _outcome: &mut CreateOutcome, + ) { + self.seen.push("create_end"); + } + + fn selfdestruct(&mut self, _contract: Address, _target: Address, _value: U256) { + self.seen.push("selfdestruct"); + } +} + +// --- the fixture ---------------------------------------------------------------------------- + +/// The code the created contract leaves behind: nothing. +fn init_code() -> Vec { + vec![STOP] +} + +/// One `LOG1`, one inner `CALL` to a self-destructing callee, one `CREATE`, one `SSTORE`. +/// +/// Written so that as many of the trait's callbacks as a transaction can reach fire in a single +/// run. `log` is the one that cannot: revm calls it only for the logs a precompile emitted, and no +/// precompile this crate registers emits any. It is covered by the direct-invocation pin instead. +fn caller_code() -> Bytes { + let init = init_code(); + let mut builder = BytecodeBuilder::default() + // A word in memory for the LOG to read. + .push_number(0xAAu64) + .push_number(0u64) + .append(MSTORE) + // LOG1(offset=0, size=32, topic=1) + .push_number(1u64) + .push_number(32u64) + .push_number(0u64) + .append(revm::bytecode::opcode::LOG1) + // CALL(gas, CALLEE, value=0, argsOffset=0, argsSize=0, retOffset=0, retSize=0) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_number(0u64) + .push_address(CALLEE) + .push_number(u128::from(INNER_CALL_GAS)) + .append(CALL) + .append(POP); + for (offset, byte) in init.iter().enumerate() { + builder = builder.push_number(u64::from(*byte)).push_number(offset as u64).append(MSTORE8); + } + builder + .push_number(init.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .sstore(U256::from(1), U256::from(9)) + .append(STOP) + .build() +} + +fn build_db() -> MemoryDatabase { + let mut db = MemoryDatabase::default(); + db.set_account_code(CONTRACT, caller_code()); + db.set_account_code( + CALLEE, + BytecodeBuilder::default().push_address(BENEFICIARY).append(SELFDESTRUCT).build(), + ); + db.set_account_balance(CALLEE, U256::from(1_000u64)); + db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000_000u64)); + db +} + +fn fixture_tx() -> MegaTransaction { + let mut tx = MegaTransaction::new( + revm::context::tx::TxEnvBuilder::default() + .caller(CALLER) + .call(CONTRACT) + .gas_limit(1_000_000) + .build_fill(), + ); + tx.enveloped_tx = Some(Bytes::new()); + tx +} + +/// Runs the fixture with `inspector` attached to a measured shim. +/// +/// Measured on both sides deliberately: the comparison is about what the wrapper forwards, so the +/// shim underneath must be the same one in both runs. A declared shim would take a different path +/// in release builds and make the two runs differ for a reason that is not the wrapper's. +fn run_with(inspector: I) +where + I: for<'a> Inspector>, +{ + let mut db = build_db(); + let mut evm = MegaEvm::new( + MegaContext::new(&mut db, MegaSpecId::REX7) + .with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(MegaSpecId::REX7)), + ) + .with_inspector(inspector); + + let outcome = evm.execute_transaction(fixture_tx()).expect("the fixture must execute"); + assert!( + outcome.result_and_state.result.is_success(), + "fixture check: {:?}", + outcome.result_and_state.result, + ); +} + +// --- the tests ------------------------------------------------------------------------------ + +/// Each of the twelve callbacks reaches the inspector inside the wrapper. +/// +/// Invoked directly rather than through a transaction, so that the two callbacks no fixture can +/// reach — `log`, which only a precompile's output produces — are covered along with the rest. What +/// this cannot see is a callback added upstream, since a name that does not exist cannot be listed; +/// [`test_wrapping_changes_no_trace_the_production_tracer_produces`] is what covers that direction. +#[test] +fn test_every_callback_the_trait_declares_is_forwarded() { + let mut wrapper = DeclaredObserver(Recorder::default()); + let inspector: &mut dyn Inspector<(), EthInterpreter> = &mut wrapper; + + let mut interpreter = Interpreter::::default(); + let mut call_inputs = sample_call_inputs(); + let mut create_inputs = sample_create_inputs(); + let mut call_outcome = sample_call_outcome(); + let mut create_outcome = sample_create_outcome(); + let mut frame_input = FrameInput::Call(Box::new(sample_call_inputs())); + let mut frame_result = FrameResult::Call(sample_call_outcome()); + + inspector.initialize_interp(&mut interpreter, &mut ()); + inspector.step(&mut interpreter, &mut ()); + inspector.step_end(&mut interpreter, &mut ()); + inspector.log(&mut (), Log::default()); + inspector.log_full(&mut interpreter, &mut (), Log::default()); + inspector.frame_start(&mut (), &mut frame_input); + inspector.frame_end(&mut (), &frame_input, &mut frame_result); + inspector.call(&mut (), &mut call_inputs); + inspector.call_end(&mut (), &call_inputs, &mut call_outcome); + inspector.create(&mut (), &mut create_inputs); + inspector.create_end(&mut (), &create_inputs, &mut create_outcome); + inspector.selfdestruct(Address::ZERO, Address::ZERO, U256::ZERO); + + assert_eq!( + wrapper.0.seen, CALLBACKS, + "every callback the trait declares must reach the inspector inside the wrapper, once, \ + unchanged", + ); +} + +/// `log_full`'s default body calls `log`, so forwarding one and not the other is not a silent +/// no-op but a rerouted callback — which is the failure a set comparison would miss. +/// +/// The recorder above overrides both, so if `DeclaredObserver::log_full` were dropped the default +/// body would forward to `DeclaredObserver::log`, the inner inspector would record `log` where the +/// EVM sent `log_full`, and the assertion in +/// [`test_every_callback_the_trait_declares_is_forwarded`] would fail on the order rather than on +/// the membership. This states that dependency so a later edit does not weaken that assertion to a +/// set comparison. +#[test] +fn test_log_full_is_forwarded_as_itself_and_not_through_log() { + let mut wrapper = DeclaredObserver(Recorder::default()); + let inspector: &mut dyn Inspector<(), EthInterpreter> = &mut wrapper; + let mut interpreter = Interpreter::::default(); + + inspector.log_full(&mut interpreter, &mut (), Log::default()); + + assert_eq!(wrapper.0.seen, ["log_full"], "the wrapper must not collapse `log_full` into `log`"); +} + +/// One transaction, run twice: the callbacks a recorder sees wrapped are the ones it sees bare. +/// +/// The fixture reaches eleven of the twelve, nested frames and a self-destruct included, so a +/// forwarder that dropped one would show up as a shorter sequence rather than as a subtle +/// difference in what the transaction produced. +#[test] +fn test_wrapping_changes_no_callback_a_recorder_sees() { + let mut bare = Recorder::default(); + run_with(&mut bare); + + let mut wrapped = DeclaredObserver(Recorder::default()); + run_with(&mut wrapped); + + assert_eq!(bare.distinct(), wrapped.0.distinct(), "the wrapper must not drop a whole callback",); + assert_eq!( + bare.seen, wrapped.0.seen, + "and must not change the order or the number of times each one fires", + ); + + let reached = bare.distinct(); + let missing: Vec<&&str> = CALLBACKS.iter().filter(|name| !reached.contains(name)).collect(); + assert_eq!( + missing, + [&"log"], + "fixture check: the fixture must keep reaching every callback a transaction can reach, so \ + that the comparison above stays worth making", + ); +} + +/// The same comparison against `revm-inspectors`' own tracer, whose callback set grows with revm's. +/// +/// This is the test that survives an upgrade. A callback added to the `Inspector` trait gets a +/// default body, so nothing here stops compiling and a recorder written against today's trait +/// records nothing new. `TracingInspector` is upgraded along with the trait, though — so if the +/// wrapper has not grown the new method, the tracer receives it bare and not wrapped, and the two +/// traces stop matching. +#[test] +fn test_wrapping_changes_no_trace_the_production_tracer_produces() { + use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; + + let trace_of = |wrap: bool| { + let mut tracer = TracingInspector::new(TracingInspectorConfig::all()); + if wrap { + run_with(DeclaredObserver(&mut tracer)); + } else { + run_with(&mut tracer); + } + (tracer.traces().nodes().len(), format!("{:?}", tracer.traces().nodes())) + }; + + let (bare_frames, bare) = trace_of(false); + let (wrapped_frames, wrapped) = trace_of(true); + + assert_eq!( + bare_frames, 3, + "fixture check: the tracer must have recorded the entry frame, the inner call and the \ + creation", + ); + assert_eq!(wrapped_frames, bare_frames, "the wrapper must not cost the tracer a frame"); + assert_eq!(bare, wrapped, "the tracer must see the same execution wrapped as bare"); +} + +// --- sample arguments for the direct-invocation pin ------------------------------------------ + +fn sample_gas() -> Gas { + Gas::new(1) +} + +fn sample_call_inputs() -> CallInputs { + CallInputs { + input: CallInput::Bytes(Bytes::new()), + return_memory_offset: 0..0, + gas_limit: 1, + reservoir: 0, + bytecode_address: Address::ZERO, + known_bytecode: Default::default(), + target_address: Address::ZERO, + caller: Address::ZERO, + value: CallValue::Transfer(U256::ZERO), + scheme: CallScheme::Call, + is_static: false, + charged_new_account_state_gas: false, + } +} + +fn sample_create_inputs() -> CreateInputs { + CreateInputs::new(Address::ZERO, CreateScheme::Create, U256::ZERO, Bytes::new(), 1, 0) +} + +fn sample_result() -> InterpreterResult { + InterpreterResult::new(InstructionResult::Stop, Bytes::new(), sample_gas()) +} + +fn sample_call_outcome() -> CallOutcome { + CallOutcome::new(sample_result(), 0..0) +} + +fn sample_create_outcome() -> CreateOutcome { + CreateOutcome::new(sample_result(), None) +} diff --git a/crates/mega-evm/tests/block_executor/inspector_guard.rs b/crates/mega-evm/tests/block_executor/inspector_guard.rs index 76178481..1abeddd7 100644 --- a/crates/mega-evm/tests/block_executor/inspector_guard.rs +++ b/crates/mega-evm/tests/block_executor/inspector_guard.rs @@ -31,24 +31,23 @@ use alloy_evm::{ EvmEnv, EvmFactory, }; use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder; -use alloy_primitives::{address, Address, Bytes, Log, Signature, TxHash, TxKind, B256, U256}; +use alloy_primitives::{address, Address, Bytes, Signature, TxHash, TxKind, B256, U256}; use mega_evm::{ alloy_consensus::{transaction::Recovered, Signed, TxLegacy}, alloy_evm::block::BlockExecutionError, test_utils::{BytecodeBuilder, MemoryDatabase}, - BlockLimits, InspectorLedger, Lane, MegaBlockExecutionCtx, MegaBlockExecutorFactory, - MegaEvmFactory, MegaHardforkConfig, MegaSpecId, MegaTransactionNew as _, - MegaTransactionOutcome, MegaTxEnvelope, TestExternalEnvs, TrustedObserver, + BlockLimits, DeclaredObserver, InspectorLedger, Lane, MegaBlockExecutionCtx, + MegaBlockExecutorFactory, MegaEvmFactory, MegaHardforkConfig, MegaSpecId, + MegaTransactionNew as _, MegaTransactionOutcome, MegaTxEnvelope, TestExternalEnvs, }; use revm::{ bytecode::opcode::{CALL, POP, STOP}, context::{BlockEnv, Cfg, ContextTr}, database::State, - handler::FrameResult, inspector::NoOpInspector, interpreter::{ - interpreter_types::MemoryTr, CallInputs, CallOutcome, CreateInputs, CreateOutcome, - FrameInput, InstructionResult, Interpreter, InterpreterTypes, + interpreter_types::MemoryTr, CallInputs, CallOutcome, InstructionResult, Interpreter, + InterpreterTypes, }, Inspector, }; @@ -171,102 +170,6 @@ impl Inspector for Observer { } } -/// The same observer, with its author's declaration that it writes nothing back. -/// -/// A separate type rather than a declaration on [`Observer`], because the pair is the experiment: -/// the two behave identically and only one of them is admitted, which is what says the criterion -/// is the declaration and not the behaviour. -#[derive(Default)] -struct DeclaredObserver { - inner: Observer, -} - -impl TrustedObserver for DeclaredObserver {} - -impl Inspector for DeclaredObserver { - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.inner.step(interp, context); - } -} - -/// A read-only declaration around `revm-inspectors`' geth tracer. -/// -/// The orphan rule keeps `TrustedObserver` from being implemented for `TracingInspector` directly -/// — from a downstream node both are foreign — so the declaration is made about a local newtype -/// that forwards every callback unchanged. `bin/mega-evme`'s replay command carries the same shape -/// for the same reason, and it is the shape a node writes to keep tracing block production. -struct TrustedTracer(revm_inspectors::tracing::TracingInspector); - -impl TrustedObserver for TrustedTracer {} - -impl Inspector for TrustedTracer -where - INTR: InterpreterTypes, - revm_inspectors::tracing::TracingInspector: Inspector, -{ - fn initialize_interp(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.initialize_interp(interp, context); - } - - fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step(interp, context); - } - - fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { - self.0.step_end(interp, context); - } - - fn log(&mut self, context: &mut CTX, log: Log) { - self.0.log(context, log); - } - - fn log_full(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { - self.0.log_full(interp, context, log); - } - - fn frame_start( - &mut self, - context: &mut CTX, - frame_input: &mut FrameInput, - ) -> Option { - self.0.frame_start(context, frame_input) - } - - fn frame_end( - &mut self, - context: &mut CTX, - frame_input: &FrameInput, - frame_result: &mut FrameResult, - ) { - self.0.frame_end(context, frame_input, frame_result); - } - - fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { - self.0.call(context, inputs) - } - - fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { - self.0.call_end(context, inputs, outcome); - } - - fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { - self.0.create(context, inputs) - } - - fn create_end( - &mut self, - context: &mut CTX, - inputs: &CreateInputs, - outcome: &mut CreateOutcome, - ) { - self.0.create_end(context, inputs, outcome); - } - - fn selfdestruct(&mut self, contract: Address, target: Address, value: U256) { - self.0.selfdestruct(contract, target, value); - } -} - fn envelope(nonce: u64) -> MegaTxEnvelope { let tx = TxLegacy { chain_id: Some(8453), @@ -624,9 +527,9 @@ fn test_the_refusal_is_not_spec_gated() { /// The green half: a declared observer is left alone, and the block it helps build is bit-identical /// to the one built without it. /// -/// [`Observer`] and [`DeclaredObserver`] do exactly the same thing, and only the declared one gets -/// here — so this and [`test_run_transaction_refuses_an_undeclared_inspector`] together say the -/// criterion really is the declaration. +/// The inspector is the same [`Observer`] the refusal test uses, wrapped in `DeclaredObserver` and +/// so declared — the two runs do exactly the same thing and only one of them is admitted, which is +/// what says the criterion really is the declaration and not the behaviour of the run. #[test] fn test_a_declared_observer_still_builds_a_block() { let build = |observe: bool| { @@ -639,7 +542,7 @@ fn test_a_declared_observer_still_builds_a_block() { &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - DeclaredObserver::default(), + DeclaredObserver(Observer::default()), ); let outcome = executor .run_transaction(Recovered::new_unchecked(&tx, CALLER)) @@ -647,7 +550,7 @@ fn test_a_declared_observer_still_builds_a_block() { assert!(!outcome.inner.undeclared_inspector, "and must report itself declared"); assert!(outcome.inner.inspector_ledger.is_zero(), "and must leave an empty ledger"); let gas = executor.commit_transaction_outcome(outcome).expect("nor at commit"); - let steps = executor.evm().inspector.inner.steps; + let steps = executor.evm().inspector.0.steps; let (_, result) = executor.finish().expect("the block must finish"); assert_eq!(result.receipts.len(), 1, "the observed block still has its receipt"); (gas, steps) @@ -699,7 +602,7 @@ fn test_the_no_op_inspector_is_admitted() { /// The real tracer that `mega-evme replay` attaches to this exact path is admitted through its /// declaration, and the block it observes is the one built without it. /// -/// The `DeclaredObserver` above is a fixture; this is the production shape, newtype and all. +/// The observer above is a fixture; this is the production shape, wrapper and all. /// `TracingInspector` receives every callback the shim measures — including the ones handed a live /// interpreter and the ones handed a frame's inputs — so if observation could move a lane by /// accident, it would move one here. Run rather than reasoned about: the refusal's blast radius is @@ -714,7 +617,7 @@ fn test_the_production_tracer_is_admitted() { &mut state, block_ctx(), evm_env(MegaSpecId::REX7), - TrustedTracer(TracingInspector::new(TracingInspectorConfig::all())), + DeclaredObserver(TracingInspector::new(TracingInspectorConfig::all())), ); let tx = envelope(0); @@ -738,7 +641,7 @@ fn test_the_production_tracer_is_admitted() { assert_eq!(result.receipts.len(), 1); } -/// The bare `TracingInspector`, without the newtype, is refused — which is what makes the newtype +/// The bare `TracingInspector`, without the wrapper, is refused — which is what makes the wrapper /// load-bearing rather than decorative. #[test] fn test_the_production_tracer_without_its_declaration_is_refused() { diff --git a/crates/mega-evm/tests/block_executor/main.rs b/crates/mega-evm/tests/block_executor/main.rs index 8619a57b..401423f4 100644 --- a/crates/mega-evm/tests/block_executor/main.rs +++ b/crates/mega-evm/tests/block_executor/main.rs @@ -3,6 +3,7 @@ mod accessed_block_hashes; mod block_limits; mod compute_gas_lanes; +mod declared_observer; mod deposit_da_exemption; mod inspector; mod inspector_guard; From 82cc5fee5b84671cab5b323d1a6bc1d599954ecb Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 00:52:23 +0800 Subject: [PATCH 201/208] fix(evm): compare a creation's inputs on their semantic fields, not their memo cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `create_inputs_rewritten` compared the whole of `CreateInputs` with the derived equality, and revm 40's `CreateInputs` carries two `OnceCell` memos — the address the creation will occupy and the hash of its init code — that are filled on demand through a shared reference. Asking a creation where it will land is what `created_address` is for, and it is what every `revm-inspectors` tracer does at every CREATE, so the comparison read the most ordinary thing an observation-only tracer does as a rewritten input. The consequences were an intervention booked for a tracer that intervened in nothing, and — for a tracer wearing a `TrustedObserver` declaration, which debug builds verify by measuring anyway — a panic at the first CREATE in the transaction. `mega-evme replay --trace` takes that path, and no offline fixture deploys anything, so the shape reached no gate. The comparison is now written over the six fields a creation's frame is built from, with the gas limit left to the envelope lane as before and the two memos excluded. What the exclusion costs is recorded where the verdict is: revm reads the address memo when it builds the frame, so a fill made with a nonce other than the caller's redirects the deployment, and telling that apart needs the pre-bump nonce and the keccak the memo exists to avoid — a reading no callback boundary can take, which puts it in the content class the declaration governs. A call's inputs keep the derived equality, which is right only because every field of `CallInputs` says what the frame does. Both claims are now closed in `tests/rex7/gas_surface.rs`: every field of both structs is classified semantic, envelope or memo against upstream's own `Debug` rendering, a semantic field needs a case that proves an edit to it is still booked, and a memo appearing on a call's inputs fails the test that licenses the derived equality. --- AGENTS.md | 2 +- bin/mega-evme/tests/trace_declaration.rs | 125 +++++++ crates/mega-evm/src/evm/AGENTS.md | 5 + crates/mega-evm/src/evm/inspector.rs | 157 +++++++- crates/mega-evm/tests/rex7/gas_surface.rs | 137 ++++++- crates/mega-evm/tests/rex7/main.rs | 4 + .../tests/rex7/shim_input_comparison.rs | 340 ++++++++++++++++++ 7 files changed, 760 insertions(+), 10 deletions(-) create mode 100644 bin/mega-evme/tests/trace_declaration.rs create mode 100644 crates/mega-evm/tests/rex7/shim_input_comparison.rs diff --git a/AGENTS.md b/AGENTS.md index 779c4f70..1179900c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,7 +131,7 @@ MegaETH separates EVM gas into two independent dimensions tracked during executi Gas an inspector writes in was never debited from the transaction's envelope, so without the term the derivation reads such a transaction as having spent less than it did and can go negative; the term is zero for every uninspected transaction and every observation-only inspector. The same booking site shifts the checkpoint baseline and re-derives the gas clamp, so an inspector's edit never enters the compute measurement and never buys compute headroom. An edit to a frame _result_'s gas is booked instead from `finalize_frame`, because whether it moves the envelope at all depends on how the frame ends: a returning or reverting frame's remainder goes back to its caller and the edit is booked, a halting one's does not and the destroyed remainder is taken on the EVM's own number. - The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited anywhere but their gas limit, a frame the inspector answered itself with a synthetic outcome, and every constant-time reading it can take off a live interpreter — because a rewrite that costs nothing still produces different state and a different receipt. + The shim also counts, on the same ledger, the rewrites it sees that move no gas at all — a frame result's classification or output coming back changed, a finished outcome's metadata (a call's `memory_offset`, a creation's `address`) rewritten around the result inside it, a frame's inputs edited on any of the fields that say what the frame will do (which is all of them but the gas limit, and but the two `OnceCell` memos a creation's inputs carry — filling one is a derived value being computed, which is what a tracer asking where a deployment landed does), a frame the inspector answered itself with a synthetic outcome, and every constant-time reading it can take off a live interpreter — because a rewrite that costs nothing still produces different state and a different receipt. That last group is stated as a rule rather than as a list: every `O(1)` reading of the interpreter's working set enters the boundary snapshot, which is what makes a program counter stepped past an instruction, a memory grown together with its memo, or a return buffer conjured in front of a frame that made no call all visible on the same lane. Every gas lane carries a gross alongside its net, and it is the gross that `is_zero` — the guard's question — reads: two edits to one lane that cancel are two edits, whether they cancel inside one frame or across a surviving frame and a rolled-back one, and a net-only reading calls that pair untouched while the execution saw a number the EVM would never have produced. The interpreter's pending action is measured on the same ledger: a frame holds its gas counter, plus a pending `NewFrame` action's `gas_limit`, or — once a terminating instruction has run — only the `Return` action's own copy, so the shim reads both objects at every live-interpreter callback and books the difference to the lane the action it was left holding names (the result lane for a `Return` action, settled at the frame's settlement point on the final classification; the envelope lane for a `NewFrame` one; the counter lane when the callback removed the action). diff --git a/bin/mega-evme/tests/trace_declaration.rs b/bin/mega-evme/tests/trace_declaration.rs new file mode 100644 index 00000000..7b40cd91 --- /dev/null +++ b/bin/mega-evme/tests/trace_declaration.rs @@ -0,0 +1,125 @@ +//! The tracer `replay --trace` declares read-only has to survive a contract creation. +//! +//! `replay` is the one command that hands the EVM a declared observer, because it is the one whose +//! transaction the canonical block path would admit. A declaration is checked in debug builds: the +//! shim measures the tracer anyway and asserts it booked nothing, so a tracer that writes anything +//! back panics at the callback that did it. +//! +//! `TracingInspector` asks each creation for the address it will occupy, which fills a memo on the +//! inputs, and the shim used to read a filled memo as a rewritten input — so this panicked at the +//! first `CREATE` in a replayed transaction. The offline replay fixtures deploy nothing, so the +//! shape reached no gate at all. This is that gate, over the object the command actually builds: +//! an RPC capture of a deploying transaction would be the other way to write it, and the tracer's +//! declaration is what both would be checking. + +use clap::Parser; +use mega_evm::{ + revm::{ + bytecode::opcode::{CREATE, CREATE2, MSTORE, POP, STOP}, + context::tx::TxEnv, + primitives::{address, Address, Bytes, TxKind, U256}, + }, + test_utils::{BytecodeBuilder, MemoryDatabase}, + MegaContext, MegaEvm, MegaSpecId, MegaTransaction, MegaTransactionNew, +}; +use mega_evme::TraceArgs; + +/// The account the transaction is sent from. +const CALLER: Address = address!("0000000000000000000000000000000000300000"); + +/// The account holding the deploying code. +const CONTRACT: Address = address!("0000000000000000000000000000000000300001"); + +/// `PUSH1 0 PUSH1 0 RETURN` — init code that deploys an empty contract. +const RETURN_EMPTY: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xf3]; + +/// Writes `code` into memory from offset zero, one 32-byte word at a time. +fn write_to_memory(builder: BytecodeBuilder, code: &[u8]) -> BytecodeBuilder { + let mut builder = builder; + for (index, chunk) in code.chunks(32).enumerate() { + let mut word = [0u8; 32]; + word[..chunk.len()].copy_from_slice(chunk); + builder = builder.push_bytes(word).push_number((index * 32) as u64).append(MSTORE); + } + builder +} + +/// Init code that creates a contract of its own before returning. +fn nested_init_code() -> Vec { + write_to_memory(BytecodeBuilder::default(), &RETURN_EMPTY) + .push_number(RETURN_EMPTY.len() as u64) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .append(POP) + .append_many(RETURN_EMPTY) + .build_vec() +} + +/// A `CREATE` and a `CREATE2` of init code that creates once more: four creations, both schemes, +/// two depths. +fn deploying_code() -> Bytes { + let init = nested_init_code(); + let size = init.len() as u64; + write_to_memory(BytecodeBuilder::default(), &init) + .push_number(size) + .push_number(0u64) + .push_number(0u64) + .append(CREATE) + .append(POP) + .push_number(0x5A17u64) + .push_number(size) + .push_number(0u64) + .push_number(0u64) + .append(CREATE2) + .append(POP) + .append(STOP) + .build() +} + +/// ★ The declared tracer runs a deploying transaction without writing anything back. +/// +/// In a debug build — which is how this suite runs — the assertion inside the shim is what fails +/// if the declaration stops holding, and it names the callback. In a release build the run simply +/// has to succeed. +#[test] +fn test_the_declared_tracer_survives_a_transaction_that_deploys() { + let mut db = MemoryDatabase::default() + .account_code(CONTRACT, deploying_code()) + .account_balance(CALLER, U256::from(1u64) << 64); + + let mut context = MegaContext::new(&mut db, MegaSpecId::REX7); + context.modify_chain(|chain| { + chain.operator_fee_scalar = Some(U256::ZERO); + chain.operator_fee_constant = Some(U256::ZERO); + }); + + let mut inspector = TraceArgs::parse_from(["mega-evme", "--trace"]).create_trusted_inspector(); + let mut evm = MegaEvm::new(context).with_trusted_inspector(&mut inspector); + + let mut tx = MegaTransaction::new(TxEnv { + caller: CALLER, + kind: TxKind::Call(CONTRACT), + gas_limit: 10_000_000, + gas_price: 0, + ..Default::default() + }); + tx.enveloped_tx = Some(Bytes::new()); + + let outcome = evm.execute_transaction(tx).expect("the replayed transaction must execute"); + assert!( + outcome.result_and_state.result.is_success(), + "the fixture must deploy, got {:?}", + outcome.result_and_state.result, + ); + assert_eq!( + outcome.result_and_state.state.values().filter(|account| account.is_created()).count(), + 4, + "the fixture must create four contracts, or it is not exercising the shape", + ); + assert!( + outcome.inspector_ledger.is_zero(), + "the tracer the command declares read-only must book nothing: {:?}", + outcome.inspector_ledger, + ); +} diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index 1093054e..d688628a 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -188,6 +188,7 @@ The first six rows are the lanes measured across a callback boundary; the three | Every `Gas` above → `state_gas_spent` | every callback that holds one | `InspectorLedger::state_gas`, settled at the same point. Not a term of the law: it moves the receipt's state-gas figure, not the envelope. Its *other* effect — a failing frame folds it into its caller's pool — arrives inside the reservoir lane, which is read after the fold. | | Every `Gas` above → `memory` (`MemoryGas`: `words_num`, `expansion_cost`) | every callback that holds one | Not a budget but a memo of how far the frame's memory has been paid for — and the number the next expanding opcode compares its requirement against, so moving it *together with the memory* skips that opcode's charge while leaving every interpreter invariant intact. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | | `CallInputs` / `CreateInputs` semantic fields, including `charged_new_account_state_gas`; `InterpreterResult::result` and `::output`; `CallOutcome::memory_offset` / `::was_precompile_called` / `::precompile_call_logs` / `::charged_new_account_state_gas`; `CreateOutcome::address` | `frame_start`, `call`, `create`, `frame_end`, `call_end`, `create_end` | Not gas. Booked on `InspectorLedger::interventions` by the rewrite comparison. | +| `CreateInputs` → `cached_address` / `cached_init_code_hash` | `frame_start`, `create` | Not gas, and not compared. Two `OnceCell` memos of the semantic fields above, filled on demand through a shared reference, so a callback that asks a creation where it will land returns the object structurally changed having edited nothing — which is what `created_address` is for and what every tracer that records a deployment does. `CREATE_INPUTS_COMPARISON` in `tests/rex7/gas_surface.rs` is where the exclusion is written down. What the exclusion costs: filling the address memo with a nonce other than the caller's redirects the creation, because `make_create_frame` reads the memo, and telling that apart needs the pre-bump nonce and — under `CREATE2` — the keccak the memo exists to avoid. That one is content-class, like the interpreter's stack and memory contents, and rests on the declaration. | | Every constant-time reading of `Interpreter`'s own fields — `bytecode` (program counter, code identity, `continue_execution`), `stack` (length), `return_data` (buffer identity), `memory` (size, window offset), `gas` → `memory` (the memo), `input` (target, code address, caller, value, calldata identity), `runtime_flag` (static flag, spec id) | the four live-interpreter callbacks | Not gas, and the whole of what a boundary can read off a live interpreter in constant time. Booked on `InspectorLedger::interventions`, off `WorkingSet`. | | `Interpreter::extend` | the four live-interpreter callbacks | Not gas, and not readable: `InterpreterTypes::Extend` carries no trait bound, so a shim generic over the interpreter has nothing it can call on it. `MegaETH` configures it as `()`. | | The **contents** of the interpreter's `stack`, `memory`, `return_data` buffer, calldata and code, at unchanged identities | the four live-interpreter callbacks | Not gas. The EVM executes on whatever it finds and meters that as its own work, because it is. | @@ -219,6 +220,10 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **On a revm bump, re-read the trait's method list against `tests/rex7/gas_surface.rs`'s `CALLBACKS`, and give any new callback a row in the shape table and a column in the cheat matrix.** This is the one direction no pin reaches, and it is the direction that adds reach. The field-level and variant-level pins in the same file cover everything else, and both fail loudly on their own. +- **Compare a frame's inputs on the fields that say what the frame does, and classify every new one.** + A call's inputs are compared by the derived equality with the gas limit normalised out, which picks up a field upstream adds by itself; a creation's are compared field by field, because two of theirs are memos an observation-only tracer fills. + The trade is that the second list is one somebody has to keep complete, and `tests/rex7/gas_surface.rs::CREATE_INPUTS_COMPARISON` is where it is closed: every field of both structs is classified semantic, envelope or memo against upstream's own `Debug` rendering, a semantic one needs a case in `tests/rex7/shim_input_comparison.rs` that proves an edit to it is booked, and a memo appearing on a *call's* inputs fails the test that licenses the derived equality. + What a comparison must never do is read a derived value being computed as an input being changed: that is the shape that made every `revm-inspectors` tracer report an intervention at every `CREATE`, and made a declared one fail the debug verification at the first contract a transaction deployed. - **Book a result rewrite from the frame's settlement point, not from the callback boundary.** Whether such an edit moves the transaction's envelope depends on how the frame ends: a returning or reverting frame's remaining gas goes back to its caller, a halting one's does not. The gas an intercepting callback puts into a synthetic outcome travels through that same lane. diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index d77befe4..984e5487 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -835,6 +835,12 @@ fn result_rewritten(before: (InstructionResult, &Bytes), after: &InterpreterResu /// /// Everything else a call input carries — who is called, with what value, under which scheme, with /// what calldata, in a static context or not — describes what the frame will do. +/// +/// Taken as the derived equality rather than field by field, which a call's inputs can afford +/// because every field of `CallInputs` is one of those descriptions: there is no memo among them, +/// so a field upstream adds joins the comparison by itself. That is a claim about upstream's +/// struct and it is pinned as one, in `tests/rex7/gas_surface.rs`, which classifies every field of +/// both input types as semantic or memo and fails if a call's inputs ever grow one of the latter. #[inline] fn call_inputs_rewritten(mut before: CallInputs, after: &CallInputs) -> bool { before.gas_limit = after.gas_limit; @@ -842,10 +848,34 @@ fn call_inputs_rewritten(mut before: CallInputs, after: &CallInputs) -> bool { } /// Whether a callback edited a creation's inputs anywhere but in their gas limit. +/// +/// Compared field by field, which the derived equality cannot stand in for here: `CreateInputs` +/// carries two `OnceCell` memos, of the address the creation will occupy and of the init code's +/// hash, and both are filled on demand through a shared reference. Filling one is a derived value +/// being computed, not an input being changed — the frame the EVM builds afterwards is built from +/// the same six numbers either way — and it is what `created_address` does, which every tracer +/// that records a deployment calls. Comparing them would report the most ordinary thing an +/// observation-only tracer does as a rewrite. +/// +/// What is compared is the whole of what the frame is built from: who creates, under which scheme, +/// with what value, from what init code, and out of what state-gas pool. The gas limit is left out +/// for the reason a call's is — it travels on the envelope lane, and comparing it here would +/// report one edit twice. +/// +/// What the exclusion costs is one shape, and it is content-class rather than free: the address +/// memo is derived from a nonce its caller supplies, and revm reads the memo when it builds the +/// frame, so filling it with a nonce other than the one the EVM would have used redirects the +/// deployment. Telling that apart needs the caller's pre-bump nonce and, under `CREATE2`, the +/// keccak of the init code that the memo exists to avoid computing — neither of which a boundary +/// crossed twice per creation can take. It joins the readings a boundary cannot make at all, and +/// rests on the declaration a block's admission rests on. #[inline] -fn create_inputs_rewritten(mut before: CreateInputs, after: &CreateInputs) -> bool { - before.set_gas_limit(after.gas_limit()); - before != *after +fn create_inputs_rewritten(before: &CreateInputs, after: &CreateInputs) -> bool { + before.caller() != after.caller() || + before.scheme() != after.scheme() || + before.value() != after.value() || + before.init_code() != after.init_code() || + before.reservoir() != after.reservoir() } /// [`call_inputs_rewritten`] / [`create_inputs_rewritten`] for the generic callback, which is @@ -858,7 +888,7 @@ fn frame_input_rewritten(before: FrameInput, after: &FrameInput) -> bool { call_inputs_rewritten(*before, after) } (FrameInput::Create(before), FrameInput::Create(after)) => { - create_inputs_rewritten(*before, after) + create_inputs_rewritten(&before, after) } (FrameInput::Empty, FrameInput::Empty) => false, _ => true, @@ -1428,7 +1458,7 @@ where Some(before.gas_limit()), Some(inputs.gas_limit()), outcome.as_ref().map(|outcome| outcome.result.gas.refunded()), - create_inputs_rewritten(before, inputs), + create_inputs_rewritten(&before, inputs), ); verify_trusted(self.trusted, context, "create"); outcome @@ -1474,7 +1504,7 @@ mod tests { bytecode::Bytecode, interpreter::{ interpreter::{EthInterpreter, ExtBytecode}, - InputsImpl, InterpreterAction, SharedMemory, + CreateScheme, InputsImpl, InterpreterAction, SharedMemory, }, }; @@ -1734,4 +1764,119 @@ mod tests { all.sort_unstable(); assert_eq!(all, names, "every reading the snapshot holds must have a case, and vice versa"); } + + /// One case: the name of a field a creation's inputs are built from, and a rewrite that moves + /// it. + type CreateCase = (&'static str, fn(&mut CreateInputs)); + + /// The creation every case below rewrites one field of. + fn create_inputs() -> CreateInputs { + CreateInputs::new( + Address::ZERO, + CreateScheme::Create, + U256::ZERO, + Bytes::from_static(&[0x60, 0x00]), + 1_000_000, + 0, + ) + } + + /// One rewrite per field the comparison is written over, each moving the field it is named + /// for. + /// + /// Every one is something an inspector can do to a creation through the setters upstream + /// gives it, and each changes what the frame does: who is recorded as the creator, which + /// address the contract lands at, what it is funded with, what code runs, and what state-gas + /// pool it draws from. + const CREATE_CASES: [CreateCase; 5] = [ + ("caller", |inputs| inputs.set_call(OTHER)), + ("scheme", |inputs| { + inputs.set_scheme(CreateScheme::Create2 { salt: U256::from(0x5A17) }); + }), + ("value", |inputs| inputs.set_value(U256::from(1))), + ("init_code", |inputs| inputs.set_init_code(Bytes::from_static(&[0x00]))), + ("reservoir", |inputs| inputs.set_reservoir(1)), + ]; + + /// ★ Every field a creation's frame is built from is one an edit to is booked. + /// + /// `CreateInputs` is compared field by field rather than by the derived equality a call's + /// inputs use, which trades a comparison that grows by itself for one someone has to keep + /// complete — so each field gets a case that moves it and asserts the shim sees it move. The + /// other half of the trade, that the list is still upstream's whole field set, is not a + /// question this module can ask; `tests/rex7/gas_surface.rs` asks it against the struct's own + /// `Debug` rendering. + #[test] + fn test_every_semantic_field_of_a_creation_is_compared() { + assert!( + !create_inputs_rewritten(&create_inputs(), &create_inputs()), + "two identical creations must not read as a rewrite", + ); + + let mut names: Vec<&str> = CREATE_CASES.iter().map(|(name, _)| *name).collect(); + let declared = names.len(); + names.sort_unstable(); + names.dedup(); + assert_eq!(names.len(), declared, "no field may be listed twice"); + + for (name, rewrite) in CREATE_CASES { + let before = create_inputs(); + let mut after = create_inputs(); + rewrite(&mut after); + assert!( + create_inputs_rewritten(&before, &after), + "a rewritten {name} must be visible to the shim", + ); + } + } + + /// ★ Filling a creation's memo cells is not a rewrite, in either direction. + /// + /// This is the whole reason the comparison is written out: `created_address` and + /// `init_code_hash` fill an `OnceCell` through a shared reference, so the object a callback + /// was handed comes back structurally different having had a derived value computed off it. + /// Every tracer that records a deployment calls the first of those, so the derived equality + /// booked an intervention for the most ordinary thing an observation-only inspector does. + /// + /// The other direction is the setters', which clear the cells: an inspector that writes back + /// the init code a creation already had has changed nothing and emptied both memos. + #[test] + fn test_filling_or_clearing_a_creations_memo_is_not_a_rewrite() { + let before = create_inputs(); + let after = create_inputs(); + after.created_address(0); + after.init_code_hash(); + assert!( + !create_inputs_rewritten(&before, &after), + "computing the created address and the init code hash changes no input", + ); + assert!( + !create_inputs_rewritten(&after, &before), + "and neither does a setter clearing the memos back down", + ); + + let mut moved = create_inputs(); + moved.set_value(U256::from(1)); + moved.created_address(0); + assert!( + create_inputs_rewritten(&before, &moved), + "a memo filled beside a real edit must not hide the edit", + ); + } + + /// ★ A creation's gas limit is not part of the comparison. + /// + /// It travels on the envelope lane, which books the amount rather than the fact, so counting + /// it here would report one edit twice. A call's inputs are excluded from their own comparison + /// the same way. + #[test] + fn test_a_creations_gas_limit_is_left_to_the_envelope_lane() { + let before = create_inputs(); + let mut after = create_inputs(); + after.set_gas_limit(before.gas_limit() + 1); + assert!( + !create_inputs_rewritten(&before, &after), + "the gas limit is booked as an amount, not as an intervention", + ); + } } diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index 11649383..d7e5b9b5 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -216,10 +216,88 @@ const CREATE_INPUTS_FIELDS: [(&str, Coverage); 8] = [ ("scheme", Coverage::NotGas("what the frame does")), ("value", Coverage::NotGas("what the frame does")), ("init_code", Coverage::NotGas("what the frame does")), - ("cached_address", Coverage::NotGas("a memo of the init code and scheme above")), - ("cached_init_code_hash", Coverage::NotGas("a memo of the init code above")), + ( + "cached_address", + Coverage::NotGas( + "a memo of the caller, the scheme and the init code above, filled on demand through a \ + shared reference — so it is left out of the rewrite comparison, which is what \ + `CREATE_INPUTS_COMPARISON` records", + ), + ), + ( + "cached_init_code_hash", + Coverage::NotGas("a memo of the init code above, left out for the same reason"), + ), +]; + +// --- what the rewrite comparison is written over ------------------------------------------------- + +/// How one field of a frame's inputs enters the rewrite comparison the shim makes at +/// `frame_start`, `call` and `create`. +/// +/// The field set is upstream's and is pinned the same way every table here is, against the +/// struct's own `Debug` rendering. What this table adds is the split the comparison is written +/// over: a field upstream adds is a field with no verdict until someone decides which of the three +/// it is, and the decision has teeth in both directions — a `Semantic` row must have a case in +/// `shim_input_comparison.rs` that proves the shim books an edit to it, and a `Memo` row appearing +/// on a *call's* inputs breaks the derived equality those are compared by. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Comparison { + /// Compared. It says what the frame will do, so an edit to it is what "the inspector rewrote + /// this frame" means, and it is booked on the interventions lane. + Semantic, + /// Left out because it is the frame's budget. It is booked on the env lane as an amount, and + /// comparing it here as well would report one edit twice. + Envelope, + /// Left out because it is a memo of the semantic fields, filled on demand through a shared + /// reference. Filling one is a derived value being computed rather than an input being + /// changed — `created_address` is the case, and every tracer that records a deployment calls + /// it — and the frame the EVM builds afterwards is built from the same numbers either way. + Memo, +} + +/// A call's inputs, by how each field enters the comparison. +/// +/// No `Memo` row, which is exactly what licenses `call_inputs_rewritten` to be the derived +/// equality with the gas limit normalised out: every field of `CallInputs` says what the frame +/// does, so one upstream adds joins the comparison by itself. +pub(crate) const CALL_INPUTS_COMPARISON: [(&str, Comparison); 12] = [ + ("gas_limit", Comparison::Envelope), + ("input", Comparison::Semantic), + ("return_memory_offset", Comparison::Semantic), + ("reservoir", Comparison::Semantic), + ("bytecode_address", Comparison::Semantic), + ("known_bytecode", Comparison::Semantic), + ("target_address", Comparison::Semantic), + ("caller", Comparison::Semantic), + ("value", Comparison::Semantic), + ("scheme", Comparison::Semantic), + ("is_static", Comparison::Semantic), + ("charged_new_account_state_gas", Comparison::Semantic), +]; + +/// A creation's inputs, by how each field enters the comparison. +/// +/// The two `Memo` rows are why `create_inputs_rewritten` is written out field by field instead: +/// the derived equality reads a filled memo as a changed input, so an observation-only tracer that +/// asked a creation for its address booked an intervention and, under a `TrustedObserver` +/// declaration, failed the debug verification at the first `CREATE` it saw. +pub(crate) const CREATE_INPUTS_COMPARISON: [(&str, Comparison); 8] = [ + ("gas_limit", Comparison::Envelope), + ("caller", Comparison::Semantic), + ("scheme", Comparison::Semantic), + ("value", Comparison::Semantic), + ("init_code", Comparison::Semantic), + ("reservoir", Comparison::Semantic), + ("cached_address", Comparison::Memo), + ("cached_init_code_hash", Comparison::Memo), ]; +/// The fields of a table an edit to must be booked as an intervention, in the table's own order. +pub(crate) fn semantic_fields(table: &[(&'static str, Comparison)]) -> Vec<&'static str> { + table.iter().filter(|(_, how)| *how == Comparison::Semantic).map(|(name, _)| *name).collect() +} + /// Everything a finished call hands back besides the result inside it. const CALL_OUTCOME_FIELDS: [(&str, Coverage); 5] = [ ("result", Coverage::NotGas("a container; its own fields are classified separately")), @@ -402,8 +480,18 @@ fn field_names(rendered: &str) -> BTreeSet { /// the table keeps for a field upstream removed is a verdict about nothing, and stale prose about /// a field that no longer exists is how a table stops being evidence. fn assert_classified(what: &str, rendered: &str, table: &[(&str, Coverage)]) { + assert_every_field_named(what, rendered, table.iter().map(|(name, _)| *name)); +} + +/// [`assert_classified`] over the comparison tables, whose verdict type is a different enum. +fn assert_compared(what: &str, rendered: &str, table: &[(&str, Comparison)]) { + assert_every_field_named(what, rendered, table.iter().map(|(name, _)| *name)); +} + +/// The body both of those share: the rendered field names are exactly the classified ones. +fn assert_every_field_named<'a>(what: &str, rendered: &str, names: impl Iterator) { let seen = field_names(rendered); - let classified: BTreeSet = table.iter().map(|(name, _)| String::from(*name)).collect(); + let classified: BTreeSet = names.map(String::from).collect(); let unclassified: Vec<&String> = seen.difference(&classified).collect(); let vanished: Vec<&String> = classified.difference(&seen).collect(); assert!( @@ -502,6 +590,49 @@ fn test_every_field_of_every_gas_carrier_has_a_verdict() { ); } +/// ★ Every field of a frame's inputs has a verdict on how it is compared, too. +/// +/// The same closure as the table above, over the same field sets, asking the other question: not +/// "does an edit to this reach the receipt through a lane" but "is an edit to this what the shim +/// calls a rewrite". A field upstream adds needs both answers, and the second is the one that was +/// missing when `CreateInputs` grew its memo cells — they were classified as not-gas, correctly, +/// while the comparison went on reading a filled memo as a changed input. +#[test] +fn test_every_field_of_a_frames_inputs_has_a_comparison_verdict() { + assert_compared( + "CallInputs", + &std::format!("{:?}", sample_call_inputs()), + &CALL_INPUTS_COMPARISON, + ); + assert_compared( + "CreateInputs", + &std::format!("{:?}", sample_create_inputs()), + &CREATE_INPUTS_COMPARISON, + ); +} + +/// ★ A call's inputs carry no memo, which is what the derived equality rests on. +/// +/// `call_inputs_rewritten` compares the whole struct with the gas limit normalised out, so it +/// picks up a field upstream adds without anyone noticing — which is the right trade only as long +/// as every field says what the frame does. A memo added to `CallInputs` would have to be +/// classified here, and this is what turns that classification into a failure rather than a row +/// nobody reads: the comparison has to be narrowed the way a creation's was. +#[test] +fn test_a_calls_inputs_carry_no_memo_field() { + let memos: Vec<&str> = CALL_INPUTS_COMPARISON + .iter() + .filter(|(_, how)| *how == Comparison::Memo) + .map(|(name, _)| *name) + .collect(); + assert_eq!( + memos, + Vec::<&str>::new(), + "a call's inputs are compared by the derived equality, which reads a filled memo as a \ + changed input; narrow `call_inputs_rewritten` to the semantic fields first", + ); +} + /// The parser the pin rests on reads what it is supposed to read. /// /// Without this, a change to `Debug`'s formatting that made the parser return nothing would turn diff --git a/crates/mega-evm/tests/rex7/main.rs b/crates/mega-evm/tests/rex7/main.rs index 4f65fc33..e51ef9a2 100644 --- a/crates/mega-evm/tests/rex7/main.rs +++ b/crates/mega-evm/tests/rex7/main.rs @@ -30,6 +30,9 @@ //! grown for free, an outcome's metadata rewritten around the result inside it, two edits to one //! signed lane that cancel, an instruction deleted by stepping the program counter past it, and a //! return buffer conjured in front of a frame that made no call. +//! - `shim_input_comparison` — what the entry callbacks call a rewrite of a frame's inputs: every +//! field a creation is built from is compared, and the two `OnceCell` memos a tracer fills by +//! asking where a deployment landed are not. //! - `shim_refusals` — the rewrites the shim refuses outright: a failed creation revived (at both //! callbacks that can), and the classification of a result frame init produced; with the near //! boundary, a frame the inspector answered itself, which is supported. @@ -120,6 +123,7 @@ mod pre_execution_intrinsic_reject; mod precompile_halt; mod result_space_tripwire; mod shim_blind_spots; +mod shim_input_comparison; mod shim_lanes; mod shim_refusals; mod shim_settlement; diff --git a/crates/mega-evm/tests/rex7/shim_input_comparison.rs b/crates/mega-evm/tests/rex7/shim_input_comparison.rs new file mode 100644 index 00000000..f081049c --- /dev/null +++ b/crates/mega-evm/tests/rex7/shim_input_comparison.rs @@ -0,0 +1,340 @@ +//! What the shim calls a rewrite of a frame's inputs, and what it does not. +//! +//! The comparison the entry callbacks make has to answer one question about an object upstream +//! owns: did this come back describing a different frame? A creation's inputs make that harder +//! than a call's, because two of their fields are `OnceCell` memos — the address the creation will +//! occupy and the hash of its init code — filled on demand through a *shared* reference. So the +//! object a callback was handed comes back structurally different having had a derived value +//! computed off it, and the derived equality read that as an edit. +//! +//! It is not an exotic shape. `created_address` is what a tracer calls to record where a +//! deployment landed, so every `revm-inspectors` tracer did it at every `CREATE`: an undeclared one +//! reported an intervention it never made, and a declared one failed the debug verification at the +//! first creation in the transaction. The fixture here is the one no test had — a transaction that +//! actually creates something. +//! +//! The other half is the cost of narrowing a comparison: a field left out is a field an edit to is +//! invisible. So every field a creation's frame is built from gets a case that edits it and +//! asserts the shim still books it, and the case list is checked against the same table +//! `gas_surface.rs` pins upstream's field set with. + +use crate::{ + common::{base_db, transact_inspected, Outcome}, + gas_surface::{semantic_fields, Comparison, CREATE_INPUTS_COMPARISON}, + inspector_common::{ledger_intervention, limits, transact_trusted}, +}; +use alloy_primitives::{Address, Bytes, U256}; +use mega_evm::{ + test_utils::{BytecodeBuilder, MemoryDatabase}, + DeclaredObserver, MegaSpecId, +}; +use revm::{ + bytecode::opcode::{CREATE, CREATE2, MSTORE, POP, STOP}, + context::CreateScheme, + interpreter::{CreateInputs, CreateOutcome, InterpreterTypes}, + Inspector, +}; +use revm_inspectors::tracing::{TracingInspector, TracingInspectorConfig}; +use std::{vec, vec::Vec}; + +// --- the fixture --------------------------------------------------------------------------- + +/// A second address, for the case that moves the creation's caller. +const OTHER: Address = Address::repeat_byte(0x0C); + +/// The salt the fixture's `CREATE2` uses, and the one the scheme-swapping case supplies. +const SALT: u64 = 0x5A17; + +/// `PUSH1 0 PUSH1 0 RETURN` — init code that deploys an empty contract. +const RETURN_EMPTY: [u8; 5] = [0x60, 0x00, 0x60, 0x00, 0xf3]; + +/// Writes `code` into memory from offset zero, one 32-byte word at a time. +/// +/// The tail word is zero-padded, which the `CREATE` that follows never reads: it is given the +/// code's true length. +fn write_to_memory(builder: BytecodeBuilder, code: &[u8]) -> BytecodeBuilder { + let mut builder = builder; + for (index, chunk) in code.chunks(32).enumerate() { + let mut word = [0u8; 32]; + word[..chunk.len()].copy_from_slice(chunk); + builder = builder.push_bytes(word).push_number((index * 32) as u64).append(MSTORE); + } + builder +} + +/// Init code that itself creates a contract before returning, so the fixture has a creation +/// nested inside a creation. +fn nested_init_code() -> Vec { + write_to_memory(BytecodeBuilder::default(), &RETURN_EMPTY) + .push_number(RETURN_EMPTY.len() as u64) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .append_many(RETURN_EMPTY) + .build_vec() +} + +/// The fixture: a `CREATE` and a `CREATE2` of init code that creates once more. +/// +/// Four `create` callbacks, over both schemes and both frame depths. `CREATE2` is not decoration: +/// its address does not depend on the caller's nonce, which is what makes filling its memo +/// something a test can do without changing where the contract lands. +fn creating_code() -> Bytes { + let init = nested_init_code(); + let size = init.len() as u64; + write_to_memory(BytecodeBuilder::default(), &init) + .push_number(size) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE) + .append(POP) + .push_number(SALT) // salt + .push_number(size) // size + .push_number(0u64) // offset + .push_number(0u64) // value + .append(CREATE2) + .append(POP) + .append(STOP) + .build() +} + +fn creating_db() -> MemoryDatabase { + base_db(creating_code()) +} + +/// Asserts the run really exercised the shape the module is about. +/// +/// Counted off the produced state rather than inside an inspector, so that a fixture that stops +/// creating anything fails the tests that rest on it rather than passing them vacuously. +fn assert_created_four(label: &str, outcome: &Outcome) { + assert!( + outcome.is_success(), + "{label}: the fixture must run to completion, got {:?}", + outcome.result, + ); + assert_eq!( + outcome.state.values().filter(|account| account.is_created()).count(), + 4, + "{label}: the fixture must create four contracts", + ); +} + +// --- the inspectors ------------------------------------------------------------------------ + +/// Fills a creation's memo cells and changes nothing else. +/// +/// The address is asked for only under `CREATE2`, where it is derived from the caller, the salt +/// and the init code and the nonce argument is ignored — so this fills the same cell the EVM +/// would have filled, with the same value. Under `CREATE` the address depends on the caller's +/// nonce, which an inspector has to look up to get right; a fill with the wrong one is a rewrite +/// the boundary cannot price, and is left to the declaration the way every other value the +/// boundary cannot read back is. +#[derive(Default)] +struct FillsTheMemo { + fills: u32, +} + +impl Inspector for FillsTheMemo { + fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option { + inputs.init_code_hash(); + if matches!(inputs.scheme(), CreateScheme::Create2 { .. }) { + inputs.created_address(0); + } + self.fills += 1; + None + } +} + +/// Edits one field of the first creation it is handed, and nothing else ever. +/// +/// One struct rather than one per field so that what differs between the cases is the edit alone. +struct EditsOneField { + edit: fn(&mut CreateInputs), + fired: bool, +} + +impl EditsOneField { + fn new(edit: fn(&mut CreateInputs)) -> Self { + Self { edit, fired: false } + } +} + +impl Inspector for EditsOneField { + fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option { + if !self.fired { + self.fired = true; + (self.edit)(inputs); + } + None + } +} + +/// One case: the field a rewrite moves, and the rewrite. +type Case = (&'static str, fn(&mut CreateInputs)); + +/// One rewrite per field a creation's frame is built from, each moving what it is named for. +/// +/// Every one changes what the frame does: who is recorded as the creator, which address the +/// contract lands at, what it is funded with, what code runs, and what state-gas pool it draws +/// from. The gas limit is deliberately absent — it is booked as an amount on the envelope lane, +/// and [`test_an_edited_gas_limit_is_booked_as_an_amount`] is its case. +const CASES: [Case; 5] = [ + ("caller", |inputs| inputs.set_call(OTHER)), + ("scheme", |inputs| inputs.set_scheme(CreateScheme::Create2 { salt: U256::from(SALT) })), + ("value", |inputs| inputs.set_value(U256::from(1))), + ("init_code", |inputs| inputs.set_init_code(Bytes::from_static(&RETURN_EMPTY))), + ("reservoir", |inputs| inputs.set_reservoir(1)), +]; + +// --- an observation-only tracer books nothing ------------------------------------------------ + +/// ★ A declared tracer runs a transaction that creates contracts without booking anything. +/// +/// The shape the narrowed comparison exists for, at the callback it broke at. A declared observer +/// is measured anyway in a debug build and asserted to have booked nothing, so before the fix this +/// panicked at the first `CREATE` — `mega-evme replay --trace` over any transaction that deploys +/// something, and every offline fixture that had one, which is to say none of them. +#[test] +fn test_a_declared_tracer_books_nothing_over_a_transaction_that_creates() { + let mut tracer = DeclaredObserver(TracingInspector::new(TracingInspectorConfig::all())); + let outcome = transact_trusted(creating_db(), &mut tracer); + + assert_created_four("declared", &outcome); + assert!( + outcome.inspector_ledger.is_zero(), + "a tracer that only reads must leave every lane empty: {:?}", + outcome.inspector_ledger, + ); + assert_eq!( + outcome.inspector_ledger.interventions, 0, + "and asking a creation for the address it will occupy is not an intervention", + ); +} + +/// ★ And so does the same tracer with no declaration, on the measured path. +/// +/// The declared run above is measured too in a debug build, so on its own it says nothing about +/// the release path an embedder drives directly — which is the shape RPC tracing takes, and the +/// one whose outcome carried the false reading to whatever read it. +#[test] +fn test_an_undeclared_tracer_books_nothing_over_the_same_transaction() { + let mut tracer = TracingInspector::new(TracingInspectorConfig::all()); + let outcome = transact_inspected(MegaSpecId::REX7, creating_db(), limits(), &mut tracer); + + assert_created_four("undeclared", &outcome); + assert!( + outcome.inspector_ledger.is_zero(), + "the measured path must read the same: {:?}", + outcome.inspector_ledger, + ); +} + +/// ★ Filling both memo cells books nothing, and the fixture really fills them. +/// +/// The tracer tests above are the shape as it occurs; this is the mechanism on its own, so that a +/// future tracer that stops calling `created_address` does not quietly take the coverage with it. +#[test] +fn test_filling_a_creations_memo_cells_books_nothing() { + let mut filler = FillsTheMemo::default(); + let outcome = transact_inspected(MegaSpecId::REX7, creating_db(), limits(), &mut filler); + + assert_eq!(filler.fills, 4, "the fixture must hand the inspector four creations"); + assert_created_four("memo filler", &outcome); + assert!( + outcome.inspector_ledger.is_zero(), + "filling a memo is a derived value being computed, not an input being changed: {:?}", + outcome.inspector_ledger, + ); +} + +// --- and every real edit is still booked ------------------------------------------------------- + +/// ★ Every field a creation's frame is built from is one an edit to is booked. +/// +/// The bite of the narrowing. A comparison written field by field is one someone has to keep +/// complete, so each field gets a case that edits it in the `create` callback and asserts exactly +/// one intervention comes back — a field dropped from `create_inputs_rewritten` fails here by +/// name. +#[test] +fn test_every_semantic_field_of_a_creation_is_still_booked_when_edited() { + for (name, edit) in CASES { + let mut inspector = EditsOneField::new(edit); + let outcome = transact_inspected(MegaSpecId::REX7, creating_db(), limits(), &mut inspector); + assert!(inspector.fired, "{name}: the case must reach a creation"); + assert_eq!( + outcome.inspector_ledger.interventions, 1, + "{name}: an edited field must be booked exactly once, got {:?}", + outcome.inspector_ledger, + ); + } +} + +/// ★ The case list is the table's semantic field set. +/// +/// What closes the loop between the three places this is written down. `gas_surface.rs` pins the +/// field set against what upstream's `Debug` renders and classifies each field as semantic, +/// envelope or memo; the comparison in `inspector.rs` is written over the semantic ones; and this +/// is the list of edits that proves each of them is really compared. A field upstream adds has to +/// be classified, and a `Semantic` classification with no case fails here. +#[test] +fn test_the_case_list_is_the_tables_semantic_field_set() { + let mut cases: Vec<&str> = CASES.iter().map(|(name, _)| *name).collect(); + let declared = cases.len(); + cases.sort_unstable(); + cases.dedup(); + assert_eq!(cases.len(), declared, "no field may be listed twice"); + + let mut semantic = semantic_fields(&CREATE_INPUTS_COMPARISON); + semantic.sort_unstable(); + assert_eq!(cases, semantic, "every semantic field needs a case, and every case a field"); + + assert_eq!( + CREATE_INPUTS_COMPARISON + .iter() + .filter(|(_, how)| *how == Comparison::Memo) + .map(|(name, _)| *name) + .collect::>(), + vec!["cached_address", "cached_init_code_hash"], + "the two fields the comparison leaves out are the two memo cells", + ); +} + +/// ★ An edited gas limit is booked as an amount, not as an intervention. +/// +/// The other field the comparison leaves out, and the reason it is a different kind of exclusion +/// from the memos': it moves the frame's budget, so the envelope lane books how much. Counting it +/// here as well would report one edit twice, and a ledger reading zero on both would be the +/// failure that matters. +#[test] +fn test_an_edited_gas_limit_is_booked_as_an_amount() { + let mut inspector = EditsOneField::new(|inputs| inputs.set_gas_limit(inputs.gas_limit() - 1)); + let outcome = transact_inspected(MegaSpecId::REX7, creating_db(), limits(), &mut inspector); + + assert!(inspector.fired, "the case must reach a creation"); + assert_eq!( + outcome.inspector_ledger.interventions, 0, + "the gas limit is not part of the rewrite comparison", + ); + assert_eq!(outcome.inspector_ledger.env.net(), -1, "the envelope lane books the amount"); +} + +/// ★ A memo filled beside a real edit does not hide the edit. +/// +/// The two halves of the module in one run: the same callback asks the creation for its address +/// and moves its value, and what comes back is the one intervention the edit deserves. +#[test] +fn test_a_memo_filled_beside_an_edit_still_books_the_edit() { + let mut inspector = EditsOneField::new(|inputs| { + inputs.set_value(U256::from(1)); + inputs.init_code_hash(); + }); + let outcome = transact_inspected(MegaSpecId::REX7, creating_db(), limits(), &mut inspector); + + assert!(inspector.fired, "the case must reach a creation"); + assert_eq!( + outcome.inspector_ledger, + ledger_intervention(), + "the edit must be booked, once, and the memo must add nothing to it", + ); +} From d80f89088dd2b779333916c7c7b13a0474091d6a Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 00:45:20 +0800 Subject: [PATCH 202/208] fix(state-test): count file errors in the differential failure summary `DiffTally::is_failure` already fails a run on unexplained differences, panics, unreadable fixtures, and a corpus that judged nothing. The CLI summary only counted the first two, so a mixed corpus exited 1 while claiming `0 tests failed out of N`, and a skipped-only corpus claimed `0 tests failed out of 0`. Unexplained, panics, and file errors now share the `TestsFailed` count, with the stdout tally listing each bucket. A run that judged nothing uses `FixtureError` instead, matching fill and validate. Chaos mode gets the same mapping. EEST `--mode diff` reads `--diff-report` JSON, not this text; the report schema is unchanged. --- crates/state-test/src/main.rs | 139 ++++++++++++++++++++++++---- crates/state-test/tests/cli_exit.rs | 37 +++++++- 2 files changed, 159 insertions(+), 17 deletions(-) diff --git a/crates/state-test/src/main.rs b/crates/state-test/src/main.rs index fad535f8..ca14bca1 100644 --- a/crates/state-test/src/main.rs +++ b/crates/state-test/src/main.rs @@ -334,14 +334,7 @@ impl Cmd { if !tally.is_failure() { return Ok(()); } - Err(TestError { - name: "diff summary".to_string(), - path: String::new(), - kind: TestErrorKind::TestsFailed { - failed: tally.count(DiffClass::Unexplained) + tally.count(DiffClass::Panic), - total: tally.total(), - }, - }) + Err(diff_summary_error(&tally)) } /// Build the chaos run's shape filter from `--chaos-shapes`. @@ -395,14 +388,7 @@ impl Cmd { if !tally.is_failure() { return Ok(()); } - Err(TestError { - name: "chaos summary".to_string(), - path: String::new(), - kind: TestErrorKind::TestsFailed { - failed: tally.flagged.len().max(1), - total: tally.total(), - }, - }) + Err(chaos_summary_error(&tally)) } /// Benchmark every fixture under the given paths and print the results as JSON. @@ -520,6 +506,62 @@ fn parse_spec(flag: &str, value: &str) -> Result { Ok(spec) } +/// Why a sweep that judged nothing failed, as a [`TestErrorKind::FixtureError`] detail. +fn no_work_detail(unit: &str, file_errors: usize) -> String { + if file_errors == 0 { + format!("no {unit} was judged; the corpus is empty, entirely skipped, or unreachable") + } else { + format!( + "no {unit} was judged ({file_errors} fixtures unreadable); \ + the corpus is empty, entirely skipped, or unreachable" + ) + } +} + +/// Maps every [`DiffTally::is_failure`] condition onto the CLI's summary error. +/// +/// Unexplained differences, panics, and unreadable fixtures share [`TestErrorKind::TestsFailed`] +/// so the `failed` count includes all three. A run that judged nothing uses +/// [`TestErrorKind::FixtureError`] instead of claiming `0 tests failed out of 0`. +fn diff_summary_error(tally: &DiffTally) -> TestError { + let unexplained = tally.count(DiffClass::Unexplained); + let panics = tally.count(DiffClass::Panic); + let file_errors = tally.file_errors.len(); + let total = tally.total(); + TestError { + name: "diff summary".to_string(), + path: String::new(), + kind: if total == 0 { + TestErrorKind::FixtureError(no_work_detail("fixture", file_errors)) + } else { + TestErrorKind::TestsFailed { failed: unexplained + panics + file_errors, total } + }, + } +} + +/// Maps every [`ChaosSweepTally::is_failure`] condition onto the CLI's summary error. +/// +/// Flagged verdicts and unreadable fixtures share [`TestErrorKind::TestsFailed`]. A run that +/// judged nothing, or that applied no mutation, uses [`TestErrorKind::FixtureError`] so it cannot +/// read as `0 tests failed`. +fn chaos_summary_error(tally: &ChaosSweepTally) -> TestError { + let file_errors = tally.file_errors.len(); + let total = tally.total(); + TestError { + name: "chaos summary".to_string(), + path: String::new(), + kind: if total == 0 { + TestErrorKind::FixtureError(no_work_detail("vector", file_errors)) + } else if tally.flagged.is_empty() && file_errors == 0 { + TestErrorKind::FixtureError( + "no mutation was applied; the run tested nothing".to_string(), + ) + } else { + TestErrorKind::TestsFailed { failed: tally.flagged.len() + file_errors, total } + }, + } +} + /// Every class a differential run can produce, in report order. const DIFF_CLASSES: [DiffClass; 5] = [ DiffClass::Pass, @@ -535,6 +577,9 @@ fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { for class in DIFF_CLASSES { println!(" {:<12} {}", class.label(), tally.count(class)); } + if !tally.file_errors.is_empty() { + println!(" {:<12} {}", "FILE_ERROR", tally.file_errors.len()); + } if tally.skipped_files > 0 { println!(" ({} file(s) skipped by filename, no unit of them judged)", tally.skipped_files); } @@ -563,6 +608,15 @@ fn print_diff_tally(tally: &DiffTally, target: SpecName, base: SpecName) { for error in &tally.file_errors { println!("FILE_ERROR\t{}", error.replace('\n', " ")); } + if tally.is_failure() { + println!( + "Diff failure: {} unexplained, {} panics, {} fixtures unreadable ({} judged)", + tally.count(DiffClass::Unexplained), + tally.count(DiffClass::Panic), + tally.file_errors.len(), + tally.total(), + ); + } } /// Every chaos verdict, in the order a reader wants them. @@ -585,6 +639,9 @@ fn print_chaos_tally(tally: &ChaosSweepTally, spec: SpecName, seed: u64, filter: for class in CHAOS_CLASSES { println!(" {:<16} {}", class.label(), tally.count(class)); } + if !tally.file_errors.is_empty() { + println!(" {:<16} {}", "FILE_ERROR", tally.file_errors.len()); + } if tally.skipped_files > 0 { println!( " ({} file(s) skipped by filename, no vector of them judged)", @@ -612,6 +669,15 @@ fn print_chaos_tally(tally: &ChaosSweepTally, spec: SpecName, seed: u64, filter: for error in &tally.file_errors { println!("FILE_ERROR\t{}", error.replace('\n', " ")); } + if tally.is_failure() { + println!( + "Chaos failure: {} flagged, {} fixtures unreadable, {} mutations ({} judged)", + tally.flagged.len(), + tally.file_errors.len(), + tally.shapes.total(), + tally.total(), + ); + } } /// The machine-readable form of [`print_chaos_tally`], for `--chaos-report`. @@ -742,4 +808,45 @@ mod tests { "error should be actionable: {err}" ); } + + #[test] + fn test_diff_summary_counts_file_errors_as_failures() { + let mut tally = DiffTally::default(); + *tally.classes.entry(DiffClass::Pass.label()).or_insert(0) += 1; + tally.file_errors.push("broken.json".to_string()); + let err = diff_summary_error(&tally); + match err.kind { + TestErrorKind::TestsFailed { failed, total } => { + assert_eq!((failed, total), (1, 1), "the unreadable fixture is a failure"); + } + other => panic!("expected TestsFailed, got {other:?}"), + } + assert!( + !err.to_string().contains("Error: 0 tests failed"), + "must not claim zero failures: {err}" + ); + } + + #[test] + fn test_diff_summary_reports_a_run_that_judged_nothing() { + let err = diff_summary_error(&DiffTally::default()); + let text = err.to_string(); + assert!(text.contains("no fixture was judged"), "{text}"); + assert!(!text.contains("0 tests failed"), "{text}"); + + let mut tally = DiffTally::default(); + tally.file_errors.push("broken.json".to_string()); + let err = diff_summary_error(&tally); + let text = err.to_string(); + assert!(text.contains("no fixture was judged"), "{text}"); + assert!(text.contains("1 fixtures unreadable"), "{text}"); + } + + #[test] + fn test_chaos_summary_reports_a_run_that_judged_nothing() { + let err = chaos_summary_error(&ChaosSweepTally::default()); + let text = err.to_string(); + assert!(text.contains("no vector was judged"), "{text}"); + assert!(!text.contains("0 tests failed"), "{text}"); + } } diff --git a/crates/state-test/tests/cli_exit.rs b/crates/state-test/tests/cli_exit.rs index 042ed6fe..3688d6c4 100644 --- a/crates/state-test/tests/cli_exit.rs +++ b/crates/state-test/tests/cli_exit.rs @@ -66,6 +66,14 @@ fn stderr(out: &std::process::Output) -> String { String::from_utf8_lossy(&out.stderr).into_owned() } +fn stdout(out: &std::process::Output) -> String { + String::from_utf8_lossy(&out.stdout).into_owned() +} + +fn combined(out: &std::process::Output) -> String { + format!("{}\n{}", stdout(out), stderr(out)) +} + #[test] fn failing_tests_exit_with_code_1() { let path = write_fixture("failing.json", FAILING_SUITE); @@ -284,6 +292,15 @@ fn diff_run_that_judged_nothing_exits_1() { let out = run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); assert_eq!(out.status.code(), Some(1), "a corpus with no fixture in it must fail"); + let empty = combined(&out); + assert!( + empty.contains("no JSON test files found") || empty.contains("no fixture was judged"), + "an empty corpus must say nothing was judged: {empty}" + ); + assert!( + !empty.contains("Error: 0 tests failed"), + "must not claim zero failures while exiting 1: {empty}" + ); // A directory holding only fixtures on the validation skip list reaches the runner but judges // no unit, which is the same hole one step further in. @@ -291,6 +308,15 @@ fn diff_run_that_judged_nothing_exits_1() { let out = run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); assert_eq!(out.status.code(), Some(1), "a sweep that judged no unit must fail"); + let skipped = combined(&out); + assert!( + skipped.contains("no fixture was judged"), + "a sweep that judged no unit must say so: {skipped}" + ); + assert!( + !skipped.contains("Error: 0 tests failed"), + "must not claim zero failures while exiting 1: {skipped}" + ); let _ = std::fs::remove_dir_all(&dir); } @@ -309,12 +335,21 @@ fn diff_run_with_an_unparseable_fixture_exits_1() { let out = run_cli(&[dir.to_str().expect("utf8 path"), "--bench-spec", "Rex7", "--diff-spec", "Rex6"]); - let stdout = String::from_utf8_lossy(&out.stdout); + let stdout = stdout(&out); + let report = combined(&out); assert!( stdout.lines().any(|l| l.split_whitespace().eq(["PASS", "1"])), "the readable fixture still runs: {stdout}" ); assert!(stdout.contains("FILE_ERROR"), "the unreadable one is reported: {stdout}"); + assert!( + report.contains("1 fixtures unreadable"), + "the summary must count unreadable fixtures: {report}" + ); + assert!( + !report.contains("Error: 0 tests failed"), + "must not claim zero failures while exiting 1: {report}" + ); assert_eq!(out.status.code(), Some(1), "a corpus the sweep only partly read is not a pass"); let _ = std::fs::remove_dir_all(&dir); } From 3a4bdd93daf38f45dd5fc36c7bfcb45fa4519b2e Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 00:39:11 +0800 Subject: [PATCH 203/208] test(state-test): prefix cli_exit tests with test_ The crate convention requires a `test_` prefix on `#[test]` functions. Rename all thirteen exit-contract tests in `cli_exit.rs`, including the three that predated the differential and fill coverage, so the file is consistent. --- crates/state-test/tests/cli_exit.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/state-test/tests/cli_exit.rs b/crates/state-test/tests/cli_exit.rs index 3688d6c4..9b438eaf 100644 --- a/crates/state-test/tests/cli_exit.rs +++ b/crates/state-test/tests/cli_exit.rs @@ -75,7 +75,7 @@ fn combined(out: &std::process::Output) -> String { } #[test] -fn failing_tests_exit_with_code_1() { +fn test_failing_tests_exit_with_code_1() { let path = write_fixture("failing.json", FAILING_SUITE); let path = path.to_str().expect("utf8 path"); @@ -93,14 +93,14 @@ fn failing_tests_exit_with_code_1() { } #[test] -fn invalid_path_exits_with_code_1() { +fn test_invalid_path_exits_with_code_1() { let out = run_cli(&["/nonexistent/state_test_cli_exit_4928"]); assert_eq!(out.status.code(), Some(1)); assert!(!out.stderr.is_empty(), "stderr should carry the error message"); } #[test] -fn passing_run_exits_with_code_0() { +fn test_passing_run_exits_with_code_0() { // A fixture whose recorded roots are the ones its execution produces. `--fill` computes them, // which is also what makes this a run with something in it to pass: the expectation exists and // is checked. @@ -122,7 +122,7 @@ fn passing_run_exits_with_code_0() { } #[test] -fn validate_run_over_a_unit_with_no_expectation_exits_1() { +fn test_validate_run_over_a_unit_with_no_expectation_exits_1() { // A unit whose `post` is empty is walked, executed against nothing, and counted by nothing. // Reading that as a pass makes "the runner checked this file" true of a file that pins no // behavior at all — and `--fill --force` writing an empty `post` is one bug away. @@ -148,7 +148,7 @@ fn validate_run_over_a_unit_with_no_expectation_exits_1() { } #[test] -fn fill_that_filled_nothing_exits_1() { +fn test_fill_that_filled_nothing_exits_1() { // `--keep-going` decides when a run stops, not whether an empty one counts. Without it the // fill loop simply has nothing to fail at, so a corpus that never arrived walks no file, // writes no fixture, and used to exit 0 — the one report that must never read as a pass. @@ -179,7 +179,7 @@ fn fill_that_filled_nothing_exits_1() { } #[test] -fn fill_tally_counts_transaction_vectors() { +fn test_fill_tally_counts_transaction_vectors() { // The tally a sweep gates on has to count what the differential sweep counts, or the two // numbers cannot be compared with each other or against a baseline recorded under the other // mode. A unit is a family of transactions; the vector is the unit both modes agree on. @@ -212,7 +212,7 @@ fn fill_tally_counts_transaction_vectors() { } #[test] -fn diff_run_with_no_unexplained_difference_exits_with_code_0() { +fn test_diff_run_with_no_unexplained_difference_exits_with_code_0() { let mut suite: serde_json::Value = serde_json::from_str(FAILING_SUITE).expect("parse"); // The differential run computes both sides itself; the recorded `post` is irrelevant, and an // empty one keeps the fixture honest about that. @@ -241,7 +241,7 @@ fn diff_run_with_no_unexplained_difference_exits_with_code_0() { } #[test] -fn diff_run_over_an_unauthorized_spec_pair_is_refused() { +fn test_diff_run_over_an_unauthorized_spec_pair_is_refused() { // Every rule in the classifier is a reading of one sentence, the Rex7 precision invariant, // which relates Rex7 to Rex6 and states nothing about any other pair. Pointed at another pair // it would grant a licence that pair never had — deciding, from mechanisms that are evidence @@ -263,7 +263,7 @@ fn diff_run_over_an_unauthorized_spec_pair_is_refused() { } #[test] -fn validate_run_that_judged_nothing_exits_1() { +fn test_validate_run_that_judged_nothing_exits_1() { // Same hole as in the differential mode, one mode over: a corpus whose every file is on the // validation skip list walks files, reaches no unit, and reports zero errors. let dir = std::env::temp_dir().join("state_test_cli_exit_all_skipped"); @@ -282,7 +282,7 @@ fn validate_run_that_judged_nothing_exits_1() { } #[test] -fn diff_run_that_judged_nothing_exits_1() { +fn test_diff_run_that_judged_nothing_exits_1() { // A sweep whose corpus never arrived reaches the gate with an empty tally: zero panics, zero // unexplained differences, every count truthful and meaningless. It must not read as a pass. let dir = std::env::temp_dir().join("state_test_cli_exit_empty_corpus"); @@ -321,7 +321,7 @@ fn diff_run_that_judged_nothing_exits_1() { } #[test] -fn diff_run_with_an_unparseable_fixture_exits_1() { +fn test_diff_run_with_an_unparseable_fixture_exits_1() { // A file the sweep cannot parse is a fixture it did not judge. Skipping it quietly is how a // corpus shrinks without anyone noticing. let dir = std::env::temp_dir().join("state_test_cli_exit_bad_fixture"); @@ -355,7 +355,7 @@ fn diff_run_with_an_unparseable_fixture_exits_1() { } #[test] -fn diff_spec_requires_an_explicit_target_spec() { +fn test_diff_spec_requires_an_explicit_target_spec() { let path = write_fixture("diff_needs_target.json", FAILING_SUITE); let out = run_cli(&[path.to_str().expect("utf8 path"), "--diff-spec", "Rex6"]); assert_eq!(out.status.code(), Some(2), "clap rejects the incomplete flag combination"); @@ -366,7 +366,7 @@ fn diff_spec_requires_an_explicit_target_spec() { } #[test] -fn diff_spec_rejects_an_unknown_spec_name() { +fn test_diff_spec_rejects_an_unknown_spec_name() { let path = write_fixture("diff_bad_spec.json", FAILING_SUITE); let out = run_cli(&[ path.to_str().expect("utf8 path"), From cc3d47feb8764bcf7ec13b1f2b16c8860a13ab01 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 01:04:11 +0800 Subject: [PATCH 204/208] docs: retarget leftover shim_measurement.rs comments after the rex7 test split --- crates/mega-evm/src/evm/AGENTS.md | 2 +- crates/mega-evm/tests/rex7/gas_surface.rs | 5 ++--- crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs | 12 ++++++------ crates/mega-state-test/src/chaos.rs | 9 +++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/mega-evm/src/evm/AGENTS.md b/crates/mega-evm/src/evm/AGENTS.md index d688628a..d8d09964 100644 --- a/crates/mega-evm/src/evm/AGENTS.md +++ b/crates/mega-evm/src/evm/AGENTS.md @@ -237,7 +237,7 @@ A *callback* upstream adds to the `Inspector` trait does neither — the trait g - **Compare every reading at every callback, not once per frame.** The four fields a frame's identity is made of — its target, the address of the code it runs, its caller and its value — together with its calldata identity, its static flag and its spec id, cannot change while it runs, which makes them the readings a cheaper shim would compare once per frame instead of twice per opcode. They can change — an inspector writes them — and the shape that exploits a per-frame comparison is an edit made in `step` and undone in `step_end`, which leaves the frame's identity equal to the EVM's at every point outside those two callbacks while the instruction in between reads something else. - `tests/rex7/shim_measurement.rs::test_a_frame_invariant_moved_and_moved_back_is_booked` is that shape, and it costs the transaction nothing, so no gas lane can stand in for the comparison. + `tests/rex7/shim_blind_spots.rs::test_a_frame_invariant_moved_and_moved_back_is_booked` is that shape, and it costs the transaction nothing, so no gas lane can stand in for the comparison. Making a reading cheaper is free to do; taking it less often needs an argument that this test survives. - **Book a lane through `Lane::book`, never by writing its net.** The gross half is what `is_zero` reads, so a booking that moves only the net is a rewrite the guard admits — and one that cancels against a later booking is exactly the shape that is invisible from the net alone. diff --git a/crates/mega-evm/tests/rex7/gas_surface.rs b/crates/mega-evm/tests/rex7/gas_surface.rs index d7e5b9b5..23400db5 100644 --- a/crates/mega-evm/tests/rex7/gas_surface.rs +++ b/crates/mega-evm/tests/rex7/gas_surface.rs @@ -557,9 +557,8 @@ fn sample_interpreter() -> Interpreter { /// Every field of every gas-carrying object an inspector is handed has a verdict. /// /// This is the closure the completeness table rests on. It is not a claim that the verdicts are -/// right — the tests in `shim_measurement.rs` and `inspector_cheat_matrix.rs` are — it is the -/// claim that there is no -/// field without one. +/// right — the tests in `shim_lanes.rs`, `shim_settlement.rs`, `shim_blind_spots.rs` and +/// `inspector_cheat_matrix.rs` are — it is the claim that there is no field without one. #[test] fn test_every_field_of_every_gas_carrier_has_a_verdict() { let gas = sample_gas(); diff --git a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs index e3d19a73..b6ef843e 100644 --- a/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs +++ b/crates/mega-evm/tests/rex7/inspector_cheat_matrix.rs @@ -1,9 +1,9 @@ //! Every rewrite shape, at every callback that can carry it. //! -//! `tests/rex7/shim_measurement.rs` pins one mechanism per test, chosen because each is a -//! different half of the measurement shim. This module asks the complementary question: not "does -//! each mechanism work" but "is there a callback on the `Inspector` trait, or a rewrite shape a -//! callback admits, that nothing measures". So the cases here are laid out as a matrix over the +//! `tests/rex7/shim_lanes.rs`, `shim_settlement.rs` and `shim_blind_spots.rs` pin one mechanism +//! per test. This module asks the complementary question: not "does each mechanism work" but "is +//! there a callback on the `Inspector` trait, or a rewrite shape a callback admits, that nothing +//! measures". So the cases here are laid out as a matrix over the //! trait's own surface — one row per callback, one column per rewrite shape — rather than over the //! shapes any particular tool is known to use. A callback added upstream, or a shape a callback //! newly admits, shows up as an empty cell. @@ -625,7 +625,7 @@ impl Cheat { /// A call's return range is shrunk to nothing rather than moved, because moving it past the /// caller's allocated memory is a panic in revm and this fixture's caller holds one word. The /// visible-effect form, where the caller then reads a word the callee never wrote, is pinned - /// in `shim_measurement.rs`. + /// in `shim_blind_spots.rs`. fn hit_outcome_metadata(&mut self, result: &mut FrameResult) { match result { FrameResult::Call(outcome) => { @@ -1203,7 +1203,7 @@ fn matrix() -> Vec { // The half of a finished outcome that sits outside the `InterpreterResult`: where a call's // return data lands, and which address a creation reports. Neither moves gas, and this // fixture discards both — the caller asks for a range the callee never fills and pops the - // address — so what the cell pins is the booking. `shim_measurement.rs` pins the forms + // address — so what the cell pins is the booking. `shim_blind_spots.rs` pins the forms // that change the produced state. cell!(at, EditOutcomeMetadata, ledger_intervention()); cell!(at, JournalWrite, InspectorLedger::default()); diff --git a/crates/mega-state-test/src/chaos.rs b/crates/mega-state-test/src/chaos.rs index 8e66e8c7..8a44feb7 100644 --- a/crates/mega-state-test/src/chaos.rs +++ b/crates/mega-state-test/src/chaos.rs @@ -3,9 +3,10 @@ //! # What this is for //! //! `MegaETH` supports rewriting inspectors in full: the measurement shim books what one does to a -//! transaction's gas, and the conservation law accounts for it. `tests/rex7/shim_measurement.rs` -//! and `tests/rex7/inspector_cheat_matrix.rs` pin that mechanism shape by shape, on fixtures built -//! to reach each shape. What neither can do is put a rewriting inspector on top of *arbitrary* +//! transaction's gas, and the conservation law accounts for it. `tests/rex7/shim_lanes.rs`, +//! `tests/rex7/shim_settlement.rs`, `tests/rex7/shim_blind_spots.rs` and +//! `tests/rex7/inspector_cheat_matrix.rs` pin that mechanism shape by shape, on fixtures built +//! to reach each shape. What they cannot do is put a rewriting inspector on top of *arbitrary* //! execution — the corner of the state space where a rewrite meets a detained frame, a latched //! resource exceed, a precompile, a `SELFDESTRUCT`, an EIP-7702 delegation, a nested revert. //! @@ -703,7 +704,7 @@ impl ChaosInspector { // correctly booked nowhere, and would then look to the ledger gate like a rewrite // the shim missed. Leave the counter alone and spend no budget; the same edit // reaches the live object at the next callback, and the dead window itself is - // pinned by `tests/rex7/shim_measurement.rs`. + // pinned by `tests/rex7/shim_settlement.rs`. if matches!(interp.bytecode.action(), Some(InterpreterAction::Return(_))) { return; } From a1fafcf9c0811dfe064bc64c02c84348f2c49e6c Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 15:05:36 +0800 Subject: [PATCH 205/208] test(limit): cover the pre-pop peek, the inspector lanes and the term rendering Closes the surviving mutants in `crates/mega-evm/src/limit/`: - `check_limit_after_pop` on the compute-gas, KV-update and state-growth trackers had no test that made the pre-merge reading differ from the live one, so returning the default `WithinLimit` went unnoticed. Each dimension now has a late frame-local exceed: a frame that stayed inside its own budget and pushes its caller past theirs once merged, with the revert case asserted alongside for the two discardable dimensions. - `record_inspector_gas_adjustment` closes a measured segment only where one is open; an adjustment taken at `initialize_interp` must book its lane and settle nothing. - `record_inspector_action_counter_adjustment` had no assertion on the lane it books, so the whole body could be dropped. The new case also pins that two cancelling edits still leave the lane non-zero for the block guard. - The code-deposit settlement blamed an undetained transaction-level exceed on detention when the detained limit had never been lowered. - `ConservationTerms`'s `Display` renders into every assertion message the law raises, and nothing asserted its text. --- crates/mega-evm/src/limit/compute_gas.rs | 33 ++++++++++ crates/mega-evm/src/limit/conservation.rs | 15 +++++ crates/mega-evm/src/limit/kv_update.rs | 48 ++++++++++++++- crates/mega-evm/src/limit/limit.rs | 74 +++++++++++++++++++++++ crates/mega-evm/src/limit/state_growth.rs | 48 ++++++++++++++- 5 files changed, 216 insertions(+), 2 deletions(-) diff --git a/crates/mega-evm/src/limit/compute_gas.rs b/crates/mega-evm/src/limit/compute_gas.rs index ee138860..41ea50f7 100644 --- a/crates/mega-evm/src/limit/compute_gas.rs +++ b/crates/mega-evm/src/limit/compute_gas.rs @@ -417,4 +417,37 @@ mod tests { tracker.record_gas_used(1); assert!(tracker.is_detained_exceed(), "usage > detained_limit must be a detained exceed"); } + + /// A frame's usage is weighed against its *caller's* budget only once the two have been + /// merged, so the pre-merge reading has to answer a question the live one cannot: the frame + /// below is already over its budget while the frame above is still inside its own. + /// + /// Compute gas is persistent, so the merge happens whether the frame returns or reverts and + /// the answer is the same either way. + #[test] + fn test_check_limit_after_pop_sees_a_frame_local_exceed_the_live_check_cannot() { + let mut tracker = ComputeGasTracker::new(MegaSpecId::REX4, 10_000); + tracker.push_frame_with_limit_for_test(100); + tracker.record_gas_used(60); + tracker.push_frame_with_limit_for_test(60); + tracker.record_gas_used(60); + + assert_eq!( + tracker.check_limit(), + LimitCheck::WithinLimit, + "the child is exactly at its own budget, and nothing else is over", + ); + for success in [true, false] { + assert_eq!( + tracker.check_limit_after_pop(success), + LimitCheck::ExceedsLimit { + kind: LimitKind::ComputeGas, + limit: 100, + used: 120, + frame_local: true, + }, + "the merged caller is 20 over its own budget (success: {success})", + ); + } + } } diff --git a/crates/mega-evm/src/limit/conservation.rs b/crates/mega-evm/src/limit/conservation.rs index 35e7fd17..e6564462 100644 --- a/crates/mega-evm/src/limit/conservation.rs +++ b/crates/mega-evm/src/limit/conservation.rs @@ -136,6 +136,9 @@ impl fmt::Display for ConservationTerms { #[cfg(test)] mod tests { + #[cfg(not(feature = "std"))] + use alloc as std; + use super::*; fn terms() -> ConservationTerms { @@ -233,4 +236,16 @@ mod tests { assert_eq!(terms.envelope_for(0), 0); assert_eq!(terms.unbooked_for(0), 0); } + + /// The term set is rendered into every assertion message the law raises, and a message that + /// names no term is a failing invariant with nothing to debug it by. Pins the full text: the + /// order the law states the terms in, and the signed lanes' signs. + #[test] + fn test_display_renders_every_term_in_the_order_the_law_states_them() { + assert_eq!( + std::format!("{}", terms()), + "enforced compute 21000, non-compute 5000, minted stipend 2300, \ + inspector conjured -400, booked destroyed 0", + ); + } } diff --git a/crates/mega-evm/src/limit/kv_update.rs b/crates/mega-evm/src/limit/kv_update.rs index fd4368ca..ad33e057 100644 --- a/crates/mega-evm/src/limit/kv_update.rs +++ b/crates/mega-evm/src/limit/kv_update.rs @@ -299,7 +299,10 @@ impl TxRuntimeLimit for KVUpdateTracker { #[cfg(test)] mod tests { - use super::*; + use super::{ + super::{LimitCheck, LimitKind}, + *, + }; /// `record_account_update` must charge exactly one KV update against the current frame /// (used by REX5+ SELFDESTRUCT-beneficiary metering); it must not be a no-op. @@ -317,4 +320,47 @@ mod tests { fn test_tx_limit_reports_configured_limit() { assert_eq!(KVUpdateTracker::new(MegaSpecId::MINI_REX, 4_321).tx_limit(), 4_321); } + + /// A frame's usage is weighed against its *caller's* budget only once the two have been + /// merged, so the pre-merge reading has to answer a question the live one cannot: the caller + /// is already over its budget while the frame on top is still inside its own. + /// + /// A child receives 98% of its caller's remaining budget, so merging one that stayed inside + /// its own budget cannot by itself push the caller past its. The charge that breaks that + /// arithmetic is the REX6 creator nonce bump, which lands on the caller's lane after the + /// child's budget has already been computed — `record_parent_discardable` below. + /// + /// The answer depends on how the frame ends: a reverting child's discardable updates vanish + /// instead of merging, and the caller stays inside its budget. + #[test] + fn test_check_limit_after_pop_sees_a_frame_local_exceed_the_live_check_cannot() { + let mut tracker = KVUpdateTracker::new(MegaSpecId::REX6, 1_000); + tracker.frame_tracker.push_dummy_frame(); + tracker.frame_tracker.push_dummy_frame(); + tracker.record_discardable(500); + tracker.frame_tracker.push_dummy_frame(); + tracker.record_discardable(470); + tracker.record_parent_discardable(20); + + assert_eq!( + tracker.check_limit(), + LimitCheck::WithinLimit, + "the top frame is exactly at its own budget, and the transaction is under its limit", + ); + assert_eq!( + tracker.check_limit_after_pop(true), + LimitCheck::ExceedsLimit { + kind: LimitKind::KVUpdate, + limit: 980, + used: 990, + frame_local: true, + }, + "the merged caller is 10 over the budget it was pushed with", + ); + assert_eq!( + tracker.check_limit_after_pop(false), + LimitCheck::WithinLimit, + "a reverting frame's updates vanish rather than merging", + ); + } } diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 3866b400..1381fe96 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -2835,6 +2835,80 @@ mod tests { assert_eq!(latched_kind(&limit), None, "an affordable charge must not latch"); } + /// With no volatile access in play the detained limit was never lowered, so an unaffordable + /// code-deposit charge is the transaction's own compute limit and nothing else. Blaming it on + /// detention would report `VolatileDataAccessOutOfGas` with a limit figure that never moved. + #[test] + fn test_create_code_deposit_tx_level_arm_leaves_an_undetained_exceed_undetained() { + let mut limits = test_limits(); + limits.tx_compute_gas_limit = 10; + let mut limit = AdditionalLimit::new(MegaSpecId::REX7, limits); + // A frame budget far above the charge, so the transaction limit is what binds. + limit.compute_gas.push_frame_with_limit_for_test(u64::MAX); + + let rewrite = limit.settle_create_code_deposit_compute_gas(11); + + let (result, _) = rewrite.expect("an unaffordable charge must rewrite the result"); + assert_eq!( + result, + AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT, + "a TX-level exceed halts the transaction", + ); + assert_eq!(latched_kind(&limit), Some(LimitKind::ComputeGas), "the TX-level arm latches"); + assert!( + limit.detained_compute_gas_halt_reason(VolatileDataAccess::empty()).is_none(), + "a limit that was never lowered cannot be what detained the transaction", + ); + } + + /// An inspector's edit to a running frame's counter is booked on its lane whether or not a + /// measured segment is open, and the segment is closed only where one is. + /// + /// `initialize_interp` is the callback with no open segment: the frame was built a moment ago + /// and its entry hook has not opened the window yet, so the baseline still belongs to another + /// frame. Closing a segment there would settle the distance between that baseline and this + /// frame's counter as compute gas this frame performed. + #[test] + fn test_an_adjustment_outside_an_open_segment_settles_no_segment() { + let mut limit = rex7_limit(); + // A baseline left behind by the frame that is still suspended below this one. + limit.checkpoint.sync_baseline(1_000_000); + let mut gas = Gas::new(500_064); + + limit.record_inspector_gas_adjustment::(&mut gas, 500_000, true); + + assert_eq!( + limit.get_usage().compute_gas, + 0, + "no segment is open, so there is no distance to settle as work", + ); + assert_eq!(gas.remaining(), 500_064, "and the counter is left exactly as the EVM had it"); + assert_eq!( + limit.inspector_ledger().gas.net(), + 64, + "the edit itself is still booked on the counter lane", + ); + } + + /// A callback that removed the frame's pending action leaves the frame carrying on from its + /// own counter, so an edit that had travelled in that action lands on the counter lane. + /// + /// The lane's gross is what the block guard reads, so two edits that cancel are still two + /// edits: a transaction an inspector took part in must not read as one the EVM produced alone. + #[test] + fn test_a_removed_actions_adjustment_is_booked_on_the_counter_lane() { + let mut limit = rex7_limit(); + assert!(limit.inspector_ledger().is_zero(), "a fresh ledger has seen nothing"); + + limit.record_inspector_action_counter_adjustment(64); + limit.record_inspector_action_counter_adjustment(-64); + + let ledger = limit.inspector_ledger(); + assert_eq!(ledger.gas.net(), 0, "the two edits cancel on the net"); + assert_eq!(ledger.gas.gross(), 128, "and the lane still carries both"); + assert!(!ledger.is_zero(), "so the guard refuses a transaction that saw them"); + } + /// `mark_frame_result_as_exceeding_limit` rewrites both frame-result variants in place. #[test] fn test_mark_frame_result_as_exceeding_limit_rewrites_both_variants() { diff --git a/crates/mega-evm/src/limit/state_growth.rs b/crates/mega-evm/src/limit/state_growth.rs index 61b59093..bdc8d571 100644 --- a/crates/mega-evm/src/limit/state_growth.rs +++ b/crates/mega-evm/src/limit/state_growth.rs @@ -380,7 +380,10 @@ impl TxRuntimeLimit for StateGrowthTracker { #[cfg(test)] mod tests { - use super::*; + use super::{ + super::{LimitCheck, LimitKind}, + *, + }; /// `reset` must clear accumulated TX-level state-growth usage so a tracker reused /// across transactions does not leak growth from the previous one. @@ -456,4 +459,47 @@ mod tests { fn test_tx_limit_reports_configured_limit() { assert_eq!(StateGrowthTracker::new(MegaSpecId::REX5, 4_321).tx_limit(), 4_321); } + + /// A frame's growth is weighed against its *caller's* budget only once the two have been + /// merged, so the pre-merge reading has to answer a question the live one cannot: the caller + /// is already over its budget while the frame on top is still inside its own. + /// + /// A child receives 98% of its caller's remaining budget, so merging one that stayed inside + /// its own budget cannot by itself push the caller past its. What breaks that arithmetic is a + /// charge that reaches the caller's lane after the child's budget has already been fixed, + /// which is what the parent-lane write below stands for. + /// + /// The answer depends on how the frame ends: a reverting frame's growth vanishes instead of + /// merging, and the caller stays inside its budget. + #[test] + fn test_check_limit_after_pop_sees_a_frame_local_exceed_the_live_check_cannot() { + let mut tracker = StateGrowthTracker::new(MegaSpecId::REX5, 1_000); + tracker.push_frame(); + tracker.push_frame(); + tracker.record_growth(500); + tracker.push_frame(); + tracker.record_growth(470); + tracker.frame_tracker.add_parent_discardable(20); + + assert_eq!( + tracker.check_limit(), + LimitCheck::WithinLimit, + "the top frame is exactly at its own budget, and the transaction is under its limit", + ); + assert_eq!( + tracker.check_limit_after_pop(true), + LimitCheck::ExceedsLimit { + kind: LimitKind::StateGrowth, + limit: 980, + used: 990, + frame_local: true, + }, + "the merged caller is 10 over the budget it was pushed with", + ); + assert_eq!( + tracker.check_limit_after_pop(false), + LimitCheck::WithinLimit, + "a reverting frame's growth vanishes rather than merging", + ); + } } From 513c3f9c6e5a10f6c5fd0f08b36bc209143c3029 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 15:05:54 +0800 Subject: [PATCH 206/208] test(evm): cover the transaction tripwires, the frame-stack accessor and the EIP-8037 mirror Closes the surviving mutants in `crates/mega-evm/src/evm/`: - Both transaction-level tripwires could be emptied out with no test noticing. They now run against a receipt whose lanes account for none of its envelope, and against a declared `TrustedObserver` whose ledger is not empty. The sandbox case is asserted separately, because the skip is what keeps a sandbox transaction out of a law stated over its parent's envelope. - `EvmTr::frame_stack` is an accessor revm's `execution_result` and `catch_error` clear the EVM's own stack through; nothing pinned that it is not a factory. - The EIP-8037 branch of the create classification is mirrored from upstream for lockstep rather than for reach, and no `MegaEVM` transaction can enter it. The classification is now driven directly under a configuration that enables and prices the split, so the hash charge, the state-gas charge and their two failure paths are all exercised. - A call frame's outcome carries the new-account state-gas flag its inputs were built with. - The frame-input comparison's empty-variant pair, the frame-init origin question in both directions, and the shim's log delegation each had no test. --- crates/mega-evm/src/evm/execution.rs | 15 ++++ crates/mega-evm/src/evm/frame.rs | 130 ++++++++++++++++++++++++++- crates/mega-evm/src/evm/inspector.rs | 89 ++++++++++++++++++ crates/mega-evm/src/evm/mod.rs | 74 ++++++++++++++- 4 files changed, 305 insertions(+), 3 deletions(-) diff --git a/crates/mega-evm/src/evm/execution.rs b/crates/mega-evm/src/evm/execution.rs index e6e41a23..79de3d07 100644 --- a/crates/mega-evm/src/evm/execution.rs +++ b/crates/mega-evm/src/evm/execution.rs @@ -2185,6 +2185,21 @@ mod mutation_tests { consume_synthetic_limit_frame(evm.ctx_ref(), result); } + /// `EvmTr::frame_stack` is an accessor, not a factory. revm's `Handler::execution_result` and + /// `Handler::catch_error` reach the EVM's own frame stack through it and clear it there, so a + /// fresh stack handed out per call would leave the real one untouched and every caller + /// clearing something nobody else can see. + #[test] + fn test_frame_stack_hands_out_the_evms_own_stack() { + let mut evm = MegaEvm::new(MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX5)); + let own = core::ptr::from_ref(&evm.inner.frame_stack); + assert_eq!( + core::ptr::from_mut(EvmTr::frame_stack(&mut evm)).cast_const(), + own, + "the trait accessor must project the EVM's own frame stack", + ); + } + #[test] fn test_frame_init_depth_short_circuit_pushes_limit_frame() { let mut evm = MegaEvm::new(MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX5)); diff --git a/crates/mega-evm/src/evm/frame.rs b/crates/mega-evm/src/evm/frame.rs index 53f24ce3..4bf65afd 100644 --- a/crates/mega-evm/src/evm/frame.rs +++ b/crates/mega-evm/src/evm/frame.rs @@ -312,9 +312,15 @@ mod tests { use alloy_primitives::{address, Address as Addr, U256}; use revm::{ context::JournalTr, - interpreter::{Gas, InstructionResult, InterpreterResult}, + context_interface::cfg::{GasId, GasParams}, + handler::{CallFrame, FrameData}, + interpreter::{ + CallInput, CallInputs, CallScheme, CallValue, Gas, InstructionResult, + InterpreterAction, InterpreterResult, + }, + primitives::hardfork::SpecId as RevmSpecId, }; - use std::{vec, vec::Vec}; + use std::{boxed::Box, vec, vec::Vec}; const DEPLOYED: Addr = address!("00000000000000000000000000000000000c0de0"); /// Ample: the deposit charge for the runtime codes below is a few hundred gas. @@ -478,4 +484,124 @@ mod tests { ); } } + + fn call_inputs(charged_new_account_state_gas: bool) -> CallInputs { + CallInputs { + input: CallInput::Bytes(Bytes::new()), + return_memory_offset: 0..0, + gas_limit: FRAME_GAS, + bytecode_address: DEPLOYED, + target_address: DEPLOYED, + caller: Addr::ZERO, + value: CallValue::Transfer(U256::ZERO), + scheme: CallScheme::Call, + is_static: false, + reservoir: 0, + known_bytecode: Default::default(), + charged_new_account_state_gas, + } + } + + /// A call frame's outcome carries the EIP-8037 new-account flag its *inputs* were built with, + /// so the caller can refund the upfront charge when the call ends in revert or halt. The flag + /// lives only on the inputs, and the outcome is the only thing that reaches the caller — read + /// it off anything else and a reverted call keeps a charge it never owed. + #[test] + fn test_a_call_frames_outcome_carries_its_inputs_new_account_state_gas_flag() { + for charged in [true, false] { + let ctx = context(); + let mut frame = EthFrame::invalid(); + frame.data = FrameData::Call(CallFrame { return_memory_range: 0..0 }); + frame.input = FrameInput::Call(Box::new(call_inputs(charged))); + + let action = InterpreterAction::Return(InterpreterResult::new( + InstructionResult::Stop, + Bytes::new(), + Gas::new(FRAME_GAS), + )); + let ItemOrResult::Result(pending) = classify_frame_action(&ctx, &mut frame, action) + else { + panic!("a returning frame classifies into a result, not into a child frame"); + }; + let (FrameResult::Call(outcome), _) = pending.split() else { + panic!("a call frame classifies into a call outcome"); + }; + + assert_eq!( + outcome.charged_new_account_state_gas, charged, + "the flag must travel from the frame's inputs onto the outcome", + ); + } + } + + /// The configuration the EIP-8037 branch of the classification is written for: the state-gas + /// split enabled, and a schedule that prices the code deposit it splits. + /// + /// No `MegaEVM` transaction can run under it. `force_amsterdam_eip8037_off` pins the flag off + /// wherever a configuration comes from, and the gas-schedule pin rejects a rewritten table, so + /// the branch is carried for lockstep with upstream rather than for reach. Calling the + /// classification directly is what lets the lockstep be checked. + fn eip8037_context() -> (MegaContext, u64) { + let per_byte = + GasParams::new_spec(RevmSpecId::AMSTERDAM).get(GasId::code_deposit_state_gas()); + assert_ne!( + per_byte, 0, + "the probe needs a schedule that prices the code deposit's state gas" + ); + + let mut ctx = context(); + ctx.inner.cfg.enable_amsterdam_eip8037 = true; + ctx.inner.cfg.gas_params.override_gas([(GasId::code_deposit_state_gas(), per_byte)]); + (ctx, per_byte) + } + + /// With EIP-8037 on, a creation pays two charges past the code deposit — a hash charge and a + /// state-gas charge — and is accepted only when it can afford both. + #[test] + fn test_the_eip8037_split_charges_the_hash_and_the_state_gas_on_top_of_the_deposit() { + let (ctx, state_gas_per_byte) = eip8037_context(); + let code = vec![0x00; 32]; + let deposit = 32 * revm::interpreter::gas::CODEDEPOSIT; + let hash = ctx.cfg().gas_params().keccak256_cost(code.len()); + let state_gas = state_gas_per_byte * 32; + assert_ne!(hash, 0, "the probe needs a priced hash charge to tell the two apart"); + + let mut result = returned(code, FRAME_GAS); + let verdict = classify_create_return(&ctx, &mut result, DEPLOYED); + + assert!(accepts(&verdict), "a creation that can afford all three charges is accepted"); + assert_eq!(result.result, InstructionResult::Return); + assert_eq!( + FRAME_GAS - result.gas.remaining(), + deposit + hash + state_gas, + "all three charges are taken, and the state gas spills out of an empty reservoir", + ); + assert_eq!( + result.gas.state_gas_spent(), + i64::try_from(state_gas).expect("the probe's state gas fits an i64"), + "the state-gas charge is recorded on the state dimension, not only on the counter", + ); + } + + /// Each of the two EIP-8037 charges fails the creation on its own when the frame is one gas + /// short of it, and names the failure out of gas. + #[test] + fn test_either_eip8037_charge_can_fail_the_creation_on_its_own() { + let (ctx, state_gas_per_byte) = eip8037_context(); + let code = vec![0x00; 32]; + let deposit = 32 * revm::interpreter::gas::CODEDEPOSIT; + let hash = ctx.cfg().gas_params().keccak256_cost(code.len()); + let state_gas = state_gas_per_byte * 32; + + for (name, gas) in [ + ("the hash charge", deposit + hash - 1), + ("the state-gas charge", deposit + hash + state_gas - 1), + ] { + let mut result = returned(code.clone(), gas); + let verdict = classify_create_return(&ctx, &mut result, DEPLOYED); + + assert!(!accepts(&verdict), "{name}: one gas short must reject the creation"); + assert_eq!(result.result, InstructionResult::OutOfGas, "{name}: classification"); + } + } } diff --git a/crates/mega-evm/src/evm/inspector.rs b/crates/mega-evm/src/evm/inspector.rs index 984e5487..3461fc13 100644 --- a/crates/mega-evm/src/evm/inspector.rs +++ b/crates/mega-evm/src/evm/inspector.rs @@ -1500,6 +1500,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::{test_utils::MemoryDatabase, EmptyExternalEnv}; use revm::{ bytecode::Bytecode, interpreter::{ @@ -1864,6 +1865,94 @@ mod tests { ); } + /// ★ Two frame inputs of the same empty variant are not a rewrite. + /// + /// The variant a frame's inputs carry is itself part of what the shim compares — a callback + /// that swapped it has rewritten the frame as thoroughly as it is possible to — so the pair + /// that did not move needs an arm of its own. `FrameInput::Empty` is revm's placeholder rather + /// than a frame it builds, and without the arm the placeholder compared against itself would + /// book an intervention nobody made. + #[test] + fn test_two_empty_frame_inputs_are_not_a_rewrite() { + assert!( + !frame_input_rewritten(FrameInput::Empty, &FrameInput::Empty), + "the placeholder compared against itself moved nothing", + ); + assert!( + frame_input_rewritten( + FrameInput::Empty, + &FrameInput::Create(Box::new(create_inputs())), + ), + "a variant swapped out of the placeholder is a rewrite", + ); + assert!( + frame_input_rewritten( + FrameInput::Create(Box::new(create_inputs())), + &FrameInput::Empty, + ), + "and so is one swapped into it", + ); + } + + /// ★ The frame-init origin question answers the settlement window, in both directions. + /// + /// It decides what a classification rewrite does — a running frame's journal decision is still + /// outstanding and follows the rewrite, an init-produced result's was taken before the + /// callback existed and is refused — so an answer stuck at either constant refuses every + /// rewrite or follows every one. + #[test] + fn test_the_frame_init_origin_tracks_the_settlement_window() { + let context: MegaContext = + MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX7); + assert!( + !context.is_frame_init_result(), + "outside the window, a result is one a frame ran to produce", + ); + + context.additional_limit.borrow_mut().set_settling_frame_init_result(true); + assert!( + context.is_frame_init_result(), + "inside the window, it is one frame init produced with no frame ever built", + ); + + context.additional_limit.borrow_mut().set_settling_frame_init_result(false); + assert!(!context.is_frame_init_result(), "and the window closes again"); + } + + /// Counts the logs it is handed, and nothing else. + #[derive(Default)] + struct LogCollectingInspector { + logs: Vec, + } + + impl Inspector for LogCollectingInspector { + fn log(&mut self, _context: &mut CTX, log: Log) { + self.logs.push(log); + } + } + + /// ★ The shim forwards the log callback to the inspector it wraps. + /// + /// This is the one callback with no interpreter and no frame inputs to compare, so there is + /// nothing for the shim to measure and its whole job is delegation. `MegaETH` reaches it from + /// `forward_precompile_logs`, which is the only way a precompile's logs are ever shown to an + /// inspector — a shim that swallowed them would hide them entirely. + #[test] + fn test_the_shim_forwards_the_log_callback_it_has_nothing_to_measure_on() { + let mut context: MegaContext = + MegaContext::new(MemoryDatabase::default(), MegaSpecId::REX7); + let mut shim = MeasuredInspector::new(LogCollectingInspector::default()); + let log = Log::new_unchecked(Address::ZERO, Vec::new(), Bytes::from_static(b"emitted")); + + Inspector::<_, EthInterpreter>::log(&mut shim, &mut context, log.clone()); + + assert_eq!(shim.inner().logs, vec![log], "the wrapped inspector must see the log"); + assert!( + context.additional_limit.borrow().inspector_ledger().is_zero(), + "and a callback with nothing to measure must book nothing", + ); + } + /// ★ A creation's gas limit is not part of the comparison. /// /// It travels on the envelope lane, which books the amount rather than the fact, so counting diff --git a/crates/mega-evm/src/evm/mod.rs b/crates/mega-evm/src/evm/mod.rs index ed940db7..cdcf17f6 100644 --- a/crates/mega-evm/src/evm/mod.rs +++ b/crates/mega-evm/src/evm/mod.rs @@ -704,7 +704,7 @@ impl MegaEvm MegaTransactionOutcome { + MegaTransactionOutcome { + result_and_state: ExecResultAndState { + result: ExecutionResult::Success { + reason: revm::context::result::SuccessReason::Stop, + gas: revm::context::result::ResultGas::new_with_state_gas(envelope, 0, 0, 0), + logs: Vec::new(), + output: revm::context::result::Output::Call(Bytes::new()), + }, + state: EvmState::default(), + }, + data_size: 0, + kv_updates: 0, + compute_gas_used: 0, + compute_gas_destroyed: 0, + compute_gas_enforced: 0, + state_growth_used: 0, + inspector_ledger: ledger, + undeclared_inspector: false, + } + } + + fn empty_limit(spec: MegaSpecId) -> AdditionalLimit { + AdditionalLimit::new(spec, EvmTxRuntimeLimits::from_spec(spec)) + } + + /// The terminal check has to fail loudly on a receipt whose envelope no lane accounts for. + /// Its reach is the paths where the envelope is decided after settlement, so a version of it + /// that reads the lanes and says nothing is the whole failure mode. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "the tracker lanes must account for the whole receipt envelope") + )] + fn test_envelope_tripwire_fires_when_the_lanes_account_for_nothing() { + debug_assert_envelope_accounted( + MegaSpecId::REX7, + false, + &empty_limit(MegaSpecId::REX7), + &unaccounted_outcome(21_000, InspectorLedger::default()), + ); + } + + /// A sandbox transaction never settles a derivation of its own — the law is stated over an + /// outer transaction's final envelope, and the sandbox's gas is a charge inside its parent's. + /// The same lanes that trip the check outside a sandbox must be passed over inside one. + #[test] + fn test_envelope_tripwire_is_skipped_inside_a_sandbox() { + debug_assert_envelope_accounted( + MegaSpecId::REX7, + true, + &empty_limit(MegaSpecId::REX7), + &unaccounted_outcome(21_000, InspectorLedger::default()), + ); + } + + /// The declaration is a checked claim, not a comment: an inspector declared + /// `TrustedObserver` that booked anything must fail the transaction it took part in, even + /// when the booking was made at a callback whose own verification is missing. + #[test] + #[cfg_attr( + debug_assertions, + should_panic(expected = "an inspector declared `TrustedObserver` wrote something back") + )] + fn test_trusted_observer_tripwire_fires_on_a_declaration_that_did_not_hold() { + let ledger = InspectorLedger { gas: Lane::once(64), ..Default::default() }; + debug_assert_trusted_observer_kept_its_promise(true, &unaccounted_outcome(0, ledger)); + } } From 230d2b147807a6e09a2d7cb65f95dc237510db09 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 15:05:54 +0800 Subject: [PATCH 207/208] chore(mutants): record six equivalent survivors with justifications - `classify_create_return`'s `state_gas_for_code > 0` guard: `>= 0` is always true for a `u64`, and the extra `record_state_cost(0)` it then evaluates is a total no-op that returns true, so the same branch is taken with nothing moved. The `==` and `<` siblings at the same site do skip a charge that is owed and are killed by the new tests. - The two `if hide > 0` clamp guards, for the same reason: `record_regular_cost(0)` leaves the counter where it was, and the baseline sync the mutant re-runs re-writes the value already assigned a line above it. - Three predicates whose mutated expression contains `cfg!(debug_assertions)`. Every build the suite compiles has assertions on, which makes each mutant the same constant as the original on every input a test can produce. The justifications state that scope rather than claiming full equivalence, and name what guards the assertions-off semantics. --- mutants/suppressions.toml | 106 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/mutants/suppressions.toml b/mutants/suppressions.toml index 92c92041..09a7df28 100644 --- a/mutants/suppressions.toml +++ b/mutants/suppressions.toml @@ -444,3 +444,109 @@ file = "crates/mega-evm/src/evm/execution.rs" mutant = "crates/mega-evm/src/evm/execution.rs:1673:1: spec-gate let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::MINI_REX); -> let is_mini_rex_enabled = self.ctx().spec.is_enabled(MegaSpecId::EQUIVALENCE);" justification = "Equivalent: this is the uninspected frame_init, so the mutant runs AdditionalLimit::finalize_frame(result, exit, 0) under EQUIVALENCE and every branch is inert. absorb_frame_local_exceed / settle_exceptional_halt_burn / settle_frame_init_reject_burn return at checkpoint.rex7_enabled(); staged_precompile is always None because stage_precompile_envelope returns at the same REX7 gate; the inspector delta is the literal 0 this site passes, so book_crossing books nothing and settle_inspector_result_gas returns without booking; try_rescue_gas's check_limit() cannot latch under EQUIVALENCE (every EvmTxRuntimeLimits::equivalence() dimension is u64::MAX, every frame-local branch is rex4_enabled-gated off, and no usage is ever recorded), and both of its effects are unreadable pre-MINI_REX regardless — rescued_gas is consumed only in last_frame_result's is_mini_rex branch and every reader of has_exceeded_limit is behind an unmutated MINI_REX gate. FrameExit::Ran is unreachable from frame init. Full mega-evm suite green with the mutant applied." reviewer = "RealiCZ (cz) via S1 spec-gate triage" + +# --- classify_create_return: the EIP-8037 state-gas guard (equivalent) ------------- +# +# `if state_gas_for_code > 0 && !gas.record_state_cost(state_gas_for_code)`. The `>` vs `>=` +# boundary can only differ at `state_gas_for_code == 0`, where the mutant additionally evaluates +# `record_state_cost(0)`. That call is a no-op that always succeeds: `reservoir >= 0` holds for +# every `u64`, so it adds 0 to `state_gas_spent`, subtracts 0 from the reservoir and returns +# `true` — making the conjunction `true && !true == false`, the same branch the original takes. +# The gas counter, the reservoir and the state-gas dimension are all left where they were. +# +# The sibling `>` -> `==` and `>` -> `<` mutants are *not* equivalent (they skip a charge that is +# owed) and are killed by +# `test_the_eip8037_split_charges_the_hash_and_the_state_gas_on_top_of_the_deposit`. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/frame.rs" +mutant = "replace > with >= in classify_create_return" +justification = "Equivalent: > vs >= differs only at state_gas_for_code == 0, where the mutant evaluates record_state_cost(0). That is a total no-op returning true (reservoir >= 0 holds for every u64; it adds 0 to state_gas_spent and subtracts 0 from the reservoir), so the conjunction is false either way and the same branch is taken with the gas counter, reservoir and state-gas dimension unchanged. The > -> == and > -> < siblings are not equivalent and are killed by test_the_eip8037_split_charges_the_hash_and_the_state_gas_on_top_of_the_deposit." +reviewer = "RealiCZ (cz)" + +# --- the two `if hide > 0` clamp guards (equivalent) ------------------------------- +# +# Both sites read `let hide = self.checkpoint_clamp_amount(gas.remaining());` and then guard +# `gas.record_regular_cost(hide)` with `if hide > 0`. `hide` is a `u64`, so `>= 0` is always +# true and the mutant's only extra work is the `hide == 0` case: +# +# - `record_regular_cost(0)` is `remaining.checked_sub(0)`, which always succeeds and leaves +# `remaining` exactly where it was, so the `debug_assert!(clamped, ...)` beside it still +# passes; +# - in `record_inspector_gas_adjustment` the guarded body also re-runs +# `sync_checkpoint_baseline(gas.remaining())`, which is a plain assignment of the value the +# line above the clamp derivation already wrote — nothing between the two moves +# `gas.remaining()`; +# - in `before_frame_run` the baseline sync sits *outside* the guard and runs either way. +# +# The `>` -> `==` and `>` -> `<` siblings at both sites skip a clamp that binds and are killed. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/limit/limit.rs" +mutant = "replace > with >= in AdditionalLimit::record_inspector_gas_adjustment" +justification = "Equivalent: hide is a u64 so >= 0 is always true, and the hide == 0 body is inert. record_regular_cost(0) is remaining.checked_sub(0) — it always succeeds and leaves remaining unchanged, so the debug_assert beside it still passes — and the sync_checkpoint_baseline(gas.remaining()) it re-runs re-writes the value the line above the clamp derivation already assigned, since nothing in between moves gas.remaining(). The > -> == and > -> < siblings at this site are not equivalent and are killed." +reviewer = "RealiCZ (cz)" + +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/limit/limit.rs" +mutant = "replace > with >= in AdditionalLimit::before_frame_run" +justification = "Equivalent: hide is a u64 so >= 0 is always true, and the hide == 0 body is inert — record_regular_cost(0) is remaining.checked_sub(0), which always succeeds and leaves remaining unchanged, so the debug_assert beside it still passes. The baseline sync at the end of the branch sits outside this guard and runs either way. The > -> == and > -> < siblings at this site are not equivalent and are killed." +reviewer = "RealiCZ (cz)" + +# --- debug-only predicates the mutation gate cannot evaluate ---------------------- +# +# The three entries below share one shape: the mutated expression contains +# `cfg!(debug_assertions)`, and every build the test suite is compiled in has assertions ON. +# Under that constant the mutant and the original reduce to the same value on every input the +# suite can produce, so no test can kill them. They are recorded here rather than left as +# survivors, and the profile scope is stated in each justification rather than glossed as full +# equivalence: with assertions OFF the two forms do differ, which is exactly why they are +# written this way in the first place. +# +# Do not "fix" these by adding a test — there is no assertions-off test build to add one to. +# What guards the release semantics is different in each case, and is named per entry. + +# `measures()` is `!self.trusted || cfg!(debug_assertions)`. In a debug build it is the constant +# `true` — every inspector takes the measuring path — so both mutants below are that same +# constant. The release semantics they would change (a declared `TrustedObserver` delegated to +# unmeasured) is a fast path, not a behavioral one: whichever way `measures` answers, the +# callback still reaches the wrapped inspector, and a declaration that turns out to be false is +# caught by `verify_trusted` after every measured callback and by the transaction-level backstop +# in `evm/mod.rs`, both of which run in exactly the profile that measures. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/inspector.rs" +mutant = "replace MeasuredInspector::measures -> bool with true" +justification = "Equivalent in every profile the suite builds: measures() is `!trusted || cfg!(debug_assertions)`, which is the constant true under debug_assertions, so the mutant is the same constant and no test can distinguish it. With assertions off the mutant would take the measuring path for a declared TrustedObserver too — a fast-path difference, not a behavioral one, since the callback reaches the wrapped inspector either way and a false declaration is caught by verify_trusted and by the transaction-level backstop in evm/mod.rs, both of which run in the measuring profile." +reviewer = "RealiCZ (cz)" + +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/evm/inspector.rs" +mutant = "delete ! in MeasuredInspector::measures" +justification = "Equivalent in every profile the suite builds: measures() is `!trusted || cfg!(debug_assertions)`, and deleting the ! leaves `trusted || cfg!(debug_assertions)`, which is the same constant true under debug_assertions. With assertions off the mutant inverts which inspectors are measured; that is guarded by verify_trusted and by the transaction-level backstop in evm/mod.rs, which run in the measuring profile. The sibling `measures -> false` is not equivalent and is killed." +reviewer = "RealiCZ (cz)" + +# `let peeked = (!duplicate && (self.rex7_enabled() || cfg!(debug_assertions))).then(...)`. +# Under debug_assertions the original is unconditionally true and the mutant reduces to +# `rex7_enabled()`, so they differ only on a frozen spec — where the peek's sole consumer is the +# `debug_assert!` cross-check twenty lines below, which `is_none_or` satisfies vacuously when the +# peek was not taken. `peek_check_limit_after_pop` takes `&self` and mutates nothing, so skipping +# it has no other effect. The REX7 settlement above reads the peek only inside +# `if self.rex7_enabled()`, where both forms are true. +# +# Making this killable would mean driving the pre-pop peek and the post-pop check to disagree on +# a frozen spec, which is the drift the assertion exists to catch — i.e. it would require the bug. +[[suppress]] +kind = "line" +category = "equivalent" +file = "crates/mega-evm/src/limit/limit.rs" +mutant = "replace || with && in AdditionalLimit::before_frame_return_result" +justification = "Equivalent in every profile the suite builds: under debug_assertions the original condition is unconditionally true and the mutant reduces to rex7_enabled(), so the two differ only on a frozen spec. There the peek's only consumer is the debug_assert cross-check below, which is_none_or satisfies vacuously when no peek was taken; peek_check_limit_after_pop takes &self and mutates nothing, and the REX7 settlement reads the peek only inside `if self.rex7_enabled()`, where both forms are true. Killing it would require driving the pre-pop peek and the post-pop check to disagree on a frozen spec, which is the drift the assertion exists to catch. With assertions off the mutant would disable the REX7 pre-pop settlement, which no build the gate produces can observe." +reviewer = "RealiCZ (cz)" From ee53a0dd6802425c5123a03486ac524d2bb801e0 Mon Sep 17 00:00:00 2001 From: RealiCZ Date: Thu, 3 Sep 2026 17:03:57 +0800 Subject: [PATCH 208/208] test(diff): validate an inspected rerun before using its frame evidence Stage two of the differential classifier re-ran both specs under the frame inspector and judged the difference on the inspected pair alone. An inspector that moved either execution could therefore explain a difference the plain runs never produced: an observation-path regression that adds an inner exceptional halt on the target, with the compute-gas difference intact, turns the original unexplained difference into EXPLAINED and clears the nightly gate. Keep the plain outcomes and require each rerun to reproduce its own plain run first, over every quantity of the outcome but the frames the rerun exists to collect -- `compare`'s ten plus the evidence `judge` reads directly, which a rerun could move while leaving the compared ten alone. Any movement discards the frame evidence, names the side and the quantities that moved, and leaves the plain verdict standing. The "inspected pair agreed" branch is dropped: two reruns that each reproduced their plain outcome disagree on exactly the quantities the plain pair did, which a debug assertion now states. --- crates/mega-state-test/AGENTS.md | 3 + crates/mega-state-test/src/diff.rs | 303 +++++++++++++++++++++++++++-- 2 files changed, 292 insertions(+), 14 deletions(-) diff --git a/crates/mega-state-test/AGENTS.md b/crates/mega-state-test/AGENTS.md index ca2764da..7432d3ef 100644 --- a/crates/mega-state-test/AGENTS.md +++ b/crates/mega-state-test/AGENTS.md @@ -21,6 +21,7 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Parallel execution uses shared queue and atomic counters with optional single-thread mode. - Differential classification is evidence-based, never a list of fixtures allowed to differ: every `Mechanism` is a fact read off an execution, and the hypothesis it falsifies is what licenses a difference. - Only an execution-provenance observation licenses anything. The fixture is the input under test, so a `Mechanism` read out of revert-payload bytes is reported and never falsifies a hypothesis; a derived quantity (the Rex7 destroyed remainder) needs an independent witness rather than certifying itself. +- Frame evidence is admissible only from a rerun that reproduced the run it stands in for: each inspected rerun is compared against its own plain outcome over every quantity but the frames it was run to collect, and a rerun that moved anything has its evidence discarded with the plain verdict left standing. - A differential run is defined for exactly one spec pair, the one whose precision invariant the classifier encodes (`DiffSpecs::new`). There is no general two-spec comparator. - A unit is a family of transactions, one per vector its `post` names (`TestUnit::vectors`). Diff, fill and bench each enumerate them; nothing takes index `{0,0,0}` and calls it the unit. - The transaction vector is the unit of counting everywhere (`FillReport::vectors`, `diff_test_suite`, validation's judged count), so one corpus produces one total whichever mode swept it. @@ -32,6 +33,7 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. ## ANTI-PATTERNS - Do not explain a differential disagreement with a fixture allowlist; add a `Mechanism` that reads the evidence instead, and state which hypothesis it falsifies. +- Do not judge a difference on an inspected rerun's frames without first checking the rerun against its plain run; an inspector that changed the execution can explain the very difference it introduced. - Do not let a `Mechanism` inferred from bytes the fixture could have written falsify a hypothesis; `Mechanism::provenance` records where an observation came from and the licensing rule follows it. - Do not classify a halt by matching its `Debug` rendering; match the `MegaHaltReason` variants with no catch-all arm, so a new variant has to be decided rather than defaulted. - Do not drop an entry the fixture-discovery walk could not read; an unreadable directory is a hole in coverage, not an empty one. @@ -48,6 +50,7 @@ The `state-test` CLI (`crates/state-test`) is a thin front-end over this crate. - Change validation semantics for roots/output/exception: `runner.rs::{validate_exception,validate_output,check_evm_execution}`. - Change worker behavior or fail-fast policy: `runner.rs::{run_test_worker,run,TestRunnerConfig}`. - Change what a differential run compares or what licenses a difference: `diff.rs::{DiffField,Mechanism,Provenance,halt_kind,judge}`. +- Change when frame evidence may decide a difference: `diff.rs::{judge_with_frame_evidence,rerun_drift}`. - Change which spec pair a differential run accepts: `diff.rs::DiffSpecs::new`. - Change how a unit's transaction vectors are enumerated: `types/test_unit.rs::TestUnit::vectors`. - Change what a fill records per unit or reports per vector: `runner.rs::{fill_unit,fill_suite,FillReport}`. diff --git a/crates/mega-state-test/src/diff.rs b/crates/mega-state-test/src/diff.rs index 6917b88b..8d029be1 100644 --- a/crates/mega-state-test/src/diff.rs +++ b/crates/mega-state-test/src/diff.rs @@ -44,6 +44,12 @@ //! and would change the execution under observation. So a difference that only a guard rejection //! explains is reported for a human rather than licensed, which is the safe direction: the gate //! over-reports instead of granting an exemption on the strength of bytes the fixture chose. +//! +//! Frame-level evidence is collected by a second, inspected pair of runs, and carries one further +//! condition: each rerun must reproduce the plain run it stands in for, quantity by quantity, +//! before its frames may decide anything. An inspector that moved the execution produced frames +//! that describe a different transaction, and letting those frames license the plain pair's +//! difference is how an observation-path regression explains itself. use crate::{ chaos::{CallbackCounter, ChaosInspector, ChaosTally, ShapeFilter}, @@ -641,28 +647,122 @@ pub fn diff_unit( // Stage two. The cheap evidence found nothing, so re-run both sides with the frame inspector, // which sees the frames the transaction's own result hides. It costs an inspected execution - // only for the units that reach here, instead of on every unit in the corpus. - let (Ok(target), Ok(base)) = ( + // only for the units that reach here, instead of on every unit in the corpus. The plain + // outcomes stay alive: they are what each rerun has to reproduce before its frames count. + let reruns = ( execute_unit_outcome(unit, indexes, &target_spec, true), execute_unit_outcome(unit, indexes, &base_spec, true), - ) else { + ); + judge_with_frame_evidence(verdict, &target, &base, reruns) +} + +/// Re-decides an unexplained difference on two inspected reruns' frame evidence — but only once +/// each rerun has shown that it reproduced the plain run it stands in for. +/// +/// The inspector attached to a rerun is supposed to observe and change nothing, and the one this +/// crate attaches implements a single read-only callback. "Supposed to" is not a check, though, +/// and a rerun that executed differently answers a different question: its frames describe an +/// execution that did not happen, and can license a difference that execution never produced. An +/// observation-path regression that introduces an inner exceptional halt on the target while the +/// compute-gas difference survives is exactly that shape — the regression would explain itself +/// and clear the nightly gate, hiding both itself and the difference it was called in to judge. +/// +/// So each side's rerun is compared against its own plain outcome first, over every quantity but +/// the frames the rerun exists to collect ([`rerun_drift`]). Any movement discards the evidence +/// and leaves the plain verdict standing, which keeps the difference flagged: +/// +/// | target rerun | base rerun | verdict | +/// | --------------- | ---------- | ------------------------------------------------------------- | +/// | did not execute | either | the plain verdict, unchanged: no evidence was collected | +/// | reproduced | reproduced | judged on the inspected pair, whose frames are admissible | +/// | moved | reproduced | the plain verdict, detail naming what moved on the target | +/// | reproduced | moved | the plain verdict, detail naming what moved on the base | +/// | moved | moved | the plain verdict, detail naming both sides' moved quantities | +fn judge_with_frame_evidence( + verdict: UnitDiffOutcome, + plain_target: &SpecOutcome, + plain_base: &SpecOutcome, + reruns: (Result, Result), +) -> UnitDiffOutcome { + // A rerun that did not execute collected nothing; there is no evidence to admit or refuse. + let (Ok(target), Ok(base)) = reruns else { return verdict; }; - let inspected_fields = compare(&target, &base); - if inspected_fields.is_empty() { - // The inspected pair agrees where the uninspected pair did not: the inspector moved the - // execution, so its evidence does not describe the difference under judgement. - return UnitDiffOutcome { - detail: Some( - "uninspected runs disagreed but inspected runs agreed; frame evidence discarded" - .to_string(), - ), - ..verdict - }; + + let drifted: Vec = + [("target", rerun_drift(plain_target, &target)), ("base", rerun_drift(plain_base, &base))] + .into_iter() + .filter(|(_, moved)| !moved.is_empty()) + .map(|(side, moved)| { + format!("frame inspector moved the {side} execution: {}", moved.join(", ")) + }) + .collect(); + if !drifted.is_empty() { + let mut detail: Vec = verdict.detail.iter().cloned().collect(); + detail.extend(drifted); + detail.push("frame evidence discarded".to_string()); + return UnitDiffOutcome { detail: Some(detail.join("; ")), ..verdict }; } + + let inspected_fields = compare(&target, &base); + // Each rerun equals its plain run quantity by quantity, so the inspected pair disagrees on + // exactly the quantities the plain pair did — the set the caller already judged, and never + // an empty one. What the reruns add is the frame evidence the outcomes now carry. + debug_assert_eq!( + inspected_fields, verdict.fields, + "reruns that reproduced both plain outcomes must disagree on the same quantities" + ); judge(&inspected_fields, &target, &base) } +/// The quantities on which an inspected rerun departed from the plain run it stands in for. +/// +/// Naming every field of the outcome rather than defaulting is deliberate: a quantity added to +/// [`SpecOutcome`] later is a compile error here instead of a silent hole in the check. Two +/// groups are compared for two reasons — [`compare`]'s ten are what the precision invariant +/// holds the specs to, and the rest are what [`judge`] reads off an outcome to decide whether a +/// mechanism was observed, which a rerun could move while leaving the compared ten alone. +/// [`SpecOutcome::frames`] is the single exclusion: collecting it is what the rerun is for. +fn rerun_drift(plain: &SpecOutcome, inspected: &SpecOutcome) -> Vec<&'static str> { + let SpecOutcome { + // Decided by `compare`. + state_root: _, + logs_root: _, + gas_used: _, + status: _, + halt_reason: _, + output: _, + compute_gas_used: _, + data_size: _, + kv_updates: _, + state_growth: _, + // Evidence read straight off the outcome. + halt_kind, + compute_gas_destroyed, + compute_gas_enforced, + rescued_gas, + detained_limit, + volatile_access, + // What the rerun exists to collect. + frames: _, + } = inspected; + + let mut moved: Vec<&'static str> = + compare(plain, inspected).iter().map(|f| f.label()).collect(); + let mut push = |differs: bool, label: &'static str| { + if differs { + moved.push(label); + } + }; + push(*halt_kind != plain.halt_kind, "halt_kind"); + push(*compute_gas_destroyed != plain.compute_gas_destroyed, "compute_gas_destroyed"); + push(*compute_gas_enforced != plain.compute_gas_enforced, "compute_gas_enforced"); + push(*rescued_gas != plain.rescued_gas, "rescued_gas"); + push(*detained_limit != plain.detained_limit, "detained_limit"); + push(*volatile_access != plain.volatile_access, "volatile_access"); + moved +} + /// The verdict body of [`UnitDiff`], before the unit's name and path are attached. #[derive(Debug, Clone)] pub struct UnitDiffOutcome { @@ -1552,6 +1652,181 @@ mod tests { assert!(verdict.mechanisms.contains(&Mechanism::ExceptionalHalt)); } + /// The plain pair a stage-two rerun is called in to settle: the target reports 700 more + /// compute gas than the base, with nothing in either outcome to license it. + fn unexplained_pair() -> (SpecOutcome, SpecOutcome, UnitDiffOutcome) { + let base = quiet(); + let mut target = base.clone(); + target.compute_gas_used += 700; + let verdict = judge(&compare(&target, &base), &target, &base); + assert_eq!(verdict.class, DiffClass::Unexplained); + (target, base, verdict) + } + + // The admissible case: both reruns reproduce their plain run, so the inner halt only the + // frames can see is the execution's own and settles the difference. + #[test] + fn test_reruns_that_reproduce_their_plain_run_supply_admissible_evidence() { + let (target, base, verdict) = unexplained_pair(); + let inspected_target = SpecOutcome { frames: halted_frames(1), ..target.clone() }; + let inspected_base = SpecOutcome { frames: Some(FrameEvidence::default()), ..base.clone() }; + + let settled = judge_with_frame_evidence( + verdict, + &target, + &base, + (Ok(inspected_target), Ok(inspected_base)), + ); + assert_eq!(settled.class, DiffClass::Explained); + assert!(settled.mechanisms.contains(&Mechanism::ExceptionalHalt)); + assert_eq!(settled.fields, vec![DiffField::ComputeGasUsed]); + } + + // The regression this gate exists for: the inspector itself introduces the inner halt and + // moves the target's numbers. The frames now describe an execution that did not happen, so + // they explain nothing and the plain verdict stands. + #[test] + fn test_a_target_rerun_that_moved_has_its_frame_evidence_discarded() { + let (target, base, verdict) = unexplained_pair(); + let mut inspected_target = target.clone(); + inspected_target.frames = halted_frames(1); + inspected_target.compute_gas_used += 5_000; + inspected_target.compute_gas_destroyed = 5_000; + let inspected_base = SpecOutcome { frames: Some(FrameEvidence::default()), ..base.clone() }; + + let settled = judge_with_frame_evidence( + verdict, + &target, + &base, + (Ok(inspected_target), Ok(inspected_base)), + ); + assert_eq!(settled.class, DiffClass::Unexplained); + assert!( + !settled.mechanisms.contains(&Mechanism::ExceptionalHalt), + "a discarded rerun contributes no mechanism: {:?}", + settled.mechanisms + ); + let detail = settled.detail.unwrap_or_default(); + assert!( + detail.contains("frame inspector moved the target execution: compute_gas_used"), + "detail should name the side that moved: {detail}" + ); + assert!( + detail.contains("compute_gas_used, compute_gas_destroyed"), + "detail should list every quantity that moved: {detail}" + ); + assert!(!detail.contains("base execution"), "the base rerun did not move: {detail}"); + assert!(detail.ends_with("frame evidence discarded"), "{detail}"); + } + + // The same rule on the other side, and over a quantity `compare` does not look at: a rerun + // that only moved the evidence `judge` reads is still a rerun of a different execution. + #[test] + fn test_a_base_rerun_that_moved_has_its_frame_evidence_discarded() { + let (target, base, verdict) = unexplained_pair(); + let inspected_target = SpecOutcome { frames: halted_frames(1), ..target.clone() }; + let mut inspected_base = base.clone(); + inspected_base.frames = Some(FrameEvidence::default()); + inspected_base.rescued_gas = 4_200; + + let settled = judge_with_frame_evidence( + verdict, + &target, + &base, + (Ok(inspected_target), Ok(inspected_base)), + ); + assert_eq!(settled.class, DiffClass::Unexplained); + assert!( + !settled.mechanisms.contains(&Mechanism::GasRescued), + "a discarded rerun contributes no mechanism: {:?}", + settled.mechanisms + ); + let detail = settled.detail.unwrap_or_default(); + assert!( + detail.contains("frame inspector moved the base execution: rescued_gas"), + "detail should name the side and its moved quantity: {detail}" + ); + assert!(!detail.contains("target execution"), "the target rerun did not move: {detail}"); + } + + // Both sides moving is reported as both, not as whichever was checked first. + #[test] + fn test_both_reruns_moving_names_both_sides() { + let (target, base, verdict) = unexplained_pair(); + let mut inspected_target = target.clone(); + inspected_target.frames = halted_frames(1); + inspected_target.state_root = B256::repeat_byte(7); + let mut inspected_base = base.clone(); + inspected_base.frames = Some(FrameEvidence::default()); + inspected_base.detained_limit = Some(50_000); + + let settled = judge_with_frame_evidence( + verdict, + &target, + &base, + (Ok(inspected_target), Ok(inspected_base)), + ); + assert_eq!(settled.class, DiffClass::Unexplained); + let detail = settled.detail.unwrap_or_default(); + assert!( + detail.contains("frame inspector moved the target execution: state_root") && + detail.contains("frame inspector moved the base execution: detained_limit"), + "both sides moved and both should be named: {detail}" + ); + } + + // A rerun that never executed collected no frames at all, which is not evidence of drift and + // not evidence of anything else: the plain verdict is returned untouched. + #[test] + fn test_a_rerun_that_did_not_execute_leaves_the_plain_verdict_alone() { + let (target, base, verdict) = unexplained_pair(); + let inspected_base = SpecOutcome { frames: Some(FrameEvidence::default()), ..base.clone() }; + let settled = judge_with_frame_evidence( + verdict.clone(), + &target, + &base, + (Err(TestErrorKind::FixtureError("rerun declined".to_string())), Ok(inspected_base)), + ); + assert_eq!(settled.class, DiffClass::Unexplained); + assert_eq!(settled.fields, verdict.fields); + assert_eq!(settled.detail, verdict.detail); + } + + /// A quantity outside `compare`'s ten to disturb in a rerun, and its drift label. + type DriftProbe = (&'static str, fn(&mut SpecOutcome)); + + // The drift check covers the whole outcome but the frames, so a rerun that differs only by + // the evidence it was run to collect reads as a faithful rerun. + #[test] + fn test_only_the_collected_frames_may_differ_between_a_run_and_its_rerun() { + let plain = quiet(); + assert!(rerun_drift(&plain, &plain).is_empty(), "a run must reproduce itself"); + let inspected = SpecOutcome { frames: halted_frames(3), ..plain.clone() }; + assert!( + rerun_drift(&plain, &inspected).is_empty(), + "collecting frame evidence is what the rerun is for" + ); + + let probes: [DriftProbe; 6] = [ + ("halt_kind", |o| o.halt_kind = Some(HaltKind::Other)), + ("compute_gas_destroyed", |o| o.compute_gas_destroyed += 1), + ("compute_gas_enforced", |o| o.compute_gas_enforced += 1), + ("rescued_gas", |o| o.rescued_gas += 1), + ("detained_limit", |o| o.detained_limit = Some(1)), + ("volatile_access", |o| o.volatile_access = 1), + ]; + for (label, mutate) in probes { + let mut moved = plain.clone(); + mutate(&mut moved); + assert_eq!(rerun_drift(&plain, &moved), vec![label]); + } + // And the ten `compare` decides, reported under their own labels. + let mut moved = plain.clone(); + moved.gas_used += 1; + moved.kv_updates += 1; + assert_eq!(rerun_drift(&plain, &moved), vec!["gas_used", "kv_updates"]); + } + // Which halts count as a crossed resource limit is read off the typed reason, variant by // variant. Every `MegaHaltReason` gets a row: the four metering halts and the detention halt // are limits, the inherited EVM's halts and `SystemTxInvalidCallee` are not. The rule this