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}