Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/mega-evm/src/limit/limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 41 additions & 18 deletions crates/mega-evm/src/sandbox/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -179,23 +179,46 @@ pub fn execute_keyless_deploy_call<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>
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!();
Comment on lines +210 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate this changed REX3 result behind a new spec

For a REX3 top-level keylessDeploy whose intrinsic calldata exceeds a non-compute transaction limit, this new branch replaces the prior node abort with a consensus-visible DataLimitExceeded halt and a rescued-gas refund. AGENTS.md explicitly marks REX3 as frozen and requires new EVM behavior to be introduced behind a new spec, so applying this behavior directly to MegaSpecId::REX3 changes historical-spec execution rather than preserving it.

Useful? React with 👍 / 👎.

mark_frame_result_as_exceeding_limit(
&mut result,
crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT,
Default::default(),
);
result
}
};
}
}
Expand Down
70 changes: 70 additions & 0 deletions crates/mega-evm/tests/rex3/keyless_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<std::convert::Infallible>::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.
Expand Down