Skip to main content

iota_types/
messages_checkpoint.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    CheckpointContentsDigest, CheckpointContentsV1, CheckpointDigest, Digest, RandomnessRound,
15    Transaction,
16    checkpoint::{
17        CheckpointContents, CheckpointSummary, CheckpointTransactionInfo, EndOfEpochData,
18    },
19    crypto::{Intent, IntentScope, UserSignature},
20    gas::GasCostSummary,
21};
22#[cfg(not(target_arch = "wasm32"))]
23use prometheus_filtered::Histogram;
24use serde::{Deserialize, Serialize};
25#[cfg(not(target_arch = "wasm32"))]
26use tap::TapFallible;
27use tracing::instrument;
28#[cfg(not(target_arch = "wasm32"))]
29use tracing::warn;
30
31use crate::{
32    base_types::{ExecutionData, ExecutionDigests, VerifiedExecutionData, random_object_ref},
33    committee::{Committee, EpochId},
34    crypto::{
35        AccountKeyPair, AggregateAuthoritySignature, AuthoritySignInfo, AuthoritySignInfoTrait,
36        AuthorityStrongQuorumSignInfo, VerificationObligation, default_hash, get_key_pair,
37    },
38    effects::{TestEffectsBuilder, TransactionEffectsAPI},
39    error::{IotaError, IotaResult},
40    global_state_hash::GlobalStateHash,
41    message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelope},
42    storage::ReadStore,
43    transaction::{TransactionAPI, TransactionEnvelope},
44};
45
46pub type CheckpointSequenceNumber = u64;
47pub type CheckpointTimestamp = u64;
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
50pub struct CheckpointRequest {
51    /// if a sequence number is specified, return the checkpoint with that
52    /// sequence number; otherwise if None returns the latest checkpoint
53    /// stored (authenticated or pending, depending on the value of
54    /// `certified` flag)
55    pub sequence_number: Option<CheckpointSequenceNumber>,
56    // A flag, if true also return the contents of the
57    // checkpoint besides the meta-data.
58    pub request_content: bool,
59    // If true, returns certified checkpoint, otherwise returns pending checkpoint
60    pub certified: bool,
61}
62
63#[expect(clippy::large_enum_variant)]
64#[derive(Clone, Debug, Serialize, Deserialize)]
65pub enum CheckpointSummaryResponse {
66    Certified(CertifiedCheckpointSummary),
67    Pending(CheckpointSummary),
68}
69
70impl CheckpointSummaryResponse {
71    pub fn contents_digest(&self) -> CheckpointContentsDigest {
72        match self {
73            Self::Certified(s) => s.contents_digest,
74            Self::Pending(s) => s.contents_digest,
75        }
76    }
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct CheckpointResponse {
81    pub checkpoint: Option<CheckpointSummaryResponse>,
82    pub contents: Option<CheckpointContents>,
83}
84
85// The constituent parts of checkpoints, signed and certified
86
87/// The Sha256 digest of an EllipticCurveMultisetHash committing to the live
88/// object set.
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
90pub struct ECMHLiveObjectSetDigest {
91    pub digest: Digest,
92}
93
94impl From<fastcrypto::hash::Digest<32>> for ECMHLiveObjectSetDigest {
95    fn from(digest: fastcrypto::hash::Digest<32>) -> Self {
96        Self {
97            digest: Digest::new(digest.digest),
98        }
99    }
100}
101
102impl Default for ECMHLiveObjectSetDigest {
103    fn default() -> Self {
104        GlobalStateHash::default().digest().into()
105    }
106}
107
108impl Message for CheckpointSummary {
109    type DigestType = CheckpointDigest;
110    const SCOPE: IntentScope = IntentScope::CheckpointSummary;
111
112    fn digest(&self) -> Self::DigestType {
113        CheckpointDigest::new(default_hash(self))
114    }
115}
116
117mod checkpoint_summary_ext {
118    pub trait Sealed {}
119    impl Sealed for super::CheckpointSummary {}
120}
121
122/// Node-only helpers for [`CheckpointSummary`], which is defined in
123/// `iota_sdk_types`. These live on an extension trait because inherent methods
124/// cannot be added to a type that is foreign to this crate.
125pub trait CheckpointSummaryExt: Sized + checkpoint_summary_ext::Sealed {
126    fn new_with_protocol_config(
127        protocol_config: &ProtocolConfig,
128        epoch: EpochId,
129        sequence_number: CheckpointSequenceNumber,
130        network_total_transactions: u64,
131        transactions: &CheckpointContents,
132        previous_digest: Option<CheckpointDigest>,
133        epoch_rolling_gas_cost_summary: GasCostSummary,
134        end_of_epoch_data: Option<EndOfEpochData>,
135        timestamp_ms: CheckpointTimestamp,
136        randomness_rounds: Vec<RandomnessRound>,
137    ) -> Self;
138
139    fn verify_epoch(&self, epoch: EpochId) -> IotaResult;
140
141    fn timestamp(&self) -> SystemTime;
142
143    #[cfg(not(target_arch = "wasm32"))]
144    fn report_checkpoint_age(&self, metrics: &Histogram);
145
146    fn parse_version_specific_data(
147        &self,
148        config: &ProtocolConfig,
149    ) -> Result<Option<CheckpointVersionSpecificData>>;
150}
151
152impl CheckpointSummaryExt for CheckpointSummary {
153    fn new_with_protocol_config(
154        protocol_config: &ProtocolConfig,
155        epoch: EpochId,
156        sequence_number: CheckpointSequenceNumber,
157        network_total_transactions: u64,
158        transactions: &CheckpointContents,
159        previous_digest: Option<CheckpointDigest>,
160        epoch_rolling_gas_cost_summary: GasCostSummary,
161        end_of_epoch_data: Option<EndOfEpochData>,
162        timestamp_ms: CheckpointTimestamp,
163        randomness_rounds: Vec<RandomnessRound>,
164    ) -> Self {
165        let contents_digest = transactions.digest();
166
167        let version_specific_data =
168            match protocol_config.checkpoint_summary_version_specific_data_as_option() {
169                None | Some(0) => Vec::new(),
170                Some(1) => bcs::to_bytes(&CheckpointVersionSpecificData::V1(
171                    CheckpointVersionSpecificDataV1 { randomness_rounds },
172                ))
173                .expect("version specific data should serialize"),
174                _ => unimplemented!(
175                    "unrecognized version_specific_data version for
176    CheckpointSummary"
177                ),
178            };
179
180        Self {
181            epoch,
182            sequence_number,
183            network_total_transactions,
184            contents_digest,
185            previous_digest,
186            epoch_rolling_gas_cost_summary,
187            end_of_epoch_data,
188            timestamp_ms,
189            version_specific_data,
190            checkpoint_commitments: Default::default(),
191        }
192    }
193
194    fn verify_epoch(&self, epoch: EpochId) -> IotaResult {
195        fp_ensure!(
196            self.epoch == epoch,
197            IotaError::WrongEpoch {
198                expected_epoch: epoch,
199                actual_epoch: self.epoch,
200            }
201        );
202        Ok(())
203    }
204
205    fn timestamp(&self) -> SystemTime {
206        UNIX_EPOCH + Duration::from_millis(self.timestamp_ms)
207    }
208
209    #[cfg(not(target_arch = "wasm32"))]
210    fn report_checkpoint_age(&self, metrics: &Histogram) {
211        SystemTime::now()
212            .duration_since(self.timestamp())
213            .map(|latency| {
214                metrics.observe(latency.as_secs_f64());
215            })
216            .tap_err(|err| {
217                warn!(
218                    checkpoint_seq = self.sequence_number,
219                    "unable to compute checkpoint age: {}", err
220                )
221            })
222            .ok();
223    }
224
225    fn parse_version_specific_data(
226        &self,
227        config: &ProtocolConfig,
228    ) -> Result<Option<CheckpointVersionSpecificData>> {
229        match config.checkpoint_summary_version_specific_data_as_option() {
230            None | Some(0) => Ok(None),
231            Some(1) => Ok(Some(bcs::from_bytes(&self.version_specific_data)?)),
232            _ => unimplemented!("unrecognized version_specific_data version in CheckpointSummary"),
233        }
234    }
235}
236
237// Checkpoints are signed by an authority and 2f+1 form a
238// certificate that others can use to catch up. The actual
239// content of the digest must at the very least commit to
240// the set of transactions contained in the certificate but
241// we might extend this to contain roots of merkle trees,
242// or other authenticated data structures to support light
243// clients and more efficient sync protocols.
244
245pub type CheckpointSummaryEnvelope<S> = Envelope<CheckpointSummary, S>;
246pub type CertifiedCheckpointSummary = CheckpointSummaryEnvelope<AuthorityStrongQuorumSignInfo>;
247pub type SignedCheckpointSummary = CheckpointSummaryEnvelope<AuthoritySignInfo>;
248
249pub type VerifiedCheckpoint = VerifiedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
250pub type TrustedCheckpoint = TrustedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
251
252impl CertifiedCheckpointSummary {
253    #[instrument(level = "trace", skip_all)]
254    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
255        self.data().verify_epoch(self.auth_sig().epoch)?;
256        self.auth_sig().verify_secure(
257            self.data(),
258            Intent::iota_app(IntentScope::CheckpointSummary),
259            committee,
260        )
261    }
262
263    /// Verifies the authority signatures of `summaries`, all certified by
264    /// `committee`, in a single batched signature verification. Cheaper than
265    /// verifying each summary on its own, at the cost of not saying which
266    /// summary failed; callers that need that can fall back to
267    /// [`Self::verify_authority_signatures`] per summary.
268    #[instrument(level = "trace", skip_all)]
269    pub fn batch_verify_authority_signatures(
270        summaries: &[Self],
271        committee: &Committee,
272    ) -> IotaResult {
273        let mut obligation = VerificationObligation::default();
274        for summary in summaries {
275            summary.data().verify_epoch(summary.auth_sig().epoch)?;
276            let idx = obligation.add_message(
277                summary.data(),
278                summary.auth_sig().epoch,
279                Intent::iota_app(IntentScope::CheckpointSummary),
280            );
281            summary
282                .auth_sig()
283                .add_to_verification_obligation(committee, &mut obligation, idx)?;
284        }
285        obligation.verify_all()
286    }
287
288    pub fn try_into_verified(self, committee: &Committee) -> IotaResult<VerifiedCheckpoint> {
289        self.verify_authority_signatures(committee)?;
290        Ok(VerifiedCheckpoint::new_from_verified(self))
291    }
292
293    pub fn verify_with_contents(
294        &self,
295        committee: &Committee,
296        contents: Option<&CheckpointContents>,
297    ) -> IotaResult {
298        self.verify_authority_signatures(committee)?;
299
300        if let Some(contents) = contents {
301            let contents_digest = contents.digest();
302            fp_ensure!(
303                contents_digest == self.data().contents_digest,
304                IotaError::GenericAuthority {
305                    error: format!(
306                        "Checkpoint contents digest mismatch: summary={:?}, received content digest {:?}, received {} transactions",
307                        self.data(),
308                        contents_digest,
309                        contents.len()
310                    )
311                }
312            );
313        }
314
315        Ok(())
316    }
317
318    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
319        let summary = self.into_data();
320        (summary.sequence_number, summary)
321    }
322
323    pub fn get_validator_signature(self) -> AggregateAuthoritySignature {
324        self.auth_sig().signature.clone()
325    }
326}
327
328impl SignedCheckpointSummary {
329    #[instrument(level = "trace", skip_all)]
330    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
331        self.data().verify_epoch(self.auth_sig().epoch)?;
332        self.auth_sig().verify_secure(
333            self.data(),
334            Intent::iota_app(IntentScope::CheckpointSummary),
335            committee,
336        )
337    }
338
339    pub fn try_into_verified(
340        self,
341        committee: &Committee,
342    ) -> IotaResult<VerifiedEnvelope<CheckpointSummary, AuthoritySignInfo>> {
343        self.verify_authority_signatures(committee)?;
344        Ok(VerifiedEnvelope::<CheckpointSummary, AuthoritySignInfo>::new_from_verified(self))
345    }
346}
347
348impl VerifiedCheckpoint {
349    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
350        self.into_inner().into_summary_and_sequence()
351    }
352}
353
354/// This is a message validators publish to consensus in order to sign
355/// checkpoint
356#[derive(Clone, Debug, Serialize, Deserialize)]
357pub struct CheckpointSignatureMessage {
358    pub summary: SignedCheckpointSummary,
359}
360
361impl CheckpointSignatureMessage {
362    pub fn verify(&self, committee: &Committee) -> IotaResult {
363        self.summary.verify_authority_signatures(committee)
364    }
365}
366
367fn execution_digests(info: &CheckpointTransactionInfo) -> ExecutionDigests {
368    ExecutionDigests {
369        transaction: info.transaction,
370        effects: info.effects,
371    }
372}
373
374mod checkpoint_contents_ext {
375    pub trait Sealed {}
376    impl Sealed for super::CheckpointContents {}
377}
378
379/// Node-only helpers for [`CheckpointContents`], which is defined in
380/// `iota_sdk_types`. They bridge the node's `ExecutionDigests` representation
381/// to the SDK type's parallel [`CheckpointTransactionInfo`] form.
382pub trait CheckpointContentsExt: Sized + checkpoint_contents_ext::Sealed {
383    fn new_with_digests_and_signatures(
384        contents: impl IntoIterator<Item = ExecutionDigests>,
385        user_signatures: Vec<Vec<UserSignature>>,
386    ) -> Self;
387
388    fn new_with_causally_ordered_execution_data<'a>(
389        contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
390    ) -> Self;
391
392    fn new_with_digests_only_for_tests(
393        contents: impl IntoIterator<Item = ExecutionDigests>,
394    ) -> Self;
395
396    fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_;
397
398    fn into_iter_with_signatures(
399        self,
400    ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)>;
401
402    /// Enumerate the transactions in the contents, pairing each with its index
403    /// in the global ordering of executed transactions since genesis.
404    fn enumerate_transactions(
405        &self,
406        ckpt: &CheckpointSummary,
407    ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_;
408}
409
410impl CheckpointContentsExt for CheckpointContents {
411    fn new_with_digests_and_signatures(
412        contents: impl IntoIterator<Item = ExecutionDigests>,
413        user_signatures: Vec<Vec<UserSignature>>,
414    ) -> Self {
415        let transactions: Vec<_> = contents.into_iter().collect();
416        assert_eq!(transactions.len(), user_signatures.len());
417        Self::new_v1(CheckpointContentsV1::new(
418            transactions
419                .into_iter()
420                .zip(user_signatures)
421                .map(|(digests, signatures)| CheckpointTransactionInfo {
422                    transaction: digests.transaction,
423                    effects: digests.effects,
424                    signatures,
425                })
426                .collect(),
427        ))
428    }
429
430    fn new_with_causally_ordered_execution_data<'a>(
431        contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
432    ) -> Self {
433        Self::new_v1(CheckpointContentsV1::new(
434            contents
435                .into_iter()
436                .map(|data| {
437                    let digests = data.digests();
438                    CheckpointTransactionInfo {
439                        transaction: digests.transaction,
440                        effects: digests.effects,
441                        signatures: data.transaction.inner().data().signatures().to_owned(),
442                    }
443                })
444                .collect(),
445        ))
446    }
447
448    fn new_with_digests_only_for_tests(
449        contents: impl IntoIterator<Item = ExecutionDigests>,
450    ) -> Self {
451        Self::new_v1(CheckpointContentsV1::new(
452            contents
453                .into_iter()
454                .map(|digests| CheckpointTransactionInfo {
455                    transaction: digests.transaction,
456                    effects: digests.effects,
457                    signatures: Vec::new(),
458                })
459                .collect(),
460        ))
461    }
462
463    fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_ {
464        self.transactions().iter().map(execution_digests)
465    }
466
467    fn into_iter_with_signatures(
468        self,
469    ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)> {
470        match self {
471            CheckpointContents::V1(v1) => v1.into_transactions().into_iter().map(|info| {
472                let digests = execution_digests(&info);
473                (digests, info.signatures)
474            }),
475            _ => unimplemented!("a new CheckpointContents variant was added and must be handled"),
476        }
477    }
478
479    fn enumerate_transactions(
480        &self,
481        ckpt: &CheckpointSummary,
482    ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_ {
483        let start = ckpt.network_total_transactions - self.len() as u64;
484
485        (0u64..)
486            .zip(self.iter())
487            .map(move |(i, digests)| (i + start, digests))
488    }
489}
490
491/// Same as CheckpointContents, but contains full contents of all Transactions
492/// and TransactionEffects associated with the checkpoint.
493// NOTE: This data structure is used for state sync of checkpoints. Therefore we attempt
494// to estimate its size in CheckpointBuilder in order to limit the maximum serialized
495// size of a checkpoint sent over the network. If this struct is modified,
496// CheckpointBuilder::split_checkpoint_chunks should also be updated accordingly.
497#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
498pub struct FullCheckpointContents {
499    transactions: Vec<ExecutionData>,
500    /// This field 'pins' user signatures for the checkpoint
501    /// The length of this vector is same as length of transactions vector
502    /// System transactions has empty signatures
503    user_signatures: Vec<Vec<UserSignature>>,
504}
505
506impl FullCheckpointContents {
507    pub fn new_with_causally_ordered_transactions<T>(contents: T) -> Self
508    where
509        T: IntoIterator<Item = ExecutionData>,
510    {
511        let (transactions, user_signatures): (Vec<_>, Vec<_>) = contents
512            .into_iter()
513            .map(|data| {
514                let sig = data.transaction.data().signatures().to_owned();
515                (data, sig)
516            })
517            .unzip();
518        assert_eq!(transactions.len(), user_signatures.len());
519        Self {
520            transactions,
521            user_signatures,
522        }
523    }
524
525    pub fn from_contents_and_execution_data(
526        contents: CheckpointContents,
527        execution_data: impl Iterator<Item = ExecutionData>,
528    ) -> Self {
529        let transactions: Vec<_> = execution_data.collect();
530        let user_signatures = contents
531            .into_iter_with_signatures()
532            .map(|(_, signatures)| signatures)
533            .collect();
534        Self {
535            transactions,
536            user_signatures,
537        }
538    }
539
540    pub fn try_from_checkpoint_contents<S>(
541        store: S,
542        contents: CheckpointContents,
543    ) -> Result<Option<Self>, crate::storage::error::Error>
544    where
545        S: ReadStore,
546    {
547        let (digests, user_signatures): (Vec<_>, Vec<_>) =
548            contents.into_iter_with_signatures().unzip();
549        let mut transactions = Vec::with_capacity(digests.len());
550        for tx in &digests {
551            if let (Some(t), Some(e)) = (
552                store.try_get_transaction(&tx.transaction)?,
553                store.try_get_transaction_effects(&tx.transaction)?,
554            ) {
555                transactions.push(ExecutionData::new((*t).clone().into_inner(), e))
556            } else {
557                return Ok(None);
558            }
559        }
560        Ok(Some(Self {
561            transactions,
562            user_signatures,
563        }))
564    }
565
566    pub fn iter(&self) -> Iter<'_, ExecutionData> {
567        self.transactions.iter()
568    }
569
570    /// Verifies that this checkpoint's digest matches the given digest, and
571    /// that all internal Transaction and TransactionEffects digests are
572    /// consistent.
573    pub fn verify_digests(&self, digest: CheckpointContentsDigest) -> Result<()> {
574        let self_digest = self.checkpoint_contents().digest();
575        fp_ensure!(
576            digest == self_digest,
577            anyhow::anyhow!(
578                "checkpoint contents digest {self_digest} does not match expected digest {digest}"
579            )
580        );
581        for tx in self.iter() {
582            let transaction_digest = tx.transaction.digest();
583            fp_ensure!(
584                tx.effects.transaction_digest() == transaction_digest,
585                anyhow::anyhow!(
586                    "transaction digest {transaction_digest} does not match expected digest {}",
587                    tx.effects.transaction_digest()
588                )
589            );
590        }
591        Ok(())
592    }
593
594    pub fn checkpoint_contents(&self) -> CheckpointContents {
595        CheckpointContents::new_with_digests_and_signatures(
596            self.transactions.iter().map(|tx| tx.digests()),
597            self.user_signatures.clone(),
598        )
599    }
600
601    pub fn into_checkpoint_contents(self) -> CheckpointContents {
602        let digests: Vec<_> = self.transactions.iter().map(|tx| tx.digests()).collect();
603        CheckpointContents::new_with_digests_and_signatures(digests, self.user_signatures)
604    }
605
606    pub fn size(&self) -> usize {
607        self.transactions.len()
608    }
609
610    pub fn random_for_testing() -> Self {
611        let (a, key): (_, AccountKeyPair) = get_key_pair();
612        let transaction = TransactionEnvelope::from_data_and_signer(
613            Transaction::new_transfer(
614                a,
615                random_object_ref(),
616                a,
617                random_object_ref(),
618                100000000000,
619                100,
620            ),
621            vec![&key],
622        );
623        let effects = TestEffectsBuilder::new(transaction.data()).build();
624        let exe_data = ExecutionData {
625            transaction,
626            effects,
627        };
628        FullCheckpointContents::new_with_causally_ordered_transactions(vec![exe_data])
629    }
630}
631
632impl IntoIterator for FullCheckpointContents {
633    type Item = ExecutionData;
634    type IntoIter = std::vec::IntoIter<Self::Item>;
635
636    fn into_iter(self) -> Self::IntoIter {
637        self.transactions.into_iter()
638    }
639}
640
641#[derive(Clone, Debug, PartialEq, Eq)]
642pub struct VerifiedCheckpointContents {
643    transactions: Vec<VerifiedExecutionData>,
644    /// This field 'pins' user signatures for the checkpoint
645    /// The length of this vector is same as length of transactions vector
646    /// System transactions has empty signatures
647    user_signatures: Vec<Vec<UserSignature>>,
648}
649
650impl VerifiedCheckpointContents {
651    pub fn new_unchecked(contents: FullCheckpointContents) -> Self {
652        Self {
653            transactions: contents
654                .transactions
655                .into_iter()
656                .map(VerifiedExecutionData::new_unchecked)
657                .collect(),
658            user_signatures: contents.user_signatures,
659        }
660    }
661
662    pub fn iter(&self) -> Iter<'_, VerifiedExecutionData> {
663        self.transactions.iter()
664    }
665
666    pub fn transactions(&self) -> &[VerifiedExecutionData] {
667        &self.transactions
668    }
669
670    pub fn into_inner(self) -> FullCheckpointContents {
671        FullCheckpointContents {
672            transactions: self
673                .transactions
674                .into_iter()
675                .map(|tx| tx.into_inner())
676                .collect(),
677            user_signatures: self.user_signatures,
678        }
679    }
680
681    pub fn into_checkpoint_contents(self) -> CheckpointContents {
682        self.into_inner().into_checkpoint_contents()
683    }
684
685    pub fn into_checkpoint_contents_digest(self) -> CheckpointContentsDigest {
686        self.into_inner().into_checkpoint_contents().digest()
687    }
688
689    pub fn num_of_transactions(&self) -> usize {
690        self.transactions.len()
691    }
692}
693
694/// Holds data in CheckpointSummary that is serialized into the
695/// `version_specific_data` field.
696#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
697pub enum CheckpointVersionSpecificData {
698    V1(CheckpointVersionSpecificDataV1),
699}
700
701impl CheckpointVersionSpecificData {
702    pub fn as_v1(&self) -> &CheckpointVersionSpecificDataV1 {
703        match self {
704            Self::V1(v) => v,
705        }
706    }
707
708    pub fn into_v1(self) -> CheckpointVersionSpecificDataV1 {
709        match self {
710            Self::V1(v) => v,
711        }
712    }
713
714    pub fn empty_for_tests() -> CheckpointVersionSpecificData {
715        CheckpointVersionSpecificData::V1(CheckpointVersionSpecificDataV1 {
716            randomness_rounds: Vec::new(),
717        })
718    }
719}
720
721#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
722pub struct CheckpointVersionSpecificDataV1 {
723    /// Lists the rounds for which RandomnessStateUpdate transactions are
724    /// present in the checkpoint.
725    pub randomness_rounds: Vec<RandomnessRound>,
726}
727
728#[cfg(test)]
729mod tests {
730    use fastcrypto::traits::KeyPair;
731    use iota_sdk_types::{ConsensusCommitDigest, TransactionDigest, TransactionEffectsDigest};
732    use rand::{SeedableRng, prelude::StdRng};
733
734    use super::*;
735    use crate::{transaction::VerifiedTransaction, utils::make_committee_key};
736
737    // TODO use the file name as a seed
738    const RNG_SEED: [u8; 32] = [
739        21, 23, 199, 200, 234, 250, 252, 178, 94, 15, 202, 178, 62, 186, 88, 137, 233, 192, 130,
740        157, 179, 179, 65, 9, 31, 249, 221, 123, 225, 112, 199, 247,
741    ];
742
743    #[test]
744    fn test_signed_checkpoint() {
745        let mut rng = StdRng::from_seed(RNG_SEED);
746        let (keys, committee) = make_committee_key(&mut rng);
747        let (_, committee2) = make_committee_key(&mut rng);
748
749        let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
750
751        // TODO: duplicated in a test below.
752
753        let signed_checkpoints: Vec<_> = keys
754            .iter()
755            .map(|k| {
756                let name = k.public().into();
757
758                SignedCheckpointSummary::new(
759                    committee.epoch,
760                    CheckpointSummary::new_with_protocol_config(
761                        &ProtocolConfig::get_for_max_version_UNSAFE(),
762                        committee.epoch,
763                        1,
764                        0,
765                        &set,
766                        None,
767                        GasCostSummary::default(),
768                        None,
769                        0,
770                        Vec::new(),
771                    ),
772                    k,
773                    name,
774                )
775            })
776            .collect();
777
778        signed_checkpoints.iter().for_each(|c| {
779            c.verify_authority_signatures(&committee)
780                .expect("signature ok")
781        });
782
783        // fails when not signed by member of committee
784        signed_checkpoints
785            .iter()
786            .for_each(|c| assert!(c.verify_authority_signatures(&committee2).is_err()));
787    }
788
789    #[test]
790    fn test_certified_checkpoint() {
791        let mut rng = StdRng::from_seed(RNG_SEED);
792        let (keys, committee) = make_committee_key(&mut rng);
793
794        let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
795
796        let summary = CheckpointSummary::new_with_protocol_config(
797            &ProtocolConfig::get_for_max_version_UNSAFE(),
798            committee.epoch,
799            1,
800            0,
801            &set,
802            None,
803            GasCostSummary::default(),
804            None,
805            0,
806            Vec::new(),
807        );
808
809        let sign_infos: Vec<_> = keys
810            .iter()
811            .map(|k| {
812                let name = k.public().into();
813
814                SignedCheckpointSummary::sign(committee.epoch, &summary, k, name)
815            })
816            .collect();
817
818        let checkpoint_cert =
819            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("Cert is OK");
820
821        // Signature is correct on proposal, and with same transactions
822        assert!(
823            checkpoint_cert
824                .verify_with_contents(&committee, Some(&set))
825                .is_ok()
826        );
827
828        // Make a bad proposal
829        let signed_checkpoints: Vec<_> = keys
830            .iter()
831            .map(|k| {
832                let name = k.public().into();
833                let set = CheckpointContents::new_with_digests_only_for_tests([
834                    ExecutionDigests::random(),
835                ]);
836
837                SignedCheckpointSummary::new(
838                    committee.epoch,
839                    CheckpointSummary::new_with_protocol_config(
840                        &ProtocolConfig::get_for_max_version_UNSAFE(),
841                        committee.epoch,
842                        1,
843                        0,
844                        &set,
845                        None,
846                        GasCostSummary::default(),
847                        None,
848                        0,
849                        Vec::new(),
850                    ),
851                    k,
852                    name,
853                )
854            })
855            .collect();
856
857        let summary = signed_checkpoints[0].data().clone();
858        let sign_infos = signed_checkpoints
859            .into_iter()
860            .map(|v| v.into_sig())
861            .collect();
862        assert!(
863            CertifiedCheckpointSummary::new(summary, sign_infos, &committee)
864                .unwrap()
865                .verify_authority_signatures(&committee)
866                .is_err()
867        )
868    }
869
870    #[test]
871    fn test_batch_verify_certified_checkpoints() {
872        let mut rng = StdRng::from_seed(RNG_SEED);
873        let (keys, committee) = make_committee_key(&mut rng);
874
875        let summary_for_sequence_number = |sequence_number| {
876            let set =
877                CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
878            CheckpointSummary::new_with_protocol_config(
879                &ProtocolConfig::get_for_max_version_UNSAFE(),
880                committee.epoch,
881                sequence_number,
882                0,
883                &set,
884                None,
885                GasCostSummary::default(),
886                None,
887                0,
888                Vec::new(),
889            )
890        };
891        let certify = |summary: CheckpointSummary| {
892            let sign_infos = keys
893                .iter()
894                .map(|k| {
895                    SignedCheckpointSummary::sign(committee.epoch, &summary, k, k.public().into())
896                })
897                .collect();
898            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
899        };
900
901        let summaries: Vec<_> = (1..=3)
902            .map(|seq| certify(summary_for_sequence_number(seq)))
903            .collect();
904        CertifiedCheckpointSummary::batch_verify_authority_signatures(&summaries, &committee)
905            .expect("signatures ok");
906
907        // A certificate whose signatures were made over a different summary
908        // fails the batch.
909        let mismatched = {
910            let summary = summary_for_sequence_number(4);
911            let sign_infos = keys
912                .iter()
913                .map(|k| {
914                    SignedCheckpointSummary::sign(
915                        committee.epoch,
916                        &summary_for_sequence_number(4),
917                        k,
918                        k.public().into(),
919                    )
920                })
921                .collect();
922            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
923        };
924        let with_mismatched: Vec<_> = summaries.into_iter().chain([mismatched]).collect();
925        assert!(
926            CertifiedCheckpointSummary::batch_verify_authority_signatures(
927                &with_mismatched,
928                &committee
929            )
930            .is_err()
931        );
932    }
933
934    // Generate a CheckpointSummary from the input transaction digest. All the other
935    // fields in the generated CheckpointSummary will be the same. The generated
936    // CheckpointSummary can be used to test how input transaction digest
937    // affects CheckpointSummary.
938    fn generate_test_checkpoint_summary_from_digest(
939        digest: TransactionDigest,
940    ) -> CheckpointSummary {
941        CheckpointSummary::new_with_protocol_config(
942            &ProtocolConfig::get_for_max_version_UNSAFE(),
943            1,
944            2,
945            10,
946            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::new(
947                digest,
948                TransactionEffectsDigest::ZERO,
949            )]),
950            None,
951            GasCostSummary::default(),
952            None,
953            100,
954            Vec::new(),
955        )
956    }
957
958    // Tests that ConsensusCommitPrologue with different consensus commit digest
959    // will result in different checkpoint content.
960    #[test]
961    fn test_checkpoint_summary_with_different_consensus_digest() {
962        // First, tests that same consensus commit digest will produce the same
963        // checkpoint content.
964        {
965            let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
966                1,
967                2,
968                100,
969                ConsensusCommitDigest::default(),
970                Vec::new(),
971            );
972            let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
973                1,
974                2,
975                100,
976                ConsensusCommitDigest::default(),
977                Vec::new(),
978            );
979            let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
980            let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
981            assert_eq!(c1.digest(), c2.digest());
982        }
983
984        // Next, tests that different consensus commit digests will produce the
985        // different checkpoint contents.
986        {
987            let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
988                1,
989                2,
990                100,
991                ConsensusCommitDigest::default(),
992                Vec::new(),
993            );
994            let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
995                1,
996                2,
997                100,
998                ConsensusCommitDigest::random(),
999                Vec::new(),
1000            );
1001            let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
1002            let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
1003            assert_ne!(c1.digest(), c2.digest());
1004        }
1005    }
1006}