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