diff --git a/crates/mega-evm/src/limit/limit.rs b/crates/mega-evm/src/limit/limit.rs index 35be1a78..ee009d80 100644 --- a/crates/mega-evm/src/limit/limit.rs +++ b/crates/mega-evm/src/limit/limit.rs @@ -728,6 +728,9 @@ impl AdditionalLimit { 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); + // Self-latch any TX-level exceed, per the resource-limit protocol (like the + // sibling `record_oracle_hint_bytes` / `record_deposit_caller_creation`). + self.check_limit(); } /// Hook called when an orginally zero storage slot is written non-zero value for the first time diff --git a/crates/mega-evm/src/sandbox/execution.rs b/crates/mega-evm/src/sandbox/execution.rs index b1867813..1bef56fd 100644 --- a/crates/mega-evm/src/sandbox/execution.rs +++ b/crates/mega-evm/src/sandbox/execution.rs @@ -56,7 +56,7 @@ use tracing::{error, warn}; use crate::{ constants, mark_frame_result_as_exceeding_limit, AdditionalLimit, EvmTxRuntimeLimits, - ExternalEnvTypes, JournalInspectTr, LimitCheck, LimitUsage, MegaContext, MegaEvm, + ExternalEnvTypes, JournalInspectTr, LimitCheck, LimitKind, LimitUsage, MegaContext, MegaEvm, MegaHaltReason, MegaSpecId, MegaTransaction, TxRuntimeLimit, VolatileDataAccess, SANDBOX_TX_SOURCE_HASH, }; @@ -179,23 +179,46 @@ pub fn execute_keyless_deploy_call if ctx.spec.is_enabled(MegaSpecId::REX3) { let mut limit = ctx.additional_limit.borrow_mut(); if !limit.record_compute_gas(cost) { - let crate::LimitCheck::ExceedsLimit { limit, used, frame_local, .. } = - limit.compute_gas.check_limit() - else { - unreachable!() - }; - return if frame_local { - // Frame-local: revert; gas returns to caller. - make_error!(KeylessDeployError::InsufficientComputeGas { limit, used }) - } else { - // TX-level: halt with OOG, marked as exceeding. - let mut result = make_halt!(); - mark_frame_result_as_exceeding_limit( - &mut result, - crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT, - Default::default(), - ); - result + // `record_compute_gas` returns false on ANY latched dimension, not just + // compute. Pre-REX4 has no `frame_result_if_exceeding_limit` guard before + // interceptor dispatch, so an intrinsic non-compute overflow from + // `before_tx_start` surfaces here; use the aggregate `check_limit()` (the + // old compute-only check returned `WithinLimit` and hit `unreachable!()`). + return match limit.check_limit() { + LimitCheck::ExceedsLimit { + kind: LimitKind::ComputeGas, + limit: compute_gas_limit, + used, + frame_local: true, + } => { + // Frame-local: revert; gas returns to caller. + make_error!(KeylessDeployError::InsufficientComputeGas { + limit: compute_gas_limit, + used, + }) + } + LimitCheck::ExceedsLimit { kind: LimitKind::ComputeGas, .. } => { + // TX-level compute: preserve frozen behavior (halt, no rescue). + let mut result = make_halt!(); + mark_frame_result_as_exceeding_limit( + &mut result, + crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT, + Default::default(), + ); + result + } + _ => { + // TX-level non-compute: rescue gas here, since the interceptor + // short-circuit skips `after_frame_run`'s rescue. + limit.rescue_gas(&gas); + let mut result = make_halt!(); + mark_frame_result_as_exceeding_limit( + &mut result, + crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT, + Default::default(), + ); + result + } }; } } diff --git a/crates/mega-evm/tests/rex3/keyless_deploy.rs b/crates/mega-evm/tests/rex3/keyless_deploy.rs index 7259b7b3..9b472c8f 100644 --- a/crates/mega-evm/tests/rex3/keyless_deploy.rs +++ b/crates/mega-evm/tests/rex3/keyless_deploy.rs @@ -111,6 +111,76 @@ fn test_rex2_keyless_deploy_does_not_record_compute_gas() { ); } +/// Regression: a Rex3 keyless-deploy call whose intrinsic data size exceeds the TX +/// data-size limit must halt cleanly instead of aborting the node. +/// +/// `before_tx_start` latches `DataLimitExceeded` from the outer calldata, but pre-Rex4 +/// specs have no `frame_result_if_exceeding_limit` guard ahead of interceptor dispatch, +/// so the latch first reaches the keyless-deploy sandbox's overhead-gas charge. That +/// site used to assume the only reachable exceed was compute gas and hit an +/// `unreachable!()` (`compute_gas.check_limit()` returns `WithinLimit` on the +/// data-size path), panicking the node. +#[test] +fn test_rex3_keyless_deploy_data_size_overflow_halts_without_panic() { + let mut db = MemoryDatabase::default(); + db.set_account_balance(CREATE2_FACTORY_DEPLOYER, U256::from(1_000_000_000_000_000_000_000u128)); + + let call_data = IKeylessDeploy::keylessDeployCall { + keylessDeploymentTransaction: Bytes::from_static(CREATE2_FACTORY_TX), + gasLimitOverride: U256::from(LARGE_GAS_LIMIT_OVERRIDE), + } + .abi_encode(); + + let external_envs = TestExternalEnvs::::new(); + let mut context = + MegaContext::new(&mut db, MegaSpecId::REX3).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 = TxEnv { + caller: TEST_CALLER, + kind: TxKind::Call(KEYLESS_DEPLOY_ADDRESS), + data: call_data.into(), + value: U256::ZERO, + gas_limit: 1_000_000_000_000, + gas_price: 0, + ..Default::default() + }; + let mut tx = MegaTransaction::new(tx); + tx.enveloped_tx = Some(Bytes::new()); + + // Force the outer calldata over the data-size limit so `before_tx_start` latches + // `DataLimitExceeded` before the keyless-deploy interceptor runs. + let runtime_limits = + EvmTxRuntimeLimits::from_spec(MegaSpecId::REX3).with_tx_data_size_limit(64); + let mut evm = + MegaEvm::new(context).with_tx_runtime_limits(runtime_limits).with_inspector(NoOpInspector); + + // Before the fix this panics via `unreachable!()`; after the fix it returns a halt. + let result = alloy_evm::Evm::transact_raw(&mut evm, tx).unwrap().result; + + assert!(!result.is_success(), "expected a data-size limit halt, got: {result:?}"); + assert!( + matches!( + result, + ExecutionResult::Halt { reason: MegaHaltReason::DataLimitExceeded { .. }, .. } + ), + "expected DataLimitExceeded halt, got: {result:?}", + ); + + // The remaining gas must be rescued for refund (as on the non-intercepted TX-level + // exceed path): the sender is charged only intrinsic + keyless-deploy overhead, not + // the full gas limit. Without the rescue the interceptor short-circuit burns the + // entire 1e12 limit. + assert!( + result.gas_used() < 100_000_000, + "expected remaining gas to be rescued (gas_used well below the 1e12 limit), got {}", + result.gas_used(), + ); +} + /// Test that keyless deploy halts when compute gas limit is set below the 100K overhead. /// In Rex3, the 100K overhead is recorded as compute gas, so if the compute gas limit /// is lower than 100K, the transaction should fail.