commonware_storage/qmdb/sync/
target.rs

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