Skip to main content

linera_chain/certificate/
lite.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2// Copyright (c) Zefchain Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{borrow::Cow, ops::Deref};
6
7use allocative::{Allocative, Key, Visitor};
8use linera_base::{
9    crypto::{CryptoHash, ValidatorPublicKey, ValidatorSignature},
10    data_types::Round,
11    ensure,
12};
13use linera_execution::committee::Committee;
14use serde::{Deserialize, Serialize};
15
16use super::{
17    CertificateValue, ConfirmedBlockCertificate, GenericCertificate, ValidatedBlockCertificate,
18};
19use crate::{
20    block::{ConfirmedBlock, ValidatedBlock},
21    data_types::{check_signatures, LiteValue, LiteVote, VoteValue},
22    justification::{CommittedQuorum, JustificationChain},
23    types::CertificateKind,
24    ChainError,
25};
26
27/// A certified statement from the committee, without the value.
28#[derive(Clone, Debug, Serialize, Deserialize)]
29#[cfg_attr(with_testing, derive(Eq, PartialEq))]
30pub struct LiteCertificate<'a> {
31    /// Hash and chain ID of the certified value (used as key for storage).
32    pub value: LiteValue,
33    /// The round in which the value was certified.
34    pub round: Round,
35    /// The unlocking round the `ValidatedBlock` voters signed (see [`VoteValue`]). Always `None`
36    /// for `ConfirmedBlock`/`Timeout` certificates and for validated blocks with no justification.
37    ///
38    /// [`VoteValue`]: crate::data_types::VoteValue
39    pub unlocking_round: Option<Round>,
40    /// The first-round attestation the `ConfirmedBlock` voters signed (see [`VoteValue`]). Only
41    /// `true` for a `ConfirmedBlock` certificate confirming a block in the chain's first round;
42    /// always `false` for `ValidatedBlock`/`Timeout` certificates.
43    ///
44    /// [`VoteValue`]: crate::data_types::VoteValue
45    pub first_round: bool,
46    /// The justification commitment the voters signed (see [`VoteValue`]): the hash of the
47    /// carried chain's top link, or `None` if the chain is empty.
48    ///
49    /// [`VoteValue`]: crate::data_types::VoteValue
50    pub justification_commitment: Option<CryptoHash>,
51    /// The justification chain attached to this certificate: for a `ValidatedBlock` certificate
52    /// it is the chain of validated quorums in rounds below `round`; for a `ConfirmedBlock`
53    /// certificate it is the full chain of validated quorums up to and including the confirm
54    /// round. Empty for `Timeout` certificates and for blocks needing no justification. Borrowed,
55    /// like `signatures`, so building a lite certificate from a full one never clones the chain.
56    pub justification: Cow<'a, JustificationChain>,
57    /// Signatures on the value.
58    pub signatures: Cow<'a, [(ValidatorPublicKey, ValidatorSignature)]>,
59}
60
61impl Allocative for LiteCertificate<'_> {
62    fn visit<'a, 'b: 'a>(&self, visitor: &'a mut Visitor<'b>) {
63        visitor.visit_field(Key::new("LiteCertificate_value"), &self.value);
64        visitor.visit_field(Key::new("LiteCertificate_round"), &self.round);
65        visitor.visit_field(
66            Key::new("LiteCertificate_justification"),
67            self.justification.as_ref(),
68        );
69        if matches!(self.signatures, Cow::Owned(_)) {
70            for (public_key, signature) in self.signatures.deref() {
71                visitor.visit_field(Key::new("ValidatorPublicKey"), public_key);
72                visitor.visit_field(Key::new("ValidatorSignature"), signature);
73            }
74        }
75    }
76}
77
78impl LiteCertificate<'_> {
79    /// Creates a new lite certificate that records the signed payload fields beyond the value
80    /// and round: the unlocking round its `ValidatedBlock` voters signed, the first-round
81    /// attestation its `ConfirmedBlock` voters signed, and the justification commitment (see
82    /// [`VoteValue`]) — with an empty justification chain.
83    ///
84    /// [`VoteValue`]: crate::data_types::VoteValue
85    pub fn new_with_payload(
86        value: LiteValue,
87        round: Round,
88        unlocking_round: Option<Round>,
89        first_round: bool,
90        justification_commitment: Option<CryptoHash>,
91        mut signatures: Vec<(ValidatorPublicKey, ValidatorSignature)>,
92    ) -> Self {
93        signatures.sort_by_key(|&(validator_name, _)| validator_name);
94
95        Self {
96            value,
97            round,
98            unlocking_round,
99            first_round,
100            justification_commitment,
101            justification: Cow::Owned(JustificationChain::default()),
102            signatures: Cow::Owned(signatures),
103        }
104    }
105
106    /// Creates a [`LiteCertificate`] from a list of votes with their validator public keys, without cryptographically checking the
107    /// signatures. Returns `None` if the votes are empty or don't have matching values and rounds.
108    pub fn try_from_votes(
109        votes: impl IntoIterator<Item = (ValidatorPublicKey, LiteVote)>,
110    ) -> Option<Self> {
111        let mut votes = votes.into_iter();
112        let (
113            public_key,
114            LiteVote {
115                value,
116                round,
117                unlocking_round,
118                first_round,
119                justification_commitment,
120                signature,
121            },
122        ) = votes.next()?;
123        let mut signatures = vec![(public_key, signature)];
124        for (validator_key, vote) in votes {
125            if vote.value.value_hash != value.value_hash
126                || vote.round != round
127                || vote.unlocking_round != unlocking_round
128                || vote.first_round != first_round
129                || vote.justification_commitment != justification_commitment
130            {
131                return None;
132            }
133            signatures.push((validator_key, vote.signature));
134        }
135        Some(LiteCertificate::new_with_payload(
136            value,
137            round,
138            unlocking_round,
139            first_round,
140            justification_commitment,
141            signatures,
142        ))
143    }
144
145    /// Verifies the certificate: its signatures, its justification chain, and that the signed
146    /// unlocking round and first-round attestation are bound to that chain exactly as
147    /// [`ValidatedBlockCertificate::check`] and [`ConfirmedBlockCertificate::check`] require. This
148    /// is the single verification the worker applies to the certificate a retry proposal carries,
149    /// so it must reject a stripped or mismatched chain, not just check the pieces in isolation.
150    ///
151    /// [`ValidatedBlockCertificate::check`]: super::ValidatedBlockCertificate::check
152    /// [`ConfirmedBlockCertificate::check`]: super::ConfirmedBlockCertificate::check
153    pub fn check(&self, committee: &Committee) -> Result<&LiteValue, ChainError> {
154        // The carried chain's links are not signature-checked: the signed justification
155        // commitment is the hash-linked head of the chain, so the single signature check over
156        // this certificate's own quorum (below) attests every link — each link's voters verified
157        // the quorum beneath them before signing over its hash.
158        let derived_commitment = self.justification.verify(self.value.value_hash)?;
159        ensure!(
160            self.justification_commitment == derived_commitment,
161            ChainError::JustificationCommitmentMismatch
162        );
163        let value = VoteValue(
164            self.value.value_hash,
165            self.round,
166            self.value.kind,
167            self.unlocking_round,
168            self.first_round,
169            self.justification_commitment,
170        );
171        check_signatures(&value, &self.signatures, committee)?;
172        let top = self.justification.top_unlocking_round();
173        match self.value.kind {
174            CertificateKind::Validated => {
175                // The signed unlocking round must be the top of the chain, which must lie strictly
176                // below the certified round.
177                ensure!(
178                    self.unlocking_round == top,
179                    ChainError::JustificationUnlockingRoundMismatch
180                );
181                ensure!(
182                    top.is_none_or(|top| top < self.round),
183                    ChainError::JustificationChainNotBelowCertificate
184                );
185            }
186            CertificateKind::Confirmed => {
187                // The first-round attestation can only be set in a round that could be a chain's
188                // first one.
189                if self.first_round {
190                    ensure!(
191                        matches!(
192                            self.round,
193                            Round::Fast
194                                | Round::MultiLeader(0)
195                                | Round::SingleLeader(0)
196                                | Round::Validator(0)
197                        ),
198                        ChainError::FalseFirstRoundAttestation
199                    );
200                }
201                match top {
202                    // An absent chain is allowed only for a first-round confirmation.
203                    None => ensure!(
204                        self.first_round,
205                        ChainError::JustificationUnlockingRoundMismatch
206                    ),
207                    // Otherwise the chain's top link is the validation in the confirmation round.
208                    Some(top) => ensure!(
209                        top == self.round,
210                        ChainError::JustificationUnlockingRoundMismatch
211                    ),
212                }
213            }
214            // Timeout certificates carry no justification.
215            CertificateKind::Timeout => ensure!(
216                top.is_none(),
217                ChainError::JustificationUnlockingRoundMismatch
218            ),
219        }
220        Ok(&self.value)
221    }
222
223    /// Returns the full justification chain that a certificate validating in a higher round
224    /// would carry below itself: the chain it already carries, with this certificate's own
225    /// quorum appended as the new top link.
226    pub fn full_justification(&self) -> JustificationChain {
227        self.justification
228            .append(self.round, self.signatures.to_vec())
229    }
230
231    /// Returns the justification commitment that a vote citing this certificate signs: the hash
232    /// of this certificate's own quorum as a [`CommittedQuorum`], which transitively commits to
233    /// the chain below it. Equals [`full_justification`](Self::full_justification)'s commitment.
234    pub fn full_justification_commitment(&self) -> CryptoHash {
235        CommittedQuorum {
236            value_hash: self.value.value_hash,
237            round: self.round,
238            unlocking_round: self.unlocking_round,
239            previous: self.justification_commitment,
240            signatures: self.signatures.to_vec(),
241        }
242        .commitment()
243    }
244
245    /// Checks whether the value matches this certificate.
246    pub fn check_value<T: CertificateValue>(&self, value: &T) -> bool {
247        self.value.chain_id == value.chain_id()
248            && T::KIND == self.value.kind
249            && self.value.value_hash == value.hash()
250    }
251
252    /// Returns the [`GenericCertificate`] with the specified value, if it matches. The
253    /// justification chain is dropped; use [`into_confirmed_certificate`](Self::into_confirmed_certificate)
254    /// or [`into_validated_certificate`](Self::into_validated_certificate) to keep it.
255    pub fn with_value<T: CertificateValue>(self, value: T) -> Option<GenericCertificate<T>> {
256        Some(self.into_quorum_and_chain(value)?.0)
257    }
258
259    /// Consumes this lite certificate into the full [`ConfirmedBlockCertificate`] for `value`,
260    /// carrying the justification chain across (never cloning it). Returns `None` if the value
261    /// does not match.
262    pub fn into_confirmed_certificate(
263        self,
264        value: ConfirmedBlock,
265    ) -> Option<ConfirmedBlockCertificate> {
266        let (quorum, justification) = self.into_quorum_and_chain(value)?;
267        Some(ConfirmedBlockCertificate::from_parts(quorum, justification))
268    }
269
270    /// Consumes this lite certificate into the full [`ValidatedBlockCertificate`] for `value`,
271    /// carrying the justification chain across (never cloning it). Returns `None` if the value
272    /// does not match.
273    pub fn into_validated_certificate(
274        self,
275        value: ValidatedBlock,
276    ) -> Option<ValidatedBlockCertificate> {
277        let (quorum, justification) = self.into_quorum_and_chain(value)?;
278        Some(ValidatedBlockCertificate::from_parts(quorum, justification))
279    }
280
281    /// Splits this lite certificate into the signed quorum for `value` and its justification
282    /// chain, moving both out. Returns `None` if the value does not match.
283    fn into_quorum_and_chain<T: CertificateValue>(
284        self,
285        value: T,
286    ) -> Option<(GenericCertificate<T>, JustificationChain)> {
287        if !self.check_value(&value) {
288            return None;
289        }
290        let quorum = GenericCertificate::new_with_payload(
291            value,
292            self.round,
293            self.unlocking_round,
294            self.first_round,
295            self.justification_commitment,
296            self.signatures.into_owned(),
297        );
298        Some((quorum, self.justification.into_owned()))
299    }
300
301    /// Returns a [`LiteCertificate`] that owns its signatures and justification chain.
302    pub fn cloned(&self) -> LiteCertificate<'static> {
303        LiteCertificate {
304            value: self.value.clone(),
305            round: self.round,
306            unlocking_round: self.unlocking_round,
307            first_round: self.first_round,
308            justification_commitment: self.justification_commitment,
309            justification: Cow::Owned(self.justification.as_ref().clone()),
310            signatures: Cow::Owned(self.signatures.clone().into_owned()),
311        }
312    }
313}