Skip to main content

ipfrs_tensorlogic/
proof_serializer.rs

1//! Proof Serializer
2//!
3//! Serializes and deserializes distributed backward-chaining proof trees for
4//! storage in IPLD and transmission over the network.
5//!
6//! A [`ProofTreeInput`] (logical tree) is flattened in DFS order into a
7//! [`SerializedProof`] whose `proof_id` is the FNV-1a hex digest of the
8//! sorted, concatenated node IDs.  Round-trip fidelity is guaranteed: every
9//! field, binding, and topology is reconstructed by [`ProofSerializer::deserialize`].
10//!
11//! # Examples
12//!
13//! ```
14//! use ipfrs_tensorlogic::proof_serializer::{
15//!     ProofNodeInput, ProofSerializer, ProofTreeInput,
16//! };
17//! use std::collections::HashMap;
18//!
19//! // Build a trivial single-node proof.
20//! let mut nodes = HashMap::new();
21//! nodes.insert(
22//!     "n0".to_string(),
23//!     ProofNodeInput {
24//!         rule_id: None,
25//!         bindings: HashMap::new(),
26//!         children: vec![],
27//!         peer_id: None,
28//!     },
29//! );
30//! let input = ProofTreeInput {
31//!     root_goal: "n0".to_string(),
32//!     nodes,
33//!     proved: true,
34//! };
35//!
36//! let ser = ProofSerializer::default();
37//! let proof = ser.serialize(&input).expect("example: should succeed in docs");
38//! assert!(proof.proved);
39//! assert_eq!(proof.depth, 0);
40//! assert_eq!(proof.edge_count, 0);
41//! ```
42
43use std::collections::{HashMap, HashSet};
44use std::sync::atomic::{AtomicU64, Ordering};
45use std::time::{SystemTime, UNIX_EPOCH};
46
47use serde::{Deserialize, Serialize};
48use thiserror::Error;
49
50// ─── Errors ──────────────────────────────────────────────────────────────────
51
52/// Errors produced by [`ProofSerializer`].
53#[derive(Debug, Error)]
54pub enum ProofSerError {
55    /// A node referenced by `root_goal` or a child ID is absent from `nodes`.
56    #[error("missing node: {0}")]
57    MissingNode(String),
58
59    /// The proof tree contains a cycle (a node appears in its own transitive children).
60    #[error("cycle detected in proof tree")]
61    CycleDetected,
62
63    /// JSON encoding/decoding error.
64    #[error("JSON error: {0}")]
65    JsonError(String),
66
67    /// `serialize` was called with an empty `nodes` map.
68    #[error("proof tree is empty")]
69    EmptyTree,
70}
71
72// ─── Input types ─────────────────────────────────────────────────────────────
73
74/// A single node as provided by the caller prior to serialization.
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76pub struct ProofNodeInput {
77    /// Identifier of the rule applied at this node, if any.
78    pub rule_id: Option<String>,
79
80    /// Variable bindings established at this node.
81    pub bindings: HashMap<String, String>,
82
83    /// Ordered list of child node IDs.
84    pub children: Vec<String>,
85
86    /// ID of the remote peer that resolved this goal (if any).
87    pub peer_id: Option<String>,
88}
89
90/// Full proof tree as produced by the distributed backward chainer.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92pub struct ProofTreeInput {
93    /// Node ID of the root goal.
94    pub root_goal: String,
95
96    /// All nodes in the tree, keyed by their node ID.
97    pub nodes: HashMap<String, ProofNodeInput>,
98
99    /// Whether the root goal was successfully proved.
100    pub proved: bool,
101}
102
103// ─── Serialized / stored types ───────────────────────────────────────────────
104
105/// A flattened representation of one node, suitable for storage and transmission.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub struct ProofNodeRecord {
108    /// Unique identifier of this node within the proof.
109    pub node_id: String,
110
111    /// Rule applied at this node, if any.
112    pub rule_id: Option<String>,
113
114    /// Variable bindings at this node.
115    pub bindings: HashMap<String, String>,
116
117    /// IDs of direct child nodes (in original order).
118    pub children_ids: Vec<String>,
119
120    /// `true` when the node has no children (base-fact or axiom leaf).
121    pub is_leaf: bool,
122
123    /// Remote peer that resolved this node's goal, if any.
124    pub peer_id: Option<String>,
125}
126
127/// A fully serialized proof tree, ready for IPLD storage or network transmission.
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129pub struct SerializedProof {
130    /// FNV-1a hex digest of the sorted, concatenated node IDs.
131    pub proof_id: String,
132
133    /// Node ID of the root goal.
134    pub goal: String,
135
136    /// All nodes in DFS pre-order.
137    pub nodes: Vec<ProofNodeRecord>,
138
139    /// Total number of directed parent→child edges.
140    pub edge_count: usize,
141
142    /// Maximum depth of the tree (root = depth 0).
143    pub depth: u32,
144
145    /// Whether the root goal was proved.
146    pub proved: bool,
147
148    /// Total number of bindings across all nodes.
149    pub total_bindings: usize,
150
151    /// Unix millisecond timestamp when this struct was created.
152    pub serialized_at_ms: u64,
153}
154
155// ─── Stats ───────────────────────────────────────────────────────────────────
156
157/// A point-in-time snapshot of [`ProofSerializerStats`] counters.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ProofSerializerStatsSnapshot {
160    pub total_serialized: u64,
161    pub total_deserialized: u64,
162    pub total_json_encoded: u64,
163    pub total_json_decoded: u64,
164}
165
166/// Atomic counters tracking [`ProofSerializer`] activity.
167#[derive(Debug, Default)]
168pub struct ProofSerializerStats {
169    total_serialized: AtomicU64,
170    total_deserialized: AtomicU64,
171    total_json_encoded: AtomicU64,
172    total_json_decoded: AtomicU64,
173}
174
175impl ProofSerializerStats {
176    /// Returns a consistent snapshot (individual atomics read sequentially).
177    pub fn snapshot(&self) -> ProofSerializerStatsSnapshot {
178        ProofSerializerStatsSnapshot {
179            total_serialized: self.total_serialized.load(Ordering::Relaxed),
180            total_deserialized: self.total_deserialized.load(Ordering::Relaxed),
181            total_json_encoded: self.total_json_encoded.load(Ordering::Relaxed),
182            total_json_decoded: self.total_json_decoded.load(Ordering::Relaxed),
183        }
184    }
185}
186
187// ─── ProofSerializer ─────────────────────────────────────────────────────────
188
189/// Serializes and deserializes distributed proof trees.
190///
191/// The serializer is stateless apart from its [`ProofSerializerStats`] counter
192/// set; it can therefore be shared freely across threads via an `Arc`.
193#[derive(Debug, Default)]
194pub struct ProofSerializer {
195    /// Cumulative operation counters.
196    pub stats: ProofSerializerStats,
197}
198
199impl ProofSerializer {
200    /// Creates a new [`ProofSerializer`] with zeroed counters.
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    // ── public API ─────────────────────────────────────────────────────────
206
207    /// Flattens `input` into a [`SerializedProof`] using DFS pre-order traversal.
208    ///
209    /// # Errors
210    ///
211    /// - [`ProofSerError::EmptyTree`] when `nodes` is empty.
212    /// - [`ProofSerError::MissingNode`] when the root or any child ID is absent.
213    /// - [`ProofSerError::CycleDetected`] when the tree contains a cycle.
214    pub fn serialize(&self, input: &ProofTreeInput) -> Result<SerializedProof, ProofSerError> {
215        if input.nodes.is_empty() {
216            return Err(ProofSerError::EmptyTree);
217        }
218
219        // DFS pre-order traversal — collect (node_id, depth) pairs.
220        let order = self.dfs_order(input)?;
221
222        // Compute aggregate metrics.
223        let depth = order.iter().map(|(_, d)| *d).max().unwrap_or(0);
224        let edge_count: usize = order
225            .iter()
226            .map(|(id, _)| input.nodes.get(id).map(|n| n.children.len()).unwrap_or(0))
227            .sum();
228        let total_bindings: usize = order
229            .iter()
230            .map(|(id, _)| input.nodes.get(id).map(|n| n.bindings.len()).unwrap_or(0))
231            .sum();
232
233        // Build flat records.
234        let nodes: Vec<ProofNodeRecord> = order
235            .iter()
236            .map(|(id, _)| {
237                let n = &input.nodes[id];
238                ProofNodeRecord {
239                    node_id: id.clone(),
240                    rule_id: n.rule_id.clone(),
241                    bindings: n.bindings.clone(),
242                    children_ids: n.children.clone(),
243                    is_leaf: n.children.is_empty(),
244                    peer_id: n.peer_id.clone(),
245                }
246            })
247            .collect();
248
249        // Compute proof_id: FNV-1a of sorted, concatenated node IDs.
250        let mut sorted_ids: Vec<&str> = order.iter().map(|(id, _)| id.as_str()).collect();
251        sorted_ids.sort_unstable();
252        let proof_id = fnv1a_hex(sorted_ids.iter().copied());
253
254        let serialized_at_ms = current_ms();
255
256        self.stats.total_serialized.fetch_add(1, Ordering::Relaxed);
257
258        Ok(SerializedProof {
259            proof_id,
260            goal: input.root_goal.clone(),
261            nodes,
262            edge_count,
263            depth,
264            proved: input.proved,
265            total_bindings,
266            serialized_at_ms,
267        })
268    }
269
270    /// Reconstructs a [`ProofTreeInput`] from a [`SerializedProof`].
271    ///
272    /// # Errors
273    ///
274    /// Returns [`ProofSerError::EmptyTree`] when the proof contains no nodes,
275    /// or [`ProofSerError::MissingNode`] when a referenced child is absent.
276    pub fn deserialize(&self, proof: &SerializedProof) -> Result<ProofTreeInput, ProofSerError> {
277        if proof.nodes.is_empty() {
278            return Err(ProofSerError::EmptyTree);
279        }
280
281        // Build the node map from the flat records.
282        let mut nodes: HashMap<String, ProofNodeInput> = HashMap::with_capacity(proof.nodes.len());
283        for record in &proof.nodes {
284            nodes.insert(
285                record.node_id.clone(),
286                ProofNodeInput {
287                    rule_id: record.rule_id.clone(),
288                    bindings: record.bindings.clone(),
289                    children: record.children_ids.clone(),
290                    peer_id: record.peer_id.clone(),
291                },
292            );
293        }
294
295        // Verify all referenced children actually exist.
296        for record in &proof.nodes {
297            for child_id in &record.children_ids {
298                if !nodes.contains_key(child_id) {
299                    return Err(ProofSerError::MissingNode(child_id.clone()));
300                }
301            }
302        }
303
304        self.stats
305            .total_deserialized
306            .fetch_add(1, Ordering::Relaxed);
307
308        Ok(ProofTreeInput {
309            root_goal: proof.goal.clone(),
310            nodes,
311            proved: proof.proved,
312        })
313    }
314
315    /// Encodes `proof` as a compact JSON string.
316    pub fn to_json(&self, proof: &SerializedProof) -> Result<String, ProofSerError> {
317        let json =
318            serde_json::to_string(proof).map_err(|e| ProofSerError::JsonError(e.to_string()))?;
319        self.stats
320            .total_json_encoded
321            .fetch_add(1, Ordering::Relaxed);
322        Ok(json)
323    }
324
325    /// Decodes a [`SerializedProof`] from a JSON string.
326    pub fn from_json(&self, json: &str) -> Result<SerializedProof, ProofSerError> {
327        let proof: SerializedProof =
328            serde_json::from_str(json).map_err(|e| ProofSerError::JsonError(e.to_string()))?;
329        self.stats
330            .total_json_decoded
331            .fetch_add(1, Ordering::Relaxed);
332        Ok(proof)
333    }
334
335    // ── internals ──────────────────────────────────────────────────────────
336
337    /// Returns all node IDs in DFS pre-order together with their depth.
338    ///
339    /// Detects cycles by tracking the current call stack (ancestors).
340    fn dfs_order(&self, input: &ProofTreeInput) -> Result<Vec<(String, u32)>, ProofSerError> {
341        let root = &input.root_goal;
342        if !input.nodes.contains_key(root) {
343            return Err(ProofSerError::MissingNode(root.clone()));
344        }
345
346        let mut order: Vec<(String, u32)> = Vec::with_capacity(input.nodes.len());
347        // `ancestors` tracks the current DFS stack to detect back-edges (cycles).
348        let mut ancestors: HashSet<String> = HashSet::new();
349
350        Self::dfs_visit(input, root, 0, &mut order, &mut ancestors)?;
351        Ok(order)
352    }
353
354    fn dfs_visit(
355        input: &ProofTreeInput,
356        node_id: &str,
357        depth: u32,
358        order: &mut Vec<(String, u32)>,
359        ancestors: &mut HashSet<String>,
360    ) -> Result<(), ProofSerError> {
361        if ancestors.contains(node_id) {
362            return Err(ProofSerError::CycleDetected);
363        }
364
365        let node = input
366            .nodes
367            .get(node_id)
368            .ok_or_else(|| ProofSerError::MissingNode(node_id.to_string()))?;
369
370        order.push((node_id.to_string(), depth));
371        ancestors.insert(node_id.to_string());
372
373        for child_id in &node.children {
374            Self::dfs_visit(input, child_id, depth + 1, order, ancestors)?;
375        }
376
377        ancestors.remove(node_id);
378        Ok(())
379    }
380}
381
382// ─── Helpers ─────────────────────────────────────────────────────────────────
383
384/// Computes the FNV-1a 64-bit hash of all strings in `parts` and returns a
385/// lower-hex string (16 characters).
386fn fnv1a_hex<'a>(parts: impl Iterator<Item = &'a str>) -> String {
387    const OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
388    const PRIME: u64 = 1_099_511_628_211;
389
390    let mut hash: u64 = OFFSET_BASIS;
391    for part in parts {
392        for byte in part.bytes() {
393            hash ^= u64::from(byte);
394            hash = hash.wrapping_mul(PRIME);
395        }
396    }
397    format!("{:016x}", hash)
398}
399
400/// Returns the current Unix time in milliseconds, falling back to 0 on error.
401fn current_ms() -> u64 {
402    SystemTime::now()
403        .duration_since(UNIX_EPOCH)
404        .map(|d| d.as_millis() as u64)
405        .unwrap_or(0)
406}
407
408// ─── Tests ───────────────────────────────────────────────────────────────────
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    // ── helpers ────────────────────────────────────────────────────────────
415
416    fn single_node_input() -> ProofTreeInput {
417        let mut nodes = HashMap::new();
418        nodes.insert(
419            "n0".to_string(),
420            ProofNodeInput {
421                rule_id: None,
422                bindings: HashMap::new(),
423                children: vec![],
424                peer_id: None,
425            },
426        );
427        ProofTreeInput {
428            root_goal: "n0".to_string(),
429            nodes,
430            proved: true,
431        }
432    }
433
434    /// Builds:
435    /// ```
436    ///        n0 (root)
437    ///       /       \
438    ///     n1         n2
439    ///    /  \
440    ///  n3    n4
441    /// ```
442    fn multi_level_input() -> ProofTreeInput {
443        let mut nodes = HashMap::new();
444        nodes.insert(
445            "n0".to_string(),
446            ProofNodeInput {
447                rule_id: Some("rule_A".to_string()),
448                bindings: [("X".to_string(), "alice".to_string())].into(),
449                children: vec!["n1".to_string(), "n2".to_string()],
450                peer_id: None,
451            },
452        );
453        nodes.insert(
454            "n1".to_string(),
455            ProofNodeInput {
456                rule_id: Some("rule_B".to_string()),
457                bindings: [("Y".to_string(), "bob".to_string())].into(),
458                children: vec!["n3".to_string(), "n4".to_string()],
459                peer_id: Some("peer-1".to_string()),
460            },
461        );
462        nodes.insert(
463            "n2".to_string(),
464            ProofNodeInput {
465                rule_id: None,
466                bindings: HashMap::new(),
467                children: vec![],
468                peer_id: None,
469            },
470        );
471        nodes.insert(
472            "n3".to_string(),
473            ProofNodeInput {
474                rule_id: None,
475                bindings: [("Z".to_string(), "carol".to_string())].into(),
476                children: vec![],
477                peer_id: Some("peer-2".to_string()),
478            },
479        );
480        nodes.insert(
481            "n4".to_string(),
482            ProofNodeInput {
483                rule_id: None,
484                bindings: HashMap::new(),
485                children: vec![],
486                peer_id: None,
487            },
488        );
489        ProofTreeInput {
490            root_goal: "n0".to_string(),
491            nodes,
492            proved: true,
493        }
494    }
495
496    // ── tests ──────────────────────────────────────────────────────────────
497
498    #[test]
499    fn test_serialize_single_node() {
500        let ser = ProofSerializer::new();
501        let input = single_node_input();
502        let proof = ser.serialize(&input).expect("serialize should succeed");
503
504        assert_eq!(proof.nodes.len(), 1);
505        assert_eq!(proof.nodes[0].node_id, "n0");
506        assert!(proof.nodes[0].is_leaf);
507        assert_eq!(proof.edge_count, 0);
508        assert_eq!(proof.depth, 0);
509        assert!(proof.proved);
510        assert_eq!(proof.total_bindings, 0);
511        assert!(!proof.proof_id.is_empty());
512    }
513
514    #[test]
515    fn test_serialize_multi_level() {
516        let ser = ProofSerializer::new();
517        let input = multi_level_input();
518        let proof = ser.serialize(&input).expect("serialize multi-level");
519
520        // 5 nodes total
521        assert_eq!(proof.nodes.len(), 5);
522
523        // depth: n0=0, n1=1, n3=2, n4=2 → max depth = 2
524        assert_eq!(proof.depth, 2);
525
526        // edges: n0→n1, n0→n2, n1→n3, n1→n4 = 4
527        assert_eq!(proof.edge_count, 4);
528
529        // DFS pre-order from n0: n0, n1, n3, n4, n2
530        assert_eq!(proof.nodes[0].node_id, "n0");
531        assert_eq!(proof.nodes[1].node_id, "n1");
532        assert_eq!(proof.nodes[2].node_id, "n3");
533        assert_eq!(proof.nodes[3].node_id, "n4");
534        assert_eq!(proof.nodes[4].node_id, "n2");
535    }
536
537    #[test]
538    fn test_proof_id_is_deterministic() {
539        let ser = ProofSerializer::new();
540        let input = multi_level_input();
541
542        let proof1 = ser.serialize(&input).expect("first serialize");
543        let proof2 = ser.serialize(&input).expect("second serialize");
544
545        // proof_id must not depend on HashMap iteration order
546        assert_eq!(proof1.proof_id, proof2.proof_id);
547    }
548
549    #[test]
550    fn test_proof_id_differs_for_different_trees() {
551        let ser = ProofSerializer::new();
552        let input1 = single_node_input();
553        let input2 = multi_level_input();
554
555        let p1 = ser.serialize(&input1).expect("test: should succeed");
556        let p2 = ser.serialize(&input2).expect("test: should succeed");
557
558        assert_ne!(p1.proof_id, p2.proof_id);
559    }
560
561    #[test]
562    fn test_deserialize_round_trip() {
563        let ser = ProofSerializer::new();
564        let input = multi_level_input();
565
566        let proof = ser.serialize(&input).expect("serialize");
567        let reconstructed = ser.deserialize(&proof).expect("deserialize");
568
569        // The node map should be equal (order-agnostic).
570        assert_eq!(reconstructed.root_goal, input.root_goal);
571        assert_eq!(reconstructed.proved, input.proved);
572        assert_eq!(reconstructed.nodes.len(), input.nodes.len());
573
574        for (id, orig_node) in &input.nodes {
575            let rec_node = reconstructed.nodes.get(id).expect("node should exist");
576            assert_eq!(rec_node.rule_id, orig_node.rule_id);
577            assert_eq!(rec_node.bindings, orig_node.bindings);
578            assert_eq!(rec_node.children, orig_node.children);
579            assert_eq!(rec_node.peer_id, orig_node.peer_id);
580        }
581    }
582
583    #[test]
584    fn test_json_round_trip() {
585        let ser = ProofSerializer::new();
586        let input = multi_level_input();
587        let proof = ser.serialize(&input).expect("serialize");
588
589        let json = ser.to_json(&proof).expect("to_json");
590        let decoded = ser.from_json(&json).expect("from_json");
591
592        assert_eq!(proof, decoded);
593    }
594
595    #[test]
596    fn test_missing_root_node_returns_error() {
597        let ser = ProofSerializer::new();
598        let mut input = single_node_input();
599        input.root_goal = "nonexistent".to_string();
600
601        let result = ser.serialize(&input);
602        assert!(matches!(result, Err(ProofSerError::MissingNode(_))));
603    }
604
605    #[test]
606    fn test_empty_tree_returns_error() {
607        let ser = ProofSerializer::new();
608        let input = ProofTreeInput {
609            root_goal: "n0".to_string(),
610            nodes: HashMap::new(),
611            proved: false,
612        };
613        let result = ser.serialize(&input);
614        assert!(matches!(result, Err(ProofSerError::EmptyTree)));
615    }
616
617    #[test]
618    fn test_proved_flag_preserved_false() {
619        let ser = ProofSerializer::new();
620        let mut input = single_node_input();
621        input.proved = false;
622
623        let proof = ser.serialize(&input).expect("serialize");
624        assert!(!proof.proved);
625        let reconstructed = ser.deserialize(&proof).expect("deserialize");
626        assert!(!reconstructed.proved);
627    }
628
629    #[test]
630    fn test_depth_computed_correctly() {
631        let ser = ProofSerializer::new();
632        // Chain: n0 → n1 → n2 → n3 (depth = 3)
633        let mut nodes = HashMap::new();
634        for i in 0..4_usize {
635            nodes.insert(
636                format!("n{}", i),
637                ProofNodeInput {
638                    rule_id: None,
639                    bindings: HashMap::new(),
640                    children: if i < 3 {
641                        vec![format!("n{}", i + 1)]
642                    } else {
643                        vec![]
644                    },
645                    peer_id: None,
646                },
647            );
648        }
649        let input = ProofTreeInput {
650            root_goal: "n0".to_string(),
651            nodes,
652            proved: true,
653        };
654        let proof = ser.serialize(&input).expect("serialize chain");
655        assert_eq!(proof.depth, 3);
656        assert_eq!(proof.edge_count, 3);
657    }
658
659    #[test]
660    fn test_edge_count_correct() {
661        let ser = ProofSerializer::new();
662        let input = multi_level_input();
663        let proof = ser.serialize(&input).expect("serialize");
664        // Edges: n0→n1, n0→n2, n1→n3, n1→n4 = 4
665        assert_eq!(proof.edge_count, 4);
666    }
667
668    #[test]
669    fn test_total_bindings_correct() {
670        let ser = ProofSerializer::new();
671        let input = multi_level_input();
672        let proof = ser.serialize(&input).expect("serialize");
673        // n0: 1, n1: 1, n2: 0, n3: 1, n4: 0 → 3
674        assert_eq!(proof.total_bindings, 3);
675    }
676
677    #[test]
678    fn test_cycle_detection() {
679        let ser = ProofSerializer::new();
680        // n0 → n1 → n0  (cycle)
681        let mut nodes = HashMap::new();
682        nodes.insert(
683            "n0".to_string(),
684            ProofNodeInput {
685                rule_id: None,
686                bindings: HashMap::new(),
687                children: vec!["n1".to_string()],
688                peer_id: None,
689            },
690        );
691        nodes.insert(
692            "n1".to_string(),
693            ProofNodeInput {
694                rule_id: None,
695                bindings: HashMap::new(),
696                children: vec!["n0".to_string()],
697                peer_id: None,
698            },
699        );
700        let input = ProofTreeInput {
701            root_goal: "n0".to_string(),
702            nodes,
703            proved: false,
704        };
705        let result = ser.serialize(&input);
706        assert!(matches!(result, Err(ProofSerError::CycleDetected)));
707    }
708
709    #[test]
710    fn test_stats_accumulation() {
711        let ser = ProofSerializer::new();
712        let input = single_node_input();
713
714        // Initial state
715        let snap0 = ser.stats.snapshot();
716        assert_eq!(snap0.total_serialized, 0);
717        assert_eq!(snap0.total_deserialized, 0);
718        assert_eq!(snap0.total_json_encoded, 0);
719        assert_eq!(snap0.total_json_decoded, 0);
720
721        let proof = ser.serialize(&input).expect("test: should succeed");
722        ser.serialize(&input).expect("test: should succeed");
723        let snap1 = ser.stats.snapshot();
724        assert_eq!(snap1.total_serialized, 2);
725
726        ser.deserialize(&proof).expect("test: should succeed");
727        let snap2 = ser.stats.snapshot();
728        assert_eq!(snap2.total_deserialized, 1);
729
730        let json = ser.to_json(&proof).expect("test: should succeed");
731        let snap3 = ser.stats.snapshot();
732        assert_eq!(snap3.total_json_encoded, 1);
733
734        ser.from_json(&json).expect("test: should succeed");
735        let snap4 = ser.stats.snapshot();
736        assert_eq!(snap4.total_json_decoded, 1);
737    }
738
739    #[test]
740    fn test_json_invalid_input_returns_error() {
741        let ser = ProofSerializer::new();
742        let result = ser.from_json("{ not valid json }");
743        assert!(matches!(result, Err(ProofSerError::JsonError(_))));
744    }
745
746    #[test]
747    fn test_is_leaf_flag_correct() {
748        let ser = ProofSerializer::new();
749        let input = multi_level_input();
750        let proof = ser.serialize(&input).expect("serialize");
751
752        for record in &proof.nodes {
753            let expected_leaf = record.children_ids.is_empty();
754            assert_eq!(
755                record.is_leaf, expected_leaf,
756                "is_leaf mismatch for node {}",
757                record.node_id
758            );
759        }
760    }
761
762    #[test]
763    fn test_peer_id_preserved() {
764        let ser = ProofSerializer::new();
765        let input = multi_level_input();
766        let proof = ser.serialize(&input).expect("serialize");
767
768        // n1 should carry peer-1
769        let n1 = proof
770            .nodes
771            .iter()
772            .find(|r| r.node_id == "n1")
773            .expect("n1 must be present");
774        assert_eq!(n1.peer_id.as_deref(), Some("peer-1"));
775    }
776}