commonware_storage/qmdb/sync/
target.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Target<D: Digest> {
13 pub root: D,
15 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
68pub 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 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 let mut buffer = Vec::new();
118 target.write(&mut buffer);
119
120 assert_eq!(buffer.len(), target.encode_size());
122
123 let mut cursor = Cursor::new(buffer);
125 let deserialized = Target::read(&mut cursor).unwrap();
126
127 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), };
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}