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