From 469a506058e65f49b14cb5c6dd591391cbbb0c02 Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Tue, 3 Feb 2026 11:29:53 +0000 Subject: [PATCH] feat: support read messages from outbox level false: Ryan Tan --- src/riscv/lib/src/pvm/outbox.rs | 129 +++++++++++++++++++++++++++++--- 1 file changed, 120 insertions(+), 9 deletions(-) diff --git a/src/riscv/lib/src/pvm/outbox.rs b/src/riscv/lib/src/pvm/outbox.rs index de0702754b..3c2bcfc75b 100644 --- a/src/riscv/lib/src/pvm/outbox.rs +++ b/src/riscv/lib/src/pvm/outbox.rs @@ -110,9 +110,25 @@ impl Outbox { message: OutboxMessage, current_level: u32, ) -> Result<(), OutboxError> { - let level_index = current_level as usize % self.levels.len(); + let level_index = self.level_index(current_level); self.levels[level_index].write_message(message, current_level) } + + fn level_index(&self, level: u32) -> usize { + level as usize % self.levels.len() + } +} + +impl Outbox { + /// Read outbox messages at the given level + /// + /// Warning: The caller must ensure that `level` is within the outbox + /// validity window + #[cfg_attr(not(test), expect(dead_code, reason = "outbox not in use"))] + pub(crate) fn read_level(&self, level: u32) -> Box<[Box<[u8]>]> { + let level_index = self.level_index(level); + self.levels[level_index].read_level(level) + } } impl<'normal> Provable<'normal> for Outbox { @@ -197,13 +213,13 @@ impl OutboxLevel { message: OutboxMessage, current_level: u32, ) -> Result<(), OutboxError> { - let previous_level = self.level.read(); + let last_written_level = self.level.read(); assert!( - current_level >= previous_level, - "current_level {current_level} must be gte to any level already stored in the outbox. Found {previous_level}" + current_level >= last_written_level, + "current_level {current_level} must be gte to any level already stored in the outbox. Found {last_written_level}" ); - if current_level > previous_level { + if current_level > last_written_level { self.next_index.write(0); self.level.write(current_level); } @@ -220,6 +236,27 @@ impl OutboxLevel { } } +impl OutboxLevel { + fn read_level(&self, level: u32) -> Box<[Box<[u8]>]> { + let last_written_level = self.level.read(); + debug_assert!( + level >= last_written_level, + "level {level} must be gte to the last written level for this outbox level slot. Found {last_written_level}" + ); + + let next_index = self.next_index.read() as usize; + if level != last_written_level || next_index == 0 { + // The outbox is empty for `level` + return Box::new([]); + } + + self.messages[..next_index] + .iter() + .map(|msg| Box::from(msg.as_ref())) + .collect::>() + } +} + impl<'normal> Provable<'normal> for OutboxLevel { type Prover = OutboxLevel>; @@ -359,19 +396,39 @@ impl DerefMut for OutboxMessage { #[cfg(test)] mod tests { + use std::ops::Bound::*; + use std::ops::RangeBounds; use std::ops::RangeInclusive; + use itertools::Itertools; use proptest::prelude::*; use super::*; - fn message_strategy(size_range: RangeInclusive) -> impl Strategy { - proptest::collection::vec(any::(), size_range) + fn safe_size_range(size_range: impl RangeBounds) -> RangeInclusive { + let start_bound = match size_range.start_bound() { + Included(s) => *s, + Excluded(s) => *s + 1, + Unbounded => 0, + }; + + match size_range.end_bound() { + Included(end) => start_bound..=MAX_OUTPUT_SIZE.min(*end), + Excluded(end) => start_bound..=MAX_OUTPUT_SIZE.min(end.saturating_sub(1)), + Unbounded => start_bound..=MAX_OUTPUT_SIZE, + } + } + + fn message_strategy( + size_range: impl RangeBounds, + ) -> impl Strategy { + let safe_range = safe_size_range(size_range); + proptest::collection::vec(any::(), safe_range) .prop_map(|data| OutboxMessage(data.into_boxed_slice())) } fn messages_strategy( - size_range: RangeInclusive, + size_range: impl RangeBounds, len: usize, ) -> impl Strategy> { proptest::collection::vec(message_strategy(size_range), len) @@ -380,7 +437,7 @@ mod tests { #[test] fn write_messages_with_varying_sizes() { proptest!(|( - messages in messages_strategy(0..=MAX_OUTPUT_SIZE, MAX_LEVEL_SIZE), + messages in messages_strategy(0.., MAX_LEVEL_SIZE), level in 0u32..TEST_OUTBOX_SIZE as u32 )| { let mut outbox = Outbox::::default(); @@ -448,4 +505,58 @@ mod tests { 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), + level in 0u32..1000 + )| { + let mut outbox = Outbox::::default(); + for msg in &messages { + prop_assert!(outbox.write_message(msg.clone(), level).is_ok()); + } + + let read_messages = outbox.read_level(level); + + prop_assert_eq!(read_messages.len(), messages.len()); + for (i, msg) in messages.iter().enumerate() { + prop_assert_eq!(read_messages[i].as_ref(), msg.as_ref() as &[u8]); + } + }); + } + + #[test] + fn read_overwritten_slot_returns_new_level_data() { + proptest!(|( + messages1 in proptest::collection::vec(messages_strategy(0..=32, 50), TEST_OUTBOX_SIZE), + messages2 in proptest::collection::vec(messages_strategy(0..=16, 10), TEST_OUTBOX_SIZE) + )|{ + let mut outbox = Outbox::::default(); + for (level, msgs) in messages1.iter().enumerate() { + for msg in msgs { + prop_assert!(outbox.write_message(msg.clone(), level as u32).is_ok()); + } + } + + for (offset, msgs) in messages2.iter().enumerate() { + let wrap_level = TEST_OUTBOX_SIZE + offset; + for msg in msgs { + prop_assert!(outbox.write_message(msg.clone(), wrap_level as u32).is_ok()); + } + let read_messages = outbox.read_level(wrap_level as u32); + let expected_messages: Box<[Box<[u8]>]> = Box::from(messages2[offset].clone().into_iter().map(|m|m.0).collect_vec()); + prop_assert_eq!(read_messages, expected_messages); + } + }); + } + + #[test] + fn read_fresh_outbox_is_empty() { + proptest!(|(level in 0u32..TEST_OUTBOX_SIZE as u32)| { + let outbox = Outbox::::default(); + let result = outbox.read_level(level); + prop_assert_eq!(result.len(), 0) + }); + } }