Skip to main content

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