diff --git a/crates/mipsevm/Cargo.toml b/crates/mipsevm/Cargo.toml index 683d349..eca9d6f 100644 --- a/crates/mipsevm/Cargo.toml +++ b/crates/mipsevm/Cargo.toml @@ -7,20 +7,20 @@ version.workspace = true authors.workspace = true [dependencies] +preimage-oracle = { path = "../preimage" } alloy-primitives = "0.3.3" alloy-sol-types = "0.3.2" once_cell = "1.18.0" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.106" fnv = "1.0.7" -revm = { version = "3.3.0", features = ["no_gas_measuring"] } -preimage-oracle = { path = "../preimage" } # misc anyhow = "1.0.75" tracing = { version = "0.1.37", optional = true } [dev-dependencies] +revm = { version = "3.3.0", features = ["no_gas_measuring"] } rand = "0.8.5" criterion = { version = "0.5.1", features = ["html_reports"] } diff --git a/crates/mipsevm/src/lib.rs b/crates/mipsevm/src/lib.rs index 0a46a4d..43cf7a1 100644 --- a/crates/mipsevm/src/lib.rs +++ b/crates/mipsevm/src/lib.rs @@ -27,7 +27,5 @@ pub use types::{Address, Fd, Gindex, Page, PageIndex, StateWitness, VMStatus}; mod mips; pub use mips::InstrumentedState; -mod evm; - #[cfg(test)] mod test_utils; diff --git a/crates/mipsevm/src/memory.rs b/crates/mipsevm/src/memory.rs index a2679ad..fb981ff 100644 --- a/crates/mipsevm/src/memory.rs +++ b/crates/mipsevm/src/memory.rs @@ -61,13 +61,13 @@ impl Memory { } // Find the page and invalidate the address within it. - match self.page_lookup(address >> page::PAGE_ADDRESS_SIZE) { + match self.page_lookup(address as u64 >> page::PAGE_ADDRESS_SIZE) { Some(page) => { let mut page = page.borrow_mut(); let prev_valid = !page.is_valid(1); // Invalidate the address within the page. - page.invalidate(address & page::PAGE_ADDRESS_MASK as u64)?; + page.invalidate(address & page::PAGE_ADDRESS_MASK as u32)?; // If the page was already invalid before, then nodes to the memory // root will also still be invalid. @@ -82,7 +82,7 @@ impl Memory { } // Find the generalized index of the first page covering the address - let mut g_index = ((1u64 << 32) | address) >> page::PAGE_ADDRESS_SIZE as u64; + let mut g_index = ((1u64 << 32) | address as u64) >> page::PAGE_ADDRESS_SIZE; // Invalidate all nodes in the branch while g_index > 0 { self.nodes.insert(g_index, None); @@ -236,7 +236,7 @@ impl Memory { anyhow::bail!("Unaligned memory access: {:x}", address); } - let page_index = address >> page::PAGE_ADDRESS_SIZE as u64; + let page_index = address as PageIndex >> page::PAGE_ADDRESS_SIZE as u64; let page_address = address as usize & page::PAGE_ADDRESS_MASK; // Attempt to look up the page. @@ -271,7 +271,7 @@ impl Memory { anyhow::bail!("Unaligned memory access: {:x}", address); } - match self.page_lookup(address >> page::PAGE_ADDRESS_SIZE as u64) { + match self.page_lookup(address as u64 >> page::PAGE_ADDRESS_SIZE as u64) { Some(page) => { let page_address = address as usize & page::PAGE_ADDRESS_MASK; Ok(u32::from_be_bytes( @@ -313,7 +313,7 @@ impl Memory { let mut address = address; let mut data = data; loop { - let page_index = address >> page::PAGE_ADDRESS_SIZE as u64; + let page_index = address as PageIndex >> page::PAGE_ADDRESS_SIZE as u64; let page_address = address as usize & page::PAGE_ADDRESS_MASK; let page = self @@ -327,7 +327,7 @@ impl Memory { if n == 0 { return Ok(()); } - address += n as u64; + address += n as u32; } Err(e) => return Err(e.into()), }; @@ -338,11 +338,11 @@ impl Memory { pub struct MemoryReader { memory: Rc>, address: Address, - count: u64, + count: u32, } impl MemoryReader { - pub fn new(memory: Rc>, address: Address, count: u64) -> Self { + pub fn new(memory: Rc>, address: Address, count: u32) -> Self { Self { memory, address, @@ -357,13 +357,13 @@ impl Read for MemoryReader { return Ok(0); } - let end_address = self.address + self.count; + let end_address = self.address + self.count as Address; - let page_index = self.address >> page::PAGE_ADDRESS_SIZE as u64; + let page_index = self.address as PageIndex >> page::PAGE_ADDRESS_SIZE as u64; let start = self.address as usize & page::PAGE_ADDRESS_MASK; let mut end = page::PAGE_SIZE; - if page_index == (end_address >> page::PAGE_ADDRESS_SIZE as u64) { + if page_index == (end_address as u64 >> page::PAGE_ADDRESS_SIZE as u64) { end = end_address as usize & page::PAGE_ADDRESS_MASK; } let n = end - start; @@ -377,8 +377,8 @@ impl Read for MemoryReader { std::io::copy(&mut vec![0; n].as_slice(), &mut buf)?; } }; - self.address += n as u64; - self.count -= n as u64; + self.address += n as u32; + self.count -= n as u32; Ok(n) } } @@ -578,7 +578,7 @@ mod test { .expect("Should not error"); let mut reader = - MemoryReader::new(Rc::clone(&memory), 0x1337 - 10, data.len() as u64 + 20); + MemoryReader::new(Rc::clone(&memory), 0x1337 - 10, data.len() as u32 + 20); let mut buf = Vec::with_capacity(1260); reader.read_to_end(&mut buf).unwrap(); diff --git a/crates/mipsevm/src/mips/instrumented.rs b/crates/mipsevm/src/mips/instrumented.rs index 234fcc0..ea40895 100644 --- a/crates/mipsevm/src/mips/instrumented.rs +++ b/crates/mipsevm/src/mips/instrumented.rs @@ -66,7 +66,7 @@ where /// - Err(_): An error occurred while processing the instruction step in the MIPS emulator. pub fn step(&mut self, proof: bool) -> Result> { self.mem_proof_enabled = proof; - self.last_mem_access = !0u32 as u64; + self.last_mem_access = !0u32 as Address; self.last_preimage_offset = !0u32; let mut witness = None; @@ -107,14 +107,9 @@ where mod test { use crate::test_utils::StaticOracle; - /// Used in tests to write the results to - const BASE_ADDR_END: u32 = 0xBF_FF_FF_F0; - - /// Used as the return-address for tests - const END_ADDR: u32 = 0xA7_EF_00_D0; - mod open_mips { use super::*; + use crate::test_utils::{BASE_ADDR_END, END_ADDR}; use crate::{Address, InstrumentedState, Memory, State}; use std::{ cell::RefCell, @@ -125,7 +120,6 @@ mod test { }; #[test] - // #[ignore] fn open_mips_tests() { let tests_path = PathBuf::from(std::env::current_dir().unwrap()) .join("open_mips_tests") diff --git a/crates/mipsevm/src/mips/mips_vm.rs b/crates/mipsevm/src/mips/mips_vm.rs index 5059b5d..58e0650 100644 --- a/crates/mipsevm/src/mips/mips_vm.rs +++ b/crates/mipsevm/src/mips/mips_vm.rs @@ -31,8 +31,8 @@ where /// - `Err(_)`: An error occurred while fetching the preimage. pub(crate) fn read_preimage(&mut self, key: B256, offset: u32) -> Result<(B256, usize)> { if key != self.last_preimage_key { - self.last_preimage_key = key; let data = self.preimage_oracle.get(key)?; + self.last_preimage_key = key; // Add the length prefix to the preimage // Resizes the `last_preimage` vec in-place to reduce reallocations. @@ -214,7 +214,7 @@ where let mut v0 = 0; let mut v1 = 0; - let (a0, a1, a2) = ( + let (a0, a1, mut a2) = ( self.state.registers[4], self.state.registers[5], self.state.registers[6], @@ -298,11 +298,8 @@ where }, Syscall::Write => match (a0 as u8).try_into() { Ok(fd @ (Fd::Stdout | Fd::StdErr)) => { - let mut reader = MemoryReader::new( - Rc::clone(&self.state.memory), - a1 as Address, - a2 as u64, - ); + let mut reader = + MemoryReader::new(Rc::clone(&self.state.memory), a1 as Address, a2); let writer: &mut dyn Write = if matches!(fd, Fd::Stdout) { &mut self.std_out } else { @@ -312,14 +309,11 @@ where v0 = a2; } Ok(Fd::HintWrite) => { - let mut reader = MemoryReader::new( - Rc::clone(&self.state.memory), - a1 as Address, - a2 as u64, - ); + let mut reader = + MemoryReader::new(Rc::clone(&self.state.memory), a1 as Address, a2); let mut hint_data = Vec::with_capacity(a2 as usize); reader.read_to_end(&mut hint_data)?; - self.state.last_hint.extend_from_slice(hint_data.as_slice()); + self.state.last_hint.extend(hint_data); // Continue processing while there is enough data to check if there are any // hints. @@ -351,7 +345,6 @@ where let alignment = a1 & 0x3; let space = 4 - alignment; - let mut a2 = a2; if space < a2 { a2 = space; } diff --git a/crates/mipsevm/src/page.rs b/crates/mipsevm/src/page.rs index 1b7f51f..7749281 100644 --- a/crates/mipsevm/src/page.rs +++ b/crates/mipsevm/src/page.rs @@ -51,7 +51,7 @@ impl CachedPage { /// ### Returns /// - A [Result] indicating if the operation was successful. pub fn invalidate(&mut self, page_addr: Address) -> Result<()> { - if page_addr >= PAGE_SIZE as u64 { + if page_addr >= PAGE_SIZE as Address { anyhow::bail!("Invalid page address: {}", page_addr); } diff --git a/crates/mipsevm/src/state.rs b/crates/mipsevm/src/state.rs index 3e3c4c2..79e2be6 100644 --- a/crates/mipsevm/src/state.rs +++ b/crates/mipsevm/src/state.rs @@ -10,7 +10,7 @@ use anyhow::Result; /// /// The [State] by itself does not contain functionality for performing instruction steps /// or executing the MIPS emulator. For this, use the [crate::InstrumentedState] struct. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct State { /// The [Memory] of the emulated MIPS thread context. pub memory: Rc>, diff --git a/crates/mipsevm/src/evm.rs b/crates/mipsevm/src/test_utils/evm.rs similarity index 63% rename from crates/mipsevm/src/evm.rs rename to crates/mipsevm/src/test_utils/evm.rs index 394632a..2e497a9 100644 --- a/crates/mipsevm/src/evm.rs +++ b/crates/mipsevm/src/test_utils/evm.rs @@ -19,10 +19,10 @@ pub const MIPS_ADDR: [u8; 20] = hex!("000000000000000000000000000000000000C0DE") pub const PREIMAGE_ORACLE_ADDR: [u8; 20] = hex!("00000000000000000000000000000000424f4f4b"); /// The creation EVM bytecode of the MIPS contract. Does not include constructor arguments. -pub const MIPS_CREATION_CODE: &str = include_str!("../bindings/mips_creation.bin"); +pub const MIPS_CREATION_CODE: &str = include_str!("../../bindings/mips_creation.bin"); /// The deployed EVM bytecode of the PreimageOracle contract. pub const PREIMAGE_ORACLE_DEPLOYED_CODE: &str = - include_str!("../bindings/preimage_oracle_deployed.bin"); + include_str!("../../bindings/preimage_oracle_deployed.bin"); /// A wrapper around a [revm] inspector with an in-memory backend that has the MIPS & PreimageOracle /// smart contracts deployed at deterministic addresses. This is used for differential testing the @@ -67,7 +67,8 @@ impl MipsEVM> { // Deploy the MIPS contract prior to deploying it manually. This contract has an immutable // variable, so we let the creation code fill this in for us, and then deploy it to the // test address. - let encoded_preimage_addr = Address::from_slice(MIPS_ADDR.as_slice()).into_word(); + let encoded_preimage_addr = + Address::from_slice(PREIMAGE_ORACLE_ADDR.as_slice()).into_word(); let mips_creation_heap = hex::decode(MIPS_CREATION_CODE)? .into_iter() .chain(encoded_preimage_addr) @@ -104,7 +105,7 @@ impl MipsEVM> { /// ### Returns /// - A [Result] containing the post-state hash of the MIPS VM or an error returned during /// execution. - pub fn step(&mut self, witness: StepWitness) -> Result { + pub fn step(&mut self, witness: StepWitness) -> Result { if witness.has_preimage() { crate::debug!( target: "mipsevm::evm", @@ -162,7 +163,7 @@ impl MipsEVM> { ); } - Ok(output) + Ok(post_state) } else { anyhow::bail!("Failed to step MIPS contract"); } @@ -212,7 +213,18 @@ impl MipsEVM> { #[cfg(test)] mod test { use super::*; + use crate::{ + test_utils::{StaticOracle, BASE_ADDR_END, END_ADDR}, + Address, InstrumentedState, Memory, State, + }; use revm::primitives::ExecutionResult; + use std::{ + cell::RefCell, + fs, + io::{self, BufReader}, + path::PathBuf, + rc::Rc, + }; #[test] fn sanity_evm_execution() { @@ -247,4 +259,192 @@ mod test { )) ); } + + #[test] + fn evm() { + let mut mips_evm = MipsEVM::new(); + mips_evm.try_init().unwrap(); + + let tests_path = PathBuf::from(std::env::current_dir().unwrap()) + .join("open_mips_tests") + .join("test") + .join("bin"); + let test_files = fs::read_dir(tests_path).unwrap(); + + for f in test_files.into_iter() { + if let Ok(f) = f { + let file_name = String::from(f.file_name().to_str().unwrap()); + println!(" -> Running test: {file_name}"); + + // Short circuit early for `exit_group.bin` + let exit_group = file_name == "exit_group.bin"; + + let program_mem = fs::read(f.path()).unwrap(); + + let mut state = { + let mut state = State::default(); + state.pc = 0; + state.next_pc = 4; + state.memory = Rc::new(RefCell::new(Memory::default())); + state + }; + state + .memory + .borrow_mut() + .set_memory_range(0, BufReader::new(program_mem.as_slice())) + .unwrap(); + + // Set the return address ($ra) to jump into when the test completes. + state.registers[31] = END_ADDR; + + let mut instrumented = InstrumentedState::new( + state, + StaticOracle::new(b"hello world".to_vec()), + io::stdout(), + io::stderr(), + ); + + for _ in 0..1000 { + if instrumented.state.pc == END_ADDR { + break; + } + if exit_group && instrumented.state.exited { + break; + } + + let instruction = instrumented + .state + .memory + .borrow_mut() + .get_memory(instrumented.state.pc as Address) + .unwrap(); + println!( + "{}", + format!( + "step: {} pc: 0x{:08x} insn: 0x{:08x}", + instrumented.state.step, instrumented.state.pc, instruction + ) + ); + + let step_witness = instrumented.step(true).unwrap().unwrap(); + + // Verify that the post state matches + let evm_post = mips_evm.step(step_witness).unwrap(); + let rust_post = instrumented.state.encode_witness().unwrap(); + + assert_eq!(evm_post, rust_post); + } + + if exit_group { + assert_ne!(END_ADDR, instrumented.state.pc, "must not reach end"); + assert!(instrumented.state.exited, "must exit"); + assert_eq!(1, instrumented.state.exit_code, "must exit with 1"); + } else { + assert_eq!(END_ADDR, instrumented.state.pc, "must reach end"); + let mut state = instrumented.state.memory.borrow_mut(); + let (done, result) = ( + state.get_memory((BASE_ADDR_END + 4) as Address).unwrap(), + state.get_memory((BASE_ADDR_END + 8) as Address).unwrap(), + ); + assert_eq!(done, 1, "must set done to 1"); + assert_eq!(result, 1, "must have success result {:?}", f.file_name()); + } + } + } + } + + #[test] + fn evm_single_step() { + let mut mips_evm = MipsEVM::new(); + mips_evm.try_init().unwrap(); + + let cases = [ + ("j MSB set target", 0, 4, 0x0A_00_00_02), + ( + "j non-zero PC region", + 0x10_00_00_00, + 0x10_00_00_04, + 0x08_00_00_02, + ), + ("jal MSB set target", 0, 4, 0x0E_00_00_02), + ( + "jal non-zero PC region", + 0x10_00_00_00, + 0x10_00_00_04, + 0x0C_00_00_02, + ), + ]; + + for (name, pc, next_pc, instruction) in cases { + println!(" -> Running test: {name}"); + + let mut state = State::default(); + state.pc = pc; + state.next_pc = next_pc; + state + .memory + .borrow_mut() + .set_memory(pc, instruction) + .unwrap(); + + let mut instrumented = InstrumentedState::new( + state, + StaticOracle::new(b"hello world".to_vec()), + io::stdout(), + io::stderr(), + ); + let step_witness = instrumented.step(true).unwrap().unwrap(); + + let evm_post = mips_evm.step(step_witness).unwrap(); + let rust_post = instrumented.state.encode_witness().unwrap(); + + assert_eq!(evm_post, rust_post); + } + } + + #[test] + fn evm_fault() { + let mut mips_evm = MipsEVM::new(); + mips_evm.try_init().unwrap(); + + let cases = [ + ("illegal instruction", 0, 0xFF_FF_FF_FFu32), + ("branch in delay slot", 8, 0x11_02_00_03), + ("jump in delay slot", 8, 0x0c_00_00_0c), + ]; + + for (name, next_pc, instruction) in cases { + println!(" -> Running test: {name}"); + + let mut state = State::default(); + state.next_pc = next_pc; + let mut initial_state = state.clone(); + state + .memory + .borrow_mut() + .set_memory(0, instruction) + .unwrap(); + + // Set the return address ($ra) to jump to when the test completes. + state.registers[31] = END_ADDR; + + let mut instrumented = InstrumentedState::new( + state, + StaticOracle::new(b"hello world".to_vec()), + io::stdout(), + io::stderr(), + ); + assert!(instrumented.step(true).is_err()); + + let instruction_proof = initial_state.memory.borrow_mut().merkle_proof(0).unwrap(); + let step_witness = StepWitness { + state: initial_state.encode_witness().unwrap(), + mem_proof: instruction_proof.to_vec(), + preimage_key: alloy_primitives::B256::ZERO, + preimage_value: Vec::default(), + preimage_offset: 0, + }; + assert!(mips_evm.step(step_witness).is_err()); + } + } } diff --git a/crates/mipsevm/src/test_utils.rs b/crates/mipsevm/src/test_utils/mod.rs similarity index 76% rename from crates/mipsevm/src/test_utils.rs rename to crates/mipsevm/src/test_utils/mod.rs index a07ed5f..d83e5b3 100644 --- a/crates/mipsevm/src/test_utils.rs +++ b/crates/mipsevm/src/test_utils/mod.rs @@ -1,9 +1,16 @@ //! Testing utilities. +use crate::PreimageOracle; use alloy_primitives::B256; use preimage_oracle::{Keccak256Key, Key}; -use crate::PreimageOracle; +pub mod evm; + +/// Used in tests to write the results to +pub const BASE_ADDR_END: u32 = 0xBF_FF_FF_F0; + +/// Used as the return-address for tests +pub const END_ADDR: u32 = 0xA7_EF_00_D0; pub struct StaticOracle { preimage_data: Vec, diff --git a/crates/mipsevm/src/types.rs b/crates/mipsevm/src/types.rs index fb4aed9..d173d81 100644 --- a/crates/mipsevm/src/types.rs +++ b/crates/mipsevm/src/types.rs @@ -12,8 +12,8 @@ pub type PageIndex = u64; /// A [Gindex] is a generalized index, defined as $2^{\text{depth}} + \text{index}$. pub type Gindex = u64; -/// An [Address] is a 64 bit address in the MIPS emulator's memory. -pub type Address = u64; +/// An [Address] is a 32 bit address in the MIPS emulator's memory. +pub type Address = u32; /// The [VMStatus] is an indicator within the [StateWitness] hash that indicates /// the current status of the MIPS emulator. diff --git a/crates/mipsevm/src/witness.rs b/crates/mipsevm/src/witness.rs index b3a80fa..3bd698c 100644 --- a/crates/mipsevm/src/witness.rs +++ b/crates/mipsevm/src/witness.rs @@ -115,17 +115,8 @@ impl StepWitness { /// ### Returns /// - The ABI encoded input to the MIPS step function. pub fn encode_step_input(&self) -> Bytes { - let mut abi_state_len = self.state.len(); - if abi_state_len % 32 != 0 { - abi_state_len += 32 - (abi_state_len % 32); - } - - // Pad state to 32 byte multiple per ABI - let mut abi_state = vec![0u8; abi_state_len]; - abi_state[..self.state.len()].copy_from_slice(&self.state); - let call = stepCall { - _0: abi_state, + _0: self.state.to_vec(), _1: self.mem_proof.clone(), };