use crate::{ mmr::Location, qmdb::sync::{self, error::EngineError}, }; use bytes::{Buf, BufMut}; use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt as _, Write}; use commonware_cryptography::Digest; use std::ops::Range; /// Target state to sync to #[derive(Debug, Clone, PartialEq, Eq)] pub struct Target { /// The root digest we're syncing to pub root: D, /// Range of operations to sync pub range: Range, } impl Write for Target { fn write(&self, buf: &mut impl BufMut) { self.root.write(buf); (*self.range.start).write(buf); (*self.range.end).write(buf); } } impl FixedSize for Target { const SIZE: usize = D::SIZE + u64::SIZE + u64::SIZE; } impl Read for Target { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let root = D::read(buf)?; let lower_bound = u64::read(buf)?; let upper_bound = u64::read(buf)?; if lower_bound >= upper_bound { return Err(CodecError::Invalid( "storage::qmdb::sync::Target", "lower_bound >= upper_bound", )); } Ok(Self { root, range: Location::new_unchecked(lower_bound)..Location::new_unchecked(upper_bound), }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Target where D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { use crate::mmr::MAX_LOCATION; let root = u.arbitrary()?; let lower = u.int_in_range(0..=MAX_LOCATION - 1)?; let upper = u.int_in_range(lower + 1..=MAX_LOCATION)?; Ok(Self { root, range: Location::new_unchecked(lower)..Location::new_unchecked(upper), }) } } /// Validate a target update against the current target pub fn validate_update( old_target: &Target, new_target: &Target, ) -> Result<(), sync::Error> where U: std::error::Error + Send + 'static, D: Digest, { if new_target.range.is_empty() { return Err(sync::Error::Engine(EngineError::InvalidTarget { lower_bound_pos: new_target.range.start, upper_bound_pos: new_target.range.end, })); } // Check if sync target moved backward if new_target.range.start < old_target.range.start || new_target.range.end < old_target.range.end { return Err(sync::Error::Engine(EngineError::SyncTargetMovedBackward { old: old_target.clone(), new: new_target.clone(), })); } if new_target.root == old_target.root { return Err(sync::Error::Engine(EngineError::SyncTargetRootUnchanged)); } Ok(()) } #[cfg(test)] mod tests { use super::*; use commonware_codec::EncodeSize as _; use commonware_cryptography::sha256; use rstest::rstest; use std::io::Cursor; #[test] fn test_sync_target_serialization() { let target = Target { root: sha256::Digest::from([42; 32]), range: Location::new_unchecked(100)..Location::new_unchecked(500), }; // Serialize let mut buffer = Vec::new(); target.write(&mut buffer); // Verify encoded size matches actual size assert_eq!(buffer.len(), target.encode_size()); // Deserialize let mut cursor = Cursor::new(buffer); let deserialized = Target::read(&mut cursor).unwrap(); // Verify assert_eq!(target, deserialized); assert_eq!(target.root, deserialized.root); assert_eq!(target.range, deserialized.range); } #[test] fn test_sync_target_read_invalid_bounds() { let target = Target { root: sha256::Digest::from([42; 32]), range: Location::new_unchecked(100)..Location::new_unchecked(50), // invalid: lower > upper }; let mut buffer = Vec::new(); target.write(&mut buffer); let mut cursor = Cursor::new(buffer); assert!(matches!( Target::::read(&mut cursor), Err(CodecError::Invalid(_, "lower_bound >= upper_bound")) )); } type TestError = sync::Error; #[rstest] #[case::valid_update( Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(100) }, Target { root: sha256::Digest::from([1; 32]), range: Location::new_unchecked(50)..Location::new_unchecked(200) }, Ok(()) )] #[case::lower_gt_upper( Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(100) }, Target { root: sha256::Digest::from([1; 32]), range: Location::new_unchecked(200)..Location::new_unchecked(100) }, Err(TestError::Engine(EngineError::InvalidTarget { lower_bound_pos: Location::new_unchecked(200), upper_bound_pos: Location::new_unchecked(100) })) )] #[case::moves_backward( Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(100) }, Target { root: sha256::Digest::from([1; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(50) }, Err(TestError::Engine(EngineError::SyncTargetMovedBackward { old: Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(100), }, new: Target { root: sha256::Digest::from([1; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(50), }, })) )] #[case::same_root( Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(0)..Location::new_unchecked(100) }, Target { root: sha256::Digest::from([0; 32]), range: Location::new_unchecked(50)..Location::new_unchecked(200) }, Err(TestError::Engine(EngineError::SyncTargetRootUnchanged)) )] fn test_validate_update( #[case] old_target: Target, #[case] new_target: Target, #[case] expected: Result<(), TestError>, ) { let result = validate_update(&old_target, &new_target); match (&result, &expected) { (Ok(()), Ok(())) => {} (Ok(()), Err(expected_err)) => { panic!("Expected error {expected_err:?} but got success"); } (Err(actual_err), Ok(())) => { panic!("Expected success but got error: {actual_err:?}"); } (Err(actual_err), Err(expected_err)) => match (actual_err, expected_err) { ( TestError::Engine(EngineError::InvalidTarget { lower_bound_pos: a_lower, upper_bound_pos: a_upper, }), TestError::Engine(EngineError::InvalidTarget { lower_bound_pos: e_lower, upper_bound_pos: e_upper, }), ) => { assert_eq!(a_lower, e_lower); assert_eq!(a_upper, e_upper); } ( TestError::Engine(EngineError::SyncTargetMovedBackward { .. }), TestError::Engine(EngineError::SyncTargetMovedBackward { .. }), ) => {} ( TestError::Engine(EngineError::SyncTargetRootUnchanged), TestError::Engine(EngineError::SyncTargetRootUnchanged), ) => {} _ => panic!("Error type mismatch: got {actual_err:?}, expected {expected_err:?}"), }, } } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, } } }