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