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