warg_transparency/log/
proof.rs

1use std::marker::PhantomData;
2
3use alloc::vec::Vec;
4use thiserror::Error;
5use warg_crypto::{
6    hash::{Hash, SupportedDigest},
7    VisitBytes,
8};
9
10use super::{hash_branch, hash_leaf, node::Node, LogData};
11
12/// A proof that a leaf is present for a root
13#[derive(Debug, Clone, PartialEq)]
14pub struct InclusionProof<D: SupportedDigest, V: VisitBytes> {
15    /// The node that you are checking is present in the given point.
16    leaf: Node,
17    /// The point in the logs history where the leaf should be present
18    log_length: usize,
19    /// Marker for digest type
20    _digest: PhantomData<D>,
21    /// Marker for value type
22    _value: PhantomData<V>,
23}
24
25/// An error occurring when attempting to validate an inclusion proof.
26#[derive(Error, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum InclusionProofError {
28    /// Indicates that the leaf is too new to be present at
29    /// the given point in the log history.
30    #[error("leaf newer than when it should be included")]
31    LeafTooNew,
32    /// Indicates that certain hashes weren't known that are
33    /// needed to perform proof validation.
34    #[error("required hash for proof is not available")]
35    HashNotKnown,
36}
37
38/// The nodes visited when verifying the inclusion proof.
39///
40/// The first [InclusionProofWalk.initial_walk_len] nodes
41/// describe the walk up to the balanced root which is
42/// the leafs ancestor.
43///
44/// The next [InclusionProofWalk.lower_broots] nodes
45/// describe the walk from the rightmost (lowest) broot
46/// up towards the broot that was reached.
47///
48/// The next [InclusionProofWalk.upper_broots] nodes
49/// describes the walk from the intersection of the
50/// previous two to the leftmost (tallest) broot.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct InclusionProofWalk {
53    pub(crate) nodes: Vec<Node>,
54    initial_walk_len: usize,
55    lower_broots: usize,
56    upper_broots: usize,
57}
58
59impl InclusionProofWalk {
60    fn initial_walk_end(&self) -> usize {
61        self.initial_walk_len
62    }
63
64    fn lower_broot_walk_end(&self) -> usize {
65        self.initial_walk_end() + self.lower_broots
66    }
67
68    fn upper_broot_walk_end(&self) -> usize {
69        self.lower_broot_walk_end() + self.upper_broots
70    }
71
72    fn initial_walk(&self) -> &[Node] {
73        &self.nodes[..self.initial_walk_end()]
74    }
75
76    fn lower_broot_walk(&self) -> &[Node] {
77        &self.nodes[self.initial_walk_end()..self.lower_broot_walk_end()]
78    }
79
80    fn upper_broot_walk(&self) -> &[Node] {
81        &self.nodes[self.lower_broot_walk_end()..self.upper_broot_walk_end()]
82    }
83}
84
85impl<D, V> InclusionProof<D, V>
86where
87    D: SupportedDigest,
88    V: VisitBytes,
89{
90    pub(crate) fn new(leaf: Node, log_length: usize) -> Self {
91        Self {
92            leaf,
93            log_length,
94            _digest: PhantomData,
95            _value: PhantomData,
96        }
97    }
98
99    /// Get the node that this proof proves the inclusion of
100    pub fn leaf(&self) -> Node {
101        self.leaf
102    }
103
104    /// Get the length of the log this proof shows the leaf was included in
105    pub fn log_length(&self) -> usize {
106        self.log_length
107    }
108
109    /// Collects all of the node indices that must be visited
110    /// in order to validate the inlcusion proof into.
111    pub fn walk(&self) -> Result<InclusionProofWalk, InclusionProofError> {
112        let length = self.log_length;
113        let broots = Node::broots_for_len(length);
114        let mut current_node = self.leaf;
115
116        if !current_node.exists_at_length(length) {
117            return Err(InclusionProofError::LeafTooNew);
118        }
119
120        let mut nodes = Vec::new();
121
122        // Walk upwards until you hit a balanced root for the original tree
123        while !broots.contains(&current_node) {
124            let sibling = current_node.sibling();
125            nodes.push(sibling);
126            current_node = current_node.parent();
127        }
128
129        let initial_walk_len = nodes.len();
130
131        let index = broots
132            .iter()
133            .position(|broot| *broot == current_node)
134            .unwrap();
135
136        let lower_broots = broots.len() - index - 1;
137        for broot in broots[index + 1..].iter().rev() {
138            nodes.push(*broot);
139        }
140
141        let upper_broots = index;
142        for broot in broots[..index].iter().rev() {
143            nodes.push(*broot);
144        }
145
146        Ok(InclusionProofWalk {
147            nodes,
148            initial_walk_len,
149            lower_broots,
150            upper_broots,
151        })
152    }
153
154    /// Evaluate an inclusion proof.
155    /// Callers should verify that the returned root matches their expectation.
156    ///
157    /// Walks the inclusion proof, hashes each layer, returns the root hash.
158    pub fn evaluate_value(
159        &self,
160        hashes: &impl LogData<D, V>,
161        value: &V,
162    ) -> Result<Hash<D>, InclusionProofError> {
163        self.evaluate_hash(hashes, hash_leaf(value))
164    }
165
166    /// Evaluate an inclusion proof.
167    /// Callers should verify that the returned root matches their expectation.
168    ///
169    /// Walks the inclusion proof, hashes each layer, returns the root hash.
170    pub fn evaluate_hash(
171        &self,
172        hashes: &impl LogData<D, V>,
173        hash: Hash<D>,
174    ) -> Result<Hash<D>, InclusionProofError> {
175        let leaf = (self.leaf, hash);
176        let walk = self.walk()?;
177
178        // Ensure all nodes are known
179        if walk.nodes.iter().any(|node| !hashes.has_hash(*node)) {
180            return Err(InclusionProofError::HashNotKnown);
181        }
182
183        // Perform initial walk up to the ancestor broot
184        let current = walk
185            .initial_walk()
186            .iter()
187            .map(|node| (*node, hashes.hash_for(*node).unwrap()))
188            .fold(leaf, combine);
189
190        // Summarize all of the smaller broots
191        let lower_broot = walk
192            .lower_broot_walk()
193            .iter()
194            .map(|node| (*node, hashes.hash_for(*node).unwrap()))
195            .reduce(combine);
196
197        // Combine broot with summary of smaller roots
198        let current = match lower_broot {
199            Some(lower_broot) => combine(current, lower_broot),
200            None => current,
201        };
202
203        // Combine with any larger roots
204        let current = walk
205            .upper_broot_walk()
206            .iter()
207            .map(|node| (*node, hashes.hash_for(*node).unwrap()))
208            .fold(current, combine);
209
210        Ok(current.1)
211    }
212}
213
214fn combine<D: SupportedDigest>(first: (Node, Hash<D>), second: (Node, Hash<D>)) -> (Node, Hash<D>) {
215    let (lhs, rhs) = if first.0.index() < second.0.index() {
216        (first.1, second.1)
217    } else {
218        (second.1, first.1)
219    };
220
221    (second.0, hash_branch::<D>(lhs, rhs))
222}
223
224/// A proof of the consistency between two points in the
225/// logs history.
226#[derive(Debug, Clone, Copy, PartialEq)]
227pub struct ConsistencyProof<D, V>
228where
229    D: SupportedDigest,
230    V: VisitBytes,
231{
232    /// The older of the two points
233    pub old_length: usize,
234    /// The newer of the two points
235    pub new_length: usize,
236    /// Marker for digest type
237    _digest: PhantomData<D>,
238    /// Marker for value type
239    _value: PhantomData<V>,
240}
241
242/// Errors occurring when validating a consistency proof
243#[derive(Error, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
244pub enum ConsistencyProofError {
245    /// Happens when old_length > new_length
246    #[error("tries to prove later value comes before earlier")]
247    PointsOutOfOrder,
248    /// Happens when hashes required for evaluation were not present
249    #[error("a hash needed for evaluation was not available")]
250    HashNotKnown,
251    /// Happens when an inclusion proof is evaluated and has an error
252    #[error("constituent inclusion proof failed")]
253    InclusionError(#[from] InclusionProofError),
254    /// Happens when two inclusion proofs are evaluated and produce different roots
255    #[error("constituent inclusion proofs diverge produce different roots")]
256    DivergingRoots,
257}
258
259impl<D, V> ConsistencyProof<D, V>
260where
261    D: SupportedDigest,
262    V: VisitBytes,
263{
264    pub(crate) fn new(old_length: usize, new_length: usize) -> Self {
265        Self {
266            old_length,
267            new_length,
268            _digest: PhantomData,
269            _value: PhantomData,
270        }
271    }
272
273    /// Evaluate an inclusion proof.
274    /// Callers should verify that the returned root matches their expectation.
275    ///
276    /// Walks the inclusion proof, hashes each layer, returns the root hash.
277    pub fn evaluate(
278        &self,
279        hashes: &impl LogData<D, V>,
280    ) -> Result<(Hash<D>, Hash<D>), ConsistencyProofError> {
281        let mut old_broots = Vec::new();
282        let mut new_root = None;
283
284        for inc_proof in self.inclusions().unwrap() {
285            let leaf_hash = hashes
286                .hash_for(inc_proof.leaf())
287                .ok_or(ConsistencyProofError::HashNotKnown)?;
288            old_broots.push(leaf_hash.clone());
289            let found_root = inc_proof.evaluate_hash(hashes, leaf_hash)?;
290            if let Some(previous_root) = &new_root {
291                if previous_root != &found_root {
292                    return Err(ConsistencyProofError::DivergingRoots);
293                }
294            } else {
295                new_root = Some(found_root);
296            }
297        }
298
299        let old_root = old_broots
300            .into_iter()
301            .rev()
302            .reduce(|new, old| hash_branch(old, new));
303        // Unwrap is safe because the minimal consistency proof always has at least one inclusion proof
304        let old_root = old_root.unwrap();
305        let new_root = new_root.unwrap();
306        Ok((old_root, new_root))
307    }
308
309    /// Convert the consistency proof into a sequence of inclusion proofs.
310    /// Each inclusion proof verifies that one of the balanced roots
311    /// of the old tree is present in the root of the new tree.
312    pub fn inclusions(&self) -> Result<Vec<InclusionProof<D, V>>, ConsistencyProofError> {
313        if self.old_length > self.new_length {
314            return Err(ConsistencyProofError::PointsOutOfOrder);
315        }
316
317        let inclusions = Node::broots_for_len(self.old_length)
318            .into_iter()
319            .map(|broot| InclusionProof::new(broot, self.new_length))
320            .collect();
321
322        Ok(inclusions)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use crate::log::{LogBuilder, VecLog};
329
330    use super::*;
331
332    use warg_crypto::hash::Sha256;
333
334    #[test]
335    fn test_inc_even_2() {
336        let mut log: VecLog<Sha256, u8> = VecLog::default();
337
338        log.push(&100);
339        log.push(&102);
340
341        let inc_proof = InclusionProof::new(Node(0), 2);
342        let expected = InclusionProofWalk {
343            nodes: vec![Node(2)],
344            initial_walk_len: 1,
345            lower_broots: 0,
346            upper_broots: 0,
347        };
348        assert_eq!(inc_proof.walk().unwrap(), expected);
349
350        assert_eq!(
351            inc_proof.evaluate_value(&log, &100).unwrap(),
352            log.as_ref()[1].clone()
353        );
354    }
355
356    #[test]
357    fn test_inc_odd_3() {
358        let mut log: VecLog<Sha256, u8> = VecLog::default();
359
360        log.push(&100);
361        log.push(&102);
362        log.push(&104);
363
364        let root: Hash<Sha256> = hash_branch(log.as_ref()[1].clone(), log.as_ref()[4].clone());
365
366        // node 0
367        let inc_proof = InclusionProof::new(Node(0), 3);
368        let expected = InclusionProofWalk {
369            nodes: vec![Node(2), Node(4)],
370            initial_walk_len: 1,
371            lower_broots: 1,
372            upper_broots: 0,
373        };
374        assert_eq!(inc_proof.walk().unwrap(), expected);
375        assert_eq!(inc_proof.evaluate_value(&log, &100).unwrap(), root);
376
377        // node 2
378        let inc_proof = InclusionProof::new(Node(2), 3);
379        let expected = InclusionProofWalk {
380            nodes: vec![Node(0), Node(4)],
381            initial_walk_len: 1,
382            lower_broots: 1,
383            upper_broots: 0,
384        };
385        assert_eq!(inc_proof.walk().unwrap(), expected);
386        assert_eq!(inc_proof.evaluate_value(&log, &102u8).unwrap(), root);
387
388        // node 4
389        let inc_proof = InclusionProof::new(Node(4), 3);
390        let expected = InclusionProofWalk {
391            nodes: vec![Node(1)],
392            initial_walk_len: 0,
393            lower_broots: 0,
394            upper_broots: 1,
395        };
396        assert_eq!(inc_proof.walk().unwrap(), expected);
397        assert_eq!(inc_proof.evaluate_value(&log, &104u8).unwrap(), root);
398    }
399
400    #[test]
401    fn test_inc_odd_7() {
402        let mut log: VecLog<Sha256, u8> = VecLog::default();
403
404        log.push(&100);
405        log.push(&102);
406        log.push(&104);
407        log.push(&106);
408        log.push(&108);
409        log.push(&110);
410        log.push(&112);
411
412        let artificial_branch: Hash<Sha256> =
413            hash_branch(log.as_ref()[9].clone(), log.as_ref()[12].clone());
414        let root: Hash<Sha256> = hash_branch(log.as_ref()[3].clone(), artificial_branch);
415
416        // node 6
417        let inc_proof = InclusionProof::new(Node(6), 7);
418        let expected = InclusionProofWalk {
419            nodes: vec![Node(4), Node(1), Node(12), Node(9)],
420            initial_walk_len: 2,
421            lower_broots: 2,
422            upper_broots: 0,
423        };
424        assert_eq!(inc_proof.walk().unwrap(), expected);
425        assert_eq!(inc_proof.evaluate_value(&log, &106).unwrap(), root);
426    }
427}