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