linera_chain/data_types/
mod.rs

1// Copyright (c) Facebook, Inc. and its affiliates.
2// Copyright (c) Zefchain Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, BTreeSet, HashSet};
6
7use allocative::Allocative;
8use async_graphql::SimpleObject;
9use custom_debug_derive::Debug;
10use linera_base::{
11    bcs,
12    crypto::{
13        AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash, Signer,
14        ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
15    },
16    data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
17    doc_scalar, ensure, hex, hex_debug,
18    identifiers::{Account, AccountOwner, ApplicationId, BlobId, ChainId, StreamId},
19};
20use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24    block::{Block, ValidatedBlock},
25    types::{
26        CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
27        ValidatedBlockCertificate,
28    },
29    ChainError,
30};
31
32pub mod metadata;
33
34pub use metadata::*;
35
36#[cfg(test)]
37#[path = "../unit_tests/data_types_tests.rs"]
38mod data_types_tests;
39
40/// A block containing operations to apply on a given chain, as well as the
41/// acknowledgment of a number of incoming messages from other chains.
42/// * Incoming messages must be selected in the order they were
43///   produced by the sending chain, but can be skipped.
44/// * When a block is proposed to a validator, all cross-chain messages must have been
45///   received ahead of time in the inbox of the chain.
46/// * This constraint does not apply to the execution of confirmed blocks.
47#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
48#[graphql(complex)]
49pub struct ProposedBlock {
50    /// The chain to which this block belongs.
51    pub chain_id: ChainId,
52    /// The number identifying the current configuration.
53    pub epoch: Epoch,
54    /// The transactions to execute in this block. Each transaction can be either
55    /// incoming messages or an operation.
56    #[debug(skip_if = Vec::is_empty)]
57    #[graphql(skip)]
58    pub transactions: Vec<Transaction>,
59    /// The block height.
60    pub height: BlockHeight,
61    /// The timestamp when this block was created. This must be later than all messages received
62    /// in this block, but no later than the current time.
63    pub timestamp: Timestamp,
64    /// The user signing for the operations in the block and paying for their execution
65    /// fees. If set, this must be the `owner` in the block proposal. `None` means that
66    /// the default account of the chain is used. This value is also used as recipient of
67    /// potential refunds for the message grants created by the operations.
68    #[debug(skip_if = Option::is_none)]
69    pub authenticated_owner: Option<AccountOwner>,
70    /// Certified hash (see `Certificate` below) of the previous block in the
71    /// chain, if any.
72    pub previous_block_hash: Option<CryptoHash>,
73}
74
75impl ProposedBlock {
76    /// Returns all the published blob IDs in this block's operations.
77    pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
78        self.operations()
79            .flat_map(Operation::published_blob_ids)
80            .collect()
81    }
82
83    /// Returns whether the block contains only rejected incoming messages, which
84    /// makes it admissible even on closed chains.
85    pub fn has_only_rejected_messages(&self) -> bool {
86        self.transactions.iter().all(|txn| {
87            matches!(
88                txn,
89                Transaction::ReceiveMessages(IncomingBundle {
90                    action: MessageAction::Reject,
91                    ..
92                })
93            )
94        })
95    }
96
97    /// Returns an iterator over all incoming [`PostedMessage`]s in this block.
98    pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
99        self.incoming_bundles()
100            .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
101    }
102
103    /// Returns the number of incoming messages.
104    pub fn message_count(&self) -> usize {
105        self.incoming_bundles()
106            .map(|im| im.bundle.messages.len())
107            .sum()
108    }
109
110    /// Returns an iterator over all transactions as references.
111    pub fn transaction_refs(&self) -> impl Iterator<Item = &Transaction> {
112        self.transactions.iter()
113    }
114
115    /// Returns all operations in this block.
116    pub fn operations(&self) -> impl Iterator<Item = &Operation> {
117        self.transactions.iter().filter_map(|tx| match tx {
118            Transaction::ExecuteOperation(operation) => Some(operation),
119            Transaction::ReceiveMessages(_) => None,
120        })
121    }
122
123    /// Returns all incoming bundles in this block.
124    pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
125        self.transactions.iter().filter_map(|tx| match tx {
126            Transaction::ReceiveMessages(bundle) => Some(bundle),
127            Transaction::ExecuteOperation(_) => None,
128        })
129    }
130
131    pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
132        let size = bcs::serialized_size(self)?;
133        ensure!(
134            size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
135            ChainError::BlockProposalTooLarge(size)
136        );
137        Ok(())
138    }
139}
140
141#[async_graphql::ComplexObject]
142impl ProposedBlock {
143    /// Metadata about the transactions in this block.
144    async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
145        self.transactions
146            .iter()
147            .map(TransactionMetadata::from_transaction)
148            .collect()
149    }
150}
151
152/// A transaction in a block: incoming messages or an operation.
153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative)]
154pub enum Transaction {
155    /// Receive a bundle of incoming messages.
156    ReceiveMessages(IncomingBundle),
157    /// Execute an operation.
158    ExecuteOperation(Operation),
159}
160
161impl BcsHashable<'_> for Transaction {}
162
163impl Transaction {
164    pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
165        match self {
166            Transaction::ReceiveMessages(bundle) => Some(bundle),
167            _ => None,
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
272impl BcsHashable<'_> for IncomingBundle {}
273
274/// What to do with a message picked from the inbox.
275#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
276pub enum MessageAction {
277    /// Execute the incoming message.
278    Accept,
279    /// Do not execute the incoming message.
280    Reject,
281}
282
283/// A set of messages from a single block, for a single destination.
284#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
285pub struct MessageBundle {
286    /// The block height.
287    pub height: BlockHeight,
288    /// The block's timestamp.
289    pub timestamp: Timestamp,
290    /// The confirmed block certificate hash.
291    pub certificate_hash: CryptoHash,
292    /// The index of the transaction in the block that is sending this bundle.
293    pub transaction_index: u32,
294    /// The relevant messages.
295    pub messages: Vec<PostedMessage>,
296}
297
298#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
299#[cfg_attr(with_testing, derive(Eq, PartialEq))]
300/// An earlier proposal that is being retried.
301pub enum OriginalProposal {
302    /// A proposal in the fast round.
303    Fast(AccountSignature),
304    /// A validated block certificate from an earlier round.
305    Regular {
306        certificate: LiteCertificate<'static>,
307    },
308}
309
310/// An authenticated proposal for a new block.
311// TODO(#456): the signature of the block owner is currently lost but it would be useful
312// to have it for auditing purposes.
313#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
314#[cfg_attr(with_testing, derive(Eq, PartialEq))]
315pub struct BlockProposal {
316    pub content: ProposalContent,
317    pub signature: AccountSignature,
318    #[debug(skip_if = Option::is_none)]
319    pub original_proposal: Option<OriginalProposal>,
320}
321
322/// A message together with kind, authentication and grant information.
323#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
324#[graphql(complex)]
325pub struct PostedMessage {
326    /// The user authentication carried by the message, if any.
327    #[debug(skip_if = Option::is_none)]
328    pub authenticated_owner: Option<AccountOwner>,
329    /// A grant to pay for the message execution.
330    #[debug(skip_if = Amount::is_zero)]
331    pub grant: Amount,
332    /// Where to send a refund for the unused part of the grant after execution, if any.
333    #[debug(skip_if = Option::is_none)]
334    pub refund_grant_to: Option<Account>,
335    /// The kind of message being sent.
336    pub kind: MessageKind,
337    /// The index of the message in the sending block.
338    pub index: u32,
339    /// The message itself.
340    pub message: Message,
341}
342
343pub trait OutgoingMessageExt {
344    /// Returns the posted message, i.e. the outgoing message without the destination.
345    fn into_posted(self, index: u32) -> PostedMessage;
346}
347
348impl OutgoingMessageExt for OutgoingMessage {
349    /// Returns the posted message, i.e. the outgoing message without the destination.
350    fn into_posted(self, index: u32) -> PostedMessage {
351        let OutgoingMessage {
352            destination: _,
353            authenticated_owner,
354            grant,
355            refund_grant_to,
356            kind,
357            message,
358        } = self;
359        PostedMessage {
360            authenticated_owner,
361            grant,
362            refund_grant_to,
363            kind,
364            index,
365            message,
366        }
367    }
368}
369
370#[async_graphql::ComplexObject]
371impl PostedMessage {
372    /// Structured message metadata for GraphQL.
373    async fn message_metadata(&self) -> MessageMetadata {
374        MessageMetadata::from(&self.message)
375    }
376}
377
378/// The execution result of a single operation.
379#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
380pub struct OperationResult(
381    #[debug(with = "hex_debug")]
382    #[serde(with = "serde_bytes")]
383    pub Vec<u8>,
384);
385
386impl BcsHashable<'_> for OperationResult {}
387
388doc_scalar!(
389    OperationResult,
390    "The execution result of a single operation."
391);
392
393/// The messages and the state hash resulting from a [`ProposedBlock`]'s execution.
394#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
395#[cfg_attr(with_testing, derive(Default))]
396pub struct BlockExecutionOutcome {
397    /// The list of outgoing messages for each transaction.
398    pub messages: Vec<Vec<OutgoingMessage>>,
399    /// The hashes and heights of previous blocks that sent messages to the same recipients.
400    pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
401    /// The hashes and heights of previous blocks that published events to the same channels.
402    pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
403    /// The hash of the chain's execution state after this block.
404    pub state_hash: CryptoHash,
405    /// The record of oracle responses for each transaction.
406    pub oracle_responses: Vec<Vec<OracleResponse>>,
407    /// The list of events produced by each transaction.
408    pub events: Vec<Vec<Event>>,
409    /// The list of blobs created by each transaction.
410    pub blobs: Vec<Vec<Blob>>,
411    /// The execution result for each operation.
412    pub operation_results: Vec<OperationResult>,
413}
414
415/// The hash and chain ID of a `CertificateValue`.
416#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
417pub struct LiteValue {
418    pub value_hash: CryptoHash,
419    pub chain_id: ChainId,
420    pub kind: CertificateKind,
421}
422
423impl LiteValue {
424    pub fn new<T: CertificateValue>(value: &T) -> Self {
425        LiteValue {
426            value_hash: value.hash(),
427            chain_id: value.chain_id(),
428            kind: T::KIND,
429        }
430    }
431}
432
433#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
434struct VoteValue(CryptoHash, Round, CertificateKind);
435
436/// A vote on a statement from a validator.
437#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
438#[serde(bound(deserialize = "T: Deserialize<'de>"))]
439pub struct Vote<T> {
440    pub value: T,
441    pub round: Round,
442    pub signature: ValidatorSignature,
443}
444
445impl<T> Vote<T> {
446    /// Use signing key to create a signed object.
447    pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
448    where
449        T: CertificateValue,
450    {
451        let hash_and_round = VoteValue(value.hash(), round, T::KIND);
452        let signature = ValidatorSignature::new(&hash_and_round, key_pair);
453        Self {
454            value,
455            round,
456            signature,
457        }
458    }
459
460    /// Returns the vote, with a `LiteValue` instead of the full value.
461    pub fn lite(&self) -> LiteVote
462    where
463        T: CertificateValue,
464    {
465        LiteVote {
466            value: LiteValue::new(&self.value),
467            round: self.round,
468            signature: self.signature,
469        }
470    }
471
472    /// Returns the value this vote is for.
473    pub fn value(&self) -> &T {
474        &self.value
475    }
476}
477
478/// A vote on a statement from a validator, represented as a `LiteValue`.
479#[derive(Clone, Debug, Serialize, Deserialize)]
480#[cfg_attr(with_testing, derive(Eq, PartialEq))]
481pub struct LiteVote {
482    pub value: LiteValue,
483    pub round: Round,
484    pub signature: ValidatorSignature,
485}
486
487impl LiteVote {
488    /// Returns the full vote, with the value, if it matches.
489    #[cfg(with_testing)]
490    pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
491        if self.value.value_hash != value.hash() {
492            return None;
493        }
494        Some(Vote {
495            value,
496            round: self.round,
497            signature: self.signature,
498        })
499    }
500
501    pub fn kind(&self) -> CertificateKind {
502        self.value.kind
503    }
504}
505
506impl MessageBundle {
507    pub fn is_skippable(&self) -> bool {
508        self.messages.iter().all(PostedMessage::is_skippable)
509    }
510
511    pub fn is_protected(&self) -> bool {
512        self.messages.iter().any(PostedMessage::is_protected)
513    }
514}
515
516impl PostedMessage {
517    pub fn is_skippable(&self) -> bool {
518        match self.kind {
519            MessageKind::Protected | MessageKind::Tracked => false,
520            MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
521        }
522    }
523
524    pub fn is_protected(&self) -> bool {
525        matches!(self.kind, MessageKind::Protected)
526    }
527
528    pub fn is_tracked(&self) -> bool {
529        matches!(self.kind, MessageKind::Tracked)
530    }
531
532    pub fn is_bouncing(&self) -> bool {
533        matches!(self.kind, MessageKind::Bouncing)
534    }
535}
536
537impl BlockExecutionOutcome {
538    pub fn with(self, block: ProposedBlock) -> Block {
539        Block::new(block, self)
540    }
541
542    pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
543        let mut required_blob_ids = HashSet::new();
544        for responses in &self.oracle_responses {
545            for response in responses {
546                if let OracleResponse::Blob(blob_id) = response {
547                    required_blob_ids.insert(*blob_id);
548                }
549            }
550        }
551
552        required_blob_ids
553    }
554
555    pub fn has_oracle_responses(&self) -> bool {
556        self.oracle_responses
557            .iter()
558            .any(|responses| !responses.is_empty())
559    }
560
561    pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
562        self.blobs.iter().flatten().map(|blob| blob.id())
563    }
564
565    pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
566        self.iter_created_blobs_ids().collect()
567    }
568}
569
570/// The data a block proposer signs.
571#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
572pub struct ProposalContent {
573    /// The proposed block.
574    pub block: ProposedBlock,
575    /// The consensus round in which this proposal is made.
576    pub round: Round,
577    /// If this is a retry from an earlier round, the execution outcome.
578    #[debug(skip_if = Option::is_none)]
579    pub outcome: Option<BlockExecutionOutcome>,
580}
581
582impl BlockProposal {
583    pub async fn new_initial<S: Signer + ?Sized>(
584        owner: AccountOwner,
585        round: Round,
586        block: ProposedBlock,
587        signer: &S,
588    ) -> Result<Self, S::Error> {
589        let content = ProposalContent {
590            round,
591            block,
592            outcome: None,
593        };
594        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
595
596        Ok(Self {
597            content,
598            signature,
599            original_proposal: None,
600        })
601    }
602
603    pub async fn new_retry_fast<S: Signer + ?Sized>(
604        owner: AccountOwner,
605        round: Round,
606        old_proposal: BlockProposal,
607        signer: &S,
608    ) -> Result<Self, S::Error> {
609        let content = ProposalContent {
610            round,
611            block: old_proposal.content.block,
612            outcome: None,
613        };
614        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
615
616        Ok(Self {
617            content,
618            signature,
619            original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
620        })
621    }
622
623    pub async fn new_retry_regular<S: Signer>(
624        owner: AccountOwner,
625        round: Round,
626        validated_block_certificate: ValidatedBlockCertificate,
627        signer: &S,
628    ) -> Result<Self, S::Error> {
629        let certificate = validated_block_certificate.lite_certificate().cloned();
630        let block = validated_block_certificate.into_inner().into_inner();
631        let (block, outcome) = block.into_proposal();
632        let content = ProposalContent {
633            block,
634            round,
635            outcome: Some(outcome),
636        };
637        let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
638
639        Ok(Self {
640            content,
641            signature,
642            original_proposal: Some(OriginalProposal::Regular { certificate }),
643        })
644    }
645
646    /// Returns the `AccountOwner` that proposed the block.
647    pub fn owner(&self) -> AccountOwner {
648        match self.signature {
649            AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
650            AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
651            AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
652        }
653    }
654
655    pub fn check_signature(&self) -> Result<(), CryptoError> {
656        self.signature.verify(&self.content)
657    }
658
659    pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
660        self.content.block.published_blob_ids().into_iter().chain(
661            self.content
662                .outcome
663                .iter()
664                .flat_map(|outcome| outcome.oracle_blob_ids()),
665        )
666    }
667
668    pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
669        self.content.block.published_blob_ids().into_iter().chain(
670            self.content.outcome.iter().flat_map(|outcome| {
671                outcome
672                    .oracle_blob_ids()
673                    .into_iter()
674                    .chain(outcome.iter_created_blobs_ids())
675            }),
676        )
677    }
678
679    /// Checks that the original proposal, if present, matches the new one and has a higher round.
680    pub fn check_invariants(&self) -> Result<(), &'static str> {
681        match (&self.original_proposal, &self.content.outcome) {
682            (None, None) => {}
683            (Some(OriginalProposal::Fast(_)), None) => ensure!(
684                self.content.round > Round::Fast,
685                "The new proposal's round must be greater than the original's"
686            ),
687            (None, Some(_))
688            | (Some(OriginalProposal::Fast(_)), Some(_))
689            | (Some(OriginalProposal::Regular { .. }), None) => {
690                return Err("Must contain a validation certificate if and only if \
691                     it contains the execution outcome from a previous round");
692            }
693            (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
694                ensure!(
695                    self.content.round > certificate.round,
696                    "The new proposal's round must be greater than the original's"
697                );
698                let block = outcome.clone().with(self.content.block.clone());
699                let value = ValidatedBlock::new(block);
700                ensure!(
701                    certificate.check_value(&value),
702                    "Lite certificate must match the given block and execution outcome"
703                );
704            }
705        }
706        Ok(())
707    }
708}
709
710impl LiteVote {
711    /// Uses the signing key to create a signed object.
712    pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
713        let hash_and_round = VoteValue(value.value_hash, round, value.kind);
714        let signature = ValidatorSignature::new(&hash_and_round, secret_key);
715        Self {
716            value,
717            round,
718            signature,
719        }
720    }
721
722    /// Verifies the signature in the vote.
723    pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
724        let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
725        Ok(self.signature.check(&hash_and_round, public_key)?)
726    }
727}
728
729pub struct SignatureAggregator<'a, T: CertificateValue> {
730    committee: &'a Committee,
731    weight: u64,
732    used_validators: HashSet<ValidatorPublicKey>,
733    partial: GenericCertificate<T>,
734}
735
736impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
737    /// Starts aggregating signatures for the given value into a certificate.
738    pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
739        Self {
740            committee,
741            weight: 0,
742            used_validators: HashSet::new(),
743            partial: GenericCertificate::new(value, round, Vec::new()),
744        }
745    }
746
747    /// Tries to append a signature to a (partial) certificate. Returns Some(certificate) if a
748    /// quorum was reached. The resulting final certificate is guaranteed to be valid in the sense
749    /// of `check` below. Returns an error if the signed value cannot be aggregated.
750    pub fn append(
751        &mut self,
752        public_key: ValidatorPublicKey,
753        signature: ValidatorSignature,
754    ) -> Result<Option<GenericCertificate<T>>, ChainError>
755    where
756        T: CertificateValue,
757    {
758        let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
759        signature.check(&hash_and_round, public_key)?;
760        // Check that each validator only appears once.
761        ensure!(
762            !self.used_validators.contains(&public_key),
763            ChainError::CertificateValidatorReuse
764        );
765        self.used_validators.insert(public_key);
766        // Update weight.
767        let voting_rights = self.committee.weight(&public_key);
768        ensure!(voting_rights > 0, ChainError::InvalidSigner);
769        self.weight += voting_rights;
770        // Update certificate.
771        self.partial.add_signature((public_key, signature));
772
773        if self.weight >= self.committee.quorum_threshold() {
774            self.weight = 0; // Prevent from creating the certificate twice.
775            Ok(Some(self.partial.clone()))
776        } else {
777            Ok(None)
778        }
779    }
780}
781
782// Checks if the array slice is strictly ordered. That means that if the array
783// has duplicates, this will return False, even if the array is sorted
784pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
785    values.windows(2).all(|pair| pair[0].0 < pair[1].0)
786}
787
788/// Verifies certificate signatures.
789pub(crate) fn check_signatures(
790    value_hash: CryptoHash,
791    certificate_kind: CertificateKind,
792    round: Round,
793    signatures: &[(ValidatorPublicKey, ValidatorSignature)],
794    committee: &Committee,
795) -> Result<(), ChainError> {
796    // Check the quorum.
797    let mut weight = 0;
798    let mut used_validators = HashSet::new();
799    for (validator, _) in signatures {
800        // Check that each validator only appears once.
801        ensure!(
802            !used_validators.contains(validator),
803            ChainError::CertificateValidatorReuse
804        );
805        used_validators.insert(*validator);
806        // Update weight.
807        let voting_rights = committee.weight(validator);
808        ensure!(voting_rights > 0, ChainError::InvalidSigner);
809        weight += voting_rights;
810    }
811    ensure!(
812        weight >= committee.quorum_threshold(),
813        ChainError::CertificateRequiresQuorum
814    );
815    // All that is left is checking signatures!
816    let hash_and_round = VoteValue(value_hash, round, certificate_kind);
817    ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
818    Ok(())
819}
820
821impl BcsSignable<'_> for ProposalContent {}
822
823impl BcsSignable<'_> for VoteValue {}
824
825doc_scalar!(
826    MessageAction,
827    "Whether an incoming message is accepted or rejected."
828);
829
830#[cfg(test)]
831mod signing {
832    use linera_base::{
833        crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
834        data_types::{BlockHeight, Epoch, Round},
835        identifiers::ChainId,
836    };
837
838    use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
839
840    #[test]
841    fn proposal_content_signing() {
842        use std::str::FromStr;
843
844        // Generated in MetaMask.
845        let secret_key = linera_base::crypto::EvmSecretKey::from_str(
846            "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
847        )
848        .unwrap();
849        let address = secret_key.address();
850
851        let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
852        let public_key = signer.public();
853
854        let proposed_block = ProposedBlock {
855            chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
856            epoch: Epoch(11),
857            transactions: vec![],
858            height: BlockHeight(11),
859            timestamp: 190000000u64.into(),
860            authenticated_owner: None,
861            previous_block_hash: None,
862        };
863
864        let proposal = ProposalContent {
865            block: proposed_block,
866            round: Round::SingleLeader(11),
867            outcome: None,
868        };
869
870        // personal_sign of the `proposal_hash` done via MetaMask.
871        // Wrap with proper variant so that bytes match (include the enum variant tag).
872        let signature = EvmSignature::from_str(
873            "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
874            3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
875        )
876        .unwrap();
877        let metamask_signature = AccountSignature::EvmSecp256k1 {
878            signature,
879            address: address.0 .0,
880        };
881
882        let signature = signer.sign(&proposal);
883        assert_eq!(signature, metamask_signature);
884
885        assert_eq!(signature.owner(), public_key.into());
886
887        let block_proposal = BlockProposal {
888            content: proposal,
889            signature,
890            original_proposal: None,
891        };
892        assert_eq!(block_proposal.owner(), public_key.into(),);
893    }
894}