Skip to main content

commonware_storage/qmdb/sync/
target.rs

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