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::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
17 doc_scalar, ensure, hex, hex_debug,
18 identifiers::{Account, AccountOwner, ApplicationId, BlobId, ChainId, StreamId},
19};
20use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
21use serde::{Deserialize, Serialize};
22
23use crate::{
24 block::{Block, ValidatedBlock},
25 types::{
26 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
27 ValidatedBlockCertificate,
28 },
29 ChainError,
30};
31
32pub mod metadata;
33
34pub use metadata::*;
35
36#[cfg(test)]
37#[path = "../unit_tests/data_types_tests.rs"]
38mod data_types_tests;
39
40#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
48#[graphql(complex)]
49pub struct ProposedBlock {
50 pub chain_id: ChainId,
52 pub epoch: Epoch,
54 #[debug(skip_if = Vec::is_empty)]
57 #[graphql(skip)]
58 pub transactions: Vec<Transaction>,
59 pub height: BlockHeight,
61 pub timestamp: Timestamp,
64 #[debug(skip_if = Option::is_none)]
69 pub authenticated_owner: Option<AccountOwner>,
70 pub previous_block_hash: Option<CryptoHash>,
73}
74
75impl ProposedBlock {
76 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
78 self.operations()
79 .flat_map(Operation::published_blob_ids)
80 .collect()
81 }
82
83 pub fn has_only_rejected_messages(&self) -> bool {
86 self.transactions.iter().all(|txn| {
87 matches!(
88 txn,
89 Transaction::ReceiveMessages(IncomingBundle {
90 action: MessageAction::Reject,
91 ..
92 })
93 )
94 })
95 }
96
97 pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
99 self.incoming_bundles()
100 .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
101 }
102
103 pub fn message_count(&self) -> usize {
105 self.incoming_bundles()
106 .map(|im| im.bundle.messages.len())
107 .sum()
108 }
109
110 pub fn transaction_refs(&self) -> impl Iterator<Item = &Transaction> {
112 self.transactions.iter()
113 }
114
115 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
117 self.transactions.iter().filter_map(|tx| match tx {
118 Transaction::ExecuteOperation(operation) => Some(operation),
119 Transaction::ReceiveMessages(_) => None,
120 })
121 }
122
123 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
125 self.transactions.iter().filter_map(|tx| match tx {
126 Transaction::ReceiveMessages(bundle) => Some(bundle),
127 Transaction::ExecuteOperation(_) => None,
128 })
129 }
130
131 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
132 let size = bcs::serialized_size(self)?;
133 ensure!(
134 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
135 ChainError::BlockProposalTooLarge(size)
136 );
137 Ok(())
138 }
139}
140
141#[async_graphql::ComplexObject]
142impl ProposedBlock {
143 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
145 self.transactions
146 .iter()
147 .map(TransactionMetadata::from_transaction)
148 .collect()
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative)]
154pub enum Transaction {
155 ReceiveMessages(IncomingBundle),
157 ExecuteOperation(Operation),
159}
160
161impl BcsHashable<'_> for Transaction {}
162
163impl Transaction {
164 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
165 match self {
166 Transaction::ReceiveMessages(bundle) => Some(bundle),
167 _ => None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
173#[graphql(name = "Operation")]
174pub struct OperationMetadata {
175 pub operation_type: String,
177 pub application_id: Option<ApplicationId>,
179 pub user_bytes_hex: Option<String>,
181 pub system_operation: Option<SystemOperationMetadata>,
183}
184
185impl From<&Operation> for OperationMetadata {
186 fn from(operation: &Operation) -> Self {
187 match operation {
188 Operation::System(sys_op) => OperationMetadata {
189 operation_type: "System".to_string(),
190 application_id: None,
191 user_bytes_hex: None,
192 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
193 },
194 Operation::User {
195 application_id,
196 bytes,
197 } => OperationMetadata {
198 operation_type: "User".to_string(),
199 application_id: Some(*application_id),
200 user_bytes_hex: Some(hex::encode(bytes)),
201 system_operation: None,
202 },
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
209pub struct TransactionMetadata {
210 pub transaction_type: String,
212 pub incoming_bundle: Option<IncomingBundle>,
214 pub operation: Option<OperationMetadata>,
216}
217
218impl TransactionMetadata {
219 pub fn from_transaction(transaction: &Transaction) -> Self {
220 match transaction {
221 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
222 transaction_type: "ReceiveMessages".to_string(),
223 incoming_bundle: Some(bundle.clone()),
224 operation: None,
225 },
226 Transaction::ExecuteOperation(op) => TransactionMetadata {
227 transaction_type: "ExecuteOperation".to_string(),
228 incoming_bundle: None,
229 operation: Some(OperationMetadata::from(op)),
230 },
231 }
232 }
233}
234
235#[derive(
237 Debug,
238 Clone,
239 Copy,
240 Eq,
241 PartialEq,
242 Ord,
243 PartialOrd,
244 Serialize,
245 Deserialize,
246 SimpleObject,
247 Allocative,
248)]
249pub struct ChainAndHeight {
250 pub chain_id: ChainId,
251 pub height: BlockHeight,
252}
253
254#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
256pub struct IncomingBundle {
257 pub origin: ChainId,
259 pub bundle: MessageBundle,
261 pub action: MessageAction,
263}
264
265impl IncomingBundle {
266 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
268 self.bundle.messages.iter()
269 }
270}
271
272impl BcsHashable<'_> for IncomingBundle {}
273
274#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
276pub enum MessageAction {
277 Accept,
279 Reject,
281}
282
283#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
285pub struct MessageBundle {
286 pub height: BlockHeight,
288 pub timestamp: Timestamp,
290 pub certificate_hash: CryptoHash,
292 pub transaction_index: u32,
294 pub messages: Vec<PostedMessage>,
296}
297
298#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
299#[cfg_attr(with_testing, derive(Eq, PartialEq))]
300pub enum OriginalProposal {
302 Fast(AccountSignature),
304 Regular {
306 certificate: LiteCertificate<'static>,
307 },
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
314#[cfg_attr(with_testing, derive(Eq, PartialEq))]
315pub struct BlockProposal {
316 pub content: ProposalContent,
317 pub signature: AccountSignature,
318 #[debug(skip_if = Option::is_none)]
319 pub original_proposal: Option<OriginalProposal>,
320}
321
322#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
324#[graphql(complex)]
325pub struct PostedMessage {
326 #[debug(skip_if = Option::is_none)]
328 pub authenticated_owner: Option<AccountOwner>,
329 #[debug(skip_if = Amount::is_zero)]
331 pub grant: Amount,
332 #[debug(skip_if = Option::is_none)]
334 pub refund_grant_to: Option<Account>,
335 pub kind: MessageKind,
337 pub index: u32,
339 pub message: Message,
341}
342
343pub trait OutgoingMessageExt {
344 fn into_posted(self, index: u32) -> PostedMessage;
346}
347
348impl OutgoingMessageExt for OutgoingMessage {
349 fn into_posted(self, index: u32) -> PostedMessage {
351 let OutgoingMessage {
352 destination: _,
353 authenticated_owner,
354 grant,
355 refund_grant_to,
356 kind,
357 message,
358 } = self;
359 PostedMessage {
360 authenticated_owner,
361 grant,
362 refund_grant_to,
363 kind,
364 index,
365 message,
366 }
367 }
368}
369
370#[async_graphql::ComplexObject]
371impl PostedMessage {
372 async fn message_metadata(&self) -> MessageMetadata {
374 MessageMetadata::from(&self.message)
375 }
376}
377
378#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
380pub struct OperationResult(
381 #[debug(with = "hex_debug")]
382 #[serde(with = "serde_bytes")]
383 pub Vec<u8>,
384);
385
386impl BcsHashable<'_> for OperationResult {}
387
388doc_scalar!(
389 OperationResult,
390 "The execution result of a single operation."
391);
392
393#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
395#[cfg_attr(with_testing, derive(Default))]
396pub struct BlockExecutionOutcome {
397 pub messages: Vec<Vec<OutgoingMessage>>,
399 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
401 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
403 pub state_hash: CryptoHash,
405 pub oracle_responses: Vec<Vec<OracleResponse>>,
407 pub events: Vec<Vec<Event>>,
409 pub blobs: Vec<Vec<Blob>>,
411 pub operation_results: Vec<OperationResult>,
413}
414
415#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
417pub struct LiteValue {
418 pub value_hash: CryptoHash,
419 pub chain_id: ChainId,
420 pub kind: CertificateKind,
421}
422
423impl LiteValue {
424 pub fn new<T: CertificateValue>(value: &T) -> Self {
425 LiteValue {
426 value_hash: value.hash(),
427 chain_id: value.chain_id(),
428 kind: T::KIND,
429 }
430 }
431}
432
433#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
434struct VoteValue(CryptoHash, Round, CertificateKind);
435
436#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
438#[serde(bound(deserialize = "T: Deserialize<'de>"))]
439pub struct Vote<T> {
440 pub value: T,
441 pub round: Round,
442 pub signature: ValidatorSignature,
443}
444
445impl<T> Vote<T> {
446 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
448 where
449 T: CertificateValue,
450 {
451 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
452 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
453 Self {
454 value,
455 round,
456 signature,
457 }
458 }
459
460 pub fn lite(&self) -> LiteVote
462 where
463 T: CertificateValue,
464 {
465 LiteVote {
466 value: LiteValue::new(&self.value),
467 round: self.round,
468 signature: self.signature,
469 }
470 }
471
472 pub fn value(&self) -> &T {
474 &self.value
475 }
476}
477
478#[derive(Clone, Debug, Serialize, Deserialize)]
480#[cfg_attr(with_testing, derive(Eq, PartialEq))]
481pub struct LiteVote {
482 pub value: LiteValue,
483 pub round: Round,
484 pub signature: ValidatorSignature,
485}
486
487impl LiteVote {
488 #[cfg(with_testing)]
490 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
491 if self.value.value_hash != value.hash() {
492 return None;
493 }
494 Some(Vote {
495 value,
496 round: self.round,
497 signature: self.signature,
498 })
499 }
500
501 pub fn kind(&self) -> CertificateKind {
502 self.value.kind
503 }
504}
505
506impl MessageBundle {
507 pub fn is_skippable(&self) -> bool {
508 self.messages.iter().all(PostedMessage::is_skippable)
509 }
510
511 pub fn is_protected(&self) -> bool {
512 self.messages.iter().any(PostedMessage::is_protected)
513 }
514}
515
516impl PostedMessage {
517 pub fn is_skippable(&self) -> bool {
518 match self.kind {
519 MessageKind::Protected | MessageKind::Tracked => false,
520 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
521 }
522 }
523
524 pub fn is_protected(&self) -> bool {
525 matches!(self.kind, MessageKind::Protected)
526 }
527
528 pub fn is_tracked(&self) -> bool {
529 matches!(self.kind, MessageKind::Tracked)
530 }
531
532 pub fn is_bouncing(&self) -> bool {
533 matches!(self.kind, MessageKind::Bouncing)
534 }
535}
536
537impl BlockExecutionOutcome {
538 pub fn with(self, block: ProposedBlock) -> Block {
539 Block::new(block, self)
540 }
541
542 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
543 let mut required_blob_ids = HashSet::new();
544 for responses in &self.oracle_responses {
545 for response in responses {
546 if let OracleResponse::Blob(blob_id) = response {
547 required_blob_ids.insert(*blob_id);
548 }
549 }
550 }
551
552 required_blob_ids
553 }
554
555 pub fn has_oracle_responses(&self) -> bool {
556 self.oracle_responses
557 .iter()
558 .any(|responses| !responses.is_empty())
559 }
560
561 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
562 self.blobs.iter().flatten().map(|blob| blob.id())
563 }
564
565 pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
566 self.iter_created_blobs_ids().collect()
567 }
568}
569
570#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
572pub struct ProposalContent {
573 pub block: ProposedBlock,
575 pub round: Round,
577 #[debug(skip_if = Option::is_none)]
579 pub outcome: Option<BlockExecutionOutcome>,
580}
581
582impl BlockProposal {
583 pub async fn new_initial<S: Signer + ?Sized>(
584 owner: AccountOwner,
585 round: Round,
586 block: ProposedBlock,
587 signer: &S,
588 ) -> Result<Self, S::Error> {
589 let content = ProposalContent {
590 round,
591 block,
592 outcome: None,
593 };
594 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
595
596 Ok(Self {
597 content,
598 signature,
599 original_proposal: None,
600 })
601 }
602
603 pub async fn new_retry_fast<S: Signer + ?Sized>(
604 owner: AccountOwner,
605 round: Round,
606 old_proposal: BlockProposal,
607 signer: &S,
608 ) -> Result<Self, S::Error> {
609 let content = ProposalContent {
610 round,
611 block: old_proposal.content.block,
612 outcome: None,
613 };
614 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
615
616 Ok(Self {
617 content,
618 signature,
619 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
620 })
621 }
622
623 pub async fn new_retry_regular<S: Signer>(
624 owner: AccountOwner,
625 round: Round,
626 validated_block_certificate: ValidatedBlockCertificate,
627 signer: &S,
628 ) -> Result<Self, S::Error> {
629 let certificate = validated_block_certificate.lite_certificate().cloned();
630 let block = validated_block_certificate.into_inner().into_inner();
631 let (block, outcome) = block.into_proposal();
632 let content = ProposalContent {
633 block,
634 round,
635 outcome: Some(outcome),
636 };
637 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
638
639 Ok(Self {
640 content,
641 signature,
642 original_proposal: Some(OriginalProposal::Regular { certificate }),
643 })
644 }
645
646 pub fn owner(&self) -> AccountOwner {
648 match self.signature {
649 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
650 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
651 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
652 }
653 }
654
655 pub fn check_signature(&self) -> Result<(), CryptoError> {
656 self.signature.verify(&self.content)
657 }
658
659 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
660 self.content.block.published_blob_ids().into_iter().chain(
661 self.content
662 .outcome
663 .iter()
664 .flat_map(|outcome| outcome.oracle_blob_ids()),
665 )
666 }
667
668 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
669 self.content.block.published_blob_ids().into_iter().chain(
670 self.content.outcome.iter().flat_map(|outcome| {
671 outcome
672 .oracle_blob_ids()
673 .into_iter()
674 .chain(outcome.iter_created_blobs_ids())
675 }),
676 )
677 }
678
679 pub fn check_invariants(&self) -> Result<(), &'static str> {
681 match (&self.original_proposal, &self.content.outcome) {
682 (None, None) => {}
683 (Some(OriginalProposal::Fast(_)), None) => ensure!(
684 self.content.round > Round::Fast,
685 "The new proposal's round must be greater than the original's"
686 ),
687 (None, Some(_))
688 | (Some(OriginalProposal::Fast(_)), Some(_))
689 | (Some(OriginalProposal::Regular { .. }), None) => {
690 return Err("Must contain a validation certificate if and only if \
691 it contains the execution outcome from a previous round");
692 }
693 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
694 ensure!(
695 self.content.round > certificate.round,
696 "The new proposal's round must be greater than the original's"
697 );
698 let block = outcome.clone().with(self.content.block.clone());
699 let value = ValidatedBlock::new(block);
700 ensure!(
701 certificate.check_value(&value),
702 "Lite certificate must match the given block and execution outcome"
703 );
704 }
705 }
706 Ok(())
707 }
708}
709
710impl LiteVote {
711 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
713 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
714 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
715 Self {
716 value,
717 round,
718 signature,
719 }
720 }
721
722 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
724 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
725 Ok(self.signature.check(&hash_and_round, public_key)?)
726 }
727}
728
729pub struct SignatureAggregator<'a, T: CertificateValue> {
730 committee: &'a Committee,
731 weight: u64,
732 used_validators: HashSet<ValidatorPublicKey>,
733 partial: GenericCertificate<T>,
734}
735
736impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
737 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
739 Self {
740 committee,
741 weight: 0,
742 used_validators: HashSet::new(),
743 partial: GenericCertificate::new(value, round, Vec::new()),
744 }
745 }
746
747 pub fn append(
751 &mut self,
752 public_key: ValidatorPublicKey,
753 signature: ValidatorSignature,
754 ) -> Result<Option<GenericCertificate<T>>, ChainError>
755 where
756 T: CertificateValue,
757 {
758 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
759 signature.check(&hash_and_round, public_key)?;
760 ensure!(
762 !self.used_validators.contains(&public_key),
763 ChainError::CertificateValidatorReuse
764 );
765 self.used_validators.insert(public_key);
766 let voting_rights = self.committee.weight(&public_key);
768 ensure!(voting_rights > 0, ChainError::InvalidSigner);
769 self.weight += voting_rights;
770 self.partial.add_signature((public_key, signature));
772
773 if self.weight >= self.committee.quorum_threshold() {
774 self.weight = 0; Ok(Some(self.partial.clone()))
776 } else {
777 Ok(None)
778 }
779 }
780}
781
782pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
785 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
786}
787
788pub(crate) fn check_signatures(
790 value_hash: CryptoHash,
791 certificate_kind: CertificateKind,
792 round: Round,
793 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
794 committee: &Committee,
795) -> Result<(), ChainError> {
796 let mut weight = 0;
798 let mut used_validators = HashSet::new();
799 for (validator, _) in signatures {
800 ensure!(
802 !used_validators.contains(validator),
803 ChainError::CertificateValidatorReuse
804 );
805 used_validators.insert(*validator);
806 let voting_rights = committee.weight(validator);
808 ensure!(voting_rights > 0, ChainError::InvalidSigner);
809 weight += voting_rights;
810 }
811 ensure!(
812 weight >= committee.quorum_threshold(),
813 ChainError::CertificateRequiresQuorum
814 );
815 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
817 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
818 Ok(())
819}
820
821impl BcsSignable<'_> for ProposalContent {}
822
823impl BcsSignable<'_> for VoteValue {}
824
825doc_scalar!(
826 MessageAction,
827 "Whether an incoming message is accepted or rejected."
828);
829
830#[cfg(test)]
831mod signing {
832 use linera_base::{
833 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
834 data_types::{BlockHeight, Epoch, Round},
835 identifiers::ChainId,
836 };
837
838 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
839
840 #[test]
841 fn proposal_content_signing() {
842 use std::str::FromStr;
843
844 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
846 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
847 )
848 .unwrap();
849 let address = secret_key.address();
850
851 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
852 let public_key = signer.public();
853
854 let proposed_block = ProposedBlock {
855 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
856 epoch: Epoch(11),
857 transactions: vec![],
858 height: BlockHeight(11),
859 timestamp: 190000000u64.into(),
860 authenticated_owner: 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 let signature = EvmSignature::from_str(
873 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
874 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
875 )
876 .unwrap();
877 let metamask_signature = AccountSignature::EvmSecp256k1 {
878 signature,
879 address: address.0 .0,
880 };
881
882 let signature = signer.sign(&proposal);
883 assert_eq!(signature, metamask_signature);
884
885 assert_eq!(signature.owner(), public_key.into());
886
887 let block_proposal = BlockProposal {
888 content: proposal,
889 signature,
890 original_proposal: None,
891 };
892 assert_eq!(block_proposal.owner(), public_key.into(),);
893 }
894}