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