1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ChainSyncRequest {
12 pub chain_id: u32,
14 pub after_sequence: u64,
16 pub after_hash: String,
18 pub max_events: u32,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ChainSyncResponse {
25 pub chain_id: u32,
27 pub events: Vec<serde_json::Value>,
29 pub has_more: bool,
31 pub tip_sequence: u64,
33 pub tip_hash: String,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ChainBridgeEvent {
40 pub remote_node_id: String,
42 pub remote_head_sequence: u64,
44 pub remote_head_hash: String,
46 pub anchored_at: chrono::DateTime<chrono::Utc>,
48}
49
50#[non_exhaustive]
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub enum ChainForkStatus {
54 InSync,
56 LocalBehind {
58 local_seq: u64,
60 remote_seq: u64,
62 },
63 RemoteBehind {
65 local_seq: u64,
67 remote_seq: u64,
69 },
70 Forked {
72 fork_point: u64,
74 local_hash: String,
76 remote_hash: String,
78 },
79}
80
81pub fn detect_chain_fork(
83 local_seq: u64,
84 local_hash: &str,
85 remote_seq: u64,
86 remote_hash: &str,
87) -> ChainForkStatus {
88 if local_seq == remote_seq && local_hash == remote_hash {
89 ChainForkStatus::InSync
90 } else if local_seq < remote_seq {
91 ChainForkStatus::LocalBehind {
92 local_seq,
93 remote_seq,
94 }
95 } else if local_seq > remote_seq {
96 ChainForkStatus::RemoteBehind {
97 local_seq,
98 remote_seq,
99 }
100 } else {
101 ChainForkStatus::Forked {
103 fork_point: local_seq,
104 local_hash: local_hash.to_string(),
105 remote_hash: remote_hash.to_string(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct SyncStateDigest {
113 pub chain_seq: u64,
115 pub chain_hash: String,
117 pub tree_root_hash: String,
119 pub governance_rule_count: u32,
121 pub uptime_secs: u64,
123}
124
125pub const CHECKPOINT_CATCHUP_THRESHOLD: u64 = 1000;
129
130pub fn sync_strategy(local_seq: u64, remote_seq: u64) -> ChainSyncStrategy {
132 let behind = local_seq.saturating_sub(remote_seq);
133 if behind == 0 {
134 ChainSyncStrategy::InSync
135 } else if behind <= CHECKPOINT_CATCHUP_THRESHOLD {
136 ChainSyncStrategy::EventByEvent { count: behind }
137 } else {
138 ChainSyncStrategy::CheckpointCatchup {
139 behind,
140 checkpoint_seq: remote_seq,
141 }
142 }
143}
144
145#[non_exhaustive]
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum ChainSyncStrategy {
149 InSync,
151 EventByEvent { count: u64 },
153 CheckpointCatchup { behind: u64, checkpoint_seq: u64 },
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct SyncFrame {
162 pub stream_type: SyncStreamType,
164 pub frame_seq: u64,
166 pub payload_type: SyncPayloadType,
168}
169
170#[non_exhaustive]
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173#[repr(u8)]
174pub enum SyncStreamType {
175 Control = 0,
176 Chain = 1,
177 Tree = 2,
178 Causal = 3,
179 Hnsw = 4,
180 CrossRef = 5,
181 Impulse = 6,
182 Ipc = 7,
183}
184
185#[non_exhaustive]
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188#[repr(u8)]
189pub enum SyncPayloadType {
190 StateDigest = 0x00,
191 ChainEvents = 0x01,
192 TreeDiff = 0x02,
193 CausalDelta = 0x03,
194 HnswBatch = 0x04,
195 CrossRefBatch = 0x05,
196 ImpulseFlood = 0x06,
197 Ack = 0x0F,
198}
199
200pub fn stream_priority(stream_type: SyncStreamType) -> u8 {
202 match stream_type {
203 SyncStreamType::Control => 0, SyncStreamType::Chain => 1,
205 SyncStreamType::Tree => 2,
206 SyncStreamType::Ipc => 3,
207 SyncStreamType::Causal => 4,
208 SyncStreamType::CrossRef => 4,
209 SyncStreamType::Hnsw => 5,
210 SyncStreamType::Impulse => 6, }
212}
213
214#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn chain_sync_request_serde_roundtrip() {
222 let req = ChainSyncRequest {
223 chain_id: 1,
224 after_sequence: 42,
225 after_hash: "abc123".into(),
226 max_events: 100,
227 };
228 let json = serde_json::to_string(&req).unwrap();
229 let decoded: ChainSyncRequest = serde_json::from_str(&json).unwrap();
230 assert_eq!(decoded.chain_id, 1);
231 assert_eq!(decoded.after_sequence, 42);
232 assert_eq!(decoded.after_hash, "abc123");
233 assert_eq!(decoded.max_events, 100);
234 }
235
236 #[test]
237 fn chain_sync_response_serde_roundtrip() {
238 let resp = ChainSyncResponse {
239 chain_id: 0,
240 events: vec![serde_json::json!({"type": "write", "key": "a"})],
241 has_more: true,
242 tip_sequence: 99,
243 tip_hash: "tip_hash_val".into(),
244 };
245 let json = serde_json::to_string(&resp).unwrap();
246 let decoded: ChainSyncResponse = serde_json::from_str(&json).unwrap();
247 assert_eq!(decoded.chain_id, 0);
248 assert_eq!(decoded.events.len(), 1);
249 assert!(decoded.has_more);
250 assert_eq!(decoded.tip_sequence, 99);
251 assert_eq!(decoded.tip_hash, "tip_hash_val");
252 }
253
254 #[test]
255 fn chain_bridge_event_serde_roundtrip() {
256 let evt = ChainBridgeEvent {
257 remote_node_id: "node-42".into(),
258 remote_head_sequence: 10,
259 remote_head_hash: "deadbeef".into(),
260 anchored_at: chrono::Utc::now(),
261 };
262 let json = serde_json::to_string(&evt).unwrap();
263 let decoded: ChainBridgeEvent = serde_json::from_str(&json).unwrap();
264 assert_eq!(decoded.remote_node_id, "node-42");
265 assert_eq!(decoded.remote_head_sequence, 10);
266 assert_eq!(decoded.remote_head_hash, "deadbeef");
267 }
268
269 #[test]
270 fn detect_chain_fork_in_sync() {
271 let status = detect_chain_fork(5, "hash_a", 5, "hash_a");
272 assert_eq!(status, ChainForkStatus::InSync);
273 }
274
275 #[test]
276 fn detect_chain_fork_local_behind() {
277 let status = detect_chain_fork(3, "hash_a", 7, "hash_b");
278 assert_eq!(
279 status,
280 ChainForkStatus::LocalBehind {
281 local_seq: 3,
282 remote_seq: 7,
283 }
284 );
285 }
286
287 #[test]
288 fn detect_chain_fork_remote_behind() {
289 let status = detect_chain_fork(10, "hash_a", 5, "hash_b");
290 assert_eq!(
291 status,
292 ChainForkStatus::RemoteBehind {
293 local_seq: 10,
294 remote_seq: 5,
295 }
296 );
297 }
298
299 #[test]
300 fn detect_chain_fork_forked() {
301 let status = detect_chain_fork(8, "hash_local", 8, "hash_remote");
302 assert_eq!(
303 status,
304 ChainForkStatus::Forked {
305 fork_point: 8,
306 local_hash: "hash_local".into(),
307 remote_hash: "hash_remote".into(),
308 }
309 );
310 }
311
312 #[test]
315 fn sync_strategy_in_sync() {
316 assert_eq!(sync_strategy(100, 100), ChainSyncStrategy::InSync);
317 }
318
319 #[test]
320 fn sync_strategy_event_by_event() {
321 let strategy = sync_strategy(1100, 500);
322 assert_eq!(strategy, ChainSyncStrategy::EventByEvent { count: 600 });
323 }
324
325 #[test]
326 fn sync_strategy_checkpoint_catchup() {
327 let strategy = sync_strategy(5000, 100);
328 assert_eq!(
329 strategy,
330 ChainSyncStrategy::CheckpointCatchup {
331 behind: 4900,
332 checkpoint_seq: 100,
333 }
334 );
335 }
336
337 #[test]
338 fn sync_strategy_boundary_at_threshold() {
339 let strategy = sync_strategy(1000, 0);
341 assert_eq!(strategy, ChainSyncStrategy::EventByEvent { count: 1000 });
342 let strategy = sync_strategy(1001, 0);
344 assert_eq!(
345 strategy,
346 ChainSyncStrategy::CheckpointCatchup {
347 behind: 1001,
348 checkpoint_seq: 0,
349 }
350 );
351 }
352
353 #[test]
354 fn sync_strategy_remote_ahead() {
355 assert_eq!(sync_strategy(50, 100), ChainSyncStrategy::InSync);
357 }
358
359 #[test]
362 fn sync_frame_serde_roundtrip() {
363 let frame = SyncFrame {
364 stream_type: SyncStreamType::Chain,
365 frame_seq: 42,
366 payload_type: SyncPayloadType::ChainEvents,
367 };
368 let json = serde_json::to_string(&frame).unwrap();
369 let restored: SyncFrame = serde_json::from_str(&json).unwrap();
370 assert_eq!(restored.stream_type, SyncStreamType::Chain);
371 assert_eq!(restored.frame_seq, 42);
372 assert_eq!(restored.payload_type, SyncPayloadType::ChainEvents);
373 }
374
375 #[test]
376 fn sync_stream_type_serde_roundtrip() {
377 let types = [
378 SyncStreamType::Control,
379 SyncStreamType::Chain,
380 SyncStreamType::Tree,
381 SyncStreamType::Causal,
382 SyncStreamType::Hnsw,
383 SyncStreamType::CrossRef,
384 SyncStreamType::Impulse,
385 SyncStreamType::Ipc,
386 ];
387 for t in types {
388 let json = serde_json::to_string(&t).unwrap();
389 let restored: SyncStreamType = serde_json::from_str(&json).unwrap();
390 assert_eq!(restored, t);
391 }
392 }
393
394 #[test]
395 fn stream_priority_ordering() {
396 assert!(stream_priority(SyncStreamType::Control) < stream_priority(SyncStreamType::Chain));
398 assert!(stream_priority(SyncStreamType::Chain) < stream_priority(SyncStreamType::Tree));
399 assert!(stream_priority(SyncStreamType::Tree) < stream_priority(SyncStreamType::Ipc));
400 assert!(stream_priority(SyncStreamType::Ipc) < stream_priority(SyncStreamType::Hnsw));
401 assert!(stream_priority(SyncStreamType::Hnsw) < stream_priority(SyncStreamType::Impulse));
402 assert_eq!(
404 stream_priority(SyncStreamType::Causal),
405 stream_priority(SyncStreamType::CrossRef)
406 );
407 }
408
409 #[test]
410 fn sync_state_digest_serde_roundtrip() {
411 let digest = SyncStateDigest {
412 chain_seq: 100,
413 chain_hash: "head_hash".into(),
414 tree_root_hash: "merkle_root".into(),
415 governance_rule_count: 5,
416 uptime_secs: 3600,
417 };
418 let json = serde_json::to_string(&digest).unwrap();
419 let decoded: SyncStateDigest = serde_json::from_str(&json).unwrap();
420 assert_eq!(decoded.chain_seq, 100);
421 assert_eq!(decoded.chain_hash, "head_hash");
422 assert_eq!(decoded.tree_root_hash, "merkle_root");
423 assert_eq!(decoded.governance_rule_count, 5);
424 assert_eq!(decoded.uptime_secs, 3600);
425 }
426}