Skip to main content

commonware_storage/qmdb/sync/
target.rs

1use crate::{
2    merkle::{Family, Location},
3    qmdb::sync::{self, 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::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
29impl<F: Family, D: Digest> Clone for Target<F, D> {
30    fn clone(&self) -> Self {
31        Self {
32            root: self.root,
33            range: self.range.clone(),
34        }
35    }
36}
37
38impl<F: Family, D: Digest> PartialEq for Target<F, D> {
39    fn eq(&self, other: &Self) -> bool {
40        self.root == other.root && self.range == other.range
41    }
42}
43
44impl<F: Family, D: Digest> Eq for Target<F, D> {}
45
46impl<F: Family, D: Digest> Write for Target<F, D> {
47    fn write(&self, buf: &mut impl BufMut) {
48        self.root.write(buf);
49        self.range.write(buf);
50    }
51}
52
53impl<F: Family, D: Digest> EncodeSize for Target<F, D> {
54    fn encode_size(&self) -> usize {
55        self.root.encode_size() + self.range.encode_size()
56    }
57}
58
59impl<F: Family, D: Digest> Read for Target<F, D> {
60    type Cfg = ();
61
62    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
63        let root = D::read(buf)?;
64        let range = NonEmptyRange::<Location<F>>::read(buf)?;
65        if !range.start().is_valid() || !range.end().is_valid() {
66            return Err(CodecError::Invalid(
67                "storage::qmdb::sync::Target",
68                "range bounds out of valid range",
69            ));
70        }
71        Ok(Self { root, range })
72    }
73}
74
75#[cfg(feature = "arbitrary")]
76impl<F: Family, D: Digest> arbitrary::Arbitrary<'_> for Target<F, D>
77where
78    D: for<'a> arbitrary::Arbitrary<'a>,
79{
80    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
81        let root = u.arbitrary()?;
82        let max_loc = F::MAX_LEAVES;
83        let lower = u.int_in_range(0..=*max_loc - 1)?;
84        let upper = u.int_in_range(lower + 1..=*max_loc)?;
85        Ok(Self {
86            root,
87            range: commonware_utils::non_empty_range!(Location::new(lower), Location::new(upper)),
88        })
89    }
90}
91
92/// Validate a target update against the current target
93pub fn validate_update<F, U, D>(
94    old_target: &Target<F, D>,
95    new_target: &Target<F, D>,
96) -> Result<(), sync::Error<F, U, D>>
97where
98    F: Family,
99    U: std::error::Error + Send + 'static,
100    D: Digest,
101{
102    if !new_target.range.end().is_valid() {
103        return Err(sync::Error::Engine(EngineError::InvalidTarget {
104            lower_bound_pos: new_target.range.start(),
105            upper_bound_pos: new_target.range.end(),
106        }));
107    }
108
109    // Start must not decrease; end must strictly increase. Same end implies same tree size implies
110    // same root (the Merkle structure is append-only), so retaining the old root under the old tree
111    // size in `retained_roots` requires a distinct end.
112    if new_target.range.start() < old_target.range.start()
113        || new_target.range.end() <= old_target.range.end()
114    {
115        return Err(sync::Error::Engine(EngineError::SyncTargetMovedBackward {
116            old: old_target.clone(),
117            new: new_target.clone(),
118        }));
119    }
120
121    if new_target.root == old_target.root {
122        return Err(sync::Error::Engine(EngineError::SyncTargetRootUnchanged));
123    }
124
125    Ok(())
126}
127
128#[cfg(test)]
129// Only `MmrFamily` is exercised here: `Target`'s codec and `validate_update` logic are
130// family-agnostic (the family only influences `Location::is_valid` via `F::MAX_LEAVES` and
131// the `arbitrary` range picker), so an MMB variant would duplicate coverage without catching
132// anything new.
133mod tests {
134    use super::*;
135    use crate::merkle::mmr::Family as MmrFamily;
136    use commonware_cryptography::sha256;
137    use commonware_utils::non_empty_range;
138    use rstest::rstest;
139    use std::io::Cursor;
140
141    fn target(root: sha256::Digest, start: u64, end: u64) -> Target<MmrFamily, sha256::Digest> {
142        Target::new(
143            root,
144            non_empty_range!(Location::new(start), Location::new(end)),
145        )
146    }
147
148    #[test]
149    fn test_sync_target_serialization() {
150        let target = target(sha256::Digest::from([42; 32]), 100, 500);
151
152        // Serialize
153        let mut buffer = Vec::new();
154        target.write(&mut buffer);
155
156        // Verify encoded size matches actual size
157        assert_eq!(buffer.len(), target.encode_size());
158
159        // Deserialize
160        let mut cursor = Cursor::new(buffer);
161        let deserialized = Target::read(&mut cursor).unwrap();
162
163        // Verify
164        assert_eq!(target, deserialized);
165        assert_eq!(target.root, deserialized.root);
166        assert_eq!(target.range, deserialized.range);
167    }
168
169    #[test]
170    fn test_sync_target_read_invalid_bounds() {
171        // Manually encode root + two Locations to bypass the Range write panic
172        let mut buffer = Vec::new();
173        sha256::Digest::from([42; 32]).write(&mut buffer);
174        Location::<MmrFamily>::new(100).write(&mut buffer); // start
175        Location::<MmrFamily>::new(50).write(&mut buffer); // end (< start = invalid)
176
177        let mut cursor = Cursor::new(buffer);
178        assert!(matches!(
179            Target::<MmrFamily, sha256::Digest>::read(&mut cursor),
180            Err(CodecError::Invalid("Range", "start must be <= end"))
181        ));
182
183        // Manually encode a target with an empty range (start == end)
184        let root = sha256::Digest::from([42; 32]);
185        let mut buffer = Vec::new();
186        root.write(&mut buffer);
187        (Location::<MmrFamily>::new(100)..Location::<MmrFamily>::new(100)).write(&mut buffer);
188
189        let mut cursor = Cursor::new(buffer);
190        assert!(matches!(
191            Target::<MmrFamily, sha256::Digest>::read(&mut cursor),
192            Err(CodecError::Invalid("NonEmptyRange", "start must be < end"))
193        ));
194    }
195
196    type TestError = sync::Error<MmrFamily, std::io::Error, sha256::Digest>;
197
198    #[rstest]
199    #[case::valid_update(
200        target(sha256::Digest::from([0; 32]), 0, 100),
201        target(sha256::Digest::from([1; 32]), 50, 200),
202        Ok(())
203    )]
204    #[case::same_start(
205        target(sha256::Digest::from([0; 32]), 0, 100),
206        target(sha256::Digest::from([1; 32]), 0, 200),
207        Ok(())
208    )]
209    #[case::same_end(
210        target(sha256::Digest::from([0; 32]), 0, 100),
211        target(sha256::Digest::from([1; 32]), 50, 100),
212        Err(TestError::Engine(EngineError::SyncTargetMovedBackward {
213            old: target(sha256::Digest::from([0; 32]), 0, 100),
214            new: target(sha256::Digest::from([1; 32]), 50, 100),
215        }))
216    )]
217    #[case::moves_backward(
218        target(sha256::Digest::from([0; 32]), 0, 100),
219        target(sha256::Digest::from([1; 32]), 0, 50),
220        Err(TestError::Engine(EngineError::SyncTargetMovedBackward {
221            old: target(sha256::Digest::from([0; 32]), 0, 100),
222            new: target(sha256::Digest::from([1; 32]), 0, 50),
223        }))
224    )]
225    #[case::same_root(
226        target(sha256::Digest::from([0; 32]), 0, 100),
227        target(sha256::Digest::from([0; 32]), 50, 200),
228        Err(TestError::Engine(EngineError::SyncTargetRootUnchanged))
229    )]
230    fn test_validate_update(
231        #[case] old_target: Target<MmrFamily, sha256::Digest>,
232        #[case] new_target: Target<MmrFamily, sha256::Digest>,
233        #[case] expected: Result<(), TestError>,
234    ) {
235        let result = validate_update(&old_target, &new_target);
236        match (&result, &expected) {
237            (Ok(()), Ok(())) => {}
238            (Ok(()), Err(expected_err)) => {
239                panic!("Expected error {expected_err:?} but got success");
240            }
241            (Err(actual_err), Ok(())) => {
242                panic!("Expected success but got error: {actual_err:?}");
243            }
244            (Err(actual_err), Err(expected_err)) => match (actual_err, expected_err) {
245                (
246                    TestError::Engine(EngineError::InvalidTarget {
247                        lower_bound_pos: a_lower,
248                        upper_bound_pos: a_upper,
249                    }),
250                    TestError::Engine(EngineError::InvalidTarget {
251                        lower_bound_pos: e_lower,
252                        upper_bound_pos: e_upper,
253                    }),
254                ) => {
255                    assert_eq!(a_lower, e_lower);
256                    assert_eq!(a_upper, e_upper);
257                }
258                (
259                    TestError::Engine(EngineError::SyncTargetMovedBackward { .. }),
260                    TestError::Engine(EngineError::SyncTargetMovedBackward { .. }),
261                ) => {}
262                (
263                    TestError::Engine(EngineError::SyncTargetRootUnchanged),
264                    TestError::Engine(EngineError::SyncTargetRootUnchanged),
265                ) => {}
266                _ => panic!("Error type mismatch: got {actual_err:?}, expected {expected_err:?}"),
267            },
268        }
269    }
270
271    #[cfg(feature = "arbitrary")]
272    mod conformance {
273        use super::*;
274        use commonware_codec::conformance::CodecConformance;
275
276        commonware_conformance::conformance_tests! {
277            CodecConformance<Target<MmrFamily, sha256::Digest>>,
278        }
279    }
280}