linera_chain/
data_types.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::collections::{BTreeMap, BTreeSet, HashSet};
6
7use async_graphql::SimpleObject;
8use custom_debug_derive::Debug;
9use linera_base::{
10    bcs,
11    crypto::{
12        AccountPublicKey, AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash,
13        Signer, ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
14    },
15    data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
16    doc_scalar, ensure, hex_debug,
17    identifiers::{Account, AccountOwner, BlobId, ChainId, MessageId},
18};
19use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23    block::{Block, ValidatedBlock},
24    types::{
25        CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
26        ValidatedBlockCertificate,
27    },
28    ChainError,
29};
30
31#[cfg(test)]
32#[path = "unit_tests/data_types_tests.rs"]
33mod data_types_tests;
34
35/// A block containing operations to apply on a given chain, as well as the
36/// acknowledgment of a number of incoming messages from other chains.
37/// * Incoming messages must be selected in the order they were
38///   produced by the sending chain, but can be skipped.
39/// * When a block is proposed to a validator, all cross-chain messages must have been
40///   received ahead of time in the inbox of the chain.
41/// * This constraint does not apply to the execution of confirmed blocks.
42#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
43pub struct ProposedBlock {
44    /// The chain to which this block belongs.
45    pub chain_id: ChainId,
46    /// The number identifying the current configuration.
47    pub epoch: Epoch,
48    /// A selection of incoming messages to be executed first. Successive messages of same
49    /// sender and height are grouped together for conciseness.
50    #[debug(skip_if = Vec::is_empty)]
51    pub incoming_bundles: Vec<IncomingBundle>,
52    /// The operations to execute.
53    #[debug(skip_if = Vec::is_empty)]
54    pub operations: Vec<Operation>,
55    /// The block height.
56    pub height: BlockHeight,
57    /// The timestamp when this block was created. This must be later than all messages received
58    /// in this block, but no later than the current time.
59    pub timestamp: Timestamp,
60    /// The user signing for the operations in the block and paying for their execution
61    /// fees. If set, this must be the `owner` in the block proposal. `None` means that
62    /// the default account of the chain is used. This value is also used as recipient of
63    /// potential refunds for the message grants created by the operations.
64    #[debug(skip_if = Option::is_none)]
65    pub authenticated_signer: Option<AccountOwner>,
66    /// Certified hash (see `Certificate` below) of the previous block in the
67    /// chain, if any.
68    pub previous_block_hash: Option<CryptoHash>,
69}
70
71impl ProposedBlock {
72    /// Returns all the published blob IDs in this block's operations.
73    pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
74        self.operations
75            .iter()
76            .flat_map(Operation::published_blob_ids)
77            .collect()
78    }
79
80    /// Returns whether the block contains only rejected incoming messages, which
81    /// makes it admissible even on closed chains.
82    pub fn has_only_rejected_messages(&self) -> bool {
83        self.operations.is_empty()
84            && self
85                .incoming_bundles
86                .iter()
87                .all(|message| message.action == MessageAction::Reject)
88    }
89
90    /// Returns an iterator over all incoming [`PostedMessage`]s in this block.
91    pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
92        self.incoming_bundles
93            .iter()
94            .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
95    }
96
97    /// Returns the number of incoming messages.
98    pub fn message_count(&self) -> usize {
99        self.incoming_bundles
100            .iter()
101            .map(|im| im.bundle.messages.len())
102            .sum()
103    }
104
105    /// Returns an iterator over all transactions.
106    ///
107    /// First incoming bundles, then operations.
108    pub fn transactions(&self) -> impl Iterator<Item = Transaction<'_>> {
109        let bundles = self
110            .incoming_bundles
111            .iter()
112            .map(Transaction::ReceiveMessages);
113        let operations = self.operations.iter().map(Transaction::ExecuteOperation);
114        bundles.chain(operations)
115    }
116
117    pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
118        let size = bcs::serialized_size(self)?;
119        ensure!(
120            size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
121            ChainError::BlockProposalTooLarge(size)
122        );
123        Ok(())
124    }
125}
126
127/// A transaction in a block: incoming messages or an operation.
128#[derive(Debug, Clone)]
129pub enum Transaction<'a> {
130    /// Receive a bundle of incoming messages.
131    ReceiveMessages(&'a IncomingBundle),
132    /// Execute an operation.
133    ExecuteOperation(&'a Operation),
134}
135
136/// A chain ID with a block height.
137#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, SimpleObject)]
138pub struct ChainAndHeight {
139    pub chain_id: ChainId,
140    pub height: BlockHeight,
141}
142
143impl ChainAndHeight {
144    /// Returns the ID of the `index`-th message sent by the block at that height.
145    pub fn to_message_id(&self, index: u32) -> MessageId {
146        MessageId {
147            chain_id: self.chain_id,
148            height: self.height,
149            index,
150        }
151    }
152}
153
154/// A bundle of cross-chain messages.
155#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
156pub struct IncomingBundle {
157    /// The origin of the messages.
158    pub origin: ChainId,
159    /// The messages to be delivered to the inbox identified by `origin`.
160    pub bundle: MessageBundle,
161    /// What to do with the message.
162    pub action: MessageAction,
163}
164
165impl IncomingBundle {
166    /// Returns an iterator over all posted messages in this bundle, together with their ID.
167    pub fn messages_and_ids(&self) -> impl Iterator<Item = (MessageId, &PostedMessage)> {
168        let chain_and_height = ChainAndHeight {
169            chain_id: self.origin,
170            height: self.bundle.height,
171        };
172        let messages = self.bundle.messages.iter();
173        messages.map(move |posted_message| {
174            let message_id = chain_and_height.to_message_id(posted_message.index);
175            (message_id, posted_message)
176        })
177    }
178}
179
180impl BcsHashable<'_> for IncomingBundle {}
181
182/// What to do with a message picked from the inbox.
183#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
184pub enum MessageAction {
185    /// Execute the incoming message.
186    Accept,
187    /// Do not execute the incoming message.
188    Reject,
189}
190
191/// A set of messages from a single block, for a single destination.
192#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject)]
193pub struct MessageBundle {
194    /// The block height.
195    pub height: BlockHeight,
196    /// The block's timestamp.
197    pub timestamp: Timestamp,
198    /// The confirmed block certificate hash.
199    pub certificate_hash: CryptoHash,
200    /// The index of the transaction in the block that is sending this bundle.
201    pub transaction_index: u32,
202    /// The relevant messages.
203    pub messages: Vec<PostedMessage>,
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize)]
207#[cfg_attr(with_testing, derive(Eq, PartialEq))]
208/// An earlier proposal that is being retried.
209pub enum OriginalProposal {
210    /// A proposal in the fast round.
211    Fast {
212        public_key: AccountPublicKey,
213        signature: AccountSignature,
214    },
215    /// A validated block certificate from an earlier round.
216    Regular {
217        certificate: LiteCertificate<'static>,
218    },
219}
220
221/// An authenticated proposal for a new block.
222// TODO(#456): the signature of the block owner is currently lost but it would be useful
223// to have it for auditing purposes.
224#[derive(Clone, Debug, Serialize, Deserialize)]
225#[cfg_attr(with_testing, derive(Eq, PartialEq))]
226pub struct BlockProposal {
227    pub content: ProposalContent,
228    pub public_key: AccountPublicKey,
229    pub signature: AccountSignature,
230    #[debug(skip_if = Option::is_none)]
231    pub original_proposal: Option<OriginalProposal>,
232}
233
234/// A message together with kind, authentication and grant information.
235#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
236pub struct PostedMessage {
237    /// The user authentication carried by the message, if any.
238    #[debug(skip_if = Option::is_none)]
239    pub authenticated_signer: Option<AccountOwner>,
240    /// A grant to pay for the message execution.
241    #[debug(skip_if = Amount::is_zero)]
242    pub grant: Amount,
243    /// Where to send a refund for the unused part of the grant after execution, if any.
244    #[debug(skip_if = Option::is_none)]
245    pub refund_grant_to: Option<Account>,
246    /// The kind of message being sent.
247    pub kind: MessageKind,
248    /// The index of the message in the sending block.
249    pub index: u32,
250    /// The message itself.
251    pub message: Message,
252}
253
254pub trait OutgoingMessageExt {
255    /// Returns the posted message, i.e. the outgoing message without the destination.
256    fn into_posted(self, index: u32) -> PostedMessage;
257}
258
259impl OutgoingMessageExt for OutgoingMessage {
260    /// Returns the posted message, i.e. the outgoing message without the destination.
261    fn into_posted(self, index: u32) -> PostedMessage {
262        let OutgoingMessage {
263            destination: _,
264            authenticated_signer,
265            grant,
266            refund_grant_to,
267            kind,
268            message,
269        } = self;
270        PostedMessage {
271            authenticated_signer,
272            grant,
273            refund_grant_to,
274            kind,
275            index,
276            message,
277        }
278    }
279}
280
281/// The execution result of a single operation.
282#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
283pub struct OperationResult(
284    #[debug(with = "hex_debug")]
285    #[serde(with = "serde_bytes")]
286    pub Vec<u8>,
287);
288
289impl BcsHashable<'_> for OperationResult {}
290
291doc_scalar!(
292    OperationResult,
293    "The execution result of a single operation."
294);
295
296/// The messages and the state hash resulting from a [`ProposedBlock`]'s execution.
297#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
298#[cfg_attr(with_testing, derive(Default))]
299pub struct BlockExecutionOutcome {
300    /// The list of outgoing messages for each transaction.
301    pub messages: Vec<Vec<OutgoingMessage>>,
302    /// The hashes of previous blocks that sent messages to the same recipients.
303    pub previous_message_blocks: BTreeMap<ChainId, CryptoHash>,
304    /// The hash of the chain's execution state after this block.
305    pub state_hash: CryptoHash,
306    /// The record of oracle responses for each transaction.
307    pub oracle_responses: Vec<Vec<OracleResponse>>,
308    /// The list of events produced by each transaction.
309    pub events: Vec<Vec<Event>>,
310    /// The list of blobs created by each transaction.
311    pub blobs: Vec<Vec<Blob>>,
312    /// The execution result for each operation.
313    pub operation_results: Vec<OperationResult>,
314}
315
316/// The hash and chain ID of a `CertificateValue`.
317#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
318pub struct LiteValue {
319    pub value_hash: CryptoHash,
320    pub chain_id: ChainId,
321    pub kind: CertificateKind,
322}
323
324impl LiteValue {
325    pub fn new<T: CertificateValue>(value: &T) -> Self {
326        LiteValue {
327            value_hash: value.hash(),
328            chain_id: value.chain_id(),
329            kind: T::KIND,
330        }
331    }
332}
333
334#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
335struct VoteValue(CryptoHash, Round, CertificateKind);
336
337/// A vote on a statement from a validator.
338#[derive(Clone, Debug, Serialize, Deserialize)]
339#[serde(bound(deserialize = "T: Deserialize<'de>"))]
340pub struct Vote<T> {
341    pub value: T,
342    pub round: Round,
343    pub public_key: ValidatorPublicKey,
344    pub signature: ValidatorSignature,
345}
346
347impl<T> Vote<T> {
348    /// Use signing key to create a signed object.
349    pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
350    where
351        T: CertificateValue,
352    {
353        let hash_and_round = VoteValue(value.hash(), round, T::KIND);
354        let signature = ValidatorSignature::new(&hash_and_round, key_pair);
355        Self {
356            value,
357            round,
358            public_key: key_pair.public(),
359            signature,
360        }
361    }
362
363    /// Returns the vote, with a `LiteValue` instead of the full value.
364    pub fn lite(&self) -> LiteVote
365    where
366        T: CertificateValue,
367    {
368        LiteVote {
369            value: LiteValue::new(&self.value),
370            round: self.round,
371            public_key: self.public_key,
372            signature: self.signature,
373        }
374    }
375
376    /// Returns the value this vote is for.
377    pub fn value(&self) -> &T {
378        &self.value
379    }
380}
381
382/// A vote on a statement from a validator, represented as a `LiteValue`.
383#[derive(Clone, Debug, Serialize, Deserialize)]
384#[cfg_attr(with_testing, derive(Eq, PartialEq))]
385pub struct LiteVote {
386    pub value: LiteValue,
387    pub round: Round,
388    pub public_key: ValidatorPublicKey,
389    pub signature: ValidatorSignature,
390}
391
392impl LiteVote {
393    /// Returns the full vote, with the value, if it matches.
394    #[cfg(any(feature = "benchmark", with_testing))]
395    pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
396        if self.value.value_hash != value.hash() {
397            return None;
398        }
399        Some(Vote {
400            value,
401            round: self.round,
402            public_key: self.public_key,
403            signature: self.signature,
404        })
405    }
406
407    pub fn kind(&self) -> CertificateKind {
408        self.value.kind
409    }
410}
411
412impl MessageBundle {
413    pub fn is_skippable(&self) -> bool {
414        self.messages.iter().all(PostedMessage::is_skippable)
415    }
416
417    pub fn is_tracked(&self) -> bool {
418        let mut tracked = false;
419        for posted_message in &self.messages {
420            match posted_message.kind {
421                MessageKind::Simple | MessageKind::Bouncing => {}
422                MessageKind::Protected => return false,
423                MessageKind::Tracked => tracked = true,
424            }
425        }
426        tracked
427    }
428
429    pub fn is_protected(&self) -> bool {
430        self.messages.iter().any(PostedMessage::is_protected)
431    }
432}
433
434impl PostedMessage {
435    pub fn is_skippable(&self) -> bool {
436        match self.kind {
437            MessageKind::Protected | MessageKind::Tracked => false,
438            MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
439        }
440    }
441
442    pub fn is_protected(&self) -> bool {
443        matches!(self.kind, MessageKind::Protected)
444    }
445
446    pub fn is_tracked(&self) -> bool {
447        matches!(self.kind, MessageKind::Tracked)
448    }
449
450    pub fn is_bouncing(&self) -> bool {
451        matches!(self.kind, MessageKind::Bouncing)
452    }
453}
454
455impl BlockExecutionOutcome {
456    pub fn with(self, block: ProposedBlock) -> Block {
457        Block::new(block, self)
458    }
459
460    pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
461        let mut required_blob_ids = HashSet::new();
462        for responses in &self.oracle_responses {
463            for response in responses {
464                if let OracleResponse::Blob(blob_id) = response {
465                    required_blob_ids.insert(*blob_id);
466                }
467            }
468        }
469
470        required_blob_ids
471    }
472
473    pub fn has_oracle_responses(&self) -> bool {
474        self.oracle_responses
475            .iter()
476            .any(|responses| !responses.is_empty())
477    }
478
479    pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
480        self.blobs.iter().flatten().map(|blob| blob.id())
481    }
482
483    pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
484        self.iter_created_blobs_ids().collect()
485    }
486}
487
488/// The data a block proposer signs.
489#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
490pub struct ProposalContent {
491    /// The proposed block.
492    pub block: ProposedBlock,
493    /// The consensus round in which this proposal is made.
494    pub round: Round,
495    /// If this is a retry from an earlier round, the execution outcome.
496    #[debug(skip_if = Option::is_none)]
497    pub outcome: Option<BlockExecutionOutcome>,
498}
499
500impl BlockProposal {
501    pub async fn new_initial<S: Signer + ?Sized>(
502        owner: AccountOwner,
503        round: Round,
504        block: ProposedBlock,
505        signer: &S,
506    ) -> Result<Self, S::Error> {
507        let content = ProposalContent {
508            round,
509            block,
510            outcome: None,
511        };
512        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
513        let public_key = signer.get_public_key(&owner).await?;
514
515        Ok(Self {
516            content,
517            public_key,
518            signature,
519            original_proposal: None,
520        })
521    }
522
523    pub async fn new_retry_fast<S: Signer + ?Sized>(
524        owner: AccountOwner,
525        round: Round,
526        old_proposal: BlockProposal,
527        signer: &S,
528    ) -> Result<Self, S::Error> {
529        let content = ProposalContent {
530            round,
531            block: old_proposal.content.block,
532            outcome: None,
533        };
534        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
535        let public_key = signer.get_public_key(&owner).await?;
536
537        Ok(Self {
538            content,
539            public_key,
540            signature,
541            original_proposal: Some(OriginalProposal::Fast {
542                public_key: old_proposal.public_key,
543                signature: old_proposal.signature,
544            }),
545        })
546    }
547
548    pub async fn new_retry_regular<S: Signer>(
549        owner: AccountOwner,
550        round: Round,
551        validated_block_certificate: ValidatedBlockCertificate,
552        signer: &S,
553    ) -> Result<Self, S::Error> {
554        let certificate = validated_block_certificate.lite_certificate().cloned();
555        let block = validated_block_certificate.into_inner().into_inner();
556        let (block, outcome) = block.into_proposal();
557        let content = ProposalContent {
558            block,
559            round,
560            outcome: Some(outcome),
561        };
562        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
563
564        let public_key = signer.get_public_key(&owner).await?;
565        Ok(Self {
566            content,
567            public_key,
568            signature,
569            original_proposal: Some(OriginalProposal::Regular { certificate }),
570        })
571    }
572
573    pub fn check_signature(&self) -> Result<(), CryptoError> {
574        self.signature.verify(&self.content, self.public_key)
575    }
576
577    pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
578        self.content.block.published_blob_ids().into_iter().chain(
579            self.content
580                .outcome
581                .iter()
582                .flat_map(|outcome| outcome.oracle_blob_ids()),
583        )
584    }
585
586    pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
587        self.content.block.published_blob_ids().into_iter().chain(
588            self.content.outcome.iter().flat_map(|outcome| {
589                outcome
590                    .oracle_blob_ids()
591                    .into_iter()
592                    .chain(outcome.iter_created_blobs_ids())
593            }),
594        )
595    }
596
597    /// Checks that the public key matches the owner and that the optional certificate matches
598    /// the outcome.
599    pub fn check_invariants(&self) -> Result<(), &'static str> {
600        match (&self.original_proposal, &self.content.outcome) {
601            (None, None) | (Some(OriginalProposal::Fast { .. }), None) => {}
602            (None, Some(_))
603            | (Some(OriginalProposal::Fast { .. }), Some(_))
604            | (Some(OriginalProposal::Regular { .. }), None) => {
605                return Err("Must contain a validation certificate if and only if \
606                     it contains the execution outcome from a previous round");
607            }
608            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
609                let block = outcome.clone().with(self.content.block.clone());
610                let value = ValidatedBlock::new(block);
611                ensure!(
612                    certificate.check_value(&value),
613                    "Lite certificate must match the given block and execution outcome"
614                );
615            }
616        }
617        Ok(())
618    }
619}
620
621impl LiteVote {
622    /// Uses the signing key to create a signed object.
623    pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
624        let hash_and_round = VoteValue(value.value_hash, round, value.kind);
625        let signature = ValidatorSignature::new(&hash_and_round, secret_key);
626        Self {
627            value,
628            round,
629            public_key: secret_key.public(),
630            signature,
631        }
632    }
633
634    /// Verifies the signature in the vote.
635    pub fn check(&self) -> Result<(), ChainError> {
636        let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
637        Ok(self.signature.check(&hash_and_round, &self.public_key)?)
638    }
639}
640
641pub struct SignatureAggregator<'a, T: CertificateValue> {
642    committee: &'a Committee,
643    weight: u64,
644    used_validators: HashSet<ValidatorPublicKey>,
645    partial: GenericCertificate<T>,
646}
647
648impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
649    /// Starts aggregating signatures for the given value into a certificate.
650    pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
651        Self {
652            committee,
653            weight: 0,
654            used_validators: HashSet::new(),
655            partial: GenericCertificate::new(value, round, Vec::new()),
656        }
657    }
658
659    /// Tries to append a signature to a (partial) certificate. Returns Some(certificate) if a
660    /// quorum was reached. The resulting final certificate is guaranteed to be valid in the sense
661    /// of `check` below. Returns an error if the signed value cannot be aggregated.
662    pub fn append(
663        &mut self,
664        public_key: ValidatorPublicKey,
665        signature: ValidatorSignature,
666    ) -> Result<Option<GenericCertificate<T>>, ChainError>
667    where
668        T: CertificateValue,
669    {
670        let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
671        signature.check(&hash_and_round, &public_key)?;
672        // Check that each validator only appears once.
673        ensure!(
674            !self.used_validators.contains(&public_key),
675            ChainError::CertificateValidatorReuse
676        );
677        self.used_validators.insert(public_key);
678        // Update weight.
679        let voting_rights = self.committee.weight(&public_key);
680        ensure!(voting_rights > 0, ChainError::InvalidSigner);
681        self.weight += voting_rights;
682        // Update certificate.
683        self.partial.add_signature((public_key, signature));
684
685        if self.weight >= self.committee.quorum_threshold() {
686            self.weight = 0; // Prevent from creating the certificate twice.
687            Ok(Some(self.partial.clone()))
688        } else {
689            Ok(None)
690        }
691    }
692}
693
694// Checks if the array slice is strictly ordered. That means that if the array
695// has duplicates, this will return False, even if the array is sorted
696pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
697    values.windows(2).all(|pair| pair[0].0 < pair[1].0)
698}
699
700/// Verifies certificate signatures.
701pub(crate) fn check_signatures(
702    value_hash: CryptoHash,
703    certificate_kind: CertificateKind,
704    round: Round,
705    signatures: &[(ValidatorPublicKey, ValidatorSignature)],
706    committee: &Committee,
707) -> Result<(), ChainError> {
708    // Check the quorum.
709    let mut weight = 0;
710    let mut used_validators = HashSet::new();
711    for (validator, _) in signatures {
712        // Check that each validator only appears once.
713        ensure!(
714            !used_validators.contains(validator),
715            ChainError::CertificateValidatorReuse
716        );
717        used_validators.insert(*validator);
718        // Update weight.
719        let voting_rights = committee.weight(validator);
720        ensure!(voting_rights > 0, ChainError::InvalidSigner);
721        weight += voting_rights;
722    }
723    ensure!(
724        weight >= committee.quorum_threshold(),
725        ChainError::CertificateRequiresQuorum
726    );
727    // All that is left is checking signatures!
728    let hash_and_round = VoteValue(value_hash, round, certificate_kind);
729    ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
730    Ok(())
731}
732
733impl BcsSignable<'_> for ProposalContent {}
734
735impl BcsSignable<'_> for VoteValue {}
736
737doc_scalar!(
738    MessageAction,
739    "Whether an incoming message is accepted or rejected."
740);
741
742#[cfg(test)]
743mod signing {
744    use linera_base::{
745        crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
746        data_types::{BlockHeight, Epoch, Round},
747        identifiers::ChainId,
748    };
749
750    use crate::data_types::{ProposalContent, ProposedBlock};
751
752    #[test]
753    fn proposal_content_signing() {
754        use std::str::FromStr;
755
756        // Generated in MetaMask.
757        let secret_key = "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66";
758
759        let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(
760            linera_base::crypto::EvmSecretKey::from_str(secret_key).unwrap(),
761        );
762
763        let proposed_block = ProposedBlock {
764            chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
765            epoch: Epoch(11),
766            incoming_bundles: vec![],
767            operations: vec![],
768            height: BlockHeight(11),
769            timestamp: 190000000u64.into(),
770            authenticated_signer: None,
771            previous_block_hash: None,
772        };
773
774        let proposal = ProposalContent {
775            block: proposed_block,
776            round: Round::SingleLeader(11),
777            outcome: None,
778        };
779
780        // personal_sign of the `proposal_hash` done via MetaMask.
781        // Wrap with proper variant so that bytes match (include the enum variant tag).
782        let metamask_signature = AccountSignature::EvmSecp256k1(EvmSignature::from_str("f2d8afcd51d0f947f5c5e31ac1db73ec5306163af7949b3bb265ba53d03374b04b1e909007b555caf098da1aded29c600bee391c6ee8b4d0962a29044555796d1b").unwrap());
783
784        let signature = signer.sign(&proposal);
785        assert_eq!(signature, metamask_signature);
786    }
787}