use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainSyncRequest {
pub chain_id: u32,
pub after_sequence: u64,
pub after_hash: String,
pub max_events: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainSyncResponse {
pub chain_id: u32,
pub events: Vec<serde_json::Value>,
pub has_more: bool,
pub tip_sequence: u64,
pub tip_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainBridgeEvent {
pub remote_node_id: String,
pub remote_head_sequence: u64,
pub remote_head_hash: String,
pub anchored_at: chrono::DateTime<chrono::Utc>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChainForkStatus {
InSync,
LocalBehind {
local_seq: u64,
remote_seq: u64,
},
RemoteBehind {
local_seq: u64,
remote_seq: u64,
},
Forked {
fork_point: u64,
local_hash: String,
remote_hash: String,
},
}
pub fn detect_chain_fork(
local_seq: u64,
local_hash: &str,
remote_seq: u64,
remote_hash: &str,
) -> ChainForkStatus {
if local_seq == remote_seq && local_hash == remote_hash {
ChainForkStatus::InSync
} else if local_seq < remote_seq {
ChainForkStatus::LocalBehind {
local_seq,
remote_seq,
}
} else if local_seq > remote_seq {
ChainForkStatus::RemoteBehind {
local_seq,
remote_seq,
}
} else {
ChainForkStatus::Forked {
fork_point: local_seq,
local_hash: local_hash.to_string(),
remote_hash: remote_hash.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStateDigest {
pub chain_seq: u64,
pub chain_hash: String,
pub tree_root_hash: String,
pub governance_rule_count: u32,
pub uptime_secs: u64,
}
pub const CHECKPOINT_CATCHUP_THRESHOLD: u64 = 1000;
pub fn sync_strategy(local_seq: u64, remote_seq: u64) -> ChainSyncStrategy {
let behind = local_seq.saturating_sub(remote_seq);
if behind == 0 {
ChainSyncStrategy::InSync
} else if behind <= CHECKPOINT_CATCHUP_THRESHOLD {
ChainSyncStrategy::EventByEvent { count: behind }
} else {
ChainSyncStrategy::CheckpointCatchup {
behind,
checkpoint_seq: remote_seq,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainSyncStrategy {
InSync,
EventByEvent { count: u64 },
CheckpointCatchup { behind: u64, checkpoint_seq: u64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncFrame {
pub stream_type: SyncStreamType,
pub frame_seq: u64,
pub payload_type: SyncPayloadType,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum SyncStreamType {
Control = 0,
Chain = 1,
Tree = 2,
Causal = 3,
Hnsw = 4,
CrossRef = 5,
Impulse = 6,
Ipc = 7,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum SyncPayloadType {
StateDigest = 0x00,
ChainEvents = 0x01,
TreeDiff = 0x02,
CausalDelta = 0x03,
HnswBatch = 0x04,
CrossRefBatch = 0x05,
ImpulseFlood = 0x06,
Ack = 0x0F,
}
pub fn stream_priority(stream_type: SyncStreamType) -> u8 {
match stream_type {
SyncStreamType::Control => 0, SyncStreamType::Chain => 1,
SyncStreamType::Tree => 2,
SyncStreamType::Ipc => 3,
SyncStreamType::Causal => 4,
SyncStreamType::CrossRef => 4,
SyncStreamType::Hnsw => 5,
SyncStreamType::Impulse => 6, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chain_sync_request_serde_roundtrip() {
let req = ChainSyncRequest {
chain_id: 1,
after_sequence: 42,
after_hash: "abc123".into(),
max_events: 100,
};
let json = serde_json::to_string(&req).unwrap();
let decoded: ChainSyncRequest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.chain_id, 1);
assert_eq!(decoded.after_sequence, 42);
assert_eq!(decoded.after_hash, "abc123");
assert_eq!(decoded.max_events, 100);
}
#[test]
fn chain_sync_response_serde_roundtrip() {
let resp = ChainSyncResponse {
chain_id: 0,
events: vec![serde_json::json!({"type": "write", "key": "a"})],
has_more: true,
tip_sequence: 99,
tip_hash: "tip_hash_val".into(),
};
let json = serde_json::to_string(&resp).unwrap();
let decoded: ChainSyncResponse = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.chain_id, 0);
assert_eq!(decoded.events.len(), 1);
assert!(decoded.has_more);
assert_eq!(decoded.tip_sequence, 99);
assert_eq!(decoded.tip_hash, "tip_hash_val");
}
#[test]
fn chain_bridge_event_serde_roundtrip() {
let evt = ChainBridgeEvent {
remote_node_id: "node-42".into(),
remote_head_sequence: 10,
remote_head_hash: "deadbeef".into(),
anchored_at: chrono::Utc::now(),
};
let json = serde_json::to_string(&evt).unwrap();
let decoded: ChainBridgeEvent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.remote_node_id, "node-42");
assert_eq!(decoded.remote_head_sequence, 10);
assert_eq!(decoded.remote_head_hash, "deadbeef");
}
#[test]
fn detect_chain_fork_in_sync() {
let status = detect_chain_fork(5, "hash_a", 5, "hash_a");
assert_eq!(status, ChainForkStatus::InSync);
}
#[test]
fn detect_chain_fork_local_behind() {
let status = detect_chain_fork(3, "hash_a", 7, "hash_b");
assert_eq!(
status,
ChainForkStatus::LocalBehind {
local_seq: 3,
remote_seq: 7,
}
);
}
#[test]
fn detect_chain_fork_remote_behind() {
let status = detect_chain_fork(10, "hash_a", 5, "hash_b");
assert_eq!(
status,
ChainForkStatus::RemoteBehind {
local_seq: 10,
remote_seq: 5,
}
);
}
#[test]
fn detect_chain_fork_forked() {
let status = detect_chain_fork(8, "hash_local", 8, "hash_remote");
assert_eq!(
status,
ChainForkStatus::Forked {
fork_point: 8,
local_hash: "hash_local".into(),
remote_hash: "hash_remote".into(),
}
);
}
#[test]
fn sync_strategy_in_sync() {
assert_eq!(sync_strategy(100, 100), ChainSyncStrategy::InSync);
}
#[test]
fn sync_strategy_event_by_event() {
let strategy = sync_strategy(1100, 500);
assert_eq!(strategy, ChainSyncStrategy::EventByEvent { count: 600 });
}
#[test]
fn sync_strategy_checkpoint_catchup() {
let strategy = sync_strategy(5000, 100);
assert_eq!(
strategy,
ChainSyncStrategy::CheckpointCatchup {
behind: 4900,
checkpoint_seq: 100,
}
);
}
#[test]
fn sync_strategy_boundary_at_threshold() {
let strategy = sync_strategy(1000, 0);
assert_eq!(strategy, ChainSyncStrategy::EventByEvent { count: 1000 });
let strategy = sync_strategy(1001, 0);
assert_eq!(
strategy,
ChainSyncStrategy::CheckpointCatchup {
behind: 1001,
checkpoint_seq: 0,
}
);
}
#[test]
fn sync_strategy_remote_ahead() {
assert_eq!(sync_strategy(50, 100), ChainSyncStrategy::InSync);
}
#[test]
fn sync_frame_serde_roundtrip() {
let frame = SyncFrame {
stream_type: SyncStreamType::Chain,
frame_seq: 42,
payload_type: SyncPayloadType::ChainEvents,
};
let json = serde_json::to_string(&frame).unwrap();
let restored: SyncFrame = serde_json::from_str(&json).unwrap();
assert_eq!(restored.stream_type, SyncStreamType::Chain);
assert_eq!(restored.frame_seq, 42);
assert_eq!(restored.payload_type, SyncPayloadType::ChainEvents);
}
#[test]
fn sync_stream_type_serde_roundtrip() {
let types = [
SyncStreamType::Control,
SyncStreamType::Chain,
SyncStreamType::Tree,
SyncStreamType::Causal,
SyncStreamType::Hnsw,
SyncStreamType::CrossRef,
SyncStreamType::Impulse,
SyncStreamType::Ipc,
];
for t in types {
let json = serde_json::to_string(&t).unwrap();
let restored: SyncStreamType = serde_json::from_str(&json).unwrap();
assert_eq!(restored, t);
}
}
#[test]
fn stream_priority_ordering() {
assert!(stream_priority(SyncStreamType::Control) < stream_priority(SyncStreamType::Chain));
assert!(stream_priority(SyncStreamType::Chain) < stream_priority(SyncStreamType::Tree));
assert!(stream_priority(SyncStreamType::Tree) < stream_priority(SyncStreamType::Ipc));
assert!(stream_priority(SyncStreamType::Ipc) < stream_priority(SyncStreamType::Hnsw));
assert!(stream_priority(SyncStreamType::Hnsw) < stream_priority(SyncStreamType::Impulse));
assert_eq!(
stream_priority(SyncStreamType::Causal),
stream_priority(SyncStreamType::CrossRef)
);
}
#[test]
fn sync_state_digest_serde_roundtrip() {
let digest = SyncStateDigest {
chain_seq: 100,
chain_hash: "head_hash".into(),
tree_root_hash: "merkle_root".into(),
governance_rule_count: 5,
uptime_secs: 3600,
};
let json = serde_json::to_string(&digest).unwrap();
let decoded: SyncStateDigest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.chain_seq, 100);
assert_eq!(decoded.chain_hash, "head_hash");
assert_eq!(decoded.tree_root_hash, "merkle_root");
assert_eq!(decoded.governance_rule_count, 5);
assert_eq!(decoded.uptime_secs, 3600);
}
}