1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TreeSyncRequest {
14 pub local_root_hash: String,
16 pub local_node_count: usize,
18 pub full_snapshot: bool,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct TreeSyncResponse {
25 pub remote_root_hash: String,
27 pub in_sync: bool,
29 pub diff_nodes: Vec<TreeNodeDiff>,
31 pub deleted_paths: Vec<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TreeNodeDiff {
38 pub path: String,
40 pub kind: String,
42 pub hash: String,
44 pub metadata: HashMap<String, String>,
46 pub chain_seq: Option<u64>,
48 pub diff_type: TreeDiffType,
50}
51
52#[non_exhaustive]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum TreeDiffType {
56 Added,
58 Modified,
60 Removed,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct MerkleProof {
67 pub path: String,
69 pub node_hash: String,
71 pub sibling_hashes: Vec<String>,
73 pub root_hash: String,
75}
76
77impl MerkleProof {
78 pub fn verify(&self) -> bool {
82 !self.path.is_empty() && !self.node_hash.is_empty() && !self.root_hash.is_empty()
83 }
84
85 #[cfg(feature = "exochain")]
87 pub fn from_inclusion(proof: &exo_resource_tree::MerkleInclusionProof) -> Self {
88 let sibling_hashes: Vec<String> = proof
89 .levels
90 .iter()
91 .flat_map(|level| {
92 level
93 .child_hashes_sorted
94 .iter()
95 .map(|h| h.iter().map(|b| format!("{b:02x}")).collect::<String>())
96 })
97 .collect();
98 Self {
99 path: proof.path.to_string(),
100 node_hash: proof
101 .node_hash
102 .iter()
103 .map(|b| format!("{b:02x}"))
104 .collect(),
105 sibling_hashes,
106 root_hash: proof
107 .root_hash
108 .iter()
109 .map(|b| format!("{b:02x}"))
110 .collect(),
111 }
112 }
113}
114
115pub fn compare_tree_roots(local_hash: &str, remote_hash: &str) -> TreeSyncAction {
117 if local_hash == remote_hash {
118 TreeSyncAction::InSync
119 } else if local_hash.is_empty() {
120 TreeSyncAction::FullPull
121 } else if remote_hash.is_empty() {
122 TreeSyncAction::FullPush
123 } else {
124 TreeSyncAction::IncrementalSync
125 }
126}
127
128#[non_exhaustive]
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum TreeSyncAction {
132 InSync,
134 FullPull,
136 FullPush,
138 IncrementalSync,
140}
141
142#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn tree_sync_request_serde_roundtrip() {
150 let req = TreeSyncRequest {
151 local_root_hash: "abc123".into(),
152 local_node_count: 42,
153 full_snapshot: false,
154 };
155 let json = serde_json::to_string(&req).unwrap();
156 let decoded: TreeSyncRequest = serde_json::from_str(&json).unwrap();
157 assert_eq!(decoded.local_root_hash, "abc123");
158 assert_eq!(decoded.local_node_count, 42);
159 assert!(!decoded.full_snapshot);
160 }
161
162 #[test]
163 fn tree_sync_response_serde_roundtrip() {
164 let resp = TreeSyncResponse {
165 remote_root_hash: "root_hash".into(),
166 in_sync: false,
167 diff_nodes: vec![TreeNodeDiff {
168 path: "/ns/svc".into(),
169 kind: "service".into(),
170 hash: "node_hash".into(),
171 metadata: HashMap::from([("version".into(), "1".into())]),
172 chain_seq: Some(5),
173 diff_type: TreeDiffType::Added,
174 }],
175 deleted_paths: vec!["/ns/old".into()],
176 };
177 let json = serde_json::to_string(&resp).unwrap();
178 let decoded: TreeSyncResponse = serde_json::from_str(&json).unwrap();
179 assert_eq!(decoded.remote_root_hash, "root_hash");
180 assert!(!decoded.in_sync);
181 assert_eq!(decoded.diff_nodes.len(), 1);
182 assert_eq!(decoded.diff_nodes[0].path, "/ns/svc");
183 assert_eq!(decoded.diff_nodes[0].diff_type, TreeDiffType::Added);
184 assert_eq!(decoded.deleted_paths, vec!["/ns/old"]);
185 }
186
187 #[test]
188 fn tree_node_diff_serde_roundtrip() {
189 let diff = TreeNodeDiff {
190 path: "/agents/bot-1".into(),
191 kind: "agent".into(),
192 hash: "diff_hash".into(),
193 metadata: HashMap::new(),
194 chain_seq: None,
195 diff_type: TreeDiffType::Modified,
196 };
197 let json = serde_json::to_string(&diff).unwrap();
198 let decoded: TreeNodeDiff = serde_json::from_str(&json).unwrap();
199 assert_eq!(decoded.path, "/agents/bot-1");
200 assert_eq!(decoded.kind, "agent");
201 assert_eq!(decoded.diff_type, TreeDiffType::Modified);
202 assert!(decoded.chain_seq.is_none());
203 }
204
205 #[test]
206 fn merkle_proof_verify_valid() {
207 let proof = MerkleProof {
208 path: "/ns/svc".into(),
209 node_hash: "node_abc".into(),
210 sibling_hashes: vec!["sib1".into(), "sib2".into()],
211 root_hash: "root_xyz".into(),
212 };
213 assert!(proof.verify());
214 }
215
216 #[test]
217 fn merkle_proof_verify_empty_fields() {
218 let empty_path = MerkleProof {
219 path: "".into(),
220 node_hash: "abc".into(),
221 sibling_hashes: vec![],
222 root_hash: "root".into(),
223 };
224 assert!(!empty_path.verify());
225
226 let empty_node = MerkleProof {
227 path: "/x".into(),
228 node_hash: "".into(),
229 sibling_hashes: vec![],
230 root_hash: "root".into(),
231 };
232 assert!(!empty_node.verify());
233
234 let empty_root = MerkleProof {
235 path: "/x".into(),
236 node_hash: "abc".into(),
237 sibling_hashes: vec![],
238 root_hash: "".into(),
239 };
240 assert!(!empty_root.verify());
241 }
242
243 #[test]
244 fn compare_tree_roots_in_sync() {
245 assert_eq!(
246 compare_tree_roots("hash_a", "hash_a"),
247 TreeSyncAction::InSync
248 );
249 }
250
251 #[test]
252 fn compare_tree_roots_full_pull() {
253 assert_eq!(
254 compare_tree_roots("", "hash_remote"),
255 TreeSyncAction::FullPull
256 );
257 }
258
259 #[test]
260 fn compare_tree_roots_full_push() {
261 assert_eq!(
262 compare_tree_roots("hash_local", ""),
263 TreeSyncAction::FullPush
264 );
265 }
266
267 #[test]
268 fn compare_tree_roots_incremental() {
269 assert_eq!(
270 compare_tree_roots("hash_a", "hash_b"),
271 TreeSyncAction::IncrementalSync
272 );
273 }
274
275 #[test]
276 fn tree_diff_type_variants() {
277 for variant in [
279 TreeDiffType::Added,
280 TreeDiffType::Modified,
281 TreeDiffType::Removed,
282 ] {
283 let json = serde_json::to_string(&variant).unwrap();
284 let decoded: TreeDiffType = serde_json::from_str(&json).unwrap();
285 assert_eq!(decoded, variant);
286 }
287 }
288}