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::{Leaf2, PubKey, SeqTypes};
7use hotshot_types::{
8    epoch_membership::EpochMembership,
9    message::UpgradeLock,
10    simple_certificate::CertificatePair,
11    stake_table::{HSStakeTable, StakeTableEntries, StakeTableEntry, supermajority_threshold},
12    vote::{self, HasViewNumber},
13};
14use tracing::Instrument;
15use vbs::version::{StaticVersion, StaticVersionType, Version};
16use versions::{EPOCH_VERSION, MAX_SUPPORTED_VERSION, Upgrade, version};
17
18pub type Certificate = CertificatePair<SeqTypes>;
19
20pub trait Quorum: Sync {
21    /// Check a threshold signature on a quorum certificate.
22    fn verify(
23        &self,
24        cert: &Certificate,
25        version: Version,
26    ) -> impl Send + Future<Output = Result<()>> {
27        async move {
28            match (version.major, version.minor) {
29                (0, 1) => self.verify_static::<StaticVersion<0, 1>>(cert).await,
30                (0, 2) => self.verify_static::<StaticVersion<0, 2>>(cert).await,
31                (0, 3) => self.verify_static::<StaticVersion<0, 3>>(cert).await,
32                (0, 4) => self.verify_static::<StaticVersion<0, 4>>(cert).await,
33                (0, 5) => self.verify_static::<StaticVersion<0, 5>>(cert).await,
34                (0, 6) => self.verify_static::<StaticVersion<0, 6>>(cert).await,
35                (0, 7) => self.verify_static::<StaticVersion<0, 7>>(cert).await,
36                (0, 8) => self.verify_static::<StaticVersion<0, 8>>(cert).await,
37                _ => {
38                    const {
39                        assert!(MAX_SUPPORTED_VERSION.major == 0);
40                        assert!(MAX_SUPPORTED_VERSION.minor == 8);
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 that QCs are signed, form a chain starting from `leaf`, with a particular protocol
55    /// version.
56    ///
57    /// This check forms the bulk of the commit rule for both HotStuff and HotStuff2.
58    fn verify_qc_chain_and_get_version<'a>(
59        &self,
60        leaf: &Leaf2,
61        certs: impl Send + IntoIterator<Item = &'a Certificate, IntoIter: Send>,
62    ) -> impl Send + Future<Output = Result<Version>> {
63        let span = tracing::trace_span!(
64            "verify_qc_chain_and_get_version",
65            height = leaf.block_header().height()
66        );
67        async move {
68            // Get the protocol version that the leaf claims it is using. At this point, the leaf is
69            // not trusted, but we will verify that this quorum (the root of trust in the system)
70            // has produced a threshold signature on this leaf, including the version number, before
71            // we act on that version.
72            //
73            // The only reason we need to read the version before checking this signature is that
74            // the version feeds into the commitment that the signature is over.
75            let version = leaf.block_header().version();
76            // Similarly, check if the protocol version is supposed to change at some point in the
77            // middle of the QC chain. Any valid (signed by this quorum) leaf that is within a few
78            // views of an upgrade taking effect will have an upgrade certificate attached telling
79            // us so.
80            let upgrade = leaf.upgrade_certificate();
81            // Enforce that this version of the software supports these protocol versions. If we see
82            // a version from the future, we must fail because we don't necessarily know how to
83            // treat objects with this version.
84            ensure!(version <= MAX_SUPPORTED_VERSION);
85            if let Some(cert) = &upgrade {
86                ensure!(cert.data.new_version <= MAX_SUPPORTED_VERSION);
87            }
88            tracing::debug!(
89                %version,
90                ?leaf,
91                "verify QC chain for leaf"
92            );
93
94            // Check the QC chain: valid signatures and sequential views.
95            let mut first = None;
96            let mut curr: Option<&Certificate> = None;
97            for cert in certs {
98                tracing::trace!(?cert, "verify cert");
99
100                // What version number do we expect the quorum to have signed over?
101                let version = match &upgrade {
102                    Some(upgrade) if cert.view_number() >= upgrade.data.new_version_first_view => {
103                        tracing::debug!(?upgrade, view = ?cert.view_number(), "using upgraded version");
104                        upgrade.data.new_version
105                    },
106                    _ => version,
107                };
108
109                // Check the signature.
110                self.verify(cert, version).await?;
111
112                // Check chaining.
113                if let Some(prev) = curr {
114                    ensure!(cert.view_number() == prev.view_number() + 1);
115                }
116                curr = Some(cert);
117
118                // Save the first QC.
119                if first.is_none() {
120                    first = Some(cert);
121                }
122            }
123
124            // Check that the first QC in the chain signs the required leaf.
125            let first_qc = first.context("empty QC chain")?;
126            ensure!(first_qc.leaf_commit() == leaf.commit());
127
128            Ok(version)
129        }
130        .instrument(span)
131    }
132}
133
134/// A stake table representing a particular quorum.
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct StakeTable {
137    entries: Vec<StakeTableEntry<PubKey>>,
138    threshold: U256,
139}
140
141impl From<HSStakeTable<SeqTypes>> for StakeTable {
142    fn from(table: HSStakeTable<SeqTypes>) -> Self {
143        StakeTableEntries::from(table).into()
144    }
145}
146
147impl From<Vec<StakeTableEntry<PubKey>>> for StakeTable {
148    fn from(entries: Vec<StakeTableEntry<PubKey>>) -> Self {
149        StakeTableEntries(entries).into()
150    }
151}
152
153impl From<StakeTableEntries<SeqTypes>> for StakeTable {
154    fn from(entries: StakeTableEntries<SeqTypes>) -> Self {
155        Self::from_iter(entries.0)
156    }
157}
158
159impl FromIterator<StakeTableEntry<PubKey>> for StakeTable {
160    fn from_iter<T: IntoIterator<Item = StakeTableEntry<PubKey>>>(entries: T) -> Self {
161        let mut total_stake = U256::ZERO;
162        let entries = entries
163            .into_iter()
164            .inspect(|entry| {
165                total_stake += entry.stake_amount;
166            })
167            .collect();
168        Self {
169            entries,
170            threshold: supermajority_threshold(total_stake),
171        }
172    }
173}
174
175impl StakeTable {
176    /// Get a stake table from a particular epoch's quorum membership.
177    pub async fn from_membership(membership: &EpochMembership<SeqTypes>) -> Self {
178        membership.stake_table().await.into()
179    }
180
181    /// Verify that a certificate is signed by a quorum of this stake table.
182    pub async fn verify_cert<V, T>(&self, cert: &impl vote::Certificate<SeqTypes, T>) -> Result<()>
183    where
184        V: StaticVersionType + 'static,
185    {
186        let upgrade = Upgrade::trivial(version(V::MAJOR, V::MINOR));
187        cert.is_valid_cert(&self.entries, self.threshold, &UpgradeLock::new(upgrade))
188            .context("invalid threshold signature")
189    }
190}
191
192/// Getters for the current epoch's stake table and the next.
193///
194/// The current [`stake_table`](StakeTablePair::stake_table) is always needed to verify a
195/// [`Certificate`] from this epoch. Depending on the [`Certificate`], the next epoch's stake table
196/// may also need to be fetched (in the case where the certificate is part of an epoch transition).
197pub trait StakeTablePair {
198    /// Get the stake table for the current epoch.
199    fn stake_table(&self) -> impl Send + Future<Output = Result<Arc<StakeTable>>>;
200
201    /// Get the stake table for the next epoch.
202    fn next_epoch_stake_table(&self) -> impl Send + Future<Output = Result<Arc<StakeTable>>>;
203}
204
205impl StakeTablePair for EpochMembership<SeqTypes> {
206    async fn stake_table(&self) -> Result<Arc<StakeTable>> {
207        Ok(Arc::new(StakeTable::from_membership(self).await))
208    }
209
210    async fn next_epoch_stake_table(&self) -> Result<Arc<StakeTable>> {
211        let membership = self.next_epoch_stake_table().await?;
212        Ok(Arc::new(StakeTable::from_membership(&membership).await))
213    }
214}
215
216impl StakeTablePair for (Arc<StakeTable>, Arc<StakeTable>) {
217    async fn stake_table(&self) -> Result<Arc<StakeTable>> {
218        Ok(self.0.clone())
219    }
220
221    async fn next_epoch_stake_table(&self) -> Result<Arc<StakeTable>> {
222        Ok(self.1.clone())
223    }
224}
225
226/// A quorum based on a [`StakeTablePair`] for a particular epoch.
227#[derive(Clone, Debug)]
228pub struct StakeTableQuorum<T> {
229    membership: T,
230    epoch_height: u64,
231}
232
233impl<T> StakeTableQuorum<T> {
234    /// Construct a quorum given a [`StakeTablePair`] and the epoch height.
235    pub fn new(membership: T, epoch_height: u64) -> Self {
236        Self {
237            membership,
238            epoch_height,
239        }
240    }
241}
242
243impl<T> Quorum for StakeTableQuorum<T>
244where
245    T: StakeTablePair + Sync,
246{
247    async fn verify_static<V: StaticVersionType + 'static>(
248        &self,
249        cert: &Certificate,
250    ) -> Result<()> {
251        let stake_table = self.membership.stake_table().await?;
252        stake_table
253            .verify_cert::<V, _>(cert.qc())
254            .await
255            .context("verifying QC")?;
256
257        if version(V::MAJOR, V::MINOR) >= EPOCH_VERSION {
258            // If this certificate is part of an epoch change, also check that the next epoch's
259            // quorum has signed.
260            if let Some(next_epoch_qc) = cert.verify_next_epoch_qc(self.epoch_height)? {
261                let stake_table = self.membership.next_epoch_stake_table().await?;
262                stake_table
263                    .verify_cert::<V, _>(next_epoch_qc)
264                    .await
265                    .context("verifying next epoch QC")?;
266            }
267        }
268
269        Ok(())
270    }
271}
272
273#[cfg(test)]
274mod test {
275    use pretty_assertions::assert_eq;
276
277    use super::*;
278    use crate::testing::{
279        AlwaysFalseQuorum, AlwaysTrueQuorum, ENABLE_EPOCHS, EpochChangeQuorum, LEGACY_VERSION,
280        VersionCheckQuorum, custom_epoch_change_leaf_chain, custom_leaf_chain_with_upgrade,
281        epoch_change_leaf_chain, leaf_chain, leaf_chain_with_upgrade, qc_chain_from_leaf_chain,
282    };
283
284    #[test_log::test(tokio::test(flavor = "multi_thread"))]
285    async fn test_valid_chain() {
286        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
287        let version = AlwaysTrueQuorum
288            .verify_qc_chain_and_get_version(
289                leaves[0].leaf(),
290                &qc_chain_from_leaf_chain(&leaves[1..]),
291            )
292            .await
293            .unwrap();
294        assert_eq!(version, leaves[0].header().version());
295    }
296
297    #[test_log::test(tokio::test(flavor = "multi_thread"))]
298    async fn test_wrong_leaf() {
299        let leaves = leaf_chain(1..=3, EPOCH_VERSION).await;
300        AlwaysTrueQuorum
301            .verify_qc_chain_and_get_version(
302                leaves[2].leaf(),
303                &qc_chain_from_leaf_chain(&leaves[1..]),
304            )
305            .await
306            .unwrap_err();
307    }
308
309    #[test_log::test(tokio::test(flavor = "multi_thread"))]
310    async fn test_invalid_qc() {
311        let leaves = leaf_chain(1..=2, EPOCH_VERSION).await;
312        AlwaysFalseQuorum
313            .verify_qc_chain_and_get_version(
314                leaves[0].leaf(),
315                &[Certificate::for_parent(leaves[1].leaf())],
316            )
317            .await
318            .unwrap_err();
319    }
320
321    #[test_log::test(tokio::test(flavor = "multi_thread"))]
322    async fn test_non_consecutive() {
323        let leaves = leaf_chain(1..=4, EPOCH_VERSION).await;
324        AlwaysTrueQuorum
325            .verify_qc_chain_and_get_version(
326                leaves[0].leaf(),
327                &qc_chain_from_leaf_chain([&leaves[1], &leaves[3]]),
328            )
329            .await
330            .unwrap_err();
331    }
332
333    #[test_log::test(tokio::test(flavor = "multi_thread"))]
334    async fn test_upgrade() {
335        let leaves = leaf_chain_with_upgrade(1..=3, 2, ENABLE_EPOCHS).await;
336        let version = VersionCheckQuorum::new(leaves.iter().map(|leaf| leaf.leaf().clone()))
337            .verify_qc_chain_and_get_version(
338                leaves[0].leaf(),
339                &qc_chain_from_leaf_chain(&leaves[1..]),
340            )
341            .await
342            .unwrap();
343        assert_eq!(version, leaves[0].header().version());
344    }
345
346    #[test_log::test(tokio::test(flavor = "multi_thread"))]
347    async fn test_illegal_upgrade() {
348        let leaves = custom_leaf_chain_with_upgrade(1..=3, 2, ENABLE_EPOCHS, |proposal| {
349            // Don't attach an upgrade certificate, so that the version change that happens within
350            // the QC change is actually malicious.
351            proposal.upgrade_certificate = None;
352        })
353        .await;
354        VersionCheckQuorum::new(leaves.iter().map(|leaf| leaf.leaf().clone()))
355            .verify_qc_chain_and_get_version(
356                leaves[0].leaf(),
357                &qc_chain_from_leaf_chain(&leaves[1..]),
358            )
359            .await
360            .unwrap_err();
361    }
362
363    #[test_log::test(tokio::test(flavor = "multi_thread"))]
364    async fn test_epoch_change() {
365        let leaves = epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION).await;
366        let version = EpochChangeQuorum::new(5)
367            .verify_qc_chain_and_get_version(
368                leaves[0].leaf(),
369                &qc_chain_from_leaf_chain(&leaves[1..]),
370            )
371            .await
372            .unwrap();
373        assert_eq!(version, leaves[0].header().version());
374    }
375
376    #[test_log::test(tokio::test(flavor = "multi_thread"))]
377    async fn test_epoch_change_missing_eqc() {
378        let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
379            // Delete the next epoch justify QC, making this an invalid epoch change QC.
380            proposal.next_epoch_justify_qc = None;
381        })
382        .await;
383        EpochChangeQuorum::new(5)
384            .verify_qc_chain_and_get_version(
385                leaves[0].leaf(),
386                &qc_chain_from_leaf_chain(&leaves[1..]),
387            )
388            .await
389            .unwrap_err();
390    }
391
392    #[test_log::test(tokio::test(flavor = "multi_thread"))]
393    async fn test_epoch_change_inconsistent_eqc_view_number() {
394        let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
395            // Tamper with the next epoch justify QC, making this an invalid epoch change QC.
396            if let Some(next_epoch_justify_qc) = &mut proposal.next_epoch_justify_qc {
397                next_epoch_justify_qc.view_number += 1;
398            }
399        })
400        .await;
401        EpochChangeQuorum::new(5)
402            .verify_qc_chain_and_get_version(
403                leaves[0].leaf(),
404                &qc_chain_from_leaf_chain(&leaves[1..]),
405            )
406            .await
407            .unwrap_err();
408    }
409
410    #[test_log::test(tokio::test(flavor = "multi_thread"))]
411    async fn test_epoch_change_inconsistent_eqc_data() {
412        let leaves = custom_epoch_change_leaf_chain(1..=5, 5, EPOCH_VERSION, |proposal| {
413            // Tamper with the next epoch justify QC, making this an invalid epoch change QC.
414            if let Some(next_epoch_justify_qc) = &mut proposal.next_epoch_justify_qc {
415                *next_epoch_justify_qc.data.block_number.as_mut().unwrap() += 1;
416            }
417        })
418        .await;
419        EpochChangeQuorum::new(5)
420            .verify_qc_chain_and_get_version(
421                leaves[0].leaf(),
422                &qc_chain_from_leaf_chain(&leaves[1..]),
423            )
424            .await
425            .unwrap_err();
426    }
427
428    #[test_log::test(tokio::test(flavor = "multi_thread"))]
429    async fn test_epoch_change_absent_eqc_before_upgrade() {
430        let leaves = custom_epoch_change_leaf_chain(1..=5, 5, LEGACY_VERSION, |proposal| {
431            // Delete the next epoch justify QC; this is allowed since epochs are not enabled yet.
432            proposal.next_epoch_justify_qc = None;
433        })
434        .await;
435        let version = EpochChangeQuorum::new(5)
436            .verify_qc_chain_and_get_version(
437                leaves[0].leaf(),
438                &qc_chain_from_leaf_chain(&leaves[1..]),
439            )
440            .await
441            .unwrap();
442        assert_eq!(version, leaves[0].header().version());
443    }
444}