commonware_storage/qmdb/sync/
target.rs1#[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#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Target<D: Digest> {
17 pub root: D,
19 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
69pub 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 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 let mut buffer = Vec::new();
126 target.write(&mut buffer);
127
128 assert_eq!(buffer.len(), target.encode_size());
130
131 let mut cursor = Cursor::new(buffer);
133 let deserialized = Target::read(&mut cursor).unwrap();
134
135 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 let mut buffer = Vec::new();
145 sha256::Digest::from([42; 32]).write(&mut buffer);
146 Location::new(100).write(&mut buffer); Location::new(50).write(&mut buffer); 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 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}