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}