#[cfg(not(feature = "std"))]
use alloc as std;
use std::{rc::Rc, vec::Vec};
use alloy_consensus::{Signed, Transaction as AlloyTransaction, TxLegacy};
use alloy_evm::{Database as AlloyDatabase, Evm};
use alloy_primitives::{Address, Bytes, Log, TxKind, U256};
use alloy_sol_types::SolCall;
use mega_system_contracts::keyless_deploy::IKeylessDeploy;
use op_revm::{handler::IsTxError, L1BlockInfo};
use revm::{
context::{
result::{ExecutionResult, ResultAndState},
BlockEnv, Cfg, ContextTr, TxEnv,
},
context_interface::Transaction,
handler::FrameResult,
interpreter::{CallOutcome, Gas, Host, InstructionResult, InterpreterResult},
primitives::KECCAK_EMPTY,
state::{AccountInfo, EvmState},
Database as RevmDatabase,
};
use tracing::{error, warn};
use crate::{
constants, mark_frame_result_as_exceeding_limit, AdditionalLimit, EvmTxRuntimeLimits,
ExternalEnvTypes, JournalInspectTr, LimitCheck, LimitUsage, MegaContext, MegaEvm,
MegaHaltReason, MegaSpecId, MegaTransaction, TxRuntimeLimit, VolatileDataAccess,
SANDBOX_TX_SOURCE_HASH,
};
use super::{
state_merge::apply_sandbox_state,
tx::{calculate_keyless_deploy_address, decode_keyless_tx, recover_signer},
};
use super::{
error::{encode_error_result, KeylessDeployError},
state::SandboxDb,
};
pub fn execute_keyless_deploy_call<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &mut MegaContext<DB, ExtEnvs>,
call_inputs: &revm::interpreter::CallInputs,
tx_bytes: &Bytes,
gas_limit_override: U256,
) -> FrameResult {
let mut gas = Gas::new(call_inputs.gas_limit);
let return_memory_offset = call_inputs.return_memory_offset.clone();
macro_rules! make_halt {
() => {
oog_frame_result(gas.limit(), &return_memory_offset)
};
}
macro_rules! make_error {
($error:expr) => {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(InstructionResult::Revert, encode_error_result($error), gas),
return_memory_offset,
))
};
}
macro_rules! make_success {
($gas_used:expr, $deployed_address:expr) => {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return,
IKeylessDeploy::keylessDeployCall::abi_encode_returns(
&IKeylessDeploy::keylessDeployReturn {
gasUsed: $gas_used,
deployedAddress: $deployed_address,
errorData: Bytes::new(),
},
)
.into(),
gas,
),
return_memory_offset,
))
};
}
macro_rules! make_execution_failure {
($gas_used:expr, $error:expr) => {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::Return, IKeylessDeploy::keylessDeployCall::abi_encode_returns(
&IKeylessDeploy::keylessDeployReturn {
gasUsed: $gas_used,
deployedAddress: Address::ZERO,
errorData: encode_error_result($error).to_vec().into(),
},
)
.into(),
gas,
),
return_memory_offset,
))
};
}
let cost = constants::rex2::KEYLESS_DEPLOY_OVERHEAD_GAS;
let has_sufficient_gas = gas.record_cost(cost);
if !has_sufficient_gas {
return make_halt!();
}
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 {
make_error!(KeylessDeployError::InsufficientComputeGas { limit, used })
} else {
let mut result = make_halt!();
mark_frame_result_as_exceeding_limit(
&mut result,
crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT,
Default::default(),
);
result
};
}
}
if !call_inputs.value.get().is_zero() {
return make_error!(KeylessDeployError::NoEtherTransfer);
}
let keyless_tx = match decode_keyless_tx(tx_bytes, ctx.spec) {
Ok(tx) => tx,
Err(e) => return make_error!(e),
};
if keyless_tx.nonce() != 0 {
return make_error!(KeylessDeployError::NonZeroTxNonce { tx_nonce: keyless_tx.nonce() });
}
if ctx.spec.is_enabled(MegaSpecId::REX5) {
let max = ctx.cfg().max_initcode_size();
let size = keyless_tx.input().len();
if size > max {
return make_error!(KeylessDeployError::InitCodeTooLarge {
size: size as u64,
max: max as u64,
});
}
}
let tx_gas_limit = keyless_tx.gas_limit();
let mut gas_limit_override_u64: u64 = gas_limit_override.try_into().unwrap_or(u64::MAX);
if gas_limit_override_u64 < tx_gas_limit {
return make_error!(KeylessDeployError::GasLimitTooLow {
tx_gas_limit,
provided_gas_limit: gas_limit_override_u64,
});
}
let deploy_signer = match recover_signer(&keyless_tx) {
Ok(addr) => addr,
Err(e) => return make_error!(e),
};
let deploy_address = calculate_keyless_deploy_address(deploy_signer);
let rex5_signer_info = if ctx.spec.is_enabled(MegaSpecId::REX5) {
match inspect_signer_parent_state(ctx, deploy_signer) {
Ok(info) => Some(info),
Err(e) => return make_error!(e),
}
} else {
None
};
let signer_nonce = if let Some(info) = &rex5_signer_info {
info.nonce
} else {
match get_account_nonce(ctx, deploy_signer) {
Ok(nonce) => nonce,
Err(e) => return make_error!(e),
}
};
if signer_nonce > 1 {
return make_error!(KeylessDeployError::SignerNonceTooHigh { signer_nonce });
}
if let Some(signer_info) = &rex5_signer_info {
if let Err(e) = validate_signer_code(ctx, signer_info) {
return make_error!(e);
}
match charge_caller_materialization_pre_sandbox(
ctx,
&mut gas,
deploy_signer,
signer_info,
&return_memory_offset,
) {
Err(e) => return make_error!(e),
Ok(Some(halt)) => return halt,
Ok(None) => {}
}
}
if ctx.spec.is_enabled(MegaSpecId::REX5) {
gas_limit_override_u64 = gas_limit_override_u64.min(gas.remaining());
if gas_limit_override_u64 < tx_gas_limit {
return make_error!(KeylessDeployError::GasLimitTooLow {
tx_gas_limit,
provided_gas_limit: gas_limit_override_u64,
});
}
}
let sandbox_tx = if ctx.spec.is_enabled(MegaSpecId::REX5) {
build_fee_free_sandbox_deposit_tx(
deploy_signer,
&keyless_tx,
tx_bytes,
gas_limit_override_u64,
)
} else {
let tx = TxEnv {
caller: deploy_signer,
kind: TxKind::Create,
data: keyless_tx.input().clone(),
value: keyless_tx.value(),
gas_limit: gas_limit_override_u64,
gas_price: keyless_tx.effective_gas_price(None),
nonce: 0,
..Default::default()
};
let mut mega_tx = MegaTransaction::new(tx);
mega_tx.enveloped_tx = Some(tx_bytes.clone());
mega_tx
};
{
let deploy_account = ctx.journal_mut().database.basic(deploy_address).map_err(|e| {
error!(
error = %e,
deploy_address = ?deploy_address,
"keyless deploy deploy-address state read failed",
);
KeylessDeployError::InternalError
});
match deploy_account {
Ok(Some(info)) if info.code_hash != KECCAK_EMPTY => {
return make_error!(KeylessDeployError::ContractAlreadyExists);
}
Err(e) => return make_error!(e),
_ => {}
}
}
let sandbox_tx_limits =
ctx.spec.is_enabled(MegaSpecId::REX5).then(|| sandbox_runtime_limits(ctx));
if let Some(error) = sandbox_intrinsic_overflow_error(ctx.spec, &sandbox_tx, sandbox_tx_limits)
{
return make_error!(error);
}
if ctx.spec.is_enabled(MegaSpecId::REX5) {
let ok = gas.record_cost(gas_limit_override_u64);
debug_assert!(
ok,
"Rex5+ sandbox pre-debit must succeed: gas_limit_override is capped to gas.remaining()",
);
}
match execute_keyless_deploy_sandbox(ctx, sandbox_tx, sandbox_tx_limits) {
SandboxOutcome::Completed { state, completion, limit_usage, volatile_accesses } => {
let gas_used = completion.gas_used();
if ctx.spec.is_enabled(MegaSpecId::REX5) {
if let Some(halt) = apply_sandbox_post_accounting(
ctx,
&mut gas,
limit_usage,
volatile_accesses,
gas_limit_override_u64,
gas_used,
&return_memory_offset,
) {
return halt;
}
}
if let Err(e) = apply_sandbox_state(ctx, state, deploy_signer) {
return make_error!(e);
}
match completion {
SandboxCompletion::Deployed { gas_used, deploy_address: deployed, logs } => {
if deployed != deploy_address {
return make_error!(KeylessDeployError::AddressMismatch);
}
for log in logs {
ctx.log(log);
}
make_success!(gas_used, deployed)
}
SandboxCompletion::EmptyCode { gas_used, logs } => {
for log in logs {
ctx.log(log);
}
make_execution_failure!(
gas_used,
KeylessDeployError::EmptyCodeDeployed { gas_used }
)
}
SandboxCompletion::ExecutionFailed { gas_used, error } => {
make_execution_failure!(gas_used, error)
}
}
}
SandboxOutcome::Rejected(e) => {
if ctx.spec.is_enabled(MegaSpecId::REX5) {
gas.erase_cost(gas_limit_override_u64);
}
make_error!(e)
}
}
}
fn build_fee_free_sandbox_deposit_tx(
deploy_signer: Address,
keyless_tx: &Signed<TxLegacy>,
raw_tx_bytes: &Bytes,
gas_limit: u64,
) -> MegaTransaction {
let tx = TxEnv {
caller: deploy_signer,
kind: TxKind::Create,
data: keyless_tx.input().clone(),
value: keyless_tx.value(),
gas_limit,
gas_price: 0,
nonce: 0,
..Default::default()
};
let mut mega_tx = MegaTransaction::new(tx);
mega_tx.enveloped_tx = Some(raw_tx_bytes.clone());
mega_tx.deposit.source_hash = SANDBOX_TX_SOURCE_HASH;
mega_tx.deposit.mint = None;
mega_tx
}
pub(crate) fn execute_keyless_deploy_sandbox<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &mut MegaContext<DB, ExtEnvs>,
sandbox_tx: MegaTransaction,
sandbox_tx_limits: Option<EvmTxRuntimeLimits>,
) -> SandboxOutcome {
let deploy_signer = sandbox_tx.caller();
let gas_limit = sandbox_tx.gas_limit();
let gas_price = sandbox_tx.gas_price();
let value = sandbox_tx.value();
let mega_spec = ctx.mega_spec();
let block = ctx.block().clone();
let chain = ctx.chain().clone();
let shared_external_envs = mega_spec
.is_enabled(MegaSpecId::REX4)
.then(|| (Rc::clone(&ctx.salt_env), Rc::clone(&ctx.oracle_env)));
let journal = ctx.journal_mut();
let mut sandbox_db = SandboxDb::new(&journal.inner.state, &mut journal.database)
.with_nonce_override(deploy_signer);
let signer_account = match sandbox_db.basic(deploy_signer) {
Ok(info) => info.unwrap_or_default(),
Err(e) => {
error!(
error = %e,
deploy_signer = ?deploy_signer,
"keyless deploy signer balance read failed",
);
return SandboxOutcome::Rejected(KeylessDeployError::InternalError);
}
};
let total_cost = if mega_spec.is_enabled(MegaSpecId::REX5) {
value
} else {
let gas_cost = U256::from(gas_limit) * U256::from(gas_price);
match gas_cost.checked_add(value) {
Some(total) => total,
None => return SandboxOutcome::Rejected(KeylessDeployError::InsufficientBalance),
}
};
if signer_account.balance < total_cost {
return SandboxOutcome::Rejected(KeylessDeployError::InsufficientBalance);
}
if let Some((salt_env, oracle_env)) = shared_external_envs {
let sandbox_ctx = MegaContext::<_, ExtEnvs>::new_with_shared_ext_envs(
sandbox_db, mega_spec, salt_env, oracle_env,
);
run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain)
} else {
let sandbox_ctx = MegaContext::new(sandbox_db, mega_spec);
run_sandbox_ctx(sandbox_ctx, sandbox_tx, sandbox_tx_limits, block, chain)
}
}
fn run_sandbox_ctx<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
sandbox_ctx: MegaContext<DB, ExtEnvs>,
sandbox_tx: MegaTransaction,
sandbox_tx_limits: Option<EvmTxRuntimeLimits>,
block: BlockEnv,
chain: L1BlockInfo,
) -> SandboxOutcome {
let sandbox_ctx = match sandbox_tx_limits {
Some(limits) => sandbox_ctx.with_tx_runtime_limits(limits),
None => sandbox_ctx,
};
let sandbox_ctx = sandbox_ctx.with_block(block).with_chain(chain).with_inside_sandbox(true);
let is_rex5_enabled = sandbox_ctx.mega_spec().is_enabled(MegaSpecId::REX5);
let mut sandbox_evm = MegaEvm::new(sandbox_ctx);
let result = sandbox_evm.transact_raw(sandbox_tx);
let limit_usage = sandbox_evm.ctx.additional_limit.borrow().get_usage();
let volatile_accesses =
sandbox_evm.ctx.volatile_data_tracker.borrow().get_volatile_data_accessed();
process_sandbox_transact_result(result, limit_usage, volatile_accesses, is_rex5_enabled)
}
#[derive(Debug)]
pub enum SandboxOutcome {
Completed {
state: EvmState,
completion: SandboxCompletion,
limit_usage: LimitUsage,
volatile_accesses: VolatileDataAccess,
},
Rejected(KeylessDeployError),
}
#[derive(Debug)]
pub enum SandboxCompletion {
Deployed {
gas_used: u64,
deploy_address: Address,
logs: Vec<Log>,
},
EmptyCode {
gas_used: u64,
logs: Vec<Log>,
},
ExecutionFailed {
gas_used: u64,
error: KeylessDeployError,
},
}
impl SandboxCompletion {
pub(crate) fn gas_used(&self) -> u64 {
match self {
Self::Deployed { gas_used, .. } |
Self::EmptyCode { gas_used, .. } |
Self::ExecutionFailed { gas_used, .. } => *gas_used,
}
}
}
fn process_sandbox_transact_result<E: core::fmt::Display + IsTxError>(
result: Result<ResultAndState<MegaHaltReason>, E>,
limit_usage: LimitUsage,
volatile_accesses: VolatileDataAccess,
is_rex5_enabled: bool,
) -> SandboxOutcome {
let completed = |state, completion| SandboxOutcome::Completed {
state,
completion,
limit_usage,
volatile_accesses,
};
match result {
Ok(ResultAndState { result: exec_result, state: sandbox_state }) => match exec_result {
ExecutionResult::Success { gas_used, output, logs, .. } => {
let revm::context::result::Output::Create(bytecode, Some(created_addr)) = output
else {
return SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated);
};
if bytecode.is_empty() {
if is_rex5_enabled {
return completed(
sandbox_state,
SandboxCompletion::EmptyCode { gas_used, logs },
);
}
return completed(
sandbox_state,
SandboxCompletion::ExecutionFailed {
gas_used,
error: KeylessDeployError::EmptyCodeDeployed { gas_used },
},
);
}
completed(
sandbox_state,
SandboxCompletion::Deployed { gas_used, deploy_address: created_addr, logs },
)
}
ExecutionResult::Revert { gas_used, output } => completed(
sandbox_state,
SandboxCompletion::ExecutionFailed {
gas_used,
error: KeylessDeployError::ExecutionReverted { gas_used, output },
},
),
ExecutionResult::Halt { gas_used, reason } => {
if matches!(reason, MegaHaltReason::Base(op_revm::OpHaltReason::FailedDeposit)) {
warn!(gas_used, "keyless deploy sandbox failed deposit (validation-reject)",);
return SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction);
}
warn!(
reason = ?reason,
gas_used,
"keyless deploy sandbox halted",
);
completed(
sandbox_state,
SandboxCompletion::ExecutionFailed {
gas_used,
error: KeylessDeployError::ExecutionHalted { gas_used, reason },
},
)
}
},
Err(e) if e.is_tx_error() => {
warn!(
error = %e,
"keyless deploy sandbox transaction rejected during validation",
);
SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction)
}
Err(e) => {
error!(
error = %e,
"keyless deploy sandbox failed with internal error",
);
SandboxOutcome::Rejected(KeylessDeployError::InternalError)
}
}
}
fn apply_sandbox_post_accounting<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
gas: &mut Gas,
limit_usage: LimitUsage,
volatile_accesses: VolatileDataAccess,
reservation: u64,
sandbox_gas_used: u64,
return_memory_offset: &core::ops::Range<usize>,
) -> Option<FrameResult> {
merge_sandbox_limit_usage(ctx, limit_usage);
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)
}
fn charge_caller_materialization_pre_sandbox<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
gas: &mut Gas,
deploy_signer: Address,
signer_info: &AccountInfo,
return_memory_offset: &core::ops::Range<usize>,
) -> Result<Option<FrameResult>, KeylessDeployError> {
if !signer_info.is_empty() {
return Ok(None);
}
let caller_storage_gas =
ctx.dynamic_storage_gas_cost.borrow_mut().new_account_gas(deploy_signer).map_err(|e| {
error!(
error = %e,
deploy_signer = ?deploy_signer,
"pre-sandbox caller storage gas computation failed",
);
KeylessDeployError::InternalError
})?;
if !gas.record_cost(caller_storage_gas) {
return Ok(Some(oog_frame_result(gas.limit(), return_memory_offset)));
}
ctx.additional_limit.borrow_mut().record_deposit_caller_creation();
if let Some(halt) = reject_if_tx_limit_overflow(ctx, gas, return_memory_offset) {
return Ok(Some(halt));
}
Ok(None)
}
fn merge_sandbox_limit_usage<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
limit_usage: LimitUsage,
) {
ctx.additional_limit.borrow_mut().merge_usage(limit_usage);
}
fn refund_unused_sandbox_gas(gas: &mut Gas, reservation: u64, sandbox_gas_used: u64) {
let unused = reservation.saturating_sub(sandbox_gas_used);
if unused > 0 {
gas.erase_cost(unused);
}
}
fn reject_if_tx_limit_overflow<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
gas: &Gas,
return_memory_offset: &core::ops::Range<usize>,
) -> Option<FrameResult> {
let mut limit = ctx.additional_limit.borrow_mut();
let limit_check = limit.check_limit();
if !limit_check.exceeded_limit() || limit_check.is_frame_local() {
return None;
}
limit.rescue_gas(gas);
let mut result = oog_frame_result(gas.limit(), return_memory_offset);
mark_frame_result_as_exceeding_limit(
&mut result,
crate::AdditionalLimit::EXCEEDING_LIMIT_INSTRUCTION_RESULT,
Default::default(),
);
Some(result)
}
fn oog_frame_result(gas_limit: u64, return_memory_offset: &core::ops::Range<usize>) -> FrameResult {
FrameResult::Call(CallOutcome::new(
InterpreterResult::new(
InstructionResult::OutOfGas,
Bytes::new(),
Gas::new_spent(gas_limit),
),
return_memory_offset.clone(),
))
}
fn sandbox_intrinsic_overflow_error(
spec: MegaSpecId,
sandbox_tx: &MegaTransaction,
sandbox_tx_limits: Option<EvmTxRuntimeLimits>,
) -> Option<KeylessDeployError> {
let limits = sandbox_tx_limits?;
match AdditionalLimit::intrinsic_check_for_tx(spec, sandbox_tx, limits) {
LimitCheck::ExceedsLimit { kind, limit, used, .. } => {
Some(KeylessDeployError::ParentBudgetExceeded { kind, limit, used })
}
LimitCheck::WithinLimit => None,
}
}
fn sandbox_runtime_limits<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
) -> EvmTxRuntimeLimits {
let parent_limit = ctx.additional_limit.borrow();
let limits = parent_limit.limits;
limits
.with_tx_compute_gas_limit(parent_limit.current_call_remaining_compute_gas())
.with_tx_data_size_limit(parent_limit.current_call_remaining_data_size())
.with_tx_kv_updates_limit(parent_limit.current_call_remaining_kv_updates())
.with_tx_state_growth_limit(parent_limit.current_call_remaining_state_growth())
}
fn get_account_nonce<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &mut MegaContext<DB, ExtEnvs>,
address: Address,
) -> Result<u64, KeylessDeployError> {
let journal = ctx.journal_mut();
if let Some(acc) = journal.state.get(&address) {
return Ok(acc.info.nonce);
}
Ok(journal
.database
.basic(address)
.map_err(|e| {
error!(
error = %e,
address = ?address,
"keyless deploy nonce read failed",
);
KeylessDeployError::InternalError
})?
.map(|info| info.nonce)
.unwrap_or(0))
}
fn inspect_signer_parent_state<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &mut MegaContext<DB, ExtEnvs>,
address: Address,
) -> Result<AccountInfo, KeylessDeployError> {
let load_code = !ctx.cfg().disable_eip3607;
let account = ctx.journal_mut().inspect_account(address, load_code).map_err(|e| {
error!(
error = %e,
address = ?address,
"keyless deploy signer parent-state inspection failed",
);
KeylessDeployError::InternalError
})?;
Ok(account.info.clone())
}
fn validate_signer_code<DB: AlloyDatabase, ExtEnvs: ExternalEnvTypes>(
ctx: &MegaContext<DB, ExtEnvs>,
signer_info: &AccountInfo,
) -> Result<(), KeylessDeployError> {
if ctx.cfg().disable_eip3607 {
return Ok(());
}
if let Some(code) = &signer_info.code {
if !code.is_empty() && !code.is_eip7702() {
return Err(KeylessDeployError::SignerHasCode);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use revm::context::result::Output;
struct FakeTxErr {
is_tx: bool,
msg: &'static str,
}
impl core::fmt::Display for FakeTxErr {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.msg)
}
}
impl IsTxError for FakeTxErr {
fn is_tx_error(&self) -> bool {
self.is_tx
}
}
#[test]
fn test_process_result_call_output_maps_to_no_contract_created() {
let result: Result<ResultAndState<MegaHaltReason>, FakeTxErr> = Ok(ResultAndState {
result: ExecutionResult::Success {
reason: revm::context::result::SuccessReason::Stop,
gas_used: 1,
gas_refunded: 0,
logs: Vec::new(),
output: Output::Call(Bytes::new()),
},
state: EvmState::default(),
});
let out = process_sandbox_transact_result(
result,
LimitUsage::default(),
VolatileDataAccess::empty(),
true,
);
assert!(
matches!(out, SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated)),
"unexpected: {out:?}",
);
}
#[test]
fn test_process_result_create_without_address_maps_to_no_contract_created() {
let result: Result<ResultAndState<MegaHaltReason>, FakeTxErr> = Ok(ResultAndState {
result: ExecutionResult::Success {
reason: revm::context::result::SuccessReason::Stop,
gas_used: 1,
gas_refunded: 0,
logs: Vec::new(),
output: Output::Create(Bytes::from_static(&[0x60, 0x00]), None),
},
state: EvmState::default(),
});
let out = process_sandbox_transact_result(
result,
LimitUsage::default(),
VolatileDataAccess::empty(),
true,
);
assert!(
matches!(out, SandboxOutcome::Rejected(KeylessDeployError::NoContractCreated)),
"unexpected: {out:?}",
);
}
#[test]
fn test_process_result_non_tx_error_maps_to_internal_error() {
let result: Result<ResultAndState<MegaHaltReason>, FakeTxErr> =
Err(FakeTxErr { is_tx: false, msg: "db blew up" });
let out = process_sandbox_transact_result(
result,
LimitUsage::default(),
VolatileDataAccess::empty(),
true,
);
assert!(
matches!(out, SandboxOutcome::Rejected(KeylessDeployError::InternalError)),
"unexpected: {out:?}",
);
}
#[test]
fn test_process_result_tx_error_maps_to_invalid_transaction() {
let result: Result<ResultAndState<MegaHaltReason>, FakeTxErr> =
Err(FakeTxErr { is_tx: true, msg: "intrinsic gas too low" });
let out = process_sandbox_transact_result(
result,
LimitUsage::default(),
VolatileDataAccess::empty(),
true,
);
assert!(
matches!(out, SandboxOutcome::Rejected(KeylessDeployError::InvalidTransaction)),
"unexpected: {out:?}",
);
}
#[test]
fn test_get_account_nonce_db_error_maps_to_internal_error() {
use crate::{
test_utils::{ErrorInjectingDatabase, MemoryDatabase},
EmptyExternalEnv,
};
use alloy_primitives::address;
let signer = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0001");
let mut db = ErrorInjectingDatabase::new(MemoryDatabase::default());
db.fail_on_account = Some(signer);
let mut ctx = MegaContext::<_, EmptyExternalEnv>::new(db, MegaSpecId::REX5);
let out = get_account_nonce(&mut ctx, signer);
assert!(matches!(out, Err(KeylessDeployError::InternalError)), "unexpected: {out:?}");
}
#[test]
fn test_get_account_nonce_returns_cached_nonce_without_touching_database() {
use crate::{
test_utils::{ErrorInjectingDatabase, MemoryDatabase},
EmptyExternalEnv,
};
use alloy_primitives::address;
use revm::{primitives::KECCAK_EMPTY, state::AccountInfo};
let signer = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0001");
let mut db = ErrorInjectingDatabase::new(MemoryDatabase::default());
db.fail_on_account = Some(signer);
let mut ctx = MegaContext::<_, EmptyExternalEnv>::new(db, MegaSpecId::REX5);
let cached_info =
AccountInfo { nonce: 7, balance: U256::ZERO, code_hash: KECCAK_EMPTY, code: None };
ctx.journal_mut().inner.state.insert(signer, cached_info.into());
let nonce = get_account_nonce(&mut ctx, signer).expect("cache hit must not error");
assert_eq!(nonce, 7, "cache-hit nonce must reflect the seeded journal entry");
}
#[test]
fn test_inspect_signer_parent_state_db_error_maps_to_internal_error() {
use crate::{
test_utils::{ErrorInjectingDatabase, MemoryDatabase},
EmptyExternalEnv,
};
use alloy_primitives::address;
let signer = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0001");
let mut db = ErrorInjectingDatabase::new(MemoryDatabase::default());
db.fail_on_account = Some(signer);
let mut ctx = MegaContext::<_, EmptyExternalEnv>::new(db, MegaSpecId::REX5);
let out = inspect_signer_parent_state(&mut ctx, signer);
assert!(
matches!(out, Err(KeylessDeployError::InternalError)),
"inspect_account DB error must map to InternalError; got {out:?}",
);
}
#[test]
fn test_inspect_signer_parent_state_skips_code_by_hash_on_vacant_when_eip3607_disabled() {
use alloy_primitives::{address, keccak256, B256};
use revm::{bytecode::Bytecode, primitives::Bytes as PrimitivesBytes};
#[derive(Debug, Default)]
struct LazyCodeSignerDb {
signer: Address,
signer_info: AccountInfo,
}
impl RevmDatabase for LazyCodeSignerDb {
type Error = core::convert::Infallible;
fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
if address == self.signer {
Ok(Some(self.signer_info.clone()))
} else {
Ok(None)
}
}
fn code_by_hash(&mut self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
panic!(
"code_by_hash must not be called on the vacant signer path when \
disable_eip3607 = true (canonical validate_account_nonce_and_code \
short-circuit)",
);
}
fn storage(&mut self, _address: Address, _index: U256) -> Result<U256, Self::Error> {
Ok(U256::ZERO)
}
fn block_hash(&mut self, _number: u64) -> Result<B256, Self::Error> {
Ok(B256::ZERO)
}
}
let signer = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0001");
let bytecode = PrimitivesBytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]);
let code_hash = keccak256(&bytecode);
let signer_info = AccountInfo { nonce: 0, balance: U256::ZERO, code_hash, code: None };
let db = LazyCodeSignerDb { signer, signer_info };
let mut ctx = MegaContext::<_, crate::EmptyExternalEnv>::new(db, MegaSpecId::REX5);
ctx.modify_cfg(|cfg| cfg.disable_eip3607 = true);
let info = inspect_signer_parent_state(&mut ctx, signer)
.expect("lookup must succeed without hydrating code");
assert_eq!(info.code_hash, code_hash, "code_hash must propagate from the DB");
assert!(
info.code.is_none(),
"disable_eip3607 = true must leave info.code as None on the vacant branch",
);
}
#[test]
fn test_validate_signer_code_disable_eip3607_short_circuits() {
use crate::{test_utils::MemoryDatabase, EmptyExternalEnv};
use revm::{
bytecode::Bytecode,
primitives::{keccak256, Bytes as PrimitivesBytes},
};
let code_bytes = PrimitivesBytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xf3]);
let code_hash = keccak256(&code_bytes);
let signer_info = AccountInfo {
nonce: 0,
balance: U256::ZERO,
code_hash,
code: Some(Bytecode::new_raw(code_bytes)),
};
let mut ctx =
MegaContext::<_, EmptyExternalEnv>::new(MemoryDatabase::default(), MegaSpecId::REX5);
ctx.modify_cfg(|cfg| cfg.disable_eip3607 = true);
let out = validate_signer_code(&ctx, &signer_info);
assert!(
matches!(out, Ok(())),
"disable_eip3607 must short-circuit before the code inspection; got {out:?}",
);
}
#[test]
fn test_charge_caller_materialization_salt_failure_returns_internal_error_without_ctx_pollution(
) {
use crate::{test_utils::MemoryDatabase, BucketId, OracleEnv, SaltEnv};
use alloy_primitives::{address, B256};
use core::fmt::{self, Display, Formatter};
#[derive(Debug, Clone, Copy)]
struct AlwaysFailSalt;
#[derive(Debug, Clone)]
struct InjectedSaltError;
impl Display for InjectedSaltError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("injected SALT failure")
}
}
impl SaltEnv for AlwaysFailSalt {
type Error = InjectedSaltError;
fn get_bucket_capacity(&self, _bucket_id: BucketId) -> Result<u64, Self::Error> {
Err(InjectedSaltError)
}
fn bucket_id_for_account(_account: Address) -> BucketId {
0
}
fn bucket_id_for_slot(_address: Address, _key: U256) -> BucketId {
0
}
}
#[derive(Debug, Clone, Copy)]
struct NoopOracle;
impl OracleEnv for NoopOracle {
fn get_oracle_storage(&self, _slot: U256) -> Option<U256> {
None
}
fn on_hint(&self, _from: Address, _topic: B256, _data: Bytes) {}
}
let signer = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0001");
let db = MemoryDatabase::default();
let envs: crate::ExternalEnvs<(AlwaysFailSalt, NoopOracle)> =
crate::ExternalEnvs { salt_env: AlwaysFailSalt, oracle_env: NoopOracle };
let mut ctx = MegaContext::new(db, MegaSpecId::REX5).with_external_envs(envs);
let mut gas = Gas::new(10_000_000);
let return_memory_offset = 0..0;
let signer_info = AccountInfo::default();
let out = charge_caller_materialization_pre_sandbox(
&ctx,
&mut gas,
signer,
&signer_info,
&return_memory_offset,
);
assert!(
matches!(out, Err(KeylessDeployError::InternalError)),
"SALT failure must surface as InternalError, got {out:?}",
);
let ctx_error = core::mem::replace(ctx.error(), Ok(()));
assert!(
matches!(ctx_error, Ok(())),
"ctx.error() must remain Ok after a SALT failure in the pre-sandbox charge; \
got {ctx_error:?}",
);
assert_eq!(
ctx.additional_limit.borrow().get_usage().state_growth,
0,
"deposit-caller state-growth must not be recorded when the storage gas computation fails",
);
}
}