diff --git a/Cargo.lock b/Cargo.lock index 7d12e51368..088ddbef0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -426,6 +426,7 @@ name = "caller-env" version = "0.1.0" dependencies = [ "brotli", + "hex", "k256", "rand 0.8.5", "rand_pcg", diff --git a/Cargo.toml b/Cargo.toml index 31b52755c1..f76e977105 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ enum-iterator = { version = "2.0.1" } eyre = { version = "0.6.5" } fnv = { version = "1.0.7" } gperftools = { version = "0.2.0" } -hex = { version = "0.4.3" } +hex = { version = "0.4.3", default-features = false } k256 = { version = "0.13.4", default-features = false} lazy_static = { version = "1.4.0" } libc = { version = "0.2.132" } diff --git a/changelog/pmikolajczyk-nit-4614.md b/changelog/pmikolajczyk-nit-4614.md new file mode 100644 index 0000000000..0678ac52ea --- /dev/null +++ b/changelog/pmikolajczyk-nit-4614.md @@ -0,0 +1,2 @@ +### Internal + - Move wavmio logic from JIT crate to caller-env (to be reused soon by SP1 validator) \ No newline at end of file diff --git a/crates/caller-env/Cargo.toml b/crates/caller-env/Cargo.toml index 33f22141cf..1e58e05538 100644 --- a/crates/caller-env/Cargo.toml +++ b/crates/caller-env/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true [dependencies] brotli = { workspace = true, optional = true } +hex = { workspace = true, features = ["alloc"] } k256 = { workspace = true, features = ["ecdsa"] } rand = { workspace = true } rand_pcg = { workspace = true } diff --git a/crates/caller-env/src/lib.rs b/crates/caller-env/src/lib.rs index d0ee1ffaa0..aca39608eb 100644 --- a/crates/caller-env/src/lib.rs +++ b/crates/caller-env/src/lib.rs @@ -21,6 +21,7 @@ pub mod wasmer_traits; pub mod brotli; pub mod arbcrypto; +pub mod wavmio; mod guest_ptr; pub mod wasip1_stub; diff --git a/crates/caller-env/src/wavmio.rs b/crates/caller-env/src/wavmio.rs new file mode 100644 index 0000000000..4603e164d7 --- /dev/null +++ b/crates/caller-env/src/wavmio.rs @@ -0,0 +1,147 @@ +// Copyright 2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +use crate::{GuestPtr, MemAccess}; +use alloc::format; +use alloc::string::String; +use core::cmp::min; + +/// Read validation inputs and set outputs for the `wavmio` host functions. +pub trait WavmIo { + fn get_u64_global(&self, idx: usize) -> Option; + fn set_u64_global(&mut self, idx: usize, val: u64) -> bool; + fn get_bytes32_global(&self, idx: usize) -> Option<&[u8; 32]>; + fn set_bytes32_global(&mut self, idx: usize, val: [u8; 32]) -> bool; + fn get_sequencer_message(&self, num: u64) -> Option<&[u8]>; + fn get_delayed_message(&self, num: u64) -> Option<&[u8]>; + fn get_preimage(&self, preimage_type: u8, hash: &[u8; 32]) -> Option<&[u8]>; +} + +/// Reads 32-bytes of global state and writes to guest memory. +pub fn get_global_state_bytes32( + mem: &mut impl MemAccess, + io: &impl WavmIo, + idx: u32, + out_ptr: GuestPtr, +) -> Result<(), String> { + let Some(global) = io.get_bytes32_global(idx as usize) else { + return Err("global read out of bounds in wavmio.getGlobalStateBytes32".into()); + }; + mem.write_slice(out_ptr, &global[..]); + Ok(()) +} + +/// Reads 32-bytes from guest memory and writes to global state. +pub fn set_global_state_bytes32( + mem: &impl MemAccess, + io: &mut impl WavmIo, + idx: u32, + src_ptr: GuestPtr, +) -> Result<(), String> { + let val = mem.read_fixed(src_ptr); + if !io.set_bytes32_global(idx as usize, val) { + return Err("global write oob in wavmio.setGlobalStateBytes32".into()); + } + Ok(()) +} + +/// Reads 8-bytes of global state. +pub fn get_global_state_u64(io: &impl WavmIo, idx: u32) -> Result { + match io.get_u64_global(idx as usize) { + Some(val) => Ok(val), + None => Err("global read out of bounds in wavmio.getGlobalStateU64".into()), + } +} + +/// Writes 8-bytes of global state. +pub fn set_global_state_u64(io: &mut impl WavmIo, idx: u32, val: u64) -> Result<(), String> { + if !io.set_u64_global(idx as usize, val) { + return Err("global write out of bounds in wavmio.setGlobalStateU64".into()); + } + Ok(()) +} + +/// Reads up to 32 bytes of a sequencer inbox message at the given offset. +pub fn read_inbox_message( + mem: &mut impl MemAccess, + io: &impl WavmIo, + msg_num: u64, + offset: u32, + out_ptr: GuestPtr, +) -> Result { + let message = io + .get_sequencer_message(msg_num) + .ok_or(format!("missing sequencer inbox message {msg_num}"))?; + read_message(mem, message, offset, out_ptr) +} + +/// Reads up to 32 bytes of a delayed inbox message at the given offset. +pub fn read_delayed_inbox_message( + mem: &mut impl MemAccess, + io: &impl WavmIo, + msg_num: u64, + offset: u32, + out_ptr: GuestPtr, +) -> Result { + let message = io + .get_delayed_message(msg_num) + .ok_or(format!("missing delayed inbox message {msg_num}"))?; + read_message(mem, message, offset, out_ptr) +} + +fn read_message( + mem: &mut impl MemAccess, + message: &[u8], + offset: u32, + out_ptr: GuestPtr, +) -> Result { + let offset = offset as usize; + let len = min(32, message.len().saturating_sub(offset)); + let read = message.get(offset..(offset + len)).unwrap_or_default(); + mem.write_slice(out_ptr, read); + Ok(read.len() as u32) +} + +/// Looks up a preimage by type and hash, reads up to 32 bytes at an aligned offset. +pub fn resolve_preimage( + mem: &mut impl MemAccess, + io: &impl WavmIo, + preimage_type: u8, + hash_ptr: GuestPtr, + offset: u32, + out_ptr: GuestPtr, + name: &str, +) -> Result { + let hash = mem.read_fixed(hash_ptr); + let offset = offset as usize; + + let Some(preimage) = io.get_preimage(preimage_type, &hash) else { + let hash_hex = hex::encode(hash); + return Err(format!( + "Missing requested preimage for hash {hash_hex} in {name}" + )); + }; + + if offset % 32 != 0 { + return Err(format!("bad offset {offset} in {name}")); + } + + let len = min(32, preimage.len().saturating_sub(offset)); + let read = preimage.get(offset..(offset + len)).unwrap_or_default(); + mem.write_slice(out_ptr, read); + Ok(read.len() as u32) +} + +/// Returns 1 if a preimage exists for the given type and hash, 0 otherwise. +pub fn validate_certificate( + mem: &impl MemAccess, + io: &impl WavmIo, + preimage_type: u8, + hash_ptr: GuestPtr, +) -> u8 { + let hash = mem.read_fixed(hash_ptr); + match io.get_preimage(preimage_type, &hash) { + Some(_) => 1, + None => 0, + } +} diff --git a/crates/jit/src/caller_env.rs b/crates/jit/src/caller_env.rs index a30662049f..d8639fd1cf 100644 --- a/crates/jit/src/caller_env.rs +++ b/crates/jit/src/caller_env.rs @@ -3,7 +3,7 @@ use crate::machine::{WasmEnv, WasmEnvMut}; use arbutil::{Bytes20, Bytes32}; -use caller_env::{ExecEnv, GuestPtr, MemAccess}; +use caller_env::{wavmio::WavmIo, ExecEnv, GuestPtr, MemAccess}; use rand::RngCore; use std::mem::{self, MaybeUninit}; use wasmer::{Memory, MemoryView, StoreMut, WasmPtr}; @@ -132,3 +132,48 @@ impl ExecEnv for JitExecEnv<'_> { } } } + +impl WavmIo for WasmEnv { + fn get_u64_global(&self, idx: usize) -> Option { + self.small_globals.get(idx).copied() + } + + fn set_u64_global(&mut self, idx: usize, val: u64) -> bool { + let Some(g) = self.small_globals.get_mut(idx) else { + return false; + }; + *g = val; + true + } + + fn get_bytes32_global(&self, idx: usize) -> Option<&[u8; 32]> { + self.large_globals.get(idx).map(|b| &b.0) + } + + fn set_bytes32_global(&mut self, idx: usize, val: [u8; 32]) -> bool { + let Some(g) = self.large_globals.get_mut(idx) else { + return false; + }; + *g = val.into(); + true + } + + fn get_sequencer_message(&self, num: u64) -> Option<&[u8]> { + self.sequencer_messages.get(&num).map(|v| v.as_slice()) + } + + fn get_delayed_message(&self, num: u64) -> Option<&[u8]> { + self.delayed_messages.get(&num).map(|v| v.as_slice()) + } + + fn get_preimage(&self, preimage_type: u8, hash: &[u8; 32]) -> Option<&[u8]> { + let Ok(pt) = preimage_type.try_into() else { + eprintln!("Go trying to get a preimage with unknown type {preimage_type}"); + return None; + }; + self.preimages + .get(&pt) + .and_then(|m| m.get(&Bytes32(*hash))) + .map(|v| v.as_slice()) + } +} diff --git a/crates/jit/src/wavmio.rs b/crates/jit/src/wavmio.rs index 97d07e4e2a..f8c9b76326 100644 --- a/crates/jit/src/wavmio.rs +++ b/crates/jit/src/wavmio.rs @@ -6,7 +6,7 @@ use crate::{ machine::{Escape, MaybeEscape, WasmEnv, WasmEnvMut}, }; use arbutil::Color; -use caller_env::{GuestPtr, MemAccess}; +use caller_env::GuestPtr; use std::{ io, io::{BufReader, BufWriter, ErrorKind}, @@ -20,49 +20,29 @@ use validation::transfer::receive_validation_input; pub fn get_global_state_bytes32(mut env: WasmEnvMut, idx: u32, out_ptr: GuestPtr) -> MaybeEscape { let (mut mem, exec) = env.jit_env(); ready_hostio(exec)?; - - let Some(global) = exec.large_globals.get(idx as usize) else { - return Escape::hostio("global read out of bounds in wavmio.getGlobalStateBytes32"); - }; - mem.write_slice(out_ptr, &global[..32]); - Ok(()) + caller_env::wavmio::get_global_state_bytes32(&mut mem, exec, idx, out_ptr) + .map_err(Escape::HostIO) } /// Writes 32-bytes of global state. pub fn set_global_state_bytes32(mut env: WasmEnvMut, idx: u32, src_ptr: GuestPtr) -> MaybeEscape { let (mem, exec) = env.jit_env(); ready_hostio(exec)?; - - let slice = mem.read_slice(src_ptr, 32); - let slice = &slice.try_into().unwrap(); - match exec.large_globals.get_mut(idx as usize) { - Some(global) => *global = *slice, - None => return Escape::hostio("global write oob in wavmio.setGlobalStateBytes32"), - }; - Ok(()) + caller_env::wavmio::set_global_state_bytes32(&mem, exec, idx, src_ptr).map_err(Escape::HostIO) } /// Reads 8-bytes of global state pub fn get_global_state_u64(mut env: WasmEnvMut, idx: u32) -> Result { let (_, exec) = env.jit_env(); ready_hostio(exec)?; - - match exec.small_globals.get(idx as usize) { - Some(global) => Ok(*global), - None => Escape::hostio("global read out of bounds in wavmio.getGlobalStateU64"), - } + caller_env::wavmio::get_global_state_u64(exec, idx).map_err(Escape::HostIO) } /// Writes 8-bytes of global state pub fn set_global_state_u64(mut env: WasmEnvMut, idx: u32, val: u64) -> MaybeEscape { let (_, exec) = env.jit_env(); ready_hostio(exec)?; - - match exec.small_globals.get_mut(idx as usize) { - Some(global) => *global = val, - None => return Escape::hostio("global write out of bounds in wavmio.setGlobalStateU64"), - } - Ok(()) + caller_env::wavmio::set_global_state_u64(exec, idx, val).map_err(Escape::HostIO) } /// Reads an inbox message. @@ -74,16 +54,8 @@ pub fn read_inbox_message( ) -> Result { let (mut mem, exec) = env.jit_env(); ready_hostio(exec)?; - - let message = match exec.sequencer_messages.get(&msg_num) { - Some(message) => message, - None => return Escape::hostio(format!("missing sequencer inbox message {msg_num}")), - }; - let offset = offset as usize; - let len = std::cmp::min(32, message.len().saturating_sub(offset)); - let read = message.get(offset..(offset + len)).unwrap_or_default(); - mem.write_slice(out_ptr, read); - Ok(read.len() as u32) + caller_env::wavmio::read_inbox_message(&mut mem, exec, msg_num, offset, out_ptr) + .map_err(Escape::HostIO) } /// Reads a delayed inbox message. @@ -95,16 +67,8 @@ pub fn read_delayed_inbox_message( ) -> Result { let (mut mem, exec) = env.jit_env(); ready_hostio(exec)?; - - let message = match exec.delayed_messages.get(&msg_num) { - Some(message) => message, - None => return Escape::hostio(format!("missing delayed inbox message {msg_num}")), - }; - let offset = offset as usize; - let len = std::cmp::min(32, message.len().saturating_sub(offset)); - let read = message.get(offset..(offset + len)).unwrap_or_default(); - mem.write_slice(out_ptr, read); - Ok(read.len() as u32) + caller_env::wavmio::read_delayed_inbox_message(&mut mem, exec, msg_num, offset, out_ptr) + .map_err(Escape::HostIO) } /// Retrieves the preimage of the given hash. @@ -144,62 +108,54 @@ pub fn resolve_preimage_impl( name: &str, ) -> Result { let (mut mem, exec) = env.jit_env(); - let offset = offset as usize; + ready_hostio(exec)?; - let Ok(preimage_type) = preimage_type.try_into() else { + if TryInto::::try_into(preimage_type).is_err() { eprintln!("Go trying to resolve pre image with unknown type {preimage_type}"); return Ok(0); - }; - - macro_rules! error { - ($text:expr $(,$args:expr)*) => {{ - let text = format!($text $(,$args)*); - return Escape::hostio(&text) - }}; } - let hash = mem.read_bytes32(hash_ptr); - - let Some(preimage) = exec - .preimages - .get(&preimage_type) - .and_then(|m| m.get(&hash)) - else { - let hash_hex = hex::encode(hash); - error!("Missing requested preimage for hash {hash_hex} in {name}") - }; - #[cfg(debug_assertions)] { use arbutil::PreimageType; + use caller_env::MemAccess; use sha2::Sha256; use sha3::{Digest, Keccak256}; - // Check if preimage rehashes to the provided hash. Exclude blob preimages - let calculated_hash: [u8; 32] = match preimage_type { - PreimageType::Keccak256 => Keccak256::digest(preimage).into(), - PreimageType::Sha2_256 => Sha256::digest(preimage).into(), - PreimageType::EthVersionedHash => *hash, - PreimageType::DACertificate => *hash, // Can't verify DACertificate hash, just accept it - }; - if calculated_hash != *hash { - error!( - "Calculated hash {} of preimage {} does not match provided hash {}", - hex::encode(calculated_hash), - hex::encode(preimage), - hex::encode(*hash) - ); + let hash: [u8; 32] = mem.read_fixed(hash_ptr); + let preimage_type: PreimageType = preimage_type.try_into().unwrap(); + if let Some(preimage) = exec + .preimages + .get(&preimage_type) + .and_then(|m| m.get(&arbutil::Bytes32(hash))) + { + let calculated_hash: [u8; 32] = match preimage_type { + PreimageType::Keccak256 => Keccak256::digest(preimage).into(), + PreimageType::Sha2_256 => Sha256::digest(preimage).into(), + PreimageType::EthVersionedHash => hash, + PreimageType::DACertificate => hash, + }; + if calculated_hash != hash { + return Escape::hostio(format!( + "Calculated hash {} of preimage {} does not match provided hash {}", + hex::encode(calculated_hash), + hex::encode(preimage), + hex::encode(hash) + )); + } } } - if offset % 32 != 0 { - error!("bad offset {offset} in {name}") - }; - - let len = std::cmp::min(32, preimage.len().saturating_sub(offset)); - let read = preimage.get(offset..(offset + len)).unwrap_or_default(); - mem.write_slice(out_ptr, read); - Ok(read.len() as u32) + caller_env::wavmio::resolve_preimage( + &mut mem, + exec, + preimage_type, + hash_ptr, + offset, + out_ptr, + name, + ) + .map_err(Escape::HostIO) } pub fn validate_certificate( @@ -207,24 +163,13 @@ pub fn validate_certificate( preimage_type: u8, hash_ptr: GuestPtr, ) -> Result { - let (mut mem, exec) = env.jit_env(); - let hash = mem.read_bytes32(hash_ptr); - - let Ok(preimage_type) = preimage_type.try_into() else { - eprintln!( - "Go trying to validate certificate for preimage with unknown type {preimage_type}" - ); - return Ok(0); - }; - - // Check if preimage exists - let exists = exec - .preimages - .get(&preimage_type) - .and_then(|m| m.get(&hash)) - .is_some(); - - Ok(if exists { 1 } else { 0 }) + let (mem, exec) = env.jit_env(); + Ok(caller_env::wavmio::validate_certificate( + &mem, + exec, + preimage_type, + hash_ptr, + )) } fn ready_hostio(env: &mut WasmEnv) -> MaybeEscape {