Skip to main content

light_client/consensus/
leaf.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result, bail, ensure};
4use committable::Committable;
5use espresso_types::{Certificate2, Leaf2, SeqTypes};
6use hotshot_query_service_types::availability::LeafQueryData;
7use hotshot_types::{
8    data::EpochNumber,
9    epoch_membership::EpochMembership,
10    simple_certificate::QuorumCertificate2,
11    traits::block_contents::BlockHeader,
12    utils::{epoch_from_block_number, is_last_block},
13    vote::HasViewNumber,
14};
15use serde::{Deserialize, Serialize};
16use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION};
17
18use super::quorum::{Certificate, ChainVersions, Quorum};
19use crate::consensus::quorum::StakeTableQuorum;
20
21/// Data sufficient to convince a client that a certain leaf is finalized.
22///
23/// There are different types of proofs for different scenarios and protocol versions. New proof
24/// types can be added while remaining compatible with old serialized proofs.
25#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
26pub enum FinalityProof {
27    /// The client has stated that they already trust the finality of this particular leaf.
28    #[default]
29    Assumption,
30
31    /// The finality follows from a 2-chain of QCs using the HotStuff2 commit rule.
32    ///
33    /// The requirements for checking finality of a leaf given a 2-chain of QCs are:
34    /// * The leaf has a protocol version indicating it was created via HotStuff2
35    /// * `committing_qc.leaf_commit() == leaf.commit()`
36    /// * `committing_qc.view_number() == leaf.view_number()`
37    /// * `deciding_qc.view_number() == committing_qc.view_number() + 1`
38    /// * Both QCs have a valid threshold signature given a stake table. When the leaf is the last
39    ///   leaf of an epoch, the deciding QC is produced in the subsequent epoch and is checked
40    ///   against the next epoch's stake table.
41    HotStuff2 {
42        committing_qc: Arc<Certificate>,
43        deciding_qc: Arc<Certificate>,
44    },
45
46    /// The finality follows from a Certificate2 in the new protocol.
47    ///
48    /// Certificate2 proves finality. The QC is needed to reconstruct the returned
49    /// [`LeafQueryData`] when the proof contains only the leaf committed by cert2.
50    NewProtocol {
51        cert2: Arc<Certificate2<SeqTypes>>,
52        leaf_qc: Box<QuorumCertificate2<SeqTypes>>,
53    },
54
55    /// The finality follows from a 3-chain of QCs using the original HotStuff commit rule.
56    ///
57    /// The requirements for checking finality of a leaf via the 3-chain rule are similar to the
58    /// `HotStuff2` finality rule, but an extra QC is required with a consecutive view number.
59    HotStuff {
60        precommit_qc: Arc<Certificate>,
61        committing_qc: Arc<Certificate>,
62        deciding_qc: Arc<Certificate>,
63    },
64}
65/// A hint that allows a verifier to verify a proof.
66///
67/// The hint should be supplied by the verifier (e.g. the light client). It represents the root of
68/// trust for this proof.
69#[derive(Clone, Copy, Debug)]
70pub enum LeafProofHint<'a, Q> {
71    /// The root of trust is a quorum for a particular epoch.
72    ///
73    /// This quorum can be used to verify the [`Certificate`]s that make up a
74    /// [`HotStuff`](FinalityProof::HotStuff) or [`HotStuff2`](FinalityProof::HotStuff2)
75    /// [`FinalityProof`].
76    Quorum(&'a Q),
77
78    /// The root of trust is an existing leaf that is already known by the verifier to be finalized.
79    ///
80    /// This can be used to check that the leaf chain in a [`LeafProof`] leads up to a leaf that is
81    /// finalized, and thus the whole chain is finalized.
82    Assumption(&'a Leaf2),
83}
84
85impl<'a> LeafProofHint<'a, StakeTableQuorum<EpochMembership<SeqTypes>>> {
86    /// Construct a [`LeafProofHint`] from a known-finalized leaf, where the quorum type doesn't
87    /// matter.
88    ///
89    /// If the quorum type matters (even though it will not be used in verification whenever the
90    /// known-finalized leaf is used), use `LeafProofHint::<Q>::Assumption(leaf)` to specify the
91    /// quorum type `Q` explicitly.
92    pub fn assumption(leaf: &'a Leaf2) -> Self {
93        Self::Assumption(leaf)
94    }
95}
96
97/// A proof that a leaf is finalized.
98#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
99pub struct LeafProof {
100    /// A chain of leaves from the requested leaf to a provably finalized leaf.
101    ///
102    /// The chain is in chronological order, so `leaves[0]` is the requested leaf and
103    /// `leaves.last()` is a leaf which is known or can be proven to be finalized. The chain is
104    /// joined by `parent_commitment`, so it can be validated by recomputing the commitment of each
105    /// leaf and comparing to the parent commitment of the next.
106    leaves: Vec<Leaf2>,
107
108    /// Some extra data proving finality for the last leaf in `leaves`.
109    proof: FinalityProof,
110}
111
112impl LeafProof {
113    /// Verify the proof.
114    ///
115    /// If successful, returns the leaf which is proven finalized.
116    pub async fn verify(
117        &self,
118        hint: LeafProofHint<'_, impl Quorum>,
119    ) -> Result<LeafQueryData<SeqTypes>> {
120        let mut leaves = self.leaves.iter();
121        let leaf = leaves.next().context("empty leaf chain")?;
122
123        // The QC signing `leaf`. This either comes from the next leaf in the chain, or from the
124        // final QC chain in case the leaf chain contains only this single leaf.
125        let mut opt_qc = None;
126
127        // Verify chaining by recomputing hashes.
128        let mut curr = leaf;
129        for next in leaves {
130            ensure!(curr.commit() == next.parent_commitment());
131            curr = next;
132
133            if opt_qc.is_none() {
134                // Get the QC signing `leaf` from the justify QC of the subsequent leaf.
135                opt_qc = Some(next.justify_qc().clone());
136            }
137        }
138
139        // Check that the final leaf is actually finalized and get the QC which signs it.
140        let final_qc = match (&self.proof, hint) {
141            (FinalityProof::Assumption, LeafProofHint::Assumption(finalized)) => {
142                // The prover claims that we already have a finalized leaf whose parent is the
143                // current leaf.
144                ensure!(finalized.parent_commitment() == curr.commit());
145                finalized.justify_qc().clone()
146            },
147            (
148                FinalityProof::HotStuff2 {
149                    committing_qc,
150                    deciding_qc,
151                },
152                LeafProofHint::Quorum(quorum),
153            ) => {
154                // Check that the given QCs form a 2-chain, which proves `curr` finalized under
155                // HotStuff2.
156                let ChainVersions {
157                    leaf: version,
158                    max_cert,
159                } = quorum
160                    .verify_qc_chain_and_get_version(curr, [&**committing_qc, &**deciding_qc])
161                    .await?;
162
163                // Check that HotStuff2 is the appropriate commit rule to use. HotStuff2 commit
164                // rule was introduced with the epochs version of HotShot and retired by the new
165                // protocol.
166                ensure!(version >= EPOCH_VERSION);
167                ensure!(
168                    version < NEW_PROTOCOL_VERSION,
169                    "HotStuff2 finality proof used for new-protocol leaf"
170                );
171                // The 2-chain rule is only sound if every QC in the chain predates the cutover;
172                // post-cutover QCs do not imply finality.
173                ensure!(
174                    max_cert < NEW_PROTOCOL_VERSION,
175                    "HotStuff2 finality proof spans the new-protocol cutover"
176                );
177
178                committing_qc.qc().clone()
179            },
180            (
181                FinalityProof::HotStuff {
182                    precommit_qc,
183                    committing_qc,
184                    deciding_qc,
185                },
186                LeafProofHint::Quorum(quorum),
187            ) => {
188                // Check that the given QCs form a 3-chain, which proves `curr` finalized under
189                // HotStuff.
190                let ChainVersions { leaf: version, .. } = quorum
191                    .verify_qc_chain_and_get_version(
192                        curr,
193                        [&**precommit_qc, &**committing_qc, &**deciding_qc],
194                    )
195                    .await?;
196
197                // Check that HotStuff is the appropriate commit rule to use. HotStuff commit rule
198                // was deprecated with the epochs version of HotShot.
199                ensure!(version < EPOCH_VERSION);
200
201                precommit_qc.qc().clone()
202            },
203            (FinalityProof::NewProtocol { cert2, leaf_qc }, LeafProofHint::Quorum(quorum)) => {
204                // Certificate2 proves finality for `curr`.
205                let version = curr.block_header().version();
206                ensure!(
207                    version >= NEW_PROTOCOL_VERSION,
208                    "new protocol finality proof used for pre-new-protocol leaf"
209                );
210                ensure!(
211                    cert2.data.leaf_commit == curr.commit(),
212                    "cert2 leaf commitment does not match leaf"
213                );
214                // A re-vote commits the last block of an epoch again, at a view of
215                // its own, so the commit may be later than the leaf. No other block is
216                // voted on again. These are the node's checks in
217                // `verify_new_protocol_leaf_chain`.
218                ensure!(
219                    cert2.view_number() >= curr.view_number(),
220                    "cert2 view number is earlier than the leaf"
221                );
222                ensure!(
223                    cert2.data.block_number == curr.block_header().block_number(),
224                    "cert2 block number does not match leaf"
225                );
226                if cert2.view_number() > curr.view_number() {
227                    ensure!(
228                        is_last_block(curr.height(), quorum.epoch_height()),
229                        "cert2 view is after the leaf's, which is not the last block of its epoch"
230                    );
231                }
232                let epoch = EpochNumber::new(epoch_from_block_number(
233                    curr.height(),
234                    quorum.epoch_height(),
235                ));
236                ensure!(
237                    cert2.data.epoch == epoch,
238                    "cert2 epoch {} does not match the leaf's epoch {epoch}",
239                    cert2.data.epoch
240                );
241
242                quorum
243                    .verify_cert2(cert2, version)
244                    .await
245                    .context("verifying cert2 signature")?;
246
247                leaf_qc.as_ref().clone()
248            },
249            (proof, hint) => {
250                let required = match proof {
251                    FinalityProof::Assumption => "finalized leaf",
252                    FinalityProof::HotStuff { .. }
253                    | FinalityProof::HotStuff2 { .. }
254                    | FinalityProof::NewProtocol { .. } => "quorum",
255                };
256                let supplied = match hint {
257                    LeafProofHint::Assumption(..) => "finalized leaf",
258                    LeafProofHint::Quorum(..) => "quorum",
259                };
260                bail!(
261                    "verifier supplied wrong hint for proof: proof requires a {required} but \
262                     supplied hint is {supplied}"
263                );
264            },
265        };
266
267        // Take the QC which signs the requested leaf: either the one we saved earlier, or the one
268        // signing the latest leaf in case the latest leaf is also the requested leaf.
269        let qc = opt_qc.unwrap_or(final_qc);
270
271        let info = LeafQueryData::new(leaf.clone(), qc)?;
272        Ok(info)
273    }
274
275    /// Append a new leaf to the proof's chain.
276    ///
277    /// Returns `true` if and only if we have enough data to prove at least the first leaf in the
278    /// chain finalized.
279    pub fn push(&mut self, new_leaf: LeafQueryData<SeqTypes>) -> bool {
280        let len = self.leaves.len();
281
282        // Check if the new leaf plus the last saved leaf contain justifying QCs that form a
283        // HotStuff2 QC chain for the leaf before. The deciding QC signs `leaves[len - 1]`, so
284        // bounding that leaf's version keeps the whole chain pre-cutover.
285        if len >= 2
286            && self.leaves[len - 2].block_header().version() >= EPOCH_VERSION
287            && self.leaves[len - 1].block_header().version() < NEW_PROTOCOL_VERSION
288        {
289            let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
290            let deciding_qc = Certificate::for_parent(new_leaf.leaf());
291            if committing_qc.view_number() == self.leaves[len - 2].view_number()
292                && deciding_qc.view_number() == committing_qc.view_number() + 1
293            {
294                // Sanity check: if we have a chain of QCs from consecutive views, they must refer
295                // to consecutive leaves.
296                debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
297                debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
298
299                self.proof = FinalityProof::HotStuff2 {
300                    committing_qc: Arc::new(committing_qc),
301                    deciding_qc: Arc::new(deciding_qc),
302                };
303
304                // We don't actually need the last leaf in the chain, we just needed it for its
305                // extra justifying QC.
306                self.leaves.pop();
307
308                return true;
309            }
310        }
311
312        // Check if the new leaf plus the last saved leaf contain QCs that form a legacy HotStuff
313        // QC chain for the leaf before.
314        if len >= 3 && self.leaves[len - 3].block_header().version() < EPOCH_VERSION {
315            let precommit_qc = Certificate::for_parent(&self.leaves[len - 2]);
316            let committing_qc = Certificate::for_parent(&self.leaves[len - 1]);
317            let deciding_qc = Certificate::for_parent(new_leaf.leaf());
318            if precommit_qc.view_number() == self.leaves[len - 3].view_number()
319                && committing_qc.view_number() == precommit_qc.view_number() + 1
320                && deciding_qc.view_number() == committing_qc.view_number() + 1
321            {
322                // Sanity check: if we have a chain of QCs from consecutive views, they must refer
323                // to consecutive leaves.
324                debug_assert!(precommit_qc.leaf_commit() == self.leaves[len - 3].commit());
325                debug_assert!(committing_qc.leaf_commit() == self.leaves[len - 2].commit());
326                debug_assert!(deciding_qc.leaf_commit() == self.leaves[len - 1].commit());
327
328                self.proof = FinalityProof::HotStuff {
329                    precommit_qc: Arc::new(precommit_qc),
330                    committing_qc: Arc::new(committing_qc),
331                    deciding_qc: Arc::new(deciding_qc),
332                };
333
334                // We don't actually need the last two leaves in the chain, we just needed them for
335                // their extra justifying QCs,.
336                self.leaves.pop();
337                self.leaves.pop();
338
339                return true;
340            }
341        }
342
343        // Nothing is finalized yet, just save the new leaf.
344        self.leaves.push(new_leaf.leaf().clone());
345        false
346    }
347
348    /// Complete a finality proof using the new protocol Certificate2.
349    pub fn add_certificate(
350        &mut self,
351        cert2: Arc<Certificate2<SeqTypes>>,
352        leaf_qc: QuorumCertificate2<SeqTypes>,
353    ) {
354        debug_assert!(cert2.data.leaf_commit == self.leaves[self.leaves.len() - 1].commit());
355
356        self.proof = FinalityProof::NewProtocol {
357            cert2,
358            leaf_qc: Box::new(leaf_qc),
359        };
360    }
361
362    /// Inspect the raw finality proof within the larger proof.
363    pub fn proof(&self) -> &FinalityProof {
364        &self.proof
365    }
366
367    /// Inspect the leaf chain, requested leaf first, without verifying it.
368    pub fn leaves(&self) -> &[Leaf2] {
369        &self.leaves
370    }
371
372    /// The epoch number whose quorum is needed to verify this proof.
373    ///
374    /// This determines the kind of [`LeafProofHint`] needed to verify the proof. If [`Some`], then
375    /// a [`LeafProofHint::Quorum`] is needed with a quorum from this epoch. If [`None`], then a
376    /// [`LeafProofHint::Assumption`] is needed.
377    pub fn epoch(&self, epoch_height: u64) -> Result<Option<EpochNumber>> {
378        if matches!(self.proof, FinalityProof::Assumption) {
379            Ok(None)
380        } else {
381            let height = self.leaves.last().context("empty leaf chain")?.height();
382            let epoch = epoch_from_block_number(height, epoch_height);
383            Ok(Some(epoch.into()))
384        }
385    }
386}
387
388#[cfg(test)]
389mod test {
390    use std::marker::PhantomData;
391
392    use hotshot_types::{data::ViewNumber, simple_vote::Vote2Data};
393    use pretty_assertions::assert_eq;
394    use versions::{DRB_AND_HEADER_UPGRADE_VERSION, Upgrade};
395
396    use super::*;
397    use crate::testing::{
398        AlwaysFalseQuorum, AlwaysTrueQuorum, LEAF_CHAIN_EPOCH_HEIGHT, LEGACY_VERSION,
399        VersionCheckQuorum, leaf_chain, leaf_chain_with_upgrade,
400    };
401
402    /// A new-protocol proof for `leaf` alone, by a `Certificate2` at `view` naming `epoch`.
403    fn cert2_proof(leaf: &LeafQueryData<SeqTypes>, view: u64, epoch: u64) -> LeafProof {
404        let data = Vote2Data {
405            leaf_commit: leaf.leaf().commit(),
406            epoch: EpochNumber::new(epoch),
407            block_number: leaf.leaf().height(),
408        };
409        // Unsigned: the quorums here accept any signature.
410        let commitment = data.commit();
411        let view = ViewNumber::new(view);
412        LeafProof {
413            leaves: vec![leaf.leaf().clone()],
414            proof: FinalityProof::NewProtocol {
415                cert2: Arc::new(Certificate2::new(data, commitment, view, None, PhantomData)),
416                leaf_qc: Box::new(leaf.qc().clone()),
417            },
418        }
419    }
420
421    /// A proof as a dishonest prover might send it: the requested leaf alone, with a 2-chain of
422    /// QCs the prover did not derive from the leaves that follow it.
423    fn hand_crafted_two_chain(
424        leaf: &LeafQueryData<SeqTypes>,
425        committing_qc: Certificate,
426        deciding_qc: Certificate,
427    ) -> LeafProof {
428        LeafProof {
429            leaves: vec![leaf.leaf().clone()],
430            proof: FinalityProof::HotStuff2 {
431                committing_qc: Arc::new(committing_qc),
432                deciding_qc: Arc::new(deciding_qc),
433            },
434        }
435    }
436
437    #[test_log::test(tokio::test(flavor = "multi_thread"))]
438    async fn test_hotstuff2() {
439        let mut proof = LeafProof::default();
440
441        // Insert some leaves, forming a chain.
442        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
443        assert!(!proof.push(leaves[0].clone()));
444        assert!(!proof.push(leaves[1].clone()));
445        assert!(proof.push(leaves[2].clone()));
446        assert_eq!(
447            proof
448                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
449                    LEAF_CHAIN_EPOCH_HEIGHT
450                )))
451                .await
452                .unwrap(),
453            leaves[0]
454        );
455    }
456
457    #[test_log::test(tokio::test(flavor = "multi_thread"))]
458    async fn test_invalid_qc() {
459        let mut proof = LeafProof::default();
460
461        // Insert some leaves, forming a chain.
462        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
463        assert!(!proof.push(leaves[0].clone()));
464        assert!(!proof.push(leaves[1].clone()));
465        assert!(proof.push(leaves[2].clone()));
466
467        // The proof is otherwise valid...
468        assert_eq!(
469            proof
470                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
471                    LEAF_CHAIN_EPOCH_HEIGHT
472                )))
473                .await
474                .unwrap(),
475            leaves[0]
476        );
477        // ...but fails if the signatures are not valid.
478        proof
479            .verify(LeafProofHint::Quorum(&AlwaysFalseQuorum::new(
480                LEAF_CHAIN_EPOCH_HEIGHT,
481            )))
482            .await
483            .unwrap_err();
484    }
485
486    #[test_log::test(tokio::test(flavor = "multi_thread"))]
487    async fn test_assumption() {
488        let mut proof = LeafProof::default();
489
490        // Insert a single leaf. We will not be able to provide proofs ending in a leaf chain, but
491        // we can return a leaf if the leaf after it is already known to be finalized.
492        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
493        assert!(!proof.push(leaves[0].clone()));
494        assert_eq!(
495            proof
496                .verify(LeafProofHint::assumption(leaves[1].leaf()))
497                .await
498                .unwrap(),
499            leaves[0]
500        );
501    }
502
503    #[test_log::test(tokio::test(flavor = "multi_thread"))]
504    async fn test_no_chain() {
505        let mut proof = LeafProof::default();
506
507        // Insert multiple leaves that don't chain. We will not be able to prove these are
508        // finalized.
509        let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
510        assert!(!proof.push(leaves[0].clone()));
511        assert!(!proof.push(leaves[2].clone()));
512
513        // Even if we start from a finalized leave that extends one of the leaves we do have (4,
514        // extends 3) we fail to generate a proof because we can't generate a chain from the
515        // requested leaf (1) to the finalized leaf (4), since leaf 2 is missing.
516        proof
517            .verify(LeafProofHint::assumption(leaves[3].leaf()))
518            .await
519            .unwrap_err();
520    }
521
522    #[test_log::test(tokio::test(flavor = "multi_thread"))]
523    async fn test_new_protocol_two_chain_rejected() {
524        let leaves = leaf_chain(1..=3, NEW_PROTOCOL_VERSION).await;
525
526        // The prover never completes a HotStuff2 chain from new-protocol leaves.
527        let mut proof = LeafProof::default();
528        assert!(!proof.push(leaves[0].clone()));
529        assert!(!proof.push(leaves[1].clone()));
530        assert!(!proof.push(leaves[2].clone()));
531
532        // A hand-crafted 2-chain proof fails to verify.
533        let proof = hand_crafted_two_chain(
534            &leaves[0],
535            Certificate::for_parent(leaves[1].leaf()),
536            Certificate::for_parent(leaves[2].leaf()),
537        );
538        let err = proof
539            .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
540                LEAF_CHAIN_EPOCH_HEIGHT,
541            )))
542            .await
543            .unwrap_err();
544        assert!(
545            err.to_string()
546                .contains("HotStuff2 finality proof used for new-protocol leaf"),
547            "{err:#}"
548        );
549    }
550
551    #[test_log::test(tokio::test(flavor = "multi_thread"))]
552    async fn test_cutover_two_chain_pre_cutover() {
553        // Upgrade at height 3: both QCs predate the cutover, so the 2-chain is valid even though
554        // the first new-protocol leaf carries the deciding QC.
555        let leaves = leaf_chain_with_upgrade(
556            1..=3,
557            3,
558            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
559        )
560        .await;
561        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
562        assert_eq!(leaves[1].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
563        assert_eq!(leaves[2].header().version(), NEW_PROTOCOL_VERSION);
564
565        let mut proof = LeafProof::default();
566        assert!(!proof.push(leaves[0].clone()));
567        assert!(!proof.push(leaves[1].clone()));
568        assert!(proof.push(leaves[2].clone()));
569        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
570        assert_eq!(
571            proof
572                .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
573                    leaves.iter().map(|leaf| leaf.leaf().clone()),
574                    LEAF_CHAIN_EPOCH_HEIGHT
575                )))
576                .await
577                .unwrap(),
578            leaves[0]
579        );
580    }
581
582    #[test_log::test(tokio::test(flavor = "multi_thread"))]
583    async fn test_cutover_two_chain_spanning_rejected() {
584        // Upgrade at height 2: the deciding QC forms at the cutover, so the 2-chain must be
585        // rejected even though the proven leaf is legacy.
586        let leaves = leaf_chain_with_upgrade(
587            1..=3,
588            2,
589            Upgrade::new(DRB_AND_HEADER_UPGRADE_VERSION, NEW_PROTOCOL_VERSION),
590        )
591        .await;
592        assert_eq!(leaves[0].header().version(), DRB_AND_HEADER_UPGRADE_VERSION);
593        assert_eq!(leaves[1].header().version(), NEW_PROTOCOL_VERSION);
594
595        // The prover never completes a 2-chain whose deciding QC postdates the cutover.
596        let mut proof = LeafProof::default();
597        assert!(!proof.push(leaves[0].clone()));
598        assert!(!proof.push(leaves[1].clone()));
599        assert!(!proof.push(leaves[2].clone()));
600
601        // A hand-crafted spanning 2-chain fails to verify.
602        let proof = hand_crafted_two_chain(
603            &leaves[0],
604            Certificate::for_parent(leaves[1].leaf()),
605            Certificate::for_parent(leaves[2].leaf()),
606        );
607        assert!(matches!(proof.proof(), FinalityProof::HotStuff2 { .. }));
608        let err = proof
609            .verify(LeafProofHint::Quorum(&VersionCheckQuorum::new(
610                leaves.iter().map(|leaf| leaf.leaf().clone()),
611                LEAF_CHAIN_EPOCH_HEIGHT,
612            )))
613            .await
614            .unwrap_err();
615        assert!(
616            err.to_string()
617                .contains("HotStuff2 finality proof spans the new-protocol cutover"),
618            "{err:#}"
619        );
620    }
621
622    #[test_log::test(tokio::test(flavor = "multi_thread"))]
623    async fn test_legacy_hotstuff_three_chain() {
624        let mut proof = LeafProof::default();
625
626        // Insert some leaves, forming a chain.
627        let leaves = leaf_chain(1..=4, LEGACY_VERSION).await;
628        assert!(!proof.push(leaves[0].clone()));
629        assert!(!proof.push(leaves[1].clone()));
630        assert!(!proof.push(leaves[2].clone()));
631        assert!(proof.push(leaves[3].clone()));
632        assert_eq!(
633            proof
634                .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
635                    LEAF_CHAIN_EPOCH_HEIGHT
636                )))
637                .await
638                .unwrap(),
639            leaves[0]
640        );
641    }
642
643    #[test_log::test(tokio::test(flavor = "multi_thread"))]
644    async fn test_legacy_hotstuff_two_chain_only() {
645        let mut proof = LeafProof::default();
646
647        // Insert some leaves, forming a 2-chain but not the 3-chain required to decide in legacy
648        // HotStuff.
649        let leaves = leaf_chain(1..=3, LEGACY_VERSION).await;
650        assert!(!proof.push(leaves[0].clone()));
651        assert!(!proof.push(leaves[1].clone()));
652        assert!(!proof.push(leaves[2].clone()));
653        proof
654            .verify(LeafProofHint::Quorum(&AlwaysTrueQuorum::new(
655                LEAF_CHAIN_EPOCH_HEIGHT,
656            )))
657            .await
658            .unwrap_err();
659    }
660
661    #[test_log::test(tokio::test(flavor = "multi_thread"))]
662    async fn test_new_protocol_later_cert2_only_for_last_block_of_epoch() {
663        // Epochs of 3 blocks: height 3 is the last block of epoch 1.
664        let leaves = leaf_chain(1..=3, NEW_PROTOCOL_VERSION).await;
665        let quorum = AlwaysTrueQuorum::new(3);
666
667        // A re-vote commits the epoch's last block at a later view.
668        let last = &leaves[2];
669        let revote = cert2_proof(last, *last.leaf().view_number() + 2, 1);
670        let verified = revote.verify(LeafProofHint::Quorum(&quorum)).await.unwrap();
671        assert_eq!(verified.leaf(), last.leaf());
672
673        // No other block is committed at a later view.
674        let middle = &leaves[1];
675        let proof = cert2_proof(middle, *middle.leaf().view_number() + 2, 1);
676        let err = proof
677            .verify(LeafProofHint::Quorum(&quorum))
678            .await
679            .unwrap_err();
680        assert!(
681            err.to_string().contains("not the last block of its epoch"),
682            "{err:#}"
683        );
684    }
685}