Skip to main content

hotshot_new_protocol/
utils.rs

1use anyhow::{anyhow, ensure};
2use committable::Committable;
3use hotshot_types::{
4    data::{EpochNumber, Leaf2, ViewNumber},
5    epoch_membership::EpochMembershipCoordinator,
6    message::UpgradeLock,
7    stake_table::StakeTableEntries,
8    traits::node_implementation::NodeType,
9    utils::{epoch_from_block_number, is_last_block},
10    vote::{Certificate, HasViewNumber},
11};
12
13use crate::message::Certificate2;
14
15/// Verify that a leaf is finalized by a new-protocol Certificate2.
16///
17/// `cert2` directly commits the newest leaf in `leaf_chain`, at the leaf's view
18/// or, for an epoch's last block committed by a re-vote, a later one. By the indirect
19/// commit rule, every ancestor of that leaf is finalized as well. This verifier
20/// validates `cert2`, then walks backward through the certified leaf's parent
21/// links until it finds `expected_height`.
22///
23/// View numbers are allowed to skip after timeouts, so the input may contain
24/// leaves that are not on the certified ancestry path. Those leaves are ignored;
25/// every accepted step must match the current leaf's justify QC, parent
26/// commitment, and block height.
27pub async fn verify_new_protocol_leaf_chain<T: NodeType>(
28    mut leaf_chain: Vec<Leaf2<T>>,
29    coordinator: &EpochMembershipCoordinator<T>,
30    expected_height: u64,
31    upgrade_lock: &UpgradeLock<T>,
32    cert2: Certificate2<T>,
33) -> anyhow::Result<Leaf2<T>> {
34    leaf_chain.sort_by_key(|l| l.view_number());
35    leaf_chain.reverse();
36
37    ensure!(!leaf_chain.is_empty(), "empty leaf chain");
38    let newest = &leaf_chain[0];
39
40    ensure!(
41        cert2.view_number() > ViewNumber::genesis(),
42        "cert2 must not be the genesis view"
43    );
44    // Verify cert2 against the leaf chain before spawning a catchup request for its stake table.
45    ensure!(
46        cert2.data.leaf_commit == newest.commit(),
47        "cert2 does not match the newest leaf in the chain"
48    );
49    ensure!(
50        cert2.data.block_number == newest.height(),
51        "cert2 block number does not match the newest leaf"
52    );
53    // A re-vote certifies an epoch's last block again at a later view, so its
54    // Cert2 is after the block's own view. No other block is voted on again.
55    ensure!(
56        cert2.view_number() >= newest.view_number(),
57        "cert2 view is before the newest leaf's"
58    );
59    ensure!(
60        cert2.view_number() == newest.view_number()
61            || is_last_block(newest.height(), *coordinator.epoch_height()),
62        "cert2 view is after the newest leaf's, which is not the last block of its epoch"
63    );
64    let epoch = EpochNumber::new(epoch_from_block_number(
65        cert2.data.block_number,
66        *coordinator.epoch_height(),
67    ));
68    ensure!(
69        cert2.data.epoch == epoch,
70        "cert2 epoch {} does not match epoch {epoch} derived from its block number {}",
71        cert2.data.epoch,
72        cert2.data.block_number
73    );
74
75    let membership = coordinator
76        .stake_table_for_epoch(Some(epoch))
77        .map_err(|err| anyhow!("no stake table available for epoch {epoch}: {err:?}"))?;
78    let entries = StakeTableEntries::<T>::from_iter(membership.stake_table()).0;
79    cert2.is_valid_cert(&entries, membership.success_threshold(), upgrade_lock)?;
80
81    if newest.height() == expected_height {
82        return Ok(newest.clone());
83    }
84
85    let mut current = newest;
86    for leaf in leaf_chain[1..].iter() {
87        let justify_qc = current.justify_qc();
88        if justify_qc.view_number() != leaf.view_number()
89            || justify_qc.data().leaf_commit != leaf.commit()
90        {
91            tracing::warn!(
92                view = ?leaf.view_number(),
93                expected_view = ?justify_qc.view_number(),
94                "leaf is off the leafchain path; expected only after a view timeout"
95            );
96            continue;
97        }
98        ensure!(
99            current.parent_commitment() == leaf.commit(),
100            "current leaf parent commitment does not match parent leaf"
101        );
102        ensure!(
103            leaf.height().checked_add(1) == Some(current.height()),
104            "leaf heights do not chain"
105        );
106        let qc_epoch = EpochNumber::new(epoch_from_block_number(
107            leaf.height(),
108            *coordinator.epoch_height(),
109        ));
110        ensure!(
111            justify_qc.data().epoch == Some(qc_epoch),
112            "justify QC claims epoch {:?} but certifies the leaf at height {} in epoch {qc_epoch}",
113            justify_qc.data().epoch,
114            leaf.height()
115        );
116        if let Some(block_number) = justify_qc.data().block_number {
117            ensure!(
118                block_number == leaf.height(),
119                "justify QC claims block number {block_number} but certifies the leaf at height {}",
120                leaf.height()
121            );
122        }
123        let membership = coordinator
124            .stake_table_for_epoch(Some(qc_epoch))
125            .map_err(|err| anyhow!("no stake table available for epoch {qc_epoch}: {err:?}"))?;
126        let entries = StakeTableEntries::<T>::from_iter(membership.stake_table()).0;
127        justify_qc.is_valid_cert(&entries, membership.success_threshold(), upgrade_lock)?;
128        if leaf.height() == expected_height {
129            return Ok(leaf.clone());
130        }
131        current = leaf;
132    }
133
134    Err(anyhow!(
135        "expected height was not found in the cert2-finalized chain"
136    ))
137}
138
139#[cfg(test)]
140mod test {
141    use alloy::primitives::U256;
142    use bitvec::vec::BitVec;
143    use committable::Commitment;
144    use hotshot_example_types::{
145        block_types::{TestBlockHeader, TestMetadata},
146        membership::{
147            TestableMembership, static_committee::StaticStakeTable,
148            strict_membership::StrictMembership,
149        },
150        node_types::TestTypes,
151        storage_types::TestStorage,
152    };
153    use hotshot_types::{
154        PeerConfig,
155        data::{QuorumProposal2, QuorumProposalWrapper, VidCommitment},
156        signature_key::{BLSPubKey, SchnorrPubKey},
157        simple_certificate::QuorumCertificate2,
158        simple_vote::{QuorumData2, VersionedVoteData, Vote2Data},
159        stake_table::{StakeTableEntry, supermajority_threshold},
160        traits::signature_key::SignatureKey,
161        utils::{BuilderCommitment, verify_leaf_chain},
162    };
163    use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION, Upgrade};
164
165    use super::*;
166
167    const EPOCH_HEIGHT: u64 = 10;
168
169    type PrivKey = <BLSPubKey as SignatureKey>::PrivateKey;
170    type Quorum = (Vec<PrivKey>, Vec<PeerConfig<TestTypes>>);
171
172    fn quorum(indexes: std::ops::Range<u64>) -> Quorum {
173        indexes
174            .map(|i| {
175                let (stake_key, priv_key) =
176                    BLSPubKey::generated_from_seed_indexed(Default::default(), i);
177                (
178                    priv_key,
179                    PeerConfig::<TestTypes> {
180                        stake_table_entry: StakeTableEntry {
181                            stake_key,
182                            stake_amount: U256::from(1),
183                        },
184                        state_ver_key: Default::default(),
185                        connect_info: None,
186                    },
187                )
188            })
189            .unzip()
190    }
191
192    /// A coordinator over a membership whose quorum is `quorum1` in epoch 1
193    /// and `quorum2` in epoch 2; no later epoch has a stake table.
194    fn coordinator(
195        (_, peers1): &Quorum,
196        (_, peers2): &Quorum,
197    ) -> EpochMembershipCoordinator<TestTypes> {
198        let membership =
199            StrictMembership::<TestTypes, StaticStakeTable<BLSPubKey, SchnorrPubKey>>::new(
200                peers1.clone(),
201                peers1.clone(),
202                peers1[0].stake_table_entry.stake_key,
203                EPOCH_HEIGHT,
204                EpochNumber::new(1),
205            );
206        membership.add_quorum_committee(EpochNumber::new(2), peers2.clone());
207        EpochMembershipCoordinator::new(
208            membership,
209            EPOCH_HEIGHT,
210            &TestStorage::<TestTypes>::default(),
211        )
212    }
213
214    fn sign((keys, peers): &Quorum, msg: &[u8]) -> <BLSPubKey as SignatureKey>::QcType {
215        let entries: Vec<_> = peers.iter().map(|p| p.stake_table_entry.clone()).collect();
216        let total: U256 = entries.iter().map(|e| e.stake_amount).sum();
217        let pp = BLSPubKey::public_parameter(&entries, supermajority_threshold(total));
218        let sigs: Vec<_> = keys
219            .iter()
220            .map(|key| BLSPubKey::sign(key, msg).unwrap())
221            .collect();
222        BLSPubKey::assemble(
223            &pp,
224            &std::iter::repeat_n(true, keys.len()).collect::<BitVec>(),
225            &sigs,
226        )
227    }
228
229    fn signed_qc(
230        data: QuorumData2<TestTypes>,
231        view: u64,
232        quorum: &Quorum,
233        upgrade_lock: &UpgradeLock<TestTypes>,
234    ) -> QuorumCertificate2<TestTypes> {
235        let view = ViewNumber::new(view);
236        let commit = VersionedVoteData::new_infallible(data, view, upgrade_lock).commit();
237        QuorumCertificate2::create_signed_certificate(
238            commit,
239            data,
240            sign(quorum, commit.as_ref()),
241            view,
242        )
243    }
244
245    fn signed_cert2(
246        data: Vote2Data<TestTypes>,
247        view: u64,
248        quorum: &Quorum,
249        upgrade_lock: &UpgradeLock<TestTypes>,
250    ) -> Certificate2<TestTypes> {
251        let view = ViewNumber::new(view);
252        let commit = VersionedVoteData::new_infallible(data.clone(), view, upgrade_lock).commit();
253        Certificate2::create_signed_certificate(commit, data, sign(quorum, commit.as_ref()), view)
254    }
255
256    fn make_leaf(
257        height: u64,
258        view: u64,
259        epoch: u64,
260        justify_qc: QuorumCertificate2<TestTypes>,
261        version: vbs::version::Version,
262    ) -> Leaf2<TestTypes> {
263        Leaf2::from_quorum_proposal(&QuorumProposalWrapper {
264            proposal: QuorumProposal2 {
265                block_header: TestBlockHeader {
266                    block_number: height,
267                    payload_commitment: VidCommitment::default(),
268                    builder_commitment: BuilderCommitment::from_bytes([]),
269                    metadata: TestMetadata {
270                        num_transactions: 0,
271                    },
272                    timestamp: 0,
273                    timestamp_millis: 0,
274                    random: 0,
275                    version,
276                },
277                view_number: ViewNumber::new(view),
278                epoch: Some(EpochNumber::new(epoch)),
279                justify_qc,
280                next_epoch_justify_qc: None,
281                upgrade_certificate: None,
282                view_change_evidence: None,
283                next_drb_result: None,
284                state_cert: None,
285            },
286        })
287    }
288
289    /// A legacy 3-chain deciding the last block of `epoch` (epoch height 10), where consecutive
290    /// epochs have disjoint quorums. The QC on the first block of `epoch + 1` is signed by
291    /// `deciding_quorum`; every other QC is signed by `epoch_quorum`.
292    fn boundary_3_chain(
293        epoch: u64,
294        epoch_quorum: &Quorum,
295        deciding_quorum: &Quorum,
296        upgrade_lock: &UpgradeLock<TestTypes>,
297    ) -> Vec<Leaf2<TestTypes>> {
298        let last = epoch * EPOCH_HEIGHT;
299        let parent_qc = signed_qc(
300            QuorumData2 {
301                leaf_commit: Commitment::from_raw([9; 32]),
302                epoch: Some(EpochNumber::new(epoch)),
303                block_number: Some(last - 1),
304            },
305            last - 1,
306            epoch_quorum,
307            upgrade_lock,
308        );
309        let last_leaf = make_leaf(last, last, epoch, parent_qc, EPOCH_VERSION);
310        let last_qc = signed_qc(
311            QuorumData2 {
312                leaf_commit: Committable::commit(&last_leaf),
313                epoch: Some(EpochNumber::new(epoch)),
314                block_number: Some(last),
315            },
316            last,
317            epoch_quorum,
318            upgrade_lock,
319        );
320        let boundary_leaf = make_leaf(last + 1, last + 1, epoch + 1, last_qc, EPOCH_VERSION);
321        let boundary_qc = signed_qc(
322            QuorumData2 {
323                leaf_commit: Committable::commit(&boundary_leaf),
324                epoch: Some(EpochNumber::new(epoch + 1)),
325                block_number: Some(last + 1),
326            },
327            last + 1,
328            deciding_quorum,
329            upgrade_lock,
330        );
331        let deciding_leaf = make_leaf(last + 2, last + 2, epoch + 1, boundary_qc, EPOCH_VERSION);
332        vec![last_leaf, boundary_leaf, deciding_leaf]
333    }
334
335    /// A self-consistent 3-chain starting at `first_height`, entirely within `epoch`, with
336    /// every QC signed by `epoch_quorum`.
337    fn same_epoch_3_chain(
338        first_height: u64,
339        epoch: u64,
340        epoch_quorum: &Quorum,
341        upgrade_lock: &UpgradeLock<TestTypes>,
342    ) -> Vec<Leaf2<TestTypes>> {
343        legacy_3_chain(
344            first_height,
345            epoch,
346            Some(EpochNumber::new(epoch)),
347            epoch_quorum,
348            upgrade_lock,
349        )
350    }
351
352    /// Like [`same_epoch_3_chain`], but every QC claims `qc_epoch`, as the QCs formed before
353    /// epochs were enabled claim none.
354    fn legacy_3_chain(
355        first_height: u64,
356        epoch: u64,
357        qc_epoch: Option<EpochNumber>,
358        epoch_quorum: &Quorum,
359        upgrade_lock: &UpgradeLock<TestTypes>,
360    ) -> Vec<Leaf2<TestTypes>> {
361        let parent_qc = signed_qc(
362            QuorumData2 {
363                leaf_commit: Commitment::from_raw([9; 32]),
364                epoch: qc_epoch,
365                block_number: Some(first_height - 1),
366            },
367            first_height - 1,
368            epoch_quorum,
369            upgrade_lock,
370        );
371        let mut chain = vec![make_leaf(
372            first_height,
373            first_height,
374            epoch,
375            parent_qc,
376            EPOCH_VERSION,
377        )];
378        for height in first_height + 1..first_height + 3 {
379            let qc = signed_qc(
380                QuorumData2 {
381                    leaf_commit: Committable::commit(chain.last().unwrap()),
382                    epoch: qc_epoch,
383                    block_number: Some(height - 1),
384                },
385                height - 1,
386                epoch_quorum,
387                upgrade_lock,
388            );
389            chain.push(make_leaf(height, height, epoch, qc, EPOCH_VERSION));
390        }
391        chain
392    }
393
394    /// A 3-chain deciding the last leaf of an epoch contains a QC signed by the next epoch's
395    /// quorum; it must be verified against that quorum's stake table.
396    #[tokio::test(flavor = "multi_thread")]
397    async fn test_verify_leaf_chain_across_epoch_boundary() {
398        let quorum1 = quorum(0..5);
399        let quorum2 = quorum(5..10);
400        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
401
402        let chain = boundary_3_chain(1, &quorum1, &quorum2, &upgrade_lock);
403        let expected = Committable::commit(&chain[0]);
404
405        let coordinator = coordinator(&quorum1, &quorum2);
406        let leaf = verify_leaf_chain(chain, &coordinator, 10, &upgrade_lock)
407            .await
408            .unwrap();
409        assert_eq!(Committable::commit(&leaf), expected);
410    }
411
412    /// A QC claiming epoch 2 in its signed payload but signed by epoch 1's quorum must fail
413    /// verification against epoch 2's stake table.
414    #[tokio::test(flavor = "multi_thread")]
415    async fn test_verify_leaf_chain_boundary_qc_wrong_quorum() {
416        let quorum1 = quorum(0..5);
417        let quorum2 = quorum(5..10);
418        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
419
420        let chain = boundary_3_chain(1, &quorum1, &quorum1, &upgrade_lock);
421
422        let coordinator = coordinator(&quorum1, &quorum2);
423        verify_leaf_chain(chain, &coordinator, 10, &upgrade_lock)
424            .await
425            .unwrap_err();
426    }
427
428    /// A fully self-consistent chain deciding a height in epoch 2 whose QCs claim — and are
429    /// genuinely signed by — epoch 1's quorum must be rejected: the signed epoch only binds a
430    /// QC to the claimed epoch's quorum, so accepting any claimed epoch would let a stale
431    /// quorum forge chains for later heights.
432    #[tokio::test(flavor = "multi_thread")]
433    async fn test_verify_leaf_chain_rejects_stale_epoch_quorum() {
434        let quorum1 = quorum(0..5);
435        let quorum2 = quorum(5..10);
436        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
437
438        let chain = same_epoch_3_chain(20, 1, &quorum1, &upgrade_lock);
439
440        let coordinator = coordinator(&quorum1, &quorum2);
441        let err = verify_leaf_chain(chain, &coordinator, 20, &upgrade_lock)
442            .await
443            .unwrap_err();
444        assert!(err.to_string().contains("claims epoch"), "{err:#}");
445    }
446
447    /// A boundary chain whose deciding QC belongs to an epoch with no available stake table
448    /// must fail verification instead of falling back to another epoch's table.
449    #[tokio::test(flavor = "multi_thread")]
450    async fn test_verify_leaf_chain_missing_next_epoch_stake_table() {
451        let quorum1 = quorum(0..5);
452        let quorum2 = quorum(5..10);
453        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
454
455        let chain = boundary_3_chain(2, &quorum2, &quorum2, &upgrade_lock);
456
457        let coordinator = coordinator(&quorum1, &quorum2);
458        let err = verify_leaf_chain(chain, &coordinator, 20, &upgrade_lock)
459            .await
460            .unwrap_err();
461        assert!(
462            err.to_string().contains("no stake table available"),
463            "{err:#}"
464        );
465    }
466
467    /// QCs formed before epochs were enabled claim no epoch. Up to the first epoch they are
468    /// verified against the genesis stake table, which the first epoch runs on as well.
469    #[tokio::test(flavor = "multi_thread")]
470    async fn test_verify_leaf_chain_accepts_pre_epoch_qcs() {
471        let quorum1 = quorum(0..5);
472        let quorum2 = quorum(5..10);
473        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
474
475        let chain = legacy_3_chain(3, 1, None, &quorum1, &upgrade_lock);
476        let expected = Committable::commit(&chain[0]);
477
478        let coordinator = coordinator(&quorum1, &quorum2);
479        let leaf = verify_leaf_chain(chain, &coordinator, 3, &upgrade_lock)
480            .await
481            .unwrap();
482        assert_eq!(Committable::commit(&leaf), expected);
483    }
484
485    /// A QC claiming no epoch is still checked against the genesis quorum's signatures.
486    #[tokio::test(flavor = "multi_thread")]
487    async fn test_verify_leaf_chain_pre_epoch_qc_wrong_quorum() {
488        let quorum1 = quorum(0..5);
489        let quorum2 = quorum(5..10);
490        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
491
492        let chain = legacy_3_chain(3, 1, None, &quorum2, &upgrade_lock);
493
494        let coordinator = coordinator(&quorum1, &quorum2);
495        verify_leaf_chain(chain, &coordinator, 3, &upgrade_lock)
496            .await
497            .unwrap_err();
498    }
499
500    /// Past the first epoch every QC must claim its epoch: a QC claiming none, genuinely
501    /// signed by the genesis quorum, must not certify a height the epoch 2 quorum owns.
502    #[tokio::test(flavor = "multi_thread")]
503    async fn test_verify_leaf_chain_rejects_pre_epoch_qcs_after_first_epoch() {
504        let quorum1 = quorum(0..5);
505        let quorum2 = quorum(5..10);
506        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(EPOCH_VERSION));
507
508        let chain = legacy_3_chain(13, 2, None, &quorum1, &upgrade_lock);
509
510        let coordinator = coordinator(&quorum1, &quorum2);
511        let err = verify_leaf_chain(chain, &coordinator, 13, &upgrade_lock)
512            .await
513            .unwrap_err();
514        assert!(err.to_string().contains("claims no epoch"), "{err:#}");
515    }
516
517    /// A new-protocol leaf range deciding the last block of `epoch`, finalized by a cert2 on
518    /// the first block of `epoch + 1`, signed by `cert2_quorum`.
519    fn boundary_cert2_chain(
520        epoch: u64,
521        epoch_quorum: &Quorum,
522        cert2_quorum: &Quorum,
523        upgrade_lock: &UpgradeLock<TestTypes>,
524    ) -> (Vec<Leaf2<TestTypes>>, Certificate2<TestTypes>) {
525        let last = epoch * EPOCH_HEIGHT;
526        let parent_qc = signed_qc(
527            QuorumData2 {
528                leaf_commit: Commitment::from_raw([9; 32]),
529                epoch: Some(EpochNumber::new(epoch)),
530                block_number: Some(last - 1),
531            },
532            last - 1,
533            epoch_quorum,
534            upgrade_lock,
535        );
536        let last_leaf = make_leaf(last, last, epoch, parent_qc, NEW_PROTOCOL_VERSION);
537        let last_qc = signed_qc(
538            QuorumData2 {
539                leaf_commit: Committable::commit(&last_leaf),
540                epoch: Some(EpochNumber::new(epoch)),
541                block_number: Some(last),
542            },
543            last,
544            epoch_quorum,
545            upgrade_lock,
546        );
547        let boundary_leaf = make_leaf(last + 1, last + 1, epoch + 1, last_qc, NEW_PROTOCOL_VERSION);
548        let cert2 = signed_cert2(
549            Vote2Data {
550                leaf_commit: Committable::commit(&boundary_leaf),
551                epoch: EpochNumber::new(epoch + 1),
552                block_number: last + 1,
553            },
554            last + 1,
555            cert2_quorum,
556            upgrade_lock,
557        );
558        (vec![last_leaf, boundary_leaf], cert2)
559    }
560
561    /// A cert2 finalizing the last leaf of an epoch is produced in the next epoch and must be
562    /// verified against the next epoch's stake table, while the boundary QC inside the chain is
563    /// verified against the previous epoch's.
564    #[tokio::test(flavor = "multi_thread")]
565    async fn test_verify_new_protocol_leaf_chain_across_epoch_boundary() {
566        let quorum1 = quorum(0..5);
567        let quorum2 = quorum(5..10);
568        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(NEW_PROTOCOL_VERSION));
569
570        let (chain, cert2) = boundary_cert2_chain(1, &quorum1, &quorum2, &upgrade_lock);
571        let expected = Committable::commit(&chain[0]);
572
573        let coordinator = coordinator(&quorum1, &quorum2);
574        let leaf = verify_new_protocol_leaf_chain(chain, &coordinator, 10, &upgrade_lock, cert2)
575            .await
576            .unwrap();
577        assert_eq!(Committable::commit(&leaf), expected);
578    }
579
580    /// A cert2 claiming epoch 2 in its signed payload but signed by epoch 1's quorum must fail
581    /// verification against epoch 2's stake table.
582    #[tokio::test(flavor = "multi_thread")]
583    async fn test_verify_new_protocol_leaf_chain_wrong_quorum() {
584        let quorum1 = quorum(0..5);
585        let quorum2 = quorum(5..10);
586        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(NEW_PROTOCOL_VERSION));
587
588        let (chain, cert2) = boundary_cert2_chain(1, &quorum1, &quorum1, &upgrade_lock);
589
590        let coordinator = coordinator(&quorum1, &quorum2);
591        verify_new_protocol_leaf_chain(chain, &coordinator, 10, &upgrade_lock, cert2)
592            .await
593            .unwrap_err();
594    }
595
596    /// A cert2 belonging to an epoch with no available stake table must fail verification
597    /// instead of falling back to another epoch's table.
598    #[tokio::test(flavor = "multi_thread")]
599    async fn test_verify_new_protocol_leaf_chain_missing_next_epoch_stake_table() {
600        let quorum1 = quorum(0..5);
601        let quorum2 = quorum(5..10);
602        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(NEW_PROTOCOL_VERSION));
603
604        let (chain, cert2) = boundary_cert2_chain(2, &quorum2, &quorum2, &upgrade_lock);
605
606        let coordinator = coordinator(&quorum1, &quorum2);
607        let err = verify_new_protocol_leaf_chain(chain, &coordinator, 20, &upgrade_lock, cert2)
608            .await
609            .unwrap_err();
610        assert!(
611            err.to_string().contains("no stake table available"),
612            "{err:#}"
613        );
614    }
615
616    /// The last leaf of `epoch` at its own view, with a parent QC signed by
617    /// `epoch_quorum`, and a cert2 over it at `cert2_view`.
618    fn last_leaf_with_cert2(
619        epoch: u64,
620        cert2_view: u64,
621        epoch_quorum: &Quorum,
622        upgrade_lock: &UpgradeLock<TestTypes>,
623    ) -> (Leaf2<TestTypes>, Certificate2<TestTypes>) {
624        let last = epoch * EPOCH_HEIGHT;
625        let parent_qc = signed_qc(
626            QuorumData2 {
627                leaf_commit: Commitment::from_raw([9; 32]),
628                epoch: Some(EpochNumber::new(epoch)),
629                block_number: Some(last - 1),
630            },
631            last - 1,
632            epoch_quorum,
633            upgrade_lock,
634        );
635        let leaf = make_leaf(last, last, epoch, parent_qc, NEW_PROTOCOL_VERSION);
636        let cert2 = signed_cert2(
637            Vote2Data {
638                leaf_commit: Committable::commit(&leaf),
639                epoch: EpochNumber::new(epoch),
640                block_number: last,
641            },
642            cert2_view,
643            epoch_quorum,
644            upgrade_lock,
645        );
646        (leaf, cert2)
647    }
648
649    /// An epoch's last block committed by a re-vote has its cert2 at a later view
650    /// than its own, and catch-up must still accept it.
651    #[tokio::test(flavor = "multi_thread")]
652    async fn test_verify_new_protocol_leaf_chain_revote_cert2() {
653        let quorum1 = quorum(0..5);
654        let quorum2 = quorum(5..10);
655        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(NEW_PROTOCOL_VERSION));
656
657        let (leaf, cert2) = last_leaf_with_cert2(1, EPOCH_HEIGHT + 2, &quorum1, &upgrade_lock);
658        let expected = Committable::commit(&leaf);
659
660        let coordinator = coordinator(&quorum1, &quorum2);
661        let verified = verify_new_protocol_leaf_chain(
662            vec![leaf],
663            &coordinator,
664            EPOCH_HEIGHT,
665            &upgrade_lock,
666            cert2,
667        )
668        .await
669        .unwrap();
670        assert_eq!(Committable::commit(&verified), expected);
671    }
672
673    /// Only an epoch's last block is voted on again, so a cert2 after the view of
674    /// any other block, or before the view of the block, is rejected.
675    #[tokio::test(flavor = "multi_thread")]
676    async fn test_verify_new_protocol_leaf_chain_cert2_view_mismatch() {
677        let quorum1 = quorum(0..5);
678        let quorum2 = quorum(5..10);
679        let upgrade_lock = UpgradeLock::<TestTypes>::new(Upgrade::trivial(NEW_PROTOCOL_VERSION));
680        let coordinator = coordinator(&quorum1, &quorum2);
681
682        let (chain, _) = boundary_cert2_chain(1, &quorum1, &quorum2, &upgrade_lock);
683        let first = &chain[1];
684        let later = signed_cert2(
685            Vote2Data {
686                leaf_commit: Committable::commit(first),
687                epoch: EpochNumber::new(2),
688                block_number: first.height(),
689            },
690            *first.view_number() + 1,
691            &quorum2,
692            &upgrade_lock,
693        );
694        let err = verify_new_protocol_leaf_chain(
695            vec![first.clone()],
696            &coordinator,
697            first.height(),
698            &upgrade_lock,
699            later,
700        )
701        .await
702        .unwrap_err();
703        assert!(err.to_string().contains("not the last block"), "{err:#}");
704
705        let (leaf, earlier) = last_leaf_with_cert2(1, EPOCH_HEIGHT - 1, &quorum1, &upgrade_lock);
706        let err = verify_new_protocol_leaf_chain(
707            vec![leaf],
708            &coordinator,
709            EPOCH_HEIGHT,
710            &upgrade_lock,
711            earlier,
712        )
713        .await
714        .unwrap_err();
715        assert!(
716            err.to_string().contains("before the newest leaf"),
717            "{err:#}"
718        );
719    }
720}