diff --git a/src/riscv/lib/src/pvm.rs b/src/riscv/lib/src/pvm.rs index 158a987d12..88a29d8260 100644 --- a/src/riscv/lib/src/pvm.rs +++ b/src/riscv/lib/src/pvm.rs @@ -8,7 +8,7 @@ pub mod durable_storage; pub mod hooks; pub(crate) mod linux; pub mod node_pvm; -mod outbox; +pub mod outbox; mod reveals; mod tezos; diff --git a/src/riscv/lib/src/pvm/common.rs b/src/riscv/lib/src/pvm/common.rs index 409791ad59..934ae8b422 100644 --- a/src/riscv/lib/src/pvm/common.rs +++ b/src/riscv/lib/src/pvm/common.rs @@ -45,6 +45,9 @@ use tezos_smart_rollup_constants::riscv::SbiError; use super::durable_storage::DurableStorage; use super::linux; use super::outbox::Outbox; +use super::outbox::OutboxProof; +use super::outbox::OutboxProofError; +use super::outbox::OutputInfo; use super::reveals::RevealRequest; use crate::default::ConstDefault; use crate::machine_state; @@ -109,11 +112,11 @@ pub(crate) type PvmProve<'a, MC, DS> = Pvm>; /// Proof-generating virtual machine #[perfect_derive(Clone, PartialEq, Eq)] pub struct Pvm { + pub(crate) system_state: linux::SupervisorState, pub(crate) machine_state: machine_state::MachineState, pub(crate) durable_storage: DS, pub(crate) outbox: Outbox, pub(crate) reveal_request: RevealRequest, - pub(crate) system_state: linux::SupervisorState, version: Atom, pub(crate) tick: Atom, pub(crate) message_counter: Atom, @@ -374,6 +377,20 @@ where Ok(proof) } + + /// Produce an outbox proof by recording the Merkle proof of a state transition + /// in which the outbox message at the given level and index is read. + pub(crate) fn produce_outbox_proof( + &self, + output_info: OutputInfo, + ) -> Result { + let proof_output = self.get_outbox_message(output_info)?; + + let merkle_tree = MerkleTree::from_foldable(self); + let merkle_proof: MerkleProof = merkle_tree.compress(); + + Ok(OutboxProof::new(merkle_proof, proof_output.info)) + } } impl<'normal, MC: MemoryConfig, PC: PageCache, DS: Provable<'normal>> Provable<'normal> diff --git a/src/riscv/lib/src/pvm/node_pvm.rs b/src/riscv/lib/src/pvm/node_pvm.rs index 7b44855737..c3ea3310ca 100644 --- a/src/riscv/lib/src/pvm/node_pvm.rs +++ b/src/riscv/lib/src/pvm/node_pvm.rs @@ -26,6 +26,9 @@ use thiserror::Error; use super::Pvm; use super::durable_storage::DurableStorage; use super::durable_storage::DurableStorageDummy; +use super::outbox::OutboxProof; +use super::outbox::OutboxProofError; +use super::outbox::OutputInfo; use crate::machine_state::page_cache::EmptyPageCache; use crate::machine_state::page_cache::PageCache; use crate::machine_state::page_cache::PageCacheInterpreted; @@ -210,6 +213,20 @@ impl, DS: DurableStorage> NodePv let proof = proof_state.produce_proof().ok()?; Some(proof) } + + /// Produce an outbox proof by recording the Merkle proof of a state transition + /// in which the outbox message at the given level and index is read. + pub fn produce_outbox_proof<'normal>( + &'normal self, + output_info: OutputInfo, + ) -> Result + where + DS: Provable<'normal>, + DS::Prover: DurableStorage> + Foldable + Foldable, + { + let proof_state = self.state.start_proof(); + proof_state.produce_outbox_proof(output_info) + } } impl> NodePvm { diff --git a/src/riscv/lib/src/pvm/outbox.rs b/src/riscv/lib/src/pvm/outbox.rs index 3c2bcfc75b..30bbed0b95 100644 --- a/src/riscv/lib/src/pvm/outbox.rs +++ b/src/riscv/lib/src/pvm/outbox.rs @@ -34,22 +34,30 @@ use octez_riscv_data::foldable::Fold; use octez_riscv_data::foldable::Foldable; use octez_riscv_data::foldable::NodeFold; use octez_riscv_data::foldable::seq_tree::IndexableSeqAsTree; +use octez_riscv_data::hash::Hash; use octez_riscv_data::merkle_proof; use octez_riscv_data::merkle_proof::Deserialiser; use octez_riscv_data::merkle_proof::DeserialiserNode; use octez_riscv_data::merkle_proof::FromProof; use octez_riscv_data::merkle_proof::Suspended; use octez_riscv_data::merkle_proof::SuspendedResult; +use octez_riscv_data::merkle_proof::proof_tree::MerkleProof; use octez_riscv_data::mode::Mode; use octez_riscv_data::mode::Normal; use octez_riscv_data::mode::Provable; use octez_riscv_data::mode::Prove; use octez_riscv_data::mode::Verify; +use octez_riscv_data::serialisation::serialise; use perfect_derive::perfect_derive; use tezos_smart_rollup_constants::core::MAX_OUTPUT_SIZE; use tezos_smart_rollup_constants::riscv::SbiError; use thiserror::Error; +use super::Pvm; +use super::durable_storage::DurableStorage; +use crate::machine_state::memory::MemoryConfig; +use crate::machine_state::page_cache::PageCache; + /// Small outbox size for testing /// /// Currently, this is the length of the fixed-size array which holds all the outbox levels. @@ -66,24 +74,77 @@ const OUTBOX_MERKLE_ARITY: usize = 2; /// The arity used to Merkleise arrays in each level const LEVEL_MERKLE_ARITY: usize = 2; +/// The outbox level and the index within that level for an outbox message +#[derive(Debug, PartialEq, Eq, Copy, Clone, Encode)] +pub struct OutputInfo { + pub level: u32, + pub index: u32, +} + +/// A raw outbox message and its outbox information +#[derive(Debug, PartialEq, Eq)] +pub struct Output { + pub message: OutboxMessage, + pub info: OutputInfo, +} + +/// Errors which can be raised when producing or verifying an outbox proof +#[derive(Error, Debug, PartialEq, Eq)] +pub enum OutboxProofError { + #[error("The outbox does not contain the level {level}")] + LevelNotFound { level: u32 }, + + #[error("The outbox for level {} does not contain a message at index {}", info.level, info.index)] + MessageNotFound { info: OutputInfo }, + + #[error(transparent)] + MessageError(#[from] OutboxMessageError), +} + +/// Errors which can be raised when writing a message to the outbox #[derive(Error, Debug)] -pub(crate) enum OutboxError { +pub(crate) enum OutboxWriteError { #[error("Outbox is full")] - OutboxFull, + FullOutbox, - #[error("Outbox message exceeds allowable size of {MAX_OUTPUT_SIZE}. Found: {size}")] - OutboxMessageTooLarge { size: usize }, + #[error(transparent)] + MessageError(#[from] OutboxMessageError), } -impl From for SbiError { - fn from(value: OutboxError) -> Self { - match value { - OutboxError::OutboxFull => Self::FullOutbox, - OutboxError::OutboxMessageTooLarge { .. } => Self::OutputTooLarge, +impl From for SbiError { + fn from(err: OutboxWriteError) -> Self { + match err { + OutboxWriteError::FullOutbox => Self::FullOutbox, + OutboxWriteError::MessageError(e) => e.into(), } } } +/// An outbox proof, containing a partial Merkle tree of a PVM state which ties +/// an outbox message with the PVM state in which the outbox includes it +#[derive(Debug, Encode)] +pub struct OutboxProof { + proof: MerkleProof, + info: OutputInfo, +} + +impl OutboxProof { + /// Create a new outbox proof from the given Merkle proof and output information + pub(crate) fn new(proof: MerkleProof, info: OutputInfo) -> Self { + Self { proof, info } + } + + /// Get the state hash of the outbox proof + pub fn state_hash(&self) -> Hash { + self.proof.root_hash() + } + + /// Serialise the outbox proof + pub fn serialise(&self) -> Vec { + serialise(self).expect("Serialisation of an outbox proof should not fail") + } +} + /// Outbox state #[perfect_derive(Clone, PartialEq, Eq)] pub struct Outbox { @@ -98,8 +159,9 @@ impl Outbox { } } - /// Write `message` to the outbox at the current level. Returns `Err(OutboxFull)` - /// if the outbox is full. + /// Write `message` to the outbox at the current level + /// + /// Returns `OutboxWriteError::FullOutbox` if the outbox is full. /// /// # Panics /// @@ -109,14 +171,30 @@ impl Outbox { &mut self, message: OutboxMessage, current_level: u32, - ) -> Result<(), OutboxError> { + ) -> Result<(), OutboxWriteError> { let level_index = self.level_index(current_level); self.levels[level_index].write_message(message, current_level) } + /// Get the internal index in the outbox corresponding to the given level fn level_index(&self, level: u32) -> usize { level as usize % self.levels.len() } + + /// Read the message associated with the given level and index from outbox + fn read_message(&self, info: OutputInfo) -> Result { + let level_index = self.level_index(info.level); + let message = self.levels[level_index].read_message(info)?; + Ok(Output { + info, + message: message.try_into()?, + }) + } + + /// Get the number of levels stored in the outbox + fn len(&self) -> usize { + self.levels.len() + } } impl Outbox { @@ -198,6 +276,45 @@ impl Decode for Outbox { } } +impl, DS: DurableStorage, M: Mode> Pvm { + /// Get the outbox message at the given level and index. This is the state transition + /// captured in outbox proofs. + pub fn get_outbox_message(&self, info: OutputInfo) -> Result + where + M: AtomMode, + { + // This check reads the current level which ensures it is also included in the + // proof when running in `Prove` mode. + self.check_level_in_outbox(info.level)?; + + self.outbox.read_message(info) + } + + fn check_level_in_outbox(&self, level: u32) -> Result<(), OutboxProofError> + where + M: AtomMode, + { + // An uninitialised outbox contains no levels + if !self.level_is_set.read() { + return Err(OutboxProofError::LevelNotFound { level }); + } + let current_level = self.level.read(); + + // A future level is not in the outbox + if level > current_level { + return Err(OutboxProofError::LevelNotFound { level }); + } + + // Levels older than the size of the outbox are not in the outbox + let oldest_outbox_level = current_level.saturating_sub(self.outbox.len() as u32 - 1); + if level < oldest_outbox_level { + return Err(OutboxProofError::LevelNotFound { level }); + } + + Ok(()) + } +} + #[perfect_derive(Clone, PartialEq, Eq)] struct OutboxLevel { messages: Box<[Atom, M>]>, @@ -212,7 +329,7 @@ impl OutboxLevel { &mut self, message: OutboxMessage, current_level: u32, - ) -> Result<(), OutboxError> { + ) -> Result<(), OutboxWriteError> { let last_written_level = self.level.read(); assert!( current_level >= last_written_level, @@ -226,7 +343,7 @@ impl OutboxLevel { let next_index = self.next_index.read() as usize; if next_index >= MAX_LEVEL_SIZE { - return Err(OutboxError::OutboxFull); + return Err(OutboxWriteError::FullOutbox); } self.messages[next_index].write(message.0); @@ -234,6 +351,13 @@ impl OutboxLevel { Ok(()) } + + fn read_message(&self, info: OutputInfo) -> Result, OutboxProofError> { + if self.level.read() != info.level || info.index >= self.next_index.read() { + return Err(OutboxProofError::MessageNotFound { info }); + } + Ok(self.messages[info.index as usize].clone()) + } } impl OutboxLevel { @@ -361,25 +485,52 @@ impl Decode for OutboxLevel { } } +#[derive(Error, Debug, PartialEq, Eq)] +pub enum OutboxMessageError { + #[error( + "The size of the outbox message is {size} B, which is larger than the maximum message size ({MAX_OUTPUT_SIZE})." + )] + MessageTooLarge { size: usize }, +} + +impl From for SbiError { + fn from(err: OutboxMessageError) -> Self { + match err { + OutboxMessageError::MessageTooLarge { .. } => Self::OutputTooLarge, + } + } +} + /// An Outbox Message is a boxed byte slice, restricted to at most [`MAX_OUTPUT_SIZE`] /// in length #[derive(Clone, Debug, PartialEq, Eq)] #[repr(transparent)] -pub(crate) struct OutboxMessage(Box<[u8]>); +pub struct OutboxMessage(Box<[u8]>); impl OutboxMessage { /// Constructs a zeroed, boxed outbox message buffer of size `size` /// /// Fails if `size` exceeds [`MAX_OUTPUT_SIZE`] - pub(crate) fn new(size: usize) -> Result { + pub(crate) fn new(size: usize) -> Result { if size > MAX_OUTPUT_SIZE { - return Err(OutboxError::OutboxMessageTooLarge { size }); + return Err(OutboxMessageError::MessageTooLarge { size }); } let boxed_slice = vec![0u8; size].into_boxed_slice(); Ok(OutboxMessage(boxed_slice)) } } +impl TryFrom> for OutboxMessage { + type Error = OutboxMessageError; + + fn try_from(value: Box<[u8]>) -> Result { + if value.len() > MAX_OUTPUT_SIZE { + return Err(OutboxMessageError::MessageTooLarge { size: value.len() }); + } + Ok(OutboxMessage(value)) + } +} + impl Deref for OutboxMessage { type Target = [u8]; @@ -404,6 +555,9 @@ mod tests { use proptest::prelude::*; use super::*; + use crate::machine_state::memory::M1M; + use crate::machine_state::page_cache::EmptyPageCache; + use crate::pvm::durable_storage::DurableStorageDummy; fn safe_size_range(size_range: impl RangeBounds) -> RangeInclusive { let start_bound = match size_range.start_bound() { @@ -434,6 +588,20 @@ mod tests { proptest::collection::vec(message_strategy(size_range), len) } + #[test] + fn test_outbox_message_too_large() { + let size = MAX_OUTPUT_SIZE + 1; + assert_eq!( + OutboxMessage::try_from(vec![1u8; size].into_boxed_slice()), + Err(OutboxMessageError::MessageTooLarge { size }) + ); + + proptest!(|(size in MAX_OUTPUT_SIZE + 1..)| { + let res = OutboxMessage::new(size).unwrap_err(); + assert!(matches!(res.into(), SbiError::OutputTooLarge)); + }) + } + #[test] fn write_messages_with_varying_sizes() { proptest!(|( @@ -492,24 +660,16 @@ mod tests { // Verify that outbox is full assert_eq!(*outbox.levels[0].next_index, MAX_LEVEL_SIZE as u32); - prop_assert!(matches!(outbox.write_message(message.clone(), 0), Err(OutboxError::OutboxFull))); + prop_assert!(matches!(outbox.write_message(message.clone(), 0), Err(OutboxWriteError::FullOutbox))); prop_assert_eq!(*outbox.levels[0].next_index, MAX_LEVEL_SIZE as u32); } }); } - #[test] - fn oversized_messages_cannot_be_created() { - proptest!(|(size in MAX_OUTPUT_SIZE + 1..=usize::MAX)| { - let res = OutboxMessage::new(size); - assert!(matches!(res, Err(OutboxError::OutboxMessageTooLarge { size: s }) if s == size)); - }) - } - #[test] fn read_level_after_write() { proptest!(|( - messages in proptest::collection::vec(message_strategy(1..=MAX_OUTPUT_SIZE), 1..MAX_LEVEL_SIZE), + messages in proptest::collection::vec(message_strategy(1..), 1..MAX_LEVEL_SIZE), level in 0u32..1000 )| { let mut outbox = Outbox::::default(); @@ -559,4 +719,243 @@ mod tests { prop_assert_eq!(result.len(), 0) }); } + + #[test] + fn test_read_message() { + proptest!(|( + messages in messages_strategy(0.., 5), + write_level in 0u32..1000 + )| { + let mut outbox = Outbox::::default(); + + // Write messages at write_level + for message in &messages { + prop_assert!(outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Read messages back + for (i, message) in messages.iter().enumerate() { + let info = OutputInfo { + level: write_level, + index: i as u32, + }; + let output = outbox.read_message(info).unwrap(); + prop_assert_eq!(&*output.message, &*message.0); + prop_assert_eq!(output.info, info); + } + }); + } + + #[test] + fn test_read_message_with_invalid_index_fails() { + proptest!(|( + messages in messages_strategy(0.., 5), + write_level in 0u32..1000, + invalid_offset in 0usize..10 + )| { + let mut outbox = Outbox::::default(); + + // Write N messages at write_level + for message in &messages { + prop_assert!(outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Try to read with index >= N + let invalid_index = messages.len() + invalid_offset; + let info = OutputInfo { + level: write_level, + index: invalid_index as u32, + }; + let output = outbox.read_message(info); + prop_assert_eq!(output, Err(OutboxProofError::MessageNotFound { info })); + }); + } + + #[test] + fn test_read_message_after_level_wraparound() { + proptest!(|( + messages in messages_strategy(0.., 15), + write_level in 0u32..1000 + )| { + let mut outbox = Outbox::::default(); + + // Write messages at write_level + for message in &messages { + prop_assert!(outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Try to read at the wrapped level without writing to it first + // The wrapped level maps to the same outbox slot but differs from the stored level + let wrapped_level = write_level + TEST_OUTBOX_SIZE as u32; + for i in 0..messages.len() { + let info = OutputInfo { + level: wrapped_level, + index: i as u32, + }; + let output = outbox.read_message(info); + prop_assert_eq!(output, Err(OutboxProofError::MessageNotFound { info })); + } + }); + } + + #[test] + fn test_read_message_from_empty_level_fails() { + proptest!(|( + level in 0u32..1000, + index in 0u32..MAX_LEVEL_SIZE as u32 + )| { + let outbox = Outbox::::default(); + + let info = OutputInfo { level, index }; + let output = outbox.read_message(info); + prop_assert_eq!(output, Err(OutboxProofError::MessageNotFound { info })); + }); + } + + #[test] + fn test_get_outbox_message_from_future_level_fails() { + proptest!(|( + messages in messages_strategy(0.., 5), + write_level in 0u32..1000 + )| { + type MC = M1M; + type PC = EmptyPageCache; + type DS = DurableStorageDummy; + + let mut pvm = Pvm::::default(); + pvm.reset(); + + // Getting a message from an uninitialised outbox fails + let info = OutputInfo { level: 0, index: 0 }; + let output = pvm.get_outbox_message(info); + prop_assert_eq!(output, Err(OutboxProofError::LevelNotFound { level: info.level })); + + pvm.level_is_set.write(true); + pvm.level.write(write_level); + + // Write messages at write_level + for message in &messages { + prop_assert!(pvm.outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Getting a message at a future level fails + let info = OutputInfo { + level: write_level + 1, + index: 0, + }; + let output = pvm.get_outbox_message(info); + prop_assert_eq!(output, Err(OutboxProofError::LevelNotFound { level: info.level })); + }) + } + + #[test] + fn test_get_outbox_message_from_valid_level() { + proptest!(|( + messages in messages_strategy(0.., 5), + write_level in 0u32..1000 + )| { + type MC = M1M; + type PC = EmptyPageCache; + type DS = DurableStorageDummy; + + let mut pvm = Pvm::::default(); + pvm.reset(); + + pvm.level_is_set.write(true); + pvm.level.write(write_level); + + // Write messages at write_level + for message in &messages { + prop_assert!(pvm.outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Read messages back at write_level + for (i, message) in messages.iter().enumerate() { + let info = OutputInfo { + level: write_level, + index: i as u32, + }; + let output = pvm.get_outbox_message(info).unwrap(); + prop_assert_eq!(&output.message, message); + prop_assert_eq!(output.info, info); + } + + // Also verify we can read with current_level up to write_level + TEST_OUTBOX_SIZE - 1 + let future_level = write_level + (TEST_OUTBOX_SIZE as u32) - 1; + pvm.level.write(future_level); + + for (i, message) in messages.iter().enumerate() { + let info = OutputInfo { + level: write_level, + index: i as u32, + }; + let output = pvm.get_outbox_message(info).unwrap(); + prop_assert_eq!(&output.message, message); + } + }); + } + + #[test] + fn test_get_outbox_message_from_old_level_fails() { + proptest!(|( + first_messages in messages_strategy(0.., 15), + second_messages in messages_strategy(0.., 5), + write_level in TEST_OUTBOX_SIZE as u32..1000 + )| { + type MC = M1M; + type PC = EmptyPageCache; + type DS = DurableStorageDummy; + + let mut pvm = Pvm::::default(); + pvm.reset(); + + let m = first_messages.len(); + let n = second_messages.len(); + + // Write M messages at write_level + for message in &first_messages { + prop_assert!(pvm.outbox.write_message(message.clone(), write_level).is_ok()); + } + + // Write N messages at write_level + TEST_OUTBOX_SIZE (where N < M) + let wrapped_level = write_level + TEST_OUTBOX_SIZE as u32; + for message in &second_messages { + prop_assert!(pvm.outbox.write_message(message.clone(), wrapped_level).is_ok()); + } + + // Set up PVM level at the wrapped level + pvm.level_is_set.write(true); + pvm.level.write(wrapped_level); + + // Reading at old level should fail + for i in 0..m { + let info = OutputInfo { + level: write_level, + index: i as u32, + }; + let output = pvm.get_outbox_message(info); + prop_assert_eq!(output, Err(OutboxProofError::LevelNotFound { level: info.level })) + } + + // Reading at wrapped level for indices 0..N should work + for (i, message) in second_messages.iter().enumerate() { + let info = OutputInfo { + level: wrapped_level, + index: i as u32, + }; + let output = pvm.get_outbox_message(info).unwrap(); + prop_assert_eq!(&output.message, message); + } + + // Reading at wrapped level for indices N..M should fail + for i in n..m { + let info = OutputInfo { + level: wrapped_level, + index: i as u32, + }; + let output = pvm.get_outbox_message(info); + prop_assert_eq!(output, Err(OutboxProofError::MessageNotFound { info })); + } + }); + } } diff --git a/src/riscv/lib/src/pvm/reveals.rs b/src/riscv/lib/src/pvm/reveals.rs index 41dafcc1b0..4b2a30faa9 100644 --- a/src/riscv/lib/src/pvm/reveals.rs +++ b/src/riscv/lib/src/pvm/reveals.rs @@ -32,7 +32,7 @@ use tezos_smart_rollup_constants::riscv::REVEAL_REQUEST_MAX_SIZE; #[perfect_derive(Clone, PartialEq, Eq)] pub struct RevealRequest { /// Reveal request payload - pub bytes: Atom<[u8; REVEAL_REQUEST_MAX_SIZE], M>, + pub bytes: Atom, M>, /// Size of reveal request payload pub size: Atom, } @@ -61,7 +61,7 @@ impl<'normal> Provable<'normal> for RevealRequest { impl Default for RevealRequest { fn default() -> Self { Self { - bytes: Atom::new([0; REVEAL_REQUEST_MAX_SIZE]), + bytes: Atom::new(crate::array_utils::boxed_from_fn(|| 0)), size: Atom::default(), } } @@ -80,7 +80,7 @@ impl Foldable for RevealRequest where M: Mode, F: Fold, - Atom<[u8; REVEAL_REQUEST_MAX_SIZE], M>: Foldable, + Atom, M>: Foldable, Atom: Foldable, { fn fold(&self, builder: F) -> F::Folded { diff --git a/src/riscv/lib/src/stepper/pvm.rs b/src/riscv/lib/src/stepper/pvm.rs index 55c55e209b..342b20f7bd 100644 --- a/src/riscv/lib/src/stepper/pvm.rs +++ b/src/riscv/lib/src/stepper/pvm.rs @@ -46,6 +46,9 @@ use crate::pvm::durable_storage::DurableStorage; use crate::pvm::durable_storage::DurableStorageDummy; use crate::pvm::hooks::NoHooks; use crate::pvm::hooks::PvmHooks; +use crate::pvm::outbox::OutboxProof; +use crate::pvm::outbox::OutboxProofError; +use crate::pvm::outbox::OutputInfo; use crate::range_utils::bound_saturating_sub; use crate::state_backend::OwnedProofPart; use crate::state_backend::ProofPart; @@ -172,6 +175,21 @@ impl, DS: DurableStorage> let proof = proof_stepper.pvm.produce_proof().ok()?; Some(proof) } + + /// Produce an outbox proof by recording the Merkle proof of a state transition + /// in which the outbox message at the given level and index is read. + pub fn produce_outbox_proof<'normal>( + &'normal self, + output_info: OutputInfo, + ) -> Result + where + MC::State>: Foldable + Foldable, + DS: Provable<'normal>, + DS::Prover: DurableStorage> + Foldable + Foldable, + { + let proof_stepper = self.start_proof_mode(); + proof_stepper.pvm.produce_outbox_proof(output_info) + } } impl< @@ -364,6 +382,14 @@ impl< pub fn eval_one(&mut self) { self.pvm.eval_one(&mut self.hooks) } + + /// Get the current level of the PVM + pub fn level(&self) -> Option { + if !self.pvm.level_is_set.read() { + return None; + } + Some(self.pvm.level.read()) + } } impl> diff --git a/src/riscv/lib/tests/expected/dummy/outbox_proof b/src/riscv/lib/tests/expected/dummy/outbox_proof new file mode 100644 index 0000000000..367c3869d2 --- /dev/null +++ b/src/riscv/lib/tests/expected/dummy/outbox_proof @@ -0,0 +1 @@ +00029c401249572466729f6bb55a434fa4ffc2f4f910e1cb4c5b41b5c9f92291cec602af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f326200000002c830a4af5c80e44af05f1d006ecc21b0ca7f1d4432b2d05e44eec84e1acaf3ed00000000000000000003001000000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101026c3812f50acc5d9f5273abcfeb969230b961e955512ecf4881166b3f4e3d6b5402a3ff93137f3daef61d880ac235e36e48a8640359732c60f20fb8f22231b41acd026566550afe07ad133da70a513cabafa0dfa4d153ad22e9ffcd575859e5f1ffef02759315f2c7a728392f3b9d672742e17f432e2fd74f38f64518939c8dcdaf4b9f02838d59d33e55962bd15a1fced0fe477c76bd44ce3cfb6a51322c82076fff5aea0269372d265ea7b660ca68f1a93bebe0028b24009473e588b4d6c4c73608734f870274a4577e6213a24151a279c7def264948fd6dd79b5c525c4ca6faaad9a15ac3b0364000000031200000002e05fb9434dabcda489d3b76c07c491e0a78a50eb77acaab15e06c81662c533040269bc02600b721fec28f175e9e4e12581e56e82d1ae27451d4b27c516ec76332802dc85cae0f6a64dcb04fe814e18fa81d7e3d31d2f5eadc7ec45a38fdfc835b85802ce88c99cc00efc09f817ec7232a94d51686b9b40a0fe8dd6dd2cb0f8ea9deb7c021816c416b9fc35a7f23c144e65284d334cfc49a88e9e7f22caaf124d81833db40271e0a99173564931c0b8acc52d2685a8e39c64dc52e3d02390fdac2a12b155cb02d701df1869ad5b18463400ba35cc7649e110010bbc75576b8946031793591cb402ea21c7b3c885cf2fb547bb51016c8bbbaca21db264cc9f332769ad8dd895f0a30312000000030102c610e85212d0697cb161d4ba431ba603f273feee7dcb7927c9ff5d74ae6cbfa31200000000000000 diff --git a/src/riscv/lib/tests/test_determinism.rs b/src/riscv/lib/tests/test_determinism.rs index ba0b9dfe3f..191897d6b4 100644 --- a/src/riscv/lib/tests/test_determinism.rs +++ b/src/riscv/lib/tests/test_determinism.rs @@ -28,7 +28,7 @@ fn test_etherlink_determinism() { } fn test_determinism(inputs: TestConfig) { - let make_stepper = make_stepper_factory(&inputs); + let make_stepper = make_stepper_factory(&inputs, None, None); let mut base_stepper = make_stepper(); let base_result = base_stepper.step_max(Bound::Unbounded); diff --git a/src/riscv/lib/tests/test_outbox_proofs.rs b/src/riscv/lib/tests/test_outbox_proofs.rs new file mode 100644 index 0000000000..4ebbe5ab02 --- /dev/null +++ b/src/riscv/lib/tests/test_outbox_proofs.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2026 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +// This ensures that Clippy does't apply rules which are allowed in tests. +#![cfg(test)] + +use std::io::Write; +use std::ops::Bound; +use std::path::PathBuf; +use std::time::Instant; + +use octez_riscv::machine_state::memory::M64M; +use octez_riscv::pvm::outbox::OutboxProof; +use octez_riscv::pvm::outbox::OutputInfo; +use octez_riscv::stepper::Stepper; +use octez_riscv_test_utils::*; + +/// The maximum size in bytes expected for an outbox proof (message size is 4096 B) +const MAX_EXPECTED_OUTBOX_PROOF_SIZE: usize = 4770; + +const ROLLUP_ADDRESS: [u8; 20] = [ + 244, 228, 124, 179, 196, 58, 104, 176, 212, 142, 48, 148, 9, 44, 164, 45, 113, 58, 221, 181, +]; + +fn test_outbox_proofs(inputs: &TestConfig) { + let make_stepper = make_stepper_factory::( + inputs, + Some(ROLLUP_ADDRESS), + Some(PathBuf::from("../../../assets/preimages").into_boxed_path()), + ); + let mut stepper = make_stepper(); + + let _result = stepper.step_max(Bound::Unbounded); + + let output_info = OutputInfo { + level: stepper.level().unwrap(), + index: 0, + }; + + eprintln!( + "> Producing outbox proof for message at level {}, index {}...", + output_info.level, output_info.index + ); + let start = Instant::now(); + let proof = stepper.produce_outbox_proof(output_info).unwrap(); + let time = start.elapsed(); + + let proof_serialisation: Vec = OutboxProof::serialise(&proof); + let proof_size = proof_serialisation.len(); + + eprintln!("> Outbox proof of size {proof_size} B produced in {time:?}"); + + if proof_size > MAX_EXPECTED_OUTBOX_PROOF_SIZE { + panic!( + "Outbox proof expected to be at most {MAX_EXPECTED_OUTBOX_PROOF_SIZE} B. Please investigate: {proof:?}" + ) + }; + + assert_eq!(stepper.hash(), proof.state_hash()); + + let mut mint = goldenfile::Mint::new(inputs.golden_dir); + let mut proof_capture = mint.new_goldenfile("outbox_proof").unwrap(); + let proof_bytes = hex::encode(proof_serialisation); + writeln!(proof_capture, "{proof_bytes}").unwrap(); +} + +#[test] +fn test_outbox_proofs_dummy_kernel() { + test_outbox_proofs(&DUMMY) +} diff --git a/src/riscv/lib/tests/test_proofs.rs b/src/riscv/lib/tests/test_proofs.rs index 5aa79deca2..ada8e0d23a 100644 --- a/src/riscv/lib/tests/test_proofs.rs +++ b/src/riscv/lib/tests/test_proofs.rs @@ -88,7 +88,7 @@ fn test_etherlink_initial_proof_regression() { } fn test_initial_proof_regression(inputs: TestConfig) { - let make_stepper = make_stepper_factory::(&inputs); + let make_stepper = make_stepper_factory::(&inputs, None, None); let mut stepper = make_stepper(); eprintln!("> Producing proof ..."); @@ -109,7 +109,7 @@ where MC::State: Foldable, for<'a> MC::State>: Foldable + Foldable, { - let make_stepper = make_stepper_factory::(&inputs); + let make_stepper = make_stepper_factory::(&inputs, None, None); let mut base_stepper = make_stepper(); let base_result = base_stepper.step_max(Bound::Unbounded); diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 060d90ffc5..14748d76d7 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -102,6 +102,8 @@ impl Drop for TestableTmpdir { /// Return a function which can produce a [`PvmStepper`] over a given [`TestConfig`]. pub fn make_stepper_factory( inputs: &TestConfig, + address: Option<[u8; 20]>, + preimages_dir: Option>, ) -> impl Fn() -> PvmStepper { let program = fs::read(inputs.kernel_path).expect("Kernel path should be valid"); @@ -111,11 +113,18 @@ pub fn make_stepper_factory( .expect("Inbox path should be valid"); let inbox = inbox.build(); - let address = [0; 20]; + let address = address.unwrap_or([0; 20]); move || { - PvmStepper::::new(&program, inbox.clone(), NoHooks, address, 1, None) - .expect("PvmStepper initialisation arguments should be valid") + PvmStepper::::new( + &program, + inbox.clone(), + NoHooks, + address, + 1, + preimages_dir.clone(), + ) + .expect("PvmStepper initialisation arguments should be valid") } }