Skip to main content

confium_transparency/
witness.rs

1//! Witness gossip protocol for transparency log monitoring.
2//!
3//! Third-party witnesses track tree heads (STHs) and gossip them
4//! between each other to detect split-view attacks. A malicious log
5//! that presents different views to different clients will be caught
6//! when witnesses compare notes.
7//!
8//! ## Protocol
9//!
10//! 1. Witness fetches the latest tree head from the log
11//! 2. Witness verifies consistency with its previous head
12//! 3. Witness gossips the new head to peer witnesses
13//! 4. Peer witnesses verify and store the head
14
15use chrono::{DateTime, Utc};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19use crate::merkle::{Hash, MerkleTree};
20
21/// A signed tree head (STH) — the log's commitment to its state at
22/// a point in time.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
24pub struct TreeHead {
25    /// Number of entries in the tree.
26    pub tree_size: u64,
27    /// Root hash.
28    pub root_hash: Hash,
29    /// Timestamp when this head was produced.
30    pub timestamp: DateTime<Utc>,
31}
32
33/// A witness that monitors a transparency log.
34///
35/// Stores all known tree heads and verifies consistency between
36/// successive heads. Heads that fail consistency are rejected.
37#[derive(Debug, Default)]
38pub struct Witness {
39    /// Known heads indexed by tree_size.
40    heads: HashMap<u64, TreeHead>,
41    /// Witness identity.
42    pub witness_id: String,
43}
44
45/// Errors during witness operations.
46#[derive(Debug, thiserror::Error)]
47pub enum WitnessError {
48    /// Consistency proof verification failed.
49    #[error("consistency proof failed: expected {expected:?}, got {actual:?}")]
50    ConsistencyFailed {
51        /// Expected root hash.
52        expected: Hash,
53        /// Actual root hash.
54        actual: Hash,
55    },
56    /// Old head not found for consistency check.
57    #[error("old tree head (size {0}) not known")]
58    OldHeadUnknown(u64),
59}
60
61impl Witness {
62    /// Create a new witness with the given identity.
63    pub fn new(witness_id: &str) -> Self {
64        Self {
65            heads: HashMap::new(),
66            witness_id: witness_id.into(),
67        }
68    }
69
70    /// Receive a new tree head. If the witness already knows a head
71    /// at a smaller tree size, `consistency_proof` must be provided
72    /// to verify the transition.
73    ///
74    /// `tree` is the current tree state (used to brute-force verify
75    /// consistency via root recomputation).
76    pub fn receive_head(&mut self, head: TreeHead, tree: &MerkleTree) -> Result<(), WitnessError> {
77        if let Some(old) = self.latest_head() {
78            if head.tree_size > old.tree_size {
79                tree.verify_consistency(
80                    old.root_hash,
81                    head.root_hash,
82                    old.tree_size as usize,
83                    head.tree_size as usize,
84                    &[],
85                )
86                .map_err(|_| WitnessError::ConsistencyFailed {
87                    expected: head.root_hash,
88                    actual: tree.root(),
89                })?;
90            }
91        }
92        self.heads.insert(head.tree_size, head);
93        Ok(())
94    }
95
96    /// Get the latest known head (largest tree_size).
97    pub fn latest_head(&self) -> Option<&TreeHead> {
98        self.heads.values().max_by_key(|h| h.tree_size)
99    }
100
101    /// Get all known heads sorted by tree_size.
102    pub fn known_heads(&self) -> Vec<&TreeHead> {
103        let mut heads: Vec<&TreeHead> = self.heads.values().collect();
104        heads.sort_by_key(|h| h.tree_size);
105        heads
106    }
107
108    /// Get a specific head by tree_size.
109    pub fn head_at(&self, tree_size: u64) -> Option<&TreeHead> {
110        self.heads.get(&tree_size)
111    }
112
113    /// Number of known heads.
114    pub fn head_count(&self) -> usize {
115        self.heads.len()
116    }
117
118    /// Gossip a head to another witness. The peer verifies and
119    /// stores the head.
120    pub fn gossip_to(
121        &self,
122        peer: &mut Witness,
123        tree_size: u64,
124        tree: &MerkleTree,
125    ) -> Result<(), WitnessError> {
126        let head = self
127            .heads
128            .get(&tree_size)
129            .ok_or(WitnessError::OldHeadUnknown(tree_size))?;
130        peer.receive_head(head.clone(), tree)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::entry::{ArtifactType, MerkleEntry};
138
139    fn build_tree(n: u64) -> MerkleTree {
140        let mut tree = MerkleTree::new();
141        for i in 0..n {
142            tree.append(MerkleEntry::new(
143                i,
144                ArtifactType::ThresholdSignature,
145                [i as u8; 32],
146            ));
147        }
148        tree
149    }
150
151    #[test]
152    fn witness_starts_empty() {
153        let w = Witness::new("w1");
154        assert_eq!(w.head_count(), 0);
155        assert!(w.latest_head().is_none());
156    }
157
158    #[test]
159    fn receive_first_head() {
160        let tree = build_tree(5);
161        let mut w = Witness::new("w1");
162        let head = TreeHead {
163            tree_size: 5,
164            root_hash: tree.root(),
165            timestamp: Utc::now(),
166        };
167        w.receive_head(head, &tree).unwrap();
168        assert_eq!(w.head_count(), 1);
169        assert_eq!(w.latest_head().unwrap().tree_size, 5);
170    }
171
172    #[test]
173    fn receive_consistent_head() {
174        let mut tree = build_tree(5);
175        let mut w = Witness::new("w1");
176
177        let head1 = TreeHead {
178            tree_size: 5,
179            root_hash: tree.root(),
180            timestamp: Utc::now(),
181        };
182        w.receive_head(head1, &tree).unwrap();
183
184        tree.append(MerkleEntry::new(
185            5,
186            ArtifactType::ThresholdSignature,
187            [5; 32],
188        ));
189        let head2 = TreeHead {
190            tree_size: 6,
191            root_hash: tree.root(),
192            timestamp: Utc::now(),
193        };
194        w.receive_head(head2, &tree).unwrap();
195        assert_eq!(w.head_count(), 2);
196        assert_eq!(w.latest_head().unwrap().tree_size, 6);
197    }
198
199    #[test]
200    fn reject_inconsistent_head() {
201        let tree = build_tree(5);
202        let mut w = Witness::new("w1");
203        let head1 = TreeHead {
204            tree_size: 5,
205            root_hash: tree.root(),
206            timestamp: Utc::now(),
207        };
208        w.receive_head(head1, &tree).unwrap();
209
210        let fake_root = [0xFF; 32];
211        let head2 = TreeHead {
212            tree_size: 10,
213            root_hash: fake_root,
214            timestamp: Utc::now(),
215        };
216        let result = w.receive_head(head2, &tree);
217        assert!(result.is_err());
218    }
219
220    #[test]
221    fn known_heads_sorted_by_size() {
222        let mut tree = build_tree(1);
223        let mut w = Witness::new("w1");
224
225        let head1 = TreeHead {
226            tree_size: 1,
227            root_hash: tree.root(),
228            timestamp: Utc::now(),
229        };
230        w.receive_head(head1, &tree).unwrap();
231
232        tree.append(MerkleEntry::new(
233            1,
234            ArtifactType::ThresholdSignature,
235            [1; 32],
236        ));
237        let head2 = TreeHead {
238            tree_size: 2,
239            root_hash: tree.root(),
240            timestamp: Utc::now(),
241        };
242        w.receive_head(head2, &tree).unwrap();
243
244        tree.append(MerkleEntry::new(
245            2,
246            ArtifactType::ThresholdSignature,
247            [2; 32],
248        ));
249        let head3 = TreeHead {
250            tree_size: 3,
251            root_hash: tree.root(),
252            timestamp: Utc::now(),
253        };
254        w.receive_head(head3, &tree).unwrap();
255
256        let heads = w.known_heads();
257        assert_eq!(heads.len(), 3);
258        assert_eq!(heads[0].tree_size, 1);
259        assert_eq!(heads[1].tree_size, 2);
260        assert_eq!(heads[2].tree_size, 3);
261    }
262
263    #[test]
264    fn gossip_to_peer() {
265        let tree = build_tree(5);
266        let mut w1 = Witness::new("w1");
267        let mut w2 = Witness::new("w2");
268
269        let head = TreeHead {
270            tree_size: 5,
271            root_hash: tree.root(),
272            timestamp: Utc::now(),
273        };
274        w1.receive_head(head, &tree).unwrap();
275        w1.gossip_to(&mut w2, 5, &tree).unwrap();
276        assert_eq!(w2.head_count(), 1);
277        assert_eq!(w2.head_at(5).unwrap().root_hash, tree.root());
278    }
279
280    #[test]
281    fn gossip_unknown_head_errors() {
282        let tree = build_tree(5);
283        let w1 = Witness::new("w1");
284        let mut w2 = Witness::new("w2");
285        assert!(w1.gossip_to(&mut w2, 99, &tree).is_err());
286    }
287
288    #[test]
289    fn head_at_returns_correct_head() {
290        let tree = build_tree(5);
291        let mut w = Witness::new("w1");
292        let head = TreeHead {
293            tree_size: 5,
294            root_hash: tree.root(),
295            timestamp: Utc::now(),
296        };
297        w.receive_head(head, &tree).unwrap();
298        assert!(w.head_at(5).is_some());
299        assert!(w.head_at(3).is_none());
300    }
301}