Skip to main content

commonware_storage/qmdb/sync/
target.rs

1use crate::{
2    merkle::{Family, Location},
3    qmdb::sync::error::EngineError,
4};
5use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
6use commonware_cryptography::Digest;
7use commonware_runtime::{Buf, BufMut};
8use commonware_utils::{non_empty_range, range::NonEmptyRange};
9
10/// Target state to sync to.
11///
12/// `PartialEq`, `Eq`, and `Clone` are implemented manually to avoid requiring `F` to implement
13/// them.
14#[derive(Debug)]
15pub struct Target<F: Family, D: Digest> {
16    /// The ops root the sync engine verifies streaming batches against.
17    pub root: D,
18    /// Range of operations to sync
19    pub range: NonEmptyRange<Location<F>>,
20}
21
22impl<F: Family, D: Digest> Target<F, D> {
23    /// Create a sync target.
24    pub const fn new(root: D, range: NonEmptyRange<Location<F>>) -> Self {
25        Self { root, range }
26    }
27
28    /// Whether this target advances relative to `from`.
29    ///
30    /// Both targets are assumed to describe valid states of the same append-only QMDB. Because
31    /// the root commits to the database size, valid targets at different sizes have distinct
32    /// roots.
33    pub fn advances(&self, from: &Self) -> bool {
34        self.range.end().is_valid()
35            && self.range.end() > from.range.end()
36            && self.range.start() >= from.range.start()
37    }
38}
39
40impl<F: Family, D: Digest> Clone for Target<F, D> {
41    fn clone(&self) -> Self {
42        Self {
43            root: self.root,
44            range: self.range.clone(),
45        }
46    }
47}
48
49impl<F: Family, D: Digest> PartialEq for Target<F, D> {
50    fn eq(&self, other: &Self) -> bool {
51        self.root == other.root && self.range == other.range
52    }
53}
54
55impl<F: Family, D: Digest> Eq for Target<F, D> {}
56
57impl<F: Family, D: Digest> Write for Target<F, D> {
58    fn write(&self, buf: &mut impl BufMut) {
59        self.root.write(buf);
60        self.range.write(buf);
61    }
62}
63
64impl<F: Family, D: Digest> EncodeSize for Target<F, D> {
65    fn encode_size(&self) -> usize {
66        self.root.encode_size() + self.range.encode_size()
67    }
68}
69
70impl<F: Family, D: Digest> Read for Target<F, D> {
71    type Cfg = ();
72
73    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
74        let root = D::read(buf)?;
75        let range = NonEmptyRange::<Location<F>>::read(buf)?;
76        if !range.start().is_valid() || !range.end().is_valid() {
77            return Err(CodecError::Invalid(
78                "storage::qmdb::sync::Target",
79                "range bounds out of valid range",
80            ));
81        }
82        Ok(Self { root, range })
83    }
84}
85
86#[cfg(feature = "arbitrary")]
87impl<F: Family, D: Digest> arbitrary::Arbitrary<'_> for Target<F, D>
88where
89    D: for<'a> arbitrary::Arbitrary<'a>,
90{
91    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
92        let root = u.arbitrary()?;
93        let max_loc = F::MAX_LEAVES;
94        let lower = u.int_in_range(0..=*max_loc - 1)?;
95        let upper = u.int_in_range(lower + 1..=*max_loc)?;
96        Ok(Self {
97            root,
98            range: commonware_utils::non_empty_range!(Location::new(lower), Location::new(upper)),
99        })
100    }
101}
102
103/// Target state for syncing to a compact-storage database.
104#[derive(Debug)]
105pub struct CompactTarget<F: Family, D: Digest> {
106    /// Target database root.
107    pub root: D,
108    /// Target database size.
109    pub size: Location<F>,
110}
111
112impl<F: Family, D: Digest> TryFrom<&CompactTarget<F, D>> for Target<F, D> {
113    type Error = EngineError<F, D>;
114
115    fn try_from(target: &CompactTarget<F, D>) -> Result<Self, Self::Error> {
116        let end = target.size;
117        let start =
118            end.checked_sub(1)
119                .filter(|_| end.is_valid())
120                .ok_or(EngineError::InvalidTarget {
121                    lower_bound_pos: Location::new(0),
122                    upper_bound_pos: end,
123                })?;
124        Ok(Self {
125            root: target.root,
126            range: non_empty_range!(start, end),
127        })
128    }
129}
130
131impl<F: Family, D: Digest> Clone for CompactTarget<F, D> {
132    fn clone(&self) -> Self {
133        Self {
134            root: self.root,
135            size: self.size,
136        }
137    }
138}
139
140impl<F: Family, D: Digest> PartialEq for CompactTarget<F, D> {
141    fn eq(&self, other: &Self) -> bool {
142        self.root == other.root && self.size == other.size
143    }
144}
145
146impl<F: Family, D: Digest> Eq for CompactTarget<F, D> {}
147
148impl<F: Family, D: Digest> Write for CompactTarget<F, D> {
149    fn write(&self, buf: &mut impl BufMut) {
150        self.root.write(buf);
151        self.size.write(buf);
152    }
153}
154
155impl<F: Family, D: Digest> EncodeSize for CompactTarget<F, D> {
156    fn encode_size(&self) -> usize {
157        self.root.encode_size() + self.size.encode_size()
158    }
159}
160
161impl<F: Family, D: Digest> Read for CompactTarget<F, D> {
162    type Cfg = ();
163
164    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
165        let root = D::read(buf)?;
166        let size = Location::<F>::read(buf)?;
167        if !size.is_valid() || size == 0 {
168            return Err(CodecError::Invalid(
169                "storage::qmdb::sync::CompactTarget",
170                "size must be in 1..=MAX_LEAVES",
171            ));
172        }
173        Ok(Self { root, size })
174    }
175}
176
177#[cfg(feature = "arbitrary")]
178impl<F: Family, D: Digest> arbitrary::Arbitrary<'_> for CompactTarget<F, D>
179where
180    D: for<'a> arbitrary::Arbitrary<'a>,
181{
182    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
183        let root = u.arbitrary()?;
184        let size = Location::new(u.int_in_range(1..=*F::MAX_LEAVES)?);
185        Ok(Self { root, size })
186    }
187}
188
189#[cfg(test)]
190// The unit tests use `MmrFamily` only. The codec and predicates are family-agnostic (the
191// family only influences `Location::is_valid` via `F::MAX_LEAVES` and the `arbitrary` range
192// picker), so an MMB variant would duplicate coverage. Codec conformance covers both families.
193mod tests {
194    use super::*;
195    use crate::merkle::mmr::Family as MmrFamily;
196    use commonware_codec::{DecodeExt as _, Encode as _};
197    use commonware_cryptography::sha256;
198    use commonware_utils::non_empty_range;
199    use std::io::Cursor;
200
201    fn target(root: sha256::Digest, start: u64, end: u64) -> Target<MmrFamily, sha256::Digest> {
202        Target::new(
203            root,
204            non_empty_range!(Location::new(start), Location::new(end)),
205        )
206    }
207
208    #[test]
209    fn test_sync_target_serialization() {
210        let target = target(sha256::Digest::from([42; 32]), 100, 500);
211
212        // Serialize
213        let mut buffer = Vec::new();
214        target.write(&mut buffer);
215
216        // Verify encoded size matches actual size
217        assert_eq!(buffer.len(), target.encode_size());
218
219        // Deserialize
220        let mut cursor = Cursor::new(buffer);
221        let deserialized = Target::read(&mut cursor).unwrap();
222
223        // Verify
224        assert_eq!(target, deserialized);
225        assert_eq!(target.root, deserialized.root);
226        assert_eq!(target.range, deserialized.range);
227    }
228
229    #[test]
230    fn test_sync_target_read_invalid_bounds() {
231        // Manually encode root + two Locations with reversed bounds
232        let mut buffer = Vec::new();
233        sha256::Digest::from([42; 32]).write(&mut buffer);
234        Location::<MmrFamily>::new(100).write(&mut buffer); // start
235        Location::<MmrFamily>::new(50).write(&mut buffer); // end (< start = invalid)
236
237        let mut cursor = Cursor::new(buffer);
238        assert!(matches!(
239            Target::<MmrFamily, sha256::Digest>::read(&mut cursor),
240            Err(CodecError::Invalid("NonEmptyRange", "start must be < end"))
241        ));
242
243        // Manually encode a target with an empty range (start == end)
244        let root = sha256::Digest::from([42; 32]);
245        let mut buffer = Vec::new();
246        root.write(&mut buffer);
247        Location::<MmrFamily>::new(100).write(&mut buffer);
248        Location::<MmrFamily>::new(100).write(&mut buffer);
249
250        let mut cursor = Cursor::new(buffer);
251        assert!(matches!(
252            Target::<MmrFamily, sha256::Digest>::read(&mut cursor),
253            Err(CodecError::Invalid("NonEmptyRange", "start must be < end"))
254        ));
255    }
256
257    #[test]
258    fn test_compact_target_decode_rejects_zero_size() {
259        let unused_root = sha256::Digest::from([42; 32]);
260        let encoded = CompactTarget::<MmrFamily, sha256::Digest> {
261            root: unused_root,
262            size: Location::new(0),
263        }
264        .encode();
265
266        assert!(CompactTarget::<MmrFamily, sha256::Digest>::decode(encoded).is_err());
267    }
268
269    #[test]
270    fn test_advances() {
271        let current_root = sha256::Digest::from([0; 32]);
272        let advanced_root = sha256::Digest::from([1; 32]);
273        let current = target(current_root, 10, 100);
274
275        // End strictly increases, start does not decrease.
276        assert!(target(advanced_root, 10, 101).advances(&current));
277        assert!(target(advanced_root, 50, 200).advances(&current));
278
279        // Same or smaller end does not advance.
280        assert!(!target(current_root, 10, 100).advances(&current));
281        assert!(!target(current_root, 10, 50).advances(&current));
282
283        // A start moving backward does not advance, even with a larger end.
284        assert!(!target(advanced_root, 5, 200).advances(&current));
285
286        // An end outside the location domain does not advance.
287        let beyond = target(advanced_root, 10, *MmrFamily::MAX_LEAVES + 1);
288        assert!(!beyond.advances(&current));
289    }
290
291    #[test]
292    fn test_compact_target_serialization() {
293        let target = CompactTarget::<MmrFamily, sha256::Digest> {
294            root: sha256::Digest::from([42; 32]),
295            size: Location::new(100),
296        };
297
298        let mut buffer = Vec::new();
299        target.write(&mut buffer);
300        assert_eq!(buffer.len(), target.encode_size());
301
302        let mut cursor = Cursor::new(buffer);
303        let deserialized = CompactTarget::read(&mut cursor).unwrap();
304        assert_eq!(target, deserialized);
305        assert_eq!(target.root, deserialized.root);
306        assert_eq!(target.size, deserialized.size);
307    }
308
309    #[test]
310    fn test_compact_target_decode_rejects_size_beyond_domain() {
311        let mut buffer = Vec::new();
312        sha256::Digest::from([42; 32]).write(&mut buffer);
313        Location::<MmrFamily>::new(*MmrFamily::MAX_LEAVES + 1).write(&mut buffer);
314
315        let mut cursor = Cursor::new(buffer);
316        assert!(matches!(
317            CompactTarget::<MmrFamily, sha256::Digest>::read(&mut cursor),
318            Err(CodecError::Invalid(_, _))
319        ));
320    }
321
322    #[test]
323    fn test_compact_target_to_ranged() {
324        let root = sha256::Digest::from([42; 32]);
325
326        // The derived range replays the one operation ending at the target.
327        let compact = CompactTarget::<MmrFamily, _> {
328            root,
329            size: Location::new(100),
330        };
331        let ranged = Target::try_from(&compact).unwrap();
332        assert_eq!(ranged.root, root);
333        assert_eq!(ranged.range.start(), Location::new(99));
334        assert_eq!(ranged.range.end(), Location::new(100));
335
336        // A size of one yields [0, 1).
337        let genesis = CompactTarget::<MmrFamily, _> {
338            root,
339            size: Location::new(1),
340        };
341        let ranged = Target::try_from(&genesis).unwrap();
342        assert_eq!(ranged.range.start(), Location::new(0));
343        assert_eq!(ranged.range.end(), Location::new(1));
344
345        // A zero size has no operation to replay.
346        let empty = CompactTarget::<MmrFamily, _> {
347            root,
348            size: Location::new(0),
349        };
350        assert!(matches!(
351            Target::try_from(&empty),
352            Err(EngineError::InvalidTarget { .. })
353        ));
354
355        // A size outside the location domain is rejected.
356        let beyond = CompactTarget::<MmrFamily, _> {
357            root,
358            size: MmrFamily::MAX_LEAVES + 1,
359        };
360        assert!(matches!(
361            Target::try_from(&beyond),
362            Err(EngineError::InvalidTarget { .. })
363        ));
364    }
365
366    #[cfg(feature = "arbitrary")]
367    mod conformance {
368        use super::*;
369        use crate::merkle::mmb;
370        use commonware_codec::conformance::CodecConformance;
371
372        commonware_conformance::conformance_tests! {
373            CodecConformance<Target<MmrFamily, sha256::Digest>>,
374            CodecConformance<Target<mmb::Family, sha256::Digest>>,
375            CodecConformance<CompactTarget<MmrFamily, sha256::Digest>>,
376            CodecConformance<CompactTarget<mmb::Family, sha256::Digest>>,
377        }
378    }
379}