use crate::{ merkle::{Family, Location}, qmdb::sync::error::EngineError, }; use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt as _, Write}; use commonware_cryptography::Digest; use commonware_runtime::{Buf, BufMut}; use commonware_utils::{non_empty_range, range::NonEmptyRange}; /// Target state to sync to. /// /// `PartialEq`, `Eq`, and `Clone` are implemented manually to avoid requiring `F` to implement /// them. #[derive(Debug)] pub struct Target { /// The ops root the sync engine verifies streaming batches against. pub root: D, /// Range of operations to sync pub range: NonEmptyRange>, } impl Target { /// Create a sync target. pub const fn new(root: D, range: NonEmptyRange>) -> Self { Self { root, range } } /// Whether this target advances relative to `from`. /// /// Both targets are assumed to describe valid states of the same append-only QMDB. Because /// the root commits to the database size, valid targets at different sizes have distinct /// roots. pub fn advances(&self, from: &Self) -> bool { self.range.end().is_valid() && self.range.end() > from.range.end() && self.range.start() >= from.range.start() } } impl Clone for Target { fn clone(&self) -> Self { Self { root: self.root, range: self.range.clone(), } } } impl PartialEq for Target { fn eq(&self, other: &Self) -> bool { self.root == other.root && self.range == other.range } } impl Eq for Target {} impl Write for Target { fn write(&self, buf: &mut impl BufMut) { self.root.write(buf); self.range.write(buf); } } impl EncodeSize for Target { fn encode_size(&self) -> usize { self.root.encode_size() + self.range.encode_size() } } impl Read for Target { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let root = D::read(buf)?; let range = NonEmptyRange::>::read(buf)?; if !range.start().is_valid() || !range.end().is_valid() { return Err(CodecError::Invalid( "storage::qmdb::sync::Target", "range bounds out of valid range", )); } Ok(Self { root, range }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for Target where D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let root = u.arbitrary()?; let max_loc = F::MAX_LEAVES; let lower = u.int_in_range(0..=*max_loc - 1)?; let upper = u.int_in_range(lower + 1..=*max_loc)?; Ok(Self { root, range: commonware_utils::non_empty_range!(Location::new(lower), Location::new(upper)), }) } } /// Target state for syncing to a compact-storage database. #[derive(Debug)] pub struct CompactTarget { /// Target database root. pub root: D, /// Target database size. pub size: Location, } impl TryFrom<&CompactTarget> for Target { type Error = EngineError; fn try_from(target: &CompactTarget) -> Result { let end = target.size; let start = end.checked_sub(1) .filter(|_| end.is_valid()) .ok_or(EngineError::InvalidTarget { lower_bound_pos: Location::new(0), upper_bound_pos: end, })?; Ok(Self { root: target.root, range: non_empty_range!(start, end), }) } } impl Clone for CompactTarget { fn clone(&self) -> Self { Self { root: self.root, size: self.size, } } } impl PartialEq for CompactTarget { fn eq(&self, other: &Self) -> bool { self.root == other.root && self.size == other.size } } impl Eq for CompactTarget {} impl Write for CompactTarget { fn write(&self, buf: &mut impl BufMut) { self.root.write(buf); self.size.write(buf); } } impl EncodeSize for CompactTarget { fn encode_size(&self) -> usize { self.root.encode_size() + self.size.encode_size() } } impl Read for CompactTarget { type Cfg = (); fn read_cfg(buf: &mut impl Buf, _: &()) -> Result { let root = D::read(buf)?; let size = Location::::read(buf)?; if !size.is_valid() || size == 0 { return Err(CodecError::Invalid( "storage::qmdb::sync::CompactTarget", "size must be in 1..=MAX_LEAVES", )); } Ok(Self { root, size }) } } #[cfg(feature = "arbitrary")] impl arbitrary::Arbitrary<'_> for CompactTarget where D: for<'a> arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let root = u.arbitrary()?; let size = Location::new(u.int_in_range(1..=*F::MAX_LEAVES)?); Ok(Self { root, size }) } } #[cfg(test)] // The unit tests use `MmrFamily` only. The codec and predicates are family-agnostic (the // family only influences `Location::is_valid` via `F::MAX_LEAVES` and the `arbitrary` range // picker), so an MMB variant would duplicate coverage. Codec conformance covers both families. mod tests { use super::*; use crate::merkle::mmr::Family as MmrFamily; use commonware_codec::{DecodeExt as _, Encode as _}; use commonware_cryptography::sha256; use commonware_utils::non_empty_range; use std::io::Cursor; fn target(root: sha256::Digest, start: u64, end: u64) -> Target { Target::new( root, non_empty_range!(Location::new(start), Location::new(end)), ) } #[test] fn test_sync_target_serialization() { let target = target(sha256::Digest::from([42; 32]), 100, 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() { // Manually encode root + two Locations with reversed bounds let mut buffer = Vec::new(); sha256::Digest::from([42; 32]).write(&mut buffer); Location::::new(100).write(&mut buffer); // start Location::::new(50).write(&mut buffer); // end (< start = invalid) let mut cursor = Cursor::new(buffer); assert!(matches!( Target::::read(&mut cursor), Err(CodecError::Invalid("NonEmptyRange", "start must be < end")) )); // Manually encode a target with an empty range (start == end) let root = sha256::Digest::from([42; 32]); let mut buffer = Vec::new(); root.write(&mut buffer); Location::::new(100).write(&mut buffer); Location::::new(100).write(&mut buffer); let mut cursor = Cursor::new(buffer); assert!(matches!( Target::::read(&mut cursor), Err(CodecError::Invalid("NonEmptyRange", "start must be < end")) )); } #[test] fn test_compact_target_decode_rejects_zero_size() { let unused_root = sha256::Digest::from([42; 32]); let encoded = CompactTarget:: { root: unused_root, size: Location::new(0), } .encode(); assert!(CompactTarget::::decode(encoded).is_err()); } #[test] fn test_advances() { let current_root = sha256::Digest::from([0; 32]); let advanced_root = sha256::Digest::from([1; 32]); let current = target(current_root, 10, 100); // End strictly increases, start does not decrease. assert!(target(advanced_root, 10, 101).advances(¤t)); assert!(target(advanced_root, 50, 200).advances(¤t)); // Same or smaller end does not advance. assert!(!target(current_root, 10, 100).advances(¤t)); assert!(!target(current_root, 10, 50).advances(¤t)); // A start moving backward does not advance, even with a larger end. assert!(!target(advanced_root, 5, 200).advances(¤t)); // An end outside the location domain does not advance. let beyond = target(advanced_root, 10, *MmrFamily::MAX_LEAVES + 1); assert!(!beyond.advances(¤t)); } #[test] fn test_compact_target_serialization() { let target = CompactTarget:: { root: sha256::Digest::from([42; 32]), size: Location::new(100), }; let mut buffer = Vec::new(); target.write(&mut buffer); assert_eq!(buffer.len(), target.encode_size()); let mut cursor = Cursor::new(buffer); let deserialized = CompactTarget::read(&mut cursor).unwrap(); assert_eq!(target, deserialized); assert_eq!(target.root, deserialized.root); assert_eq!(target.size, deserialized.size); } #[test] fn test_compact_target_decode_rejects_size_beyond_domain() { let mut buffer = Vec::new(); sha256::Digest::from([42; 32]).write(&mut buffer); Location::::new(*MmrFamily::MAX_LEAVES + 1).write(&mut buffer); let mut cursor = Cursor::new(buffer); assert!(matches!( CompactTarget::::read(&mut cursor), Err(CodecError::Invalid(_, _)) )); } #[test] fn test_compact_target_to_ranged() { let root = sha256::Digest::from([42; 32]); // The derived range replays the one operation ending at the target. let compact = CompactTarget:: { root, size: Location::new(100), }; let ranged = Target::try_from(&compact).unwrap(); assert_eq!(ranged.root, root); assert_eq!(ranged.range.start(), Location::new(99)); assert_eq!(ranged.range.end(), Location::new(100)); // A size of one yields [0, 1). let genesis = CompactTarget:: { root, size: Location::new(1), }; let ranged = Target::try_from(&genesis).unwrap(); assert_eq!(ranged.range.start(), Location::new(0)); assert_eq!(ranged.range.end(), Location::new(1)); // A zero size has no operation to replay. let empty = CompactTarget:: { root, size: Location::new(0), }; assert!(matches!( Target::try_from(&empty), Err(EngineError::InvalidTarget { .. }) )); // A size outside the location domain is rejected. let beyond = CompactTarget:: { root, size: MmrFamily::MAX_LEAVES + 1, }; assert!(matches!( Target::try_from(&beyond), Err(EngineError::InvalidTarget { .. }) )); } #[cfg(feature = "arbitrary")] mod conformance { use super::*; use crate::merkle::mmb; use commonware_codec::conformance::CodecConformance; commonware_conformance::conformance_tests! { CodecConformance>, CodecConformance>, CodecConformance>, CodecConformance>, } } }