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 once_cell::sync::OnceCell;
15use prometheus::Histogram;
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use serde_with::serde_as;
19use shared_crypto::intent::{Intent, IntentScope};
20use tap::TapFallible;
21use tracing::warn;
22
23pub use crate::digests::{CheckpointContentsDigest, CheckpointDigest};
24use crate::{
25    accumulator::Accumulator,
26    base_types::{
27        AuthorityName, ExecutionData, ExecutionDigests, VerifiedExecutionData, random_object_ref,
28    },
29    committee::{Committee, EpochId, ProtocolVersion, StakeUnit},
30    crypto::{
31        AccountKeyPair, AggregateAuthoritySignature, AuthoritySignInfo, AuthoritySignInfoTrait,
32        AuthorityStrongQuorumSignInfo, RandomnessRound, default_hash, get_key_pair,
33    },
34    digests::Digest,
35    effects::{TestEffectsBuilder, TransactionEffectsAPI},
36    error::{IotaError, IotaResult},
37    gas::GasCostSummary,
38    iota_serde::{AsProtocolVersion, BigInt, Readable},
39    message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelope},
40    signature::GenericSignature,
41    storage::ReadStore,
42    transaction::{Transaction, TransactionData},
43};
44
45pub type CheckpointSequenceNumber = u64;
46pub type CheckpointTimestamp = u64;
47
48#[derive(Clone, Debug, Serialize, Deserialize)]
49pub struct CheckpointRequest {
50    /// if a sequence number is specified, return the checkpoint with that
51    /// sequence number; otherwise if None returns the latest checkpoint
52    /// stored (authenticated or pending, depending on the value of
53    /// `certified` flag)
54    pub sequence_number: Option<CheckpointSequenceNumber>,
55    // A flag, if true also return the contents of the
56    // checkpoint besides the meta-data.
57    pub request_content: bool,
58    // If true, returns certified checkpoint, otherwise returns pending checkpoint
59    pub certified: bool,
60}
61
62#[expect(clippy::large_enum_variant)]
63#[derive(Clone, Debug, Serialize, Deserialize)]
64pub enum CheckpointSummaryResponse {
65    Certified(CertifiedCheckpointSummary),
66    Pending(CheckpointSummary),
67}
68
69impl CheckpointSummaryResponse {
70    pub fn content_digest(&self) -> CheckpointContentsDigest {
71        match self {
72            Self::Certified(s) => s.content_digest,
73            Self::Pending(s) => s.content_digest,
74        }
75    }
76}
77
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct CheckpointResponse {
80    pub checkpoint: Option<CheckpointSummaryResponse>,
81    pub contents: Option<CheckpointContents>,
82}
83
84// The constituent parts of checkpoints, signed and certified
85
86/// The Sha256 digest of an EllipticCurveMultisetHash committing to the live
87/// object set.
88#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
89pub struct ECMHLiveObjectSetDigest {
90    #[schemars(with = "[u8; 32]")]
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        Accumulator::default().digest().into()
105    }
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
109pub enum CheckpointCommitment {
110    ECMHLiveObjectSetDigest(ECMHLiveObjectSetDigest),
111    // Other commitment types (e.g. merkle roots) go here.
112}
113
114impl From<ECMHLiveObjectSetDigest> for CheckpointCommitment {
115    fn from(d: ECMHLiveObjectSetDigest) -> Self {
116        Self::ECMHLiveObjectSetDigest(d)
117    }
118}
119
120#[serde_as]
121#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
122#[serde(rename_all = "camelCase")]
123pub struct EndOfEpochData {
124    /// next_epoch_committee is `Some` if and only if the current checkpoint is
125    /// the last checkpoint of an epoch.
126    /// Therefore next_epoch_committee can be used to pick the last checkpoint
127    /// of an epoch, which is often useful to get epoch level summary stats
128    /// like total gas cost of an epoch, or the total number of transactions
129    /// from genesis to the end of an epoch. The committee is stored as a
130    /// vector of validator pub key and stake pairs. The vector
131    /// should be sorted based on the Committee data structure.
132    #[schemars(with = "Vec<(AuthorityName, BigInt<u64>)>")]
133    #[serde_as(as = "Vec<(_, Readable<BigInt<u64>, _>)>")]
134    pub next_epoch_committee: Vec<(AuthorityName, StakeUnit)>,
135
136    /// The protocol version that is in effect during the epoch that starts
137    /// immediately after this checkpoint.
138    #[schemars(with = "AsProtocolVersion")]
139    #[serde_as(as = "Readable<AsProtocolVersion, _>")]
140    pub next_epoch_protocol_version: ProtocolVersion,
141
142    /// Commitments to epoch specific state (e.g. live object set)
143    pub epoch_commitments: Vec<CheckpointCommitment>,
144
145    /// The number of tokens that were minted (if positive) or burnt (if
146    /// negative) in this epoch.
147    pub epoch_supply_change: i64,
148}
149
150#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
151pub struct CheckpointSummary {
152    pub epoch: EpochId,
153    pub sequence_number: CheckpointSequenceNumber,
154    /// Total number of transactions committed since genesis, including those in
155    /// this checkpoint.
156    pub network_total_transactions: u64,
157    pub content_digest: CheckpointContentsDigest,
158    pub previous_digest: Option<CheckpointDigest>,
159    /// The running total gas costs of all transactions included in the current
160    /// epoch so far until this checkpoint.
161    pub epoch_rolling_gas_cost_summary: GasCostSummary,
162
163    /// Timestamp of the checkpoint - number of milliseconds from the Unix epoch
164    /// Checkpoint timestamps are monotonic, but not strongly monotonic -
165    /// subsequent checkpoints can have same timestamp if they originate
166    /// from the same underlining consensus commit
167    pub timestamp_ms: CheckpointTimestamp,
168
169    /// Commitments to checkpoint-specific state (e.g. txns in checkpoint,
170    /// objects read/written in checkpoint).
171    pub checkpoint_commitments: Vec<CheckpointCommitment>,
172
173    /// Present only on the final checkpoint of the epoch.
174    pub end_of_epoch_data: Option<EndOfEpochData>,
175
176    /// CheckpointSummary is not an evolvable structure - it must be readable by
177    /// any version of the code. Therefore, in order to allow extensions to
178    /// be added to CheckpointSummary, we allow opaque data to be added to
179    /// checkpoints which can be deserialized based on the current
180    /// protocol version.
181    ///
182    /// This is implemented with BCS-serialized `CheckpointVersionSpecificData`.
183    pub version_specific_data: Vec<u8>,
184}
185
186impl Message for CheckpointSummary {
187    type DigestType = CheckpointDigest;
188    const SCOPE: IntentScope = IntentScope::CheckpointSummary;
189
190    fn digest(&self) -> Self::DigestType {
191        CheckpointDigest::new(default_hash(self))
192    }
193}
194
195impl CheckpointSummary {
196    pub fn new(
197        protocol_config: &ProtocolConfig,
198        epoch: EpochId,
199        sequence_number: CheckpointSequenceNumber,
200        network_total_transactions: u64,
201        transactions: &CheckpointContents,
202        previous_digest: Option<CheckpointDigest>,
203        epoch_rolling_gas_cost_summary: GasCostSummary,
204        end_of_epoch_data: Option<EndOfEpochData>,
205        timestamp_ms: CheckpointTimestamp,
206        randomness_rounds: Vec<RandomnessRound>,
207    ) -> CheckpointSummary {
208        let content_digest = *transactions.digest();
209
210        let version_specific_data = match protocol_config
211            .checkpoint_summary_version_specific_data_as_option()
212        {
213            None | Some(0) => Vec::new(),
214            Some(1) => bcs::to_bytes(&CheckpointVersionSpecificData::V1(
215                CheckpointVersionSpecificDataV1 { randomness_rounds },
216            ))
217            .expect("version specific data should serialize"),
218            _ => unimplemented!("unrecognized version_specific_data version for CheckpointSummary"),
219        };
220
221        Self {
222            epoch,
223            sequence_number,
224            network_total_transactions,
225            content_digest,
226            previous_digest,
227            epoch_rolling_gas_cost_summary,
228            end_of_epoch_data,
229            timestamp_ms,
230            version_specific_data,
231            checkpoint_commitments: Default::default(),
232        }
233    }
234
235    pub fn verify_epoch(&self, epoch: EpochId) -> IotaResult {
236        fp_ensure!(
237            self.epoch == epoch,
238            IotaError::WrongEpoch {
239                expected_epoch: epoch,
240                actual_epoch: self.epoch,
241            }
242        );
243        Ok(())
244    }
245
246    pub fn sequence_number(&self) -> &CheckpointSequenceNumber {
247        &self.sequence_number
248    }
249
250    pub fn timestamp(&self) -> SystemTime {
251        UNIX_EPOCH + Duration::from_millis(self.timestamp_ms)
252    }
253
254    pub fn next_epoch_committee(&self) -> Option<&[(AuthorityName, StakeUnit)]> {
255        self.end_of_epoch_data
256            .as_ref()
257            .map(|e| e.next_epoch_committee.as_slice())
258    }
259
260    pub fn report_checkpoint_age(&self, metrics: &Histogram) {
261        SystemTime::now()
262            .duration_since(self.timestamp())
263            .map(|latency| {
264                metrics.observe(latency.as_secs_f64());
265            })
266            .tap_err(|err| {
267                warn!(
268                    checkpoint_seq = self.sequence_number,
269                    "unable to compute checkpoint age: {}", err
270                )
271            })
272            .ok();
273    }
274
275    pub fn is_last_checkpoint_of_epoch(&self) -> bool {
276        self.end_of_epoch_data.is_some()
277    }
278
279    pub fn version_specific_data(
280        &self,
281        config: &ProtocolConfig,
282    ) -> Result<Option<CheckpointVersionSpecificData>> {
283        match config.checkpoint_summary_version_specific_data_as_option() {
284            None | Some(0) => Ok(None),
285            Some(1) => Ok(Some(bcs::from_bytes(&self.version_specific_data)?)),
286            _ => unimplemented!("unrecognized version_specific_data version in CheckpointSummary"),
287        }
288    }
289}
290
291impl Display for CheckpointSummary {
292    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
293        write!(
294            f,
295            "CheckpointSummary {{ epoch: {:?}, seq: {:?}, content_digest: {},
296            epoch_rolling_gas_cost_summary: {:?}}}",
297            self.epoch,
298            self.sequence_number,
299            self.content_digest,
300            self.epoch_rolling_gas_cost_summary,
301        )
302    }
303}
304
305// Checkpoints are signed by an authority and 2f+1 form a
306// certificate that others can use to catch up. The actual
307// content of the digest must at the very least commit to
308// the set of transactions contained in the certificate but
309// we might extend this to contain roots of merkle trees,
310// or other authenticated data structures to support light
311// clients and more efficient sync protocols.
312
313pub type CheckpointSummaryEnvelope<S> = Envelope<CheckpointSummary, S>;
314pub type CertifiedCheckpointSummary = CheckpointSummaryEnvelope<AuthorityStrongQuorumSignInfo>;
315pub type SignedCheckpointSummary = CheckpointSummaryEnvelope<AuthoritySignInfo>;
316
317pub type VerifiedCheckpoint = VerifiedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
318pub type TrustedCheckpoint = TrustedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
319
320impl CertifiedCheckpointSummary {
321    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
322        self.data().verify_epoch(self.auth_sig().epoch)?;
323        self.auth_sig().verify_secure(
324            self.data(),
325            Intent::iota_app(IntentScope::CheckpointSummary),
326            committee,
327        )
328    }
329
330    pub fn try_into_verified(self, committee: &Committee) -> IotaResult<VerifiedCheckpoint> {
331        self.verify_authority_signatures(committee)?;
332        Ok(VerifiedCheckpoint::new_from_verified(self))
333    }
334
335    pub fn verify_with_contents(
336        &self,
337        committee: &Committee,
338        contents: Option<&CheckpointContents>,
339    ) -> IotaResult {
340        self.verify_authority_signatures(committee)?;
341
342        if let Some(contents) = contents {
343            let content_digest = *contents.digest();
344            fp_ensure!(
345                content_digest == self.data().content_digest,
346                IotaError::GenericAuthority {
347                    error: format!(
348                        "Checkpoint contents digest mismatch: summary={:?}, received content digest {:?}, received {} transactions",
349                        self.data(),
350                        content_digest,
351                        contents.size()
352                    )
353                }
354            );
355        }
356
357        Ok(())
358    }
359
360    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
361        let summary = self.into_data();
362        (summary.sequence_number, summary)
363    }
364
365    pub fn get_validator_signature(self) -> AggregateAuthoritySignature {
366        self.auth_sig().signature.clone()
367    }
368}
369
370impl SignedCheckpointSummary {
371    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
372        self.data().verify_epoch(self.auth_sig().epoch)?;
373        self.auth_sig().verify_secure(
374            self.data(),
375            Intent::iota_app(IntentScope::CheckpointSummary),
376            committee,
377        )
378    }
379
380    pub fn try_into_verified(
381        self,
382        committee: &Committee,
383    ) -> IotaResult<VerifiedEnvelope<CheckpointSummary, AuthoritySignInfo>> {
384        self.verify_authority_signatures(committee)?;
385        Ok(VerifiedEnvelope::<CheckpointSummary, AuthoritySignInfo>::new_from_verified(self))
386    }
387}
388
389impl VerifiedCheckpoint {
390    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
391        self.into_inner().into_summary_and_sequence()
392    }
393}
394
395/// This is a message validators publish to consensus in order to sign
396/// checkpoint
397#[derive(Clone, Debug, Serialize, Deserialize)]
398pub struct CheckpointSignatureMessage {
399    pub summary: SignedCheckpointSummary,
400}
401
402impl CheckpointSignatureMessage {
403    pub fn verify(&self, committee: &Committee) -> IotaResult {
404        self.summary.verify_authority_signatures(committee)
405    }
406}
407
408#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
409pub enum CheckpointContents {
410    V1(CheckpointContentsV1),
411}
412
413/// CheckpointContents are the transactions included in an upcoming checkpoint.
414/// They must have already been causally ordered. Since the causal order
415/// algorithm is the same among validators, we expect all honest validators to
416/// come up with the same order for each checkpoint content.
417#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
418pub struct CheckpointContentsV1 {
419    #[serde(skip)]
420    digest: OnceCell<CheckpointContentsDigest>,
421
422    transactions: Vec<ExecutionDigests>,
423    /// This field 'pins' user signatures for the checkpoint
424    /// The length of this vector is same as length of transactions vector
425    /// System transactions has empty signatures
426    user_signatures: Vec<Vec<GenericSignature>>,
427}
428
429impl CheckpointContents {
430    pub fn new_with_digests_and_signatures(
431        contents: impl IntoIterator<Item = ExecutionDigests>,
432        user_signatures: Vec<Vec<GenericSignature>>,
433    ) -> Self {
434        let transactions: Vec<_> = contents.into_iter().collect();
435        assert_eq!(transactions.len(), user_signatures.len());
436        Self::V1(CheckpointContentsV1 {
437            digest: Default::default(),
438            transactions,
439            user_signatures,
440        })
441    }
442
443    pub fn new_with_causally_ordered_execution_data<'a>(
444        contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
445    ) -> Self {
446        let (transactions, user_signatures): (Vec<_>, Vec<_>) = contents
447            .into_iter()
448            .map(|data| {
449                (
450                    data.digests(),
451                    data.transaction.inner().data().tx_signatures().to_owned(),
452                )
453            })
454            .unzip();
455        assert_eq!(transactions.len(), user_signatures.len());
456        Self::V1(CheckpointContentsV1 {
457            digest: Default::default(),
458            transactions,
459            user_signatures,
460        })
461    }
462
463    pub fn new_with_digests_only_for_tests(
464        contents: impl IntoIterator<Item = ExecutionDigests>,
465    ) -> Self {
466        let transactions: Vec<_> = contents.into_iter().collect();
467        let user_signatures = transactions.iter().map(|_| vec![]).collect();
468        Self::V1(CheckpointContentsV1 {
469            digest: Default::default(),
470            transactions,
471            user_signatures,
472        })
473    }
474
475    fn as_v1(&self) -> &CheckpointContentsV1 {
476        match self {
477            Self::V1(v) => v,
478        }
479    }
480
481    fn into_v1(self) -> CheckpointContentsV1 {
482        match self {
483            Self::V1(v) => v,
484        }
485    }
486
487    pub fn iter(&self) -> Iter<'_, ExecutionDigests> {
488        self.as_v1().transactions.iter()
489    }
490
491    pub fn into_iter_with_signatures(
492        self,
493    ) -> impl Iterator<Item = (ExecutionDigests, Vec<GenericSignature>)> {
494        let CheckpointContentsV1 {
495            transactions,
496            user_signatures,
497            ..
498        } = self.into_v1();
499
500        transactions.into_iter().zip(user_signatures)
501    }
502
503    /// Return an iterator that enumerates the transactions in the contents.
504    /// The iterator item is a tuple of (sequence_number, &ExecutionDigests),
505    /// where the sequence_number indicates the index of the transaction in the
506    /// global ordering of executed transactions since genesis.
507    pub fn enumerate_transactions(
508        &self,
509        ckpt: &CheckpointSummary,
510    ) -> impl Iterator<Item = (u64, &ExecutionDigests)> {
511        let start = ckpt.network_total_transactions - self.size() as u64;
512
513        (0u64..)
514            .zip(self.iter())
515            .map(move |(i, digests)| (i + start, digests))
516    }
517
518    pub fn into_inner(self) -> Vec<ExecutionDigests> {
519        self.into_v1().transactions
520    }
521
522    pub fn inner(&self) -> &[ExecutionDigests] {
523        &self.as_v1().transactions
524    }
525
526    pub fn size(&self) -> usize {
527        self.as_v1().transactions.len()
528    }
529
530    pub fn digest(&self) -> &CheckpointContentsDigest {
531        self.as_v1()
532            .digest
533            .get_or_init(|| CheckpointContentsDigest::new(default_hash(self)))
534    }
535}
536
537/// Same as CheckpointContents, but contains full contents of all Transactions
538/// and TransactionEffects associated with the checkpoint.
539// NOTE: This data structure is used for state sync of checkpoints. Therefore we attempt
540// to estimate its size in CheckpointBuilder in order to limit the maximum serialized
541// size of a checkpoint sent over the network. If this struct is modified,
542// CheckpointBuilder::split_checkpoint_chunks should also be updated accordingly.
543#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
544pub struct FullCheckpointContents {
545    transactions: Vec<ExecutionData>,
546    /// This field 'pins' user signatures for the checkpoint
547    /// The length of this vector is same as length of transactions vector
548    /// System transactions has empty signatures
549    user_signatures: Vec<Vec<GenericSignature>>,
550}
551
552impl FullCheckpointContents {
553    pub fn new_with_causally_ordered_transactions<T>(contents: T) -> Self
554    where
555        T: IntoIterator<Item = ExecutionData>,
556    {
557        let (transactions, user_signatures): (Vec<_>, Vec<_>) = contents
558            .into_iter()
559            .map(|data| {
560                let sig = data.transaction.data().tx_signatures().to_owned();
561                (data, sig)
562            })
563            .unzip();
564        assert_eq!(transactions.len(), user_signatures.len());
565        Self {
566            transactions,
567            user_signatures,
568        }
569    }
570    pub fn from_contents_and_execution_data(
571        contents: CheckpointContents,
572        execution_data: impl Iterator<Item = ExecutionData>,
573    ) -> Self {
574        let transactions: Vec<_> = execution_data.collect();
575        Self {
576            transactions,
577            user_signatures: contents.into_v1().user_signatures,
578        }
579    }
580    pub fn 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.get_transaction(&tx.transaction)?,
591                store.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}