1use std::{
12 collections::{BTreeMap, BTreeSet, HashSet},
13 sync::Arc,
14};
15
16use allocative::Allocative;
17use async_graphql::SimpleObject;
18use custom_debug_derive::Debug;
19use linera_base::{
20 bcs,
21 crypto::{
22 AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash, Signer,
23 ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
24 },
25 data_types::{
26 Amount, Blob, BlockHeight, Cursor, Epoch, Event, MessagePolicy, OracleResponse, Round,
27 Timestamp,
28 },
29 doc_scalar, ensure, hex, hex_debug,
30 identifiers::{
31 Account, AccountOwner, ApplicationId, BlobId, ChainId, GenericApplicationId, StreamId,
32 },
33 time::Duration,
34};
35use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
36use serde::{Deserialize, Serialize};
37use tracing::{info, instrument};
38
39use crate::{
40 block::{Block, ValidatedBlock},
41 types::{
42 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
43 ValidatedBlockCertificate,
44 },
45 ChainError,
46};
47
48pub mod metadata;
49pub mod proof;
50
51pub use metadata::*;
52
53#[cfg(test)]
54#[path = "../unit_tests/data_types_tests.rs"]
55mod data_types_tests;
56
57#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
65#[graphql(complex)]
66pub struct ProposedBlock {
67 pub chain_id: ChainId,
69 pub epoch: Epoch,
71 #[debug(skip_if = Vec::is_empty)]
74 #[graphql(skip)]
75 pub transactions: Vec<Transaction>,
76 pub height: BlockHeight,
78 pub timestamp: Timestamp,
81 #[debug(skip_if = Option::is_none)]
86 pub authenticated_owner: Option<AccountOwner>,
87 pub previous_block_hash: Option<CryptoHash>,
90}
91
92impl ProposedBlock {
93 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
95 self.operations()
96 .flat_map(Operation::published_blob_ids)
97 .collect()
98 }
99
100 pub fn starts_with_checkpoint(&self) -> bool {
105 self.transactions
106 .first()
107 .is_some_and(Transaction::is_checkpoint)
108 }
109
110 pub fn has_only_rejected_messages(&self) -> bool {
113 self.transactions.iter().all(|txn| {
114 matches!(
115 txn,
116 Transaction::ReceiveMessages(IncomingBundle {
117 action: MessageAction::Reject,
118 ..
119 })
120 )
121 })
122 }
123
124 pub fn operations(&self) -> impl Iterator<Item = &Operation> {
126 self.transactions.iter().filter_map(|tx| match tx {
127 Transaction::ExecuteOperation(operation) => Some(operation),
128 Transaction::ReceiveMessages(_) => None,
129 })
130 }
131
132 pub fn incoming_bundles(&self) -> impl Iterator<Item = &IncomingBundle> {
134 self.transactions.iter().filter_map(|tx| match tx {
135 Transaction::ReceiveMessages(bundle) => Some(bundle),
136 Transaction::ExecuteOperation(_) => None,
137 })
138 }
139
140 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
142 let size = bcs::serialized_size(self)?;
143 ensure!(
144 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
145 ChainError::BlockProposalTooLarge(size)
146 );
147 Ok(())
148 }
149}
150
151#[async_graphql::ComplexObject]
152impl ProposedBlock {
153 async fn transaction_metadata(&self) -> Vec<TransactionMetadata> {
155 self.transactions
156 .iter()
157 .map(TransactionMetadata::from_transaction)
158 .collect()
159 }
160}
161
162#[derive(
164 Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Allocative, strum::AsRefStr,
165)]
166pub enum Transaction {
167 ReceiveMessages(IncomingBundle),
169 ExecuteOperation(Operation),
171}
172
173impl BcsHashable<'_> for Transaction {}
174
175impl Transaction {
176 pub fn incoming_bundle(&self) -> Option<&IncomingBundle> {
178 match self {
179 Transaction::ReceiveMessages(bundle) => Some(bundle),
180 _ => None,
181 }
182 }
183
184 pub fn is_update_stream(&self) -> bool {
186 matches!(
187 self,
188 Transaction::ExecuteOperation(op) if op.is_update_stream()
189 )
190 }
191
192 pub fn is_checkpoint(&self) -> bool {
194 matches!(
195 self,
196 Transaction::ExecuteOperation(op) if op.is_checkpoint()
197 )
198 }
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
203#[graphql(name = "Operation")]
204pub struct OperationMetadata {
205 pub operation_type: String,
207 pub application_id: Option<ApplicationId>,
209 pub user_bytes_hex: Option<String>,
211 pub system_operation: Option<SystemOperationMetadata>,
213}
214
215impl From<&Operation> for OperationMetadata {
216 fn from(operation: &Operation) -> Self {
217 match operation {
218 Operation::System(sys_op) => OperationMetadata {
219 operation_type: "System".to_string(),
220 application_id: None,
221 user_bytes_hex: None,
222 system_operation: Some(SystemOperationMetadata::from(sys_op.as_ref())),
223 },
224 Operation::User {
225 application_id,
226 bytes,
227 } => OperationMetadata {
228 operation_type: "User".to_string(),
229 application_id: Some(*application_id),
230 user_bytes_hex: Some(hex::encode(bytes)),
231 system_operation: None,
232 },
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, SimpleObject)]
239pub struct TransactionMetadata {
240 pub transaction_type: String,
242 pub incoming_bundle: Option<IncomingBundle>,
244 pub operation: Option<OperationMetadata>,
246}
247
248impl TransactionMetadata {
249 pub fn from_transaction(transaction: &Transaction) -> Self {
251 match transaction {
252 Transaction::ReceiveMessages(bundle) => TransactionMetadata {
253 transaction_type: "ReceiveMessages".to_string(),
254 incoming_bundle: Some(bundle.clone()),
255 operation: None,
256 },
257 Transaction::ExecuteOperation(op) => TransactionMetadata {
258 transaction_type: "ExecuteOperation".to_string(),
259 incoming_bundle: None,
260 operation: Some(OperationMetadata::from(op)),
261 },
262 }
263 }
264}
265
266#[derive(
268 Debug,
269 Clone,
270 Copy,
271 Eq,
272 PartialEq,
273 Ord,
274 PartialOrd,
275 Serialize,
276 Deserialize,
277 SimpleObject,
278 Allocative,
279)]
280pub struct ChainAndHeight {
281 pub chain_id: ChainId,
283 pub height: BlockHeight,
285}
286
287#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
289pub struct IncomingBundle {
290 pub origin: ChainId,
292 pub bundle: MessageBundle,
294 pub action: MessageAction,
296}
297
298impl IncomingBundle {
299 pub fn messages(&self) -> impl Iterator<Item = &PostedMessage> {
301 self.bundle.messages.iter()
302 }
303
304 fn matches_policy(&self, policy: &MessagePolicy) -> bool {
305 if let Some(chain_ids) = &policy.restrict_chain_ids_to {
306 if !chain_ids.contains(&self.origin) {
307 return false;
308 }
309 }
310 if policy.ignore_chain_ids.contains(&self.origin) {
311 return false;
312 }
313 if !policy.never_reject_application_ids.is_empty()
314 && self.messages().all(|posted_msg| {
315 policy
316 .never_reject_application_ids
317 .contains(&posted_msg.message.application_id())
318 })
319 {
320 return true;
321 }
322 if let Some(app_ids) = &policy.reject_message_bundles_without_application_ids {
323 if !self
324 .messages()
325 .any(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
326 {
327 return false;
328 }
329 }
330 if let Some(app_ids) = &policy.reject_message_bundles_with_other_application_ids {
331 if !self
332 .messages()
333 .all(|posted_msg| app_ids.contains(&posted_msg.message.application_id()))
334 {
335 return false;
336 }
337 }
338 !policy.is_reject()
339 }
340
341 #[instrument(level = "trace", skip(self))]
344 pub fn apply_policy(mut self, policy: &MessagePolicy) -> Option<IncomingBundle> {
345 if !self.matches_policy(policy) {
346 if self.bundle.is_skippable() {
347 return None;
348 } else if !self.bundle.is_protected() {
349 info!(
350 origin = %self.origin,
351 "Rejecting incoming message bundle due to the message policy"
352 );
353 self.action = MessageAction::Reject;
354 }
355 }
356 Some(self)
357 }
358}
359
360impl BcsHashable<'_> for IncomingBundle {}
361
362#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
364pub enum MessageAction {
365 Accept,
367 Reject,
369}
370
371#[derive(Clone, Debug, Default, PartialEq, Eq)]
373pub enum BundleFailurePolicy {
374 #[default]
376 Abort,
377 AutoRetry {
391 max_failures: u32,
393 never_reject_application_ids: Arc<HashSet<GenericApplicationId>>,
397 },
398}
399
400#[derive(Clone, Debug, PartialEq, Eq)]
402pub struct BundleExecutionPolicy {
403 pub on_failure: BundleFailurePolicy,
405 pub time_budget: Option<Duration>,
407}
408
409impl BundleExecutionPolicy {
410 pub fn committed() -> Self {
412 BundleExecutionPolicy {
413 on_failure: BundleFailurePolicy::Abort,
414 time_budget: None,
415 }
416 }
417}
418
419#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject, Allocative)]
421pub struct MessageBundle {
422 pub height: BlockHeight,
424 pub timestamp: Timestamp,
426 pub certificate_hash: CryptoHash,
428 pub transaction_index: u32,
430 pub messages: Vec<PostedMessage>,
432}
433
434#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
435#[cfg_attr(with_testing, derive(Eq, PartialEq))]
436pub enum OriginalProposal {
438 Fast(AccountSignature),
440 Regular {
442 certificate: LiteCertificate<'static>,
444 },
445}
446
447#[derive(Clone, Debug, Serialize, Deserialize, Allocative)]
451#[cfg_attr(with_testing, derive(Eq, PartialEq))]
452pub struct BlockProposal {
453 pub content: ProposalContent,
456 pub signature: AccountSignature,
458 #[debug(skip_if = Option::is_none)]
460 pub original_proposal: Option<OriginalProposal>,
461}
462
463#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
465#[graphql(complex)]
466pub struct PostedMessage {
467 #[debug(skip_if = Option::is_none)]
469 pub authenticated_owner: Option<AccountOwner>,
470 #[debug(skip_if = Amount::is_zero)]
472 pub grant: Amount,
473 #[debug(skip_if = Option::is_none)]
475 pub refund_grant_to: Option<Account>,
476 pub kind: MessageKind,
478 pub message: Message,
480}
481
482pub trait OutgoingMessageExt {
484 fn into_posted(self) -> PostedMessage;
486}
487
488impl OutgoingMessageExt for OutgoingMessage {
489 fn into_posted(self) -> PostedMessage {
491 let OutgoingMessage {
492 destination: _,
493 authenticated_owner,
494 grant,
495 refund_grant_to,
496 kind,
497 message,
498 } = self;
499 PostedMessage {
500 authenticated_owner,
501 grant,
502 refund_grant_to,
503 kind,
504 message,
505 }
506 }
507}
508
509#[async_graphql::ComplexObject]
510impl PostedMessage {
511 async fn message_metadata(&self) -> MessageMetadata {
513 MessageMetadata::from(&self.message)
514 }
515}
516
517#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
519pub struct OperationResult(
520 #[debug(with = "hex_debug")]
521 #[serde(with = "serde_bytes")]
522 pub Vec<u8>,
523);
524
525impl BcsHashable<'_> for OperationResult {}
526
527doc_scalar!(
528 OperationResult,
529 "The execution result of a single operation."
530);
531
532#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject, Allocative)]
534#[cfg_attr(with_testing, derive(Default))]
535pub struct BlockExecutionOutcome {
536 pub messages: Vec<Vec<OutgoingMessage>>,
538 pub previous_message_blocks: BTreeMap<ChainId, (CryptoHash, BlockHeight)>,
540 pub previous_event_blocks: BTreeMap<StreamId, (CryptoHash, BlockHeight)>,
542 pub state_hash: CryptoHash,
544 pub oracle_responses: Vec<Vec<OracleResponse>>,
546 pub events: Vec<Vec<Event>>,
548 pub blobs: Vec<Vec<Blob>>,
550 pub operation_results: Vec<OperationResult>,
552}
553
554#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, Allocative)]
556pub struct LiteValue {
557 pub value_hash: CryptoHash,
559 pub chain_id: ChainId,
561 pub kind: CertificateKind,
563}
564
565impl LiteValue {
566 pub fn new<T: CertificateValue>(value: &T) -> Self {
568 LiteValue {
569 value_hash: value.hash(),
570 chain_id: value.chain_id(),
571 kind: T::KIND,
572 }
573 }
574}
575
576#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
601pub struct VoteValue(
602 pub(crate) CryptoHash,
603 pub(crate) Round,
604 pub(crate) CertificateKind,
605 pub(crate) Option<Round>,
606 pub(crate) bool,
607 pub(crate) Option<CryptoHash>,
608);
609
610#[derive(Allocative, Clone, Debug, Serialize, Deserialize)]
612#[serde(bound(deserialize = "T: Deserialize<'de>"))]
613pub struct Vote<T> {
614 pub value: T,
616 pub round: Round,
618 pub unlocking_round: Option<Round>,
621 pub first_round: bool,
625 pub justification_commitment: Option<CryptoHash>,
628 pub signature: ValidatorSignature,
631}
632
633impl<T> Vote<T> {
634 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
636 where
637 T: CertificateValue,
638 {
639 Self::new_with_unlocking_round(value, round, None, None, key_pair)
640 }
641
642 pub fn new_with_unlocking_round(
645 value: T,
646 round: Round,
647 unlocking_round: Option<Round>,
648 justification_commitment: Option<CryptoHash>,
649 key_pair: &ValidatorSecretKey,
650 ) -> Self
651 where
652 T: CertificateValue,
653 {
654 let hash_and_round = VoteValue(
655 value.hash(),
656 round,
657 T::KIND,
658 unlocking_round,
659 false,
660 justification_commitment,
661 );
662 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
663 Self {
664 value,
665 round,
666 unlocking_round,
667 first_round: false,
668 justification_commitment,
669 signature,
670 }
671 }
672
673 pub fn new_with_first_round(
677 value: T,
678 round: Round,
679 first_round: bool,
680 justification_commitment: Option<CryptoHash>,
681 key_pair: &ValidatorSecretKey,
682 ) -> Self
683 where
684 T: CertificateValue,
685 {
686 let hash_and_round = VoteValue(
687 value.hash(),
688 round,
689 T::KIND,
690 None,
691 first_round,
692 justification_commitment,
693 );
694 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
695 Self {
696 value,
697 round,
698 unlocking_round: None,
699 first_round,
700 justification_commitment,
701 signature,
702 }
703 }
704
705 pub fn lite(&self) -> LiteVote
707 where
708 T: CertificateValue,
709 {
710 LiteVote {
711 value: LiteValue::new(&self.value),
712 round: self.round,
713 unlocking_round: self.unlocking_round,
714 first_round: self.first_round,
715 justification_commitment: self.justification_commitment,
716 signature: self.signature,
717 }
718 }
719
720 pub fn value(&self) -> &T {
722 &self.value
723 }
724}
725
726#[derive(Clone, Debug, Serialize, Deserialize)]
728#[cfg_attr(with_testing, derive(Eq, PartialEq))]
729pub struct LiteVote {
730 pub value: LiteValue,
732 pub round: Round,
734 pub unlocking_round: Option<Round>,
737 pub first_round: bool,
741 pub justification_commitment: Option<CryptoHash>,
744 pub signature: ValidatorSignature,
747}
748
749impl LiteVote {
750 #[cfg(with_testing)]
752 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
753 if self.value.value_hash != value.hash() {
754 return None;
755 }
756 Some(Vote {
757 value,
758 round: self.round,
759 unlocking_round: self.unlocking_round,
760 first_round: self.first_round,
761 justification_commitment: self.justification_commitment,
762 signature: self.signature,
763 })
764 }
765
766 pub fn kind(&self) -> CertificateKind {
768 self.value.kind
769 }
770}
771
772impl MessageBundle {
773 pub fn cursor(&self) -> Cursor {
776 Cursor {
777 height: self.height,
778 index: self.transaction_index,
779 }
780 }
781
782 pub fn estimated_size(&self) -> usize {
784 let overhead = 60;
786 let messages_size: usize = self
787 .messages
788 .iter()
789 .map(PostedMessage::estimated_size)
790 .sum();
791 overhead + messages_size
792 }
793
794 pub fn is_skippable(&self) -> bool {
796 self.messages.iter().all(PostedMessage::is_skippable)
797 }
798
799 pub fn is_protected(&self) -> bool {
801 self.messages.iter().any(PostedMessage::is_protected)
802 }
803}
804
805impl PostedMessage {
806 pub fn estimated_size(&self) -> usize {
808 let overhead = 92;
810 let message_size = match &self.message {
811 Message::System(_) => 256, Message::User { bytes, .. } => 64 + bytes.len(),
813 };
814 overhead + message_size
815 }
816
817 pub fn is_skippable(&self) -> bool {
819 match self.kind {
820 MessageKind::Protected | MessageKind::Tracked => false,
821 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
822 }
823 }
824
825 pub fn is_protected(&self) -> bool {
827 matches!(self.kind, MessageKind::Protected)
828 }
829
830 pub fn is_tracked(&self) -> bool {
832 matches!(self.kind, MessageKind::Tracked)
833 }
834
835 pub fn is_bouncing(&self) -> bool {
837 matches!(self.kind, MessageKind::Bouncing)
838 }
839}
840
841impl BlockExecutionOutcome {
842 pub fn with(self, block: ProposedBlock) -> Block {
844 Block::new(block, self)
845 }
846
847 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
849 let mut required_blob_ids = HashSet::new();
850 for responses in &self.oracle_responses {
851 for response in responses {
852 match response {
853 OracleResponse::Blob(blob_id) => {
854 required_blob_ids.insert(*blob_id);
855 }
856 OracleResponse::Checkpoint { used_blobs, .. } => {
857 required_blob_ids.extend(used_blobs.iter().copied());
858 }
859 _ => {}
860 }
861 }
862 }
863
864 required_blob_ids
865 }
866
867 pub fn has_oracle_responses(&self) -> bool {
869 self.oracle_responses
870 .iter()
871 .any(|responses| !responses.is_empty())
872 }
873
874 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
876 self.blobs.iter().flatten().map(|blob| blob.id())
877 }
878}
879
880#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Allocative)]
882pub struct ProposalContent {
883 pub block: ProposedBlock,
885 pub round: Round,
887 #[debug(skip_if = Option::is_none)]
889 pub outcome: Option<BlockExecutionOutcome>,
890}
891
892impl BlockProposal {
893 pub async fn new_initial<S: Signer + ?Sized>(
895 owner: AccountOwner,
896 round: Round,
897 block: ProposedBlock,
898 signer: &S,
899 ) -> Result<Self, S::Error> {
900 let content = ProposalContent {
901 round,
902 block,
903 outcome: None,
904 };
905 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
906
907 Ok(Self {
908 content,
909 signature,
910 original_proposal: None,
911 })
912 }
913
914 pub async fn new_retry_fast<S: Signer + ?Sized>(
916 owner: AccountOwner,
917 round: Round,
918 old_proposal: BlockProposal,
919 signer: &S,
920 ) -> Result<Self, S::Error> {
921 let content = ProposalContent {
922 round,
923 block: old_proposal.content.block,
924 outcome: None,
925 };
926 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
927
928 Ok(Self {
929 content,
930 signature,
931 original_proposal: Some(OriginalProposal::Fast(old_proposal.signature)),
932 })
933 }
934
935 pub async fn new_retry_regular<S: Signer>(
937 owner: AccountOwner,
938 round: Round,
939 validated_block_certificate: ValidatedBlockCertificate,
940 signer: &S,
941 ) -> Result<Self, S::Error> {
942 let certificate = validated_block_certificate.lite_certificate().cloned();
943 let block = validated_block_certificate.into_inner().into_inner();
944 let (block, outcome) = block.into_proposal();
945 let content = ProposalContent {
946 block,
947 round,
948 outcome: Some(outcome),
949 };
950 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
951
952 Ok(Self {
953 content,
954 signature,
955 original_proposal: Some(OriginalProposal::Regular { certificate }),
956 })
957 }
958
959 pub fn owner(&self) -> AccountOwner {
961 match self.signature {
962 AccountSignature::Ed25519 { public_key, .. } => public_key.into(),
963 AccountSignature::Secp256k1 { public_key, .. } => public_key.into(),
964 AccountSignature::EvmSecp256k1 { address, .. } => AccountOwner::Address20(address),
965 }
966 }
967
968 pub fn check_signature(&self) -> Result<(), CryptoError> {
970 self.signature.verify(&self.content)
971 }
972
973 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
975 self.content.block.published_blob_ids().into_iter().chain(
976 self.content
977 .outcome
978 .iter()
979 .flat_map(|outcome| outcome.oracle_blob_ids()),
980 )
981 }
982
983 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
985 self.content.block.published_blob_ids().into_iter().chain(
986 self.content.outcome.iter().flat_map(|outcome| {
987 outcome
988 .oracle_blob_ids()
989 .into_iter()
990 .chain(outcome.iter_created_blobs_ids())
991 }),
992 )
993 }
994
995 pub fn check_invariants(&self) -> Result<(), &'static str> {
997 match (&self.original_proposal, &self.content.outcome) {
998 (None, None) => {}
999 (Some(OriginalProposal::Fast(_)), None) => ensure!(
1000 self.content.round > Round::Fast,
1001 "The new proposal's round must be greater than the original's"
1002 ),
1003 (None, Some(_))
1004 | (Some(OriginalProposal::Fast(_)), Some(_))
1005 | (Some(OriginalProposal::Regular { .. }), None) => {
1006 return Err("Must contain a validation certificate if and only if \
1007 it contains the execution outcome from a previous round");
1008 }
1009 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
1010 ensure!(
1011 self.content.round > certificate.round,
1012 "The new proposal's round must be greater than the original's"
1013 );
1014 let block = outcome.clone().with(self.content.block.clone());
1015 let value = ValidatedBlock::new(block);
1016 ensure!(
1017 certificate.check_value(&value),
1018 "Lite certificate must match the given block and execution outcome"
1019 );
1020 }
1021 }
1022 Ok(())
1023 }
1024}
1025
1026impl LiteVote {
1027 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
1029 let hash_and_round = VoteValue(value.value_hash, round, value.kind, None, false, None);
1030 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
1031 Self {
1032 value,
1033 round,
1034 unlocking_round: None,
1035 first_round: false,
1036 justification_commitment: None,
1037 signature,
1038 }
1039 }
1040
1041 pub fn check(&self, public_key: ValidatorPublicKey) -> Result<(), ChainError> {
1043 let hash_and_round = VoteValue(
1044 self.value.value_hash,
1045 self.round,
1046 self.value.kind,
1047 self.unlocking_round,
1048 self.first_round,
1049 self.justification_commitment,
1050 );
1051 Ok(self.signature.check(&hash_and_round, public_key)?)
1052 }
1053}
1054
1055pub struct SignatureAggregator<'a, T: CertificateValue> {
1057 committee: &'a Committee,
1058 weight: u64,
1059 used_validators: HashSet<ValidatorPublicKey>,
1060 partial: GenericCertificate<T>,
1061}
1062
1063impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
1064 pub fn new(
1068 value: T,
1069 round: Round,
1070 unlocking_round: Option<Round>,
1071 first_round: bool,
1072 justification_commitment: Option<CryptoHash>,
1073 committee: &'a Committee,
1074 ) -> Self {
1075 Self {
1076 committee,
1077 weight: 0,
1078 used_validators: HashSet::new(),
1079 partial: GenericCertificate::new_with_payload(
1080 value,
1081 round,
1082 unlocking_round,
1083 first_round,
1084 justification_commitment,
1085 Vec::new(),
1086 ),
1087 }
1088 }
1089
1090 pub fn append(
1094 &mut self,
1095 public_key: ValidatorPublicKey,
1096 signature: ValidatorSignature,
1097 ) -> Result<Option<GenericCertificate<T>>, ChainError>
1098 where
1099 T: CertificateValue,
1100 {
1101 let hash_and_round = VoteValue(
1102 self.partial.hash(),
1103 self.partial.round,
1104 T::KIND,
1105 self.partial.unlocking_round(),
1106 self.partial.first_round(),
1107 self.partial.justification_commitment(),
1108 );
1109 signature.check(&hash_and_round, public_key)?;
1110 ensure!(
1112 !self.used_validators.contains(&public_key),
1113 ChainError::CertificateValidatorReuse
1114 );
1115 self.used_validators.insert(public_key);
1116 let voting_rights = self.committee.weight(&public_key);
1118 ensure!(voting_rights > 0, ChainError::InvalidSigner);
1119 self.weight += voting_rights;
1120 self.partial.add_signature((public_key, signature));
1122
1123 if self.weight >= self.committee.quorum_threshold() {
1124 self.weight = 0; Ok(Some(self.partial.clone()))
1126 } else {
1127 Ok(None)
1128 }
1129 }
1130}
1131
1132pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
1135 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
1136}
1137
1138pub(crate) fn check_signatures(
1141 value: &VoteValue,
1142 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
1143 committee: &Committee,
1144) -> Result<(), ChainError> {
1145 let mut weight = 0;
1147 let mut used_validators = HashSet::new();
1148 for (validator, _) in signatures {
1149 ensure!(
1151 !used_validators.contains(validator),
1152 ChainError::CertificateValidatorReuse
1153 );
1154 used_validators.insert(*validator);
1155 let voting_rights = committee.weight(validator);
1157 ensure!(voting_rights > 0, ChainError::InvalidSigner);
1158 weight += voting_rights;
1159 }
1160 ensure!(
1161 weight >= committee.quorum_threshold(),
1162 ChainError::CertificateRequiresQuorum
1163 );
1164 ValidatorSignature::verify_batch(value, signatures.iter())?;
1166 Ok(())
1167}
1168
1169impl BcsSignable<'_> for ProposalContent {}
1170
1171impl BcsSignable<'_> for VoteValue {}
1172
1173doc_scalar!(
1174 MessageAction,
1175 "Whether an incoming message is accepted or rejected."
1176);
1177
1178#[cfg(test)]
1179mod signing {
1180 use linera_base::{
1181 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
1182 data_types::{BlockHeight, Epoch, Round},
1183 identifiers::ChainId,
1184 };
1185
1186 use crate::data_types::{BlockProposal, ProposalContent, ProposedBlock};
1187
1188 #[test]
1189 fn proposal_content_signing() {
1190 use std::str::FromStr;
1191
1192 let secret_key = linera_base::crypto::EvmSecretKey::from_str(
1194 "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66",
1195 )
1196 .unwrap();
1197 let address = secret_key.address();
1198
1199 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(secret_key);
1200 let public_key = signer.public();
1201
1202 let proposed_block = ProposedBlock {
1203 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
1204 epoch: Epoch(11),
1205 transactions: vec![],
1206 height: BlockHeight(11),
1207 timestamp: 190000000u64.into(),
1208 authenticated_owner: None,
1209 previous_block_hash: None,
1210 };
1211
1212 let proposal = ProposalContent {
1213 block: proposed_block,
1214 round: Round::SingleLeader(11),
1215 outcome: None,
1216 };
1217
1218 let signature = EvmSignature::from_str(
1221 "d69d31203f59be441fd02cdf68b2504cbcdd7215905c9b7dc3a7ccbf09afe14550\
1222 3c93b391810ce9edd6ee36b1e817b2d0e9dabdf4a098da8c2f670ef4198e8a1b",
1223 )
1224 .unwrap();
1225 let metamask_signature = AccountSignature::EvmSecp256k1 {
1226 signature,
1227 address: address.0 .0,
1228 };
1229
1230 let signature = signer.sign(&proposal);
1231 assert_eq!(signature, metamask_signature);
1232
1233 assert_eq!(signature.owner(), public_key.into());
1234
1235 let block_proposal = BlockProposal {
1236 content: proposal,
1237 signature,
1238 original_proposal: None,
1239 };
1240 assert_eq!(block_proposal.owner(), public_key.into(),);
1241 }
1242}