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}