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, Cursor, Epoch, Event, MessagePolicy, OracleResponse, Round,
21 Timestamp,
22 },
23 doc_scalar, ensure, hex, hex_debug,
24 identifiers::{
25 Account, AccountOwner, ApplicationId, BlobId, ChainId, GenericApplicationId, StreamId,
26 },
27 time::Duration,
28};
29use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
30use serde::{Deserialize, Serialize};
31use tracing::{info, instrument};
32
33use crate::{
34 block::{Block, ValidatedBlock},
35 types::{
36 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
37 ValidatedBlockCertificate,
38 },
39 ChainError,
40};
41
42pub mod metadata;
43
44pub use metadata::*;
45
46#[cfg(test)]
47#[path = "../unit_tests/data_types_tests.rs"]
48mod data_types_tests;
49
50#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
58#[graphql(complex)]
59pub struct ProposedBlock {
60 pub chain_id: ChainId,
62 pub epoch: Epoch,
64 #[debug(skip_if = Vec::is_empty)]
67 #[graphql(skip)]
68 pub transactions: Vec<Transaction>,
69 pub height: BlockHeight,
71 pub timestamp: Timestamp,
74 #[debug(skip_if = Option::is_none)]
79 pub authenticated_owner: Option<AccountOwner>,
80 pub previous_block_hash: Option<CryptoHash>,
83}
84
85impl ProposedBlock {
86 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
88 self.operations()
89 .flat_map(Operation::published_blob_ids)
90 .collect()
91 }
92
93 pub fn starts_with_checkpoint(&self) -> bool {
98 self.transactions
99 .first()
100 .is_some_and(Transaction::is_checkpoint)
101 }
102
103 pub fn has_only_rejected_messages(&self) -> bool {
106 self.transactions.iter().all(|txn| {
107 matches!(
108 txn,
109 Transaction::ReceiveMessages(IncomingBundle {
110 action: MessageAction::Reject,
111 ..
112 })
113 )
114 })
115 }
116
117 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
119 self.transactions.iter().filter_map(|tx| match tx {
120 Transaction::ExecuteOperation(operation) => Some(operation),
121 Transaction::ReceiveMessages(_) => None,
122 })
123 }
124
125 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
127 self.transactions.iter().filter_map(|tx| match tx {
128 Transaction::ReceiveMessages(bundle) => Some(bundle),
129 Transaction::ExecuteOperation(_) => None,
130 })
131 }
132
133 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(
157 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative, strum::AsRefStr,
158)]
159pub enum Transaction {
160 ReceiveMessages(IncomingBundle),
162 ExecuteOperation(Operation),
164}
165
166impl BcsHashable<'_> for Transaction {}
167
168impl Transaction {
169 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
171 match self {
172 Transaction::ReceiveMessages(bundle) => Some(bundle),
173 _ => None,
174 }
175 }
176
177 pub fn is_update_stream(&self) -> bool {
179 matches!(
180 self,
181 Transaction::ExecuteOperation(op) if op.is_update_stream()
182 )
183 }
184
185 pub fn is_checkpoint(&self) -> bool {
187 matches!(
188 self,
189 Transaction::ExecuteOperation(op) if op.is_checkpoint()
190 )
191 }
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
196#[graphql(name = "Operation")]
197pub struct OperationMetadata {
198 pub operation_type: String,
200 pub application_id: Option<ApplicationId>,
202 pub user_bytes_hex: Option<String>,
204 pub system_operation: Option<SystemOperationMetadata>,
206}
207
208impl From<&Operation> for OperationMetadata {
209 fn from(operation: &Operation) -> Self {
210 match operation {
211 Operation::System(sys_op) => OperationMetadata {
212 operation_type: "System".to_string(),
213 application_id: None,
214 user_bytes_hex: None,
215 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
216 },
217 Operation::User {
218 application_id,
219 bytes,
220 } => OperationMetadata {
221 operation_type: "User".to_string(),
222 application_id: Some(*application_id),
223 user_bytes_hex: Some(hex::encode(bytes)),
224 system_operation: None,
225 },
226 }
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
232pub struct TransactionMetadata {
233 pub transaction_type: String,
235 pub incoming_bundle: Option<IncomingBundle>,
237 pub operation: Option<OperationMetadata>,
239}
240
241impl TransactionMetadata {
242 pub fn from_transaction(transaction: &Transaction) -> Self {
244 match transaction {
245 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
246 transaction_type: "ReceiveMessages".to_string(),
247 incoming_bundle: Some(bundle.clone()),
248 operation: None,
249 },
250 Transaction::ExecuteOperation(op) => TransactionMetadata {
251 transaction_type: "ExecuteOperation".to_string(),
252 incoming_bundle: None,
253 operation: Some(OperationMetadata::from(op)),
254 },
255 }
256 }
257}
258
259#[derive(
261 Debug,
262 Clone,
263 Copy,
264 Eq,
265 PartialEq,
266 Ord,
267 PartialOrd,
268 Serialize,
269 Deserialize,
270 SimpleObject,
271 Allocative,
272)]
273pub struct ChainAndHeight {
274 pub chain_id: ChainId,
276 pub height: BlockHeight,
278}
279
280#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
282pub struct IncomingBundle {
283 pub origin: ChainId,
285 pub bundle: MessageBundle,
287 pub action: MessageAction,
289}
290
291impl IncomingBundle {
292 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
294 self.bundle.messages.iter()
295 }
296
297 fn matches_policy(&self, policy: &MessagePolicy) -> bool {
298 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
299 if !chain_ids.contains(&self.origin) {
300 return false;
301 }
302 }
303 if policy.ignore_chain_ids.contains(&self.origin) {
304 return false;
305 }
306 if !policy.never_reject_application_ids.is_empty()
307 && self.messages().all(|posted_msg| {
308 policy
309 .never_reject_application_ids
310 .contains(&posted_msg.message.application_id())
311 })
312 {
313 return true;
314 }
315 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
316 if !self
317 .messages()
318 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
319 {
320 return false;
321 }
322 }
323 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
324 if !self
325 .messages()
326 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
327 {
328 return false;
329 }
330 }
331 !policy.is_reject()
332 }
333
334 #[instrument(level = "trace", skip(self))]
337 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
338 if !self.matches_policy(policy) {
339 if self.bundle.is_skippable() {
340 return None;
341 } else if !self.bundle.is_protected() {
342 info!(
343 origin = %self.origin,
344 "Rejecting incoming message bundle due to the message policy"
345 );
346 self.action = MessageAction::Reject;
347 }
348 }
349 Some(self)
350 }
351}
352
353impl BcsHashable<'_> for IncomingBundle {}
354
355#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
357pub enum MessageAction {
358 Accept,
360 Reject,
362}
363
364#[derive(Clone, Debug, Default, PartialEq, Eq)]
366pub enum BundleFailurePolicy {
367 #[default]
369 Abort,
370 AutoRetry {
384 max_failures: u32,
386 never_reject_application_ids: Arc<HashSet<GenericApplicationId>>,
390 },
391}
392
393#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct BundleExecutionPolicy {
396 pub on_failure: BundleFailurePolicy,
398 pub time_budget: Option<Duration>,
400}
401
402impl BundleExecutionPolicy {
403 pub fn committed() -> Self {
405 BundleExecutionPolicy {
406 on_failure: BundleFailurePolicy::Abort,
407 time_budget: None,
408 }
409 }
410}
411
412#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
414pub struct MessageBundle {
415 pub height: BlockHeight,
417 pub timestamp: Timestamp,
419 pub certificate_hash: CryptoHash,
421 pub transaction_index: u32,
423 pub messages: Vec<PostedMessage>,
425}
426
427#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
428#[cfg_attr(with_testing, derive(Eq, PartialEq))]
429pub enum OriginalProposal {
431 Fast(AccountSignature),
433 Regular {
435 certificate: LiteCertificate<'static>,
437 },
438}
439
440#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
444#[cfg_attr(with_testing, derive(Eq, PartialEq))]
445pub struct BlockProposal {
446 pub content: ProposalContent,
449 pub signature: AccountSignature,
451 #[debug(skip_if = Option::is_none)]
453 pub original_proposal: Option<OriginalProposal>,
454}
455
456#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
458#[graphql(complex)]
459pub struct PostedMessage {
460 #[debug(skip_if = Option::is_none)]
462 pub authenticated_owner: Option<AccountOwner>,
463 #[debug(skip_if = Amount::is_zero)]
465 pub grant: Amount,
466 #[debug(skip_if = Option::is_none)]
468 pub refund_grant_to: Option<Account>,
469 pub kind: MessageKind,
471 pub message: Message,
473}
474
475pub trait OutgoingMessageExt {
477 fn into_posted(self) -> PostedMessage;
479}
480
481impl OutgoingMessageExt for OutgoingMessage {
482 fn into_posted(self) -> PostedMessage {
484 let OutgoingMessage {
485 destination: _,
486 authenticated_owner,
487 grant,
488 refund_grant_to,
489 kind,
490 message,
491 } = self;
492 PostedMessage {
493 authenticated_owner,
494 grant,
495 refund_grant_to,
496 kind,
497 message,
498 }
499 }
500}
501
502#[async_graphql::ComplexObject]
503impl PostedMessage {
504 async fn message_metadata(&self) -> MessageMetadata {
506 MessageMetadata::from(&self.message)
507 }
508}
509
510#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
512pub struct OperationResult(
513 #[debug(with = "hex_debug")]
514 #[serde(with = "serde_bytes")]
515 pub Vec<u8>,
516);
517
518impl BcsHashable<'_> for OperationResult {}
519
520doc_scalar!(
521 OperationResult,
522 "The execution result of a single operation."
523);
524
525#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
527#[cfg_attr(with_testing, derive(Default))]
528pub struct BlockExecutionOutcome {
529 pub messages: Vec<Vec<OutgoingMessage>>,
531 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
533 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
535 pub state_hash: CryptoHash,
537 pub oracle_responses: Vec<Vec<OracleResponse>>,
539 pub events: Vec<Vec<Event>>,
541 pub blobs: Vec<Vec<Blob>>,
543 pub operation_results: Vec<OperationResult>,
545}
546
547#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
549pub struct LiteValue {
550 pub value_hash: CryptoHash,
552 pub chain_id: ChainId,
554 pub kind: CertificateKind,
556}
557
558impl LiteValue {
559 pub fn new<T: CertificateValue>(value: &T) -> Self {
561 LiteValue {
562 value_hash: value.hash(),
563 chain_id: value.chain_id(),
564 kind: T::KIND,
565 }
566 }
567}
568
569#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
594pub struct VoteValue(
595 pub(crate) CryptoHash,
596 pub(crate) Round,
597 pub(crate) CertificateKind,
598 pub(crate) Option<Round>,
599 pub(crate) bool,
600 pub(crate) Option<CryptoHash>,
601);
602
603#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
605#[serde(bound(deserialize = "T: Deserialize<'de>"))]
606pub struct Vote<T> {
607 pub value: T,
609 pub round: Round,
611 pub unlocking_round: Option<Round>,
614 pub first_round: bool,
618 pub justification_commitment: Option<CryptoHash>,
621 pub signature: ValidatorSignature,
624}
625
626impl<T> Vote<T> {
627 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
629 where
630 T: CertificateValue,
631 {
632 Self::new_with_unlocking_round(value, round, None, None, key_pair)
633 }
634
635 pub fn new_with_unlocking_round(
638 value: T,
639 round: Round,
640 unlocking_round: Option<Round>,
641 justification_commitment: Option<CryptoHash>,
642 key_pair: &ValidatorSecretKey,
643 ) -> Self
644 where
645 T: CertificateValue,
646 {
647 let hash_and_round = VoteValue(
648 value.hash(),
649 round,
650 T::KIND,
651 unlocking_round,
652 false,
653 justification_commitment,
654 );
655 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
656 Self {
657 value,
658 round,
659 unlocking_round,
660 first_round: false,
661 justification_commitment,
662 signature,
663 }
664 }
665
666 pub fn new_with_first_round(
670 value: T,
671 round: Round,
672 first_round: bool,
673 justification_commitment: Option<CryptoHash>,
674 key_pair: &ValidatorSecretKey,
675 ) -> Self
676 where
677 T: CertificateValue,
678 {
679 let hash_and_round = VoteValue(
680 value.hash(),
681 round,
682 T::KIND,
683 None,
684 first_round,
685 justification_commitment,
686 );
687 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
688 Self {
689 value,
690 round,
691 unlocking_round: None,
692 first_round,
693 justification_commitment,
694 signature,
695 }
696 }
697
698 pub fn lite(&self) -> LiteVote
700 where
701 T: CertificateValue,
702 {
703 LiteVote {
704 value: LiteValue::new(&self.value),
705 round: self.round,
706 unlocking_round: self.unlocking_round,
707 first_round: self.first_round,
708 justification_commitment: self.justification_commitment,
709 signature: self.signature,
710 }
711 }
712
713 pub fn value(&self) -> &T {
715 &self.value
716 }
717}
718
719#[derive(Clone, Debug, Serialize, Deserialize)]
721#[cfg_attr(with_testing, derive(Eq, PartialEq))]
722pub struct LiteVote {
723 pub value: LiteValue,
725 pub round: Round,
727 pub unlocking_round: Option<Round>,
730 pub first_round: bool,
734 pub justification_commitment: Option<CryptoHash>,
737 pub signature: ValidatorSignature,
740}
741
742impl LiteVote {
743 #[cfg(with_testing)]
745 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
746 if self.value.value_hash != value.hash() {
747 return None;
748 }
749 Some(Vote {
750 value,
751 round: self.round,
752 unlocking_round: self.unlocking_round,
753 first_round: self.first_round,
754 justification_commitment: self.justification_commitment,
755 signature: self.signature,
756 })
757 }
758
759 pub fn kind(&self) -> CertificateKind {
761 self.value.kind
762 }
763}
764
765impl MessageBundle {
766 pub fn cursor(&self) -> Cursor {
769 Cursor {
770 height: self.height,
771 index: self.transaction_index,
772 }
773 }
774
775 pub fn estimated_size(&self) -> usize {
777 let overhead = 60;
779 let messages_size: usize = self
780 .messages
781 .iter()
782 .map(PostedMessage::estimated_size)
783 .sum();
784 overhead + messages_size
785 }
786
787 pub fn is_skippable(&self) -> bool {
789 self.messages.iter().all(PostedMessage::is_skippable)
790 }
791
792 pub fn is_protected(&self) -> bool {
794 self.messages.iter().any(PostedMessage::is_protected)
795 }
796}
797
798impl PostedMessage {
799 pub fn estimated_size(&self) -> usize {
801 let overhead = 92;
803 let message_size = match &self.message {
804 Message::System(_) => 256, Message::User { bytes, .. } => 64 + bytes.len(),
806 };
807 overhead + message_size
808 }
809
810 pub fn is_skippable(&self) -> bool {
812 match self.kind {
813 MessageKind::Protected | MessageKind::Tracked => false,
814 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
815 }
816 }
817
818 pub fn is_protected(&self) -> bool {
820 matches!(self.kind, MessageKind::Protected)
821 }
822
823 pub fn is_tracked(&self) -> bool {
825 matches!(self.kind, MessageKind::Tracked)
826 }
827
828 pub fn is_bouncing(&self) -> bool {
830 matches!(self.kind, MessageKind::Bouncing)
831 }
832}
833
834impl BlockExecutionOutcome {
835 pub fn with(self, block: ProposedBlock) -> Block {
837 Block::new(block, self)
838 }
839
840 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
842 let mut required_blob_ids = HashSet::new();
843 for responses in &self.oracle_responses {
844 for response in responses {
845 match response {
846 OracleResponse::Blob(blob_id) => {
847 required_blob_ids.insert(*blob_id);
848 }
849 OracleResponse::Checkpoint { used_blobs, .. } => {
850 required_blob_ids.extend(used_blobs.iter().copied());
851 }
852 _ => {}
853 }
854 }
855 }
856
857 required_blob_ids
858 }
859
860 pub fn has_oracle_responses(&self) -> bool {
862 self.oracle_responses
863 .iter()
864 .any(|responses| !responses.is_empty())
865 }
866
867 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
869 self.blobs.iter().flatten().map(|blob| blob.id())
870 }
871}
872
873#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
875pub struct ProposalContent {
876 pub block: ProposedBlock,
878 pub round: Round,
880 #[debug(skip_if = Option::is_none)]
882 pub outcome: Option<BlockExecutionOutcome>,
883}
884
885impl BlockProposal {
886 pub async fn new_initial<S: Signer + ?Sized>(
888 owner: AccountOwner,
889 round: Round,
890 block: ProposedBlock,
891 signer: &S,
892 ) -> Result<Self, S::Error> {
893 let content = ProposalContent {
894 round,
895 block,
896 outcome: None,
897 };
898 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
899
900 Ok(Self {
901 content,
902 signature,
903 original_proposal: None,
904 })
905 }
906
907 pub async fn new_retry_fast<S: Signer + ?Sized>(
909 owner: AccountOwner,
910 round: Round,
911 old_proposal: BlockProposal,
912 signer: &S,
913 ) -> Result<Self, S::Error> {
914 let content = ProposalContent {
915 round,
916 block: old_proposal.content.block,
917 outcome: None,
918 };
919 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
920
921 Ok(Self {
922 content,
923 signature,
924 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
925 })
926 }
927
928 pub async fn new_retry_regular<S: Signer>(
930 owner: AccountOwner,
931 round: Round,
932 validated_block_certificate: ValidatedBlockCertificate,
933 signer: &S,
934 ) -> Result<Self, S::Error> {
935 let certificate = validated_block_certificate.lite_certificate().cloned();
936 let block = validated_block_certificate.into_inner().into_inner();
937 let (block, outcome) = block.into_proposal();
938 let content = ProposalContent {
939 block,
940 round,
941 outcome: Some(outcome),
942 };
943 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
944
945 Ok(Self {
946 content,
947 signature,
948 original_proposal: Some(OriginalProposal::Regular { certificate }),
949 })
950 }
951
952 pub fn owner(&self) -> AccountOwner {
954 match self.signature {
955 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
956 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
957 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
958 }
959 }
960
961 pub fn check_signature(&self) -> Result<(), CryptoError> {
963 self.signature.verify(&self.content)
964 }
965
966 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
968 self.content.block.published_blob_ids().into_iter().chain(
969 self.content
970 .outcome
971 .iter()
972 .flat_map(|outcome| outcome.oracle_blob_ids()),
973 )
974 }
975
976 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
978 self.content.block.published_blob_ids().into_iter().chain(
979 self.content.outcome.iter().flat_map(|outcome| {
980 outcome
981 .oracle_blob_ids()
982 .into_iter()
983 .chain(outcome.iter_created_blobs_ids())
984 }),
985 )
986 }
987
988 pub fn check_invariants(&self) -> Result<(), &'static str> {
990 match (&self.original_proposal, &self.content.outcome) {
991 (None, None) => {}
992 (Some(OriginalProposal::Fast(_)), None) => ensure!(
993 self.content.round > Round::Fast,
994 "The new proposal's round must be greater than the original's"
995 ),
996 (None, Some(_))
997 | (Some(OriginalProposal::Fast(_)), Some(_))
998 | (Some(OriginalProposal::Regular { .. }), None) => {
999 return Err("Must contain a validation certificate if and only if \
1000 it contains the execution outcome from a previous round");
1001 }
1002 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
1003 ensure!(
1004 self.content.round > certificate.round,
1005 "The new proposal's round must be greater than the original's"
1006 );
1007 let block = outcome.clone().with(self.content.block.clone());
1008 let value = ValidatedBlock::new(block);
1009 ensure!(
1010 certificate.check_value(&value),
1011 "Lite certificate must match the given block and execution outcome"
1012 );
1013 }
1014 }
1015 Ok(())
1016 }
1017}
1018
1019impl LiteVote {
1020 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
1022 let hash_and_round = VoteValue(value.value_hash, round, value.kind, None, false, None);
1023 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
1024 Self {
1025 value,
1026 round,
1027 unlocking_round: None,
1028 first_round: false,
1029 justification_commitment: None,
1030 signature,
1031 }
1032 }
1033
1034 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
1036 let hash_and_round = VoteValue(
1037 self.value.value_hash,
1038 self.round,
1039 self.value.kind,
1040 self.unlocking_round,
1041 self.first_round,
1042 self.justification_commitment,
1043 );
1044 Ok(self.signature.check(&hash_and_round, public_key)?)
1045 }
1046}
1047
1048pub struct SignatureAggregator<'a, T: CertificateValue> {
1050 committee: &'a Committee,
1051 weight: u64,
1052 used_validators: HashSet<ValidatorPublicKey>,
1053 partial: GenericCertificate<T>,
1054}
1055
1056impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
1057 pub fn new(
1061 value: T,
1062 round: Round,
1063 unlocking_round: Option<Round>,
1064 first_round: bool,
1065 justification_commitment: Option<CryptoHash>,
1066 committee: &'a Committee,
1067 ) -> Self {
1068 Self {
1069 committee,
1070 weight: 0,
1071 used_validators: HashSet::new(),
1072 partial: GenericCertificate::new_with_payload(
1073 value,
1074 round,
1075 unlocking_round,
1076 first_round,
1077 justification_commitment,
1078 Vec::new(),
1079 ),
1080 }
1081 }
1082
1083 pub fn append(
1087 &mut self,
1088 public_key: ValidatorPublicKey,
1089 signature: ValidatorSignature,
1090 ) -> Result<Option<GenericCertificate<T>>, ChainError>
1091 where
1092 T: CertificateValue,
1093 {
1094 let hash_and_round = VoteValue(
1095 self.partial.hash(),
1096 self.partial.round,
1097 T::KIND,
1098 self.partial.unlocking_round(),
1099 self.partial.first_round(),
1100 self.partial.justification_commitment(),
1101 );
1102 signature.check(&hash_and_round, public_key)?;
1103 ensure!(
1105 !self.used_validators.contains(&public_key),
1106 ChainError::CertificateValidatorReuse
1107 );
1108 self.used_validators.insert(public_key);
1109 let voting_rights = self.committee.weight(&public_key);
1111 ensure!(voting_rights > 0, ChainError::InvalidSigner);
1112 self.weight += voting_rights;
1113 self.partial.add_signature((public_key, signature));
1115
1116 if self.weight >= self.committee.quorum_threshold() {
1117 self.weight = 0; Ok(Some(self.partial.clone()))
1119 } else {
1120 Ok(None)
1121 }
1122 }
1123}
1124
1125pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
1128 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
1129}
1130
1131pub(crate) fn check_signatures(
1134 value: &VoteValue,
1135 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
1136 committee: &Committee,
1137) -> Result<(), ChainError> {
1138 let mut weight = 0;
1140 let mut used_validators = HashSet::new();
1141 for (validator, _) in signatures {
1142 ensure!(
1144 !used_validators.contains(validator),
1145 ChainError::CertificateValidatorReuse
1146 );
1147 used_validators.insert(*validator);
1148 let voting_rights = committee.weight(validator);
1150 ensure!(voting_rights > 0, ChainError::InvalidSigner);
1151 weight += voting_rights;
1152 }
1153 ensure!(
1154 weight >= committee.quorum_threshold(),
1155 ChainError::CertificateRequiresQuorum
1156 );
1157 ValidatorSignature::verify_batch(value, signatures.iter())?;
1159 Ok(())
1160}
1161
1162impl BcsSignable<'_> for ProposalContent {}
1163
1164impl BcsSignable<'_> for VoteValue {}
1165
1166doc_scalar!(
1167 MessageAction,
1168 "Whether an incoming message is accepted or rejected."
1169);
1170
1171#[cfg(test)]
1172mod signing {
1173 use linera_base::{
1174 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
1175 data_types::{BlockHeight, Epoch, Round},
1176 identifiers::ChainId,
1177 };
1178
1179 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
1180
1181 #[test]
1182 fn proposal_content_signing() {
1183 use std::str::FromStr;
1184
1185 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
1187 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
1188 )
1189 .unwrap();
1190 let address = secret_key.address();
1191
1192 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
1193 let public_key = signer.public();
1194
1195 let proposed_block = ProposedBlock {
1196 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
1197 epoch: Epoch(11),
1198 transactions: vec![],
1199 height: BlockHeight(11),
1200 timestamp: 190000000u64.into(),
1201 authenticated_owner: None,
1202 previous_block_hash: None,
1203 };
1204
1205 let proposal = ProposalContent {
1206 block: proposed_block,
1207 round: Round::SingleLeader(11),
1208 outcome: None,
1209 };
1210
1211 let signature = EvmSignature::from_str(
1214 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
1215 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
1216 )
1217 .unwrap();
1218 let metamask_signature = AccountSignature::EvmSecp256k1 {
1219 signature,
1220 address: address.0 .0,
1221 };
1222
1223 let signature = signer.sign(&proposal);
1224 assert_eq!(signature, metamask_signature);
1225
1226 assert_eq!(signature.owner(), public_key.into());
1227
1228 let block_proposal = BlockProposal {
1229 content: proposal,
1230 signature,
1231 original_proposal: None,
1232 };
1233 assert_eq!(block_proposal.owner(), public_key.into(),);
1234 }
1235}