1use 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::{
17 Amount, Blob, BlockHeight, Epoch, Event, MessagePolicy, OracleResponse, Round, Timestamp,
18 },
19 doc_scalar, ensure, hex, hex_debug,
20 identifiers::{Account, AccountOwner, ApplicationId, BlobId, ChainId, StreamId},
21 time::Duration,
22};
23use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
24use serde::{Deserialize, Serialize};
25use tracing::instrument;
26
27use crate::{
28 block::{Block, ValidatedBlock},
29 types::{
30 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
31 ValidatedBlockCertificate,
32 },
33 ChainError,
34};
35
36pub mod metadata;
37
38pub use metadata::*;
39
40#[cfg(test)]
41#[path = "../unit_tests/data_types_tests.rs"]
42mod data_types_tests;
43
44#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
52#[graphql(complex)]
53pub struct ProposedBlock {
54 pub chain_id: ChainId,
56 pub epoch: Epoch,
58 #[debug(skip_if = Vec::is_empty)]
61 #[graphql(skip)]
62 pub transactions: Vec<Transaction>,
63 pub height: BlockHeight,
65 pub timestamp: Timestamp,
68 #[debug(skip_if = Option::is_none)]
73 pub authenticated_signer: Option<AccountOwner>,
74 pub previous_block_hash: Option<CryptoHash>,
77}
78
79impl ProposedBlock {
80 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
82 self.operations()
83 .flat_map(Operation::published_blob_ids)
84 .collect()
85 }
86
87 pub fn has_only_rejected_messages(&self) -> bool {
90 self.transactions.iter().all(|txn| {
91 matches!(
92 txn,
93 Transaction::ReceiveMessages(IncomingBundle {
94 action: MessageAction::Reject,
95 ..
96 })
97 )
98 })
99 }
100
101 pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
103 self.incoming_bundles()
104 .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
105 }
106
107 pub fn message_count(&self) -> usize {
109 self.incoming_bundles()
110 .map(|im| im.bundle.messages.len())
111 .sum()
112 }
113
114 pub fn transaction_refs(&self) -> impl Iterator<Item = &Transaction> {
116 self.transactions.iter()
117 }
118
119 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
121 self.transactions.iter().filter_map(|tx| match tx {
122 Transaction::ExecuteOperation(operation) => Some(operation),
123 Transaction::ReceiveMessages(_) => None,
124 })
125 }
126
127 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
129 self.transactions.iter().filter_map(|tx| match tx {
130 Transaction::ReceiveMessages(bundle) => Some(bundle),
131 Transaction::ExecuteOperation(_) => None,
132 })
133 }
134
135 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
136 let size = bcs::serialized_size(self)?;
137 ensure!(
138 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
139 ChainError::BlockProposalTooLarge(size)
140 );
141 Ok(())
142 }
143}
144
145#[async_graphql::ComplexObject]
146impl ProposedBlock {
147 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
149 self.transactions
150 .iter()
151 .map(TransactionMetadata::from_transaction)
152 .collect()
153 }
154}
155
156#[derive(
158 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative, strum::AsRefStr,
159)]
160pub enum Transaction {
161 ReceiveMessages(IncomingBundle),
163 ExecuteOperation(Operation),
165}
166
167impl BcsHashable<'_> for Transaction {}
168
169impl Transaction {
170 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
171 match self {
172 Transaction::ReceiveMessages(bundle) => Some(bundle),
173 _ => None,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
179#[graphql(name = "Operation")]
180pub struct OperationMetadata {
181 pub operation_type: String,
183 pub application_id: Option<ApplicationId>,
185 pub user_bytes_hex: Option<String>,
187 pub system_operation: Option<SystemOperationMetadata>,
189}
190
191impl From<&Operation> for OperationMetadata {
192 fn from(operation: &Operation) -> Self {
193 match operation {
194 Operation::System(sys_op) => OperationMetadata {
195 operation_type: "System".to_string(),
196 application_id: None,
197 user_bytes_hex: None,
198 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
199 },
200 Operation::User {
201 application_id,
202 bytes,
203 } => OperationMetadata {
204 operation_type: "User".to_string(),
205 application_id: Some(*application_id),
206 user_bytes_hex: Some(hex::encode(bytes)),
207 system_operation: None,
208 },
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
215pub struct TransactionMetadata {
216 pub transaction_type: String,
218 pub incoming_bundle: Option<IncomingBundle>,
220 pub operation: Option<OperationMetadata>,
222}
223
224impl TransactionMetadata {
225 pub fn from_transaction(transaction: &Transaction) -> Self {
226 match transaction {
227 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
228 transaction_type: "ReceiveMessages".to_string(),
229 incoming_bundle: Some(bundle.clone()),
230 operation: None,
231 },
232 Transaction::ExecuteOperation(op) => TransactionMetadata {
233 transaction_type: "ExecuteOperation".to_string(),
234 incoming_bundle: None,
235 operation: Some(OperationMetadata::from(op)),
236 },
237 }
238 }
239}
240
241#[derive(
243 Debug,
244 Clone,
245 Copy,
246 Eq,
247 PartialEq,
248 Ord,
249 PartialOrd,
250 Serialize,
251 Deserialize,
252 SimpleObject,
253 Allocative,
254)]
255pub struct ChainAndHeight {
256 pub chain_id: ChainId,
257 pub height: BlockHeight,
258}
259
260#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
262pub struct IncomingBundle {
263 pub origin: ChainId,
265 pub bundle: MessageBundle,
267 pub action: MessageAction,
269}
270
271impl IncomingBundle {
272 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
274 self.bundle.messages.iter()
275 }
276
277 #[instrument(level = "trace", skip(self))]
278 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
279 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
280 if !chain_ids.contains(&self.origin) {
281 return None;
282 }
283 }
284 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
285 if !self
286 .messages()
287 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
288 {
289 return None;
290 }
291 }
292 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
293 if !self
294 .messages()
295 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
296 {
297 return None;
298 }
299 }
300 if policy.is_reject() {
301 if self.bundle.is_skippable() {
302 return None;
303 } else if !self.bundle.is_protected() {
304 self.action = MessageAction::Reject;
305 }
306 }
307 Some(self)
308 }
309}
310
311impl BcsHashable<'_> for IncomingBundle {}
312
313#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
315pub enum MessageAction {
316 Accept,
318 Reject,
320}
321
322#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
324pub enum BundleFailurePolicy {
325 #[default]
327 Abort,
328 AutoRetry {
339 max_failures: u32,
341 },
342}
343
344#[derive(Copy, Clone, Debug, PartialEq, Eq)]
346pub struct BundleExecutionPolicy {
347 pub on_failure: BundleFailurePolicy,
349 pub time_budget: Option<Duration>,
353}
354
355impl BundleExecutionPolicy {
356 pub fn committed() -> Self {
358 BundleExecutionPolicy {
359 on_failure: BundleFailurePolicy::Abort,
360 time_budget: None,
361 }
362 }
363}
364
365#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
367pub struct MessageBundle {
368 pub height: BlockHeight,
370 pub timestamp: Timestamp,
372 pub certificate_hash: CryptoHash,
374 pub transaction_index: u32,
376 pub messages: Vec<PostedMessage>,
378}
379
380impl MessageBundle {
381 pub fn estimated_size(&self) -> usize {
383 let overhead = 60;
385 let messages_size: usize = self
386 .messages
387 .iter()
388 .map(PostedMessage::estimated_size)
389 .sum();
390 overhead + messages_size
391 }
392}
393
394impl PostedMessage {
395 pub fn estimated_size(&self) -> usize {
397 let overhead = 96;
399 let message_size = match &self.message {
400 Message::System(_) => 256, Message::User { bytes, .. } => 64 + bytes.len(),
402 };
403 overhead + message_size
404 }
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
408#[cfg_attr(with_testing, derive(Eq, PartialEq))]
409pub enum OriginalProposal {
411 Fast(AccountSignature),
413 Regular {
415 certificate: LiteCertificate<'static>,
416 },
417}
418
419#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
423#[cfg_attr(with_testing, derive(Eq, PartialEq))]
424pub struct BlockProposal {
425 pub content: ProposalContent,
426 pub signature: AccountSignature,
427 #[debug(skip_if = Option::is_none)]
428 pub original_proposal: Option<OriginalProposal>,
429}
430
431#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
433#[graphql(complex)]
434pub struct PostedMessage {
435 #[debug(skip_if = Option::is_none)]
437 pub authenticated_signer: Option<AccountOwner>,
438 #[debug(skip_if = Amount::is_zero)]
440 pub grant: Amount,
441 #[debug(skip_if = Option::is_none)]
443 pub refund_grant_to: Option<Account>,
444 pub kind: MessageKind,
446 pub index: u32,
448 pub message: Message,
450}
451
452pub trait OutgoingMessageExt {
453 fn into_posted(self, index: u32) -> PostedMessage;
455}
456
457impl OutgoingMessageExt for OutgoingMessage {
458 fn into_posted(self, index: u32) -> PostedMessage {
460 let OutgoingMessage {
461 destination: _,
462 authenticated_signer,
463 grant,
464 refund_grant_to,
465 kind,
466 message,
467 } = self;
468 PostedMessage {
469 authenticated_signer,
470 grant,
471 refund_grant_to,
472 kind,
473 index,
474 message,
475 }
476 }
477}
478
479#[async_graphql::ComplexObject]
480impl PostedMessage {
481 async fn message_metadata(&self) -> MessageMetadata {
483 MessageMetadata::from(&self.message)
484 }
485}
486
487#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
489pub struct OperationResult(
490 #[debug(with = "hex_debug")]
491 #[serde(with = "serde_bytes")]
492 pub Vec<u8>,
493);
494
495impl BcsHashable<'_> for OperationResult {}
496
497doc_scalar!(
498 OperationResult,
499 "The execution result of a single operation."
500);
501
502#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
504#[cfg_attr(with_testing, derive(Default))]
505pub struct BlockExecutionOutcome {
506 pub messages: Vec<Vec<OutgoingMessage>>,
508 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
510 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
512 pub state_hash: CryptoHash,
514 pub oracle_responses: Vec<Vec<OracleResponse>>,
516 pub events: Vec<Vec<Event>>,
518 pub blobs: Vec<Vec<Blob>>,
520 pub operation_results: Vec<OperationResult>,
522}
523
524#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
526pub struct LiteValue {
527 pub value_hash: CryptoHash,
528 pub chain_id: ChainId,
529 pub kind: CertificateKind,
530}
531
532impl LiteValue {
533 pub fn new<T: CertificateValue>(value: &T) -> Self {
534 LiteValue {
535 value_hash: value.hash(),
536 chain_id: value.chain_id(),
537 kind: T::KIND,
538 }
539 }
540}
541
542#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
544pub struct VoteValue(CryptoHash, Round, CertificateKind);
545
546#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
548#[serde(bound(deserialize = "T: Deserialize<'de>"))]
549pub struct Vote<T> {
550 pub value: T,
551 pub round: Round,
552 pub signature: ValidatorSignature,
553}
554
555impl<T> Vote<T> {
556 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
558 where
559 T: CertificateValue,
560 {
561 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
562 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
563 Self {
564 value,
565 round,
566 signature,
567 }
568 }
569
570 pub fn lite(&self) -> LiteVote
572 where
573 T: CertificateValue,
574 {
575 LiteVote {
576 value: LiteValue::new(&self.value),
577 round: self.round,
578 signature: self.signature,
579 }
580 }
581
582 pub fn value(&self) -> &T {
584 &self.value
585 }
586}
587
588#[derive(Clone, Debug, Serialize, Deserialize)]
590#[cfg_attr(with_testing, derive(Eq, PartialEq))]
591pub struct LiteVote {
592 pub value: LiteValue,
593 pub round: Round,
594 pub signature: ValidatorSignature,
595}
596
597impl LiteVote {
598 #[cfg(with_testing)]
600 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
601 if self.value.value_hash != value.hash() {
602 return None;
603 }
604 Some(Vote {
605 value,
606 round: self.round,
607 signature: self.signature,
608 })
609 }
610
611 pub fn kind(&self) -> CertificateKind {
612 self.value.kind
613 }
614}
615
616impl MessageBundle {
617 pub fn is_skippable(&self) -> bool {
618 self.messages.iter().all(PostedMessage::is_skippable)
619 }
620
621 pub fn is_protected(&self) -> bool {
622 self.messages.iter().any(PostedMessage::is_protected)
623 }
624}
625
626impl PostedMessage {
627 pub fn is_skippable(&self) -> bool {
628 match self.kind {
629 MessageKind::Protected | MessageKind::Tracked => false,
630 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
631 }
632 }
633
634 pub fn is_protected(&self) -> bool {
635 matches!(self.kind, MessageKind::Protected)
636 }
637
638 pub fn is_tracked(&self) -> bool {
639 matches!(self.kind, MessageKind::Tracked)
640 }
641
642 pub fn is_bouncing(&self) -> bool {
643 matches!(self.kind, MessageKind::Bouncing)
644 }
645}
646
647impl BlockExecutionOutcome {
648 pub fn with(self, block: ProposedBlock) -> Block {
649 Block::new(block, self)
650 }
651
652 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
653 let mut required_blob_ids = HashSet::new();
654 for responses in &self.oracle_responses {
655 for response in responses {
656 if let OracleResponse::Blob(blob_id) = response {
657 required_blob_ids.insert(*blob_id);
658 }
659 }
660 }
661
662 required_blob_ids
663 }
664
665 pub fn has_oracle_responses(&self) -> bool {
666 self.oracle_responses
667 .iter()
668 .any(|responses| !responses.is_empty())
669 }
670
671 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
672 self.blobs.iter().flatten().map(|blob| blob.id())
673 }
674
675 pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
676 self.iter_created_blobs_ids().collect()
677 }
678}
679
680#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
682pub struct ProposalContent {
683 pub block: ProposedBlock,
685 pub round: Round,
687 #[debug(skip_if = Option::is_none)]
689 pub outcome: Option<BlockExecutionOutcome>,
690}
691
692impl BlockProposal {
693 pub async fn new_initial<S: Signer + ?Sized>(
694 owner: AccountOwner,
695 round: Round,
696 block: ProposedBlock,
697 signer: &S,
698 ) -> Result<Self, S::Error> {
699 let content = ProposalContent {
700 round,
701 block,
702 outcome: None,
703 };
704 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
705
706 Ok(Self {
707 content,
708 signature,
709 original_proposal: None,
710 })
711 }
712
713 pub async fn new_retry_fast<S: Signer + ?Sized>(
714 owner: AccountOwner,
715 round: Round,
716 old_proposal: BlockProposal,
717 signer: &S,
718 ) -> Result<Self, S::Error> {
719 let content = ProposalContent {
720 round,
721 block: old_proposal.content.block,
722 outcome: None,
723 };
724 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
725
726 Ok(Self {
727 content,
728 signature,
729 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
730 })
731 }
732
733 pub async fn new_retry_regular<S: Signer>(
734 owner: AccountOwner,
735 round: Round,
736 validated_block_certificate: ValidatedBlockCertificate,
737 signer: &S,
738 ) -> Result<Self, S::Error> {
739 let certificate = validated_block_certificate.lite_certificate().cloned();
740 let block = validated_block_certificate.into_inner().into_inner();
741 let (block, outcome) = block.into_proposal();
742 let content = ProposalContent {
743 block,
744 round,
745 outcome: Some(outcome),
746 };
747 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
748
749 Ok(Self {
750 content,
751 signature,
752 original_proposal: Some(OriginalProposal::Regular { certificate }),
753 })
754 }
755
756 pub fn owner(&self) -> AccountOwner {
758 match self.signature {
759 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
760 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
761 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
762 }
763 }
764
765 pub fn check_signature(&self) -> Result<(), CryptoError> {
766 self.signature.verify(&self.content)
767 }
768
769 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
770 self.content.block.published_blob_ids().into_iter().chain(
771 self.content
772 .outcome
773 .iter()
774 .flat_map(|outcome| outcome.oracle_blob_ids()),
775 )
776 }
777
778 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
779 self.content.block.published_blob_ids().into_iter().chain(
780 self.content.outcome.iter().flat_map(|outcome| {
781 outcome
782 .oracle_blob_ids()
783 .into_iter()
784 .chain(outcome.iter_created_blobs_ids())
785 }),
786 )
787 }
788
789 pub fn check_invariants(&self) -> Result<(), &'static str> {
791 match (&self.original_proposal, &self.content.outcome) {
792 (None, None) => {}
793 (Some(OriginalProposal::Fast(_)), None) => ensure!(
794 self.content.round > Round::Fast,
795 "The new proposal's round must be greater than the original's"
796 ),
797 (None, Some(_))
798 | (Some(OriginalProposal::Fast(_)), Some(_))
799 | (Some(OriginalProposal::Regular { .. }), None) => {
800 return Err("Must contain a validation certificate if and only if \
801 it contains the execution outcome from a previous round");
802 }
803 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
804 ensure!(
805 self.content.round > certificate.round,
806 "The new proposal's round must be greater than the original's"
807 );
808 let block = outcome.clone().with(self.content.block.clone());
809 let value = ValidatedBlock::new(block);
810 ensure!(
811 certificate.check_value(&value),
812 "Lite certificate must match the given block and execution outcome"
813 );
814 }
815 }
816 Ok(())
817 }
818}
819
820impl LiteVote {
821 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
823 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
824 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
825 Self {
826 value,
827 round,
828 signature,
829 }
830 }
831
832 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
834 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
835 Ok(self.signature.check(&hash_and_round, public_key)?)
836 }
837}
838
839pub struct SignatureAggregator<'a, T: CertificateValue> {
840 committee: &'a Committee,
841 weight: u64,
842 used_validators: HashSet<ValidatorPublicKey>,
843 partial: GenericCertificate<T>,
844}
845
846impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
847 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
849 Self {
850 committee,
851 weight: 0,
852 used_validators: HashSet::new(),
853 partial: GenericCertificate::new(value, round, Vec::new()),
854 }
855 }
856
857 pub fn append(
861 &mut self,
862 public_key: ValidatorPublicKey,
863 signature: ValidatorSignature,
864 ) -> Result<Option<GenericCertificate<T>>, ChainError>
865 where
866 T: CertificateValue,
867 {
868 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
869 signature.check(&hash_and_round, public_key)?;
870 ensure!(
872 !self.used_validators.contains(&public_key),
873 ChainError::CertificateValidatorReuse
874 );
875 self.used_validators.insert(public_key);
876 let voting_rights = self.committee.weight(&public_key);
878 ensure!(voting_rights > 0, ChainError::InvalidSigner);
879 self.weight += voting_rights;
880 self.partial.add_signature((public_key, signature));
882
883 if self.weight >= self.committee.quorum_threshold() {
884 self.weight = 0; Ok(Some(self.partial.clone()))
886 } else {
887 Ok(None)
888 }
889 }
890}
891
892pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
895 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
896}
897
898pub(crate) fn check_signatures(
900 value_hash: CryptoHash,
901 certificate_kind: CertificateKind,
902 round: Round,
903 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
904 committee: &Committee,
905) -> Result<(), ChainError> {
906 let mut weight = 0;
908 let mut used_validators = HashSet::new();
909 for (validator, _) in signatures {
910 ensure!(
912 !used_validators.contains(validator),
913 ChainError::CertificateValidatorReuse
914 );
915 used_validators.insert(*validator);
916 let voting_rights = committee.weight(validator);
918 ensure!(voting_rights > 0, ChainError::InvalidSigner);
919 weight += voting_rights;
920 }
921 ensure!(
922 weight >= committee.quorum_threshold(),
923 ChainError::CertificateRequiresQuorum
924 );
925 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
927 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
928 Ok(())
929}
930
931impl BcsSignable<'_> for ProposalContent {}
932
933impl BcsSignable<'_> for VoteValue {}
934
935doc_scalar!(
936 MessageAction,
937 "Whether an incoming message is accepted or rejected."
938);
939
940#[cfg(test)]
941mod signing {
942 use linera_base::{
943 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
944 data_types::{BlockHeight, Epoch, Round},
945 identifiers::ChainId,
946 };
947
948 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
949
950 #[test]
951 fn proposal_content_signing() {
952 use std::str::FromStr;
953
954 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
956 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
957 )
958 .unwrap();
959 let address = secret_key.address();
960
961 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
962 let public_key = signer.public();
963
964 let proposed_block = ProposedBlock {
965 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
966 epoch: Epoch(11),
967 transactions: vec![],
968 height: BlockHeight(11),
969 timestamp: 190000000u64.into(),
970 authenticated_signer: None,
971 previous_block_hash: None,
972 };
973
974 let proposal = ProposalContent {
975 block: proposed_block,
976 round: Round::SingleLeader(11),
977 outcome: None,
978 };
979
980 let signature = EvmSignature::from_str(
983 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
984 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
985 )
986 .unwrap();
987 let metamask_signature = AccountSignature::EvmSecp256k1 {
988 signature,
989 address: address.0 .0,
990 };
991
992 let signature = signer.sign(&proposal);
993 assert_eq!(signature, metamask_signature);
994
995 assert_eq!(signature.owner(), public_key.into());
996
997 let block_proposal = BlockProposal {
998 content: proposal,
999 signature,
1000 original_proposal: None,
1001 };
1002 assert_eq!(block_proposal.owner(), public_key.into(),);
1003 }
1004}