1use std::{
6 slice::Iter,
7 time::{Duration, SystemTime, UNIX_EPOCH},
8};
9
10use anyhow::Result;
11use fastcrypto::hash::MultisetHash;
12use iota_protocol_config::ProtocolConfig;
13use iota_sdk_types::{
14 CheckpointContents, CheckpointContentsDigest, CheckpointContentsV1, CheckpointDigest,
15 CheckpointSummary, CheckpointTransactionInfo, Digest, EndOfEpochData, GasCostSummary,
16 RandomnessRound, Transaction,
17 crypto::{Intent, IntentScope, UserSignature},
18};
19#[cfg(not(target_arch = "wasm32"))]
20use prometheus_filtered::Histogram;
21use serde::{Deserialize, Serialize};
22#[cfg(not(target_arch = "wasm32"))]
23use tap::TapFallible;
24use tracing::instrument;
25#[cfg(not(target_arch = "wasm32"))]
26use tracing::warn;
27
28use crate::{
29 base_types::{ExecutionData, ExecutionDigests, VerifiedExecutionData, random_object_ref},
30 committee::{Committee, EpochId},
31 crypto::{
32 AccountPrivateKey, AggregateAuthoritySignature, AuthoritySignInfo, AuthoritySignInfoTrait,
33 AuthorityStrongQuorumSignInfo, VerificationObligation, default_hash, get_key_pair,
34 zero_ed25519_signature,
35 },
36 effects::{TestEffectsBuilder, TransactionEffectsAPI},
37 error::{IotaError, IotaResult},
38 global_state_hash::GlobalStateHash,
39 message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelope},
40 storage::ReadStore,
41 transaction::{TransactionAPI, TransactionEnvelope},
42};
43
44pub type CheckpointSequenceNumber = u64;
45pub type CheckpointTimestamp = u64;
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct CheckpointRequest {
49 pub sequence_number: Option<CheckpointSequenceNumber>,
54 pub request_content: bool,
57 pub certified: bool,
59}
60
61#[expect(clippy::large_enum_variant)]
62#[derive(Clone, Debug, Serialize, Deserialize)]
63pub enum CheckpointSummaryResponse {
64 Certified(CertifiedCheckpointSummary),
65 Pending(CheckpointSummary),
66}
67
68impl CheckpointSummaryResponse {
69 pub fn contents_digest(&self) -> CheckpointContentsDigest {
70 match self {
71 Self::Certified(s) => s.contents_digest,
72 Self::Pending(s) => s.contents_digest,
73 }
74 }
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub struct CheckpointResponse {
79 pub checkpoint: Option<CheckpointSummaryResponse>,
80 pub contents: Option<CheckpointContents>,
81}
82
83#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
88pub struct ECMHLiveObjectSetDigest {
89 pub digest: Digest,
90}
91
92impl From<fastcrypto::hash::Digest<32>> for ECMHLiveObjectSetDigest {
93 fn from(digest: fastcrypto::hash::Digest<32>) -> Self {
94 Self {
95 digest: Digest::new(digest.digest),
96 }
97 }
98}
99
100impl Default for ECMHLiveObjectSetDigest {
101 fn default() -> Self {
102 GlobalStateHash::default().digest().into()
103 }
104}
105
106impl Message for CheckpointSummary {
107 type DigestType = CheckpointDigest;
108 const SCOPE: IntentScope = IntentScope::CheckpointSummary;
109
110 fn digest(&self) -> Self::DigestType {
111 CheckpointDigest::new(default_hash(self))
112 }
113}
114
115mod checkpoint_summary_ext {
116 pub trait Sealed {}
117 impl Sealed for super::CheckpointSummary {}
118}
119
120pub trait CheckpointSummaryExt: Sized + checkpoint_summary_ext::Sealed {
124 fn new_with_protocol_config(
125 protocol_config: &ProtocolConfig,
126 epoch: EpochId,
127 sequence_number: CheckpointSequenceNumber,
128 network_total_transactions: u64,
129 transactions: &CheckpointContents,
130 previous_digest: Option<CheckpointDigest>,
131 epoch_rolling_gas_cost_summary: GasCostSummary,
132 end_of_epoch_data: Option<EndOfEpochData>,
133 timestamp_ms: CheckpointTimestamp,
134 randomness_rounds: Vec<RandomnessRound>,
135 ) -> Self;
136
137 fn verify_epoch(&self, epoch: EpochId) -> IotaResult;
138
139 fn timestamp(&self) -> SystemTime;
140
141 #[cfg(not(target_arch = "wasm32"))]
142 fn report_checkpoint_age(&self, metrics: &Histogram);
143
144 fn parse_version_specific_data(
145 &self,
146 config: &ProtocolConfig,
147 ) -> Result<Option<CheckpointVersionSpecificData>>;
148}
149
150impl CheckpointSummaryExt for CheckpointSummary {
151 fn new_with_protocol_config(
152 protocol_config: &ProtocolConfig,
153 epoch: EpochId,
154 sequence_number: CheckpointSequenceNumber,
155 network_total_transactions: u64,
156 transactions: &CheckpointContents,
157 previous_digest: Option<CheckpointDigest>,
158 epoch_rolling_gas_cost_summary: GasCostSummary,
159 end_of_epoch_data: Option<EndOfEpochData>,
160 timestamp_ms: CheckpointTimestamp,
161 randomness_rounds: Vec<RandomnessRound>,
162 ) -> Self {
163 let contents_digest = transactions.digest();
164
165 let version_specific_data =
166 match protocol_config.checkpoint_summary_version_specific_data_as_option() {
167 None | Some(0) => Vec::new(),
168 Some(1) => bcs::to_bytes(&CheckpointVersionSpecificData::V1(
169 CheckpointVersionSpecificDataV1 { randomness_rounds },
170 ))
171 .expect("version specific data should serialize"),
172 _ => unimplemented!(
173 "unrecognized version_specific_data version for
174 CheckpointSummary"
175 ),
176 };
177
178 Self {
179 epoch,
180 sequence_number,
181 network_total_transactions,
182 contents_digest,
183 previous_digest,
184 epoch_rolling_gas_cost_summary,
185 end_of_epoch_data,
186 timestamp_ms,
187 version_specific_data,
188 checkpoint_commitments: Default::default(),
189 }
190 }
191
192 fn verify_epoch(&self, epoch: EpochId) -> IotaResult {
193 fp_ensure!(
194 self.epoch == epoch,
195 IotaError::WrongEpoch {
196 expected_epoch: epoch,
197 actual_epoch: self.epoch,
198 }
199 );
200 Ok(())
201 }
202
203 fn timestamp(&self) -> SystemTime {
204 UNIX_EPOCH + Duration::from_millis(self.timestamp_ms)
205 }
206
207 #[cfg(not(target_arch = "wasm32"))]
208 fn report_checkpoint_age(&self, metrics: &Histogram) {
209 SystemTime::now()
210 .duration_since(self.timestamp())
211 .map(|latency| {
212 metrics.observe(latency.as_secs_f64());
213 })
214 .tap_err(|err| {
215 warn!(
216 checkpoint_seq = self.sequence_number,
217 "unable to compute checkpoint age: {}", err
218 )
219 })
220 .ok();
221 }
222
223 fn parse_version_specific_data(
224 &self,
225 config: &ProtocolConfig,
226 ) -> Result<Option<CheckpointVersionSpecificData>> {
227 match config.checkpoint_summary_version_specific_data_as_option() {
228 None | Some(0) => Ok(None),
229 Some(1) => Ok(Some(bcs::from_bytes(&self.version_specific_data)?)),
230 _ => unimplemented!("unrecognized version_specific_data version in CheckpointSummary"),
231 }
232 }
233}
234
235pub type CheckpointSummaryEnvelope<S> = Envelope<CheckpointSummary, S>;
244pub type CertifiedCheckpointSummary = CheckpointSummaryEnvelope<AuthorityStrongQuorumSignInfo>;
245pub type SignedCheckpointSummary = CheckpointSummaryEnvelope<AuthoritySignInfo>;
246
247pub type VerifiedCheckpoint = VerifiedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
248pub type TrustedCheckpoint = TrustedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
249
250impl CertifiedCheckpointSummary {
251 #[instrument(level = "trace", skip_all)]
252 pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
253 self.data().verify_epoch(self.auth_sig().epoch)?;
254 self.auth_sig().verify_secure(
255 self.data(),
256 Intent::iota_app(IntentScope::CheckpointSummary),
257 committee,
258 )
259 }
260
261 #[instrument(level = "trace", skip_all)]
267 pub fn batch_verify_authority_signatures(
268 summaries: &[Self],
269 committee: &Committee,
270 ) -> IotaResult {
271 let mut obligation = VerificationObligation::default();
272 for summary in summaries {
273 summary.data().verify_epoch(summary.auth_sig().epoch)?;
274 let idx = obligation.add_message(
275 summary.data(),
276 summary.auth_sig().epoch,
277 Intent::iota_app(IntentScope::CheckpointSummary),
278 );
279 summary
280 .auth_sig()
281 .add_to_verification_obligation(committee, &mut obligation, idx)?;
282 }
283 obligation.verify_all()
284 }
285
286 pub fn try_into_verified(self, committee: &Committee) -> IotaResult<VerifiedCheckpoint> {
287 self.verify_authority_signatures(committee)?;
288 Ok(VerifiedCheckpoint::new_from_verified(self))
289 }
290
291 pub fn verify_with_contents(
292 &self,
293 committee: &Committee,
294 contents: Option<&CheckpointContents>,
295 ) -> IotaResult {
296 self.verify_authority_signatures(committee)?;
297
298 if let Some(contents) = contents {
299 let contents_digest = contents.digest();
300 fp_ensure!(
301 contents_digest == self.data().contents_digest,
302 IotaError::GenericAuthority {
303 error: format!(
304 "Checkpoint contents digest mismatch: summary={:?}, received content digest {:?}, received {} transactions",
305 self.data(),
306 contents_digest,
307 contents.len()
308 )
309 }
310 );
311 }
312
313 Ok(())
314 }
315
316 pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
317 let summary = self.into_data();
318 (summary.sequence_number, summary)
319 }
320
321 pub fn get_validator_signature(self) -> AggregateAuthoritySignature {
322 self.auth_sig().signature.clone()
323 }
324}
325
326impl SignedCheckpointSummary {
327 #[instrument(level = "trace", skip_all)]
328 pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
329 self.data().verify_epoch(self.auth_sig().epoch)?;
330 self.auth_sig().verify_secure(
331 self.data(),
332 Intent::iota_app(IntentScope::CheckpointSummary),
333 committee,
334 )
335 }
336
337 pub fn try_into_verified(
338 self,
339 committee: &Committee,
340 ) -> IotaResult<VerifiedEnvelope<CheckpointSummary, AuthoritySignInfo>> {
341 self.verify_authority_signatures(committee)?;
342 Ok(VerifiedEnvelope::<CheckpointSummary, AuthoritySignInfo>::new_from_verified(self))
343 }
344}
345
346impl VerifiedCheckpoint {
347 pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
348 self.into_inner().into_summary_and_sequence()
349 }
350}
351
352#[derive(Clone, Debug, Serialize, Deserialize)]
355pub struct CheckpointSignatureMessage {
356 pub summary: SignedCheckpointSummary,
357}
358
359impl CheckpointSignatureMessage {
360 pub fn verify(&self, committee: &Committee) -> IotaResult {
361 self.summary.verify_authority_signatures(committee)
362 }
363}
364
365fn execution_digests(info: &CheckpointTransactionInfo) -> ExecutionDigests {
366 ExecutionDigests {
367 transaction: info.transaction,
368 effects: info.effects,
369 }
370}
371
372mod checkpoint_contents_ext {
373 pub trait Sealed {}
374 impl Sealed for super::CheckpointContents {}
375}
376
377pub trait CheckpointContentsExt: Sized + checkpoint_contents_ext::Sealed {
381 fn new_with_digests_and_signatures(
382 contents: impl IntoIterator<Item = ExecutionDigests>,
383 user_signatures: Vec<Vec<UserSignature>>,
384 ) -> Self;
385
386 fn new_with_causally_ordered_execution_data<'a>(
387 contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
388 ) -> Self;
389
390 fn new_with_digests_only_for_tests(
391 contents: impl IntoIterator<Item = ExecutionDigests>,
392 ) -> Self;
393
394 fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_;
395
396 fn into_iter_with_signatures(
397 self,
398 ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)>;
399
400 fn enumerate_transactions(
403 &self,
404 ckpt: &CheckpointSummary,
405 ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_;
406}
407
408impl CheckpointContentsExt for CheckpointContents {
409 fn new_with_digests_and_signatures(
410 contents: impl IntoIterator<Item = ExecutionDigests>,
411 user_signatures: Vec<Vec<UserSignature>>,
412 ) -> Self {
413 let transactions: Vec<_> = contents.into_iter().collect();
414 assert_eq!(transactions.len(), user_signatures.len());
415 Self::new_v1(CheckpointContentsV1::new(
416 transactions
417 .into_iter()
418 .zip(user_signatures)
419 .map(|(digests, signatures)| CheckpointTransactionInfo {
420 transaction: digests.transaction,
421 effects: digests.effects,
422 signatures,
423 })
424 .collect(),
425 ))
426 }
427
428 fn new_with_causally_ordered_execution_data<'a>(
429 contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
430 ) -> Self {
431 Self::new_v1(CheckpointContentsV1::new(
432 contents
433 .into_iter()
434 .map(|data| {
435 let digests = data.digests();
436 CheckpointTransactionInfo {
437 transaction: digests.transaction,
438 effects: digests.effects,
439 signatures: data.transaction.inner().data().signatures().to_owned(),
440 }
441 })
442 .collect(),
443 ))
444 }
445
446 fn new_with_digests_only_for_tests(
447 contents: impl IntoIterator<Item = ExecutionDigests>,
448 ) -> Self {
449 Self::new_v1(CheckpointContentsV1::new(
450 contents
451 .into_iter()
452 .map(|digests| CheckpointTransactionInfo {
453 transaction: digests.transaction,
454 effects: digests.effects,
455 signatures: Vec::new(),
456 })
457 .collect(),
458 ))
459 }
460
461 fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_ {
462 self.transactions().iter().map(execution_digests)
463 }
464
465 fn into_iter_with_signatures(
466 self,
467 ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)> {
468 match self {
469 CheckpointContents::V1(v1) => v1.into_transactions().into_iter().map(|info| {
470 let digests = execution_digests(&info);
471 (digests, info.signatures)
472 }),
473 _ => unimplemented!("a new CheckpointContents variant was added and must be handled"),
474 }
475 }
476
477 fn enumerate_transactions(
478 &self,
479 ckpt: &CheckpointSummary,
480 ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_ {
481 let start = ckpt.network_total_transactions - self.len() as u64;
482
483 (0u64..)
484 .zip(self.iter())
485 .map(move |(i, digests)| (i + start, digests))
486 }
487}
488
489#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(try_from = "RawFullCheckpointContents")]
497pub struct FullCheckpointContents {
498 transactions: Vec<ExecutionData>,
499 user_signatures: Vec<Vec<UserSignature>>,
507}
508
509#[derive(Deserialize)]
513struct RawFullCheckpointContents {
514 transactions: Vec<ExecutionData>,
515 user_signatures: Vec<Vec<UserSignature>>,
516}
517
518impl TryFrom<RawFullCheckpointContents> for FullCheckpointContents {
519 type Error = anyhow::Error;
520
521 fn try_from(raw: RawFullCheckpointContents) -> Result<Self> {
522 fp_ensure!(
523 raw.transactions.len() == raw.user_signatures.len(),
524 anyhow::anyhow!(
525 "checkpoint contents hold {} transactions but {} pinned signature sets",
526 raw.transactions.len(),
527 raw.user_signatures.len()
528 )
529 );
530 Ok(Self {
531 transactions: raw.transactions,
532 user_signatures: raw.user_signatures,
533 })
534 }
535}
536
537impl FullCheckpointContents {
538 pub fn new_with_causally_ordered_transactions<T>(contents: T) -> Self
539 where
540 T: IntoIterator<Item = ExecutionData>,
541 {
542 let (transactions, user_signatures): (Vec<_>, Vec<_>) = contents
543 .into_iter()
544 .map(|data| {
545 let sig = data.transaction.data().signatures().to_owned();
546 (data, sig)
547 })
548 .unzip();
549 assert_eq!(transactions.len(), user_signatures.len());
550 Self {
551 transactions,
552 user_signatures,
553 }
554 }
555
556 pub fn from_contents_and_execution_data(
564 contents: CheckpointContents,
565 execution_data: impl Iterator<Item = ExecutionData>,
566 ) -> Result<Self> {
567 let transactions: Vec<_> = execution_data.collect();
568 let user_signatures: Vec<Vec<UserSignature>> = contents
569 .into_iter_with_signatures()
570 .map(|(_, signatures)| signatures)
571 .collect();
572 fp_ensure!(
573 transactions.len() == user_signatures.len(),
574 anyhow::anyhow!(
575 "checkpoint contents pin {} signature sets for {} transactions",
576 user_signatures.len(),
577 transactions.len()
578 )
579 );
580 Ok(Self {
581 transactions,
582 user_signatures,
583 })
584 }
585
586 pub fn try_from_checkpoint_contents<S>(
587 store: S,
588 contents: CheckpointContents,
589 ) -> Result<Option<Self>, crate::storage::error::Error>
590 where
591 S: ReadStore,
592 {
593 let (digests, user_signatures): (Vec<_>, Vec<_>) =
594 contents.into_iter_with_signatures().unzip();
595 let mut transactions = Vec::with_capacity(digests.len());
596 for tx in &digests {
597 if let (Some(t), Some(e)) = (
598 store.try_get_transaction(&tx.transaction)?,
599 store.try_get_transaction_effects(&tx.transaction)?,
600 ) {
601 transactions.push(ExecutionData::new((*t).clone().into_inner(), e))
602 } else {
603 return Ok(None);
604 }
605 }
606 Ok(Some(Self {
607 transactions,
608 user_signatures,
609 }))
610 }
611
612 pub fn iter(&self) -> Iter<'_, ExecutionData> {
613 self.transactions.iter()
614 }
615
616 pub fn verify_digests(&self, digest: CheckpointContentsDigest) -> Result<()> {
627 fp_ensure!(
628 self.transactions.len() == self.user_signatures.len(),
629 anyhow::anyhow!(
630 "checkpoint contents hold {} transactions but {} pinned signature sets",
631 self.transactions.len(),
632 self.user_signatures.len()
633 )
634 );
635
636 let self_digest = self.checkpoint_contents().digest();
637 fp_ensure!(
638 digest == self_digest,
639 anyhow::anyhow!(
640 "checkpoint contents digest {self_digest} does not match expected digest {digest}"
641 )
642 );
643 let placeholder_signature = [UserSignature::Simple(zero_ed25519_signature())];
644 for (tx, pinned_signatures) in self.transactions.iter().zip(self.user_signatures.iter()) {
645 let transaction_digest = tx.transaction.digest();
646 fp_ensure!(
647 tx.effects.transaction_digest() == transaction_digest,
648 anyhow::anyhow!(
649 "transaction digest {transaction_digest} does not match expected digest {}",
650 tx.effects.transaction_digest()
651 )
652 );
653 let transaction = tx.transaction.data().transaction();
659 let expected_signatures: &[UserSignature] = if pinned_signatures.is_empty()
660 && (transaction.is_genesis_tx() || transaction.is_end_of_epoch_tx())
661 {
662 &placeholder_signature
663 } else {
664 pinned_signatures.as_slice()
665 };
666 fp_ensure!(
667 tx.transaction.data().signatures() == expected_signatures,
668 anyhow::anyhow!(
669 "transaction {transaction_digest} does not carry the signatures pinned by the checkpoint contents"
670 )
671 );
672 }
673 Ok(())
674 }
675
676 pub fn verify(self, digest: CheckpointContentsDigest) -> Result<VerifiedCheckpointContents> {
681 self.verify_digests(digest)?;
682 Ok(VerifiedCheckpointContents::new_unchecked(self))
683 }
684
685 pub fn checkpoint_contents(&self) -> CheckpointContents {
686 CheckpointContents::new_with_digests_and_signatures(
687 self.transactions.iter().map(|tx| tx.digests()),
688 self.user_signatures.clone(),
689 )
690 }
691
692 pub fn into_checkpoint_contents(self) -> CheckpointContents {
693 let digests: Vec<_> = self.transactions.iter().map(|tx| tx.digests()).collect();
694 CheckpointContents::new_with_digests_and_signatures(digests, self.user_signatures)
695 }
696
697 pub fn size(&self) -> usize {
698 self.transactions.len()
699 }
700
701 pub fn random_for_testing() -> Self {
702 let (a, key): (_, AccountPrivateKey) = get_key_pair();
703 let transaction = TransactionEnvelope::from_data_and_signer(
704 Transaction::new_transfer(
705 a,
706 random_object_ref(),
707 a,
708 random_object_ref(),
709 100000000000,
710 100,
711 ),
712 vec![&key],
713 );
714 let effects = TestEffectsBuilder::new(transaction.data()).build();
715 let exe_data = ExecutionData {
716 transaction,
717 effects,
718 };
719 FullCheckpointContents::new_with_causally_ordered_transactions(vec![exe_data])
720 }
721}
722
723impl IntoIterator for FullCheckpointContents {
724 type Item = ExecutionData;
725 type IntoIter = std::vec::IntoIter<Self::Item>;
726
727 fn into_iter(self) -> Self::IntoIter {
728 self.transactions.into_iter()
729 }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq)]
733pub struct VerifiedCheckpointContents {
734 transactions: Vec<VerifiedExecutionData>,
735 user_signatures: Vec<Vec<UserSignature>>,
743}
744
745impl VerifiedCheckpointContents {
746 pub fn new_unchecked(contents: FullCheckpointContents) -> Self {
750 Self {
751 transactions: contents
752 .transactions
753 .into_iter()
754 .map(VerifiedExecutionData::new_unchecked)
755 .collect(),
756 user_signatures: contents.user_signatures,
757 }
758 }
759
760 pub fn iter(&self) -> Iter<'_, VerifiedExecutionData> {
761 self.transactions.iter()
762 }
763
764 pub fn transactions(&self) -> &[VerifiedExecutionData] {
765 &self.transactions
766 }
767
768 pub fn into_inner(self) -> FullCheckpointContents {
769 FullCheckpointContents {
770 transactions: self
771 .transactions
772 .into_iter()
773 .map(|tx| tx.into_inner())
774 .collect(),
775 user_signatures: self.user_signatures,
776 }
777 }
778
779 pub fn into_checkpoint_contents(self) -> CheckpointContents {
780 self.into_inner().into_checkpoint_contents()
781 }
782
783 pub fn into_checkpoint_contents_digest(self) -> CheckpointContentsDigest {
784 self.into_inner().into_checkpoint_contents().digest()
785 }
786
787 pub fn num_of_transactions(&self) -> usize {
788 self.transactions.len()
789 }
790}
791
792#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
795pub enum CheckpointVersionSpecificData {
796 V1(CheckpointVersionSpecificDataV1),
797}
798
799impl CheckpointVersionSpecificData {
800 pub fn as_v1(&self) -> &CheckpointVersionSpecificDataV1 {
801 match self {
802 Self::V1(v) => v,
803 }
804 }
805
806 pub fn into_v1(self) -> CheckpointVersionSpecificDataV1 {
807 match self {
808 Self::V1(v) => v,
809 }
810 }
811
812 pub fn empty_for_tests() -> CheckpointVersionSpecificData {
813 CheckpointVersionSpecificData::V1(CheckpointVersionSpecificDataV1 {
814 randomness_rounds: Vec::new(),
815 })
816 }
817}
818
819#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
820pub struct CheckpointVersionSpecificDataV1 {
821 pub randomness_rounds: Vec<RandomnessRound>,
824}
825
826#[cfg(test)]
827mod tests {
828 use std::collections::BTreeMap;
829
830 use fastcrypto::traits::KeyPair;
831 use iota_sdk_types::{
832 ConsensusCommitDigest, ObjectId, TransactionDigest, TransactionEffectsDigest,
833 TransactionKind,
834 };
835 use rand::{SeedableRng, prelude::StdRng};
836
837 use super::*;
838 use crate::{
839 object::OBJECT_START_VERSION, transaction::VerifiedTransaction, utils::make_committee_key,
840 };
841
842 const RNG_SEED: [u8; 32] = [
844 21, 23, 199, 200, 234, 250, 252, 178, 94, 15, 202, 178, 62, 186, 88, 137, 233, 192, 130,
845 157, 179, 179, 65, 9, 31, 249, 221, 123, 225, 112, 199, 247,
846 ];
847
848 #[test]
849 fn test_signed_checkpoint() {
850 let mut rng = StdRng::from_seed(RNG_SEED);
851 let (keys, committee) = make_committee_key(&mut rng);
852 let (_, committee2) = make_committee_key(&mut rng);
853
854 let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
855
856 let signed_checkpoints: Vec<_> = keys
859 .iter()
860 .map(|k| {
861 let name = k.public().into();
862
863 SignedCheckpointSummary::new(
864 committee.epoch,
865 CheckpointSummary::new_with_protocol_config(
866 &ProtocolConfig::get_for_max_version_UNSAFE(),
867 committee.epoch,
868 1,
869 0,
870 &set,
871 None,
872 GasCostSummary::default(),
873 None,
874 0,
875 Vec::new(),
876 ),
877 k,
878 name,
879 )
880 })
881 .collect();
882
883 signed_checkpoints.iter().for_each(|c| {
884 c.verify_authority_signatures(&committee)
885 .expect("signature ok")
886 });
887
888 signed_checkpoints
890 .iter()
891 .for_each(|c| assert!(c.verify_authority_signatures(&committee2).is_err()));
892 }
893
894 #[test]
895 fn test_certified_checkpoint() {
896 let mut rng = StdRng::from_seed(RNG_SEED);
897 let (keys, committee) = make_committee_key(&mut rng);
898
899 let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
900
901 let summary = CheckpointSummary::new_with_protocol_config(
902 &ProtocolConfig::get_for_max_version_UNSAFE(),
903 committee.epoch,
904 1,
905 0,
906 &set,
907 None,
908 GasCostSummary::default(),
909 None,
910 0,
911 Vec::new(),
912 );
913
914 let sign_infos: Vec<_> = keys
915 .iter()
916 .map(|k| {
917 let name = k.public().into();
918
919 SignedCheckpointSummary::sign(committee.epoch, &summary, k, name)
920 })
921 .collect();
922
923 let checkpoint_cert =
924 CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("Cert is OK");
925
926 assert!(
928 checkpoint_cert
929 .verify_with_contents(&committee, Some(&set))
930 .is_ok()
931 );
932
933 let signed_checkpoints: Vec<_> = keys
935 .iter()
936 .map(|k| {
937 let name = k.public().into();
938 let set = CheckpointContents::new_with_digests_only_for_tests([
939 ExecutionDigests::random(),
940 ]);
941
942 SignedCheckpointSummary::new(
943 committee.epoch,
944 CheckpointSummary::new_with_protocol_config(
945 &ProtocolConfig::get_for_max_version_UNSAFE(),
946 committee.epoch,
947 1,
948 0,
949 &set,
950 None,
951 GasCostSummary::default(),
952 None,
953 0,
954 Vec::new(),
955 ),
956 k,
957 name,
958 )
959 })
960 .collect();
961
962 let summary = signed_checkpoints[0].data().clone();
963 let sign_infos = signed_checkpoints
964 .into_iter()
965 .map(|v| v.into_sig())
966 .collect();
967 assert!(
968 CertifiedCheckpointSummary::new(summary, sign_infos, &committee)
969 .unwrap()
970 .verify_authority_signatures(&committee)
971 .is_err()
972 )
973 }
974
975 #[test]
976 fn test_batch_verify_certified_checkpoints() {
977 let mut rng = StdRng::from_seed(RNG_SEED);
978 let (keys, committee) = make_committee_key(&mut rng);
979
980 let summary_for_sequence_number = |sequence_number| {
981 let set =
982 CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
983 CheckpointSummary::new_with_protocol_config(
984 &ProtocolConfig::get_for_max_version_UNSAFE(),
985 committee.epoch,
986 sequence_number,
987 0,
988 &set,
989 None,
990 GasCostSummary::default(),
991 None,
992 0,
993 Vec::new(),
994 )
995 };
996 let certify = |summary: CheckpointSummary| {
997 let sign_infos = keys
998 .iter()
999 .map(|k| {
1000 SignedCheckpointSummary::sign(committee.epoch, &summary, k, k.public().into())
1001 })
1002 .collect();
1003 CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
1004 };
1005
1006 let summaries: Vec<_> = (1..=3)
1007 .map(|seq| certify(summary_for_sequence_number(seq)))
1008 .collect();
1009 CertifiedCheckpointSummary::batch_verify_authority_signatures(&summaries, &committee)
1010 .expect("signatures ok");
1011
1012 let mismatched = {
1015 let summary = summary_for_sequence_number(4);
1016 let sign_infos = keys
1017 .iter()
1018 .map(|k| {
1019 SignedCheckpointSummary::sign(
1020 committee.epoch,
1021 &summary_for_sequence_number(4),
1022 k,
1023 k.public().into(),
1024 )
1025 })
1026 .collect();
1027 CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
1028 };
1029 let with_mismatched: Vec<_> = summaries.into_iter().chain([mismatched]).collect();
1030 assert!(
1031 CertifiedCheckpointSummary::batch_verify_authority_signatures(
1032 &with_mismatched,
1033 &committee
1034 )
1035 .is_err()
1036 );
1037 }
1038
1039 fn generate_test_checkpoint_summary_from_digest(
1044 digest: TransactionDigest,
1045 ) -> CheckpointSummary {
1046 CheckpointSummary::new_with_protocol_config(
1047 &ProtocolConfig::get_for_max_version_UNSAFE(),
1048 1,
1049 2,
1050 10,
1051 &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::new(
1052 digest,
1053 TransactionEffectsDigest::ZERO,
1054 )]),
1055 None,
1056 GasCostSummary::default(),
1057 None,
1058 100,
1059 Vec::new(),
1060 )
1061 }
1062
1063 #[test]
1066 fn test_checkpoint_summary_with_different_consensus_digest() {
1067 {
1070 let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1071 1,
1072 2,
1073 100,
1074 ConsensusCommitDigest::default(),
1075 Vec::new(),
1076 );
1077 let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1078 1,
1079 2,
1080 100,
1081 ConsensusCommitDigest::default(),
1082 Vec::new(),
1083 );
1084 let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
1085 let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
1086 assert_eq!(c1.digest(), c2.digest());
1087 }
1088
1089 {
1092 let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1093 1,
1094 2,
1095 100,
1096 ConsensusCommitDigest::default(),
1097 Vec::new(),
1098 );
1099 let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1100 1,
1101 2,
1102 100,
1103 ConsensusCommitDigest::random(),
1104 Vec::new(),
1105 );
1106 let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
1107 let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
1108 assert_ne!(c1.digest(), c2.digest());
1109 }
1110 }
1111
1112 fn contents_with_a_swapped_envelope() -> (
1116 CheckpointContentsDigest,
1117 FullCheckpointContents,
1118 FullCheckpointContents,
1119 ) {
1120 let contents = FullCheckpointContents::random_for_testing();
1121 let digest = contents.checkpoint_contents().digest();
1122
1123 let (_, other_key): (_, AccountPrivateKey) = get_key_pair();
1124 let original = contents.transactions[0].transaction.clone();
1125 let swapped = TransactionEnvelope::from_data_and_signer(
1126 original.data().transaction().clone(),
1127 vec![&other_key],
1128 );
1129 assert_eq!(swapped.digest(), original.digest());
1130 assert_ne!(swapped.data().signatures(), original.data().signatures());
1131
1132 let mut tampered = contents.clone();
1133 tampered.transactions[0].transaction = swapped;
1134 assert_eq!(tampered.checkpoint_contents().digest(), digest);
1135
1136 (digest, contents, tampered)
1137 }
1138
1139 #[test]
1142 fn verify_digests_rejects_signatures_the_checkpoint_did_not_pin() {
1143 let (digest, contents, tampered) = contents_with_a_swapped_envelope();
1144 contents
1145 .verify_digests(digest)
1146 .expect("contents served as built must verify");
1147 tampered
1148 .verify_digests(digest)
1149 .expect_err("a transaction carrying unpinned signatures must be rejected");
1150 }
1151
1152 #[test]
1154 fn verify_hands_back_verified_contents_that_carry_the_same_digest() {
1155 let (digest, contents, _) = contents_with_a_swapped_envelope();
1156
1157 let verified = contents
1158 .verify(digest)
1159 .expect("contents served as built must verify");
1160
1161 assert_eq!(verified.into_checkpoint_contents_digest(), digest);
1162 }
1163
1164 #[test]
1165 fn verify_refuses_contents_that_fail_verification() {
1166 let (digest, _, tampered) = contents_with_a_swapped_envelope();
1167
1168 tampered
1169 .verify(digest)
1170 .expect_err("contents that fail verification must not become verified");
1171 }
1172
1173 #[test]
1174 fn verify_digests_rejects_a_signature_count_that_does_not_match() {
1175 let contents = FullCheckpointContents::random_for_testing();
1176 let digest = contents.checkpoint_contents().digest();
1177
1178 let mut tampered = contents;
1179 tampered.user_signatures.clear();
1180
1181 tampered
1184 .verify_digests(digest)
1185 .expect_err("a signature count that does not match must be rejected");
1186 }
1187
1188 #[test]
1192 fn verify_digests_accepts_a_system_transaction_pinned_without_signatures() {
1193 let transaction = VerifiedTransaction::new_end_of_epoch_transaction(vec![]).into_inner();
1194 assert_eq!(transaction.data().signatures().len(), 1);
1195 let effects = TestEffectsBuilder::new(transaction.data()).build();
1196 let execution_data = ExecutionData {
1197 transaction,
1198 effects,
1199 };
1200 let digests = execution_data.digests();
1201
1202 let checkpoint_contents =
1206 CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1207 let digest = checkpoint_contents.digest();
1208 let contents = FullCheckpointContents::from_contents_and_execution_data(
1209 checkpoint_contents,
1210 std::iter::once(execution_data),
1211 )
1212 .expect("one transaction per pinned signature set");
1213
1214 contents
1215 .verify_digests(digest)
1216 .expect("a system transaction pinned without signatures must verify");
1217 }
1218
1219 #[test]
1222 fn verify_digests_rejects_a_system_transaction_without_the_placeholder_signature() {
1223 let (_, key): (_, AccountPrivateKey) = get_key_pair();
1224 let system_data = Transaction::new_system_transaction(TransactionKind::EndOfEpoch(vec![]));
1225 let transaction = TransactionEnvelope::from_data_and_signer(system_data, vec![&key]);
1226 let effects = TestEffectsBuilder::new(transaction.data()).build();
1227 let execution_data = ExecutionData {
1228 transaction,
1229 effects,
1230 };
1231 let digests = execution_data.digests();
1232 let checkpoint_contents =
1233 CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1234 let digest = checkpoint_contents.digest();
1235 let contents = FullCheckpointContents::from_contents_and_execution_data(
1236 checkpoint_contents,
1237 std::iter::once(execution_data),
1238 )
1239 .expect("one transaction per pinned signature set");
1240
1241 contents
1242 .verify_digests(digest)
1243 .expect_err("a system transaction signed with a real key must be rejected");
1244 }
1245
1246 #[test]
1249 fn deserializing_contents_with_a_mismatched_signature_count_fails() {
1250 let mut tampered = FullCheckpointContents::random_for_testing();
1251 tampered.user_signatures.clear();
1252 let bytes = bcs::to_bytes(&tampered).expect("serializing the tampered value must work");
1253
1254 bcs::from_bytes::<FullCheckpointContents>(&bytes)
1255 .expect_err("a signature count that does not match must not deserialize");
1256 }
1257
1258 #[test]
1261 fn from_contents_and_execution_data_rejects_mismatched_lengths() {
1262 let contents = FullCheckpointContents::random_for_testing();
1263 let checkpoint_contents = contents.checkpoint_contents();
1264
1265 FullCheckpointContents::from_contents_and_execution_data(
1266 checkpoint_contents,
1267 std::iter::empty(),
1268 )
1269 .expect_err("a signature count that does not match the transactions must be rejected");
1270 }
1271
1272 #[test]
1277 fn verify_digests_rejects_a_consensus_system_transaction_pinned_without_signatures() {
1278 let transaction = VerifiedTransaction::new_consensus_commit_prologue_v1(
1279 0,
1280 0,
1281 0,
1282 ConsensusCommitDigest::default(),
1283 vec![],
1284 )
1285 .into_inner();
1286 let effects = TestEffectsBuilder::new(transaction.data())
1289 .with_shared_input_versions(BTreeMap::from([(ObjectId::CLOCK, OBJECT_START_VERSION)]))
1290 .build();
1291 let execution_data = ExecutionData {
1292 transaction,
1293 effects,
1294 };
1295 let digests = execution_data.digests();
1296 let checkpoint_contents =
1297 CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1298 let digest = checkpoint_contents.digest();
1299 let contents = FullCheckpointContents::from_contents_and_execution_data(
1300 checkpoint_contents,
1301 std::iter::once(execution_data),
1302 )
1303 .expect("one transaction per pinned signature set");
1304
1305 contents.verify_digests(digest).expect_err(
1306 "a consensus system transaction pinned without signatures must be rejected",
1307 );
1308 }
1309}