From 4d5d767249a1240d053dde74b228e4352c676d22 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 25 Mar 2026 14:55:23 -0300 Subject: [PATCH] add tests. minor refactor --- nssa/core/src/program.rs | 156 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 6 deletions(-) diff --git a/nssa/core/src/program.rs b/nssa/core/src/program.rs index 5cd46432..0e9246fe 100644 --- a/nssa/core/src/program.rs +++ b/nssa/core/src/program.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -#[cfg(feature = "host")] +#[cfg(any(feature = "host", test))] use borsh::{BorshDeserialize, BorshSerialize}; use risc0_zkvm::{DeserializeOwned, guest::env, serde::Deserializer}; use serde::{Deserialize, Serialize}; @@ -166,6 +166,7 @@ pub struct ValidityWindow { } impl ValidityWindow { + /// Creates a window with no bounds, valid for every block ID. #[must_use] pub const fn new_unbounded() -> Self { Self { @@ -174,12 +175,14 @@ impl ValidityWindow { } } - /// Valid for block IDs in the range [from, to), where `from` is included and `to` is excluded. + /// Returns `true` if `id` falls within the half-open range `[from, to)`. + /// A `None` bound on either side is treated as unbounded in that direction. #[must_use] pub fn is_valid_for_block_id(&self, id: BlockId) -> bool { self.from.is_none_or(|start| id >= start) && self.to.is_none_or(|end| id < end) } + /// Returns `Err(InvalidWindow)` if both bounds are set and `from >= to`. const fn check_window(&self) -> Result<(), InvalidWindow> { if let (Some(from_id), Some(until_id)) = (self.from, self.to) && from_id >= until_id @@ -190,15 +193,31 @@ impl ValidityWindow { } } + /// Inclusive lower bound. `None` means the window starts at the beginning of the chain. #[must_use] pub const fn from(&self) -> Option { self.from } + /// Exclusive upper bound. `None` means the window has no expiry. #[must_use] pub const fn to(&self) -> Option { self.to } + + /// Sets the inclusive lower bound. Returns `Err` if the updated window would be empty or inverted. + pub fn set_from(&mut self, id: Option) -> Result<(), InvalidWindow> { + let prev = self.from; + self.from = id; + self.check_window().inspect_err(|_| self.from = prev) + } + + /// Sets the exclusive upper bound. Returns `Err` if the updated window would be empty or inverted. + pub fn set_to(&mut self, id: Option) -> Result<(), InvalidWindow> { + let prev = self.to; + self.to = id; + self.check_window().inspect_err(|_| self.to = prev) + } } impl TryFrom<(Option, Option)> for ValidityWindow { type Error = InvalidWindow; @@ -213,6 +232,38 @@ impl TryFrom<(Option, Option)> for ValidityWindow { } } +impl TryFrom> for ValidityWindow { + type Error = InvalidWindow; + + fn try_from(value: std::ops::Range) -> Result { + (Some(value.start), Some(value.end)).try_into() + } +} + +impl From> for ValidityWindow { + fn from(value: std::ops::RangeFrom) -> Self { + Self { + from: Some(value.start), + to: None, + } + } +} + +impl From> for ValidityWindow { + fn from(value: std::ops::RangeTo) -> Self { + Self { + from: None, + to: Some(value.end), + } + } +} + +impl From for ValidityWindow { + fn from(_: std::ops::RangeFull) -> Self { + Self::new_unbounded() + } +} + #[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)] #[error("Invalid window")] pub struct InvalidWindow; @@ -261,14 +312,12 @@ impl ProgramOutput { } pub fn valid_from_id(mut self, id: Option) -> Result { - self.validity_window.from = id; - self.validity_window.check_window()?; + self.validity_window.set_from(id)?; Ok(self) } pub fn valid_until_id(mut self, id: Option) -> Result { - self.validity_window.to = id; - self.validity_window.check_window()?; + self.validity_window.set_to(id)?; Ok(self) } } @@ -443,6 +492,101 @@ fn validate_uniqueness_of_account_ids(pre_states: &[AccountWithMetadata]) -> boo mod tests { use super::*; + #[test] + fn validity_window_unbounded_accepts_any_block() { + let w = ValidityWindow::new_unbounded(); + assert!(w.is_valid_for_block_id(0)); + assert!(w.is_valid_for_block_id(u64::MAX)); + } + + #[test] + fn validity_window_bounded_range_includes_from_excludes_to() { + let w: ValidityWindow = (Some(5), Some(10)).try_into().unwrap(); + assert!(!w.is_valid_for_block_id(4)); + assert!(w.is_valid_for_block_id(5)); + assert!(w.is_valid_for_block_id(9)); + assert!(!w.is_valid_for_block_id(10)); + } + + #[test] + fn validity_window_only_from_bound() { + let w: ValidityWindow = (Some(5), None).try_into().unwrap(); + assert!(!w.is_valid_for_block_id(4)); + assert!(w.is_valid_for_block_id(5)); + assert!(w.is_valid_for_block_id(u64::MAX)); + } + + #[test] + fn validity_window_only_to_bound() { + let w: ValidityWindow = (None, Some(5)).try_into().unwrap(); + assert!(w.is_valid_for_block_id(0)); + assert!(w.is_valid_for_block_id(4)); + assert!(!w.is_valid_for_block_id(5)); + } + + #[test] + fn validity_window_adjacent_bounds_are_invalid() { + // [5, 5) is an empty range — from == to + assert!(ValidityWindow::try_from((Some(5), Some(5))).is_err()); + } + + #[test] + fn validity_window_inverted_bounds_are_invalid() { + assert!(ValidityWindow::try_from((Some(10), Some(5))).is_err()); + } + + #[test] + fn validity_window_getters_match_construction() { + let w: ValidityWindow = (Some(3), Some(7)).try_into().unwrap(); + assert_eq!(w.from(), Some(3)); + assert_eq!(w.to(), Some(7)); + } + + #[test] + fn validity_window_getters_for_unbounded() { + let w = ValidityWindow::new_unbounded(); + assert_eq!(w.from(), None); + assert_eq!(w.to(), None); + } + + #[test] + fn validity_window_from_range() { + let w = ValidityWindow::try_from(5u64..10).unwrap(); + assert_eq!(w.from(), Some(5)); + assert_eq!(w.to(), Some(10)); + } + + #[test] + fn validity_window_from_range_empty_is_invalid() { + assert!(ValidityWindow::try_from(5u64..5).is_err()); + } + + #[test] + fn validity_window_from_range_inverted_is_invalid() { + assert!(ValidityWindow::try_from(10u64..5).is_err()); + } + + #[test] + fn validity_window_from_range_from() { + let w: ValidityWindow = (5u64..).into(); + assert_eq!(w.from(), Some(5)); + assert_eq!(w.to(), None); + } + + #[test] + fn validity_window_from_range_to() { + let w: ValidityWindow = (..10u64).into(); + assert_eq!(w.from(), None); + assert_eq!(w.to(), Some(10)); + } + + #[test] + fn validity_window_from_range_full() { + let w: ValidityWindow = (..).into(); + assert_eq!(w.from(), None); + assert_eq!(w.to(), None); + } + #[test] fn post_state_new_with_claim_constructor() { let account = Account {