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        AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash, Signer,
13        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(AccountSignature),
212    /// A validated block certificate from an earlier round.
213    Regular {
214        certificate: LiteCertificate<'static>,
215    },
216}
217
218/// An authenticated proposal for a new block.
219// TODO(#456): the signature of the block owner is currently lost but it would be useful
220// to have it for auditing purposes.
221#[derive(Clone, Debug, Serialize, Deserialize)]
222#[cfg_attr(with_testing, derive(Eq, PartialEq))]
223pub struct BlockProposal {
224    pub content: ProposalContent,
225    pub signature: AccountSignature,
226    #[debug(skip_if = Option::is_none)]
227    pub original_proposal: Option<OriginalProposal>,
228}
229
230/// A message together with kind, authentication and grant information.
231#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
232pub struct PostedMessage {
233    /// The user authentication carried by the message, if any.
234    #[debug(skip_if = Option::is_none)]
235    pub authenticated_signer: Option<AccountOwner>,
236    /// A grant to pay for the message execution.
237    #[debug(skip_if = Amount::is_zero)]
238    pub grant: Amount,
239    /// Where to send a refund for the unused part of the grant after execution, if any.
240    #[debug(skip_if = Option::is_none)]
241    pub refund_grant_to: Option<Account>,
242    /// The kind of message being sent.
243    pub kind: MessageKind,
244    /// The index of the message in the sending block.
245    pub index: u32,
246    /// The message itself.
247    pub message: Message,
248}
249
250pub trait OutgoingMessageExt {
251    /// Returns the posted message, i.e. the outgoing message without the destination.
252    fn into_posted(self, index: u32) -> PostedMessage;
253}
254
255impl OutgoingMessageExt for OutgoingMessage {
256    /// Returns the posted message, i.e. the outgoing message without the destination.
257    fn into_posted(self, index: u32) -> PostedMessage {
258        let OutgoingMessage {
259            destination: _,
260            authenticated_signer,
261            grant,
262            refund_grant_to,
263            kind,
264            message,
265        } = self;
266        PostedMessage {
267            authenticated_signer,
268            grant,
269            refund_grant_to,
270            kind,
271            index,
272            message,
273        }
274    }
275}
276
277/// The execution result of a single operation.
278#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
279pub struct OperationResult(
280    #[debug(with = "hex_debug")]
281    #[serde(with = "serde_bytes")]
282    pub Vec<u8>,
283);
284
285impl BcsHashable<'_> for OperationResult {}
286
287doc_scalar!(
288    OperationResult,
289    "The execution result of a single operation."
290);
291
292/// The messages and the state hash resulting from a [`ProposedBlock`]'s execution.
293#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
294#[cfg_attr(with_testing, derive(Default))]
295pub struct BlockExecutionOutcome {
296    /// The list of outgoing messages for each transaction.
297    pub messages: Vec<Vec<OutgoingMessage>>,
298    /// The hashes of previous blocks that sent messages to the same recipients.
299    pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
300    /// The hash of the chain's execution state after this block.
301    pub state_hash: CryptoHash,
302    /// The record of oracle responses for each transaction.
303    pub oracle_responses: Vec<Vec<OracleResponse>>,
304    /// The list of events produced by each transaction.
305    pub events: Vec<Vec<Event>>,
306    /// The list of blobs created by each transaction.
307    pub blobs: Vec<Vec<Blob>>,
308    /// The execution result for each operation.
309    pub operation_results: Vec<OperationResult>,
310}
311
312/// The hash and chain ID of a `CertificateValue`.
313#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
314pub struct LiteValue {
315    pub value_hash: CryptoHash,
316    pub chain_id: ChainId,
317    pub kind: CertificateKind,
318}
319
320impl LiteValue {
321    pub fn new<T: CertificateValue>(value: &T) -> Self {
322        LiteValue {
323            value_hash: value.hash(),
324            chain_id: value.chain_id(),
325            kind: T::KIND,
326        }
327    }
328}
329
330#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
331struct VoteValue(CryptoHash, Round, CertificateKind);
332
333/// A vote on a statement from a validator.
334#[derive(Clone, Debug, Serialize, Deserialize)]
335#[serde(bound(deserialize = "T: Deserialize<'de>"))]
336pub struct Vote<T> {
337    pub value: T,
338    pub round: Round,
339    pub public_key: ValidatorPublicKey,
340    pub signature: ValidatorSignature,
341}
342
343impl<T> Vote<T> {
344    /// Use signing key to create a signed object.
345    pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
346    where
347        T: CertificateValue,
348    {
349        let hash_and_round = VoteValue(value.hash(), round, T::KIND);
350        let signature = ValidatorSignature::new(&hash_and_round, key_pair);
351        Self {
352            value,
353            round,
354            public_key: key_pair.public(),
355            signature,
356        }
357    }
358
359    /// Returns the vote, with a `LiteValue` instead of the full value.
360    pub fn lite(&self) -> LiteVote
361    where
362        T: CertificateValue,
363    {
364        LiteVote {
365            value: LiteValue::new(&self.value),
366            round: self.round,
367            public_key: self.public_key,
368            signature: self.signature,
369        }
370    }
371
372    /// Returns the value this vote is for.
373    pub fn value(&self) -> &T {
374        &self.value
375    }
376}
377
378/// A vote on a statement from a validator, represented as a `LiteValue`.
379#[derive(Clone, Debug, Serialize, Deserialize)]
380#[cfg_attr(with_testing, derive(Eq, PartialEq))]
381pub struct LiteVote {
382    pub value: LiteValue,
383    pub round: Round,
384    pub public_key: ValidatorPublicKey,
385    pub signature: ValidatorSignature,
386}
387
388impl LiteVote {
389    /// Returns the full vote, with the value, if it matches.
390    #[cfg(any(feature = "benchmark", with_testing))]
391    pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
392        if self.value.value_hash != value.hash() {
393            return None;
394        }
395        Some(Vote {
396            value,
397            round: self.round,
398            public_key: self.public_key,
399            signature: self.signature,
400        })
401    }
402
403    pub fn kind(&self) -> CertificateKind {
404        self.value.kind
405    }
406}
407
408impl MessageBundle {
409    pub fn is_skippable(&self) -> bool {
410        self.messages.iter().all(PostedMessage::is_skippable)
411    }
412
413    pub fn is_tracked(&self) -> bool {
414        let mut tracked = false;
415        for posted_message in &self.messages {
416            match posted_message.kind {
417                MessageKind::Simple | MessageKind::Bouncing => {}
418                MessageKind::Protected => return false,
419                MessageKind::Tracked => tracked = true,
420            }
421        }
422        tracked
423    }
424
425    pub fn is_protected(&self) -> bool {
426        self.messages.iter().any(PostedMessage::is_protected)
427    }
428}
429
430impl PostedMessage {
431    pub fn is_skippable(&self) -> bool {
432        match self.kind {
433            MessageKind::Protected | MessageKind::Tracked => false,
434            MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
435        }
436    }
437
438    pub fn is_protected(&self) -> bool {
439        matches!(self.kind, MessageKind::Protected)
440    }
441
442    pub fn is_tracked(&self) -> bool {
443        matches!(self.kind, MessageKind::Tracked)
444    }
445
446    pub fn is_bouncing(&self) -> bool {
447        matches!(self.kind, MessageKind::Bouncing)
448    }
449}
450
451impl BlockExecutionOutcome {
452    pub fn with(self, block: ProposedBlock) -> Block {
453        Block::new(block, self)
454    }
455
456    pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
457        let mut required_blob_ids = HashSet::new();
458        for responses in &self.oracle_responses {
459            for response in responses {
460                if let OracleResponse::Blob(blob_id) = response {
461                    required_blob_ids.insert(*blob_id);
462                }
463            }
464        }
465
466        required_blob_ids
467    }
468
469    pub fn has_oracle_responses(&self) -> bool {
470        self.oracle_responses
471            .iter()
472            .any(|responses| !responses.is_empty())
473    }
474
475    pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
476        self.blobs.iter().flatten().map(|blob| blob.id())
477    }
478
479    pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
480        self.iter_created_blobs_ids().collect()
481    }
482}
483
484/// The data a block proposer signs.
485#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
486pub struct ProposalContent {
487    /// The proposed block.
488    pub block: ProposedBlock,
489    /// The consensus round in which this proposal is made.
490    pub round: Round,
491    /// If this is a retry from an earlier round, the execution outcome.
492    #[debug(skip_if = Option::is_none)]
493    pub outcome: Option<BlockExecutionOutcome>,
494}
495
496impl BlockProposal {
497    pub async fn new_initial<S: Signer + ?Sized>(
498        owner: AccountOwner,
499        round: Round,
500        block: ProposedBlock,
501        signer: &S,
502    ) -> Result<Self, S::Error> {
503        let content = ProposalContent {
504            round,
505            block,
506            outcome: None,
507        };
508        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
509
510        Ok(Self {
511            content,
512            signature,
513            original_proposal: None,
514        })
515    }
516
517    pub async fn new_retry_fast<S: Signer + ?Sized>(
518        owner: AccountOwner,
519        round: Round,
520        old_proposal: BlockProposal,
521        signer: &S,
522    ) -> Result<Self, S::Error> {
523        let content = ProposalContent {
524            round,
525            block: old_proposal.content.block,
526            outcome: None,
527        };
528        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
529
530        Ok(Self {
531            content,
532            signature,
533            original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
534        })
535    }
536
537    pub async fn new_retry_regular<S: Signer>(
538        owner: AccountOwner,
539        round: Round,
540        validated_block_certificate: ValidatedBlockCertificate,
541        signer: &S,
542    ) -> Result<Self, S::Error> {
543        let certificate = validated_block_certificate.lite_certificate().cloned();
544        let block = validated_block_certificate.into_inner().into_inner();
545        let (block, outcome) = block.into_proposal();
546        let content = ProposalContent {
547            block,
548            round,
549            outcome: Some(outcome),
550        };
551        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
552
553        Ok(Self {
554            content,
555            signature,
556            original_proposal: Some(OriginalProposal::Regular { certificate }),
557        })
558    }
559
560    /// Returns the `AccountOwner` that proposed the block.
561    pub fn owner(&self) -> AccountOwner {
562        match self.signature {
563            AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
564            AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
565            AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
566        }
567    }
568
569    pub fn check_signature(&self) -> Result<(), CryptoError> {
570        self.signature.verify(&self.content)
571    }
572
573    pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
574        self.content.block.published_blob_ids().into_iter().chain(
575            self.content
576                .outcome
577                .iter()
578                .flat_map(|outcome| outcome.oracle_blob_ids()),
579        )
580    }
581
582    pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
583        self.content.block.published_blob_ids().into_iter().chain(
584            self.content.outcome.iter().flat_map(|outcome| {
585                outcome
586                    .oracle_blob_ids()
587                    .into_iter()
588                    .chain(outcome.iter_created_blobs_ids())
589            }),
590        )
591    }
592
593    /// Checks that the original proposal, if present, matches the new one and has a higher round.
594    pub fn check_invariants(&self) -> Result<(), &'static str> {
595        match (&self.original_proposal, &self.content.outcome) {
596            (None, None) => {}
597            (Some(OriginalProposal::Fast(_)), None) => ensure!(
598                self.content.round > Round::Fast,
599                "The new proposal's round must be greater than the original's"
600            ),
601            (None, Some(_))
602            | (Some(OriginalProposal::Fast(_)), Some(_))
603            | (Some(OriginalProposal::Regular { .. }), None) => {
604                return Err("Must contain a validation certificate if and only if \
605                     it contains the execution outcome from a previous round");
606            }
607            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
608                ensure!(
609                    self.content.round > certificate.round,
610                    "The new proposal's round must be greater than the original's"
611                );
612                let block = outcome.clone().with(self.content.block.clone());
613                let value = ValidatedBlock::new(block);
614                ensure!(
615                    certificate.check_value(&value),
616                    "Lite certificate must match the given block and execution outcome"
617                );
618            }
619        }
620        Ok(())
621    }
622}
623
624impl LiteVote {
625    /// Uses the signing key to create a signed object.
626    pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
627        let hash_and_round = VoteValue(value.value_hash, round, value.kind);
628        let signature = ValidatorSignature::new(&hash_and_round, secret_key);
629        Self {
630            value,
631            round,
632            public_key: secret_key.public(),
633            signature,
634        }
635    }
636
637    /// Verifies the signature in the vote.
638    pub fn check(&self) -> Result<(), ChainError> {
639        let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
640        Ok(self.signature.check(&hash_and_round, self.public_key)?)
641    }
642}
643
644pub struct SignatureAggregator<'a, T: CertificateValue> {
645    committee: &'a Committee,
646    weight: u64,
647    used_validators: HashSet<ValidatorPublicKey>,
648    partial: GenericCertificate<T>,
649}
650
651impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
652    /// Starts aggregating signatures for the given value into a certificate.
653    pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
654        Self {
655            committee,
656            weight: 0,
657            used_validators: HashSet::new(),
658            partial: GenericCertificate::new(value, round, Vec::new()),
659        }
660    }
661
662    /// Tries to append a signature to a (partial) certificate. Returns Some(certificate) if a
663    /// quorum was reached. The resulting final certificate is guaranteed to be valid in the sense
664    /// of `check` below. Returns an error if the signed value cannot be aggregated.
665    pub fn append(
666        &mut self,
667        public_key: ValidatorPublicKey,
668        signature: ValidatorSignature,
669    ) -> Result<Option<GenericCertificate<T>>, ChainError>
670    where
671        T: CertificateValue,
672    {
673        let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
674        signature.check(&hash_and_round, public_key)?;
675        // Check that each validator only appears once.
676        ensure!(
677            !self.used_validators.contains(&public_key),
678            ChainError::CertificateValidatorReuse
679        );
680        self.used_validators.insert(public_key);
681        // Update weight.
682        let voting_rights = self.committee.weight(&public_key);
683        ensure!(voting_rights > 0, ChainError::InvalidSigner);
684        self.weight += voting_rights;
685        // Update certificate.
686        self.partial.add_signature((public_key, signature));
687
688        if self.weight >= self.committee.quorum_threshold() {
689            self.weight = 0; // Prevent from creating the certificate twice.
690            Ok(Some(self.partial.clone()))
691        } else {
692            Ok(None)
693        }
694    }
695}
696
697// Checks if the array slice is strictly ordered. That means that if the array
698// has duplicates, this will return False, even if the array is sorted
699pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
700    values.windows(2).all(|pair| pair[0].0 < pair[1].0)
701}
702
703/// Verifies certificate signatures.
704pub(crate) fn check_signatures(
705    value_hash: CryptoHash,
706    certificate_kind: CertificateKind,
707    round: Round,
708    signatures: &[(ValidatorPublicKey, ValidatorSignature)],
709    committee: &Committee,
710) -> Result<(), ChainError> {
711    // Check the quorum.
712    let mut weight = 0;
713    let mut used_validators = HashSet::new();
714    for (validator, _) in signatures {
715        // Check that each validator only appears once.
716        ensure!(
717            !used_validators.contains(validator),
718            ChainError::CertificateValidatorReuse
719        );
720        used_validators.insert(*validator);
721        // Update weight.
722        let voting_rights = committee.weight(validator);
723        ensure!(voting_rights > 0, ChainError::InvalidSigner);
724        weight += voting_rights;
725    }
726    ensure!(
727        weight >= committee.quorum_threshold(),
728        ChainError::CertificateRequiresQuorum
729    );
730    // All that is left is checking signatures!
731    let hash_and_round = VoteValue(value_hash, round, certificate_kind);
732    ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
733    Ok(())
734}
735
736impl BcsSignable<'_> for ProposalContent {}
737
738impl BcsSignable<'_> for VoteValue {}
739
740doc_scalar!(
741    MessageAction,
742    "Whether an incoming message is accepted or rejected."
743);
744
745#[cfg(test)]
746mod signing {
747    use linera_base::{
748        crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
749        data_types::{BlockHeight, Epoch, Round},
750        identifiers::ChainId,
751    };
752
753    use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
754
755    #[test]
756    fn proposal_content_signing() {
757        use std::str::FromStr;
758
759        // Generated in MetaMask.
760        let secret_key = linera_base::crypto::EvmSecretKey::from_str(
761            "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
762        )
763        .unwrap();
764        let address = secret_key.address();
765
766        let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
767        let public_key = signer.public();
768
769        let proposed_block = ProposedBlock {
770            chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
771            epoch: Epoch(11),
772            incoming_bundles: vec![],
773            operations: vec![],
774            height: BlockHeight(11),
775            timestamp: 190000000u64.into(),
776            authenticated_signer: None,
777            previous_block_hash: None,
778        };
779
780        let proposal = ProposalContent {
781            block: proposed_block,
782            round: Round::SingleLeader(11),
783            outcome: None,
784        };
785
786        // personal_sign of the `proposal_hash` done via MetaMask.
787        // Wrap with proper variant so that bytes match (include the enum variant tag).
788        let signature = EvmSignature::from_str(
789            "f2d8afcd51d0f947f5c5e31ac1db73ec5306163af7949b3bb265ba53d03374b0\
790            4b1e909007b555caf098da1aded29c600bee391c6ee8b4d0962a29044555796d1b",
791        )
792        .unwrap();
793        let metamask_signature = AccountSignature::EvmSecp256k1 {
794            signature,
795            address: address.0 .0,
796        };
797
798        let signature = signer.sign(&proposal);
799        assert_eq!(signature, metamask_signature);
800
801        assert_eq!(signature.owner(), public_key.into());
802
803        let block_proposal = BlockProposal {
804            content: proposal,
805            signature,
806            original_proposal: None,
807        };
808        assert_eq!(block_proposal.owner(), public_key.into(),);
809    }
810}