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