1use std::{future::Future, sync::Arc};
2
3use alloy::primitives::U256;
4use anyhow::{Context, Result, bail, ensure};
5use committable::Committable;
6use espresso_types::{Certificate2, Leaf2, PubKey, SeqTypes};
7use hotshot_types::{
8 data::ViewNumber,
9 epoch_membership::EpochMembership,
10 message::UpgradeLock,
11 simple_certificate::CertificatePair,
12 stake_table::{HSStakeTable, StakeTableEntries, StakeTableEntry, supermajority_threshold},
13 vote::{self, HasViewNumber},
14};
15use tracing::Instrument;
16use vbs::version::{StaticVersion, StaticVersionType, Version};
17use versions::{EPOCH_VERSION, MAX_SUPPORTED_VERSION, Upgrade, version};
18
19pub type Certificate = CertificatePair<SeqTypes>;
20
21pub trait Quorum: Sync {
22 fn verify(
24 &self,
25 cert: &Certificate,
26 version: Version,
27 ) -> impl Send + Future<Output = Result<()>> {
28 async move {
29 match (version.major, version.minor) {
30 (0, 1) => self.verify_static::<StaticVersion<0, 1>>(cert).await,
31 (0, 2) => self.verify_static::<StaticVersion<0, 2>>(cert).await,
32 (0, 3) => self.verify_static::<StaticVersion<0, 3>>(cert).await,
33 (0, 4) => self.verify_static::<StaticVersion<0, 4>>(cert).await,
34 (0, 5) => self.verify_static::<StaticVersion<0, 5>>(cert).await,
35 (0, 6) => self.verify_static::<StaticVersion<0, 6>>(cert).await,
36 (0, 7) => self.verify_static::<StaticVersion<0, 7>>(cert).await,
37 _ => {
38 const {
39 assert!(MAX_SUPPORTED_VERSION.major == 0);
40 assert!(MAX_SUPPORTED_VERSION.minor == 7);
41 }
42 bail!("unsupported version {version}");
43 },
44 }
45 }
46 }
47
48 fn verify_static<V: StaticVersionType + 'static>(
50 &self,
51 qc: &Certificate,
52 ) -> impl Send + Future<Output = Result<()>>;
53
54 fn verify_cert2(
56 &self,
57 cert2: &Certificate2<SeqTypes>,
58 version: Version,
59 ) -> impl Send + Future<Output = Result<()>> {
60 async move {
61 ensure!(
64 cert2.view_number() > ViewNumber::genesis(),
65 "cert2 must not be the genesis view"
66 );
67 match (version.major, version.minor) {
68 (0, 1) => self.verify_cert2_static::<StaticVersion<0, 1>>(cert2).await,
69 (0, 2) => self.verify_cert2_static::<StaticVersion<0, 2>>(cert2).await,
70 (0, 3) => self.verify_cert2_static::<StaticVersion<0, 3>>(cert2).await,
71 (0, 4) => self.verify_cert2_static::<StaticVersion<0, 4>>(cert2).await,
72 (0, 5) => self.verify_cert2_static::<StaticVersion<0, 5>>(cert2).await,
73 (0, 6) => self.verify_cert2_static::<StaticVersion<0, 6>>(cert2).await,
74 (0, 7) => self.verify_cert2_static::<StaticVersion<0, 7>>(cert2).await,
75 _ => {
76 const {
77 assert!(MAX_SUPPORTED_VERSION.major == 0);
78 assert!(MAX_SUPPORTED_VERSION.minor == 7);
79 }
80 bail!("unsupported version {version}");
81 },
82 }
83 }
84 }
85
86 fn verify_cert2_static<V: StaticVersionType + 'static>(
88 &self,
89 cert2: &Certificate2<SeqTypes>,
90 ) -> impl Send + Future<Output = Result<()>>;
91
92 fn verify_next_epoch(
94 &self,
95 cert: &Certificate,
96 version: Version,
97 ) -> impl Send + Future<Output = Result<()>> {
98 async move {
99 match (version.major, version.minor) {
100 (0, 1) => {
101 self.verify_next_epoch_static::<StaticVersion<0, 1>>(cert)
102 .await
103 },
104 (0, 2) => {
105 self.verify_next_epoch_static::<StaticVersion<0, 2>>(cert)
106 .await
107 },
108 (0, 3) => {
109 self.verify_next_epoch_static::<StaticVersion<0, 3>>(cert)
110 .await
111 },
112 (0, 4) => {
113 self.verify_next_epoch_static::<StaticVersion<0, 4>>(cert)
114 .await
115 },
116 (0, 5) => {
117 self.verify_next_epoch_static::<StaticVersion<0, 5>>(cert)
118 .await
119 },
120 (0, 6) => {
121 self.verify_next_epoch_static::<StaticVersion<0, 6>>(cert)
122 .await
123 },
124 (0, 7) => {
125 self.verify_next_epoch_static::<StaticVersion<0, 7>>(cert)
126 .await
127 },
128 _ => {
129 const {
130 assert!(MAX_SUPPORTED_VERSION.major == 0);
131 assert!(MAX_SUPPORTED_VERSION.minor == 7);
132 }
133 bail!("unsupported version {version}");
134 },
135 }
136 }
137 }
138
139 fn verify_next_epoch_static<V: StaticVersionType + 'static>(
146 &self,
147 qc: &Certificate,
148 ) -> impl Send + Future<Output = Result<()>> {
149 self.verify_static::<V>(qc)
150 }
151
152 fn epoch_height(&self) -> u64;
154
155 fn verify_qc_chain_and_get_version<'a>(
160 &self,
161 leaf: &Leaf2,
162 certs: impl Send + IntoIterator<Item = &'a Certificate, IntoIter: Send>,
163 ) -> impl Send + Future<Output = Result<ChainVersions>> {
164 let span = tracing::trace_span!(
165 "verify_qc_chain_and_get_version",
166 height = leaf.block_header().height()
167 );
168 async move {
169 let version = leaf.block_header().version();
177 let upgrade = leaf.upgrade_certificate();
182 ensure!(version <= MAX_SUPPORTED_VERSION);
186 if let Some(cert) = &upgrade {
187 ensure!(cert.data.new_version <= MAX_SUPPORTED_VERSION);
188 }
189 tracing::debug!(
190 %version,
191 ?leaf,
192 "verify QC chain for leaf"
193 );
194
195 let mut first = None;
197 let mut curr: Option<&Certificate> = None;
198 let mut max_cert_version = version;
199 for cert in certs {
200 tracing::trace!(?cert, "verify cert");
201
202 let cert_version = match &upgrade {
204 Some(upgrade) if cert.view_number() >= upgrade.data.new_version_first_view => {
205 tracing::debug!(?upgrade, view = ?cert.view_number(), "using upgraded version");
206 upgrade.data.new_version
207 },
208 _ => version,
209 };
210 if cert_version > max_cert_version {
211 max_cert_version = cert_version;
212 }
213
214 let committing = first.unwrap_or(cert);
220 let next_epoch = match (committing.epoch(), cert.epoch()) {
221 (None, _) | (_, None) => false,
224 (Some(leaf_epoch), Some(cert_epoch)) if cert_epoch == leaf_epoch => false,
225 (Some(leaf_epoch), Some(cert_epoch)) if cert_epoch == leaf_epoch + 1 => {
226 committing.next_epoch_qc().is_some()
232 },
233 (Some(leaf_epoch), Some(cert_epoch)) => {
234 bail!(
235 "certificate from epoch {cert_epoch} cannot justify a leaf in epoch \
236 {leaf_epoch}"
237 );
238 },
239 };
240
241 if next_epoch {
243 self.verify_next_epoch(cert, cert_version).await?;
244 } else {
245 self.verify(cert, cert_version).await?;
246 }
247
248 if let Some(prev) = curr {
250 ensure!(cert.view_number() == prev.view_number() + 1);
251 }
252 curr = Some(cert);
253
254 if first.is_none() {
256 first = Some(cert);
257 }
258 }
259
260 let first_qc = first.context("empty QC chain")?;
262 ensure!(first_qc.leaf_commit() == leaf.commit());
263
264 Ok(ChainVersions {
265 leaf: version,
266 max_cert: max_cert_version,
267 })
268 }
269 .instrument(span)
270 }
271}
272
273#[derive(Clone, Copy, Debug)]
275pub struct ChainVersions {
276 pub leaf: Version,
278
279 pub max_cert: Version,
283}
284
285#[derive(Clone, Debug, PartialEq, Eq)]
287pub struct StakeTable {
288 entries: Vec<StakeTableEntry<PubKey>>,
289 threshold: U256,
290}
291
292impl From<HSStakeTable<SeqTypes>> for StakeTable {
293 fn from(table: HSStakeTable<SeqTypes>) -> Self {
294 StakeTableEntries::from(table).into()
295 }
296}
297
298impl From<Vec<StakeTableEntry<PubKey>>> for StakeTable {
299 fn from(entries: Vec<StakeTableEntry<PubKey>>) -> Self {
300 StakeTableEntries(entries).into()
301 }
302}
303
304impl From<StakeTableEntries<SeqTypes>> for StakeTable {
305 fn from(entries: StakeTableEntries<SeqTypes>) -> Self {
306 Self::from_iter(entries.0)
307 }
308}
309
310impl From<StakeTable> for Vec<StakeTableEntry<PubKey>> {
311 fn from(stake_table: StakeTable) -> Self {
312 stake_table.entries
313 }
314}
315
316impl FromIterator<StakeTableEntry<PubKey>> for StakeTable {
317 fn from_iter<T: IntoIterator<Item = StakeTableEntry<PubKey>>>(entries: T) -> Self {
318 let mut total_stake = U256::ZERO;
319 let entries = entries
320 .into_iter()
321 .inspect(|entry| {
322 total_stake += entry.stake_amount;
323 })
324 .collect();
325 Self {
326 entries,
327 threshold: supermajority_threshold(total_stake),
328 }
329 }
330}
331
332impl StakeTable {
333 pub async fn from_membership(membership: &EpochMembership<SeqTypes>) -> Self {
335 HSStakeTable::from_iter(membership.stake_table()).into()
336 }
337
338 pub async fn verify_cert<V, T>(&self, cert: &impl vote::Certificate<SeqTypes, T>) -> Result<()>
340 where
341 V: StaticVersionType + 'static,
342 {
343 let upgrade = Upgrade::trivial(version(V::MAJOR, V::MINOR));
344 cert.is_valid_cert(&self.entries, self.threshold, &UpgradeLock::new(upgrade))
345 .context("invalid threshold signature")
346 }
347}
348
349pub trait StakeTablePair {
355 fn stake_table(&self) -> impl Send + Future<Output = Result<Arc<StakeTable>>>;
357
358 fn next_epoch_stake_table(&self) -> impl Send + Future<Output = Result<Arc<StakeTable>>>;
360}
361
362impl StakeTablePair for EpochMembership<SeqTypes> {
363 async fn stake_table(&self) -> Result<Arc<StakeTable>> {
364 Ok(Arc::new(StakeTable::from_membership(self).await))
365 }
366
367 async fn next_epoch_stake_table(&self) -> Result<Arc<StakeTable>> {
368 let membership = self.next_epoch_stake_table()?;
369 Ok(Arc::new(StakeTable::from_membership(&membership).await))
370 }
371}
372
373impl StakeTablePair for (Arc<StakeTable>, Arc<StakeTable>) {
374 async fn stake_table(&self) -> Result<Arc<StakeTable>> {
375 Ok(self.0.clone())
376 }
377
378 async fn next_epoch_stake_table(&self) -> Result<Arc<StakeTable>> {
379 Ok(self.1.clone())
380 }
381}
382
383#[derive(Clone, Debug)]
385pub struct StakeTableQuorum<T> {
386 membership: T,
387 epoch_height: u64,
388}
389
390impl<T> StakeTableQuorum<T> {
391 pub fn new(membership: T, epoch_height: u64) -> Self {
393 Self {
394 membership,
395 epoch_height,
396 }
397 }
398}
399
400impl<T> Quorum for StakeTableQuorum<T>
401where
402 T: StakeTablePair + Sync,
403{
404 async fn verify_static<V: StaticVersionType + 'static>(
405 &self,
406 cert: &Certificate,
407 ) -> Result<()> {
408 let stake_table = self.membership.stake_table().await?;
409 stake_table
410 .verify_cert::<V, _>(cert.qc())
411 .await
412 .context("verifying QC")?;
413
414 if version(V::MAJOR, V::MINOR) >= EPOCH_VERSION {
415 match cert.verify_next_epoch_qc(self.epoch_height)? {
421 Some(next_epoch_qc) => {
422 let stake_table = self.membership.next_epoch_stake_table().await?;
423 stake_table
424 .verify_cert::<V, _>(next_epoch_qc)
425 .await
426 .context("verifying next epoch QC")?;
427 },
428 None => ensure!(
429 cert.next_epoch_qc().is_none(),
430 "certificate carries a next-epoch QC but is not an epoch transition"
431 ),
432 }
433 }
434
435 Ok(())
436 }
437
438 async fn verify_cert2_static<V: StaticVersionType + 'static>(
439 &self,
440 cert2: &Certificate2<SeqTypes>,
441 ) -> Result<()> {
442 let stake_table = self.membership.stake_table().await?;
443 stake_table
444 .verify_cert::<V, _>(cert2)
445 .await
446 .context("verifying cert2")
447 }
448
449 async fn verify_next_epoch_static<V: StaticVersionType + 'static>(
450 &self,
451 cert: &Certificate,
452 ) -> Result<()> {
453 let stake_table = self.membership.next_epoch_stake_table().await?;
454 stake_table
455 .verify_cert::<V, _>(cert.qc())
456 .await
457 .context("verifying QC against next epoch quorum")?;
458
459 ensure!(
463 cert.next_epoch_qc().is_none(),
464 "unexpected next-epoch QC on a certificate from the next epoch"
465 );
466
467 Ok(())
468 }
469
470 fn epoch_height(&self) -> u64 {
471 self.epoch_height
472 }
473}
474
475#[cfg(test)]
476mod test {
477 use std::marker::PhantomData;
478
479 use bitvec::vec::BitVec;
480 use committable::Commitment;
481 use espresso_types::PrivKey;
482 use hotshot_query_service_types::availability::LeafQueryData;
483 use hotshot_types::{
484 data::{EpochNumber, ViewNumber},
485 simple_certificate::{NextEpochQuorumCertificate2, QuorumCertificate2},
486 simple_vote::{NextEpochQuorumData2, QuorumData2, VersionedVoteData, Vote2Data},
487 traits::signature_key::SignatureKey,
488 vote::Certificate as _,
489 };
490 use pretty_assertions::assert_eq;
491 use versions::NEW_PROTOCOL_VERSION;
492
493 use super::*;
494 use crate::testing::{
495 AlwaysFalseQuorum, AlwaysTrueQuorum, ENABLE_EPOCHS, EpochChangeQuorum,
496 LEAF_CHAIN_EPOCH_HEIGHT, LEGACY_VERSION, VersionCheckQuorum,
497 custom_epoch_change_leaf_chain, custom_leaf_chain_with_upgrade, epoch_change_leaf_chain,
498 leaf_chain, leaf_chain_with_upgrade, qc_chain_from_leaf_chain,
499 };
500
501 #[test_log::test(tokio::test(flavor = "multi_thread"))]
502 async fn test_valid_chain() {
503 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
504 let ChainVersions { leaf: version, .. } = AlwaysTrueQuorum::new(LEAF_CHAIN_EPOCH_HEIGHT)
505 .verify_qc_chain_and_get_version(
506 leaves[0].leaf(),
507 &qc_chain_from_leaf_chain(&leaves[1..]),
508 )
509 .await
510 .unwrap();
511 assert_eq!(version, leaves[0].header().version());
512 }
513
514 #[test_log::test(tokio::test(flavor = "multi_thread"))]
515 async fn test_wrong_leaf() {
516 let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
517 AlwaysTrueQuorum::new(LEAF_CHAIN_EPOCH_HEIGHT)
518 .verify_qc_chain_and_get_version(
519 leaves[2].leaf(),
520 &qc_chain_from_leaf_chain(&leaves[1..]),
521 )
522 .await
523 .unwrap_err();
524 }
525
526 #[test_log::test(tokio::test(flavor = "multi_thread"))]
527 async fn test_invalid_qc() {
528 let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
529 AlwaysFalseQuorum::new(LEAF_CHAIN_EPOCH_HEIGHT)
530 .verify_qc_chain_and_get_version(
531 leaves[0].leaf(),
532 &[Certificate::for_parent(leaves[1].leaf())],
533 )
534 .await
535 .unwrap_err();
536 }
537
538 #[test_log::test(tokio::test(flavor = "multi_thread"))]
539 async fn test_non_consecutive() {
540 let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
541 AlwaysTrueQuorum::new(LEAF_CHAIN_EPOCH_HEIGHT)
542 .verify_qc_chain_and_get_version(
543 leaves[0].leaf(),
544 &qc_chain_from_leaf_chain([&leaves[1], &leaves[3]]),
545 )
546 .await
547 .unwrap_err();
548 }
549
550 #[test_log::test(tokio::test(flavor = "multi_thread"))]
551 async fn test_upgrade() {
552 let leaves = leaf_chain_with_upgrade(1..=3, 2, ENABLE_EPOCHS).await;
553 let ChainVersions { leaf: version, .. } = VersionCheckQuorum::new(
554 leaves.iter().map(|leaf| leaf.leaf().clone()),
555 LEAF_CHAIN_EPOCH_HEIGHT,
556 )
557 .verify_qc_chain_and_get_version(leaves[0].leaf(), &qc_chain_from_leaf_chain(&leaves[1..]))
558 .await
559 .unwrap();
560 assert_eq!(version, leaves[0].header().version());
561 }
562
563 #[test_log::test(tokio::test(flavor = "multi_thread"))]
564 async fn test_illegal_upgrade() {
565 let leaves = custom_leaf_chain_with_upgrade(1..=3, 2, ENABLE_EPOCHS, |proposal| {
566 proposal.upgrade_certificate = None;
569 })
570 .await;
571 VersionCheckQuorum::new(
572 leaves.iter().map(|leaf| leaf.leaf().clone()),
573 LEAF_CHAIN_EPOCH_HEIGHT,
574 )
575 .verify_qc_chain_and_get_version(leaves[0].leaf(), &qc_chain_from_leaf_chain(&leaves[1..]))
576 .await
577 .unwrap_err();
578 }
579
580 #[test_log::test(tokio::test(flavor = "multi_thread"))]
581 async fn test_epoch_change() {
582 let leaves = epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION).await;
583 let ChainVersions { leaf: version, .. } = EpochChangeQuorum::new(5)
584 .verify_qc_chain_and_get_version(
585 leaves[0].leaf(),
586 &qc_chain_from_leaf_chain(&leaves[1..]),
587 )
588 .await
589 .unwrap();
590 assert_eq!(version, leaves[0].header().version());
591 }
592
593 #[test_log::test(tokio::test(flavor = "multi_thread"))]
594 async fn test_epoch_change_missing_eqc() {
595 let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
596 proposal.next_epoch_justify_qc = None;
598 })
599 .await;
600 EpochChangeQuorum::new(5)
601 .verify_qc_chain_and_get_version(
602 leaves[0].leaf(),
603 &qc_chain_from_leaf_chain(&leaves[1..]),
604 )
605 .await
606 .unwrap_err();
607 }
608
609 #[test_log::test(tokio::test(flavor = "multi_thread"))]
610 async fn test_epoch_change_inconsistent_eqc_view_number() {
611 let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
612 if let Some(next_epoch_justify_qc) = &mut proposal.next_epoch_justify_qc {
614 next_epoch_justify_qc.view_number += 1;
615 }
616 })
617 .await;
618 EpochChangeQuorum::new(5)
619 .verify_qc_chain_and_get_version(
620 leaves[0].leaf(),
621 &qc_chain_from_leaf_chain(&leaves[1..]),
622 )
623 .await
624 .unwrap_err();
625 }
626
627 #[test_log::test(tokio::test(flavor = "multi_thread"))]
628 async fn test_epoch_change_inconsistent_eqc_data() {
629 let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
630 if let Some(next_epoch_justify_qc) = &mut proposal.next_epoch_justify_qc {
632 *next_epoch_justify_qc.data.block_number.as_mut().unwrap() += 1;
633 }
634 })
635 .await;
636 EpochChangeQuorum::new(5)
637 .verify_qc_chain_and_get_version(
638 leaves[0].leaf(),
639 &qc_chain_from_leaf_chain(&leaves[1..]),
640 )
641 .await
642 .unwrap_err();
643 }
644
645 #[test_log::test(tokio::test(flavor = "multi_thread"))]
646 async fn test_epoch_change_absent_eqc_before_upgrade() {
647 let leaves = custom_epoch_change_leaf_chain(1..=5, 5, LEGACY_VERSION, |proposal| {
648 proposal.next_epoch_justify_qc = None;
650 })
651 .await;
652 let ChainVersions { leaf: version, .. } = EpochChangeQuorum::new(5)
653 .verify_qc_chain_and_get_version(
654 leaves[0].leaf(),
655 &qc_chain_from_leaf_chain(&leaves[1..]),
656 )
657 .await
658 .unwrap();
659 assert_eq!(version, leaves[0].header().version());
660 }
661
662 const BOUNDARY_EPOCH_HEIGHT: u64 = 10;
663
664 fn boundary_quorum(seeds: impl IntoIterator<Item = u64>) -> QuorumKeys {
665 seeds
666 .into_iter()
667 .map(|i| {
668 let (stake_key, priv_key) =
669 PubKey::generated_from_seed_indexed(Default::default(), i);
670 (
671 priv_key,
672 StakeTableEntry {
673 stake_key,
674 stake_amount: U256::from(1),
675 },
676 )
677 })
678 .unzip()
679 }
680
681 type QuorumKeys = (Vec<PrivKey>, Vec<StakeTableEntry<PubKey>>);
682
683 fn boundary_signature(
684 msg: &[u8],
685 (keys, entries): &QuorumKeys,
686 ) -> <PubKey as SignatureKey>::QcType {
687 let total = entries
688 .iter()
689 .fold(U256::ZERO, |acc, entry| acc + entry.stake_amount);
690 let pp = PubKey::public_parameter(entries, supermajority_threshold(total));
691 let sigs = keys
692 .iter()
693 .map(|key| PubKey::sign(key, msg).unwrap())
694 .collect::<Vec<_>>();
695 PubKey::assemble(
696 &pp,
697 &std::iter::repeat_n(true, keys.len()).collect::<BitVec>(),
698 &sigs,
699 )
700 }
701
702 fn boundary_signed_qc(
703 data: QuorumData2<SeqTypes>,
704 view: ViewNumber,
705 quorum: &QuorumKeys,
706 ) -> QuorumCertificate2<SeqTypes> {
707 let commit = VersionedVoteData::new_infallible(
708 data,
709 view,
710 &UpgradeLock::<SeqTypes>::new(Upgrade::trivial(EPOCH_VERSION)),
711 )
712 .commit();
713 let sig = boundary_signature(commit.as_ref(), quorum);
714 QuorumCertificate2::create_signed_certificate(commit, data, sig, view)
715 }
716
717 fn boundary_signed_next_epoch_qc(
718 data: QuorumData2<SeqTypes>,
719 view: ViewNumber,
720 quorum: &QuorumKeys,
721 ) -> NextEpochQuorumCertificate2<SeqTypes> {
722 let data: NextEpochQuorumData2<SeqTypes> = data.into();
723 let commit = VersionedVoteData::new_infallible(
724 data.clone(),
725 view,
726 &UpgradeLock::<SeqTypes>::new(Upgrade::trivial(EPOCH_VERSION)),
727 )
728 .commit();
729 let commit_bytes: [u8; 32] = commit.into();
730 let sig = boundary_signature(commit.as_ref(), quorum);
731 NextEpochQuorumCertificate2::new(
732 data,
733 Commitment::from_raw(commit_bytes),
734 view,
735 Some(sig),
736 Default::default(),
737 )
738 }
739
740 async fn epoch_boundary_fixture(
745 deciding_signed_by_next: bool,
746 ) -> (
747 Vec<LeafQueryData<SeqTypes>>,
748 Certificate,
749 Certificate,
750 StakeTableQuorum<(Arc<StakeTable>, Arc<StakeTable>)>,
751 ) {
752 let current = boundary_quorum(0..5);
753 let next = boundary_quorum(5..10);
754
755 let leaves = leaf_chain(9..=11, EPOCH_VERSION).await;
756
757 let committing_data = QuorumData2 {
759 leaf_commit: Committable::commit(leaves[1].leaf()),
760 epoch: Some(EpochNumber::new(1)),
761 block_number: Some(10),
762 };
763 let committing_qc = Certificate::new(
764 boundary_signed_qc(committing_data, ViewNumber::new(10), ¤t),
765 Some(boundary_signed_next_epoch_qc(
766 committing_data,
767 ViewNumber::new(10),
768 &next,
769 )),
770 );
771
772 let deciding_quorum = if deciding_signed_by_next {
773 &next
774 } else {
775 ¤t
776 };
777 let deciding_qc = Certificate::non_epoch_change(boundary_signed_qc(
778 QuorumData2 {
779 leaf_commit: Committable::commit(leaves[2].leaf()),
780 epoch: Some(EpochNumber::new(2)),
781 block_number: Some(11),
782 },
783 ViewNumber::new(11),
784 deciding_quorum,
785 ));
786
787 let quorum = StakeTableQuorum::new(
788 (
789 Arc::new(StakeTable::from(current.1)),
790 Arc::new(StakeTable::from(next.1)),
791 ),
792 BOUNDARY_EPOCH_HEIGHT,
793 );
794 (leaves, committing_qc, deciding_qc, quorum)
795 }
796
797 #[test_log::test(tokio::test(flavor = "multi_thread"))]
801 async fn test_epoch_boundary_quorum_change() {
802 let (leaves, committing_qc, deciding_qc, quorum) = epoch_boundary_fixture(true).await;
803 let ChainVersions { leaf: version, .. } = quorum
804 .verify_qc_chain_and_get_version(leaves[1].leaf(), [&committing_qc, &deciding_qc])
805 .await
806 .unwrap();
807 assert_eq!(version, leaves[1].header().version());
808 }
809
810 #[test_log::test(tokio::test(flavor = "multi_thread"))]
813 async fn test_epoch_boundary_deciding_qc_wrong_quorum() {
814 let (leaves, committing_qc, deciding_qc, quorum) = epoch_boundary_fixture(false).await;
815 quorum
816 .verify_qc_chain_and_get_version(leaves[1].leaf(), [&committing_qc, &deciding_qc])
817 .await
818 .unwrap_err();
819 }
820
821 async fn mid_epoch_forgery_fixture(
826 disguise_as_boundary: bool,
827 ) -> (
828 Vec<LeafQueryData<SeqTypes>>,
829 Certificate,
830 Certificate,
831 StakeTableQuorum<(Arc<StakeTable>, Arc<StakeTable>)>,
832 ) {
833 let current = boundary_quorum(0..5);
834 let next = boundary_quorum(5..10);
835
836 let leaves = leaf_chain(4..=6, EPOCH_VERSION).await;
838
839 let committing_data = QuorumData2 {
840 leaf_commit: Committable::commit(leaves[1].leaf()),
841 epoch: Some(EpochNumber::new(1)),
842 block_number: Some(5),
843 };
844 let committing_qc = Certificate::new(
845 boundary_signed_qc(committing_data, ViewNumber::new(5), ¤t),
846 disguise_as_boundary
847 .then(|| boundary_signed_next_epoch_qc(committing_data, ViewNumber::new(5), &next)),
848 );
849
850 let deciding_qc = Certificate::non_epoch_change(boundary_signed_qc(
851 QuorumData2 {
852 leaf_commit: Committable::commit(leaves[2].leaf()),
853 epoch: Some(EpochNumber::new(2)),
854 block_number: Some(6),
855 },
856 ViewNumber::new(6),
857 &next,
858 ));
859
860 let quorum = StakeTableQuorum::new(
861 (
862 Arc::new(StakeTable::from(current.1)),
863 Arc::new(StakeTable::from(next.1)),
864 ),
865 BOUNDARY_EPOCH_HEIGHT,
866 );
867 (leaves, committing_qc, deciding_qc, quorum)
868 }
869
870 #[test_log::test(tokio::test(flavor = "multi_thread"))]
874 async fn test_mid_epoch_next_epoch_forgery_rejected() {
875 let (leaves, committing_qc, deciding_qc, quorum) = mid_epoch_forgery_fixture(false).await;
876 quorum
877 .verify_qc_chain_and_get_version(leaves[1].leaf(), [&committing_qc, &deciding_qc])
878 .await
879 .unwrap_err();
880 }
881
882 #[test_log::test(tokio::test(flavor = "multi_thread"))]
885 async fn test_mid_epoch_next_epoch_forgery_with_fake_transition_rejected() {
886 let (leaves, committing_qc, deciding_qc, quorum) = mid_epoch_forgery_fixture(true).await;
887 quorum
888 .verify_qc_chain_and_get_version(leaves[1].leaf(), [&committing_qc, &deciding_qc])
889 .await
890 .unwrap_err();
891 }
892
893 #[test_log::test(tokio::test(flavor = "multi_thread"))]
894 async fn test_unsigned_genesis_view_cert2_rejected() {
895 let stake_table: StakeTable = (0..5)
896 .map(|i| StakeTableEntry {
897 stake_key: PubKey::generated_from_seed_indexed(Default::default(), i).0,
898 stake_amount: U256::from(1),
899 })
900 .collect();
901 let quorum =
902 StakeTableQuorum::new((Arc::new(stake_table.clone()), Arc::new(stake_table)), 10);
903
904 let data = Vote2Data {
905 leaf_commit: Commitment::from_raw([7; 32]),
906 epoch: EpochNumber::new(100),
907 block_number: 1000,
908 };
909 let cert2 = Certificate2::<SeqTypes>::new(
910 data.clone(),
911 data.commit(),
912 ViewNumber::genesis(),
913 None,
914 PhantomData,
915 );
916
917 let err = quorum
918 .verify_cert2(&cert2, NEW_PROTOCOL_VERSION)
919 .await
920 .unwrap_err();
921 assert!(
922 err.to_string().contains("genesis view"),
923 "unexpected error: {err:#}"
924 );
925 }
926}