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