1use std::collections::{BTreeMap, BTreeSet, HashSet};
6
7use async_graphql::SimpleObject;
8use custom_debug_derive::Debug;
9use linera_base::{
10 bcs,
11 crypto::{
12 AccountPublicKey, AccountSignature, BcsHashable, BcsSignable, CryptoError, CryptoHash,
13 Signer, ValidatorPublicKey, ValidatorSecretKey, ValidatorSignature,
14 },
15 data_types::{Amount, Blob, BlockHeight, Epoch, Event, OracleResponse, Round, Timestamp},
16 doc_scalar, ensure, hex_debug,
17 identifiers::{Account, AccountOwner, BlobId, ChainId, MessageId},
18};
19use linera_execution::{committee::Committee, Message, MessageKind, Operation, OutgoingMessage};
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 block::{Block, ValidatedBlock},
24 types::{
25 CertificateKind, CertificateValue, GenericCertificate, LiteCertificate,
26 ValidatedBlockCertificate,
27 },
28 ChainError,
29};
30
31#[cfg(test)]
32#[path = "unit_tests/data_types_tests.rs"]
33mod data_types_tests;
34
35#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
43pub struct ProposedBlock {
44 pub chain_id: ChainId,
46 pub epoch: Epoch,
48 #[debug(skip_if = Vec::is_empty)]
51 pub incoming_bundles: Vec<IncomingBundle>,
52 #[debug(skip_if = Vec::is_empty)]
54 pub operations: Vec<Operation>,
55 pub height: BlockHeight,
57 pub timestamp: Timestamp,
60 #[debug(skip_if = Option::is_none)]
65 pub authenticated_signer: Option<AccountOwner>,
66 pub previous_block_hash: Option<CryptoHash>,
69}
70
71impl ProposedBlock {
72 pub fn published_blob_ids(&self) -> BTreeSet<BlobId> {
74 self.operations
75 .iter()
76 .flat_map(Operation::published_blob_ids)
77 .collect()
78 }
79
80 pub fn has_only_rejected_messages(&self) -> bool {
83 self.operations.is_empty()
84 && self
85 .incoming_bundles
86 .iter()
87 .all(|message| message.action == MessageAction::Reject)
88 }
89
90 pub fn incoming_messages(&self) -> impl Iterator<Item = &PostedMessage> {
92 self.incoming_bundles
93 .iter()
94 .flat_map(|incoming_bundle| &incoming_bundle.bundle.messages)
95 }
96
97 pub fn message_count(&self) -> usize {
99 self.incoming_bundles
100 .iter()
101 .map(|im| im.bundle.messages.len())
102 .sum()
103 }
104
105 pub fn transactions(&self) -> impl Iterator<Item = Transaction<'_>> {
109 let bundles = self
110 .incoming_bundles
111 .iter()
112 .map(Transaction::ReceiveMessages);
113 let operations = self.operations.iter().map(Transaction::ExecuteOperation);
114 bundles.chain(operations)
115 }
116
117 pub fn check_proposal_size(&self, maximum_block_proposal_size: u64) -> Result<(), ChainError> {
118 let size = bcs::serialized_size(self)?;
119 ensure!(
120 size <= usize::try_from(maximum_block_proposal_size).unwrap_or(usize::MAX),
121 ChainError::BlockProposalTooLarge(size)
122 );
123 Ok(())
124 }
125}
126
127#[derive(Debug, Clone)]
129pub enum Transaction<'a> {
130 ReceiveMessages(&'a IncomingBundle),
132 ExecuteOperation(&'a Operation),
134}
135
136#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, SimpleObject)]
138pub struct ChainAndHeight {
139 pub chain_id: ChainId,
140 pub height: BlockHeight,
141}
142
143impl ChainAndHeight {
144 pub fn to_message_id(&self, index: u32) -> MessageId {
146 MessageId {
147 chain_id: self.chain_id,
148 height: self.height,
149 index,
150 }
151 }
152}
153
154#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
156pub struct IncomingBundle {
157 pub origin: ChainId,
159 pub bundle: MessageBundle,
161 pub action: MessageAction,
163}
164
165impl IncomingBundle {
166 pub fn messages_and_ids(&self) -> impl Iterator<Item = (MessageId, &PostedMessage)> {
168 let chain_and_height = ChainAndHeight {
169 chain_id: self.origin,
170 height: self.bundle.height,
171 };
172 let messages = self.bundle.messages.iter();
173 messages.map(move |posted_message| {
174 let message_id = chain_and_height.to_message_id(posted_message.index);
175 (message_id, posted_message)
176 })
177 }
178}
179
180impl BcsHashable<'_> for IncomingBundle {}
181
182#[derive(Copy, Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
184pub enum MessageAction {
185 Accept,
187 Reject,
189}
190
191#[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, SimpleObject)]
193pub struct MessageBundle {
194 pub height: BlockHeight,
196 pub timestamp: Timestamp,
198 pub certificate_hash: CryptoHash,
200 pub transaction_index: u32,
202 pub messages: Vec<PostedMessage>,
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize)]
207#[cfg_attr(with_testing, derive(Eq, PartialEq))]
208pub enum OriginalProposal {
210 Fast {
212 public_key: AccountPublicKey,
213 signature: AccountSignature,
214 },
215 Regular {
217 certificate: LiteCertificate<'static>,
218 },
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize)]
225#[cfg_attr(with_testing, derive(Eq, PartialEq))]
226pub struct BlockProposal {
227 pub content: ProposalContent,
228 pub public_key: AccountPublicKey,
229 pub signature: AccountSignature,
230 #[debug(skip_if = Option::is_none)]
231 pub original_proposal: Option<OriginalProposal>,
232}
233
234#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
236pub struct PostedMessage {
237 #[debug(skip_if = Option::is_none)]
239 pub authenticated_signer: Option<AccountOwner>,
240 #[debug(skip_if = Amount::is_zero)]
242 pub grant: Amount,
243 #[debug(skip_if = Option::is_none)]
245 pub refund_grant_to: Option<Account>,
246 pub kind: MessageKind,
248 pub index: u32,
250 pub message: Message,
252}
253
254pub trait OutgoingMessageExt {
255 fn into_posted(self, index: u32) -> PostedMessage;
257}
258
259impl OutgoingMessageExt for OutgoingMessage {
260 fn into_posted(self, index: u32) -> PostedMessage {
262 let OutgoingMessage {
263 destination: _,
264 authenticated_signer,
265 grant,
266 refund_grant_to,
267 kind,
268 message,
269 } = self;
270 PostedMessage {
271 authenticated_signer,
272 grant,
273 refund_grant_to,
274 kind,
275 index,
276 message,
277 }
278 }
279}
280
281#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
283pub struct OperationResult(
284 #[debug(with = "hex_debug")]
285 #[serde(with = "serde_bytes")]
286 pub Vec<u8>,
287);
288
289impl BcsHashable<'_> for OperationResult {}
290
291doc_scalar!(
292 OperationResult,
293 "The execution result of a single operation."
294);
295
296#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, SimpleObject)]
298#[cfg_attr(with_testing, derive(Default))]
299pub struct BlockExecutionOutcome {
300 pub messages: Vec<Vec<OutgoingMessage>>,
302 pub previous_message_blocks: BTreeMap<ChainId, CryptoHash>,
304 pub state_hash: CryptoHash,
306 pub oracle_responses: Vec<Vec<OracleResponse>>,
308 pub events: Vec<Vec<Event>>,
310 pub blobs: Vec<Vec<Blob>>,
312 pub operation_results: Vec<OperationResult>,
314}
315
316#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
318pub struct LiteValue {
319 pub value_hash: CryptoHash,
320 pub chain_id: ChainId,
321 pub kind: CertificateKind,
322}
323
324impl LiteValue {
325 pub fn new<T: CertificateValue>(value: &T) -> Self {
326 LiteValue {
327 value_hash: value.hash(),
328 chain_id: value.chain_id(),
329 kind: T::KIND,
330 }
331 }
332}
333
334#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
335struct VoteValue(CryptoHash, Round, CertificateKind);
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
339#[serde(bound(deserialize = "T: Deserialize<'de>"))]
340pub struct Vote<T> {
341 pub value: T,
342 pub round: Round,
343 pub public_key: ValidatorPublicKey,
344 pub signature: ValidatorSignature,
345}
346
347impl<T> Vote<T> {
348 pub fn new(value: T, round: Round, key_pair: &ValidatorSecretKey) -> Self
350 where
351 T: CertificateValue,
352 {
353 let hash_and_round = VoteValue(value.hash(), round, T::KIND);
354 let signature = ValidatorSignature::new(&hash_and_round, key_pair);
355 Self {
356 value,
357 round,
358 public_key: key_pair.public(),
359 signature,
360 }
361 }
362
363 pub fn lite(&self) -> LiteVote
365 where
366 T: CertificateValue,
367 {
368 LiteVote {
369 value: LiteValue::new(&self.value),
370 round: self.round,
371 public_key: self.public_key,
372 signature: self.signature,
373 }
374 }
375
376 pub fn value(&self) -> &T {
378 &self.value
379 }
380}
381
382#[derive(Clone, Debug, Serialize, Deserialize)]
384#[cfg_attr(with_testing, derive(Eq, PartialEq))]
385pub struct LiteVote {
386 pub value: LiteValue,
387 pub round: Round,
388 pub public_key: ValidatorPublicKey,
389 pub signature: ValidatorSignature,
390}
391
392impl LiteVote {
393 #[cfg(any(feature = "benchmark", with_testing))]
395 pub fn with_value<T: CertificateValue>(self, value: T) -> Option<Vote<T>> {
396 if self.value.value_hash != value.hash() {
397 return None;
398 }
399 Some(Vote {
400 value,
401 round: self.round,
402 public_key: self.public_key,
403 signature: self.signature,
404 })
405 }
406
407 pub fn kind(&self) -> CertificateKind {
408 self.value.kind
409 }
410}
411
412impl MessageBundle {
413 pub fn is_skippable(&self) -> bool {
414 self.messages.iter().all(PostedMessage::is_skippable)
415 }
416
417 pub fn is_tracked(&self) -> bool {
418 let mut tracked = false;
419 for posted_message in &self.messages {
420 match posted_message.kind {
421 MessageKind::Simple | MessageKind::Bouncing => {}
422 MessageKind::Protected => return false,
423 MessageKind::Tracked => tracked = true,
424 }
425 }
426 tracked
427 }
428
429 pub fn is_protected(&self) -> bool {
430 self.messages.iter().any(PostedMessage::is_protected)
431 }
432}
433
434impl PostedMessage {
435 pub fn is_skippable(&self) -> bool {
436 match self.kind {
437 MessageKind::Protected | MessageKind::Tracked => false,
438 MessageKind::Simple | MessageKind::Bouncing => self.grant == Amount::ZERO,
439 }
440 }
441
442 pub fn is_protected(&self) -> bool {
443 matches!(self.kind, MessageKind::Protected)
444 }
445
446 pub fn is_tracked(&self) -> bool {
447 matches!(self.kind, MessageKind::Tracked)
448 }
449
450 pub fn is_bouncing(&self) -> bool {
451 matches!(self.kind, MessageKind::Bouncing)
452 }
453}
454
455impl BlockExecutionOutcome {
456 pub fn with(self, block: ProposedBlock) -> Block {
457 Block::new(block, self)
458 }
459
460 pub fn oracle_blob_ids(&self) -> HashSet<BlobId> {
461 let mut required_blob_ids = HashSet::new();
462 for responses in &self.oracle_responses {
463 for response in responses {
464 if let OracleResponse::Blob(blob_id) = response {
465 required_blob_ids.insert(*blob_id);
466 }
467 }
468 }
469
470 required_blob_ids
471 }
472
473 pub fn has_oracle_responses(&self) -> bool {
474 self.oracle_responses
475 .iter()
476 .any(|responses| !responses.is_empty())
477 }
478
479 pub fn iter_created_blobs_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
480 self.blobs.iter().flatten().map(|blob| blob.id())
481 }
482
483 pub fn created_blobs_ids(&self) -> HashSet<BlobId> {
484 self.iter_created_blobs_ids().collect()
485 }
486}
487
488#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
490pub struct ProposalContent {
491 pub block: ProposedBlock,
493 pub round: Round,
495 #[debug(skip_if = Option::is_none)]
497 pub outcome: Option<BlockExecutionOutcome>,
498}
499
500impl BlockProposal {
501 pub async fn new_initial<S: Signer + ?Sized>(
502 owner: AccountOwner,
503 round: Round,
504 block: ProposedBlock,
505 signer: &S,
506 ) -> Result<Self, S::Error> {
507 let content = ProposalContent {
508 round,
509 block,
510 outcome: None,
511 };
512 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
513 let public_key = signer.get_public_key(&owner).await?;
514
515 Ok(Self {
516 content,
517 public_key,
518 signature,
519 original_proposal: None,
520 })
521 }
522
523 pub async fn new_retry_fast<S: Signer + ?Sized>(
524 owner: AccountOwner,
525 round: Round,
526 old_proposal: BlockProposal,
527 signer: &S,
528 ) -> Result<Self, S::Error> {
529 let content = ProposalContent {
530 round,
531 block: old_proposal.content.block,
532 outcome: None,
533 };
534 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
535 let public_key = signer.get_public_key(&owner).await?;
536
537 Ok(Self {
538 content,
539 public_key,
540 signature,
541 original_proposal: Some(OriginalProposal::Fast {
542 public_key: old_proposal.public_key,
543 signature: old_proposal.signature,
544 }),
545 })
546 }
547
548 pub async fn new_retry_regular<S: Signer>(
549 owner: AccountOwner,
550 round: Round,
551 validated_block_certificate: ValidatedBlockCertificate,
552 signer: &S,
553 ) -> Result<Self, S::Error> {
554 let certificate = validated_block_certificate.lite_certificate().cloned();
555 let block = validated_block_certificate.into_inner().into_inner();
556 let (block, outcome) = block.into_proposal();
557 let content = ProposalContent {
558 block,
559 round,
560 outcome: Some(outcome),
561 };
562 let signature = signer.sign(&owner, &CryptoHash::new(&content)).await?;
563
564 let public_key = signer.get_public_key(&owner).await?;
565 Ok(Self {
566 content,
567 public_key,
568 signature,
569 original_proposal: Some(OriginalProposal::Regular { certificate }),
570 })
571 }
572
573 pub fn check_signature(&self) -> Result<(), CryptoError> {
574 self.signature.verify(&self.content, self.public_key)
575 }
576
577 pub fn required_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
578 self.content.block.published_blob_ids().into_iter().chain(
579 self.content
580 .outcome
581 .iter()
582 .flat_map(|outcome| outcome.oracle_blob_ids()),
583 )
584 }
585
586 pub fn expected_blob_ids(&self) -> impl Iterator<Item = BlobId> + '_ {
587 self.content.block.published_blob_ids().into_iter().chain(
588 self.content.outcome.iter().flat_map(|outcome| {
589 outcome
590 .oracle_blob_ids()
591 .into_iter()
592 .chain(outcome.iter_created_blobs_ids())
593 }),
594 )
595 }
596
597 pub fn check_invariants(&self) -> Result<(), &'static str> {
600 match (&self.original_proposal, &self.content.outcome) {
601 (None, None) | (Some(OriginalProposal::Fast { .. }), None) => {}
602 (None, Some(_))
603 | (Some(OriginalProposal::Fast { .. }), Some(_))
604 | (Some(OriginalProposal::Regular { .. }), None) => {
605 return Err("Must contain a validation certificate if and only if \
606 it contains the execution outcome from a previous round");
607 }
608 (Some(OriginalProposal::Regular { certificate }), Some(outcome)) => {
609 let block = outcome.clone().with(self.content.block.clone());
610 let value = ValidatedBlock::new(block);
611 ensure!(
612 certificate.check_value(&value),
613 "Lite certificate must match the given block and execution outcome"
614 );
615 }
616 }
617 Ok(())
618 }
619}
620
621impl LiteVote {
622 pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
624 let hash_and_round = VoteValue(value.value_hash, round, value.kind);
625 let signature = ValidatorSignature::new(&hash_and_round, secret_key);
626 Self {
627 value,
628 round,
629 public_key: secret_key.public(),
630 signature,
631 }
632 }
633
634 pub fn check(&self) -> Result<(), ChainError> {
636 let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
637 Ok(self.signature.check(&hash_and_round, &self.public_key)?)
638 }
639}
640
641pub struct SignatureAggregator<'a, T: CertificateValue> {
642 committee: &'a Committee,
643 weight: u64,
644 used_validators: HashSet<ValidatorPublicKey>,
645 partial: GenericCertificate<T>,
646}
647
648impl<'a, T: CertificateValue> SignatureAggregator<'a, T> {
649 pub fn new(value: T, round: Round, committee: &'a Committee) -> Self {
651 Self {
652 committee,
653 weight: 0,
654 used_validators: HashSet::new(),
655 partial: GenericCertificate::new(value, round, Vec::new()),
656 }
657 }
658
659 pub fn append(
663 &mut self,
664 public_key: ValidatorPublicKey,
665 signature: ValidatorSignature,
666 ) -> Result<Option<GenericCertificate<T>>, ChainError>
667 where
668 T: CertificateValue,
669 {
670 let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
671 signature.check(&hash_and_round, &public_key)?;
672 ensure!(
674 !self.used_validators.contains(&public_key),
675 ChainError::CertificateValidatorReuse
676 );
677 self.used_validators.insert(public_key);
678 let voting_rights = self.committee.weight(&public_key);
680 ensure!(voting_rights > 0, ChainError::InvalidSigner);
681 self.weight += voting_rights;
682 self.partial.add_signature((public_key, signature));
684
685 if self.weight >= self.committee.quorum_threshold() {
686 self.weight = 0; Ok(Some(self.partial.clone()))
688 } else {
689 Ok(None)
690 }
691 }
692}
693
694pub(crate) fn is_strictly_ordered(values: &[(ValidatorPublicKey, ValidatorSignature)]) -> bool {
697 values.windows(2).all(|pair| pair[0].0 < pair[1].0)
698}
699
700pub(crate) fn check_signatures(
702 value_hash: CryptoHash,
703 certificate_kind: CertificateKind,
704 round: Round,
705 signatures: &[(ValidatorPublicKey, ValidatorSignature)],
706 committee: &Committee,
707) -> Result<(), ChainError> {
708 let mut weight = 0;
710 let mut used_validators = HashSet::new();
711 for (validator, _) in signatures {
712 ensure!(
714 !used_validators.contains(validator),
715 ChainError::CertificateValidatorReuse
716 );
717 used_validators.insert(*validator);
718 let voting_rights = committee.weight(validator);
720 ensure!(voting_rights > 0, ChainError::InvalidSigner);
721 weight += voting_rights;
722 }
723 ensure!(
724 weight >= committee.quorum_threshold(),
725 ChainError::CertificateRequiresQuorum
726 );
727 let hash_and_round = VoteValue(value_hash, round, certificate_kind);
729 ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
730 Ok(())
731}
732
733impl BcsSignable<'_> for ProposalContent {}
734
735impl BcsSignable<'_> for VoteValue {}
736
737doc_scalar!(
738 MessageAction,
739 "Whether an incoming message is accepted or rejected."
740);
741
742#[cfg(test)]
743mod signing {
744 use linera_base::{
745 crypto::{AccountSecretKey, AccountSignature, CryptoHash, EvmSignature, TestString},
746 data_types::{BlockHeight, Epoch, Round},
747 identifiers::ChainId,
748 };
749
750 use crate::data_types::{ProposalContent, ProposedBlock};
751
752 #[test]
753 fn proposal_content_signing() {
754 use std::str::FromStr;
755
756 let secret_key = "f77a21701522a03b01c111ad2d2cdaf2b8403b47507ee0aec3c2e52b765d7a66";
758
759 let signer: AccountSecretKey = AccountSecretKey::EvmSecp256k1(
760 linera_base::crypto::EvmSecretKey::from_str(secret_key).unwrap(),
761 );
762
763 let proposed_block = ProposedBlock {
764 chain_id: ChainId(CryptoHash::new(&TestString::new("ChainId"))),
765 epoch: Epoch(11),
766 incoming_bundles: vec![],
767 operations: vec![],
768 height: BlockHeight(11),
769 timestamp: 190000000u64.into(),
770 authenticated_signer: None,
771 previous_block_hash: None,
772 };
773
774 let proposal = ProposalContent {
775 block: proposed_block,
776 round: Round::SingleLeader(11),
777 outcome: None,
778 };
779
780 let metamask_signature = AccountSignature::EvmSecp256k1(EvmSignature::from_str("f2d8afcd51d0f947f5c5e31ac1db73ec5306163af7949b3bb265ba53d03374b04b1e909007b555caf098da1aded29c600bee391c6ee8b4d0962a29044555796d1b").unwrap());
783
784 let signature = signer.sign(&proposal);
785 assert_eq!(signature, metamask_signature);
786 }
787}