From fecc46bcc166da2184a93790f7705129d96e2d04 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:52:32 +0200 Subject: [PATCH] feat: add support for fuzzing bytecodes vs interpreter Co-authored-by: dergoegge --- Cargo.lock | 36 +++++++ Cargo.toml | 2 +- crates/revmc-cli/src/lib.rs | 2 +- crates/revmc-cli/src/main.rs | 31 ++++-- crates/revmc/Cargo.toml | 5 + crates/revmc/src/bytecode/info.rs | 6 +- crates/revmc/src/lib.rs | 5 +- crates/revmc/src/tests/mod.rs | 33 ++++--- crates/revmc/src/tests/runner.rs | 146 ++++++++++++++++++++-------- fuzz/.gitignore | 4 + fuzz/Cargo.toml | 23 +++++ fuzz/fuzz_targets/vs_interpreter.rs | 72 ++++++++++++++ 12 files changed, 297 insertions(+), 68 deletions(-) create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/fuzz_targets/vs_interpreter.rs diff --git a/Cargo.lock b/Cargo.lock index 03f7df2..7b618af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,6 +442,11 @@ name = "cc" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cexpr" @@ -1344,6 +1349,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1397,6 +1411,17 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.8.4" @@ -2097,6 +2122,7 @@ name = "revmc" version = "0.1.0" dependencies = [ "alloy-primitives", + "arbitrary", "bitflags 2.6.0", "bitvec", "paste", @@ -2204,6 +2230,16 @@ dependencies = [ "revmc-context", ] +[[package]] +name = "revmc-fuzz" +version = "0.0.0" +dependencies = [ + "eyre", + "libfuzzer-sys", + "revmc", + "revmc-build", +] + [[package]] name = "revmc-llvm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index db4ef91..a08626b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "examples/*"] +members = ["crates/*", "examples/*", "fuzz"] default-members = ["crates/revmc-cli"] resolver = "2" diff --git a/crates/revmc-cli/src/lib.rs b/crates/revmc-cli/src/lib.rs index ddbdcca..8641384 100644 --- a/crates/revmc-cli/src/lib.rs +++ b/crates/revmc-cli/src/lib.rs @@ -36,7 +36,7 @@ pub fn read_code_string(contents: &[u8], ext: Option<&str>) -> Result> { hex::decode(first_line).wrap_err("given code is not valid hex") } else if ext == Some("bin") || !contents.is_ascii() { Ok(contents.to_vec()) - } else if ext == Some("evm") { + } else if ext == Some("evm") || contents.is_ascii() { parse_evm_dsl(utf8()?) } else { Err(eyre!("could not determine bytecode type")) diff --git a/crates/revmc-cli/src/main.rs b/crates/revmc-cli/src/main.rs index 6c2f888..3bc93b8 100644 --- a/crates/revmc-cli/src/main.rs +++ b/crates/revmc-cli/src/main.rs @@ -2,7 +2,8 @@ use clap::{Parser, ValueEnum}; use color_eyre::{eyre::eyre, Result}; -use revm_primitives::{address, Bytes, Env, SpecId}; +use revm_interpreter::{opcode::make_instruction_table, SharedMemory}; +use revm_primitives::{address, spec_to_generic, Bytes, Env, SpecId}; use revmc::{eyre::ensure, EvmCompiler, EvmContext, EvmLlvmBackend, OptimizationLevel}; use revmc_cli::{get_benches, read_code, Bench}; use std::{ @@ -38,6 +39,10 @@ struct Cli { #[arg(long)] aot: bool, + /// Interpret the code instead of compiling. + #[arg(long, conflicts_with = "aot")] + interpret: bool, + /// Target triple. #[arg(long, default_value = "native")] target: String, @@ -197,20 +202,28 @@ fn main() -> Result<()> { unsafe { compiler.jit_function(f_id)? } }; + #[allow(unused_parens)] + let table = spec_to_generic!(spec_id, (const { &make_instruction_table::<_, SPEC>() })); let mut run = |f: revmc::EvmCompilerFn| { let mut interpreter = revm_interpreter::Interpreter::new(contract.clone(), gas_limit, false); host.clear(); - let (mut ecx, stack, stack_len) = - EvmContext::from_interpreter_with_stack(&mut interpreter, &mut host); - for (i, input) in stack_input.iter().enumerate() { - stack.as_mut_slice()[i] = input.into(); - } - *stack_len = stack_input.len(); + if cli.interpret { + let action = interpreter.run(SharedMemory::new(), table, &mut host); + (interpreter.instruction_result, action) + } else { + let (mut ecx, stack, stack_len) = + EvmContext::from_interpreter_with_stack(&mut interpreter, &mut host); - let r = unsafe { f.call_noinline(Some(stack), Some(stack_len), &mut ecx) }; - (r, interpreter.next_action) + for (i, input) in stack_input.iter().enumerate() { + stack.as_mut_slice()[i] = input.into(); + } + *stack_len = stack_input.len(); + + let r = unsafe { f.call_noinline(Some(stack), Some(stack_len), &mut ecx) }; + (r, interpreter.next_action) + } }; if cli.n_iters == 0 { diff --git a/crates/revmc/Cargo.toml b/crates/revmc/Cargo.toml index 05ac488..772a4ed 100644 --- a/crates/revmc/Cargo.toml +++ b/crates/revmc/Cargo.toml @@ -36,6 +36,8 @@ bitvec = "1" rustc-hash.workspace = true tracing.workspace = true +arbitrary = { version = "1.3", optional = true } + [dev-dependencies] revmc-context = { workspace = true, features = ["host-ext-any"] } paste.workspace = true @@ -53,3 +55,6 @@ asm-keccak = ["alloy-primitives/asm-keccak"] # I don't think this is supported, but it's necessary for --all-features to work in workspaces which # also have this feature. optimism = ["revm-primitives/optimism", "revm-interpreter/optimism"] + +# Internal features. +__fuzzing = ["dep:arbitrary"] diff --git a/crates/revmc/src/bytecode/info.rs b/crates/revmc/src/bytecode/info.rs index 15404f8..338951f 100644 --- a/crates/revmc/src/bytecode/info.rs +++ b/crates/revmc/src/bytecode/info.rs @@ -79,11 +79,9 @@ impl OpcodeInfo { } /// Returns the static info map for the given `SpecId`. +#[allow(unused_parens)] pub const fn op_info_map(spec_id: SpecId) -> &'static [OpcodeInfo; 256] { - spec_to_generic!(spec_id, { - const MAP: &[OpcodeInfo; 256] = &make_map(::SPEC_ID); - MAP - }) + spec_to_generic!(spec_id, (const { &make_map(::SPEC_ID) })) } #[allow(unused_mut)] diff --git a/crates/revmc/src/lib.rs b/crates/revmc/src/lib.rs index 7021ac5..7f66956 100644 --- a/crates/revmc/src/lib.rs +++ b/crates/revmc/src/lib.rs @@ -18,8 +18,9 @@ pub use compiler::EvmCompiler; mod linker; pub use linker::Linker; -#[cfg(test)] -mod tests; +/// Internal tests and testing utilities. Not public API. +#[cfg(any(test, feature = "__fuzzing"))] +pub mod tests; #[allow(ambiguous_glob_reexports)] #[doc(inline)] diff --git a/crates/revmc/src/tests/mod.rs b/crates/revmc/src/tests/mod.rs index c0d22b9..e2ce0be 100644 --- a/crates/revmc/src/tests/mod.rs +++ b/crates/revmc/src/tests/mod.rs @@ -1,4 +1,10 @@ -#![allow(clippy::needless_update, unreachable_pub)] +#![allow( + clippy::needless_update, + unreachable_pub, + dead_code, + missing_docs, + missing_debug_implementations +)] use crate::*; use primitives::SpecId; @@ -11,12 +17,15 @@ use revm_primitives::{hex, keccak256, Address, Bytes, LogData, B256, KECCAK_EMPT #[macro_use] mod macros; -mod fibonacci; mod meta; + +#[cfg(not(feature = "__fuzzing"))] +mod fibonacci; +#[cfg(not(feature = "__fuzzing"))] mod resume; mod runner; -use runner::*; +pub use runner::*; const I256_MAX: U256 = U256::from_limbs([ 0xFFFFFFFFFFFFFFFF, @@ -677,7 +686,7 @@ tests! { sstore1(@raw { bytecode: &[op::PUSH1, 200, op::SLOAD, op::PUSH1, 100, op::PUSH1, 200, op::SSTORE, op::PUSH1, 200, op::SLOAD], expected_stack: &[0_U256, 100_U256], - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, assert_host: Some(|host| { assert_eq!(host.storage.get(&200_U256), Some(&100_U256)); }), @@ -754,7 +763,7 @@ tests! { // NOTE: The address is pushed by the caller. expected_stack: &[], expected_memory: &0x69_U256.to_be_bytes::<32>(), - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, expected_next_action: InterpreterAction::Create { inputs: Box::new(CreateInputs { caller: DEF_ADDR, @@ -771,7 +780,7 @@ tests! { // NOTE: The address is pushed by the caller. expected_stack: &[], expected_memory: &0x69_U256.to_be_bytes::<32>(), - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, expected_next_action: InterpreterAction::Create { inputs: Box::new(CreateInputs { caller: DEF_ADDR, @@ -797,7 +806,7 @@ tests! { // NOTE: The return is pushed by the caller. expected_stack: &[], expected_memory: &[0; 32], - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, expected_next_action: InterpreterAction::Call { inputs: Box::new(CallInputs { input: Bytes::copy_from_slice(&[0; 3]), @@ -853,7 +862,7 @@ tests! { selfdestruct(@raw { bytecode: &[op::PUSH1, 0x69, op::SELFDESTRUCT, op::INVALID], expected_return: InstructionResult::SelfDestruct, - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, assert_host: Some(|host| { assert_eq!(host.selfdestructs, [(DEF_ADDR, Address::with_last_byte(0x69))]); }), @@ -871,14 +880,14 @@ tests! { ecx.contract.input = Bytes::from(&hex!("c0406226")); }), expected_return: InstructionResult::Return, - expected_stack: STACK_WHAT_THE_INTERPRETER_SAYS, - expected_gas: GAS_WHAT_THE_INTERPRETER_SAYS, - expected_memory: MEMORY_WHAT_THE_INTERPRETER_SAYS, + expected_stack: STACK_WHAT_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, + expected_memory: MEMORY_WHAT_INTERPRETER_SAYS, expected_next_action: InterpreterAction::Return { result: InterpreterResult { result: InstructionResult::Return, output: Bytes::copy_from_slice(&1_U256.to_be_bytes::<32>()), - gas: Gas::new(GAS_WHAT_THE_INTERPRETER_SAYS), + gas: Gas::new(GAS_WHAT_INTERPRETER_SAYS), }, }, assert_host: Some(|host| { diff --git a/crates/revmc/src/tests/runner.rs b/crates/revmc/src/tests/runner.rs index 26f442f..7171159 100644 --- a/crates/revmc/src/tests/runner.rs +++ b/crates/revmc/src/tests/runner.rs @@ -1,4 +1,5 @@ use super::*; +use arbitrary::Arbitrary; use interpreter::LoadAccountResult; use revm_interpreter::{opcode as op, Contract, DummyHost, Host}; use revm_primitives::{ @@ -21,6 +22,22 @@ pub struct TestCase<'a> { pub assert_ecx: Option)>, } +impl<'a> Arbitrary<'a> for TestCase<'a> { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let spec_id = SpecId::try_from_u8(u.arbitrary()?).unwrap_or(DEF_SPEC); + Ok(Self::what_interpreter_says(u.arbitrary()?, spec_id)) + } + + fn arbitrary_take_rest(mut u: arbitrary::Unstructured<'a>) -> arbitrary::Result { + let spec_id = SpecId::try_from_u8(u.arbitrary()?).unwrap_or(DEF_SPEC); + Ok(Self::what_interpreter_says(u.take_rest(), spec_id)) + } + + fn size_hint(depth: usize) -> (usize, Option) { + <(u8, &'static [u8]) as Arbitrary>::size_hint(depth) + } +} + impl Default for TestCase<'_> { fn default() -> Self { Self { @@ -55,6 +72,23 @@ impl fmt::Debug for TestCase<'_> { } } +impl<'a> TestCase<'a> { + pub fn what_interpreter_says(bytecode: &'a [u8], spec_id: SpecId) -> Self { + Self { + bytecode, + spec_id, + modify_ecx: None, + expected_return: RETURN_WHAT_INTERPRETER_SAYS, + expected_stack: STACK_WHAT_INTERPRETER_SAYS, + expected_memory: MEMORY_WHAT_INTERPRETER_SAYS, + expected_gas: GAS_WHAT_INTERPRETER_SAYS, + expected_next_action: ACTION_WHAT_INTERPRETER_SAYS, + assert_host: None, + assert_ecx: None, + } + } +} + // Default values. pub const DEF_SPEC: SpecId = SpecId::CANCUN; pub const DEF_OPINFOS: &[OpcodeInfo; 256] = op_info_map(DEF_SPEC); @@ -74,10 +108,18 @@ pub static DEF_CODEMAP: OnceLock> = OnceL pub const OTHER_ADDR: Address = Address::repeat_byte(0x69); pub const DEF_BN: U256 = uint!(500_U256); -pub const STACK_WHAT_THE_INTERPRETER_SAYS: &[U256] = - &[U256::from_be_slice(&GAS_WHAT_THE_INTERPRETER_SAYS.to_be_bytes())]; -pub const MEMORY_WHAT_THE_INTERPRETER_SAYS: &[u8] = &GAS_WHAT_THE_INTERPRETER_SAYS.to_be_bytes(); -pub const GAS_WHAT_THE_INTERPRETER_SAYS: u64 = 0x4682e332d6612de1; +pub const RETURN_WHAT_INTERPRETER_SAYS: InstructionResult = InstructionResult::PrecompileError; +pub const STACK_WHAT_INTERPRETER_SAYS: &[U256] = + &[U256::from_be_slice(&GAS_WHAT_INTERPRETER_SAYS.to_be_bytes())]; +pub const MEMORY_WHAT_INTERPRETER_SAYS: &[u8] = &GAS_WHAT_INTERPRETER_SAYS.to_be_bytes(); +pub const GAS_WHAT_INTERPRETER_SAYS: u64 = 0x4682e332d6612de1; +pub const ACTION_WHAT_INTERPRETER_SAYS: InterpreterAction = InterpreterAction::Return { + result: InterpreterResult { + gas: Gas::new(GAS_WHAT_INTERPRETER_SAYS), + output: Bytes::from_static(MEMORY_WHAT_INTERPRETER_SAYS), + result: RETURN_WHAT_INTERPRETER_SAYS, + }, +}; pub fn def_env() -> &'static Env { DEF_ENV.get_or_init(|| Env { @@ -151,6 +193,12 @@ pub struct TestHost { pub selfdestructs: Vec<(Address, Address)>, } +impl Default for TestHost { + fn default() -> Self { + Self::new() + } +} + impl TestHost { pub fn new() -> Self { Self { @@ -335,20 +383,25 @@ fn run_compiled_test_case(test_case: &TestCase<'_>, f: EvmCompilerFn) { let interpreter_action = interpreter.run(memory, &table, &mut int_host); - assert_eq!( - interpreter.instruction_result, expected_return, - "interpreter return value mismatch" - ); + let mut expected_return = expected_return; + if expected_return == RETURN_WHAT_INTERPRETER_SAYS { + expected_return = interpreter.instruction_result; + } else { + assert_eq!( + interpreter.instruction_result, expected_return, + "interpreter return value mismatch" + ); + } let mut expected_stack = expected_stack; - if expected_stack == STACK_WHAT_THE_INTERPRETER_SAYS { + if expected_stack == STACK_WHAT_INTERPRETER_SAYS { expected_stack = interpreter.stack.data(); } else { assert_eq!(interpreter.stack.data(), expected_stack, "interpreter stack mismatch"); } let mut expected_memory = expected_memory; - if expected_memory == MEMORY_WHAT_THE_INTERPRETER_SAYS { + if expected_memory == MEMORY_WHAT_INTERPRETER_SAYS { expected_memory = interpreter.shared_memory.context_memory(); } else { assert_eq!( @@ -359,26 +412,28 @@ fn run_compiled_test_case(test_case: &TestCase<'_>, f: EvmCompilerFn) { } let mut expected_gas = expected_gas; - if expected_gas == GAS_WHAT_THE_INTERPRETER_SAYS { - println!("asked for interpreter gas: {}", interpreter.gas.spent()); + if expected_gas == GAS_WHAT_INTERPRETER_SAYS { expected_gas = interpreter.gas.spent(); } else { assert_eq!(interpreter.gas.spent(), expected_gas, "interpreter gas mismatch"); } - // Check next action only if it's not the default. This should be `None` but `run` - // returns a default value. - if !(expected_next_action.is_none() - && interpreter_action - == (InterpreterAction::Return { - result: InterpreterResult { - result: interpreter.instruction_result, - output: Bytes::new(), - gas: interpreter.gas, - }, - })) - { - assert_actions(&interpreter_action, expected_next_action); + // This is what the interpreter returns when the internal action is None in `run`. + let default_action = InterpreterAction::Return { + result: InterpreterResult { + result: interpreter.instruction_result, + output: Bytes::new(), + gas: interpreter.gas, + }, + }; + let mut expected_next_action = expected_next_action; + if *expected_next_action == ACTION_WHAT_INTERPRETER_SAYS { + expected_next_action = &interpreter_action; + } else { + if expected_next_action.is_none() { + expected_next_action = &default_action; + } + assert_actions(&interpreter_action, &expected_next_action); } if let Some(assert_host) = assert_host { @@ -387,31 +442,44 @@ fn run_compiled_test_case(test_case: &TestCase<'_>, f: EvmCompilerFn) { let actual_return = unsafe { f.call(Some(stack), Some(stack_len), ecx) }; - assert_eq!(actual_return, expected_return, "return value mismatch"); + // We can have a stack overflow/underflow before other error codes due to sections. + if matches!( + actual_return, + InstructionResult::StackOverflow | InstructionResult::StackUnderflow + ) { + assert_eq!( + actual_return.is_error(), + expected_return.is_error(), + "return value mismatch" + ); + } else { + assert_eq!(actual_return, expected_return, "return value mismatch"); + } let actual_stack = stack.as_slice().iter().take(*stack_len).map(|x| x.to_u256()).collect::>(); - // On EVM halt all available gas is consumed, so we can go a bit off here. + // On EVM halt all available gas is consumed, so resulting stack, memory, and gas do not + // matter. We do less work than the interpreter by bailing out earlier due to sections. if !actual_return.is_error() { assert_eq!(actual_stack, *expected_stack, "stack mismatch"); - } - assert_eq!( - MemDisplay(ecx.memory.context_memory()), - MemDisplay(expected_memory), - "interpreter memory mismatch" - ); + assert_eq!( + MemDisplay(ecx.memory.context_memory()), + MemDisplay(expected_memory), + "interpreter memory mismatch" + ); - // On EVM halt all available gas is consumed, so we can go a bit off here. - if !actual_return.is_error() { assert_eq!(ecx.gas.spent(), expected_gas, "gas mismatch"); } - assert_actions(ecx.next_action, expected_next_action); + let actual_next_action = + if ecx.next_action.is_none() { &default_action } else { &*ecx.next_action }; + assert_actions(actual_next_action, &expected_next_action); - if let Some(assert_host) = assert_host { - assert_host(ecx.host.downcast_ref().unwrap()); + if let Some(_assert_host) = assert_host { + #[cfg(not(feature = "__fuzzing"))] + _assert_host(ecx.host.downcast_ref().unwrap()); } if let Some(assert_ecx) = assert_ecx { @@ -438,7 +506,7 @@ fn assert_actions(actual: &InterpreterAction, expected: &InterpreterAction) { ) => { assert_eq!(result.result, expected_result.result, "result mismatch"); assert_eq!(result.output, expected_result.output, "result output mismatch"); - if expected_result.gas.limit() != GAS_WHAT_THE_INTERPRETER_SAYS { + if expected_result.gas.limit() != GAS_WHAT_INTERPRETER_SAYS { assert_eq!(result.gas.spent(), expected_result.gas.spent(), "result gas mismatch"); } } diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..2b00e95 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "revmc-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +revmc = { workspace = true, features = ["llvm-prefer-dynamic", "__fuzzing"] } +eyre.workspace = true + +[build-dependencies] +revmc-build.workspace = true + +[[bin]] +name = "vs_interpreter" +path = "fuzz_targets/vs_interpreter.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/vs_interpreter.rs b/fuzz/fuzz_targets/vs_interpreter.rs new file mode 100644 index 0000000..0f1d604 --- /dev/null +++ b/fuzz/fuzz_targets/vs_interpreter.rs @@ -0,0 +1,72 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use revmc::{ + interpreter::OPCODE_INFO_JUMPTABLE, + tests::{run_test_case, TestCase}, + EvmCompiler, EvmLlvmBackend, OpcodesIter, OptimizationLevel, +}; +use std::path::PathBuf; + +fuzz_target!(|test_case: TestCase<'_>| { + if should_skip(test_case.bytecode) { + return; + } + + let mut test_case = test_case; + // EOF is not yet implemented. + if test_case.spec_id > revmc::primitives::SpecId::CANCUN { + test_case.spec_id = revmc::primitives::SpecId::CANCUN; + } + + let context = revmc::llvm::inkwell::context::Context::create(); + let backend = EvmLlvmBackend::new(&context, false, OptimizationLevel::None).unwrap(); + let mut compiler = EvmCompiler::new(backend); + if let Ok(dump_location) = std::env::var("COMPILER_DUMP") { + compiler.set_dump_to(Some(PathBuf::from(dump_location))); + } + run_test_case(&test_case, &mut compiler); +}); + +fn should_skip(bytecode: &[u8]) -> bool { + OpcodesIter::new(bytecode).any(|op| { + let Some(info) = OPCODE_INFO_JUMPTABLE[op.opcode as usize] else { return true }; + // Skip all EOF opcodes since they might have different error codes in the interpreter. + if is_eof(op.opcode) { + return true; + } + // Skip if the immediate is incomplete. + // TODO: What is the expected behavior here? + if info.immediate_size() > 0 && op.immediate.is_none() { + return true; + } + false + }) +} + +// https://github.com/ipsilon/eof/blob/53e0e987e10bedee36d6c6ad0a6d1cfe806905e2/spec/eof.md?plain=1#L159 +fn is_eof(op: u8) -> bool { + use revmc::interpreter::opcode::*; + matches!( + op, + RJUMP + | RJUMPI + | RJUMPV + | CALLF + | RETF + | JUMPF + | EOFCREATE + | RETURNCONTRACT + | DATALOAD + | DATALOADN + | DATASIZE + | DATACOPY + | DUPN + | SWAPN + | EXCHANGE + | RETURNDATALOAD + | EXTCALL + | EXTDELEGATECALL + | EXTSTATICCALL + ) +}