commonware_storage/adb/sync/
target.rs

1use crate::adb::sync;
2use bytes::{Buf, BufMut};
3use commonware_codec::{Error as CodecError, FixedSize, Read, ReadExt as _, Write};
4use commonware_cryptography::Digest;
5
6/// Target state to sync to
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Target<D: Digest> {
9    /// The root digest we're syncing to
10    pub root: D,
11    /// Lower bound of operations to sync (inclusive)
12    pub lower_bound_ops: u64,
13    /// Upper bound of operations to sync (inclusive)
14    pub upper_bound_ops: u64,
15}
16
17impl<D: Digest> Write for Target<D> {
18    fn write(&self, buf: &mut impl BufMut) {
19        self.root.write(buf);
20        self.lower_bound_ops.write(buf);
21        self.upper_bound_ops.write(buf);
22    }
23}
24
25impl<D: Digest> FixedSize for Target<D> {
26    const SIZE: usize = D::SIZE + u64::SIZE + u64::SIZE;
27}
28
29impl<D: Digest> Read for Target<D> {
30    type Cfg = ();
31
32    fn read_cfg(buf: &mut impl Buf, _: &()) -> Result<Self, CodecError> {
33        let root = D::read(buf)?;
34        let lower_bound_ops = u64::read(buf)?;
35        let upper_bound_ops = u64::read(buf)?;
36        Ok(Self {
37            root,
38            lower_bound_ops,
39            upper_bound_ops,
40        })
41    }
42}
43
44/// Validate a target update against the current target
45pub fn validate_update<T, U, D>(
46    old_target: &Target<D>,
47    new_target: &Target<D>,
48) -> Result<(), sync::Error<T, U, D>>
49where
50    T: std::error::Error + Send + 'static,
51    U: std::error::Error + Send + 'static,
52    D: Digest,
53{
54    if new_target.lower_bound_ops > new_target.upper_bound_ops {
55        return Err(sync::Error::InvalidTarget {
56            lower_bound_pos: new_target.lower_bound_ops,
57            upper_bound_pos: new_target.upper_bound_ops,
58        });
59    }
60
61    if new_target.lower_bound_ops < old_target.lower_bound_ops
62        || new_target.upper_bound_ops < old_target.upper_bound_ops
63    {
64        return Err(sync::Error::SyncTargetMovedBackward {
65            old: old_target.clone(),
66            new: new_target.clone(),
67        });
68    }
69
70    if new_target.root == old_target.root {
71        return Err(sync::Error::SyncTargetRootUnchanged);
72    }
73
74    Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use commonware_codec::EncodeSize as _;
81    use commonware_cryptography::sha256;
82    use std::io::Cursor;
83    use test_case::test_case;
84
85    #[test]
86    fn test_sync_target_serialization() {
87        let target = Target {
88            root: sha256::Digest::from([42; 32]),
89            lower_bound_ops: 100,
90            upper_bound_ops: 500,
91        };
92
93        // Serialize
94        let mut buffer = Vec::new();
95        target.write(&mut buffer);
96
97        // Verify encoded size matches actual size
98        assert_eq!(buffer.len(), target.encode_size());
99
100        // Deserialize
101        let mut cursor = Cursor::new(buffer);
102        let deserialized = Target::read(&mut cursor).unwrap();
103
104        // Verify
105        assert_eq!(target, deserialized);
106        assert_eq!(target.root, deserialized.root);
107        assert_eq!(target.lower_bound_ops, deserialized.lower_bound_ops);
108        assert_eq!(target.upper_bound_ops, deserialized.upper_bound_ops);
109    }
110
111    type TestError = sync::Error<std::io::Error, std::io::Error, sha256::Digest>;
112
113    #[test_case(
114        Target { root: sha256::Digest::from([0; 32]), lower_bound_ops: 0, upper_bound_ops: 100 },
115        Target { root: sha256::Digest::from([1; 32]), lower_bound_ops: 50, upper_bound_ops: 200 },
116        Ok(());
117        "valid update"
118    )]
119    #[test_case(
120        Target { root: sha256::Digest::from([0; 32]), lower_bound_ops: 0, upper_bound_ops: 100 },
121        Target { root: sha256::Digest::from([1; 32]), lower_bound_ops: 200, upper_bound_ops: 100 },
122        Err(TestError::InvalidTarget { lower_bound_pos: 200, upper_bound_pos: 100 });
123        "invalid bounds - lower > upper"
124    )]
125    #[test_case(
126        Target { root: sha256::Digest::from([0; 32]), lower_bound_ops: 0, upper_bound_ops: 100 },
127        Target { root: sha256::Digest::from([1; 32]), lower_bound_ops: 0, upper_bound_ops: 50 },
128        Err(TestError::SyncTargetMovedBackward {
129            old: Target {
130                root: sha256::Digest::from([0; 32]),
131                lower_bound_ops: 0,
132                upper_bound_ops: 100,
133            },
134            new: Target {
135                root: sha256::Digest::from([1; 32]),
136                lower_bound_ops: 0,
137                upper_bound_ops: 50,
138            },
139        });
140        "moves backward"
141    )]
142    #[test_case(
143        Target { root: sha256::Digest::from([0; 32]), lower_bound_ops: 0, upper_bound_ops: 100 },
144        Target { root: sha256::Digest::from([0; 32]), lower_bound_ops: 50, upper_bound_ops: 200 },
145        Err(TestError::SyncTargetRootUnchanged);
146        "same root"
147    )]
148    fn test_validate_update(
149        old_target: Target<sha256::Digest>,
150        new_target: Target<sha256::Digest>,
151        expected: Result<(), TestError>,
152    ) {
153        let result = validate_update(&old_target, &new_target);
154        match (&result, &expected) {
155            (Ok(()), Ok(())) => {}
156            (Ok(()), Err(expected_err)) => {
157                panic!("Expected error {expected_err:?} but got success");
158            }
159            (Err(actual_err), Ok(())) => {
160                panic!("Expected success but got error: {actual_err:?}");
161            }
162            (Err(actual_err), Err(expected_err)) => match (actual_err, expected_err) {
163                (
164                    TestError::InvalidTarget {
165                        lower_bound_pos: a_lower,
166                        upper_bound_pos: a_upper,
167                    },
168                    TestError::InvalidTarget {
169                        lower_bound_pos: e_lower,
170                        upper_bound_pos: e_upper,
171                    },
172                ) => {
173                    assert_eq!(a_lower, e_lower);
174                    assert_eq!(a_upper, e_upper);
175                }
176                (
177                    TestError::SyncTargetMovedBackward { .. },
178                    TestError::SyncTargetMovedBackward { .. },
179                ) => {}
180                (TestError::SyncTargetRootUnchanged, TestError::SyncTargetRootUnchanged) => {}
181                _ => panic!("Error type mismatch: got {actual_err:?}, expected {expected_err:?}"),
182            },
183        }
184    }
185}