Skip to main content

converge_core/
integrity.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Integrity primitives for Converge.
5//!
6//! This module provides cryptographic and causal ordering primitives that can
7//! be used alongside the core Context and Fact types to add integrity features:
8//!
9//! - **Lamport Clock**: Provides causal ordering of events without wall clocks
10//! - **Content Hash**: SHA-256 hash of content for tamper detection
11//! - **Merkle Root**: Cryptographic commitment to a set of facts for audit verification
12//!
13//! # Example
14//!
15//! ```
16//! use converge_core::integrity::{LamportClock, ContentHash, MerkleRoot};
17//!
18//! // Lamport clock for causal ordering
19//! let mut clock = LamportClock::new();
20//! let t1 = clock.tick(); // Event 1
21//! let t2 = clock.tick(); // Event 2 (causally after Event 1)
22//!
23//! // Content hash for integrity
24//! let hash = ContentHash::compute("important data");
25//! assert!(hash.verify("important data"));
26//!
27//! // Merkle root for audit trail
28//! let hashes = vec![
29//!     ContentHash::compute("fact 1"),
30//!     ContentHash::compute("fact 2"),
31//! ];
32//! let root = MerkleRoot::compute(&hashes);
33//! ```
34
35use serde::{Deserialize, Serialize};
36
37use crate::context::{ContextFact, ContextKey, ContextState};
38
39// ============================================================================
40// Lamport Clock
41// ============================================================================
42
43/// A Lamport logical clock for causal ordering of events.
44///
45/// Lamport clocks provide a partial ordering of events in a distributed system
46/// without requiring synchronized wall clocks. The key property is:
47/// if event A happened-before event B, then `clock(A) < clock(B)`.
48///
49/// # Example
50///
51/// ```
52/// use converge_core::integrity::LamportClock;
53///
54/// let mut clock = LamportClock::new();
55/// assert_eq!(clock.time(), 0);
56///
57/// clock.tick();
58/// assert_eq!(clock.time(), 1);
59///
60/// // When receiving a message with clock=5, update to max+1
61/// clock.update(5);
62/// assert_eq!(clock.time(), 6);
63/// ```
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
65pub struct LamportClock {
66    time: u64,
67}
68
69impl LamportClock {
70    /// Creates a new Lamport clock initialized to 0.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self { time: 0 }
74    }
75
76    /// Returns the current logical time.
77    #[must_use]
78    pub const fn time(&self) -> u64 {
79        self.time
80    }
81
82    /// Increments the clock and returns the new time.
83    /// Called before any local event (e.g., creating a fact).
84    pub fn tick(&mut self) -> u64 {
85        self.time += 1;
86        self.time
87    }
88
89    /// Updates the clock based on a received timestamp.
90    /// Sets clock to `max(local, received) + 1`.
91    /// Called when receiving facts from another source.
92    pub fn update(&mut self, received: u64) -> u64 {
93        self.time = self.time.max(received) + 1;
94        self.time
95    }
96}
97
98// ============================================================================
99// Content Hash
100// ============================================================================
101
102/// Error type for ContentHash operations.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum ContentHashError {
105    /// Invalid hexadecimal character.
106    InvalidHex,
107    /// Invalid string length.
108    InvalidLength,
109}
110
111impl std::fmt::Display for ContentHashError {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            Self::InvalidHex => write!(f, "invalid hexadecimal character"),
115            Self::InvalidLength => write!(f, "invalid string length (expected 64 hex chars)"),
116        }
117    }
118}
119
120impl std::error::Error for ContentHashError {}
121
122/// A SHA-256 hash of content for integrity verification.
123///
124/// Content hashes enable:
125/// - Tamper detection: any change to content changes the hash
126/// - Efficient comparison: compare 32 bytes instead of full content
127/// - Merkle tree construction: hashes combine into a tree
128#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
129pub struct ContentHash(pub [u8; 32]);
130
131impl ContentHash {
132    /// Computes a SHA-256 hash of the given content.
133    #[must_use]
134    pub fn compute(content: &str) -> Self {
135        use sha2::{Digest, Sha256};
136        let mut hasher = Sha256::new();
137        hasher.update(content.as_bytes());
138        Self(hasher.finalize().into())
139    }
140
141    /// Computes the hash of a Fact (combines key, id, and content).
142    #[must_use]
143    pub fn compute_fact(fact: &ContextFact) -> Self {
144        let payload = fact
145            .to_wire()
146            .map(|wire| wire.payload.payload.to_string())
147            .unwrap_or_else(|error| error.to_string());
148        let combined = format!("{:?}|{}|{}", fact.key(), fact.id(), payload);
149        Self::compute(&combined)
150    }
151
152    /// Combines two hashes (for Merkle tree internal nodes).
153    #[must_use]
154    pub fn combine(left: &Self, right: &Self) -> Self {
155        use sha2::{Digest, Sha256};
156        let mut hasher = Sha256::new();
157        hasher.update(left.0);
158        hasher.update(right.0);
159        Self(hasher.finalize().into())
160    }
161
162    /// Verifies that this hash matches the given content.
163    #[must_use]
164    pub fn verify(&self, content: &str) -> bool {
165        *self == Self::compute(content)
166    }
167
168    /// Returns the hash as a hex string.
169    #[must_use]
170    pub fn to_hex(&self) -> String {
171        self.0.iter().map(|b| format!("{b:02x}")).collect()
172    }
173
174    /// Creates a `ContentHash` from a hex string.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the string is not valid hex or wrong length.
179    pub fn from_hex(s: &str) -> Result<Self, ContentHashError> {
180        if s.len() != 64 {
181            return Err(ContentHashError::InvalidLength);
182        }
183        let mut result = [0u8; 32];
184        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
185            let high = Self::hex_char_to_nibble(chunk[0])?;
186            let low = Self::hex_char_to_nibble(chunk[1])?;
187            result[i] = (high << 4) | low;
188        }
189        Ok(Self(result))
190    }
191
192    fn hex_char_to_nibble(c: u8) -> Result<u8, ContentHashError> {
193        match c {
194            b'0'..=b'9' => Ok(c - b'0'),
195            b'a'..=b'f' => Ok(c - b'a' + 10),
196            b'A'..=b'F' => Ok(c - b'A' + 10),
197            _ => Err(ContentHashError::InvalidHex),
198        }
199    }
200}
201
202impl std::fmt::Display for ContentHash {
203    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204        write!(f, "{}", &self.to_hex()[..16]) // Short form for display
205    }
206}
207
208// ============================================================================
209// Merkle Tree
210// ============================================================================
211
212/// A Merkle tree root hash representing the integrity of a set of facts.
213///
214/// The Merkle root changes if any fact is added, modified, or reordered.
215/// This enables:
216/// - Tamper detection: compare roots to verify integrity
217/// - Efficient sync: different roots mean different states
218/// - Audit proofs: prove a fact exists without revealing all facts
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220pub struct MerkleRoot(pub ContentHash);
221
222impl MerkleRoot {
223    /// Computes the Merkle root from a list of content hashes.
224    ///
225    /// Uses a standard binary Merkle tree construction.
226    /// Empty list returns a hash of empty string.
227    /// Single element is combined with itself (Bitcoin-style).
228    #[must_use]
229    pub fn compute(hashes: &[ContentHash]) -> Self {
230        if hashes.is_empty() {
231            return Self(ContentHash::compute(""));
232        }
233
234        let mut current_level: Vec<ContentHash> = hashes.to_vec();
235
236        // Keep combining until we reach a single root
237        // For a single element, we still iterate once to combine it with itself
238        loop {
239            if current_level.len() == 1 {
240                // If we started with 1 element, combine with itself
241                // If we reduced to 1 element through combinations, we're done
242                if hashes.len() == 1 {
243                    return Self(ContentHash::combine(&current_level[0], &current_level[0]));
244                }
245                return Self(current_level.into_iter().next().unwrap());
246            }
247
248            let mut next_level = Vec::new();
249
250            for chunk in current_level.chunks(2) {
251                let combined = if chunk.len() == 2 {
252                    ContentHash::combine(&chunk[0], &chunk[1])
253                } else {
254                    // Odd number: duplicate the last hash
255                    ContentHash::combine(&chunk[0], &chunk[0])
256                };
257                next_level.push(combined);
258            }
259
260            current_level = next_level;
261        }
262    }
263
264    /// Computes the Merkle root from a Context's facts.
265    ///
266    /// Facts are hashed in deterministic order (by key, then by position).
267    #[must_use]
268    pub fn from_context(ctx: &ContextState) -> Self {
269        let mut all_hashes: Vec<ContentHash> = Vec::new();
270        let mut keys: Vec<_> = ctx.all_keys();
271        keys.sort();
272
273        for key in keys {
274            for fact in ctx.get(key) {
275                all_hashes.push(ContentHash::compute_fact(fact));
276            }
277        }
278
279        Self::compute(&all_hashes)
280    }
281
282    /// Returns the root hash as a hex string.
283    #[must_use]
284    pub fn to_hex(&self) -> String {
285        self.0.to_hex()
286    }
287}
288
289// ============================================================================
290// Tracked Context
291// ============================================================================
292
293/// A wrapper around Context that tracks integrity metadata.
294///
295/// This provides optional integrity tracking without modifying the core types.
296#[derive(Debug, Clone, Serialize)]
297pub struct TrackedContext {
298    /// The underlying context.
299    pub context: ContextState,
300    /// Lamport clock for causal ordering.
301    pub clock: LamportClock,
302    /// Cached Merkle root (invalidated on changes).
303    merkle_root: Option<MerkleRoot>,
304    /// Hash of each fact (by key and index).
305    fact_hashes: Vec<(ContextKey, String, ContentHash)>,
306}
307
308impl TrackedContext {
309    /// Creates a new tracked context wrapping an existing context.
310    #[must_use]
311    pub fn new(context: ContextState) -> Self {
312        let mut tracked = Self {
313            context,
314            clock: LamportClock::new(),
315            merkle_root: None,
316            fact_hashes: Vec::new(),
317        };
318        tracked.recompute_hashes();
319        tracked
320    }
321
322    /// Creates an empty tracked context.
323    #[must_use]
324    pub fn empty() -> Self {
325        Self::new(ContextState::new())
326    }
327
328    /// Returns the current Lamport clock time.
329    #[must_use]
330    pub fn clock_time(&self) -> u64 {
331        self.clock.time()
332    }
333
334    /// Ticks the clock and returns the new time.
335    pub fn tick(&mut self) -> u64 {
336        self.clock.tick()
337    }
338
339    /// Computes and returns the Merkle root.
340    #[must_use]
341    pub fn merkle_root(&mut self) -> &MerkleRoot {
342        if self.merkle_root.is_none() {
343            self.merkle_root = Some(MerkleRoot::from_context(&self.context));
344        }
345        self.merkle_root.as_ref().unwrap()
346    }
347
348    /// Verifies the integrity of all facts.
349    ///
350    /// Returns `true` if all recorded hashes match current fact content.
351    #[must_use]
352    pub fn verify_integrity(&self) -> bool {
353        for (key, id, expected_hash) in &self.fact_hashes {
354            if let Some(fact) = self
355                .context
356                .get(*key)
357                .iter()
358                .find(|f| f.id().as_str() == id)
359            {
360                if ContentHash::compute_fact(fact) != *expected_hash {
361                    return false;
362                }
363            } else {
364                return false; // Fact missing
365            }
366        }
367        true
368    }
369
370    /// Recomputes all fact hashes.
371    fn recompute_hashes(&mut self) {
372        self.fact_hashes.clear();
373        for key in self.context.all_keys() {
374            for fact in self.context.get(key) {
375                let hash = ContentHash::compute_fact(fact);
376                self.fact_hashes.push((key, fact.id().to_string(), hash));
377            }
378        }
379        self.merkle_root = None; // Invalidate cached root
380    }
381
382    /// Adds a fact to the underlying context and updates tracking.
383    ///
384    /// # Errors
385    ///
386    /// Returns an error if the fact conflicts with an existing fact.
387    pub(crate) fn add_fact(
388        &mut self,
389        fact: ContextFact,
390    ) -> Result<bool, crate::error::ConvergeError> {
391        let key = fact.key();
392        let id = fact.id().to_string();
393        let hash = ContentHash::compute_fact(&fact);
394
395        let changed = self.context.add_fact(fact)?;
396
397        if changed {
398            self.fact_hashes.push((key, id, hash));
399            self.merkle_root = None; // Invalidate cached root
400            self.clock.tick();
401        }
402
403        Ok(changed)
404    }
405
406    /// Extracts an integrity proof from the current state.
407    pub fn extract_proof(&mut self) -> IntegrityProof {
408        let merkle_root = self.merkle_root().clone();
409        IntegrityProof {
410            merkle_root,
411            clock_time: self.clock_time(),
412            fact_count: self.fact_hashes.len(),
413        }
414    }
415}
416
417impl Default for TrackedContext {
418    fn default() -> Self {
419        Self::empty()
420    }
421}
422
423// ============================================================================
424// Integrity Proof
425// ============================================================================
426
427/// Cryptographic integrity proof for a converged context.
428///
429/// Produced by the engine at the end of convergence. Provides:
430/// - Merkle root: tamper-evident commitment to all facts
431/// - Clock time: causal ordering count (number of fact-addition events)
432/// - Fact count: total facts in the context
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
434pub struct IntegrityProof {
435    /// Merkle root over all facts in deterministic order.
436    pub merkle_root: MerkleRoot,
437    /// Final Lamport clock time (number of tracked events).
438    pub clock_time: u64,
439    /// Number of facts in the context.
440    pub fact_count: usize,
441}
442
443// ============================================================================
444// Tests
445// ============================================================================
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    // =========================================================================
452    // Lamport Clock Tests
453    // =========================================================================
454
455    #[test]
456    fn lamport_clock_starts_at_zero() {
457        let clock = LamportClock::new();
458        assert_eq!(clock.time(), 0);
459    }
460
461    #[test]
462    fn lamport_clock_tick_increments() {
463        let mut clock = LamportClock::new();
464        assert_eq!(clock.tick(), 1);
465        assert_eq!(clock.tick(), 2);
466        assert_eq!(clock.tick(), 3);
467    }
468
469    #[test]
470    fn lamport_clock_update_takes_max_plus_one() {
471        let mut clock = LamportClock::new();
472        clock.tick(); // 1
473        clock.tick(); // 2
474
475        // Received message with clock=5, should become 6
476        assert_eq!(clock.update(5), 6);
477
478        // Received message with clock=3, should become 7 (max(6,3)+1)
479        assert_eq!(clock.update(3), 7);
480    }
481
482    // =========================================================================
483    // Content Hash Tests
484    // =========================================================================
485
486    #[test]
487
488    fn content_hash_is_deterministic() {
489        let h1 = ContentHash::compute("hello world");
490        let h2 = ContentHash::compute("hello world");
491        assert_eq!(h1, h2);
492    }
493
494    #[test]
495
496    fn content_hash_changes_with_content() {
497        let h1 = ContentHash::compute("hello");
498        let h2 = ContentHash::compute("world");
499        assert_ne!(h1, h2);
500    }
501
502    #[test]
503
504    fn content_hash_verify_works() {
505        let hash = ContentHash::compute("test");
506        assert!(hash.verify("test"));
507        assert!(!hash.verify("modified"));
508    }
509
510    #[test]
511
512    fn content_hash_hex_roundtrip() {
513        let original = ContentHash::compute("test content");
514        let hex = original.to_hex();
515        let restored = ContentHash::from_hex(&hex).unwrap();
516        assert_eq!(original, restored);
517    }
518
519    // =========================================================================
520    // Merkle Tree Tests
521    // =========================================================================
522
523    #[test]
524
525    fn merkle_root_empty_list() {
526        let root = MerkleRoot::compute(&[]);
527        let empty_hash = ContentHash::compute("");
528        assert_eq!(root.0, empty_hash);
529    }
530
531    #[test]
532
533    fn merkle_root_single_element() {
534        let h = ContentHash::compute("only element");
535        let root = MerkleRoot::compute(std::slice::from_ref(&h));
536        let expected = ContentHash::combine(&h, &h);
537        assert_eq!(root.0, expected);
538    }
539
540    #[test]
541
542    fn merkle_root_two_elements() {
543        let h1 = ContentHash::compute("first");
544        let h2 = ContentHash::compute("second");
545        let root = MerkleRoot::compute(&[h1.clone(), h2.clone()]);
546        let expected = ContentHash::combine(&h1, &h2);
547        assert_eq!(root.0, expected);
548    }
549
550    #[test]
551
552    fn merkle_root_is_deterministic() {
553        let hashes = vec![
554            ContentHash::compute("a"),
555            ContentHash::compute("b"),
556            ContentHash::compute("c"),
557        ];
558        let root1 = MerkleRoot::compute(&hashes);
559        let root2 = MerkleRoot::compute(&hashes);
560        assert_eq!(root1, root2);
561    }
562
563    #[test]
564
565    fn merkle_root_changes_with_different_content() {
566        let hashes1 = vec![ContentHash::compute("a"), ContentHash::compute("b")];
567        let hashes2 = vec![ContentHash::compute("a"), ContentHash::compute("c")];
568        let root1 = MerkleRoot::compute(&hashes1);
569        let root2 = MerkleRoot::compute(&hashes2);
570        assert_ne!(root1, root2);
571    }
572
573    // =========================================================================
574    // Tracked Context Tests
575    // =========================================================================
576
577    #[test]
578    fn tracked_context_starts_empty() {
579        let tracked = TrackedContext::empty();
580        assert_eq!(tracked.clock_time(), 0);
581        assert!(!tracked.context.has(ContextKey::Seeds));
582    }
583
584    #[test]
585    fn tracked_context_ticks_on_add() {
586        let mut tracked = TrackedContext::empty();
587        assert_eq!(tracked.clock_time(), 0);
588
589        tracked
590            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "seed"))
591            .unwrap();
592        assert_eq!(tracked.clock_time(), 1);
593
594        tracked
595            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s2", "seed2"))
596            .unwrap();
597        assert_eq!(tracked.clock_time(), 2);
598    }
599
600    #[test]
601    fn tracked_context_computes_merkle_root() {
602        let mut tracked = TrackedContext::empty();
603        tracked
604            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "first"))
605            .unwrap();
606        tracked
607            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s2", "second"))
608            .unwrap();
609
610        let root1 = tracked.merkle_root().clone();
611
612        // Adding another fact changes the root
613        tracked
614            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s3", "third"))
615            .unwrap();
616        let root2 = tracked.merkle_root().clone();
617
618        assert_ne!(root1, root2);
619    }
620
621    #[test]
622    fn tracked_context_verifies_integrity() {
623        let mut tracked = TrackedContext::empty();
624        tracked
625            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "test"))
626            .unwrap();
627
628        assert!(tracked.verify_integrity());
629    }
630
631    #[test]
632    fn integrity_proof_serializes() {
633        let mut tracked = TrackedContext::empty();
634        tracked
635            .add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "test"))
636            .unwrap();
637        let proof = tracked.extract_proof();
638        assert_eq!(proof.clock_time, 1);
639        assert_eq!(proof.fact_count, 1);
640
641        let json = serde_json::to_string(&proof).unwrap();
642        let deser: IntegrityProof = serde_json::from_str(&json).unwrap();
643        assert_eq!(proof, deser);
644    }
645
646    #[test]
647    fn integrity_proof_changes_with_different_facts() {
648        let mut t1 = TrackedContext::empty();
649        t1.add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "alpha"))
650            .unwrap();
651        let proof1 = t1.extract_proof();
652
653        let mut t2 = TrackedContext::empty();
654        t2.add_fact(crate::context::new_fact(ContextKey::Seeds, "s1", "beta"))
655            .unwrap();
656        let proof2 = t2.extract_proof();
657
658        assert_ne!(proof1.merkle_root, proof2.merkle_root);
659        assert_eq!(proof1.clock_time, proof2.clock_time);
660        assert_eq!(proof1.fact_count, proof2.fact_count);
661    }
662}