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(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative)]
157pub enum Transaction {
158 ReceiveMessages(IncomingBundle),
160 ExecuteOperation(Operation),
162}
163
164impl BcsHashable<'_> for Transaction {}
165
166impl Transaction {
167 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
168 match self {
169 Transaction::ReceiveMessages(bundle) => Some(bundle),
170 _ => None,
171 }
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
176#[graphql(name = "Operation")]
177pub struct OperationMetadata {
178 pub operation_type: String,
180 pub application_id: Option<ApplicationId>,
182 pub user_bytes_hex: Option<String>,
184 pub system_operation: Option<SystemOperationMetadata>,
186}
187
188impl From<&Operation> for OperationMetadata {
189 fn from(operation: &Operation) -> Self {
190 match operation {
191 Operation::System(sys_op) => OperationMetadata {
192 operation_type: "System".to_string(),
193 application_id: None,
194 user_bytes_hex: None,
195 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
196 },
197 Operation::User {
198 application_id,
199 bytes,
200 } => OperationMetadata {
201 operation_type: "User".to_string(),
202 application_id: Some(*application_id),
203 user_bytes_hex: Some(hex::encode(bytes)),
204 system_operation: None,
205 },
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
212pub struct TransactionMetadata {
213 pub transaction_type: String,
215 pub incoming_bundle: Option<IncomingBundle>,
217 pub operation: Option<OperationMetadata>,
219}
220
221impl TransactionMetadata {
222 pub fn from_transaction(transaction: &Transaction) -> Self {
223 match transaction {
224 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
225 transaction_type: "ReceiveMessages".to_string(),
226 incoming_bundle: Some(bundle.clone()),
227 operation: None,
228 },
229 Transaction::ExecuteOperation(op) => TransactionMetadata {
230 transaction_type: "ExecuteOperation".to_string(),
231 incoming_bundle: None,
232 operation: Some(OperationMetadata::from(op)),
233 },
234 }
235 }
236}
237
238#[derive(
240 Debug,
241 Clone,
242 Copy,
243 Eq,
244 PartialEq,
245 Ord,
246 PartialOrd,
247 Serialize,
248 Deserialize,
249 SimpleObject,
250 Allocative,
251)]
252pub struct ChainAndHeight {
253 pub chain_id: ChainId,
254 pub height: BlockHeight,
255}
256
257#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
259pub struct IncomingBundle {
260 pub origin: ChainId,
262 pub bundle: MessageBundle,
264 pub action: MessageAction,
266}
267
268impl IncomingBundle {
269 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
271 self.bundle.messages.iter()
272 }
273
274 #[instrument(level = "trace", skip(self))]
275 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
276 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
277 if !chain_ids.contains(&self.origin) {
278 return None;
279 }
280 }
281 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
282 if !self
283 .messages()
284 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
285 {
286 return None;
287 }
288 }
289 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
290 if !self
291 .messages()
292 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
293 {
294 return None;
295 }
296 }
297 if policy.is_reject() {
298 if self.bundle.is_skippable() {
299 return None;
300 } else if !self.bundle.is_protected() {
301 self.action = MessageAction::Reject;
302 }
303 }
304 Some(self)
305 }
306}
307
308impl BcsHashable<'_> for IncomingBundle {}
309
310#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
312pub enum MessageAction {
313 Accept,
315 Reject,
317}
318
319#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
321pub struct MessageBundle {
322 pub height: BlockHeight,
324 pub timestamp: Timestamp,
326 pub certificate_hash: CryptoHash,
328 pub transaction_index: u32,
330 pub messages: Vec<PostedMessage>,
332}
333
334#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
335#[cfg_attr(with_testing, derive(Eq, PartialEq))]
336pub enum OriginalProposal {
338 Fast(AccountSignature),
340 Regular {
342 certificate: LiteCertificate<'static>,
343 },
344}
345
346#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
350#[cfg_attr(with_testing, derive(Eq, PartialEq))]
351pub struct BlockProposal {
352 pub content: ProposalContent,
353 pub signature: AccountSignature,
354 #[debug(skip_if = Option::is_none)]
355 pub original_proposal: Option<OriginalProposal>,
356}
357
358#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
360#[graphql(complex)]
361pub struct PostedMessage {
362 #[debug(skip_if = Option::is_none)]
364 pub authenticated_owner: Option<AccountOwner>,
365 #[debug(skip_if = Amount::is_zero)]
367 pub grant: Amount,
368 #[debug(skip_if = Option::is_none)]
370 pub refund_grant_to: Option<Account>,
371 pub kind: MessageKind,
373 pub index: u32,
375 pub message: Message,
377}
378
379pub trait OutgoingMessageExt {
380 fn into_posted(self, index: u32) -> PostedMessage;
382}
383
384impl OutgoingMessageExt for OutgoingMessage {
385 fn into_posted(self, index: u32) -> PostedMessage {
387 let OutgoingMessage {
388 destination: _,
389 authenticated_owner,
390 grant,
391 refund_grant_to,
392 kind,
393 message,
394 } = self;
395 PostedMessage {
396 authenticated_owner,
397 grant,
398 refund_grant_to,
399 kind,
400 index,
401 message,
402 }
403 }
404}
405
406#[async_graphql::ComplexObject]
407impl PostedMessage {
408 async fn message_metadata(&self) -> MessageMetadata {
410 MessageMetadata::from(&self.message)
411 }
412}
413
414#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
416pub struct OperationResult(
417 #[debug(with = "hex_debug")]
418 #[serde(with = "serde_bytes")]
419 pub Vec<u8>,
420);
421
422impl BcsHashable<'_> for OperationResult {}
423
424doc_scalar!(
425 OperationResult,
426 "The execution result of a single operation."
427);
428
429#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
431#[cfg_attr(with_testing, derive(Default))]
432pub struct BlockExecutionOutcome {
433 pub messages: Vec<Vec<OutgoingMessage>>,
435 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
437 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
439 pub state_hash: CryptoHash,
441 pub oracle_responses: Vec<Vec<OracleResponse>>,
443 pub events: Vec<Vec<Event>>,
445 pub blobs: Vec<Vec<Blob>>,
447 pub operation_results: Vec<OperationResult>,
449}
450
451#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
453pub struct LiteValue {
454 pub value_hash: CryptoHash,
455 pub chain_id: ChainId,
456 pub kind: CertificateKind,
457}
458
459impl LiteValue {
460 pub fn new<T: CertificateValue>(value: &T) -> Self {
461 LiteValue {
462 value_hash: value.hash(),
463 chain_id: value.chain_id(),
464 kind: T::KIND,
465 }
466 }
467}
468
469#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
470struct VoteValue(CryptoHash, Round, CertificateKind);
471
472#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
474#[serde(bound(deserialize = "T: Deserialize<'de>"))]
475pub struct Vote<T> {
476 pub value: T,
477 pub round: Round,
478 pub signature: ValidatorSignature,
479}
480
481impl<T> Vote<T> {
482 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
484 where
485 T: CertificateValue,
486 {
487 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
488 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
489 Self {
490 value,
491 round,
492 signature,
493 }
494 }
495
496 pub fn lite(&self) -> LiteVote
498 where
499 T: CertificateValue,
500 {
501 LiteVote {
502 value: LiteValue::new(&self.value),
503 round: self.round,
504 signature: self.signature,
505 }
506 }
507
508 pub fn value(&self) -> &T {
510 &self.value
511 }
512}
513
514#[derive(Clone, Debug, Serialize, Deserialize)]
516#[cfg_attr(with_testing, derive(Eq, PartialEq))]
517pub struct LiteVote {
518 pub value: LiteValue,
519 pub round: Round,
520 pub signature: ValidatorSignature,
521}
522
523impl LiteVote {
524 #[cfg(with_testing)]
526 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
527 if self.value.value_hash != value.hash() {
528 return None;
529 }
530 Some(Vote {
531 value,
532 round: self.round,
533 signature: self.signature,
534 })
535 }
536
537 pub fn kind(&self) -> CertificateKind {
538 self.value.kind
539 }
540}
541
542impl MessageBundle {
543 pub fn is_skippable(&self) -> bool {
544 self.messages.iter().all(PostedMessage::is_skippable)
545 }
546
547 pub fn is_protected(&self) -> bool {
548 self.messages.iter().any(PostedMessage::is_protected)
549 }
550}
551
552impl PostedMessage {
553 pub fn is_skippable(&self) -> bool {
554 match self.kind {
555 MessageKind::Protected | MessageKind::Tracked => false,
556 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
557 }
558 }
559
560 pub fn is_protected(&self) -> bool {
561 matches!(self.kind, MessageKind::Protected)
562 }
563
564 pub fn is_tracked(&self) -> bool {
565 matches!(self.kind, MessageKind::Tracked)
566 }
567
568 pub fn is_bouncing(&self) -> bool {
569 matches!(self.kind, MessageKind::Bouncing)
570 }
571}
572
573impl BlockExecutionOutcome {
574 pub fn with(self, block: ProposedBlock) -> Block {
575 Block::new(block, self)
576 }
577
578 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
579 let mut required_blob_ids = HashSet::new();
580 for responses in &self.oracle_responses {
581 for response in responses {
582 if let OracleResponse::Blob(blob_id) = response {
583 required_blob_ids.insert(*blob_id);
584 }
585 }
586 }
587
588 required_blob_ids
589 }
590
591 pub fn has_oracle_responses(&self) -> bool {
592 self.oracle_responses
593 .iter()
594 .any(|responses| !responses.is_empty())
595 }
596
597 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
598 self.blobs.iter().flatten().map(|blob| blob.id())
599 }
600
601 pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
602 self.iter_created_blobs_ids().collect()
603 }
604}
605
606#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
608pub struct ProposalContent {
609 pub block: ProposedBlock,
611 pub round: Round,
613 #[debug(skip_if = Option::is_none)]
615 pub outcome: Option<BlockExecutionOutcome>,
616}
617
618impl BlockProposal {
619 pub async fn new_initial<S: Signer + ?Sized>(
620 owner: AccountOwner,
621 round: Round,
622 block: ProposedBlock,
623 signer: &S,
624 ) -> Result<Self, S::Error> {
625 let content = ProposalContent {
626 round,
627 block,
628 outcome: None,
629 };
630 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
631
632 Ok(Self {
633 content,
634 signature,
635 original_proposal: None,
636 })
637 }
638
639 pub async fn new_retry_fast<S: Signer + ?Sized>(
640 owner: AccountOwner,
641 round: Round,
642 old_proposal: BlockProposal,
643 signer: &S,
644 ) -> Result<Self, S::Error> {
645 let content = ProposalContent {
646 round,
647 block: old_proposal.content.block,
648 outcome: None,
649 };
650 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
651
652 Ok(Self {
653 content,
654 signature,
655 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
656 })
657 }
658
659 pub async fn new_retry_regular<S: Signer>(
660 owner: AccountOwner,
661 round: Round,
662 validated_block_certificate: ValidatedBlockCertificate,
663 signer: &S,
664 ) -> Result<Self, S::Error> {
665 let certificate = validated_block_certificate.lite_certificate().cloned();
666 let block = validated_block_certificate.into_inner().into_inner();
667 let (block, outcome) = block.into_proposal();
668 let content = ProposalContent {
669 block,
670 round,
671 outcome: Some(outcome),
672 };
673 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
674
675 Ok(Self {
676 content,
677 signature,
678 original_proposal: Some(OriginalProposal::Regular { certificate }),
679 })
680 }
681
682 pub fn owner(&self) -> AccountOwner {
684 match self.signature {
685 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
686 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
687 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
688 }
689 }
690
691 pub fn check_signature(&self) -> Result<(), CryptoError> {
692 self.signature.verify(&self.content)
693 }
694
695 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
696 self.content.block.published_blob_ids().into_iter().chain(
697 self.content
698 .outcome
699 .iter()
700 .flat_map(|outcome| outcome.oracle_blob_ids()),
701 )
702 }
703
704 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
705 self.content.block.published_blob_ids().into_iter().chain(
706 self.content.outcome.iter().flat_map(|outcome| {
707 outcome
708 .oracle_blob_ids()
709 .into_iter()
710 .chain(outcome.iter_created_blobs_ids())
711 }),
712 )
713 }
714
715 pub fn check_invariants(&self) -> Result<(), &'static str> {
717 match (&self.original_proposal, &self.content.outcome) {
718 (None, None) => {}
719 (Some(OriginalProposal::Fast(_)), None) => ensure!(
720 self.content.round > Round::Fast,
721 "The new proposal's round must be greater than the original's"
722 ),
723 (None, Some(_))
724 | (Some(OriginalProposal::Fast(_)), Some(_))
725 | (Some(OriginalProposal::Regular { .. }), None) => {
726 return Err("Must contain a validation certificate if and only if \
727 it contains the execution outcome from a previous round");
728 }
729 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
730 ensure!(
731 self.content.round > certificate.round,
732 "The new proposal's round must be greater than the original's"
733 );
734 let block = outcome.clone().with(self.content.block.clone());
735 let value = ValidatedBlock::new(block);
736 ensure!(
737 certificate.check_value(&value),
738 "Lite certificate must match the given block and execution outcome"
739 );
740 }
741 }
742 Ok(())
743 }
744}
745
746impl LiteVote {
747 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
749 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
750 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
751 Self {
752 value,
753 round,
754 signature,
755 }
756 }
757
758 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
760 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
761 Ok(self.signature.check(&hash_and_round, public_key)?)
762 }
763}
764
765pub struct SignatureAggregator<'a, T: CertificateValue> {
766 committee: &'a Committee,
767 weight: u64,
768 used_validators: HashSet<ValidatorPublicKey>,
769 partial: GenericCertificate<T>,
770}
771
772impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
773 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
775 Self {
776 committee,
777 weight: 0,
778 used_validators: HashSet::new(),
779 partial: GenericCertificate::new(value, round, Vec::new()),
780 }
781 }
782
783 pub fn append(
787 &mut self,
788 public_key: ValidatorPublicKey,
789 signature: ValidatorSignature,
790 ) -> Result<Option<GenericCertificate<T>>, ChainError>
791 where
792 T: CertificateValue,
793 {
794 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
795 signature.check(&hash_and_round, public_key)?;
796 ensure!(
798 !self.used_validators.contains(&public_key),
799 ChainError::CertificateValidatorReuse
800 );
801 self.used_validators.insert(public_key);
802 let voting_rights = self.committee.weight(&public_key);
804 ensure!(voting_rights > 0, ChainError::InvalidSigner);
805 self.weight += voting_rights;
806 self.partial.add_signature((public_key, signature));
808
809 if self.weight >= self.committee.quorum_threshold() {
810 self.weight = 0; Ok(Some(self.partial.clone()))
812 } else {
813 Ok(None)
814 }
815 }
816}
817
818pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
821 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
822}
823
824pub(crate) fn check_signatures(
826 value_hash: CryptoHash,
827 certificate_kind: CertificateKind,
828 round: Round,
829 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
830 committee: &Committee,
831) -> Result<(), ChainError> {
832 let mut weight = 0;
834 let mut used_validators = HashSet::new();
835 for (validator, _) in signatures {
836 ensure!(
838 !used_validators.contains(validator),
839 ChainError::CertificateValidatorReuse
840 );
841 used_validators.insert(*validator);
842 let voting_rights = committee.weight(validator);
844 ensure!(voting_rights > 0, ChainError::InvalidSigner);
845 weight += voting_rights;
846 }
847 ensure!(
848 weight >= committee.quorum_threshold(),
849 ChainError::CertificateRequiresQuorum
850 );
851 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
853 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
854 Ok(())
855}
856
857impl BcsSignable<'_> for ProposalContent {}
858
859impl BcsSignable<'_> for VoteValue {}
860
861doc_scalar!(
862 MessageAction,
863 "Whether an incoming message is accepted or rejected."
864);
865
866#[cfg(test)]
867mod signing {
868 use linera_base::{
869 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
870 data_types::{BlockHeight, Epoch, Round},
871 identifiers::ChainId,
872 };
873
874 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
875
876 #[test]
877 fn proposal_content_signing() {
878 use std::str::FromStr;
879
880 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
882 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
883 )
884 .unwrap();
885 let address = secret_key.address();
886
887 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
888 let public_key = signer.public();
889
890 let proposed_block = ProposedBlock {
891 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
892 epoch: Epoch(11),
893 transactions: vec![],
894 height: BlockHeight(11),
895 timestamp: 190000000u64.into(),
896 authenticated_owner: None,
897 previous_block_hash: None,
898 };
899
900 let proposal = ProposalContent {
901 block: proposed_block,
902 round: Round::SingleLeader(11),
903 outcome: None,
904 };
905
906 let signature = EvmSignature::from_str(
909 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
910 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
911 )
912 .unwrap();
913 let metamask_signature = AccountSignature::EvmSecp256k1 {
914 signature,
915 address: address.0 .0,
916 };
917
918 let signature = signer.sign(&proposal);
919 assert_eq!(signature, metamask_signature);
920
921 assert_eq!(signature.owner(), public_key.into());
922
923 let block_proposal = BlockProposal {
924 content: proposal,
925 signature,
926 original_proposal: None,
927 };
928 assert_eq!(block_proposal.owner(), public_key.into(),);
929 }
930}