Skip to main content

calimero_node_primitives/sync/
state_machine.rs

1//! Shared sync state abstractions for protocol negotiation.
2//!
3//! Provides types and functions shared between production code (`SyncManager`)
4//! and simulation (`SimNode`) for building handshakes and protocol selection.
5//!
6//! # Design
7//!
8//! - `LocalSyncState` trait abstracts access to node state
9//! - `build_handshake()` creates handshakes from any implementor
10//! - Estimation functions provide consistent heuristics for both environments
11//!
12//! # Usage
13//!
14//! ```rust,ignore
15//! // Any type can provide its local state
16//! impl LocalSyncState for MyNode {
17//!     fn root_hash(&self) -> [u8; 32] { ... }
18//!     fn entity_count(&self) -> u64 { ... }
19//!     fn max_depth(&self) -> u32 { ... }
20//!     fn dag_heads(&self) -> Vec<[u8; 32]> { ... }
21//! }
22//!
23//! // Build handshake using shared logic
24//! let handshake = build_handshake(&my_node);
25//!
26//! // Use select_protocol() for negotiation
27//! let selection = select_protocol(&local_hs, &remote_hs);
28//! ```
29
30use super::handshake::SyncHandshake;
31
32// =============================================================================
33// Local State Trait
34// =============================================================================
35
36/// Trait for accessing local sync state.
37///
38/// Implement this trait to enable building handshakes and protocol selection.
39/// Both `SyncManager` (production) and `SimNode` (simulation) implement this,
40/// ensuring consistent behavior across both environments.
41pub trait LocalSyncState {
42    /// Current Merkle root hash.
43    ///
44    /// Returns `[0; 32]` for fresh nodes with no state.
45    fn root_hash(&self) -> [u8; 32];
46
47    /// Number of entities in local storage.
48    ///
49    /// Used for divergence calculation in protocol selection.
50    fn entity_count(&self) -> u64;
51
52    /// Maximum depth of the Merkle tree.
53    ///
54    /// Used to select optimal sync protocol:
55    /// - Depth > 3 with low divergence → SubtreePrefetch
56    /// - Depth 1-2 with many children → LevelWise
57    fn max_depth(&self) -> u32;
58
59    /// Current DAG heads (latest delta IDs).
60    ///
61    /// Used for delta sync compatibility checking.
62    fn dag_heads(&self) -> Vec<[u8; 32]>;
63
64    /// Whether this node has any state.
65    ///
66    /// Default implementation: `root_hash != [0; 32]`
67    fn has_state(&self) -> bool {
68        self.root_hash() != [0; 32]
69    }
70}
71
72// =============================================================================
73// Handshake Builder
74// =============================================================================
75
76/// Build a `SyncHandshake` from any type implementing `LocalSyncState`.
77///
78/// This is the canonical way to create handshakes, ensuring consistent
79/// behavior between production and simulation code.
80///
81/// # Example
82///
83/// ```rust,ignore
84/// let handshake = build_handshake(&my_node);
85/// ```
86#[must_use]
87pub fn build_handshake<T: LocalSyncState>(state: &T) -> SyncHandshake {
88    SyncHandshake::new(
89        state.root_hash(),
90        state.entity_count(),
91        state.max_depth(),
92        state.dag_heads(),
93    )
94}
95
96/// Build a `SyncHandshake` from raw state values.
97///
98/// Useful when you have the values but don't have a `LocalSyncState` implementor.
99#[must_use]
100pub fn build_handshake_from_raw(
101    root_hash: [u8; 32],
102    entity_count: u64,
103    max_depth: u32,
104    dag_heads: Vec<[u8; 32]>,
105) -> SyncHandshake {
106    SyncHandshake::new(root_hash, entity_count, max_depth, dag_heads)
107}
108
109// =============================================================================
110// Estimation Helpers
111// =============================================================================
112
113/// Estimate entity count from available data.
114///
115/// This provides a consistent heuristic used when exact entity count is unknown.
116/// The estimation is based on:
117/// - Zero if root hash is zero (fresh node)
118/// - dag_heads.len() if available, minimum 1 if has state
119///
120/// # Arguments
121///
122/// * `root_hash` - The Merkle root hash
123/// * `dag_heads_len` - Number of DAG heads
124#[must_use]
125pub fn estimate_entity_count(root_hash: [u8; 32], dag_heads_len: usize) -> u64 {
126    if root_hash == [0; 32] {
127        0
128    } else if dag_heads_len == 0 {
129        1 // Has state but no heads - assume at least 1 entity
130    } else {
131        dag_heads_len as u64
132    }
133}
134
135/// Estimate max depth from entity count.
136///
137/// Uses log2 approximation for balanced tree estimation:
138/// `max_depth ≈ ceil(log2(entity_count))`
139///
140/// This is bounded to a reasonable maximum to prevent overflow.
141///
142/// # Arguments
143///
144/// * `entity_count` - Number of entities in the tree
145#[must_use]
146pub fn estimate_max_depth(entity_count: u64) -> u32 {
147    if entity_count == 0 {
148        0
149    } else {
150        // log2(n) ≈ 64 - leading_zeros(n)
151        // For a balanced tree, depth is roughly log2(entity_count)
152        let log2_approx = 64u32.saturating_sub(entity_count.leading_zeros());
153        log2_approx.max(1).min(32)
154    }
155}
156
157// =============================================================================
158// Tests
159// =============================================================================
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    // Test implementation of LocalSyncState
166    struct TestNode {
167        root_hash: [u8; 32],
168        entity_count: u64,
169        max_depth: u32,
170        dag_heads: Vec<[u8; 32]>,
171    }
172
173    impl LocalSyncState for TestNode {
174        fn root_hash(&self) -> [u8; 32] {
175            self.root_hash
176        }
177
178        fn entity_count(&self) -> u64 {
179            self.entity_count
180        }
181
182        fn max_depth(&self) -> u32 {
183            self.max_depth
184        }
185
186        fn dag_heads(&self) -> Vec<[u8; 32]> {
187            self.dag_heads.clone()
188        }
189    }
190
191    #[test]
192    fn test_build_handshake_fresh_node() {
193        let node = TestNode {
194            root_hash: [0; 32],
195            entity_count: 0,
196            max_depth: 0,
197            dag_heads: vec![],
198        };
199
200        let hs = build_handshake(&node);
201
202        assert_eq!(hs.root_hash, [0; 32]);
203        assert_eq!(hs.entity_count, 0);
204        assert_eq!(hs.max_depth, 0);
205        assert!(hs.dag_heads.is_empty());
206        assert!(!hs.has_state);
207    }
208
209    #[test]
210    fn test_build_handshake_initialized_node() {
211        let node = TestNode {
212            root_hash: [42; 32],
213            entity_count: 100,
214            max_depth: 5,
215            dag_heads: vec![[1; 32], [2; 32]],
216        };
217
218        let hs = build_handshake(&node);
219
220        assert_eq!(hs.root_hash, [42; 32]);
221        assert_eq!(hs.entity_count, 100);
222        assert_eq!(hs.max_depth, 5);
223        assert_eq!(hs.dag_heads.len(), 2);
224        assert!(hs.has_state);
225    }
226
227    #[test]
228    fn test_estimate_entity_count() {
229        // Fresh node
230        assert_eq!(estimate_entity_count([0; 32], 0), 0);
231
232        // Has state, no heads
233        assert_eq!(estimate_entity_count([1; 32], 0), 1);
234
235        // Has state with heads
236        assert_eq!(estimate_entity_count([1; 32], 5), 5);
237    }
238
239    #[test]
240    fn test_estimate_max_depth() {
241        assert_eq!(estimate_max_depth(0), 0);
242        assert_eq!(estimate_max_depth(1), 1);
243        assert_eq!(estimate_max_depth(2), 2);
244        assert_eq!(estimate_max_depth(16), 5); // log2(16) = 4, +1 = 5
245        assert_eq!(estimate_max_depth(256), 9); // log2(256) = 8, +1 = 9
246    }
247
248    #[test]
249    fn test_has_state_default_implementation() {
250        let fresh = TestNode {
251            root_hash: [0; 32],
252            entity_count: 0,
253            max_depth: 0,
254            dag_heads: vec![],
255        };
256        assert!(!fresh.has_state());
257
258        let initialized = TestNode {
259            root_hash: [1; 32],
260            entity_count: 1,
261            max_depth: 1,
262            dag_heads: vec![],
263        };
264        assert!(initialized.has_state());
265    }
266}