Skip to main content

commonware_storage/qmdb/sync/
target.rs

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