use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeSyncRequest {
pub local_root_hash: String,
pub local_node_count: usize,
pub full_snapshot: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeSyncResponse {
pub remote_root_hash: String,
pub in_sync: bool,
pub diff_nodes: Vec<TreeNodeDiff>,
pub deleted_paths: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreeNodeDiff {
pub path: String,
pub kind: String,
pub hash: String,
pub metadata: HashMap<String, String>,
pub chain_seq: Option<u64>,
pub diff_type: TreeDiffType,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TreeDiffType {
Added,
Modified,
Removed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleProof {
pub path: String,
pub node_hash: String,
pub sibling_hashes: Vec<String>,
pub root_hash: String,
}
impl MerkleProof {
pub fn verify(&self) -> bool {
!self.path.is_empty() && !self.node_hash.is_empty() && !self.root_hash.is_empty()
}
#[cfg(feature = "exochain")]
pub fn from_inclusion(proof: &exo_resource_tree::MerkleInclusionProof) -> Self {
let sibling_hashes: Vec<String> = proof
.levels
.iter()
.flat_map(|level| {
level
.child_hashes_sorted
.iter()
.map(|h| h.iter().map(|b| format!("{b:02x}")).collect::<String>())
})
.collect();
Self {
path: proof.path.to_string(),
node_hash: proof
.node_hash
.iter()
.map(|b| format!("{b:02x}"))
.collect(),
sibling_hashes,
root_hash: proof
.root_hash
.iter()
.map(|b| format!("{b:02x}"))
.collect(),
}
}
}
pub fn compare_tree_roots(local_hash: &str, remote_hash: &str) -> TreeSyncAction {
if local_hash == remote_hash {
TreeSyncAction::InSync
} else if local_hash.is_empty() {
TreeSyncAction::FullPull
} else if remote_hash.is_empty() {
TreeSyncAction::FullPush
} else {
TreeSyncAction::IncrementalSync
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TreeSyncAction {
InSync,
FullPull,
FullPush,
IncrementalSync,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tree_sync_request_serde_roundtrip() {
let req = TreeSyncRequest {
local_root_hash: "abc123".into(),
local_node_count: 42,
full_snapshot: false,
};
let json = serde_json::to_string(&req).unwrap();
let decoded: TreeSyncRequest = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.local_root_hash, "abc123");
assert_eq!(decoded.local_node_count, 42);
assert!(!decoded.full_snapshot);
}
#[test]
fn tree_sync_response_serde_roundtrip() {
let resp = TreeSyncResponse {
remote_root_hash: "root_hash".into(),
in_sync: false,
diff_nodes: vec![TreeNodeDiff {
path: "/ns/svc".into(),
kind: "service".into(),
hash: "node_hash".into(),
metadata: HashMap::from([("version".into(), "1".into())]),
chain_seq: Some(5),
diff_type: TreeDiffType::Added,
}],
deleted_paths: vec!["/ns/old".into()],
};
let json = serde_json::to_string(&resp).unwrap();
let decoded: TreeSyncResponse = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.remote_root_hash, "root_hash");
assert!(!decoded.in_sync);
assert_eq!(decoded.diff_nodes.len(), 1);
assert_eq!(decoded.diff_nodes[0].path, "/ns/svc");
assert_eq!(decoded.diff_nodes[0].diff_type, TreeDiffType::Added);
assert_eq!(decoded.deleted_paths, vec!["/ns/old"]);
}
#[test]
fn tree_node_diff_serde_roundtrip() {
let diff = TreeNodeDiff {
path: "/agents/bot-1".into(),
kind: "agent".into(),
hash: "diff_hash".into(),
metadata: HashMap::new(),
chain_seq: None,
diff_type: TreeDiffType::Modified,
};
let json = serde_json::to_string(&diff).unwrap();
let decoded: TreeNodeDiff = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.path, "/agents/bot-1");
assert_eq!(decoded.kind, "agent");
assert_eq!(decoded.diff_type, TreeDiffType::Modified);
assert!(decoded.chain_seq.is_none());
}
#[test]
fn merkle_proof_verify_valid() {
let proof = MerkleProof {
path: "/ns/svc".into(),
node_hash: "node_abc".into(),
sibling_hashes: vec!["sib1".into(), "sib2".into()],
root_hash: "root_xyz".into(),
};
assert!(proof.verify());
}
#[test]
fn merkle_proof_verify_empty_fields() {
let empty_path = MerkleProof {
path: "".into(),
node_hash: "abc".into(),
sibling_hashes: vec![],
root_hash: "root".into(),
};
assert!(!empty_path.verify());
let empty_node = MerkleProof {
path: "/x".into(),
node_hash: "".into(),
sibling_hashes: vec![],
root_hash: "root".into(),
};
assert!(!empty_node.verify());
let empty_root = MerkleProof {
path: "/x".into(),
node_hash: "abc".into(),
sibling_hashes: vec![],
root_hash: "".into(),
};
assert!(!empty_root.verify());
}
#[test]
fn compare_tree_roots_in_sync() {
assert_eq!(
compare_tree_roots("hash_a", "hash_a"),
TreeSyncAction::InSync
);
}
#[test]
fn compare_tree_roots_full_pull() {
assert_eq!(
compare_tree_roots("", "hash_remote"),
TreeSyncAction::FullPull
);
}
#[test]
fn compare_tree_roots_full_push() {
assert_eq!(
compare_tree_roots("hash_local", ""),
TreeSyncAction::FullPush
);
}
#[test]
fn compare_tree_roots_incremental() {
assert_eq!(
compare_tree_roots("hash_a", "hash_b"),
TreeSyncAction::IncrementalSync
);
}
#[test]
fn tree_diff_type_variants() {
for variant in [
TreeDiffType::Added,
TreeDiffType::Modified,
TreeDiffType::Removed,
] {
let json = serde_json::to_string(&variant).unwrap();
let decoded: TreeDiffType = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, variant);
}
}
}