commonware_storage/qmdb/sync/
target.rs1use 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#[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> 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
76pub 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 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 let mut buffer = Vec::new();
125 target.write(&mut buffer);
126
127 assert_eq!(buffer.len(), target.encode_size());
129
130 let mut cursor = Cursor::new(buffer);
132 let deserialized = Target::read(&mut cursor).unwrap();
133
134 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), };
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}