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    CheckpointContents, CheckpointContentsDigest, CheckpointContentsV1, CheckpointDigest,
15    CheckpointSummary, CheckpointTransactionInfo, Digest, EndOfEpochData, GasCostSummary,
16    RandomnessRound, Transaction,
17    crypto::{Intent, IntentScope, UserSignature},
18};
19#[cfg(not(target_arch = "wasm32"))]
20use prometheus_filtered::Histogram;
21use serde::{Deserialize, Serialize};
22#[cfg(not(target_arch = "wasm32"))]
23use tap::TapFallible;
24use tracing::instrument;
25#[cfg(not(target_arch = "wasm32"))]
26use tracing::warn;
27
28use crate::{
29    base_types::{ExecutionData, ExecutionDigests, VerifiedExecutionData, random_object_ref},
30    committee::{Committee, EpochId},
31    crypto::{
32        AccountPrivateKey, AggregateAuthoritySignature, AuthoritySignInfo, AuthoritySignInfoTrait,
33        AuthorityStrongQuorumSignInfo, VerificationObligation, default_hash, get_key_pair,
34        zero_ed25519_signature,
35    },
36    effects::{TestEffectsBuilder, TransactionEffectsAPI},
37    error::{IotaError, IotaResult},
38    global_state_hash::GlobalStateHash,
39    message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelope},
40    storage::ReadStore,
41    transaction::{TransactionAPI, TransactionEnvelope},
42};
43
44pub type CheckpointSequenceNumber = u64;
45pub type CheckpointTimestamp = u64;
46
47#[derive(Clone, Debug, Serialize, Deserialize)]
48pub struct CheckpointRequest {
49    /// 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 contents_digest(&self) -> CheckpointContentsDigest {
70        match self {
71            Self::Certified(s) => s.contents_digest,
72            Self::Pending(s) => s.contents_digest,
73        }
74    }
75}
76
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub struct CheckpointResponse {
79    pub checkpoint: Option<CheckpointSummaryResponse>,
80    pub contents: Option<CheckpointContents>,
81}
82
83// 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
106impl Message for CheckpointSummary {
107    type DigestType = CheckpointDigest;
108    const SCOPE: IntentScope = IntentScope::CheckpointSummary;
109
110    fn digest(&self) -> Self::DigestType {
111        CheckpointDigest::new(default_hash(self))
112    }
113}
114
115mod checkpoint_summary_ext {
116    pub trait Sealed {}
117    impl Sealed for super::CheckpointSummary {}
118}
119
120/// Node-only helpers for [`CheckpointSummary`], which is defined in
121/// `iota_sdk_types`. These live on an extension trait because inherent methods
122/// cannot be added to a type that is foreign to this crate.
123pub trait CheckpointSummaryExt: Sized + checkpoint_summary_ext::Sealed {
124    fn new_with_protocol_config(
125        protocol_config: &ProtocolConfig,
126        epoch: EpochId,
127        sequence_number: CheckpointSequenceNumber,
128        network_total_transactions: u64,
129        transactions: &CheckpointContents,
130        previous_digest: Option<CheckpointDigest>,
131        epoch_rolling_gas_cost_summary: GasCostSummary,
132        end_of_epoch_data: Option<EndOfEpochData>,
133        timestamp_ms: CheckpointTimestamp,
134        randomness_rounds: Vec<RandomnessRound>,
135    ) -> Self;
136
137    fn verify_epoch(&self, epoch: EpochId) -> IotaResult;
138
139    fn timestamp(&self) -> SystemTime;
140
141    #[cfg(not(target_arch = "wasm32"))]
142    fn report_checkpoint_age(&self, metrics: &Histogram);
143
144    fn parse_version_specific_data(
145        &self,
146        config: &ProtocolConfig,
147    ) -> Result<Option<CheckpointVersionSpecificData>>;
148}
149
150impl CheckpointSummaryExt for CheckpointSummary {
151    fn new_with_protocol_config(
152        protocol_config: &ProtocolConfig,
153        epoch: EpochId,
154        sequence_number: CheckpointSequenceNumber,
155        network_total_transactions: u64,
156        transactions: &CheckpointContents,
157        previous_digest: Option<CheckpointDigest>,
158        epoch_rolling_gas_cost_summary: GasCostSummary,
159        end_of_epoch_data: Option<EndOfEpochData>,
160        timestamp_ms: CheckpointTimestamp,
161        randomness_rounds: Vec<RandomnessRound>,
162    ) -> Self {
163        let contents_digest = transactions.digest();
164
165        let version_specific_data =
166            match protocol_config.checkpoint_summary_version_specific_data_as_option() {
167                None | Some(0) => Vec::new(),
168                Some(1) => bcs::to_bytes(&CheckpointVersionSpecificData::V1(
169                    CheckpointVersionSpecificDataV1 { randomness_rounds },
170                ))
171                .expect("version specific data should serialize"),
172                _ => unimplemented!(
173                    "unrecognized version_specific_data version for
174    CheckpointSummary"
175                ),
176            };
177
178        Self {
179            epoch,
180            sequence_number,
181            network_total_transactions,
182            contents_digest,
183            previous_digest,
184            epoch_rolling_gas_cost_summary,
185            end_of_epoch_data,
186            timestamp_ms,
187            version_specific_data,
188            checkpoint_commitments: Default::default(),
189        }
190    }
191
192    fn verify_epoch(&self, epoch: EpochId) -> IotaResult {
193        fp_ensure!(
194            self.epoch == epoch,
195            IotaError::WrongEpoch {
196                expected_epoch: epoch,
197                actual_epoch: self.epoch,
198            }
199        );
200        Ok(())
201    }
202
203    fn timestamp(&self) -> SystemTime {
204        UNIX_EPOCH + Duration::from_millis(self.timestamp_ms)
205    }
206
207    #[cfg(not(target_arch = "wasm32"))]
208    fn report_checkpoint_age(&self, metrics: &Histogram) {
209        SystemTime::now()
210            .duration_since(self.timestamp())
211            .map(|latency| {
212                metrics.observe(latency.as_secs_f64());
213            })
214            .tap_err(|err| {
215                warn!(
216                    checkpoint_seq = self.sequence_number,
217                    "unable to compute checkpoint age: {}", err
218                )
219            })
220            .ok();
221    }
222
223    fn parse_version_specific_data(
224        &self,
225        config: &ProtocolConfig,
226    ) -> Result<Option<CheckpointVersionSpecificData>> {
227        match config.checkpoint_summary_version_specific_data_as_option() {
228            None | Some(0) => Ok(None),
229            Some(1) => Ok(Some(bcs::from_bytes(&self.version_specific_data)?)),
230            _ => unimplemented!("unrecognized version_specific_data version in CheckpointSummary"),
231        }
232    }
233}
234
235// Checkpoints are signed by an authority and 2f+1 form a
236// certificate that others can use to catch up. The actual
237// content of the digest must at the very least commit to
238// the set of transactions contained in the certificate but
239// we might extend this to contain roots of merkle trees,
240// or other authenticated data structures to support light
241// clients and more efficient sync protocols.
242
243pub type CheckpointSummaryEnvelope<S> = Envelope<CheckpointSummary, S>;
244pub type CertifiedCheckpointSummary = CheckpointSummaryEnvelope<AuthorityStrongQuorumSignInfo>;
245pub type SignedCheckpointSummary = CheckpointSummaryEnvelope<AuthoritySignInfo>;
246
247pub type VerifiedCheckpoint = VerifiedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
248pub type TrustedCheckpoint = TrustedEnvelope<CheckpointSummary, AuthorityStrongQuorumSignInfo>;
249
250impl CertifiedCheckpointSummary {
251    #[instrument(level = "trace", skip_all)]
252    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
253        self.data().verify_epoch(self.auth_sig().epoch)?;
254        self.auth_sig().verify_secure(
255            self.data(),
256            Intent::iota_app(IntentScope::CheckpointSummary),
257            committee,
258        )
259    }
260
261    /// Verifies the authority signatures of `summaries`, all certified by
262    /// `committee`, in a single batched signature verification. Cheaper than
263    /// verifying each summary on its own, at the cost of not saying which
264    /// summary failed; callers that need that can fall back to
265    /// [`Self::verify_authority_signatures`] per summary.
266    #[instrument(level = "trace", skip_all)]
267    pub fn batch_verify_authority_signatures(
268        summaries: &[Self],
269        committee: &Committee,
270    ) -> IotaResult {
271        let mut obligation = VerificationObligation::default();
272        for summary in summaries {
273            summary.data().verify_epoch(summary.auth_sig().epoch)?;
274            let idx = obligation.add_message(
275                summary.data(),
276                summary.auth_sig().epoch,
277                Intent::iota_app(IntentScope::CheckpointSummary),
278            );
279            summary
280                .auth_sig()
281                .add_to_verification_obligation(committee, &mut obligation, idx)?;
282        }
283        obligation.verify_all()
284    }
285
286    pub fn try_into_verified(self, committee: &Committee) -> IotaResult<VerifiedCheckpoint> {
287        self.verify_authority_signatures(committee)?;
288        Ok(VerifiedCheckpoint::new_from_verified(self))
289    }
290
291    pub fn verify_with_contents(
292        &self,
293        committee: &Committee,
294        contents: Option<&CheckpointContents>,
295    ) -> IotaResult {
296        self.verify_authority_signatures(committee)?;
297
298        if let Some(contents) = contents {
299            let contents_digest = contents.digest();
300            fp_ensure!(
301                contents_digest == self.data().contents_digest,
302                IotaError::GenericAuthority {
303                    error: format!(
304                        "Checkpoint contents digest mismatch: summary={:?}, received content digest {:?}, received {} transactions",
305                        self.data(),
306                        contents_digest,
307                        contents.len()
308                    )
309                }
310            );
311        }
312
313        Ok(())
314    }
315
316    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
317        let summary = self.into_data();
318        (summary.sequence_number, summary)
319    }
320
321    pub fn get_validator_signature(self) -> AggregateAuthoritySignature {
322        self.auth_sig().signature.clone()
323    }
324}
325
326impl SignedCheckpointSummary {
327    #[instrument(level = "trace", skip_all)]
328    pub fn verify_authority_signatures(&self, committee: &Committee) -> IotaResult {
329        self.data().verify_epoch(self.auth_sig().epoch)?;
330        self.auth_sig().verify_secure(
331            self.data(),
332            Intent::iota_app(IntentScope::CheckpointSummary),
333            committee,
334        )
335    }
336
337    pub fn try_into_verified(
338        self,
339        committee: &Committee,
340    ) -> IotaResult<VerifiedEnvelope<CheckpointSummary, AuthoritySignInfo>> {
341        self.verify_authority_signatures(committee)?;
342        Ok(VerifiedEnvelope::<CheckpointSummary, AuthoritySignInfo>::new_from_verified(self))
343    }
344}
345
346impl VerifiedCheckpoint {
347    pub fn into_summary_and_sequence(self) -> (CheckpointSequenceNumber, CheckpointSummary) {
348        self.into_inner().into_summary_and_sequence()
349    }
350}
351
352/// This is a message validators publish to consensus in order to sign
353/// checkpoint
354#[derive(Clone, Debug, Serialize, Deserialize)]
355pub struct CheckpointSignatureMessage {
356    pub summary: SignedCheckpointSummary,
357}
358
359impl CheckpointSignatureMessage {
360    pub fn verify(&self, committee: &Committee) -> IotaResult {
361        self.summary.verify_authority_signatures(committee)
362    }
363}
364
365fn execution_digests(info: &CheckpointTransactionInfo) -> ExecutionDigests {
366    ExecutionDigests {
367        transaction: info.transaction,
368        effects: info.effects,
369    }
370}
371
372mod checkpoint_contents_ext {
373    pub trait Sealed {}
374    impl Sealed for super::CheckpointContents {}
375}
376
377/// Node-only helpers for [`CheckpointContents`], which is defined in
378/// `iota_sdk_types`. They bridge the node's `ExecutionDigests` representation
379/// to the SDK type's parallel [`CheckpointTransactionInfo`] form.
380pub trait CheckpointContentsExt: Sized + checkpoint_contents_ext::Sealed {
381    fn new_with_digests_and_signatures(
382        contents: impl IntoIterator<Item = ExecutionDigests>,
383        user_signatures: Vec<Vec<UserSignature>>,
384    ) -> Self;
385
386    fn new_with_causally_ordered_execution_data<'a>(
387        contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
388    ) -> Self;
389
390    fn new_with_digests_only_for_tests(
391        contents: impl IntoIterator<Item = ExecutionDigests>,
392    ) -> Self;
393
394    fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_;
395
396    fn into_iter_with_signatures(
397        self,
398    ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)>;
399
400    /// Enumerate the transactions in the contents, pairing each with its index
401    /// in the global ordering of executed transactions since genesis.
402    fn enumerate_transactions(
403        &self,
404        ckpt: &CheckpointSummary,
405    ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_;
406}
407
408impl CheckpointContentsExt for CheckpointContents {
409    fn new_with_digests_and_signatures(
410        contents: impl IntoIterator<Item = ExecutionDigests>,
411        user_signatures: Vec<Vec<UserSignature>>,
412    ) -> Self {
413        let transactions: Vec<_> = contents.into_iter().collect();
414        assert_eq!(transactions.len(), user_signatures.len());
415        Self::new_v1(CheckpointContentsV1::new(
416            transactions
417                .into_iter()
418                .zip(user_signatures)
419                .map(|(digests, signatures)| CheckpointTransactionInfo {
420                    transaction: digests.transaction,
421                    effects: digests.effects,
422                    signatures,
423                })
424                .collect(),
425        ))
426    }
427
428    fn new_with_causally_ordered_execution_data<'a>(
429        contents: impl IntoIterator<Item = &'a VerifiedExecutionData>,
430    ) -> Self {
431        Self::new_v1(CheckpointContentsV1::new(
432            contents
433                .into_iter()
434                .map(|data| {
435                    let digests = data.digests();
436                    CheckpointTransactionInfo {
437                        transaction: digests.transaction,
438                        effects: digests.effects,
439                        signatures: data.transaction.inner().data().signatures().to_owned(),
440                    }
441                })
442                .collect(),
443        ))
444    }
445
446    fn new_with_digests_only_for_tests(
447        contents: impl IntoIterator<Item = ExecutionDigests>,
448    ) -> Self {
449        Self::new_v1(CheckpointContentsV1::new(
450            contents
451                .into_iter()
452                .map(|digests| CheckpointTransactionInfo {
453                    transaction: digests.transaction,
454                    effects: digests.effects,
455                    signatures: Vec::new(),
456                })
457                .collect(),
458        ))
459    }
460
461    fn iter(&self) -> impl DoubleEndedIterator<Item = ExecutionDigests> + ExactSizeIterator + '_ {
462        self.transactions().iter().map(execution_digests)
463    }
464
465    fn into_iter_with_signatures(
466        self,
467    ) -> impl Iterator<Item = (ExecutionDigests, Vec<UserSignature>)> {
468        match self {
469            CheckpointContents::V1(v1) => v1.into_transactions().into_iter().map(|info| {
470                let digests = execution_digests(&info);
471                (digests, info.signatures)
472            }),
473            _ => unimplemented!("a new CheckpointContents variant was added and must be handled"),
474        }
475    }
476
477    fn enumerate_transactions(
478        &self,
479        ckpt: &CheckpointSummary,
480    ) -> impl Iterator<Item = (u64, ExecutionDigests)> + '_ {
481        let start = ckpt.network_total_transactions - self.len() as u64;
482
483        (0u64..)
484            .zip(self.iter())
485            .map(move |(i, digests)| (i + start, digests))
486    }
487}
488
489/// Same as CheckpointContents, but contains full contents of all Transactions
490/// and TransactionEffects associated with the checkpoint.
491// NOTE: This data structure is used for state sync of checkpoints. Therefore we attempt
492// to estimate its size in CheckpointBuilder in order to limit the maximum serialized
493// size of a checkpoint sent over the network. If this struct is modified,
494// CheckpointBuilder::split_checkpoint_chunks should also be updated accordingly.
495#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(try_from = "RawFullCheckpointContents")]
497pub struct FullCheckpointContents {
498    transactions: Vec<ExecutionData>,
499    /// The signatures the checkpoint pins for each transaction, in the same
500    /// order and number as `transactions`. The checkpoint contents digest
501    /// covers them, unlike the signatures inside the transactions, and they
502    /// differ from those for system transactions: the ones the node builds
503    /// outside consensus, at genesis and at the end of an epoch, are pinned
504    /// with an empty set while their envelopes carry a zero placeholder
505    /// signature.
506    user_signatures: Vec<Vec<UserSignature>>,
507}
508
509/// The wire shape of [`FullCheckpointContents`]. Deserialization goes through
510/// it so that the two vectors are known to be the same length before a
511/// [`FullCheckpointContents`] value exists.
512#[derive(Deserialize)]
513struct RawFullCheckpointContents {
514    transactions: Vec<ExecutionData>,
515    user_signatures: Vec<Vec<UserSignature>>,
516}
517
518impl TryFrom<RawFullCheckpointContents> for FullCheckpointContents {
519    type Error = anyhow::Error;
520
521    fn try_from(raw: RawFullCheckpointContents) -> Result<Self> {
522        fp_ensure!(
523            raw.transactions.len() == raw.user_signatures.len(),
524            anyhow::anyhow!(
525                "checkpoint contents hold {} transactions but {} pinned signature sets",
526                raw.transactions.len(),
527                raw.user_signatures.len()
528            )
529        );
530        Ok(Self {
531            transactions: raw.transactions,
532            user_signatures: raw.user_signatures,
533        })
534    }
535}
536
537impl FullCheckpointContents {
538    pub fn new_with_causally_ordered_transactions<T>(contents: T) -> Self
539    where
540        T: IntoIterator<Item = ExecutionData>,
541    {
542        let (transactions, user_signatures): (Vec<_>, Vec<_>) = contents
543            .into_iter()
544            .map(|data| {
545                let sig = data.transaction.data().signatures().to_owned();
546                (data, sig)
547            })
548            .unzip();
549        assert_eq!(transactions.len(), user_signatures.len());
550        Self {
551            transactions,
552            user_signatures,
553        }
554    }
555
556    /// Pairs the signatures pinned in `contents` with `execution_data`.
557    ///
558    /// # Errors
559    ///
560    /// Returns an error when `execution_data` does not hold exactly one
561    /// transaction per entry of `contents`, so a caller assembling the two
562    /// from separately received parts gets a rejection rather than a panic.
563    pub fn from_contents_and_execution_data(
564        contents: CheckpointContents,
565        execution_data: impl Iterator<Item = ExecutionData>,
566    ) -> Result<Self> {
567        let transactions: Vec<_> = execution_data.collect();
568        let user_signatures: Vec<Vec<UserSignature>> = contents
569            .into_iter_with_signatures()
570            .map(|(_, signatures)| signatures)
571            .collect();
572        fp_ensure!(
573            transactions.len() == user_signatures.len(),
574            anyhow::anyhow!(
575                "checkpoint contents pin {} signature sets for {} transactions",
576                user_signatures.len(),
577                transactions.len()
578            )
579        );
580        Ok(Self {
581            transactions,
582            user_signatures,
583        })
584    }
585
586    pub fn try_from_checkpoint_contents<S>(
587        store: S,
588        contents: CheckpointContents,
589    ) -> Result<Option<Self>, crate::storage::error::Error>
590    where
591        S: ReadStore,
592    {
593        let (digests, user_signatures): (Vec<_>, Vec<_>) =
594            contents.into_iter_with_signatures().unzip();
595        let mut transactions = Vec::with_capacity(digests.len());
596        for tx in &digests {
597            if let (Some(t), Some(e)) = (
598                store.try_get_transaction(&tx.transaction)?,
599                store.try_get_transaction_effects(&tx.transaction)?,
600            ) {
601                transactions.push(ExecutionData::new((*t).clone().into_inner(), e))
602            } else {
603                return Ok(None);
604            }
605        }
606        Ok(Some(Self {
607            transactions,
608            user_signatures,
609        }))
610    }
611
612    pub fn iter(&self) -> Iter<'_, ExecutionData> {
613        self.transactions.iter()
614    }
615
616    /// Verifies that this checkpoint's digest matches the given digest, that
617    /// all internal Transaction and TransactionEffects digests are consistent,
618    /// and that every transaction carries the signatures the checkpoint pinned.
619    ///
620    /// The transactions and the signatures pinned for them are separate fields,
621    /// and the transaction digest does not cover the signatures, so a peer can
622    /// serve a transaction whose signatures differ from the ones the committee
623    /// agreed on without changing any digest. That matters for a transaction
624    /// authenticated by a `MoveAuthenticator`, whose payload decides what the
625    /// transaction executes.
626    pub fn verify_digests(&self, digest: CheckpointContentsDigest) -> Result<()> {
627        fp_ensure!(
628            self.transactions.len() == self.user_signatures.len(),
629            anyhow::anyhow!(
630                "checkpoint contents hold {} transactions but {} pinned signature sets",
631                self.transactions.len(),
632                self.user_signatures.len()
633            )
634        );
635
636        let self_digest = self.checkpoint_contents().digest();
637        fp_ensure!(
638            digest == self_digest,
639            anyhow::anyhow!(
640                "checkpoint contents digest {self_digest} does not match expected digest {digest}"
641            )
642        );
643        let placeholder_signature = [UserSignature::Simple(zero_ed25519_signature())];
644        for (tx, pinned_signatures) in self.transactions.iter().zip(self.user_signatures.iter()) {
645            let transaction_digest = tx.transaction.digest();
646            fp_ensure!(
647                tx.effects.transaction_digest() == transaction_digest,
648                anyhow::anyhow!(
649                    "transaction digest {transaction_digest} does not match expected digest {}",
650                    tx.effects.transaction_digest()
651                )
652            );
653            // Genesis and end-of-epoch transactions are the two the node builds
654            // outside consensus, and the checkpoint pins no signatures for
655            // them, while their envelopes carry the zero placeholder every
656            // system transaction is built with. For those two the placeholder
657            // is the reference; everything else must match what was pinned.
658            let transaction = tx.transaction.data().transaction();
659            let expected_signatures: &[UserSignature] = if pinned_signatures.is_empty()
660                && (transaction.is_genesis_tx() || transaction.is_end_of_epoch_tx())
661            {
662                &placeholder_signature
663            } else {
664                pinned_signatures.as_slice()
665            };
666            fp_ensure!(
667                tx.transaction.data().signatures() == expected_signatures,
668                anyhow::anyhow!(
669                    "transaction {transaction_digest} does not carry the signatures pinned by the checkpoint contents"
670                )
671            );
672        }
673        Ok(())
674    }
675
676    /// Verifies the contents against `digest` and, when they pass, hands them
677    /// back as verified. Contents received from a peer must go through here:
678    /// it is the only way to obtain a [`VerifiedCheckpointContents`] from
679    /// untrusted data.
680    pub fn verify(self, digest: CheckpointContentsDigest) -> Result<VerifiedCheckpointContents> {
681        self.verify_digests(digest)?;
682        Ok(VerifiedCheckpointContents::new_unchecked(self))
683    }
684
685    pub fn checkpoint_contents(&self) -> CheckpointContents {
686        CheckpointContents::new_with_digests_and_signatures(
687            self.transactions.iter().map(|tx| tx.digests()),
688            self.user_signatures.clone(),
689        )
690    }
691
692    pub fn into_checkpoint_contents(self) -> CheckpointContents {
693        let digests: Vec<_> = self.transactions.iter().map(|tx| tx.digests()).collect();
694        CheckpointContents::new_with_digests_and_signatures(digests, self.user_signatures)
695    }
696
697    pub fn size(&self) -> usize {
698        self.transactions.len()
699    }
700
701    pub fn random_for_testing() -> Self {
702        let (a, key): (_, AccountPrivateKey) = get_key_pair();
703        let transaction = TransactionEnvelope::from_data_and_signer(
704            Transaction::new_transfer(
705                a,
706                random_object_ref(),
707                a,
708                random_object_ref(),
709                100000000000,
710                100,
711            ),
712            vec![&key],
713        );
714        let effects = TestEffectsBuilder::new(transaction.data()).build();
715        let exe_data = ExecutionData {
716            transaction,
717            effects,
718        };
719        FullCheckpointContents::new_with_causally_ordered_transactions(vec![exe_data])
720    }
721}
722
723impl IntoIterator for FullCheckpointContents {
724    type Item = ExecutionData;
725    type IntoIter = std::vec::IntoIter<Self::Item>;
726
727    fn into_iter(self) -> Self::IntoIter {
728        self.transactions.into_iter()
729    }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq)]
733pub struct VerifiedCheckpointContents {
734    transactions: Vec<VerifiedExecutionData>,
735    /// The signatures the checkpoint pins for each transaction, in the same
736    /// order and number as `transactions`. The checkpoint contents digest
737    /// covers them, unlike the signatures inside the transactions, and they
738    /// differ from those for system transactions: the ones the node builds
739    /// outside consensus, at genesis and at the end of an epoch, are pinned
740    /// with an empty set while their envelopes carry a zero placeholder
741    /// signature.
742    user_signatures: Vec<Vec<UserSignature>>,
743}
744
745impl VerifiedCheckpointContents {
746    /// Wraps contents without verifying them. Only for contents the node built
747    /// itself; anything received from a peer goes through
748    /// [`FullCheckpointContents::verify`].
749    pub fn new_unchecked(contents: FullCheckpointContents) -> Self {
750        Self {
751            transactions: contents
752                .transactions
753                .into_iter()
754                .map(VerifiedExecutionData::new_unchecked)
755                .collect(),
756            user_signatures: contents.user_signatures,
757        }
758    }
759
760    pub fn iter(&self) -> Iter<'_, VerifiedExecutionData> {
761        self.transactions.iter()
762    }
763
764    pub fn transactions(&self) -> &[VerifiedExecutionData] {
765        &self.transactions
766    }
767
768    pub fn into_inner(self) -> FullCheckpointContents {
769        FullCheckpointContents {
770            transactions: self
771                .transactions
772                .into_iter()
773                .map(|tx| tx.into_inner())
774                .collect(),
775            user_signatures: self.user_signatures,
776        }
777    }
778
779    pub fn into_checkpoint_contents(self) -> CheckpointContents {
780        self.into_inner().into_checkpoint_contents()
781    }
782
783    pub fn into_checkpoint_contents_digest(self) -> CheckpointContentsDigest {
784        self.into_inner().into_checkpoint_contents().digest()
785    }
786
787    pub fn num_of_transactions(&self) -> usize {
788        self.transactions.len()
789    }
790}
791
792/// Holds data in CheckpointSummary that is serialized into the
793/// `version_specific_data` field.
794#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
795pub enum CheckpointVersionSpecificData {
796    V1(CheckpointVersionSpecificDataV1),
797}
798
799impl CheckpointVersionSpecificData {
800    pub fn as_v1(&self) -> &CheckpointVersionSpecificDataV1 {
801        match self {
802            Self::V1(v) => v,
803        }
804    }
805
806    pub fn into_v1(self) -> CheckpointVersionSpecificDataV1 {
807        match self {
808            Self::V1(v) => v,
809        }
810    }
811
812    pub fn empty_for_tests() -> CheckpointVersionSpecificData {
813        CheckpointVersionSpecificData::V1(CheckpointVersionSpecificDataV1 {
814            randomness_rounds: Vec::new(),
815        })
816    }
817}
818
819#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
820pub struct CheckpointVersionSpecificDataV1 {
821    /// Lists the rounds for which RandomnessStateUpdate transactions are
822    /// present in the checkpoint.
823    pub randomness_rounds: Vec<RandomnessRound>,
824}
825
826#[cfg(test)]
827mod tests {
828    use std::collections::BTreeMap;
829
830    use fastcrypto::traits::KeyPair;
831    use iota_sdk_types::{
832        ConsensusCommitDigest, ObjectId, TransactionDigest, TransactionEffectsDigest,
833        TransactionKind,
834    };
835    use rand::{SeedableRng, prelude::StdRng};
836
837    use super::*;
838    use crate::{
839        object::OBJECT_START_VERSION, transaction::VerifiedTransaction, utils::make_committee_key,
840    };
841
842    // TODO use the file name as a seed
843    const RNG_SEED: [u8; 32] = [
844        21, 23, 199, 200, 234, 250, 252, 178, 94, 15, 202, 178, 62, 186, 88, 137, 233, 192, 130,
845        157, 179, 179, 65, 9, 31, 249, 221, 123, 225, 112, 199, 247,
846    ];
847
848    #[test]
849    fn test_signed_checkpoint() {
850        let mut rng = StdRng::from_seed(RNG_SEED);
851        let (keys, committee) = make_committee_key(&mut rng);
852        let (_, committee2) = make_committee_key(&mut rng);
853
854        let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
855
856        // TODO: duplicated in a test below.
857
858        let signed_checkpoints: Vec<_> = keys
859            .iter()
860            .map(|k| {
861                let name = k.public().into();
862
863                SignedCheckpointSummary::new(
864                    committee.epoch,
865                    CheckpointSummary::new_with_protocol_config(
866                        &ProtocolConfig::get_for_max_version_UNSAFE(),
867                        committee.epoch,
868                        1,
869                        0,
870                        &set,
871                        None,
872                        GasCostSummary::default(),
873                        None,
874                        0,
875                        Vec::new(),
876                    ),
877                    k,
878                    name,
879                )
880            })
881            .collect();
882
883        signed_checkpoints.iter().for_each(|c| {
884            c.verify_authority_signatures(&committee)
885                .expect("signature ok")
886        });
887
888        // fails when not signed by member of committee
889        signed_checkpoints
890            .iter()
891            .for_each(|c| assert!(c.verify_authority_signatures(&committee2).is_err()));
892    }
893
894    #[test]
895    fn test_certified_checkpoint() {
896        let mut rng = StdRng::from_seed(RNG_SEED);
897        let (keys, committee) = make_committee_key(&mut rng);
898
899        let set = CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
900
901        let summary = CheckpointSummary::new_with_protocol_config(
902            &ProtocolConfig::get_for_max_version_UNSAFE(),
903            committee.epoch,
904            1,
905            0,
906            &set,
907            None,
908            GasCostSummary::default(),
909            None,
910            0,
911            Vec::new(),
912        );
913
914        let sign_infos: Vec<_> = keys
915            .iter()
916            .map(|k| {
917                let name = k.public().into();
918
919                SignedCheckpointSummary::sign(committee.epoch, &summary, k, name)
920            })
921            .collect();
922
923        let checkpoint_cert =
924            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("Cert is OK");
925
926        // Signature is correct on proposal, and with same transactions
927        assert!(
928            checkpoint_cert
929                .verify_with_contents(&committee, Some(&set))
930                .is_ok()
931        );
932
933        // Make a bad proposal
934        let signed_checkpoints: Vec<_> = keys
935            .iter()
936            .map(|k| {
937                let name = k.public().into();
938                let set = CheckpointContents::new_with_digests_only_for_tests([
939                    ExecutionDigests::random(),
940                ]);
941
942                SignedCheckpointSummary::new(
943                    committee.epoch,
944                    CheckpointSummary::new_with_protocol_config(
945                        &ProtocolConfig::get_for_max_version_UNSAFE(),
946                        committee.epoch,
947                        1,
948                        0,
949                        &set,
950                        None,
951                        GasCostSummary::default(),
952                        None,
953                        0,
954                        Vec::new(),
955                    ),
956                    k,
957                    name,
958                )
959            })
960            .collect();
961
962        let summary = signed_checkpoints[0].data().clone();
963        let sign_infos = signed_checkpoints
964            .into_iter()
965            .map(|v| v.into_sig())
966            .collect();
967        assert!(
968            CertifiedCheckpointSummary::new(summary, sign_infos, &committee)
969                .unwrap()
970                .verify_authority_signatures(&committee)
971                .is_err()
972        )
973    }
974
975    #[test]
976    fn test_batch_verify_certified_checkpoints() {
977        let mut rng = StdRng::from_seed(RNG_SEED);
978        let (keys, committee) = make_committee_key(&mut rng);
979
980        let summary_for_sequence_number = |sequence_number| {
981            let set =
982                CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
983            CheckpointSummary::new_with_protocol_config(
984                &ProtocolConfig::get_for_max_version_UNSAFE(),
985                committee.epoch,
986                sequence_number,
987                0,
988                &set,
989                None,
990                GasCostSummary::default(),
991                None,
992                0,
993                Vec::new(),
994            )
995        };
996        let certify = |summary: CheckpointSummary| {
997            let sign_infos = keys
998                .iter()
999                .map(|k| {
1000                    SignedCheckpointSummary::sign(committee.epoch, &summary, k, k.public().into())
1001                })
1002                .collect();
1003            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
1004        };
1005
1006        let summaries: Vec<_> = (1..=3)
1007            .map(|seq| certify(summary_for_sequence_number(seq)))
1008            .collect();
1009        CertifiedCheckpointSummary::batch_verify_authority_signatures(&summaries, &committee)
1010            .expect("signatures ok");
1011
1012        // A certificate whose signatures were made over a different summary
1013        // fails the batch.
1014        let mismatched = {
1015            let summary = summary_for_sequence_number(4);
1016            let sign_infos = keys
1017                .iter()
1018                .map(|k| {
1019                    SignedCheckpointSummary::sign(
1020                        committee.epoch,
1021                        &summary_for_sequence_number(4),
1022                        k,
1023                        k.public().into(),
1024                    )
1025                })
1026                .collect();
1027            CertifiedCheckpointSummary::new(summary, sign_infos, &committee).expect("cert is OK")
1028        };
1029        let with_mismatched: Vec<_> = summaries.into_iter().chain([mismatched]).collect();
1030        assert!(
1031            CertifiedCheckpointSummary::batch_verify_authority_signatures(
1032                &with_mismatched,
1033                &committee
1034            )
1035            .is_err()
1036        );
1037    }
1038
1039    // Generate a CheckpointSummary from the input transaction digest. All the other
1040    // fields in the generated CheckpointSummary will be the same. The generated
1041    // CheckpointSummary can be used to test how input transaction digest
1042    // affects CheckpointSummary.
1043    fn generate_test_checkpoint_summary_from_digest(
1044        digest: TransactionDigest,
1045    ) -> CheckpointSummary {
1046        CheckpointSummary::new_with_protocol_config(
1047            &ProtocolConfig::get_for_max_version_UNSAFE(),
1048            1,
1049            2,
1050            10,
1051            &CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::new(
1052                digest,
1053                TransactionEffectsDigest::ZERO,
1054            )]),
1055            None,
1056            GasCostSummary::default(),
1057            None,
1058            100,
1059            Vec::new(),
1060        )
1061    }
1062
1063    // Tests that ConsensusCommitPrologue with different consensus commit digest
1064    // will result in different checkpoint content.
1065    #[test]
1066    fn test_checkpoint_summary_with_different_consensus_digest() {
1067        // First, tests that same consensus commit digest will produce the same
1068        // checkpoint content.
1069        {
1070            let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1071                1,
1072                2,
1073                100,
1074                ConsensusCommitDigest::default(),
1075                Vec::new(),
1076            );
1077            let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1078                1,
1079                2,
1080                100,
1081                ConsensusCommitDigest::default(),
1082                Vec::new(),
1083            );
1084            let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
1085            let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
1086            assert_eq!(c1.digest(), c2.digest());
1087        }
1088
1089        // Next, tests that different consensus commit digests will produce the
1090        // different checkpoint contents.
1091        {
1092            let t1 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1093                1,
1094                2,
1095                100,
1096                ConsensusCommitDigest::default(),
1097                Vec::new(),
1098            );
1099            let t2 = VerifiedTransaction::new_consensus_commit_prologue_v1(
1100                1,
1101                2,
1102                100,
1103                ConsensusCommitDigest::random(),
1104                Vec::new(),
1105            );
1106            let c1 = generate_test_checkpoint_summary_from_digest(*t1.digest());
1107            let c2 = generate_test_checkpoint_summary_from_digest(*t2.digest());
1108            assert_ne!(c1.digest(), c2.digest());
1109        }
1110    }
1111
1112    /// Contents built as served, plus a copy whose transaction was re-signed
1113    /// with a different key. The transaction digest is unchanged, so the
1114    /// contents digest is too; only the envelope's signatures differ.
1115    fn contents_with_a_swapped_envelope() -> (
1116        CheckpointContentsDigest,
1117        FullCheckpointContents,
1118        FullCheckpointContents,
1119    ) {
1120        let contents = FullCheckpointContents::random_for_testing();
1121        let digest = contents.checkpoint_contents().digest();
1122
1123        let (_, other_key): (_, AccountPrivateKey) = get_key_pair();
1124        let original = contents.transactions[0].transaction.clone();
1125        let swapped = TransactionEnvelope::from_data_and_signer(
1126            original.data().transaction().clone(),
1127            vec![&other_key],
1128        );
1129        assert_eq!(swapped.digest(), original.digest());
1130        assert_ne!(swapped.data().signatures(), original.data().signatures());
1131
1132        let mut tampered = contents.clone();
1133        tampered.transactions[0].transaction = swapped;
1134        assert_eq!(tampered.checkpoint_contents().digest(), digest);
1135
1136        (digest, contents, tampered)
1137    }
1138
1139    /// The checkpoint pins the signatures separately from the transactions, and
1140    /// no digest covers them, so a swapped envelope must be caught here.
1141    #[test]
1142    fn verify_digests_rejects_signatures_the_checkpoint_did_not_pin() {
1143        let (digest, contents, tampered) = contents_with_a_swapped_envelope();
1144        contents
1145            .verify_digests(digest)
1146            .expect("contents served as built must verify");
1147        tampered
1148            .verify_digests(digest)
1149            .expect_err("a transaction carrying unpinned signatures must be rejected");
1150    }
1151
1152    /// `verify` is the one door from untrusted contents to the verified type.
1153    #[test]
1154    fn verify_hands_back_verified_contents_that_carry_the_same_digest() {
1155        let (digest, contents, _) = contents_with_a_swapped_envelope();
1156
1157        let verified = contents
1158            .verify(digest)
1159            .expect("contents served as built must verify");
1160
1161        assert_eq!(verified.into_checkpoint_contents_digest(), digest);
1162    }
1163
1164    #[test]
1165    fn verify_refuses_contents_that_fail_verification() {
1166        let (digest, _, tampered) = contents_with_a_swapped_envelope();
1167
1168        tampered
1169            .verify(digest)
1170            .expect_err("contents that fail verification must not become verified");
1171    }
1172
1173    #[test]
1174    fn verify_digests_rejects_a_signature_count_that_does_not_match() {
1175        let contents = FullCheckpointContents::random_for_testing();
1176        let digest = contents.checkpoint_contents().digest();
1177
1178        let mut tampered = contents;
1179        tampered.user_signatures.clear();
1180
1181        // Building the contents asserts the two lengths are equal, so this has
1182        // to be rejected before that point.
1183        tampered
1184            .verify_digests(digest)
1185            .expect_err("a signature count that does not match must be rejected");
1186    }
1187
1188    /// The node pins an empty signature set for the system transactions it
1189    /// builds itself, while their envelopes carry a zero placeholder
1190    /// signature, so the pinned-signature check must leave them alone.
1191    #[test]
1192    fn verify_digests_accepts_a_system_transaction_pinned_without_signatures() {
1193        let transaction = VerifiedTransaction::new_end_of_epoch_transaction(vec![]).into_inner();
1194        assert_eq!(transaction.data().signatures().len(), 1);
1195        let effects = TestEffectsBuilder::new(transaction.data()).build();
1196        let execution_data = ExecutionData {
1197            transaction,
1198            effects,
1199        };
1200        let digests = execution_data.digests();
1201
1202        // Mirrors the checkpoint builder at the end of an epoch and the genesis
1203        // builder, which both pin no signatures for the transaction they
1204        // create.
1205        let checkpoint_contents =
1206            CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1207        let digest = checkpoint_contents.digest();
1208        let contents = FullCheckpointContents::from_contents_and_execution_data(
1209            checkpoint_contents,
1210            std::iter::once(execution_data),
1211        )
1212        .expect("one transaction per pinned signature set");
1213
1214        contents
1215            .verify_digests(digest)
1216            .expect("a system transaction pinned without signatures must verify");
1217    }
1218
1219    /// Every system transaction is built with the same zero placeholder
1220    /// signature, so anything else in that slot did not come from the node.
1221    #[test]
1222    fn verify_digests_rejects_a_system_transaction_without_the_placeholder_signature() {
1223        let (_, key): (_, AccountPrivateKey) = get_key_pair();
1224        let system_data = Transaction::new_system_transaction(TransactionKind::EndOfEpoch(vec![]));
1225        let transaction = TransactionEnvelope::from_data_and_signer(system_data, vec![&key]);
1226        let effects = TestEffectsBuilder::new(transaction.data()).build();
1227        let execution_data = ExecutionData {
1228            transaction,
1229            effects,
1230        };
1231        let digests = execution_data.digests();
1232        let checkpoint_contents =
1233            CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1234        let digest = checkpoint_contents.digest();
1235        let contents = FullCheckpointContents::from_contents_and_execution_data(
1236            checkpoint_contents,
1237            std::iter::once(execution_data),
1238        )
1239        .expect("one transaction per pinned signature set");
1240
1241        contents
1242            .verify_digests(digest)
1243            .expect_err("a system transaction signed with a real key must be rejected");
1244    }
1245
1246    /// The wire type must not admit a value whose two vectors disagree in
1247    /// length, so the invariant holds for every deserialized value.
1248    #[test]
1249    fn deserializing_contents_with_a_mismatched_signature_count_fails() {
1250        let mut tampered = FullCheckpointContents::random_for_testing();
1251        tampered.user_signatures.clear();
1252        let bytes = bcs::to_bytes(&tampered).expect("serializing the tampered value must work");
1253
1254        bcs::from_bytes::<FullCheckpointContents>(&bytes)
1255            .expect_err("a signature count that does not match must not deserialize");
1256    }
1257
1258    /// The two lists arrive in separate fields of the checkpoint data a node
1259    /// receives, so a mismatch is an input error, never a panic.
1260    #[test]
1261    fn from_contents_and_execution_data_rejects_mismatched_lengths() {
1262        let contents = FullCheckpointContents::random_for_testing();
1263        let checkpoint_contents = contents.checkpoint_contents();
1264
1265        FullCheckpointContents::from_contents_and_execution_data(
1266            checkpoint_contents,
1267            std::iter::empty(),
1268        )
1269        .expect_err("a signature count that does not match the transactions must be rejected");
1270    }
1271
1272    /// Only genesis and end-of-epoch transactions are pinned without
1273    /// signatures. A system transaction that went through consensus is pinned
1274    /// with its placeholder, so an empty pinned set for one is not something
1275    /// the node produces and must not be accepted.
1276    #[test]
1277    fn verify_digests_rejects_a_consensus_system_transaction_pinned_without_signatures() {
1278        let transaction = VerifiedTransaction::new_consensus_commit_prologue_v1(
1279            0,
1280            0,
1281            0,
1282            ConsensusCommitDigest::default(),
1283            vec![],
1284        )
1285        .into_inner();
1286        // The prologue mutates the shared clock, so the builder needs its
1287        // version to derive a valid lamport version.
1288        let effects = TestEffectsBuilder::new(transaction.data())
1289            .with_shared_input_versions(BTreeMap::from([(ObjectId::CLOCK, OBJECT_START_VERSION)]))
1290            .build();
1291        let execution_data = ExecutionData {
1292            transaction,
1293            effects,
1294        };
1295        let digests = execution_data.digests();
1296        let checkpoint_contents =
1297            CheckpointContents::new_with_digests_and_signatures(vec![digests], vec![vec![]]);
1298        let digest = checkpoint_contents.digest();
1299        let contents = FullCheckpointContents::from_contents_and_execution_data(
1300            checkpoint_contents,
1301            std::iter::once(execution_data),
1302        )
1303        .expect("one transaction per pinned signature set");
1304
1305        contents.verify_digests(digest).expect_err(
1306            "a consensus system transaction pinned without signatures must be rejected",
1307        );
1308    }
1309}