use std::{cell::Cell, convert::Infallible};
use alloy_consensus::{Signed, TxLegacy};
use alloy_evm::{block::BlockExecutor, EvmEnv};
use alloy_op_evm::block::receipt_builder::OpAlloyReceiptBuilder;
use alloy_primitives::{address, Address, Bytes, Signature, TxKind, B256, U256};
use mega_evm::{
test_utils::{BytecodeBuilder, GasInspector, MemoryDatabase},
BlockLimits, MegaBlockExecutionCtx, MegaBlockExecutorFactory, MegaEvmFactory,
MegaHardforkConfig, MegaSpecId, MegaTxEnvelope, TestExternalEnvs,
};
use revm::{
bytecode::opcode::{ADD, CALL, GAS, PUSH0, SLOAD, SSTORE},
context::{BlockEnv, ContextTr, JournalTr},
database::State,
interpreter::{
CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, InstructionResult,
InterpreterResult, InterpreterTypes,
},
Inspector,
};
const CALLER: Address = address!("2000000000000000000000000000000000000002");
const CONTRACT: Address = address!("1000000000000000000000000000000000000001");
fn create_transaction(
nonce: u64,
gas_limit: u64,
) -> alloy_consensus::transaction::Recovered<MegaTxEnvelope> {
let tx_legacy = TxLegacy {
chain_id: Some(8453), nonce,
gas_price: 1_000_000,
gas_limit,
to: TxKind::Call(CONTRACT),
value: U256::ZERO,
input: Bytes::new(),
};
let signed = Signed::new_unchecked(tx_legacy, Signature::test_signature(), Default::default());
let tx = MegaTxEnvelope::Legacy(signed);
alloy_consensus::transaction::Recovered::new_unchecked(tx, CALLER)
}
fn create_deploy_transaction(
nonce: u64,
gas_limit: u64,
init_code: Bytes,
) -> alloy_consensus::transaction::Recovered<MegaTxEnvelope> {
let tx_legacy = TxLegacy {
chain_id: Some(8453),
nonce,
gas_price: 1_000_000,
gas_limit,
to: TxKind::Create,
value: U256::ZERO,
input: init_code,
};
let signed = Signed::new_unchecked(tx_legacy, Signature::test_signature(), Default::default());
let tx = MegaTxEnvelope::Legacy(signed);
alloy_consensus::transaction::Recovered::new_unchecked(tx, CALLER)
}
fn create_test_contract() -> Bytes {
BytecodeBuilder::default()
.append(PUSH0) .append(SLOAD) .push_number(1u8) .append(ADD) .append(PUSH0) .append(SSTORE) .stop()
.build()
}
#[test]
fn test_inspector_works_with_block_executor() {
let mut db = MemoryDatabase::default();
let bytecode = create_test_contract();
db.set_account_code(CONTRACT, bytecode);
db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000u64));
let mut state = State::builder().with_database(&mut db).build();
use alloy_hardforks::ForkCondition;
use mega_evm::MegaHardfork;
let external_envs = TestExternalEnvs::<Infallible>::new();
let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs);
let chain_spec =
MegaHardforkConfig::default().with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0));
let receipt_builder = OpAlloyReceiptBuilder::default();
let block_executor_factory =
MegaBlockExecutorFactory::new(chain_spec, evm_factory, receipt_builder);
let mut cfg_env = revm::context::CfgEnv::default();
cfg_env.spec = MegaSpecId::MINI_REX;
let block_env = BlockEnv {
number: U256::from(1000),
timestamp: U256::from(1_800_000_000),
gas_limit: 30_000_000,
..Default::default()
};
let evm_env = EvmEnv::new(cfg_env, block_env);
let block_ctx =
MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits());
let inspector = GasInspector::new();
let mut executor = block_executor_factory
.create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector);
let tx = create_transaction(0, 1_000_000);
let result = executor.execute_transaction(&tx);
assert!(result.is_ok(), "Transaction should succeed: {:?}", result.err());
let records = executor.evm().inspector.records();
assert!(!records.is_empty(), "Inspector should have recorded opcodes");
let opcodes: Vec<_> = records.iter().map(|r| r.opcode.as_str()).collect();
assert!(opcodes.contains(&"SLOAD"), "Should record SLOAD opcode");
assert!(opcodes.contains(&"ADD"), "Should record ADD opcode");
assert!(opcodes.contains(&"SSTORE"), "Should record SSTORE opcode");
for record in &records {
assert!(
record.gas_before >= record.gas_after,
"Gas should decrease or stay the same after opcode execution"
);
}
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");
}
#[derive(Default)]
struct SkipNestedCallInspector {
calls_intercepted: Cell<u32>,
call_ends: Cell<u32>,
}
impl<CTX: ContextTr, INTR: InterpreterTypes> Inspector<CTX, INTR> for SkipNestedCallInspector {
#[allow(clippy::if_then_some_else_none)] fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
let depth = context.journal().depth();
if depth > 0 {
self.calls_intercepted.set(self.calls_intercepted.get() + 1);
Some(CallOutcome {
result: InterpreterResult {
result: InstructionResult::Stop,
output: Bytes::new(),
gas: Gas::new(inputs.gas_limit),
},
memory_offset: 0..0,
})
} else {
None
}
}
fn call_end(&mut self, _context: &mut CTX, _inputs: &CallInputs, _outcome: &mut CallOutcome) {
self.call_ends.set(self.call_ends.get() + 1);
}
}
const CONTRACT_B: Address = address!("3000000000000000000000000000000000000003");
fn create_caller_contract() -> Bytes {
BytecodeBuilder::default()
.push_number(0u8) .push_number(0u8) .push_number(0u8) .push_number(0u8) .push_number(0u8) .push_address(CONTRACT_B) .append(GAS) .append(CALL)
.stop()
.build()
}
fn create_target_contract() -> Bytes {
BytecodeBuilder::default().stop().build()
}
#[test]
fn test_inspector_early_return_with_additional_limits() {
let mut db = MemoryDatabase::default();
db.set_account_code(CONTRACT, create_caller_contract());
db.set_account_code(CONTRACT_B, create_target_contract());
db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000u64));
let mut state = State::builder().with_database(&mut db).build();
use alloy_hardforks::ForkCondition;
use mega_evm::MegaHardfork;
let external_envs = TestExternalEnvs::<Infallible>::new();
let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs);
let chain_spec =
MegaHardforkConfig::default().with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0));
let receipt_builder = OpAlloyReceiptBuilder::default();
let block_executor_factory =
MegaBlockExecutorFactory::new(chain_spec, evm_factory, receipt_builder);
let mut cfg_env = revm::context::CfgEnv::default();
cfg_env.spec = MegaSpecId::MINI_REX;
let block_env = BlockEnv {
number: U256::from(1000),
timestamp: U256::from(1_800_000_000),
gas_limit: 30_000_000,
..Default::default()
};
let evm_env = EvmEnv::new(cfg_env, block_env);
let block_ctx =
MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits());
let inspector = SkipNestedCallInspector::default();
let mut executor = block_executor_factory
.create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector);
let tx = create_transaction(0, 1_000_000);
let result = executor.execute_transaction(&tx);
assert!(result.is_ok(), "Transaction should succeed: {:?}", result.err());
assert_eq!(
executor.evm().inspector.calls_intercepted.get(),
1,
"Inspector should have intercepted 1 nested call"
);
assert_eq!(
executor.evm().inspector.call_ends.get(),
2,
"call_end should be invoked for both the main call and the intercepted nested call"
);
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");
}
#[derive(Default)]
struct SkipCreateInspector {
creates_intercepted: Cell<u32>,
create_ends: Cell<u32>,
}
impl<CTX: ContextTr, INTR: InterpreterTypes> Inspector<CTX, INTR> for SkipCreateInspector {
fn create(&mut self, _context: &mut CTX, inputs: &mut CreateInputs) -> Option<CreateOutcome> {
self.creates_intercepted.set(self.creates_intercepted.get() + 1);
Some(CreateOutcome {
result: InterpreterResult {
result: InstructionResult::Stop,
output: Bytes::new(),
gas: Gas::new(inputs.gas_limit),
},
address: None,
})
}
fn create_end(
&mut self,
_context: &mut CTX,
_inputs: &CreateInputs,
_outcome: &mut CreateOutcome,
) {
self.create_ends.set(self.create_ends.get() + 1);
}
}
#[test]
fn test_inspector_early_return_create_with_additional_limits() {
let mut db = MemoryDatabase::default();
db.set_account_balance(CALLER, U256::from(1_000_000_000_000_000u64));
let mut state = State::builder().with_database(&mut db).build();
use alloy_hardforks::ForkCondition;
use mega_evm::MegaHardfork;
let external_envs = TestExternalEnvs::<Infallible>::new();
let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs);
let chain_spec =
MegaHardforkConfig::default().with(MegaHardfork::MiniRex, ForkCondition::Timestamp(0));
let receipt_builder = OpAlloyReceiptBuilder::default();
let block_executor_factory =
MegaBlockExecutorFactory::new(chain_spec, evm_factory, receipt_builder);
let mut cfg_env = revm::context::CfgEnv::default();
cfg_env.spec = MegaSpecId::MINI_REX;
let block_env = BlockEnv {
number: U256::from(1000),
timestamp: U256::from(1_800_000_000),
gas_limit: 30_000_000,
..Default::default()
};
let evm_env = EvmEnv::new(cfg_env, block_env);
let block_ctx =
MegaBlockExecutionCtx::new(B256::ZERO, None, Bytes::new(), BlockLimits::no_limits());
let inspector = SkipCreateInspector::default();
let mut executor = block_executor_factory
.create_executor_with_inspector(&mut state, block_ctx, evm_env, inspector);
let init_code = Bytes::from(vec![0x00]);
let tx = create_deploy_transaction(0, 10_000_000, init_code);
let result = executor.execute_transaction(&tx);
assert!(result.is_ok(), "Transaction should succeed: {:?}", result.err());
assert_eq!(
executor.evm().inspector.creates_intercepted.get(),
1,
"Inspector should have intercepted 1 create operation"
);
assert_eq!(
executor.evm().inspector.create_ends.get(),
1,
"create_end should be invoked for the intercepted create"
);
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");
}