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