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