1use std::{
6 collections::{BTreeMap, BTreeSet, HashSet},
7 sync::Arc,
8};
9
10use allocative::Allocative;
11use async_graphql::SimpleObject;
12use custom_debug_derive::Debug;
13use linera_base::{
14 bcs,
15 crypto::{
16 AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash, Signer,
17 ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
18 },
19 data_types::{
20 Amount, Blob, BlockHeight, Epoch, Event, MessagePolicy, OracleResponse, Round, Timestamp,
21 },
22 doc_scalar, ensure, hex, hex_debug,
23 identifiers::{
24 Account, AccountOwner, ApplicationId, BlobId, ChainId, GenericApplicationId, StreamId,
25 },
26 time::Duration,
27};
28use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
29use serde::{Deserialize, Serialize};
30use tracing::instrument;
31
32use crate::{
33 block::{Block, ValidatedBlock},
34 types::{
35 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
36 ValidatedBlockCertificate,
37 },
38 ChainError,
39};
40
41pub mod metadata;
42
43pub use metadata::*;
44
45#[cfg(test)]
46#[path = "../unit_tests/data_types_tests.rs"]
47mod data_types_tests;
48
49#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
57#[graphql(complex)]
58pub struct ProposedBlock {
59 pub chain_id: ChainId,
61 pub epoch: Epoch,
63 #[debug(skip_if = Vec::is_empty)]
66 #[graphql(skip)]
67 pub transactions: Vec<Transaction>,
68 pub height: BlockHeight,
70 pub timestamp: Timestamp,
73 #[debug(skip_if = Option::is_none)]
78 pub authenticated_owner: Option<AccountOwner>,
79 pub previous_block_hash: Option<CryptoHash>,
82}
83
84impl ProposedBlock {
85 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
87 self.operations()
88 .flat_map(Operation::published_blob_ids)
89 .collect()
90 }
91
92 pub fn has_only_rejected_messages(&self) -> bool {
95 self.transactions.iter().all(|txn| {
96 matches!(
97 txn,
98 Transaction::ReceiveMessages(IncomingBundle {
99 action: MessageAction::Reject,
100 ..
101 })
102 )
103 })
104 }
105
106 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
108 self.transactions.iter().filter_map(|tx| match tx {
109 Transaction::ExecuteOperation(operation) => Some(operation),
110 Transaction::ReceiveMessages(_) => None,
111 })
112 }
113
114 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
116 self.transactions.iter().filter_map(|tx| match tx {
117 Transaction::ReceiveMessages(bundle) => Some(bundle),
118 Transaction::ExecuteOperation(_) => None,
119 })
120 }
121
122 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
123 let size = bcs::serialized_size(self)?;
124 ensure!(
125 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
126 ChainError::BlockProposalTooLarge(size)
127 );
128 Ok(())
129 }
130}
131
132#[async_graphql::ComplexObject]
133impl ProposedBlock {
134 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
136 self.transactions
137 .iter()
138 .map(TransactionMetadata::from_transaction)
139 .collect()
140 }
141}
142
143#[derive(
145 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative, strum::AsRefStr,
146)]
147pub enum Transaction {
148 ReceiveMessages(IncomingBundle),
150 ExecuteOperation(Operation),
152}
153
154impl BcsHashable<'_> for Transaction {}
155
156impl Transaction {
157 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
158 match self {
159 Transaction::ReceiveMessages(bundle) => Some(bundle),
160 _ => None,
161 }
162 }
163
164 pub fn is_update_stream(&self) -> bool {
165 matches!(
166 self,
167 Transaction::ExecuteOperation(op) if op.is_update_stream()
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 fn matches_policy(&self, policy: &MessagePolicy) -> bool {
272 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
273 if !chain_ids.contains(&self.origin) {
274 return false;
275 }
276 }
277 if policy.ignore_chain_ids.contains(&self.origin) {
278 return false;
279 }
280 if !policy.never_reject_application_ids.is_empty()
281 && self.messages().all(|posted_msg| {
282 policy
283 .never_reject_application_ids
284 .contains(&posted_msg.message.application_id())
285 })
286 {
287 return true;
288 }
289 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
290 if !self
291 .messages()
292 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
293 {
294 return false;
295 }
296 }
297 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
298 if !self
299 .messages()
300 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
301 {
302 return false;
303 }
304 }
305 !policy.is_reject()
306 }
307
308 #[instrument(level = "trace", skip(self))]
309 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
310 if !self.matches_policy(policy) {
311 if self.bundle.is_skippable() {
312 return None;
313 } else if !self.bundle.is_protected() {
314 self.action = MessageAction::Reject;
315 }
316 }
317 Some(self)
318 }
319}
320
321impl BcsHashable<'_> for IncomingBundle {}
322
323#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
325pub enum MessageAction {
326 Accept,
328 Reject,
330}
331
332#[derive(Clone, Debug, Default, PartialEq, Eq)]
334pub enum BundleFailurePolicy {
335 #[default]
337 Abort,
338 AutoRetry {
352 max_failures: u32,
354 never_reject_application_ids: Arc<HashSet<GenericApplicationId>>,
358 },
359}
360
361#[derive(Clone, Debug, PartialEq, Eq)]
363pub struct BundleExecutionPolicy {
364 pub on_failure: BundleFailurePolicy,
366 pub time_budget: Option<Duration>,
368}
369
370impl BundleExecutionPolicy {
371 pub fn committed() -> Self {
373 BundleExecutionPolicy {
374 on_failure: BundleFailurePolicy::Abort,
375 time_budget: None,
376 }
377 }
378}
379
380#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
382pub struct MessageBundle {
383 pub height: BlockHeight,
385 pub timestamp: Timestamp,
387 pub certificate_hash: CryptoHash,
389 pub transaction_index: u32,
391 pub messages: Vec<PostedMessage>,
393}
394
395#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
396#[cfg_attr(with_testing, derive(Eq, PartialEq))]
397pub enum OriginalProposal {
399 Fast(AccountSignature),
401 Regular {
403 certificate: LiteCertificate<'static>,
404 },
405}
406
407#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
411#[cfg_attr(with_testing, derive(Eq, PartialEq))]
412pub struct BlockProposal {
413 pub content: ProposalContent,
414 pub signature: AccountSignature,
415 #[debug(skip_if = Option::is_none)]
416 pub original_proposal: Option<OriginalProposal>,
417}
418
419#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
421#[graphql(complex)]
422pub struct PostedMessage {
423 #[debug(skip_if = Option::is_none)]
425 pub authenticated_owner: Option<AccountOwner>,
426 #[debug(skip_if = Amount::is_zero)]
428 pub grant: Amount,
429 #[debug(skip_if = Option::is_none)]
431 pub refund_grant_to: Option<Account>,
432 pub kind: MessageKind,
434 pub index: u32,
436 pub message: Message,
438}
439
440pub trait OutgoingMessageExt {
441 fn into_posted(self, index: u32) -> PostedMessage;
443}
444
445impl OutgoingMessageExt for OutgoingMessage {
446 fn into_posted(self, index: u32) -> PostedMessage {
448 let OutgoingMessage {
449 destination: _,
450 authenticated_owner,
451 grant,
452 refund_grant_to,
453 kind,
454 message,
455 } = self;
456 PostedMessage {
457 authenticated_owner,
458 grant,
459 refund_grant_to,
460 kind,
461 index,
462 message,
463 }
464 }
465}
466
467#[async_graphql::ComplexObject]
468impl PostedMessage {
469 async fn message_metadata(&self) -> MessageMetadata {
471 MessageMetadata::from(&self.message)
472 }
473}
474
475#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
477pub struct OperationResult(
478 #[debug(with = "hex_debug")]
479 #[serde(with = "serde_bytes")]
480 pub Vec<u8>,
481);
482
483impl BcsHashable<'_> for OperationResult {}
484
485doc_scalar!(
486 OperationResult,
487 "The execution result of a single operation."
488);
489
490#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
492#[cfg_attr(with_testing, derive(Default))]
493pub struct BlockExecutionOutcome {
494 pub messages: Vec<Vec<OutgoingMessage>>,
496 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
498 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
500 pub state_hash: CryptoHash,
502 pub oracle_responses: Vec<Vec<OracleResponse>>,
504 pub events: Vec<Vec<Event>>,
506 pub blobs: Vec<Vec<Blob>>,
508 pub operation_results: Vec<OperationResult>,
510}
511
512#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
514pub struct LiteValue {
515 pub value_hash: CryptoHash,
516 pub chain_id: ChainId,
517 pub kind: CertificateKind,
518}
519
520impl LiteValue {
521 pub fn new<T: CertificateValue>(value: &T) -> Self {
522 LiteValue {
523 value_hash: value.hash(),
524 chain_id: value.chain_id(),
525 kind: T::KIND,
526 }
527 }
528}
529
530#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
532pub struct VoteValue(CryptoHash, Round, CertificateKind);
533
534#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
536#[serde(bound(deserialize = "T: Deserialize<'de>"))]
537pub struct Vote<T> {
538 pub value: T,
539 pub round: Round,
540 pub signature: ValidatorSignature,
541}
542
543impl<T> Vote<T> {
544 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
546 where
547 T: CertificateValue,
548 {
549 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
550 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
551 Self {
552 value,
553 round,
554 signature,
555 }
556 }
557
558 pub fn lite(&self) -> LiteVote
560 where
561 T: CertificateValue,
562 {
563 LiteVote {
564 value: LiteValue::new(&self.value),
565 round: self.round,
566 signature: self.signature,
567 }
568 }
569
570 pub fn value(&self) -> &T {
572 &self.value
573 }
574}
575
576#[derive(Clone, Debug, Serialize, Deserialize)]
578#[cfg_attr(with_testing, derive(Eq, PartialEq))]
579pub struct LiteVote {
580 pub value: LiteValue,
581 pub round: Round,
582 pub signature: ValidatorSignature,
583}
584
585impl LiteVote {
586 #[cfg(with_testing)]
588 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
589 if self.value.value_hash != value.hash() {
590 return None;
591 }
592 Some(Vote {
593 value,
594 round: self.round,
595 signature: self.signature,
596 })
597 }
598
599 pub fn kind(&self) -> CertificateKind {
600 self.value.kind
601 }
602}
603
604impl MessageBundle {
605 pub fn estimated_size(&self) -> usize {
607 let overhead = 60;
609 let messages_size: usize = self
610 .messages
611 .iter()
612 .map(PostedMessage::estimated_size)
613 .sum();
614 overhead + messages_size
615 }
616
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 estimated_size(&self) -> usize {
629 let overhead = 96;
631 let message_size = match &self.message {
632 Message::System(_) => 256, Message::User { bytes, .. } => 64 + bytes.len(),
634 };
635 overhead + message_size
636 }
637
638 pub fn is_skippable(&self) -> bool {
639 match self.kind {
640 MessageKind::Protected | MessageKind::Tracked => false,
641 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
642 }
643 }
644
645 pub fn is_protected(&self) -> bool {
646 matches!(self.kind, MessageKind::Protected)
647 }
648
649 pub fn is_tracked(&self) -> bool {
650 matches!(self.kind, MessageKind::Tracked)
651 }
652
653 pub fn is_bouncing(&self) -> bool {
654 matches!(self.kind, MessageKind::Bouncing)
655 }
656}
657
658impl BlockExecutionOutcome {
659 pub fn with(self, block: ProposedBlock) -> Block {
660 Block::new(block, self)
661 }
662
663 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
664 let mut required_blob_ids = HashSet::new();
665 for responses in &self.oracle_responses {
666 for response in responses {
667 if let OracleResponse::Blob(blob_id) = response {
668 required_blob_ids.insert(*blob_id);
669 }
670 }
671 }
672
673 required_blob_ids
674 }
675
676 pub fn has_oracle_responses(&self) -> bool {
677 self.oracle_responses
678 .iter()
679 .any(|responses| !responses.is_empty())
680 }
681
682 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
683 self.blobs.iter().flatten().map(|blob| blob.id())
684 }
685}
686
687#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
689pub struct ProposalContent {
690 pub block: ProposedBlock,
692 pub round: Round,
694 #[debug(skip_if = Option::is_none)]
696 pub outcome: Option<BlockExecutionOutcome>,
697}
698
699impl BlockProposal {
700 pub async fn new_initial<S: Signer + ?Sized>(
701 owner: AccountOwner,
702 round: Round,
703 block: ProposedBlock,
704 signer: &S,
705 ) -> Result<Self, S::Error> {
706 let content = ProposalContent {
707 round,
708 block,
709 outcome: None,
710 };
711 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
712
713 Ok(Self {
714 content,
715 signature,
716 original_proposal: None,
717 })
718 }
719
720 pub async fn new_retry_fast<S: Signer + ?Sized>(
721 owner: AccountOwner,
722 round: Round,
723 old_proposal: BlockProposal,
724 signer: &S,
725 ) -> Result<Self, S::Error> {
726 let content = ProposalContent {
727 round,
728 block: old_proposal.content.block,
729 outcome: None,
730 };
731 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
732
733 Ok(Self {
734 content,
735 signature,
736 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
737 })
738 }
739
740 pub async fn new_retry_regular<S: Signer>(
741 owner: AccountOwner,
742 round: Round,
743 validated_block_certificate: ValidatedBlockCertificate,
744 signer: &S,
745 ) -> Result<Self, S::Error> {
746 let certificate = validated_block_certificate.lite_certificate().cloned();
747 let block = validated_block_certificate.into_inner().into_inner();
748 let (block, outcome) = block.into_proposal();
749 let content = ProposalContent {
750 block,
751 round,
752 outcome: Some(outcome),
753 };
754 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
755
756 Ok(Self {
757 content,
758 signature,
759 original_proposal: Some(OriginalProposal::Regular { certificate }),
760 })
761 }
762
763 pub fn owner(&self) -> AccountOwner {
765 match self.signature {
766 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
767 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
768 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
769 }
770 }
771
772 pub fn check_signature(&self) -> Result<(), CryptoError> {
773 self.signature.verify(&self.content)
774 }
775
776 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
777 self.content.block.published_blob_ids().into_iter().chain(
778 self.content
779 .outcome
780 .iter()
781 .flat_map(|outcome| outcome.oracle_blob_ids()),
782 )
783 }
784
785 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
786 self.content.block.published_blob_ids().into_iter().chain(
787 self.content.outcome.iter().flat_map(|outcome| {
788 outcome
789 .oracle_blob_ids()
790 .into_iter()
791 .chain(outcome.iter_created_blobs_ids())
792 }),
793 )
794 }
795
796 pub fn check_invariants(&self) -> Result<(), &'static str> {
798 match (&self.original_proposal, &self.content.outcome) {
799 (None, None) => {}
800 (Some(OriginalProposal::Fast(_)), None) => ensure!(
801 self.content.round > Round::Fast,
802 "The new proposal's round must be greater than the original's"
803 ),
804 (None, Some(_))
805 | (Some(OriginalProposal::Fast(_)), Some(_))
806 | (Some(OriginalProposal::Regular { .. }), None) => {
807 return Err("Must contain a validation certificate if and only if \
808 it contains the execution outcome from a previous round");
809 }
810 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
811 ensure!(
812 self.content.round > certificate.round,
813 "The new proposal's round must be greater than the original's"
814 );
815 let block = outcome.clone().with(self.content.block.clone());
816 let value = ValidatedBlock::new(block);
817 ensure!(
818 certificate.check_value(&value),
819 "Lite certificate must match the given block and execution outcome"
820 );
821 }
822 }
823 Ok(())
824 }
825}
826
827impl LiteVote {
828 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
830 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
831 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
832 Self {
833 value,
834 round,
835 signature,
836 }
837 }
838
839 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
841 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
842 Ok(self.signature.check(&hash_and_round, public_key)?)
843 }
844}
845
846pub struct SignatureAggregator<'a, T: CertificateValue> {
847 committee: &'a Committee,
848 weight: u64,
849 used_validators: HashSet<ValidatorPublicKey>,
850 partial: GenericCertificate<T>,
851}
852
853impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
854 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
856 Self {
857 committee,
858 weight: 0,
859 used_validators: HashSet::new(),
860 partial: GenericCertificate::new(value, round, Vec::new()),
861 }
862 }
863
864 pub fn append(
868 &mut self,
869 public_key: ValidatorPublicKey,
870 signature: ValidatorSignature,
871 ) -> Result<Option<GenericCertificate<T>>, ChainError>
872 where
873 T: CertificateValue,
874 {
875 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
876 signature.check(&hash_and_round, public_key)?;
877 ensure!(
879 !self.used_validators.contains(&public_key),
880 ChainError::CertificateValidatorReuse
881 );
882 self.used_validators.insert(public_key);
883 let voting_rights = self.committee.weight(&public_key);
885 ensure!(voting_rights > 0, ChainError::InvalidSigner);
886 self.weight += voting_rights;
887 self.partial.add_signature((public_key, signature));
889
890 if self.weight >= self.committee.quorum_threshold() {
891 self.weight = 0; Ok(Some(self.partial.clone()))
893 } else {
894 Ok(None)
895 }
896 }
897}
898
899pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
902 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
903}
904
905pub(crate) fn check_signatures(
907 value_hash: CryptoHash,
908 certificate_kind: CertificateKind,
909 round: Round,
910 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
911 committee: &Committee,
912) -> Result<(), ChainError> {
913 let mut weight = 0;
915 let mut used_validators = HashSet::new();
916 for (validator, _) in signatures {
917 ensure!(
919 !used_validators.contains(validator),
920 ChainError::CertificateValidatorReuse
921 );
922 used_validators.insert(*validator);
923 let voting_rights = committee.weight(validator);
925 ensure!(voting_rights > 0, ChainError::InvalidSigner);
926 weight += voting_rights;
927 }
928 ensure!(
929 weight >= committee.quorum_threshold(),
930 ChainError::CertificateRequiresQuorum
931 );
932 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
934 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
935 Ok(())
936}
937
938impl BcsSignable<'_> for ProposalContent {}
939
940impl BcsSignable<'_> for VoteValue {}
941
942doc_scalar!(
943 MessageAction,
944 "Whether an incoming message is accepted or rejected."
945);
946
947#[cfg(test)]
948mod signing {
949 use linera_base::{
950 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
951 data_types::{BlockHeight, Epoch, Round},
952 identifiers::ChainId,
953 };
954
955 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
956
957 #[test]
958 fn proposal_content_signing() {
959 use std::str::FromStr;
960
961 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
963 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
964 )
965 .unwrap();
966 let address = secret_key.address();
967
968 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
969 let public_key = signer.public();
970
971 let proposed_block = ProposedBlock {
972 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
973 epoch: Epoch(11),
974 transactions: vec![],
975 height: BlockHeight(11),
976 timestamp: 190000000u64.into(),
977 authenticated_owner: None,
978 previous_block_hash: None,
979 };
980
981 let proposal = ProposalContent {
982 block: proposed_block,
983 round: Round::SingleLeader(11),
984 outcome: None,
985 };
986
987 let signature = EvmSignature::from_str(
990 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
991 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
992 )
993 .unwrap();
994 let metamask_signature = AccountSignature::EvmSecp256k1 {
995 signature,
996 address: address.0 .0,
997 };
998
999 let signature = signer.sign(&proposal);
1000 assert_eq!(signature, metamask_signature);
1001
1002 assert_eq!(signature.owner(), public_key.into());
1003
1004 let block_proposal = BlockProposal {
1005 content: proposal,
1006 signature,
1007 original_proposal: None,
1008 };
1009 assert_eq!(block_proposal.owner(), public_key.into(),);
1010 }
1011}