Skip to main content

iota_sdk_types/
checkpoint.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use super::{
6    CheckpointContentsDigest, CheckpointDigest, Digest, GasCostSummary, Object, SignedTransaction,
7    TransactionDigest, TransactionEffects, TransactionEffectsDigest, TransactionEvents,
8    UserSignature, ValidatorAggregatedSignature, ValidatorCommitteeMember,
9};
10
11pub type CheckpointSequenceNumber = u64;
12pub type CheckpointTimestamp = u64;
13pub type EpochId = u64;
14pub type StakeUnit = u64;
15pub type ProtocolVersion = u64;
16
17/// A commitment made by a checkpoint.
18///
19/// # BCS
20///
21/// The BCS serialized form for this type is defined by the following ABNF:
22///
23/// ```text
24/// ; CheckpointCommitment is an enum and each variant is prefixed with its index
25/// checkpoint-commitment = ecmh-live-object-set
26/// ecmh-live-object-set = %d00 digest
27/// ```
28#[derive(Clone, Debug, Eq, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
30#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
31#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
32#[non_exhaustive]
33pub enum CheckpointCommitment {
34    /// An Elliptic Curve Multiset Hash attesting to the set of Objects that
35    /// compose the live state of the IOTA blockchain.
36    EcmhLiveObjectSet { digest: Digest },
37    // Other commitment types (e.g. merkle roots) go here.
38}
39
40impl CheckpointCommitment {
41    crate::def_is!(EcmhLiveObjectSet);
42
43    pub fn as_ecmh_live_object_set_digest(&self) -> Digest {
44        let Self::EcmhLiveObjectSet { digest } = self;
45        *digest
46    }
47}
48
49impl std::fmt::Display for CheckpointCommitment {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        match self {
52            CheckpointCommitment::EcmhLiveObjectSet { digest } => {
53                write!(f, "EcmhLiveObjectSet({digest})")
54            }
55        }
56    }
57}
58
59/// Data, which when included in a [`CheckpointSummary`], signals the end of an
60/// `Epoch`.
61///
62/// # BCS
63///
64/// The BCS serialized form for this type is defined by the following ABNF:
65///
66/// ```text
67/// end-of-epoch-data = (vector validator-committee-member)   ; next-epoch-committee
68///                     u64                                   ; next-epoch-protocol-version
69///                     (vector checkpoint-commitment)        ; epoch-commitments
70///                     i64                                   ; epoch-supply-change
71/// ```
72#[derive(Clone, Debug, Eq, PartialEq)]
73#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
74#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
75#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
76pub struct EndOfEpochData {
77    /// The set of Validators that will be in the ValidatorCommittee for the
78    /// next epoch.
79    pub next_epoch_committee: Vec<ValidatorCommitteeMember>,
80    /// The protocol version that is in effect during the next epoch.
81    #[cfg_attr(feature = "serde", serde(with = "crate::_serde::ReadableDisplay"))]
82    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u64"))]
83    pub next_epoch_protocol_version: ProtocolVersion,
84    /// Commitments to epoch specific state (e.g. live object set)
85    pub epoch_commitments: Vec<CheckpointCommitment>,
86    /// The number of tokens that were minted (if positive) or burnt (if
87    /// negative) in this epoch.
88    pub epoch_supply_change: i64,
89}
90
91impl crate::TreeDisplay for EndOfEpochData {
92    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
93        w.header("End of Epoch Data")?;
94        w.children("Next Epoch Committee", &self.next_epoch_committee, false)?;
95        w.leaf(
96            "Next Epoch Protocol Version",
97            &self.next_epoch_protocol_version,
98            false,
99        )?;
100        w.leaves("Epoch Commitments", &self.epoch_commitments, false)?;
101        w.leaf("Epoch Supply Change", &self.epoch_supply_change, true)
102    }
103}
104
105/// A header for a Checkpoint on the IOTA blockchain.
106///
107/// On the IOTA network, checkpoints define the history of the blockchain. They
108/// are quite similar to the concept of blocks used by other blockchains like
109/// Bitcoin or Ethereum. The IOTA blockchain, however, forms checkpoints after
110/// transaction execution has already happened to provide a certified history of
111/// the chain, instead of being formed before execution.
112///
113/// Checkpoints commit to a variety of state including but not limited to:
114/// - The hash of the previous checkpoint.
115/// - The set of transaction digests, their corresponding effects digests, as
116///   well as the set of user signatures which authorized its execution.
117/// - The object's produced by a transaction.
118/// - The set of live objects that make up the current state of the chain.
119/// - On epoch transitions, the next validator committee.
120///
121/// `CheckpointSummary`s themselves don't directly include all of the above
122/// information but they are the top-level type by which all the above are
123/// committed to transitively via cryptographic hashes included in the summary.
124/// `CheckpointSummary`s are signed and certified by a quorum of the validator
125/// committee in a given epoch in order to allow verification of the chain's
126/// state.
127///
128/// # BCS
129///
130/// The BCS serialized form for this type is defined by the following ABNF:
131///
132/// ```text
133/// checkpoint-summary = u64                            ; epoch
134///                      u64                            ; sequence_number
135///                      u64                            ; network_total_transactions
136///                      checkpoint-contents-digest     ; contents_digest
137///                      (option checkpoint-digest)     ; previous_digest
138///                      gas-cost-summary               ; epoch_rolling_gas_cost_summary
139///                      u64                            ; timestamp_ms
140///                      (vector checkpoint-commitment) ; checkpoint_commitments
141///                      (option end-of-epoch-data)     ; end_of_epoch_data
142///                      bytes                          ; version_specific_data
143/// ```
144#[derive(Clone, derive_more::Debug, Eq, PartialEq)]
145#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
146#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
147#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
148pub struct CheckpointSummary {
149    /// Epoch that this checkpoint belongs to.
150    #[cfg_attr(feature = "serde", serde(with = "crate::_serde::ReadableDisplay"))]
151    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u64"))]
152    pub epoch: EpochId,
153    /// The height of this checkpoint.
154    #[cfg_attr(feature = "serde", serde(with = "crate::_serde::ReadableDisplay"))]
155    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u64"))]
156    pub sequence_number: CheckpointSequenceNumber,
157    /// Total number of transactions committed since genesis, including those in
158    /// this checkpoint.
159    #[cfg_attr(feature = "serde", serde(with = "crate::_serde::ReadableDisplay"))]
160    pub network_total_transactions: u64,
161    /// The hash of the [`CheckpointContents`] for this checkpoint.
162    pub contents_digest: CheckpointContentsDigest,
163    /// The hash of the previous `CheckpointSummary`.
164    ///
165    /// This will be only be `None` for the first, or genesis checkpoint.
166    #[cfg_attr(feature = "serde", serde(default))]
167    pub previous_digest: Option<CheckpointDigest>,
168    /// The running total gas costs of all transactions included in the current
169    /// epoch so far until this checkpoint.
170    pub epoch_rolling_gas_cost_summary: GasCostSummary,
171    /// Timestamp of the checkpoint - number of milliseconds from the Unix epoch
172    /// Checkpoint timestamps are monotonic, but not strongly monotonic -
173    /// subsequent checkpoints can have same timestamp if they originate
174    /// from the same underlining consensus commit
175    #[cfg_attr(feature = "serde", serde(with = "crate::_serde::ReadableDisplay"))]
176    #[cfg_attr(feature = "bcs-schema", bcs_schema(as_type = "u64"))]
177    pub timestamp_ms: CheckpointTimestamp,
178    /// Commitments to checkpoint-specific state.
179    #[cfg_attr(feature = "serde", serde(default))]
180    pub checkpoint_commitments: Vec<CheckpointCommitment>,
181    /// Extra data only present in the final checkpoint of an epoch.
182    #[cfg_attr(feature = "serde", serde(default))]
183    pub end_of_epoch_data: Option<EndOfEpochData>,
184    /// CheckpointSummary is not an evolvable structure - it must be readable by
185    /// any version of the code. Therefore, in order to allow extensions to
186    /// be added to CheckpointSummary, we allow opaque data to be added to
187    /// checkpoints which can be deserialized based on the current
188    /// protocol version.
189    #[cfg_attr(
190        feature = "serde",
191        serde(default, with = "crate::_serde::ReadableBase64Encoded")
192    )]
193    #[debug(
194        "{:?}",
195        <base64ct::Base64 as base64ct::Encoding>::encode_string(version_specific_data)
196    )]
197    pub version_specific_data: Vec<u8>,
198}
199
200impl crate::TreeDisplay for CheckpointSummary {
201    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
202        w.header("Checkpoint Summary")?;
203        w.leaf("Epoch", &self.epoch, false)?;
204        w.leaf("Sequence Number", &self.sequence_number, false)?;
205        w.leaf(
206            "Network Total Transactions",
207            &self.network_total_transactions,
208            false,
209        )?;
210        w.leaf("Contents Digest", &self.contents_digest, false)?;
211        w.option_leaf("Previous Digest", &self.previous_digest, false)?;
212        w.child(
213            "Epoch Rolling Gas Cost",
214            &self.epoch_rolling_gas_cost_summary,
215            false,
216        )?;
217        w.leaf("Timestamp (ms)", &self.timestamp_ms, false)?;
218        w.leaves(
219            "Checkpoint Commitments",
220            &self.checkpoint_commitments,
221            false,
222        )?;
223        w.option_child("End of Epoch Data", &self.end_of_epoch_data, true)
224    }
225}
226
227impl CheckpointSummary {
228    /// Construct a `CheckpointSummary` from its constituent parts.
229    #[expect(clippy::too_many_arguments)]
230    pub fn new(
231        epoch: EpochId,
232        sequence_number: CheckpointSequenceNumber,
233        network_total_transactions: u64,
234        contents_digest: CheckpointContentsDigest,
235        previous_digest: Option<CheckpointDigest>,
236        epoch_rolling_gas_cost_summary: GasCostSummary,
237        timestamp_ms: CheckpointTimestamp,
238        checkpoint_commitments: Vec<CheckpointCommitment>,
239        end_of_epoch_data: Option<EndOfEpochData>,
240        version_specific_data: Vec<u8>,
241    ) -> Self {
242        Self {
243            epoch,
244            sequence_number,
245            network_total_transactions,
246            contents_digest,
247            previous_digest,
248            epoch_rolling_gas_cost_summary,
249            timestamp_ms,
250            checkpoint_commitments,
251            end_of_epoch_data,
252            version_specific_data,
253        }
254    }
255
256    /// The epoch that this checkpoint belongs to.
257    pub fn epoch(&self) -> EpochId {
258        self.epoch
259    }
260
261    /// The height of this checkpoint.
262    pub fn sequence_number(&self) -> CheckpointSequenceNumber {
263        self.sequence_number
264    }
265
266    /// Total number of transactions committed since genesis, including those in
267    /// this checkpoint.
268    pub fn network_total_transactions(&self) -> u64 {
269        self.network_total_transactions
270    }
271
272    /// The hash of the [`CheckpointContents`] for this checkpoint.
273    pub fn contents_digest(&self) -> CheckpointContentsDigest {
274        self.contents_digest
275    }
276
277    /// The hash of the previous `CheckpointSummary`, or `None` for the genesis
278    /// checkpoint.
279    pub fn previous_digest(&self) -> Option<CheckpointDigest> {
280        self.previous_digest
281    }
282
283    /// The running total gas costs of all transactions included in the current
284    /// epoch so far until this checkpoint.
285    pub fn epoch_rolling_gas_cost_summary(&self) -> &GasCostSummary {
286        &self.epoch_rolling_gas_cost_summary
287    }
288
289    /// Timestamp of the checkpoint, in milliseconds from the Unix epoch.
290    pub fn timestamp_ms(&self) -> CheckpointTimestamp {
291        self.timestamp_ms
292    }
293
294    /// Commitments to checkpoint-specific state.
295    pub fn checkpoint_commitments(&self) -> &[CheckpointCommitment] {
296        &self.checkpoint_commitments
297    }
298
299    /// Extra data present only in the final checkpoint of an epoch.
300    pub fn end_of_epoch_data(&self) -> Option<&EndOfEpochData> {
301        self.end_of_epoch_data.as_ref()
302    }
303
304    /// Opaque, protocol-version-specific data carried by the checkpoint.
305    pub fn version_specific_data(&self) -> &[u8] {
306        &self.version_specific_data
307    }
308
309    /// The validator committee that takes effect in the next epoch, present
310    /// only on the final checkpoint of an epoch.
311    pub fn next_epoch_committee(&self) -> Option<&[ValidatorCommitteeMember]> {
312        self.end_of_epoch_data
313            .as_ref()
314            .map(|data| data.next_epoch_committee.as_slice())
315    }
316
317    /// Whether this is the final checkpoint of an epoch.
318    pub fn is_last_checkpoint_of_epoch(&self) -> bool {
319        self.end_of_epoch_data.is_some()
320    }
321}
322
323#[derive(Clone, Debug, PartialEq)]
324#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
325#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
326#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
327pub struct SignedCheckpointSummary {
328    pub checkpoint: CheckpointSummary,
329    pub signature: ValidatorAggregatedSignature,
330}
331
332impl crate::TreeDisplay for SignedCheckpointSummary {
333    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
334        w.header("Signed Checkpoint Summary")?;
335        w.child("Checkpoint", &self.checkpoint, false)?;
336        w.child("Signature", &self.signature, true)
337    }
338}
339
340/// The committed to contents of a checkpoint.
341///
342/// `CheckpointContents` contains a list of digests of Transactions, their
343/// effects, and the user signatures that authorized their execution included in
344/// a checkpoint.
345///
346/// # BCS
347///
348/// The BCS serialized form for this type is defined by the following ABNF:
349///
350/// ```text
351/// checkpoint-contents = %d00 checkpoint-contents-v1 ; variant 0
352/// ```
353#[derive(Clone, Debug, Eq, PartialEq)]
354#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
355#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
356#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
357#[non_exhaustive]
358pub enum CheckpointContents {
359    V1(CheckpointContentsV1),
360}
361
362impl CheckpointContents {
363    pub fn new_v1(contents: CheckpointContentsV1) -> Self {
364        Self::V1(contents)
365    }
366
367    crate::def_is_as_into_opt!(V1(CheckpointContentsV1));
368
369    /// Returns a reference to the list of transactions in this checkpoint.
370    pub fn transactions(&self) -> &[CheckpointTransactionInfo] {
371        match self {
372            CheckpointContents::V1(v1) => v1.transactions(),
373        }
374    }
375
376    /// Consumes the `CheckpointContentsV1` and returns the list of
377    /// transactions.
378    pub fn into_transactions(self) -> Vec<CheckpointTransactionInfo> {
379        match self {
380            CheckpointContents::V1(v1) => v1.into_transactions(),
381        }
382    }
383
384    /// The number of transactions in this checkpoint.
385    pub fn len(&self) -> usize {
386        match self {
387            CheckpointContents::V1(v1) => v1.len(),
388        }
389    }
390
391    /// Whether this checkpoint has no transactions.
392    pub fn is_empty(&self) -> bool {
393        match self {
394            CheckpointContents::V1(v1) => v1.is_empty(),
395        }
396    }
397}
398
399/// CheckpointContents are the transactions included in an upcoming checkpoint.
400/// They must have already been causally ordered. Since the causal order
401/// algorithm is the same among validators, we expect all honest validators to
402/// come up with the same order for each checkpoint content.
403///
404/// # BCS
405///
406/// The BCS serialized form for this type is defined by the following ABNF:
407///
408/// ```text
409/// checkpoint-contents-v1 = (vector execution-digests)      ; transaction and effect digests
410///                          (vector (vector user-signature)) ; set of user signatures for each
411///                                                           ; transaction. MUST be the same
412///                                                           ; length as the vector of digests
413///
414/// execution-digests = transaction-digest transaction-effects-digest   ; transaction, effects
415/// ```
416#[derive(Clone, Debug, Eq, PartialEq)]
417#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
418pub struct CheckpointContentsV1 {
419    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=2).lift()))]
420    transactions: Vec<CheckpointTransactionInfo>,
421}
422
423impl CheckpointContentsV1 {
424    pub fn new(transactions: Vec<CheckpointTransactionInfo>) -> Self {
425        Self { transactions }
426    }
427
428    /// Returns a reference to the list of transactions in this checkpoint.
429    pub fn transactions(&self) -> &[CheckpointTransactionInfo] {
430        &self.transactions
431    }
432
433    /// Consumes the `CheckpointContentsV1` and returns the list of
434    /// transactions.
435    pub fn into_transactions(self) -> Vec<CheckpointTransactionInfo> {
436        self.transactions
437    }
438
439    /// The number of transactions in this checkpoint.
440    pub fn len(&self) -> usize {
441        self.transactions.len()
442    }
443
444    /// Whether this checkpoint has no transactions.
445    pub fn is_empty(&self) -> bool {
446        self.transactions.is_empty()
447    }
448}
449
450impl crate::TreeDisplay for CheckpointContents {
451    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
452        w.enum_name("Checkpoint Contents");
453        match self {
454            Self::V1(v1) => v1.fmt_tree(w),
455        }
456    }
457}
458
459impl crate::TreeDisplay for CheckpointContentsV1 {
460    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
461        w.header("Checkpoint Contents V1")?;
462        w.children("Transactions", self.transactions(), true)
463    }
464}
465
466/// Transaction information committed to in a checkpoint
467#[derive(Clone, Debug, Eq, PartialEq)]
468#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
469#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
470pub struct CheckpointTransactionInfo {
471    pub transaction: TransactionDigest,
472    pub effects: TransactionEffectsDigest,
473    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=2).lift()))]
474    pub signatures: Vec<UserSignature>,
475}
476
477impl crate::TreeDisplay for CheckpointTransactionInfo {
478    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
479        w.header("Checkpoint Transaction Info")?;
480        w.leaf("Transaction", &self.transaction, false)?;
481        w.leaf("Effects", &self.effects, false)?;
482        w.children("Signatures", &self.signatures, true)
483    }
484}
485
486#[derive(Clone, Debug, PartialEq)]
487#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
488#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
489#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
490pub struct CheckpointData {
491    pub checkpoint_summary: SignedCheckpointSummary,
492    pub checkpoint_contents: CheckpointContents,
493    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=1).lift()))]
494    pub transactions: Vec<CheckpointTransaction>,
495}
496
497impl crate::TreeDisplay for CheckpointData {
498    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
499        w.header("Checkpoint Data")?;
500        w.child("Checkpoint Summary", &self.checkpoint_summary, false)?;
501        w.child("Contents", &self.checkpoint_contents, false)?;
502        w.children("Transactions", &self.transactions, true)
503    }
504}
505
506#[derive(Clone, Debug, Eq, PartialEq)]
507#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
508#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
509#[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
510pub struct CheckpointTransaction {
511    /// The input Transaction
512    #[cfg_attr(
513        feature = "serde",
514        serde(with = "::serde_with::As::<crate::_serde::SignedTransactionWithIntentMessage>")
515    )]
516    #[cfg_attr(
517        feature = "bcs-schema",
518        bcs_schema(as_type = "%d01 intent-signed-transaction")
519    )]
520    pub transaction: SignedTransaction,
521    /// The effects produced by executing this transaction
522    pub effects: TransactionEffects,
523    /// The events, if any, emitted by this transaction during execution
524    pub events: Option<TransactionEvents>,
525    /// The state of all inputs to this transaction as they were prior to
526    /// execution.
527    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=2).lift()))]
528    pub input_objects: Vec<Object>,
529    /// The state of all output objects created or mutated by this transaction.
530    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=2).lift()))]
531    pub output_objects: Vec<Object>,
532}
533
534impl crate::TreeDisplay for CheckpointTransaction {
535    fn fmt_tree(&self, w: &mut crate::TreeWriter<'_, '_>) -> std::fmt::Result {
536        w.header("Checkpoint Transaction")?;
537        w.child("Transaction", &self.transaction, false)?;
538        w.child("Effects", &self.effects, false)?;
539        w.option_child("Events", &self.events, false)?;
540        w.children("Input Objects", &self.input_objects, false)?;
541        w.children("Output Objects", &self.output_objects, true)
542    }
543}
544
545crate::impl_tree_display!(
546    EndOfEpochData,
547    CheckpointSummary,
548    SignedCheckpointSummary,
549    CheckpointContents,
550    CheckpointContentsV1,
551    CheckpointTransactionInfo,
552    CheckpointData,
553    CheckpointTransaction
554);
555
556#[cfg(feature = "serde")]
557#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
558mod serialization {
559    use serde::{Deserialize, Deserializer, Serialize, Serializer};
560
561    use super::*;
562
563    impl Serialize for CheckpointContentsV1 {
564        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
565        where
566            S: Serializer,
567        {
568            use serde::ser::{SerializeSeq, SerializeTuple};
569
570            if serializer.is_human_readable() {
571                serializer.serialize_newtype_struct("CheckpointContentsV1", &self.transactions)
572            } else {
573                #[derive(serde::Serialize)]
574                struct Digests<'a> {
575                    transaction: &'a TransactionDigest,
576                    effects: &'a TransactionEffectsDigest,
577                }
578
579                struct DigestSeq<'a>(&'a CheckpointContentsV1);
580                impl Serialize for DigestSeq<'_> {
581                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
582                    where
583                        S: Serializer,
584                    {
585                        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
586                        for txn in &self.0.transactions {
587                            let digests = Digests {
588                                transaction: &txn.transaction,
589                                effects: &txn.effects,
590                            };
591                            seq.serialize_element(&digests)?;
592                        }
593                        seq.end()
594                    }
595                }
596
597                struct SignatureSeq<'a>(&'a CheckpointContentsV1);
598                impl Serialize for SignatureSeq<'_> {
599                    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600                    where
601                        S: Serializer,
602                    {
603                        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
604                        for txn in &self.0.transactions {
605                            seq.serialize_element(&txn.signatures)?;
606                        }
607                        seq.end()
608                    }
609                }
610
611                let mut s = serializer.serialize_tuple(2)?;
612                s.serialize_element(&DigestSeq(self))?;
613                s.serialize_element(&SignatureSeq(self))?;
614                s.end()
615            }
616        }
617    }
618
619    #[derive(serde::Deserialize)]
620    #[cfg_attr(feature = "bcs-schema", derive(iota_bcs_schema::BcsSchema))]
621    struct ExecutionDigests {
622        transaction: TransactionDigest,
623        effects: TransactionEffectsDigest,
624    }
625
626    #[derive(serde::Deserialize)]
627    #[cfg_attr(
628        feature = "bcs-schema",
629        derive(iota_bcs_schema::BcsSchema),
630        bcs_schema(name = "checkpoint-contents-v1")
631    )]
632    struct BinaryContentsV1 {
633        digests: Vec<ExecutionDigests>,
634        signatures: Vec<Vec<UserSignature>>,
635    }
636
637    impl<'de> Deserialize<'de> for CheckpointContentsV1 {
638        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
639        where
640            D: Deserializer<'de>,
641        {
642            if deserializer.is_human_readable() {
643                let transactions: Vec<CheckpointTransactionInfo> =
644                    Deserialize::deserialize(deserializer)?;
645                Ok(Self { transactions })
646            } else {
647                let BinaryContentsV1 {
648                    digests,
649                    signatures,
650                } = Deserialize::deserialize(deserializer)?;
651
652                if digests.len() != signatures.len() {
653                    return Err(serde::de::Error::custom(
654                        "must have same number of signatures as transactions",
655                    ));
656                }
657
658                Ok(Self {
659                    transactions: digests
660                        .into_iter()
661                        .zip(signatures)
662                        .map(
663                            |(
664                                ExecutionDigests {
665                                    transaction,
666                                    effects,
667                                },
668                                signatures,
669                            )| CheckpointTransactionInfo {
670                                transaction,
671                                effects,
672                                signatures,
673                            },
674                        )
675                        .collect(),
676                })
677            }
678        }
679    }
680
681    #[cfg(test)]
682    mod tests {
683        use base64ct::{Base64, Encoding};
684        #[cfg(target_arch = "wasm32")]
685        use wasm_bindgen_test::wasm_bindgen_test as test;
686
687        use super::*;
688
689        #[test]
690        fn signed_checkpoint_fixture() {
691            // Checkpoint summaries created from a local network (iota start command)
692            // http://localhost:9000/api/v1/checkpoints to see the list of checkpoints
693            // To get the data of checkpoint 1 as base64, use:
694            // curl -s http://localhost:9000/api/v1/checkpoints/1 -H "Accept: application/bcs" | base64
695            const FIXTURES: &[&str] = &[
696                "AAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAIBqk0HxZmh1Bym2oL/3TlEnvb0FZbMJ594JGx2ZX9w2oASBCLJ9nhRE2EUG3C/XMPTdJTbK/1GjM585faUsOUQhFYgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC9f941lwEAAAAAAgAAAAAAAAAAAACx8KVNWdScdFfM3RDAC41byY37f2pdIhrjGI8SQVY7Vel7TCBQ/kvuRdINIrazvwgUOjAAAAEAAAAAAAEAEAAAAAAAAQA=",
697                "DQAAAAAAAAB4DgAAAAAAAEo/AAAAAAAAIGJzt6qiBfbQHQufWpLivtr60pLRjm9dy7ulx34XrVVTASCV+2EoRe+2oCMWuVWVtl3ZIEdyaJgPhs+mCXiNtq6YygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADyWus1lwEAAAABAmCNz/bRVQQKZW9IGbExEbUsV0aoa6cvOV+6/i7DhH0egUDmJKdR/fa18gULxyBc+dMABMkLDHQK/9Mmzmc8wrI6LSTVPir+sobfxmj9QGAInW0rF7eZ3Tb5DTMuVKejONSIEwAAAAAAAGCZ8l72H4AyuRRjZGCYLFzG8TTvHdrnZlfyy/7B6/yNCXN0CA32/PDcuLxLDY4K9dgOu/8rTFmfVPQtYxLfwxQnYHjBzDR+u77FGYviWFE/OGuTDQLCdJqAPiMwlV69GhCIEwAAAAAAAAkAAAAAAAAAAQAgIeRTzjDpjnTS3fkN3QCskISnmr5Z49j8JKFBGGuQjcAA8IoalbkCAAoAAWsAAAAAAAAADQAAAAAAAAC4F4HnXo6T6kpusCM8Gm7uXzE44DhcL0Faldy/mECSwlxBrcy4taqwhCdfgWVMmAsUOjAAAAEAAAAAAAEAEAAAAAAAAQA=",
698            ];
699
700            for fixture in FIXTURES {
701                let bcs = Base64::decode_vec(fixture).unwrap();
702
703                let checkpoint: SignedCheckpointSummary = bcs::from_bytes(&bcs).unwrap();
704                let bytes = bcs::to_bytes(&checkpoint).unwrap();
705                assert_eq!(bcs, bytes);
706                let json = serde_json::to_string_pretty(&checkpoint).unwrap();
707                println!("{json}");
708            }
709        }
710
711        #[test]
712        fn contents_fixture() {
713            let fixture = "AAEgp6oAB8Qadn8+FqtdqeDIp8ViQNOZpMKs44MN0N5y7zIgqn5dKR1+8poL0pLNwRo/2knMnodwMTEDhqYL03kdewQBAWEAgpORkfH6ewjfFQYZJhmjkYq0/B3Set4mLJX/G0wUPb/V4H41gJipYu4I6ToyixnEuPQWxHKLckhNn+0UmI+pAJ9GegzEh0q2HWABmFMpFoPw0229dCfzWNOhHW5bes4H";
714
715            let bcs = Base64::decode_vec(fixture).unwrap();
716
717            let contents: CheckpointContents = bcs::from_bytes(&bcs).unwrap();
718            let bytes = bcs::to_bytes(&contents).unwrap();
719            assert_eq!(bcs, bytes);
720            let json = serde_json::to_string_pretty(&contents).unwrap();
721            println!("{json}");
722        }
723    }
724}