Skip to main content

light_client/consensus/
quorum.rs

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    /// Check a threshold signature on a quorum certificate.
23    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    /// Same as [`verify`](Self::verify), but with the version as a type-level parameter.
49    fn verify_static<V: StaticVersionType + 'static>(
50        &self,
51        qc: &Certificate,
52    ) -> impl Send + Future<Output = Result<()>>;
53
54    /// Verify a new protocol Certificate2 threshold signature.
55    fn verify_cert2(
56        &self,
57        cert2: &Certificate2<SeqTypes>,
58        version: Version,
59    ) -> impl Send + Future<Output = Result<()>> {
60        async move {
61            // `is_valid_cert` accepts any genesis-view certificate without checking signatures,
62            // and no real Certificate2 is ever formed in the genesis view.
63            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    /// Same as [`verify_cert2`](Self::verify_cert2), but with the version as a type-level parameter.
87    fn verify_cert2_static<V: StaticVersionType + 'static>(
88        &self,
89        cert2: &Certificate2<SeqTypes>,
90    ) -> impl Send + Future<Output = Result<()>>;
91
92    /// Check a threshold signature on a certificate signed by the epoch after this quorum's.
93    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    /// Same as [`verify_next_epoch`](Self::verify_next_epoch), but with the version as a
140    /// type-level parameter.
141    ///
142    /// The default delegates to [`verify_static`](Self::verify_static), which is only appropriate
143    /// for quorums that do not distinguish epochs (such as test mocks). Implementations backed by
144    /// per-epoch stake tables must override this to verify against the next epoch's quorum.
145    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    /// The number of blocks in an epoch.
153    fn epoch_height(&self) -> u64;
154
155    /// Verify that QCs are signed, form a chain starting from `leaf`, with a particular protocol
156    /// version.
157    ///
158    /// This check forms the bulk of the commit rule for both HotStuff and HotStuff2.
159    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            // Get the protocol version that the leaf claims it is using. At this point, the leaf is
170            // not trusted, but we will verify that this quorum (the root of trust in the system)
171            // has produced a threshold signature on this leaf, including the version number, before
172            // we act on that version.
173            //
174            // The only reason we need to read the version before checking this signature is that
175            // the version feeds into the commitment that the signature is over.
176            let version = leaf.block_header().version();
177            // Similarly, check if the protocol version is supposed to change at some point in the
178            // middle of the QC chain. Any valid (signed by this quorum) leaf that is within a few
179            // views of an upgrade taking effect will have an upgrade certificate attached telling
180            // us so.
181            let upgrade = leaf.upgrade_certificate();
182            // Enforce that this version of the software supports these protocol versions. If we see
183            // a version from the future, we must fail because we don't necessarily know how to
184            // treat objects with this version.
185            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            // Check the QC chain: valid signatures and sequential views.
196            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                // What version number do we expect the quorum to have signed over?
203                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                // Which epoch's quorum do we expect to have signed this certificate? A chain
215                // proving the last leaf of an epoch necessarily contains certificates produced in
216                // the subsequent epoch, signed by the next epoch's quorum. Each certificate commits
217                // to its epoch in the signed payload, so dispatching on it is sound: a certificate
218                // claiming the wrong epoch will fail signature verification.
219                let committing = first.unwrap_or(cert);
220                let next_epoch = match (committing.epoch(), cert.epoch()) {
221                    // Certificates from before the epochs upgrade are always checked against the
222                    // quorum supplied for this proof.
223                    (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                        // Only the last leaf of an epoch is justified by the next epoch's quorum,
227                        // and its committing QC is dual-signed: it carries a next-epoch QC that
228                        // `verify` has already checked against the next epoch's quorum. Requiring
229                        // that here stops the next epoch's quorum from finalizing an interior leaf
230                        // of the previous epoch, which it never co-signed.
231                        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                // Check the signature.
242                if next_epoch {
243                    self.verify_next_epoch(cert, cert_version).await?;
244                } else {
245                    self.verify(cert, cert_version).await?;
246                }
247
248                // Check chaining.
249                if let Some(prev) = curr {
250                    ensure!(cert.view_number() == prev.view_number() + 1);
251                }
252                curr = Some(cert);
253
254                // Save the first QC.
255                if first.is_none() {
256                    first = Some(cert);
257                }
258            }
259
260            // Check that the first QC in the chain signs the required leaf.
261            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/// The protocol versions extracted while verifying a QC chain.
274#[derive(Clone, Copy, Debug)]
275pub struct ChainVersions {
276    /// The protocol version of the leaf the chain proves finalized.
277    pub leaf: Version,
278
279    /// The highest protocol version any certificate in the chain was verified under.
280    ///
281    /// May exceed [`leaf`](Self::leaf) when an upgrade takes effect within the chain.
282    pub max_cert: Version,
283}
284
285/// A stake table representing a particular quorum.
286#[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    /// Get a stake table from a particular epoch's quorum membership.
334    pub async fn from_membership(membership: &EpochMembership<SeqTypes>) -> Self {
335        HSStakeTable::from_iter(membership.stake_table()).into()
336    }
337
338    /// Verify that a certificate is signed by a quorum of this stake table.
339    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
349/// Getters for the current epoch's stake table and the next.
350///
351/// The current [`stake_table`](StakeTablePair::stake_table) is always needed to verify a
352/// [`Certificate`] from this epoch. Depending on the [`Certificate`], the next epoch's stake table
353/// may also need to be fetched (in the case where the certificate is part of an epoch transition).
354pub trait StakeTablePair {
355    /// Get the stake table for the current epoch.
356    fn stake_table(&self) -> impl Send + Future<Output = Result<Arc<StakeTable>>>;
357
358    /// Get the stake table for the next epoch.
359    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/// A quorum based on a [`StakeTablePair`] for a particular epoch.
384#[derive(Clone, Debug)]
385pub struct StakeTableQuorum<T> {
386    membership: T,
387    epoch_height: u64,
388}
389
390impl<T> StakeTableQuorum<T> {
391    /// Construct a quorum given a [`StakeTablePair`] and the epoch height.
392    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            // If this certificate is part of an epoch change, also check that the next epoch's
416            // quorum has signed. Reject a next-epoch QC attached to any other certificate: it would
417            // otherwise go unverified, yet the epoch dispatch in `verify_qc_chain_and_get_version`
418            // treats a next-epoch QC on the committing certificate as proof that the next epoch
419            // co-signed the leaf.
420            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        // A certificate from the epoch after the proof's epoch is never itself part of an epoch
460        // transition (transition certificates belong to the epoch that is ending), so there is no
461        // second next-epoch QC to check.
462        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            // Don't attach an upgrade certificate, so that the version change that happens within
567            // the QC change is actually malicious.
568            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            // Delete the next epoch justify QC, making this an invalid epoch change QC.
597            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            // Tamper with the next epoch justify QC, making this an invalid epoch change QC.
613            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            // Tamper with the next epoch justify QC, making this an invalid epoch change QC.
631            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            // Delete the next epoch justify QC; this is allowed since epochs are not enabled yet.
649            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    /// Build a 2-chain proving the last leaf of epoch 1 (block 10), where epochs 1 and 2 have
741    /// disjoint quorums. The deciding QC is produced in epoch 2. If `deciding_signed_by_next` it
742    /// is correctly signed by epoch 2's quorum; otherwise it is (invalidly) signed by epoch 1's
743    /// quorum.
744    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        // Block 10 is an epoch transition block, so its QC is dual-signed by both quorums.
758        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), &current),
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            &current
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    /// A 2-chain proving the last leaf of an epoch includes a deciding QC signed by the next
798    /// epoch's quorum; it must be verified against that quorum, not the quorum of the epoch of the
799    /// leaf under proof.
800    #[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    /// A deciding QC claiming to be from the next epoch but signed by the current epoch's quorum
811    /// must fail verification.
812    #[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    /// Build a 2-chain over an interior (non-boundary) leaf of epoch 1 whose deciding QC is forged
822    /// by epoch 2's quorum, mimicking a next epoch trying to finalize a leaf the current epoch
823    /// never decided. If `disguise_as_boundary`, the committing QC also carries a next-epoch QC to
824    /// imitate a genuine epoch-transition committing QC.
825    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        // Block 5 is in the interior of epoch 1 (epoch height 10), not an epoch boundary.
837        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), &current),
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    /// The next epoch's quorum must not be able to finalize an interior leaf of the previous epoch:
871    /// only the last leaf of an epoch is justified by the next epoch, so a deciding QC signed by
872    /// the next epoch over a non-boundary leaf must be rejected.
873    #[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    /// The same forgery must be rejected even when the committing QC is dressed up with a
883    /// next-epoch QC to imitate a genuine epoch-transition committing QC.
884    #[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}