1use std::collections::{BTreeMap, BTreeSet, HashSet};
6
7use async_graphql::SimpleObject;
8use custom_debug_derive::Debug;
9use linera_base::{
10 bcs,
11 crypto::{
12 AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash, Signer,
13 ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
14 },
15 data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
16 doc_scalar, ensure, hex, hex_debug,
17 identifiers::{Account, AccountOwner, ApplicationId, BlobId, ChainId, StreamId},
18};
19use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 block::{Block, ValidatedBlock},
24 types::{
25 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
26 ValidatedBlockCertificate,
27 },
28 ChainError,
29};
30
31pub mod metadata;
32
33pub use metadata::*;
34
35#[cfg(test)]
36#[path = "../unit_tests/data_types_tests.rs"]
37mod data_types_tests;
38
39#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
47#[graphql(complex)]
48pub struct ProposedBlock {
49 pub chain_id: ChainId,
51 pub epoch: Epoch,
53 #[debug(skip_if = Vec::is_empty)]
56 #[graphql(skip)]
57 pub transactions: Vec<Transaction>,
58 pub height: BlockHeight,
60 pub timestamp: Timestamp,
63 #[debug(skip_if = Option::is_none)]
68 pub authenticated_owner: Option<AccountOwner>,
69 pub previous_block_hash: Option<CryptoHash>,
72}
73
74impl ProposedBlock {
75 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
77 self.operations()
78 .flat_map(Operation::published_blob_ids)
79 .collect()
80 }
81
82 pub fn has_only_rejected_messages(&self) -> bool {
85 self.transactions.iter().all(|txn| {
86 matches!(
87 txn,
88 Transaction::ReceiveMessages(IncomingBundle {
89 action: MessageAction::Reject,
90 ..
91 })
92 )
93 })
94 }
95
96 pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
98 self.incoming_bundles()
99 .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
100 }
101
102 pub fn message_count(&self) -> usize {
104 self.incoming_bundles()
105 .map(|im| im.bundle.messages.len())
106 .sum()
107 }
108
109 pub fn transaction_refs(&self) -> impl Iterator<Item = &Transaction> {
111 self.transactions.iter()
112 }
113
114 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
116 self.transactions.iter().filter_map(|tx| match tx {
117 Transaction::ExecuteOperation(operation) => Some(operation),
118 Transaction::ReceiveMessages(_) => None,
119 })
120 }
121
122 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
124 self.transactions.iter().filter_map(|tx| match tx {
125 Transaction::ReceiveMessages(bundle) => Some(bundle),
126 Transaction::ExecuteOperation(_) => None,
127 })
128 }
129
130 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
131 let size = bcs::serialized_size(self)?;
132 ensure!(
133 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
134 ChainError::BlockProposalTooLarge(size)
135 );
136 Ok(())
137 }
138}
139
140#[async_graphql::ComplexObject]
141impl ProposedBlock {
142 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
144 self.transactions
145 .iter()
146 .map(TransactionMetadata::from_transaction)
147 .collect()
148 }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
153pub enum Transaction {
154 ReceiveMessages(IncomingBundle),
156 ExecuteOperation(Operation),
158}
159
160impl BcsHashable<'_> for Transaction {}
161
162impl Transaction {
163 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
164 match self {
165 Transaction::ReceiveMessages(bundle) => Some(bundle),
166 _ => None,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
172#[graphql(name = "Operation")]
173pub struct OperationMetadata {
174 pub operation_type: String,
176 pub application_id: Option<ApplicationId>,
178 pub user_bytes_hex: Option<String>,
180 pub system_operation: Option<SystemOperationMetadata>,
182}
183
184impl From<&Operation> for OperationMetadata {
185 fn from(operation: &Operation) -> Self {
186 match operation {
187 Operation::System(sys_op) => OperationMetadata {
188 operation_type: "System".to_string(),
189 application_id: None,
190 user_bytes_hex: None,
191 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
192 },
193 Operation::User {
194 application_id,
195 bytes,
196 } => OperationMetadata {
197 operation_type: "User".to_string(),
198 application_id: Some(*application_id),
199 user_bytes_hex: Some(hex::encode(bytes)),
200 system_operation: None,
201 },
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
208pub struct TransactionMetadata {
209 pub transaction_type: String,
211 pub incoming_bundle: Option<IncomingBundle>,
213 pub operation: Option<OperationMetadata>,
215}
216
217impl TransactionMetadata {
218 pub fn from_transaction(transaction: &Transaction) -> Self {
219 match transaction {
220 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
221 transaction_type: "ReceiveMessages".to_string(),
222 incoming_bundle: Some(bundle.clone()),
223 operation: None,
224 },
225 Transaction::ExecuteOperation(op) => TransactionMetadata {
226 transaction_type: "ExecuteOperation".to_string(),
227 incoming_bundle: None,
228 operation: Some(OperationMetadata::from(op)),
229 },
230 }
231 }
232}
233
234#[derive(
236 Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, SimpleObject,
237)]
238pub struct ChainAndHeight {
239 pub chain_id: ChainId,
240 pub height: BlockHeight,
241}
242
243#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
245pub struct IncomingBundle {
246 pub origin: ChainId,
248 pub bundle: MessageBundle,
250 pub action: MessageAction,
252}
253
254impl IncomingBundle {
255 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
257 self.bundle.messages.iter()
258 }
259}
260
261impl BcsHashable<'_> for IncomingBundle {}
262
263#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
265pub enum MessageAction {
266 Accept,
268 Reject,
270}
271
272#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject)]
274pub struct MessageBundle {
275 pub height: BlockHeight,
277 pub timestamp: Timestamp,
279 pub certificate_hash: CryptoHash,
281 pub transaction_index: u32,
283 pub messages: Vec<PostedMessage>,
285}
286
287#[derive(Clone, Debug, Serialize, Deserialize)]
288#[cfg_attr(with_testing, derive(Eq, PartialEq))]
289pub enum OriginalProposal {
291 Fast(AccountSignature),
293 Regular {
295 certificate: LiteCertificate<'static>,
296 },
297}
298
299#[derive(Clone, Debug, Serialize, Deserialize)]
303#[cfg_attr(with_testing, derive(Eq, PartialEq))]
304pub struct BlockProposal {
305 pub content: ProposalContent,
306 pub signature: AccountSignature,
307 #[debug(skip_if = Option::is_none)]
308 pub original_proposal: Option<OriginalProposal>,
309}
310
311#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
313#[graphql(complex)]
314pub struct PostedMessage {
315 #[debug(skip_if = Option::is_none)]
317 pub authenticated_owner: Option<AccountOwner>,
318 #[debug(skip_if = Amount::is_zero)]
320 pub grant: Amount,
321 #[debug(skip_if = Option::is_none)]
323 pub refund_grant_to: Option<Account>,
324 pub kind: MessageKind,
326 pub index: u32,
328 pub message: Message,
330}
331
332pub trait OutgoingMessageExt {
333 fn into_posted(self, index: u32) -> PostedMessage;
335}
336
337impl OutgoingMessageExt for OutgoingMessage {
338 fn into_posted(self, index: u32) -> PostedMessage {
340 let OutgoingMessage {
341 destination: _,
342 authenticated_owner,
343 grant,
344 refund_grant_to,
345 kind,
346 message,
347 } = self;
348 PostedMessage {
349 authenticated_owner,
350 grant,
351 refund_grant_to,
352 kind,
353 index,
354 message,
355 }
356 }
357}
358
359#[async_graphql::ComplexObject]
360impl PostedMessage {
361 async fn message_metadata(&self) -> MessageMetadata {
363 MessageMetadata::from(&self.message)
364 }
365}
366
367#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
369pub struct OperationResult(
370 #[debug(with = "hex_debug")]
371 #[serde(with = "serde_bytes")]
372 pub Vec<u8>,
373);
374
375impl BcsHashable<'_> for OperationResult {}
376
377doc_scalar!(
378 OperationResult,
379 "The execution result of a single operation."
380);
381
382#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
384#[cfg_attr(with_testing, derive(Default))]
385pub struct BlockExecutionOutcome {
386 pub messages: Vec<Vec<OutgoingMessage>>,
388 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
390 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
392 pub state_hash: CryptoHash,
394 pub oracle_responses: Vec<Vec<OracleResponse>>,
396 pub events: Vec<Vec<Event>>,
398 pub blobs: Vec<Vec<Blob>>,
400 pub operation_results: Vec<OperationResult>,
402}
403
404#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
406pub struct LiteValue {
407 pub value_hash: CryptoHash,
408 pub chain_id: ChainId,
409 pub kind: CertificateKind,
410}
411
412impl LiteValue {
413 pub fn new<T: CertificateValue>(value: &T) -> Self {
414 LiteValue {
415 value_hash: value.hash(),
416 chain_id: value.chain_id(),
417 kind: T::KIND,
418 }
419 }
420}
421
422#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
423struct VoteValue(CryptoHash, Round, CertificateKind);
424
425#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(bound(deserialize = "T: Deserialize<'de>"))]
428pub struct Vote<T> {
429 pub value: T,
430 pub round: Round,
431 pub signature: ValidatorSignature,
432}
433
434impl<T> Vote<T> {
435 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
437 where
438 T: CertificateValue,
439 {
440 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
441 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
442 Self {
443 value,
444 round,
445 signature,
446 }
447 }
448
449 pub fn lite(&self) -> LiteVote
451 where
452 T: CertificateValue,
453 {
454 LiteVote {
455 value: LiteValue::new(&self.value),
456 round: self.round,
457 signature: self.signature,
458 }
459 }
460
461 pub fn value(&self) -> &T {
463 &self.value
464 }
465}
466
467#[derive(Clone, Debug, Serialize, Deserialize)]
469#[cfg_attr(with_testing, derive(Eq, PartialEq))]
470pub struct LiteVote {
471 pub value: LiteValue,
472 pub round: Round,
473 pub signature: ValidatorSignature,
474}
475
476impl LiteVote {
477 #[cfg(with_testing)]
479 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
480 if self.value.value_hash != value.hash() {
481 return None;
482 }
483 Some(Vote {
484 value,
485 round: self.round,
486 signature: self.signature,
487 })
488 }
489
490 pub fn kind(&self) -> CertificateKind {
491 self.value.kind
492 }
493}
494
495impl MessageBundle {
496 pub fn is_skippable(&self) -> bool {
497 self.messages.iter().all(PostedMessage::is_skippable)
498 }
499
500 pub fn is_protected(&self) -> bool {
501 self.messages.iter().any(PostedMessage::is_protected)
502 }
503}
504
505impl PostedMessage {
506 pub fn is_skippable(&self) -> bool {
507 match self.kind {
508 MessageKind::Protected | MessageKind::Tracked => false,
509 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
510 }
511 }
512
513 pub fn is_protected(&self) -> bool {
514 matches!(self.kind, MessageKind::Protected)
515 }
516
517 pub fn is_tracked(&self) -> bool {
518 matches!(self.kind, MessageKind::Tracked)
519 }
520
521 pub fn is_bouncing(&self) -> bool {
522 matches!(self.kind, MessageKind::Bouncing)
523 }
524}
525
526impl BlockExecutionOutcome {
527 pub fn with(self, block: ProposedBlock) -> Block {
528 Block::new(block, self)
529 }
530
531 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
532 let mut required_blob_ids = HashSet::new();
533 for responses in &self.oracle_responses {
534 for response in responses {
535 if let OracleResponse::Blob(blob_id) = response {
536 required_blob_ids.insert(*blob_id);
537 }
538 }
539 }
540
541 required_blob_ids
542 }
543
544 pub fn has_oracle_responses(&self) -> bool {
545 self.oracle_responses
546 .iter()
547 .any(|responses| !responses.is_empty())
548 }
549
550 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
551 self.blobs.iter().flatten().map(|blob| blob.id())
552 }
553
554 pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
555 self.iter_created_blobs_ids().collect()
556 }
557}
558
559#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
561pub struct ProposalContent {
562 pub block: ProposedBlock,
564 pub round: Round,
566 #[debug(skip_if = Option::is_none)]
568 pub outcome: Option<BlockExecutionOutcome>,
569}
570
571impl BlockProposal {
572 pub async fn new_initial<S: Signer + ?Sized>(
573 owner: AccountOwner,
574 round: Round,
575 block: ProposedBlock,
576 signer: &S,
577 ) -> Result<Self, S::Error> {
578 let content = ProposalContent {
579 round,
580 block,
581 outcome: None,
582 };
583 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
584
585 Ok(Self {
586 content,
587 signature,
588 original_proposal: None,
589 })
590 }
591
592 pub async fn new_retry_fast<S: Signer + ?Sized>(
593 owner: AccountOwner,
594 round: Round,
595 old_proposal: BlockProposal,
596 signer: &S,
597 ) -> Result<Self, S::Error> {
598 let content = ProposalContent {
599 round,
600 block: old_proposal.content.block,
601 outcome: None,
602 };
603 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
604
605 Ok(Self {
606 content,
607 signature,
608 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
609 })
610 }
611
612 pub async fn new_retry_regular<S: Signer>(
613 owner: AccountOwner,
614 round: Round,
615 validated_block_certificate: ValidatedBlockCertificate,
616 signer: &S,
617 ) -> Result<Self, S::Error> {
618 let certificate = validated_block_certificate.lite_certificate().cloned();
619 let block = validated_block_certificate.into_inner().into_inner();
620 let (block, outcome) = block.into_proposal();
621 let content = ProposalContent {
622 block,
623 round,
624 outcome: Some(outcome),
625 };
626 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
627
628 Ok(Self {
629 content,
630 signature,
631 original_proposal: Some(OriginalProposal::Regular { certificate }),
632 })
633 }
634
635 pub fn owner(&self) -> AccountOwner {
637 match self.signature {
638 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
639 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
640 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
641 }
642 }
643
644 pub fn check_signature(&self) -> Result<(), CryptoError> {
645 self.signature.verify(&self.content)
646 }
647
648 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
649 self.content.block.published_blob_ids().into_iter().chain(
650 self.content
651 .outcome
652 .iter()
653 .flat_map(|outcome| outcome.oracle_blob_ids()),
654 )
655 }
656
657 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
658 self.content.block.published_blob_ids().into_iter().chain(
659 self.content.outcome.iter().flat_map(|outcome| {
660 outcome
661 .oracle_blob_ids()
662 .into_iter()
663 .chain(outcome.iter_created_blobs_ids())
664 }),
665 )
666 }
667
668 pub fn check_invariants(&self) -> Result<(), &'static str> {
670 match (&self.original_proposal, &self.content.outcome) {
671 (None, None) => {}
672 (Some(OriginalProposal::Fast(_)), None) => ensure!(
673 self.content.round > Round::Fast,
674 "The new proposal's round must be greater than the original's"
675 ),
676 (None, Some(_))
677 | (Some(OriginalProposal::Fast(_)), Some(_))
678 | (Some(OriginalProposal::Regular { .. }), None) => {
679 return Err("Must contain a validation certificate if and only if \
680 it contains the execution outcome from a previous round");
681 }
682 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
683 ensure!(
684 self.content.round > certificate.round,
685 "The new proposal's round must be greater than the original's"
686 );
687 let block = outcome.clone().with(self.content.block.clone());
688 let value = ValidatedBlock::new(block);
689 ensure!(
690 certificate.check_value(&value),
691 "Lite certificate must match the given block and execution outcome"
692 );
693 }
694 }
695 Ok(())
696 }
697}
698
699impl LiteVote {
700 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
702 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
703 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
704 Self {
705 value,
706 round,
707 signature,
708 }
709 }
710
711 pub fn check(&self, public_key: ValidatorPublicKey) -> 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, 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 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 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 ensure!(
751 !self.used_validators.contains(&public_key),
752 ChainError::CertificateValidatorReuse
753 );
754 self.used_validators.insert(public_key);
755 let voting_rights = self.committee.weight(&public_key);
757 ensure!(voting_rights > 0, ChainError::InvalidSigner);
758 self.weight += voting_rights;
759 self.partial.add_signature((public_key, signature));
761
762 if self.weight >= self.committee.quorum_threshold() {
763 self.weight = 0; Ok(Some(self.partial.clone()))
765 } else {
766 Ok(None)
767 }
768 }
769}
770
771pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
774 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
775}
776
777pub(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 let mut weight = 0;
787 let mut used_validators = HashSet::new();
788 for (validator, _) in signatures {
789 ensure!(
791 !used_validators.contains(validator),
792 ChainError::CertificateValidatorReuse
793 );
794 used_validators.insert(*validator);
795 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 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);
818
819#[cfg(test)]
820mod signing {
821 use linera_base::{
822 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
823 data_types::{BlockHeight, Epoch, Round},
824 identifiers::ChainId,
825 };
826
827 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
828
829 #[test]
830 fn proposal_content_signing() {
831 use std::str::FromStr;
832
833 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
835 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
836 )
837 .unwrap();
838 let address = secret_key.address();
839
840 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
841 let public_key = signer.public();
842
843 let proposed_block = ProposedBlock {
844 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
845 epoch: Epoch(11),
846 transactions: vec![],
847 height: BlockHeight(11),
848 timestamp: 190000000u64.into(),
849 authenticated_owner: None,
850 previous_block_hash: None,
851 };
852
853 let proposal = ProposalContent {
854 block: proposed_block,
855 round: Round::SingleLeader(11),
856 outcome: None,
857 };
858
859 let signature = EvmSignature::from_str(
862 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
863 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
864 )
865 .unwrap();
866 let metamask_signature = AccountSignature::EvmSecp256k1 {
867 signature,
868 address: address.0 .0,
869 };
870
871 let signature = signer.sign(&proposal);
872 assert_eq!(signature, metamask_signature);
873
874 assert_eq!(signature.owner(), public_key.into());
875
876 let block_proposal = BlockProposal {
877 content: proposal,
878 signature,
879 original_proposal: None,
880 };
881 assert_eq!(block_proposal.owner(), public_key.into(),);
882 }
883}