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