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::{info, 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_signer: 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> {
124 let size = bcs::serialized_size(self)?;
125 ensure!(
126 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
127 ChainError::BlockProposalTooLarge(size)
128 );
129 Ok(())
130 }
131}
132
133#[async_graphql::ComplexObject]
134impl ProposedBlock {
135 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
137 self.transactions
138 .iter()
139 .map(TransactionMetadata::from_transaction)
140 .collect()
141 }
142}
143
144#[derive(
146 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative, strum::AsRefStr,
147)]
148pub enum Transaction {
149 ReceiveMessages(IncomingBundle),
151 ExecuteOperation(Operation),
153}
154
155impl BcsHashable<'_> for Transaction {}
156
157impl Transaction {
158 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
160 match self {
161 Transaction::ReceiveMessages(bundle) => Some(bundle),
162 _ => None,
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
169#[graphql(name = "Operation")]
170pub struct OperationMetadata {
171 pub operation_type: String,
173 pub application_id: Option<ApplicationId>,
175 pub user_bytes_hex: Option<String>,
177 pub system_operation: Option<SystemOperationMetadata>,
179}
180
181impl From<&Operation> for OperationMetadata {
182 fn from(operation: &Operation) -> Self {
183 match operation {
184 Operation::System(sys_op) => OperationMetadata {
185 operation_type: "System".to_string(),
186 application_id: None,
187 user_bytes_hex: None,
188 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
189 },
190 Operation::User {
191 application_id,
192 bytes,
193 } => OperationMetadata {
194 operation_type: "User".to_string(),
195 application_id: Some(*application_id),
196 user_bytes_hex: Some(hex::encode(bytes)),
197 system_operation: None,
198 },
199 }
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
205pub struct TransactionMetadata {
206 pub transaction_type: String,
208 pub incoming_bundle: Option<IncomingBundle>,
210 pub operation: Option<OperationMetadata>,
212}
213
214impl TransactionMetadata {
215 pub fn from_transaction(transaction: &Transaction) -> Self {
217 match transaction {
218 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
219 transaction_type: "ReceiveMessages".to_string(),
220 incoming_bundle: Some(bundle.clone()),
221 operation: None,
222 },
223 Transaction::ExecuteOperation(op) => TransactionMetadata {
224 transaction_type: "ExecuteOperation".to_string(),
225 incoming_bundle: None,
226 operation: Some(OperationMetadata::from(op)),
227 },
228 }
229 }
230}
231
232#[derive(
234 Debug,
235 Clone,
236 Copy,
237 Eq,
238 PartialEq,
239 Ord,
240 PartialOrd,
241 Serialize,
242 Deserialize,
243 SimpleObject,
244 Allocative,
245)]
246pub struct ChainAndHeight {
247 pub chain_id: ChainId,
249 pub height: BlockHeight,
251}
252
253#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
255pub struct IncomingBundle {
256 pub origin: ChainId,
258 pub bundle: MessageBundle,
260 pub action: MessageAction,
262}
263
264impl IncomingBundle {
265 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
267 self.bundle.messages.iter()
268 }
269
270 #[instrument(level = "trace", skip(self))]
273 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
274 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
275 if !chain_ids.contains(&self.origin) {
276 return None;
277 }
278 }
279 if policy.ignore_chain_ids.contains(&self.origin) {
280 return None;
281 }
282 if !policy.never_reject_application_ids.is_empty()
283 && self.messages().all(|posted_msg| {
284 policy
285 .never_reject_application_ids
286 .contains(&posted_msg.message.application_id())
287 })
288 {
289 return Some(self);
290 }
291 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
292 if !self
293 .messages()
294 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
295 {
296 return None;
297 }
298 }
299 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
300 if !self
301 .messages()
302 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
303 {
304 return None;
305 }
306 }
307 if policy.is_reject() {
308 if self.bundle.is_skippable() {
309 return None;
310 } else if !self.bundle.is_protected() {
311 info!(
312 origin = %self.origin,
313 "Rejecting incoming message bundle due to the message policy"
314 );
315 self.action = MessageAction::Reject;
316 }
317 }
318 Some(self)
319 }
320}
321
322impl BcsHashable<'_> for IncomingBundle {}
323
324#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
326pub enum MessageAction {
327 Accept,
329 Reject,
331}
332
333#[derive(Clone, Debug, Default, PartialEq, Eq)]
335pub enum BundleFailurePolicy {
336 #[default]
338 Abort,
339 AutoRetry {
353 max_failures: u32,
355 never_reject_application_ids: Arc<HashSet<GenericApplicationId>>,
359 },
360}
361
362#[derive(Clone, Debug, PartialEq, Eq)]
364pub struct BundleExecutionPolicy {
365 pub on_failure: BundleFailurePolicy,
367 pub time_budget: Option<Duration>,
371}
372
373impl BundleExecutionPolicy {
374 pub fn committed() -> Self {
376 BundleExecutionPolicy {
377 on_failure: BundleFailurePolicy::Abort,
378 time_budget: None,
379 }
380 }
381}
382
383#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
385pub struct MessageBundle {
386 pub height: BlockHeight,
388 pub timestamp: Timestamp,
390 pub certificate_hash: CryptoHash,
392 pub transaction_index: u32,
394 pub messages: Vec<PostedMessage>,
396}
397
398impl MessageBundle {
399 pub fn estimated_size(&self) -> usize {
401 let overhead = 60;
403 let messages_size: usize = self
404 .messages
405 .iter()
406 .map(PostedMessage::estimated_size)
407 .sum();
408 overhead + messages_size
409 }
410}
411
412impl PostedMessage {
413 pub fn estimated_size(&self) -> usize {
415 let overhead = 96;
417 let message_size = match &self.message {
418 Message::System(_) => 256, Message::User { bytes, .. } => 64 + bytes.len(),
420 };
421 overhead + message_size
422 }
423}
424
425#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
426#[cfg_attr(with_testing, derive(Eq, PartialEq))]
427pub enum OriginalProposal {
429 Fast(AccountSignature),
431 Regular {
433 certificate: LiteCertificate<'static>,
435 },
436}
437
438#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
442#[cfg_attr(with_testing, derive(Eq, PartialEq))]
443pub struct BlockProposal {
444 pub content: ProposalContent,
447 pub signature: AccountSignature,
449 #[debug(skip_if = Option::is_none)]
451 pub original_proposal: Option<OriginalProposal>,
452}
453
454#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
456#[graphql(complex)]
457pub struct PostedMessage {
458 #[debug(skip_if = Option::is_none)]
460 pub authenticated_signer: Option<AccountOwner>,
461 #[debug(skip_if = Amount::is_zero)]
463 pub grant: Amount,
464 #[debug(skip_if = Option::is_none)]
466 pub refund_grant_to: Option<Account>,
467 pub kind: MessageKind,
469 pub index: u32,
471 pub message: Message,
473}
474
475pub trait OutgoingMessageExt {
477 fn into_posted(self, index: u32) -> PostedMessage;
479}
480
481impl OutgoingMessageExt for OutgoingMessage {
482 fn into_posted(self, index: u32) -> PostedMessage {
484 let OutgoingMessage {
485 destination: _,
486 authenticated_signer,
487 grant,
488 refund_grant_to,
489 kind,
490 message,
491 } = self;
492 PostedMessage {
493 authenticated_signer,
494 grant,
495 refund_grant_to,
496 kind,
497 index,
498 message,
499 }
500 }
501}
502
503#[async_graphql::ComplexObject]
504impl PostedMessage {
505 async fn message_metadata(&self) -> MessageMetadata {
507 MessageMetadata::from(&self.message)
508 }
509}
510
511#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
513pub struct OperationResult(
514 #[debug(with = "hex_debug")]
515 #[serde(with = "serde_bytes")]
516 pub Vec<u8>,
517);
518
519impl BcsHashable<'_> for OperationResult {}
520
521doc_scalar!(
522 OperationResult,
523 "The execution result of a single operation."
524);
525
526#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
528#[cfg_attr(with_testing, derive(Default))]
529pub struct BlockExecutionOutcome {
530 pub messages: Vec<Vec<OutgoingMessage>>,
532 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
534 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
536 pub state_hash: CryptoHash,
538 pub oracle_responses: Vec<Vec<OracleResponse>>,
540 pub events: Vec<Vec<Event>>,
542 pub blobs: Vec<Vec<Blob>>,
544 pub operation_results: Vec<OperationResult>,
546}
547
548#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
550pub struct LiteValue {
551 pub value_hash: CryptoHash,
553 pub chain_id: ChainId,
555 pub kind: CertificateKind,
557}
558
559impl LiteValue {
560 pub fn new<T: CertificateValue>(value: &T) -> Self {
562 LiteValue {
563 value_hash: value.hash(),
564 chain_id: value.chain_id(),
565 kind: T::KIND,
566 }
567 }
568}
569
570#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
573pub struct VoteValue(CryptoHash, Round, CertificateKind);
574
575#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
577#[serde(bound(deserialize = "T: Deserialize<'de>"))]
578pub struct Vote<T> {
579 pub value: T,
581 pub round: Round,
583 pub signature: ValidatorSignature,
585}
586
587impl<T> Vote<T> {
588 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
590 where
591 T: CertificateValue,
592 {
593 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
594 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
595 Self {
596 value,
597 round,
598 signature,
599 }
600 }
601
602 pub fn lite(&self) -> LiteVote
604 where
605 T: CertificateValue,
606 {
607 LiteVote {
608 value: LiteValue::new(&self.value),
609 round: self.round,
610 signature: self.signature,
611 }
612 }
613
614 pub fn value(&self) -> &T {
616 &self.value
617 }
618}
619
620#[derive(Clone, Debug, Serialize, Deserialize)]
622#[cfg_attr(with_testing, derive(Eq, PartialEq))]
623pub struct LiteVote {
624 pub value: LiteValue,
626 pub round: Round,
628 pub signature: ValidatorSignature,
630}
631
632impl LiteVote {
633 #[cfg(with_testing)]
635 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
636 if self.value.value_hash != value.hash() {
637 return None;
638 }
639 Some(Vote {
640 value,
641 round: self.round,
642 signature: self.signature,
643 })
644 }
645
646 pub fn kind(&self) -> CertificateKind {
648 self.value.kind
649 }
650}
651
652impl MessageBundle {
653 pub fn is_skippable(&self) -> bool {
655 self.messages.iter().all(PostedMessage::is_skippable)
656 }
657
658 pub fn is_protected(&self) -> bool {
660 self.messages.iter().any(PostedMessage::is_protected)
661 }
662}
663
664impl PostedMessage {
665 pub fn is_skippable(&self) -> bool {
667 match self.kind {
668 MessageKind::Protected | MessageKind::Tracked => false,
669 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
670 }
671 }
672
673 pub fn is_protected(&self) -> bool {
675 matches!(self.kind, MessageKind::Protected)
676 }
677
678 pub fn is_tracked(&self) -> bool {
680 matches!(self.kind, MessageKind::Tracked)
681 }
682
683 pub fn is_bouncing(&self) -> bool {
685 matches!(self.kind, MessageKind::Bouncing)
686 }
687}
688
689impl BlockExecutionOutcome {
690 pub fn with(self, block: ProposedBlock) -> Block {
692 Block::new(block, self)
693 }
694
695 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
697 let mut required_blob_ids = HashSet::new();
698 for responses in &self.oracle_responses {
699 for response in responses {
700 if let OracleResponse::Blob(blob_id) = response {
701 required_blob_ids.insert(*blob_id);
702 }
703 }
704 }
705
706 required_blob_ids
707 }
708
709 pub fn has_oracle_responses(&self) -> bool {
711 self.oracle_responses
712 .iter()
713 .any(|responses| !responses.is_empty())
714 }
715
716 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
718 self.blobs.iter().flatten().map(|blob| blob.id())
719 }
720}
721
722#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
724pub struct ProposalContent {
725 pub block: ProposedBlock,
727 pub round: Round,
729 #[debug(skip_if = Option::is_none)]
731 pub outcome: Option<BlockExecutionOutcome>,
732}
733
734impl BlockProposal {
735 pub async fn new_initial<S: Signer + ?Sized>(
737 owner: AccountOwner,
738 round: Round,
739 block: ProposedBlock,
740 signer: &S,
741 ) -> Result<Self, S::Error> {
742 let content = ProposalContent {
743 round,
744 block,
745 outcome: None,
746 };
747 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
748
749 Ok(Self {
750 content,
751 signature,
752 original_proposal: None,
753 })
754 }
755
756 pub async fn new_retry_fast<S: Signer + ?Sized>(
758 owner: AccountOwner,
759 round: Round,
760 old_proposal: BlockProposal,
761 signer: &S,
762 ) -> Result<Self, S::Error> {
763 let content = ProposalContent {
764 round,
765 block: old_proposal.content.block,
766 outcome: None,
767 };
768 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
769
770 Ok(Self {
771 content,
772 signature,
773 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
774 })
775 }
776
777 pub async fn new_retry_regular<S: Signer>(
779 owner: AccountOwner,
780 round: Round,
781 validated_block_certificate: ValidatedBlockCertificate,
782 signer: &S,
783 ) -> Result<Self, S::Error> {
784 let certificate = validated_block_certificate.lite_certificate().cloned();
785 let block = validated_block_certificate.into_inner().into_inner();
786 let (block, outcome) = block.into_proposal();
787 let content = ProposalContent {
788 block,
789 round,
790 outcome: Some(outcome),
791 };
792 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
793
794 Ok(Self {
795 content,
796 signature,
797 original_proposal: Some(OriginalProposal::Regular { certificate }),
798 })
799 }
800
801 pub fn owner(&self) -> AccountOwner {
803 match self.signature {
804 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
805 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
806 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
807 }
808 }
809
810 pub fn check_signature(&self) -> Result<(), CryptoError> {
812 self.signature.verify(&self.content)
813 }
814
815 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
817 self.content.block.published_blob_ids().into_iter().chain(
818 self.content
819 .outcome
820 .iter()
821 .flat_map(|outcome| outcome.oracle_blob_ids()),
822 )
823 }
824
825 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
827 self.content.block.published_blob_ids().into_iter().chain(
828 self.content.outcome.iter().flat_map(|outcome| {
829 outcome
830 .oracle_blob_ids()
831 .into_iter()
832 .chain(outcome.iter_created_blobs_ids())
833 }),
834 )
835 }
836
837 pub fn check_invariants(&self) -> Result<(), &'static str> {
839 match (&self.original_proposal, &self.content.outcome) {
840 (None, None) => {}
841 (Some(OriginalProposal::Fast(_)), None) => ensure!(
842 self.content.round > Round::Fast,
843 "The new proposal's round must be greater than the original's"
844 ),
845 (None, Some(_))
846 | (Some(OriginalProposal::Fast(_)), Some(_))
847 | (Some(OriginalProposal::Regular { .. }), None) => {
848 return Err("Must contain a validation certificate if and only if \
849 it contains the execution outcome from a previous round");
850 }
851 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
852 ensure!(
853 self.content.round > certificate.round,
854 "The new proposal's round must be greater than the original's"
855 );
856 let block = outcome.clone().with(self.content.block.clone());
857 let value = ValidatedBlock::new(block);
858 ensure!(
859 certificate.check_value(&value),
860 "Lite certificate must match the given block and execution outcome"
861 );
862 }
863 }
864 Ok(())
865 }
866}
867
868impl LiteVote {
869 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
871 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
872 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
873 Self {
874 value,
875 round,
876 signature,
877 }
878 }
879
880 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
882 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
883 Ok(self.signature.check(&hash_and_round, public_key)?)
884 }
885}
886
887pub struct SignatureAggregator<'a, T: CertificateValue> {
889 committee: &'a Committee,
890 weight: u64,
891 used_validators: HashSet<ValidatorPublicKey>,
892 partial: GenericCertificate<T>,
893}
894
895impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
896 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
898 Self {
899 committee,
900 weight: 0,
901 used_validators: HashSet::new(),
902 partial: GenericCertificate::new(value, round, Vec::new()),
903 }
904 }
905
906 pub fn append(
910 &mut self,
911 public_key: ValidatorPublicKey,
912 signature: ValidatorSignature,
913 ) -> Result<Option<GenericCertificate<T>>, ChainError>
914 where
915 T: CertificateValue,
916 {
917 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
918 signature.check(&hash_and_round, public_key)?;
919 ensure!(
921 !self.used_validators.contains(&public_key),
922 ChainError::CertificateValidatorReuse
923 );
924 self.used_validators.insert(public_key);
925 let voting_rights = self.committee.weight(&public_key);
927 ensure!(voting_rights > 0, ChainError::InvalidSigner);
928 self.weight += voting_rights;
929 self.partial.add_signature((public_key, signature));
931
932 if self.weight >= self.committee.quorum_threshold() {
933 self.weight = 0; Ok(Some(self.partial.clone()))
935 } else {
936 Ok(None)
937 }
938 }
939}
940
941pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
944 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
945}
946
947pub(crate) fn check_signatures(
949 value_hash: CryptoHash,
950 certificate_kind: CertificateKind,
951 round: Round,
952 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
953 committee: &Committee,
954) -> Result<(), ChainError> {
955 let mut weight = 0;
957 let mut used_validators = HashSet::new();
958 for (validator, _) in signatures {
959 ensure!(
961 !used_validators.contains(validator),
962 ChainError::CertificateValidatorReuse
963 );
964 used_validators.insert(*validator);
965 let voting_rights = committee.weight(validator);
967 ensure!(voting_rights > 0, ChainError::InvalidSigner);
968 weight += voting_rights;
969 }
970 ensure!(
971 weight >= committee.quorum_threshold(),
972 ChainError::CertificateRequiresQuorum
973 );
974 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
976 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
977 Ok(())
978}
979
980impl BcsSignable<'_> for ProposalContent {}
981
982impl BcsSignable<'_> for VoteValue {}
983
984doc_scalar!(
985 MessageAction,
986 "Whether an incoming message is accepted or rejected."
987);
988
989#[cfg(test)]
990mod signing {
991 use linera_base::{
992 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
993 data_types::{BlockHeight, Epoch, Round},
994 identifiers::ChainId,
995 };
996
997 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
998
999 #[test]
1000 fn proposal_content_signing() {
1001 use std::str::FromStr;
1002
1003 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
1005 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
1006 )
1007 .unwrap();
1008 let address = secret_key.address();
1009
1010 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
1011 let public_key = signer.public();
1012
1013 let proposed_block = ProposedBlock {
1014 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
1015 epoch: Epoch(11),
1016 transactions: vec![],
1017 height: BlockHeight(11),
1018 timestamp: 190000000u64.into(),
1019 authenticated_signer: None,
1020 previous_block_hash: None,
1021 };
1022
1023 let proposal = ProposalContent {
1024 block: proposed_block,
1025 round: Round::SingleLeader(11),
1026 outcome: None,
1027 };
1028
1029 let signature = EvmSignature::from_str(
1032 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
1033 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
1034 )
1035 .unwrap();
1036 let metamask_signature = AccountSignature::EvmSecp256k1 {
1037 signature,
1038 address: address.0 .0,
1039 };
1040
1041 let signature = signer.sign(&proposal);
1042 assert_eq!(signature, metamask_signature);
1043
1044 assert_eq!(signature.owner(), public_key.into());
1045
1046 let block_proposal = BlockProposal {
1047 content: proposal,
1048 signature,
1049 original_proposal: None,
1050 };
1051 assert_eq!(block_proposal.owner(), public_key.into(),);
1052 }
1053}