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