penumbra_sdk_tct/
validate.rs

1//! Validation checks to ensure that [`Tree`]s are well-formed.
2
3use std::{
4    collections::BTreeMap,
5    fmt::{Display, Write},
6};
7
8use crate::prelude::*;
9
10/// Verify that the inner index of the tree is correct with respect to the tree structure
11/// itself.
12///
13/// This is an expensive operation that requires traversing the entire tree structure,
14/// building an auxiliary reverse index, and re-hashing every leaf of the tree.
15///
16/// If this ever returns `Err`, it indicates either a bug in this crate, or a tree that was
17/// deserialized from an untrustworthy source.
18pub fn index(tree: &Tree) -> Result<(), IndexMalformed> {
19    // A reverse index from positions back to the commitments that are supposed to map to their
20    // hashes
21    let reverse_index: BTreeMap<Position, StateCommitment> = tree
22        .commitments_unordered()
23        .map(|(commitment, position)| (position, commitment))
24        .collect();
25
26    let mut errors = vec![];
27
28    let mut stack = vec![tree.structure()];
29    while let Some(node) = stack.pop() {
30        stack.extend(node.children());
31
32        if let Kind::Leaf {
33            commitment: Some(actual_commitment),
34        } = node.kind()
35        {
36            // We're at a leaf, so check it:
37            if let Some(&expected_commitment) = reverse_index.get(&node.position()) {
38                if actual_commitment != expected_commitment {
39                    errors.push(IndexError::CommitmentMismatch {
40                        position: node.position(),
41                        expected_commitment,
42                        actual_commitment,
43                    });
44                }
45                let expected_hash = Hash::of(actual_commitment);
46                if expected_hash != node.hash() {
47                    errors.push(IndexError::HashMismatch {
48                        commitment: expected_commitment,
49                        position: node.position(),
50                        expected_hash,
51                        found_hash: node.hash(),
52                    });
53                }
54            } else {
55                // It's okay for there to be an unindexed witness on the frontier (because the
56                // frontier is always represented, even if it's marked for later forgetting),
57                // but otherwise we want to ensure that all witnesses are indexed
58                errors.push(IndexError::UnindexedWitness {
59                    position: node.position(),
60                    found_hash: node.hash(),
61                });
62            };
63        }
64    }
65
66    // Return an error if any were discovered
67    if errors.is_empty() {
68        Ok(())
69    } else {
70        Err(IndexMalformed { errors })
71    }
72}
73
74/// The index for the tree contained at least one error.
75#[derive(Clone, Debug, Error)]
76#[error("malformed index:{}", display_errors(.errors))]
77pub struct IndexMalformed {
78    /// The errors found in the index.
79    pub errors: Vec<IndexError>,
80}
81
82/// An error occurred when verifying the tree's index.
83#[derive(Clone, Debug, Error)]
84pub enum IndexError {
85    /// The index is missing a position.
86    #[error("unindexed position `{position:?}` with hash {found_hash:?}")]
87    UnindexedWitness {
88        /// The position expected to be present in the index.
89        position: Position,
90        /// The hash found at that position.
91        found_hash: Hash,
92    },
93    /// A commitment in the index points to a leaf with a different commitment
94    #[error("found commitment {actual_commitment:?} at position {position:?} but expected {expected_commitment:?}")]
95    CommitmentMismatch {
96        /// The position of the leaf that was found to have the wrong commitment.
97        position: Position,
98        /// The commitment that was expected.
99        expected_commitment: StateCommitment,
100        /// The commitment that was found.
101        actual_commitment: StateCommitment,
102    },
103    /// A commitment in the index doesn't match the hash in the tree at that position.
104    #[error("mismatched hash for commitment {commitment:?} at position `{position:?}`: found {found_hash:?}, expected {expected_hash:?}")]
105    HashMismatch {
106        /// The commitment which should have the found hash.
107        commitment: StateCommitment,
108        /// The position that commitment maps to in the index.
109        position: Position,
110        /// The expected hash value of that commitment.
111        expected_hash: Hash,
112        /// The actual hash found in the tree structure at the position in the index for that commitment.
113        found_hash: Hash,
114    },
115}
116
117/// Verify that every witnessed commitment can be used to generate a proof of inclusion which is
118/// valid with respect to the current root.
119///
120/// This is an expensive operation that requires traversing the entire tree structure and doing
121/// a lot of hashing.
122///
123/// If this ever returns `Err`, it indicates either a bug in this crate, or a tree that was
124/// deserialized from an untrustworthy source.
125pub fn all_proofs(tree: &Tree) -> Result<(), InvalidWitnesses> {
126    let root = tree.root();
127
128    let mut errors = vec![];
129
130    for (commitment, position) in tree.commitments_unordered() {
131        if let Some(proof) = tree.witness(commitment) {
132            if proof.verify(root).is_err() {
133                errors.push(WitnessError::InvalidProof {
134                    proof: Box::new(proof),
135                });
136            }
137        } else {
138            errors.push(WitnessError::UnwitnessedCommitment {
139                commitment,
140                position,
141            })
142        }
143    }
144
145    if errors.is_empty() {
146        Ok(())
147    } else {
148        Err(InvalidWitnesses { root, errors })
149    }
150}
151
152/// At least one proof generated by the tree failed to verify against the root.
153#[derive(Clone, Debug, Error)]
154#[error(
155    "invalid witnesses produced by tree for root {root:?}:{}",
156    display_errors(errors)
157)]
158pub struct InvalidWitnesses {
159    /// The root of the tree at which the errors were found.
160    pub root: Root,
161    /// The errors found.
162    pub errors: Vec<WitnessError>,
163}
164
165/// An error occurred when verifying the tree's contained witnesses.
166#[derive(Clone, Debug, Error)]
167pub enum WitnessError {
168    /// The index contains a commitment that is not witnessed.
169    #[error("unwitnessed commitment {commitment:?} at position `{position:?}`")]
170    UnwitnessedCommitment {
171        /// The commitment that was not present in the tree.
172        commitment: StateCommitment,
173        /// The position at which it was supposed to appear.
174        position: Position,
175    },
176    /// The proof produced by the tree does not verify against the root.
177    #[error("invalid proof for commitment {:?} at position `{:?}`", .proof.commitment(), .proof.position())]
178    InvalidProof {
179        /// The proof which failed to verify.
180        proof: Box<Proof>,
181    },
182}
183
184/// Verify that every internally cached hash matches what it should be, by re-hashing all of them.
185///
186/// This is an expensive operation that requires traversing the entire tree structure and doing
187/// a lot of hashing.
188///
189/// If this ever returns `Err`, it indicates a bug in this crate.
190pub fn cached_hashes(tree: &Tree) -> Result<(), InvalidCachedHashes> {
191    use structure::*;
192
193    fn check_hashes(errors: &mut Vec<InvalidCachedHash>, node: Node) {
194        // IMPORTANT: we need to traverse children before parent, to avoid overwriting the
195        // parent's hash before we have a chance to check it! This is why we don't use
196        // `structure::traverse` here, because that is a pre-order traversal.
197        for child in node.children() {
198            // The frontier is the only place where cached hashes occur
199            if child.place() == Place::Frontier {
200                check_hashes(errors, child);
201            }
202        }
203
204        if let Some(cached) = node.cached_hash() {
205            // IMPORTANT: we need to clear the cache to actually recompute it!
206            node.clear_cached_hash();
207
208            let recomputed = node.hash();
209
210            if cached != recomputed {
211                errors.push(InvalidCachedHash {
212                    place: node.place(),
213                    kind: node.kind(),
214                    height: node.height(),
215                    index: node.index(),
216                    cached,
217                    recomputed,
218                })
219            }
220        }
221    }
222
223    let mut errors = vec![];
224    check_hashes(&mut errors, tree.structure());
225
226    if errors.is_empty() {
227        Ok(())
228    } else {
229        Err(InvalidCachedHashes { errors })
230    }
231}
232
233/// The tree contained at least one internal cached hash that was incorrect.
234#[derive(Clone, Debug, Error)]
235#[error("invalid cached hashes:{}", display_errors(.errors))]
236pub struct InvalidCachedHashes {
237    /// The errors found in the tree.
238    pub errors: Vec<InvalidCachedHash>,
239}
240
241/// An mismatch between a cached hash and the hash it ought to have been.
242#[derive(Clone, Debug, Error)]
243#[error("cache for `{place}::{kind}` at height {height}, index {index} is incorrect: found {cached:?}, expected {recomputed:?}")]
244pub struct InvalidCachedHash {
245    /// The place of the node with the error.
246    pub place: Place,
247    /// The kind of the node with the error.
248    pub kind: Kind,
249    /// The height of the node with the error.
250    pub height: u8,
251    /// The index of the node with the error.
252    pub index: u64,
253    /// The previous cached hash at that location.
254    pub cached: Hash,
255    /// The recomputed hash that should have been there.
256    pub recomputed: Hash,
257}
258
259/// Verify that the internal forgotten versions are consistent throughout the tree.
260///
261/// This is a relatively expensive operation which requires traversing the entire tree structure.
262///
263/// If this ever returns `Err`, it indicates a bug in this crate.
264pub fn forgotten(tree: &Tree) -> Result<(), InvalidForgotten> {
265    use structure::*;
266
267    fn check_forgotten(
268        errors: &mut Vec<InvalidForgottenVersion>,
269        expected_max: Option<Forgotten>,
270        node: Node,
271    ) {
272        let children = node.children();
273        let actual_max = node
274            .children()
275            .iter()
276            .map(Node::forgotten)
277            .max()
278            .unwrap_or_default();
279
280        if let Some(expected_max) = expected_max {
281            // Check the expected forgotten version here
282            if actual_max != expected_max {
283                errors.push(InvalidForgottenVersion {
284                    kind: node.kind(),
285                    place: node.place(),
286                    height: node.height(),
287                    index: node.index(),
288                    expected_max,
289                    actual_max,
290                })
291            }
292
293            // Check the children
294            for child in children {
295                check_forgotten(errors, Some(child.forgotten()), child);
296            }
297        }
298    }
299
300    let mut errors = vec![];
301    check_forgotten(&mut errors, None, tree.structure());
302
303    if errors.is_empty() {
304        Ok(())
305    } else {
306        Err(InvalidForgotten { errors })
307    }
308}
309
310/// The tree contained at least one discrepancy in the internal forgotten versions of its nodes.
311#[derive(Clone, Debug, Error)]
312#[error("invalid forgotten versions:{}", display_errors(.errors))]
313pub struct InvalidForgotten {
314    /// The errors found in the tree.
315    pub errors: Vec<InvalidForgottenVersion>,
316}
317
318/// A mismatch between the expected maximum forgotten version and the actual one.
319#[derive(Clone, Debug, Error)]
320#[error("forgotten version mismatch for `{place}::{kind}` at height {height}, index {index}: found {actual_max:?}, expected {expected_max:?}")]
321pub struct InvalidForgottenVersion {
322    /// The place of the node with the error.
323    pub place: Place,
324    /// The kind of the node with the error.
325    pub kind: Kind,
326    /// The height of the node with the error.
327    pub height: u8,
328    /// The index of the node with the error.
329    pub index: u64,
330    /// The actual maximum forgotten version.
331    pub actual_max: Forgotten,
332    /// The expected maximum forgotten version.
333    pub expected_max: Forgotten,
334}
335
336// A helper function to display a line-separated list of errors
337fn display_errors(errors: impl IntoIterator<Item = impl Display>) -> String {
338    let mut output = String::new();
339    for error in errors.into_iter() {
340        write!(&mut output, "\n  {error}").unwrap();
341    }
342    output
343}