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
15pub 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 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 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 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 #[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 #[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}