Skip to main content

calimero_node_primitives/sync/
delta.rs

1//! Delta sync types (CIP §4 - State Machine, DELTA SYNC branch).
2//!
3//! Types for delta-based synchronization when few deltas are missing.
4
5use borsh::{BorshDeserialize, BorshSerialize};
6
7// =============================================================================
8// Constants
9// =============================================================================
10
11/// Default threshold for choosing delta sync vs state-based sync.
12///
13/// If fewer than this many deltas are missing, use delta sync.
14/// If more are missing, escalate to state-based sync (HashComparison, etc.).
15///
16/// This is a heuristic balance between:
17/// - Delta sync: O(missing) round trips, but exact
18/// - State sync: O(log n) comparisons, but may transfer more data
19pub const DEFAULT_DELTA_SYNC_THRESHOLD: usize = 128;
20
21// =============================================================================
22// Request/Response Types
23// =============================================================================
24
25/// Request for delta-based synchronization.
26///
27/// Used when few deltas are missing and their IDs are known.
28/// The responder should return the requested deltas in causal order.
29///
30/// See CIP §4 - State Machine (DELTA SYNC branch).
31#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
32pub struct DeltaSyncRequest {
33    /// IDs of missing deltas to request.
34    pub missing_delta_ids: Vec<[u8; 32]>,
35}
36
37impl DeltaSyncRequest {
38    /// Create a new delta sync request.
39    #[must_use]
40    pub fn new(missing_delta_ids: Vec<[u8; 32]>) -> Self {
41        Self { missing_delta_ids }
42    }
43
44    /// Check if the request is within the recommended threshold.
45    #[must_use]
46    pub fn is_within_threshold(&self) -> bool {
47        self.missing_delta_ids.len() <= DEFAULT_DELTA_SYNC_THRESHOLD
48    }
49
50    /// Number of deltas being requested.
51    #[must_use]
52    pub fn count(&self) -> usize {
53        self.missing_delta_ids.len()
54    }
55}
56
57/// Response to a delta sync request.
58///
59/// Contains the requested deltas in causal order (parents before children).
60/// If some deltas are not found, they are omitted from the response.
61#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
62pub struct DeltaSyncResponse {
63    /// Deltas in causal order (parents first).
64    /// Each delta is serialized as bytes for transport.
65    pub deltas: Vec<DeltaPayload>,
66
67    /// IDs of deltas that were requested but not found.
68    pub not_found: Vec<[u8; 32]>,
69}
70
71impl DeltaSyncResponse {
72    /// Create a response with found deltas.
73    #[must_use]
74    pub fn new(deltas: Vec<DeltaPayload>, not_found: Vec<[u8; 32]>) -> Self {
75        Self { deltas, not_found }
76    }
77
78    /// Create an empty response (no deltas found).
79    #[must_use]
80    pub fn empty(not_found: Vec<[u8; 32]>) -> Self {
81        Self {
82            deltas: vec![],
83            not_found,
84        }
85    }
86
87    /// Check if all requested deltas were found.
88    #[must_use]
89    pub fn is_complete(&self) -> bool {
90        self.not_found.is_empty()
91    }
92
93    /// Number of deltas returned.
94    #[must_use]
95    pub fn count(&self) -> usize {
96        self.deltas.len()
97    }
98}
99
100// =============================================================================
101// Delta Payload
102// =============================================================================
103
104/// A delta payload for transport.
105///
106/// Contains the delta data and metadata needed for application.
107#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize)]
108pub struct DeltaPayload {
109    /// Unique delta ID (content hash).
110    pub id: [u8; 32],
111
112    /// Parent delta IDs (for causal ordering).
113    pub parents: Vec<[u8; 32]>,
114
115    /// Serialized delta operations (Borsh-encoded).
116    pub payload: Vec<u8>,
117
118    /// HLC timestamp when the delta was created.
119    pub hlc_timestamp: u64,
120
121    /// Expected root hash after applying this delta.
122    ///
123    /// This hash is captured by the originating node at delta creation time.
124    /// It serves two purposes:
125    /// 1. **Linear history**: When deltas are applied in sequence without
126    ///    concurrent branches, receivers can verify they reach the same state.
127    /// 2. **DAG consistency**: When concurrent deltas exist, this hash ensures
128    ///    nodes build identical DAG structures even if their local root hashes
129    ///    differ due to different merge ordering. The hash reflects the
130    ///    originator's state, not a universal truth.
131    ///
132    /// **Verification strategy**: Compare against this hash only when applying
133    /// deltas from a single linear chain. For concurrent/merged deltas, use
134    /// the Merkle tree reconciliation protocol instead.
135    pub expected_root_hash: [u8; 32],
136}
137
138impl DeltaPayload {
139    /// Check if this delta has no parents (genesis delta).
140    #[must_use]
141    pub fn is_genesis(&self) -> bool {
142        self.parents.is_empty()
143    }
144}
145
146// =============================================================================
147// Apply Result
148// =============================================================================
149
150/// Result of attempting to apply deltas.
151///
152/// **Note**: This is a local-only type used to report the outcome of delta
153/// application. It is not transmitted over the wire (hence no Borsh traits).
154#[derive(Clone, Debug)]
155pub enum DeltaApplyResult {
156    /// All deltas applied successfully.
157    Success {
158        /// Number of deltas applied.
159        applied_count: usize,
160        /// New root hash after applying.
161        new_root_hash: [u8; 32],
162    },
163
164    /// Some deltas could not be applied due to missing parents.
165    /// Suggests escalation to state-based sync.
166    MissingParents {
167        /// Delta IDs whose parents are missing.
168        missing_parent_deltas: Vec<[u8; 32]>,
169        /// Number of deltas successfully applied before failure.
170        applied_before_failure: usize,
171    },
172
173    /// Delta application failed (hash mismatch, corruption, etc.).
174    Failed {
175        /// Description of the failure.
176        reason: String,
177    },
178}
179
180impl DeltaApplyResult {
181    /// Check if delta application was fully successful.
182    #[must_use]
183    pub fn is_success(&self) -> bool {
184        matches!(self, Self::Success { .. })
185    }
186
187    /// Check if escalation to state-based sync is needed.
188    #[must_use]
189    pub fn needs_state_sync(&self) -> bool {
190        matches!(self, Self::MissingParents { .. })
191    }
192}
193
194// =============================================================================
195// Tests
196// =============================================================================
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_delta_sync_request_roundtrip() {
204        let request = DeltaSyncRequest::new(vec![[1; 32], [2; 32], [3; 32]]);
205
206        let encoded = borsh::to_vec(&request).expect("serialize");
207        let decoded: DeltaSyncRequest = borsh::from_slice(&encoded).expect("deserialize");
208
209        assert_eq!(request, decoded);
210        assert_eq!(decoded.count(), 3);
211    }
212
213    #[test]
214    fn test_delta_sync_request_threshold() {
215        // Within threshold
216        let small_request = DeltaSyncRequest::new(vec![[1; 32]; 10]);
217        assert!(small_request.is_within_threshold());
218
219        // At threshold
220        let at_threshold = DeltaSyncRequest::new(vec![[1; 32]; DEFAULT_DELTA_SYNC_THRESHOLD]);
221        assert!(at_threshold.is_within_threshold());
222
223        // Over threshold
224        let large_request = DeltaSyncRequest::new(vec![[1; 32]; DEFAULT_DELTA_SYNC_THRESHOLD + 1]);
225        assert!(!large_request.is_within_threshold());
226    }
227
228    #[test]
229    fn test_delta_payload_roundtrip() {
230        let payload = DeltaPayload {
231            id: [1; 32],
232            parents: vec![[2; 32], [3; 32]],
233            payload: vec![4, 5, 6, 7],
234            hlc_timestamp: 12345678,
235            expected_root_hash: [8; 32],
236        };
237
238        let encoded = borsh::to_vec(&payload).expect("serialize");
239        let decoded: DeltaPayload = borsh::from_slice(&encoded).expect("deserialize");
240
241        assert_eq!(payload, decoded);
242        assert!(!decoded.is_genesis());
243    }
244
245    #[test]
246    fn test_delta_payload_genesis() {
247        let genesis = DeltaPayload {
248            id: [1; 32],
249            parents: vec![], // No parents = genesis
250            payload: vec![1, 2, 3],
251            hlc_timestamp: 0,
252            expected_root_hash: [2; 32],
253        };
254
255        assert!(genesis.is_genesis());
256
257        let non_genesis = DeltaPayload {
258            id: [2; 32],
259            parents: vec![[1; 32]], // Has parent
260            payload: vec![4, 5, 6],
261            hlc_timestamp: 1,
262            expected_root_hash: [3; 32],
263        };
264
265        assert!(!non_genesis.is_genesis());
266    }
267
268    #[test]
269    fn test_delta_sync_response_roundtrip() {
270        let delta1 = DeltaPayload {
271            id: [1; 32],
272            parents: vec![],
273            payload: vec![1, 2, 3],
274            hlc_timestamp: 100,
275            expected_root_hash: [10; 32],
276        };
277        let delta2 = DeltaPayload {
278            id: [2; 32],
279            parents: vec![[1; 32]],
280            payload: vec![4, 5, 6],
281            hlc_timestamp: 200,
282            expected_root_hash: [20; 32],
283        };
284
285        let response = DeltaSyncResponse::new(vec![delta1, delta2], vec![[99; 32]]);
286
287        let encoded = borsh::to_vec(&response).expect("serialize");
288        let decoded: DeltaSyncResponse = borsh::from_slice(&encoded).expect("deserialize");
289
290        assert_eq!(response, decoded);
291        assert_eq!(decoded.count(), 2);
292        assert!(!decoded.is_complete()); // Has not_found entries
293    }
294
295    #[test]
296    fn test_delta_sync_response_complete() {
297        let delta = DeltaPayload {
298            id: [1; 32],
299            parents: vec![],
300            payload: vec![1, 2, 3],
301            hlc_timestamp: 100,
302            expected_root_hash: [10; 32],
303        };
304
305        let complete_response = DeltaSyncResponse::new(vec![delta], vec![]); // No not_found
306        assert!(complete_response.is_complete());
307
308        let incomplete_response = DeltaSyncResponse::empty(vec![[1; 32]]);
309        assert!(!incomplete_response.is_complete());
310        assert_eq!(incomplete_response.count(), 0);
311    }
312
313    #[test]
314    fn test_delta_apply_result_success() {
315        let success = DeltaApplyResult::Success {
316            applied_count: 5,
317            new_root_hash: [1; 32],
318        };
319        assert!(success.is_success());
320        assert!(!success.needs_state_sync());
321    }
322
323    #[test]
324    fn test_delta_apply_result_missing_parents() {
325        let missing = DeltaApplyResult::MissingParents {
326            missing_parent_deltas: vec![[1; 32]],
327            applied_before_failure: 3,
328        };
329        assert!(!missing.is_success());
330        assert!(missing.needs_state_sync());
331    }
332
333    #[test]
334    fn test_delta_apply_result_failed() {
335        let failed = DeltaApplyResult::Failed {
336            reason: "hash mismatch".to_string(),
337        };
338        assert!(!failed.is_success());
339        assert!(!failed.needs_state_sync());
340    }
341}