light_client/consensus/leaf.rs
1use std::sync::Arc;
2
3use anyhow::{Context, Result, bail, ensure};
4use committable::Committable;
5use espresso_types::{Leaf2, SeqTypes};
6use hotshot_query_service::availability::LeafQueryData;
7use hotshot_types::{data::EpochNumber, epoch_membership::EpochMembership, vote::HasViewNumber};
8use serde::{Deserialize, Serialize};
9use versions::EPOCH_VERSION;
10
11use super::quorum::{Certificate, Quorum};
12use crate::consensus::quorum::StakeTableQuorum;
13
14/// Data sufficient to convince a client that a certain leaf is finalized.
15///
16/// There are different types of proofs for different scenarios and protocol versions. New proof
17/// types can be added while remaining compatible with old serialized proofs.
18#[derive(Clone, Debug, Default, Deserialize, Serialize)]
19pub enum FinalityProof {
20 /// The client has stated that they already trust the finality of this particular leaf.
21 #[default]
22 Assumption,
23
24 /// The finality follows from a 2-chain of QCs using the HotStuff2 commit rule.
25 ///
26 /// The requirements for checking finality of a leaf given a 2-chain of QCs are:
27 /// * The leaf has a protocol version indicating it was created via HotStuff2
28 /// * `committing_qc.leaf_commit() == leaf.commit()`
29 /// * `committing_qc.view_number() == leaf.view_number()`
30 /// * `deciding_qc.view_number() == committing_qc.view_number() + 1`
31 /// * Both QCs have a valid threshold signature given a stake table
32 HotStuff2 {
33 committing_qc: Arc<Certificate>,
34 deciding_qc: Arc<Certificate>,
35 },
36
37 /// The finality follows from a 3-chain of QCs using the original HotStuff commit rule.
38 ///
39 /// The requirements for checking finality of a leaf via the 3-chain rule are similar to the
40 /// `HotStuff2` finality rule, but an extra QC is required with a consecutive view number.
41 HotStuff {
42 precommit_qc: Arc<Certificate>,
43 committing_qc: Arc<Certificate>,
44 deciding_qc: Arc<Certificate>,
45 },
46}
47
48impl FinalityProof {
49 /// The epoch number whose quorum is needed to verify this proof.
50 ///
51 /// This determines the kind of [`LeafProofHint`] needed to verify the proof. If [`Some`], then
52 /// a [`LeafProofHint::Quorum`] is needed with a quorum from this epoch. If [`None`], then a
53 /// [`LeafProofHint::Assumption`] is needed.
54 pub fn epoch(&self) -> Option<EpochNumber> {
55 match self {
56 Self::Assumption => None,
57 Self::HotStuff2 { committing_qc, .. } => committing_qc.epoch(),
58 Self::HotStuff { precommit_qc, .. } => precommit_qc.epoch(),
59 }
60 }
61}
62
63/// A hint that allows a verifier to verify a proof.
64///
65/// The hint should be supplied by the verifier (e.g. the light client). It represents the root of
66/// trust for this proof.
67#[derive(Clone, Copy, Debug)]
68pub enum LeafProofHint<'a, Q> {
69 /// The root of trust is a quorum for a particular epoch.
70 ///
71 /// This quorum can be used to verify the [`Certificate`]s that make up a
72 /// [`HotStuff`](FinalityProof::HotStuff) or [`HotStuff2`](FinalityProof::HotStuff2)
73 /// [`FinalityProof`].
74 Quorum(&'a Q),
75
76 /// The root of trust is an existing leaf that is already known by the verifier to be finalized.
77 ///
78 /// This can be used to check that the leaf chain in a [`LeafProof`] leads up to a leaf that is
79 /// finalized, and thus the whole chain is finalized.
80 Assumption(&'a Leaf2),
81}
82
83impl<'a> LeafProofHint<'a, StakeTableQuorum<EpochMembership<SeqTypes>>> {
84 /// Construct a [`LeafProofHint`] from a known-finalized leaf, where the quorum type doesn't
85 /// matter.
86 ///
87 /// If the quorum type matters (even though it will not be used in verification whenever the
88 /// known-finalized leaf is used), use `LeafProofHint::<Q>::Assumption(leaf)` to specify the
89 /// quorum type `Q` explicitly.
90 pub fn assumption(leaf: &'a Leaf2) -> Self {
91 Self::Assumption(leaf)
92 }
93}
94
95/// A proof that a leaf is finalized.
96#[derive(Clone, Debug, Default, Deserialize, Serialize)]
97pub struct LeafProof {
98 /// A chain of leaves from the requested leaf to a provably finalized leaf.
99 ///
100 /// The chain is in chronological order, so `leaves[0]` is the requested leaf and
101 /// `leaves.last()` is a leaf which is known or can be proven to be finalized. The chain is
102 /// joined by `parent_commitment`, so it can be validated by recomputing the commitment of each
103 /// leaf and comparing to the parent commitment of the next.
104 leaves: Vec<Leaf2>,
105
106 /// Some extra data proving finality for the last leaf in `leaves`.
107 proof: FinalityProof,
108}
109
110impl LeafProof {
111 /// Verify the proof.
112 ///
113 /// If successful, returns the leaf which is proven finalized.
114 pub async fn verify(
115 &self,
116 hint: LeafProofHint<'_, impl Quorum>,
117 ) -> Result<LeafQueryData<SeqTypes>> {
118 let mut leaves = self.leaves.iter();
119 let leaf = leaves.next().context("empty leaf chain")?;
120
121 // The QC signing `leaf`. This either comes from the next leaf in the chain, or from the
122 // final QC chain in case the leaf chain contains only this single leaf.
123 let mut opt_qc = None;
124
125 // Verify chaining by recomputing hashes.
126 let mut curr = leaf;
127 for next in leaves {
128 ensure!(curr.commit() == next.parent_commitment());
129 curr = next;
130
131 if opt_qc.is_none() {
132 // Get the QC signing `leaf` from the justify QC of the subsequent leaf.
133 opt_qc = Some(next.justify_qc().clone());
134 }
135 }
136
137 // Check that the final leaf is actually finalized and get the QC which signs it.
138 let final_qc = match (&self.proof, hint) {
139 (FinalityProof::Assumption, LeafProofHint::Assumption(finalized)) => {
140 // The prover claims that we already have a finalized leaf whose parent is the
141 // current leaf.
142 ensure!(finalized.parent_commitment() == curr.commit());
143 finalized.justify_qc()
144 },
145 (
146 FinalityProof::HotStuff2 {
147 committing_qc,
148 deciding_qc,
149 },
150 LeafProofHint::Quorum(quorum),
151 ) => {
152 // Check that the given QCs form a 2-chain, which proves `curr` finalized under
153 // HotStuff2.
154 let version = quorum
155 .verify_qc_chain_and_get_version(curr, [&**committing_qc, &**deciding_qc])
156 .await?;
157
158 // Check that HotStuff2 is the appropriate commit rule to use. HotStuff2 commit rule
159 // was introduced with the epochs version of HotShot.
160 ensure!(version >= EPOCH_VERSION);
161
162 committing_qc.qc().clone()
163 },
164 (
165 FinalityProof::HotStuff {
166 precommit_qc,
167 committing_qc,
168 deciding_qc,
169 },
170 LeafProofHint::Quorum(quorum),
171 ) => {
172 // Check that the given QCs form a 3-chain, which proves `curr` finalized under
173 // HotStuff.
174 let version = quorum
175 .verify_qc_chain_and_get_version(
176 curr,
177 [&**precommit_qc, &**committing_qc, &**deciding_qc],
178 )
179 .await?;
180
181 // Check that HotStuff is the appropriate commit rule to use. HotStuff commit rule
182 // was deprecated with the epochs version of HotShot.
183 ensure!(version < EPOCH_VERSION);
184
185 precommit_qc.qc().clone()
186 },
187 (proof, hint) => {
188 let required = match proof {
189 FinalityProof::Assumption => "finalized leaf",
190 FinalityProof::HotStuff { .. } | FinalityProof::HotStuff2 { .. } => "quorum",
191 };
192 let supplied = match hint {
193 LeafProofHint::Assumption(..) => "finalized leaf",
194 LeafProofHint::Quorum(..) => "quorum",
195 };
196 bail!(
197 "verifier supplied wrong hint for proof: proof requires a {required} but \
198 supplied hint is {supplied}"
199 );
200 },
201 };
202
203 // Take the QC which signs the requested leaf: either the one we saved earlier, or the one
204 // signing the latest leaf in case the latest leaf is also the requested leaf.
205 let qc = opt_qc.unwrap_or(final_qc);
206
207 let info = LeafQueryData::new(leaf.clone(), qc)?;
208 Ok(info)
209 }
210
211 /// Append a new leaf to the proof's chain.
212 ///
213 /// Returns `true` if and only if we have enough data to prove at least the first leaf in the
214 /// chain finalized.
215 pub fn push(&mut self, new_leaf: LeafQueryData<SeqTypes>) -> bool {
216 let len = self.leaves.len();
217
218 // Check if the new leaf plus the last saved leaf contain justifying QCs that form a
219 // HotStuff2 QC chain for the leaf before.
220 if len >= 2 && self.leaves[len - 2].block_header().version() >= EPOCH_VERSION {
221 let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
222 let deciding_qc = Certificate::for_parent(new_leaf.leaf());
223 if committing_qc.view_number() == self.leaves[len - 2].view_number()
224 && deciding_qc.view_number() == committing_qc.view_number() + 1
225 {
226 // Sanity check: if we have a chain of QCs from consecutive views, they must refer
227 // to consecutive leaves.
228 debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
229 debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
230
231 self.proof = FinalityProof::HotStuff2 {
232 committing_qc: Arc::new(committing_qc),
233 deciding_qc: Arc::new(deciding_qc),
234 };
235
236 // We don't actually need the last leaf in the chain, we just needed it for its
237 // extra justifying QC.
238 self.leaves.pop();
239
240 return true;
241 }
242 }
243
244 // Check if the new leaf plus the last saved leaf contain QCs that form a legacy HotStuff
245 // QC chain for the leaf before.
246 if len >= 3 && self.leaves[len - 3].block_header().version() < EPOCH_VERSION {
247 let precommit_qc = Certificate::for_parent(&self.leaves[len - 2]);
248 let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
249 let deciding_qc = Certificate::for_parent(new_leaf.leaf());
250 if precommit_qc.view_number() == self.leaves[len - 3].view_number()
251 && committing_qc.view_number() == precommit_qc.view_number() + 1
252 && deciding_qc.view_number() == committing_qc.view_number() + 1
253 {
254 // Sanity check: if we have a chain of QCs from consecutive views, they must refer
255 // to consecutive leaves.
256 debug_assert!(precommit_qc.leaf_commit() == self.leaves[len - 3].commit());
257 debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
258 debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
259
260 self.proof = FinalityProof::HotStuff {
261 precommit_qc: Arc::new(precommit_qc),
262 committing_qc: Arc::new(committing_qc),
263 deciding_qc: Arc::new(deciding_qc),
264 };
265
266 // We don't actually need the last two leaves in the chain, we just needed them for
267 // their extra justifying QCs,.
268 self.leaves.pop();
269 self.leaves.pop();
270
271 return true;
272 }
273 }
274
275 // Nothing is finalized yet, just save the new leaf.
276 self.leaves.push(new_leaf.leaf().clone());
277 false
278 }
279
280 /// Complete a finality proof by appending 2 QCs which extend from the last pushed leaf.
281 ///
282 /// This is meant to be called by the prover and so it is assumed that the provided QCs
283 /// correctly form a 2-chain and that the protocol version is HotStuff2. If these conditions are
284 /// met, this function will not fail but may produce a proof which fails to verify.
285 pub fn add_qc_chain(&mut self, committing_qc: Arc<Certificate>, deciding_qc: Arc<Certificate>) {
286 debug_assert!(
287 committing_qc.view_number() == self.leaves[self.leaves.len() - 1].view_number()
288 );
289 debug_assert!(committing_qc.leaf_commit() == self.leaves[self.leaves.len() - 1].commit());
290 debug_assert!(deciding_qc.view_number() == committing_qc.view_number() + 1);
291
292 self.proof = FinalityProof::HotStuff2 {
293 committing_qc,
294 deciding_qc,
295 };
296 }
297
298 /// Inspect the raw finality proof within the larger proof.
299 pub fn proof(&self) -> &FinalityProof {
300 &self.proof
301 }
302}
303
304#[cfg(test)]
305mod test {
306 use pretty_assertions::assert_eq;
307
308 use super::*;
309 use crate::testing::{AlwaysFalseQuorum, AlwaysTrueQuorum, LEGACY_VERSION, leaf_chain};
310
311 #[test_log::test(tokio::test(flavor = "multi_thread"))]
312 async fn test_hotstuff2() {
313 let mut proof = LeafProof::default();
314
315 // Insert some leaves, forming a chain.
316 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
317 assert!(!proof.push(leaves[0].clone()));
318 assert!(!proof.push(leaves[1].clone()));
319 assert!(proof.push(leaves[2].clone()));
320 assert_eq!(
321 proof
322 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
323 .await
324 .unwrap(),
325 leaves[0]
326 );
327 }
328
329 #[test_log::test(tokio::test(flavor = "multi_thread"))]
330 async fn test_invalid_qc() {
331 let mut proof = LeafProof::default();
332
333 // Insert some leaves, forming a chain.
334 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
335 assert!(!proof.push(leaves[0].clone()));
336 assert!(!proof.push(leaves[1].clone()));
337 assert!(proof.push(leaves[2].clone()));
338
339 // The proof is otherwise valid...
340 assert_eq!(
341 proof
342 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
343 .await
344 .unwrap(),
345 leaves[0]
346 );
347 // ...but fails if the signatures are not valid.
348 proof
349 .verify(LeafProofHint::Quorum(&AlwaysFalseQuorum))
350 .await
351 .unwrap_err();
352 }
353
354 #[test_log::test(tokio::test(flavor = "multi_thread"))]
355 async fn test_assumption() {
356 let mut proof = LeafProof::default();
357
358 // Insert a single leaf. We will not be able to provide proofs ending in a leaf chain, but
359 // we can return a leaf if the leaf after it is already known to be finalized.
360 let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
361 assert!(!proof.push(leaves[0].clone()));
362 assert_eq!(
363 proof
364 .verify(LeafProofHint::assumption(leaves[1].leaf()))
365 .await
366 .unwrap(),
367 leaves[0]
368 );
369 }
370
371 #[test_log::test(tokio::test(flavor = "multi_thread"))]
372 async fn test_no_chain() {
373 let mut proof = LeafProof::default();
374
375 // Insert multiple leaves that don't chain. We will not be able to prove these are
376 // finalized.
377 let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
378 assert!(!proof.push(leaves[0].clone()));
379 assert!(!proof.push(leaves[2].clone()));
380
381 // Even if we start from a finalized leave that extends one of the leaves we do have (4,
382 // extends 3) we fail to generate a proof because we can't generate a chain from the
383 // requested leaf (1) to the finalized leaf (4), since leaf 2 is missing.
384 proof
385 .verify(LeafProofHint::assumption(leaves[3].leaf()))
386 .await
387 .unwrap_err();
388 }
389
390 #[test_log::test(tokio::test(flavor = "multi_thread"))]
391 async fn test_final_qcs() {
392 let mut proof = LeafProof::default();
393
394 // Insert a single leaf, plus an extra QC chain proving it finalized.
395 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
396 assert!(!proof.push(leaves[0].clone()));
397 proof.add_qc_chain(
398 Arc::new(Certificate::for_parent(leaves[1].leaf())),
399 Arc::new(Certificate::for_parent(leaves[2].leaf())),
400 );
401 assert_eq!(
402 proof
403 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
404 .await
405 .unwrap(),
406 leaves[0]
407 );
408 }
409
410 #[test_log::test(tokio::test(flavor = "multi_thread"))]
411 async fn test_legacy_hotstuff_three_chain() {
412 let mut proof = LeafProof::default();
413
414 // Insert some leaves, forming a chain.
415 let leaves = leaf_chain(1..=4, LEGACY_VERSION).await;
416 assert!(!proof.push(leaves[0].clone()));
417 assert!(!proof.push(leaves[1].clone()));
418 assert!(!proof.push(leaves[2].clone()));
419 assert!(proof.push(leaves[3].clone()));
420 assert_eq!(
421 proof
422 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
423 .await
424 .unwrap(),
425 leaves[0]
426 );
427 }
428
429 #[test_log::test(tokio::test(flavor = "multi_thread"))]
430 async fn test_legacy_hotstuff_two_chain_only() {
431 let mut proof = LeafProof::default();
432
433 // Insert some leaves, forming a 2-chain but not the 3-chain required to decide in legacy
434 // HotStuff.
435 let leaves = leaf_chain(1..=3, LEGACY_VERSION).await;
436 assert!(!proof.push(leaves[0].clone()));
437 assert!(!proof.push(leaves[1].clone()));
438 assert!(!proof.push(leaves[2].clone()));
439 proof
440 .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum))
441 .await
442 .unwrap_err();
443 }
444}