Skip to main content

iota_types/
test_checkpoint_data_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6
7use iota_protocol_config::ProtocolConfig;
8use iota_sdk_types::{
9    Address, EndOfEpochTransactionKind, Event, Identifier, MoveStruct, ObjectId, ObjectReference,
10    Owner, SharedObjectReference, StructTag, TransactionDigest, TransactionKind, TypeTag, Version,
11    checkpoint::{CheckpointContents, CheckpointSummary, EndOfEpochData},
12};
13use tap::Pipe;
14
15use crate::{
16    base_types::{ExecutionDigests, dbg_addr, random_object_ref},
17    committee::Committee,
18    effects::{
19        TestEffectsBuilder, TransactionEffects, TransactionEffectsAPI,
20        TransactionEffectsExtForTesting, TransactionEvents,
21    },
22    event::SystemEpochInfoEventV2,
23    full_checkpoint_content::{CheckpointData, CheckpointTransaction},
24    gas_coin::GAS,
25    messages_checkpoint::{
26        CertifiedCheckpointSummary, CheckpointContentsExt, CheckpointSummaryExt,
27    },
28    object::{GAS_VALUE_FOR_TESTING, MoveStructExt, Object},
29    programmable_transaction_builder::ProgrammableTransactionBuilder,
30    transaction::{CallArg, SenderSignedData, Transaction, TransactionData, TransactionDataAPI},
31};
32
33/// A builder for creating test checkpoint data.
34/// Once initialized, the builder can be used to build multiple checkpoints.
35/// Call `start_transaction` to begin creating a new transaction.
36/// Call `finish_transaction` to complete the current transaction and add it to
37/// the current checkpoint. After all transactions are added, call
38/// `build_checkpoint` to get the final checkpoint data. This will also
39/// increment the stored checkpoint sequence number. Start the above process
40/// again to build the next checkpoint. NOTE: The generated checkpoint data is
41/// not guaranteed to be semantically valid or consistent. For instance, all
42/// object digests will be randomly set. It focuses on providing a way to
43/// generate various shaped test data for testing purposes.
44/// If you need to test the validity of the checkpoint data, you should use
45/// Simulacrum instead.
46pub struct TestCheckpointDataBuilder {
47    /// Map of all live objects in the state.
48    live_objects: HashMap<ObjectId, Object>,
49    /// Map of all wrapped objects in the state.
50    wrapped_objects: HashMap<ObjectId, Object>,
51    /// A map from sender addresses to gas objects they own.
52    /// These are created automatically when a transaction is started.
53    /// Users of this builder should not need to worry about them.
54    gas_map: HashMap<Address, ObjectId>,
55
56    /// The current checkpoint builder.
57    /// It is initialized when the builder is created, and is reset when
58    /// `build_checkpoint` is called.
59    checkpoint_builder: CheckpointBuilder,
60}
61
62struct CheckpointBuilder {
63    /// Checkpoint number for the current checkpoint we are building.
64    checkpoint: u64,
65    /// Epoch number for the current checkpoint we are building.
66    epoch: u64,
67    /// Counter for the total number of transactions added to the builder.
68    network_total_transactions: u64,
69    /// Transactions that have been added to the current checkpoint.
70    transactions: Vec<CheckpointTransaction>,
71    /// The current transaction being built.
72    next_transaction: Option<TransactionBuilder>,
73}
74
75struct TransactionBuilder {
76    sender_idx: u8,
77    gas: ObjectReference,
78    move_calls: Vec<(ObjectId, &'static str, &'static str)>,
79    created_objects: BTreeMap<ObjectId, Object>,
80    mutated_objects: BTreeMap<ObjectId, Object>,
81    unwrapped_objects: BTreeSet<ObjectId>,
82    wrapped_objects: BTreeSet<ObjectId>,
83    deleted_objects: BTreeSet<ObjectId>,
84    frozen_objects: BTreeSet<ObjectReference>,
85    shared_inputs: BTreeMap<ObjectId, Shared>,
86    events: Option<Vec<Event>>,
87}
88
89struct Shared {
90    mutable: bool,
91    object: Object,
92}
93
94impl TransactionBuilder {
95    pub fn new(sender_idx: u8, gas: ObjectReference) -> Self {
96        Self {
97            sender_idx,
98            gas,
99            move_calls: vec![],
100            created_objects: BTreeMap::new(),
101            mutated_objects: BTreeMap::new(),
102            unwrapped_objects: BTreeSet::new(),
103            wrapped_objects: BTreeSet::new(),
104            deleted_objects: BTreeSet::new(),
105            frozen_objects: BTreeSet::new(),
106            shared_inputs: BTreeMap::new(),
107            events: None,
108        }
109    }
110}
111
112impl TestCheckpointDataBuilder {
113    pub fn new(checkpoint: u64) -> Self {
114        Self {
115            live_objects: HashMap::new(),
116            wrapped_objects: HashMap::new(),
117            gas_map: HashMap::new(),
118            checkpoint_builder: CheckpointBuilder {
119                checkpoint,
120                epoch: 0,
121                network_total_transactions: 0,
122                transactions: vec![],
123                next_transaction: None,
124            },
125        }
126    }
127
128    /// Set the epoch for the checkpoint.
129    pub fn with_epoch(mut self, epoch: u64) -> Self {
130        self.checkpoint_builder.epoch = epoch;
131        self
132    }
133
134    /// Start creating a new transaction.
135    /// `sender_idx` is a convenient representation of the sender's address.
136    /// A proper Address will be derived from it.
137    /// It will also create a gas object for the sender if it doesn't already
138    /// exist in the live object map. You do not need to create the gas
139    /// object yourself.
140    pub fn start_transaction(mut self, sender_idx: u8) -> Self {
141        assert!(self.checkpoint_builder.next_transaction.is_none());
142        let sender = Self::derive_address(sender_idx);
143        let gas_id = self.gas_map.entry(sender).or_insert_with(|| {
144            let gas = Object::with_owner_for_testing(sender);
145            let id = gas.id();
146            self.live_objects.insert(id, gas);
147            id
148        });
149        let gas_ref = self.live_objects.get(gas_id).cloned().unwrap().object_ref();
150        self.checkpoint_builder.next_transaction =
151            Some(TransactionBuilder::new(sender_idx, gas_ref));
152        self
153    }
154
155    /// Create a new object in the transaction.
156    /// `object_idx` is a convenient representation of the object's ID.
157    /// The object will be created as a IOTA coin object, with default balance,
158    /// and the transaction sender as its owner.
159    pub fn create_owned_object(self, object_idx: u64) -> Self {
160        self.create_iota_object(object_idx, GAS_VALUE_FOR_TESTING)
161    }
162
163    /// Create a new shared object in the transaction.
164    /// `object_idx` is a convenient representation of the object's ID.
165    /// The object will be created as a IOTA coin object, with default balance,
166    /// and it is a shared object.
167    pub fn create_shared_object(self, object_idx: u64) -> Self {
168        self.create_coin_object_with_owner(
169            object_idx,
170            Owner::Shared(Version::MIN_VALID_INCL),
171            GAS_VALUE_FOR_TESTING,
172            GAS::type_tag(),
173        )
174    }
175
176    /// Create a new IOTA coin object in the transaction.
177    /// `object_idx` is a convenient representation of the object's ID.
178    /// `balance` is the amount of IOTA to be created.
179    pub fn create_iota_object(self, object_idx: u64, balance: u64) -> Self {
180        let sender_idx = self
181            .checkpoint_builder
182            .next_transaction
183            .as_ref()
184            .unwrap()
185            .sender_idx;
186        self.create_coin_object(object_idx, sender_idx, balance, GAS::type_tag())
187    }
188
189    /// Create a new coin object in the transaction.
190    /// `object_idx` is a convenient representation of the object's ID.
191    /// `owner_idx` is a convenient representation of the object's owner's
192    /// address. `balance` is the amount of IOTA to be created.
193    /// `coin_type` is the type of the coin to be created.
194    pub fn create_coin_object(
195        self,
196        object_idx: u64,
197        owner_idx: u8,
198        balance: u64,
199        coin_type: TypeTag,
200    ) -> Self {
201        self.create_coin_object_with_owner(
202            object_idx,
203            Owner::Address(Self::derive_address(owner_idx)),
204            balance,
205            coin_type,
206        )
207    }
208
209    fn create_coin_object_with_owner(
210        mut self,
211        object_idx: u64,
212        owner: Owner,
213        balance: u64,
214        coin_type: TypeTag,
215    ) -> Self {
216        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
217        let object_id = Self::derive_object_id(object_idx);
218        assert!(
219            !self.live_objects.contains_key(&object_id),
220            "Object already exists: {object_id}. Please use a different object index.",
221        );
222        let move_object = MoveStruct::new_coin(
223            coin_type,
224            // version doesn't matter since we will set it to the lamport version when we finalize
225            // the transaction
226            Version::MIN_VALID_INCL,
227            object_id,
228            balance,
229        );
230        let object = Object::new_move(move_object, owner, TransactionDigest::ZERO);
231        tx_builder.created_objects.insert(object_id, object);
232        self
233    }
234
235    /// Mutate an existing owned object in the transaction.
236    /// `object_idx` is a convenient representation of the object's ID.
237    pub fn mutate_owned_object(mut self, object_idx: u64) -> Self {
238        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
239        let object_id = Self::derive_object_id(object_idx);
240        let object = self
241            .live_objects
242            .get(&object_id)
243            .cloned()
244            .expect("Mutating an object that doesn't exist");
245        tx_builder.mutated_objects.insert(object_id, object);
246        self
247    }
248
249    /// Mutate an existing shared object in the transaction.
250    pub fn mutate_shared_object(self, object_idx: u64) -> Self {
251        self.access_shared_object(object_idx, true)
252    }
253
254    /// Transfer an existing object to a new owner.
255    /// `object_idx` is a convenient representation of the object's ID.
256    /// `recipient_idx` is a convenient representation of the recipient's
257    /// address.
258    pub fn transfer_object(self, object_idx: u64, recipient_idx: u8) -> Self {
259        self.change_object_owner(
260            object_idx,
261            Owner::Address(Self::derive_address(recipient_idx)),
262        )
263    }
264
265    /// Change the owner of an existing object.
266    /// `object_idx` is a convenient representation of the object's ID.
267    /// `owner` is the new owner of the object.
268    pub fn change_object_owner(mut self, object_idx: u64, owner: Owner) -> Self {
269        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
270        let object_id = Self::derive_object_id(object_idx);
271        let mut object = self.live_objects.get(&object_id).unwrap().clone();
272        object.owner = owner;
273        tx_builder.mutated_objects.insert(object_id, object);
274        self
275    }
276
277    /// Transfer part of an existing coin object's balance to a new owner.
278    /// `object_idx` is a convenient representation of the object's ID.
279    /// `new_object_idx` is a convenient representation of the new object's ID.
280    /// `recipient_idx` is a convenient representation of the recipient's
281    /// address. `amount` is the amount of balance to be transferred.
282    pub fn transfer_coin_balance(
283        mut self,
284        object_idx: u64,
285        new_object_idx: u64,
286        recipient_idx: u8,
287        amount: u64,
288    ) -> Self {
289        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
290        let object_id = Self::derive_object_id(object_idx);
291        let mut object = self
292            .live_objects
293            .get(&object_id)
294            .cloned()
295            .expect("Mutating an object that does not exist");
296        let coin_type = object.coin_type_opt().cloned().unwrap();
297        // Withdraw balance from coin object.
298        let move_object = object.data.as_opt_mut_struct().unwrap();
299        let old_balance = move_object.get_coin_value_unchecked();
300        let new_balance = old_balance - amount;
301        move_object.set_coin_value_unchecked(new_balance);
302        tx_builder.mutated_objects.insert(object_id, object);
303
304        // Deposit balance into new coin object.
305        self.create_coin_object(new_object_idx, recipient_idx, amount, coin_type)
306    }
307
308    /// Wrap an existing object in the transaction.
309    /// `object_idx` is a convenient representation of the object's ID.
310    pub fn wrap_object(mut self, object_idx: u64) -> Self {
311        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
312        let object_id = Self::derive_object_id(object_idx);
313        assert!(self.live_objects.contains_key(&object_id));
314        tx_builder.wrapped_objects.insert(object_id);
315        self
316    }
317
318    /// Unwrap an existing object from the transaction.
319    /// `object_idx` is a convenient representation of the object's ID.
320    pub fn unwrap_object(mut self, object_idx: u64) -> Self {
321        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
322        let object_id = Self::derive_object_id(object_idx);
323        assert!(self.wrapped_objects.contains_key(&object_id));
324        tx_builder.unwrapped_objects.insert(object_id);
325        self
326    }
327
328    /// Delete an existing object from the transaction.
329    /// `object_idx` is a convenient representation of the object's ID.
330    pub fn delete_object(mut self, object_idx: u64) -> Self {
331        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
332        let object_id = Self::derive_object_id(object_idx);
333        assert!(self.live_objects.contains_key(&object_id));
334        tx_builder.deleted_objects.insert(object_id);
335        self
336    }
337
338    /// Add an immutable object as an input to the transaction.
339    ///
340    /// Fails if the object is not live or if its owner is not
341    /// [Owner::Immutable]).
342    pub fn read_frozen_object(mut self, object_id: u64) -> Self {
343        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
344        let object_id = Self::derive_object_id(object_id);
345
346        let obj = self
347            .live_objects
348            .get(&object_id)
349            .expect("Frozen object not found");
350
351        assert!(obj.owner().is_immutable());
352        tx_builder.frozen_objects.insert(obj.object_ref());
353        self
354    }
355
356    /// Add a read to a shared object to the transaction's effects.
357    pub fn read_shared_object(self, object_idx: u64) -> Self {
358        self.access_shared_object(object_idx, false)
359    }
360
361    /// Add events to the transaction.
362    /// `events` is a vector of events to be added to the transaction.
363    pub fn with_events(mut self, events: Vec<Event>) -> Self {
364        self.checkpoint_builder
365            .next_transaction
366            .as_mut()
367            .unwrap()
368            .events = Some(events);
369        self
370    }
371
372    /// Add a move call PTB command to the transaction.
373    /// `package` is the ID of the package to be called.
374    /// `module` is the name of the module to be called.
375    /// `function` is the name of the function to be called.
376    pub fn add_move_call(
377        mut self,
378        package: ObjectId,
379        module: &'static str,
380        function: &'static str,
381    ) -> Self {
382        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
383        tx_builder.move_calls.push((package, module, function));
384        self
385    }
386
387    /// Complete the current transaction and add it to the checkpoint.
388    /// This will also finalize all the object changes, and reflect them in the
389    /// live object map.
390    pub fn finish_transaction(mut self) -> Self {
391        let TransactionBuilder {
392            sender_idx,
393            gas,
394            move_calls,
395            created_objects,
396            mutated_objects,
397            unwrapped_objects,
398            wrapped_objects,
399            deleted_objects,
400            frozen_objects,
401            shared_inputs,
402            events,
403        } = self.checkpoint_builder.next_transaction.take().unwrap();
404
405        let sender = Self::derive_address(sender_idx);
406        let events = events.map(TransactionEvents);
407        let events_digest = events.as_ref().map(|events| events.digest());
408
409        let mut pt_builder = ProgrammableTransactionBuilder::new();
410        for (package, module, function) in move_calls {
411            pt_builder
412                .move_call(
413                    package,
414                    Identifier::from_static(module),
415                    Identifier::from_static(function),
416                    vec![],
417                    vec![],
418                )
419                .unwrap();
420        }
421
422        for &object_ref in &frozen_objects {
423            pt_builder
424                .obj(CallArg::ImmutableOrOwned(object_ref))
425                .expect("Failed to add frozen object input");
426        }
427
428        for (id, input) in &shared_inputs {
429            let &Owner::Shared(initial_shared_version) = input.object.owner() else {
430                panic!("Accessing a non-shared object as shared");
431            };
432
433            pt_builder
434                .obj(CallArg::Shared(SharedObjectReference::new(
435                    *id,
436                    initial_shared_version,
437                    input.mutable,
438                )))
439                .expect("Failed to add shared object input");
440        }
441
442        let pt = pt_builder.finish();
443        let tx_data = TransactionData::new(TransactionKind::Programmable(pt), sender, gas, 1, 1);
444        let tx = Transaction::new(SenderSignedData::new(tx_data, vec![]));
445
446        let wrapped_objects: Vec<_> = wrapped_objects
447            .into_iter()
448            .map(|id| self.live_objects.remove(&id).unwrap())
449            .collect();
450        let deleted_objects: Vec<_> = deleted_objects
451            .into_iter()
452            .map(|id| self.live_objects.remove(&id).unwrap())
453            .collect();
454        let unwrapped_objects: Vec<_> = unwrapped_objects
455            .into_iter()
456            .map(|id| self.wrapped_objects.remove(&id).unwrap())
457            .collect();
458
459        let mut effects_builder = TestEffectsBuilder::new(tx.data())
460            .with_created_objects(created_objects.iter().map(|(id, o)| (*id, *o.owner())))
461            .with_mutated_objects(
462                mutated_objects
463                    .iter()
464                    .map(|(id, o)| (*id, o.version(), *o.owner())),
465            )
466            .with_wrapped_objects(wrapped_objects.iter().map(|o| (o.id(), o.version())))
467            .with_unwrapped_objects(unwrapped_objects.iter().map(|o| (o.id(), *o.owner())))
468            .with_deleted_objects(deleted_objects.iter().map(|o| (o.id(), o.version())))
469            .with_frozen_objects(
470                frozen_objects
471                    .into_iter()
472                    .map(|object_ref| object_ref.object_id),
473            )
474            .with_shared_input_versions(
475                shared_inputs
476                    .iter()
477                    .map(|(id, input)| (*id, input.object.version()))
478                    .collect(),
479            );
480
481        if let Some(events_digest) = &events_digest {
482            effects_builder = effects_builder.with_events_digest(*events_digest);
483        }
484
485        let effects = effects_builder.build();
486        let lamport_version = effects.lamport_version();
487        let input_objects: Vec<_> = mutated_objects
488            .keys()
489            .chain(
490                shared_inputs
491                    .iter()
492                    .filter(|(_, i)| i.mutable)
493                    .map(|(id, _)| id),
494            )
495            .map(|id| self.live_objects.get(id).unwrap().clone())
496            .chain(deleted_objects)
497            .chain(wrapped_objects.clone())
498            .chain(std::iter::once(
499                self.live_objects.get(&gas.object_id).unwrap().clone(),
500            ))
501            .collect();
502        let output_objects: Vec<_> = created_objects
503            .values()
504            .cloned()
505            .chain(mutated_objects.values().cloned())
506            .chain(
507                shared_inputs
508                    .values()
509                    .filter(|i| i.mutable)
510                    .map(|i| i.object.clone()),
511            )
512            .chain(unwrapped_objects)
513            .chain(std::iter::once(
514                self.live_objects.get(&gas.object_id).cloned().unwrap(),
515            ))
516            .map(|mut o| {
517                o.data
518                    .as_opt_mut_struct()
519                    .unwrap()
520                    .increment_version_to(lamport_version);
521                o
522            })
523            .collect();
524        self.live_objects
525            .extend(output_objects.iter().map(|o| (o.id(), o.clone())));
526        self.wrapped_objects
527            .extend(wrapped_objects.iter().map(|o| (o.id(), o.clone())));
528
529        self.checkpoint_builder
530            .transactions
531            .push(CheckpointTransaction {
532                transaction: tx,
533                effects,
534                events,
535                input_objects,
536                output_objects,
537            });
538        self
539    }
540
541    /// Creates a transaction that advances the epoch, adds it to the
542    /// checkpoint, and then builds the checkpoint. This increments the
543    /// stored checkpoint sequence number and epoch. If `safe_mode` is true,
544    /// the epoch end transaction will not include the `SystemEpochInfoEvent`.
545    pub fn advance_epoch(&mut self, safe_mode: bool) -> CheckpointData {
546        let (committee, _) = Committee::new_simple_test_committee();
547        let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
548        let tx_kind = EndOfEpochTransactionKind::new_change_epoch(
549            self.checkpoint_builder.epoch + 1,
550            protocol_config.version.as_u64(),
551            Default::default(),
552            Default::default(),
553            Default::default(),
554            Default::default(),
555            Default::default(),
556            Default::default(),
557        );
558
559        // TODO: need the system state object wrapper and dynamic field object to
560        // "correctly" mock advancing epoch, at least to satisfy kv_epoch_starts
561        // pipeline.
562        let end_of_epoch_tx = TransactionData::new(
563            TransactionKind::EndOfEpoch(vec![tx_kind]),
564            Address::ZERO,
565            random_object_ref(),
566            1,
567            1,
568        )
569        .pipe(|data| SenderSignedData::new(data, vec![]))
570        .pipe(Transaction::new);
571
572        let events = if !safe_mode {
573            let system_epoch_info_event = SystemEpochInfoEventV2 {
574                epoch: self.checkpoint_builder.epoch,
575                protocol_version: protocol_config.version.as_u64(),
576                ..Default::default()
577            };
578            Some(vec![Event {
579                package_id: ObjectId::SYSTEM,
580                module: Identifier::from_static("iota_system_state_inner"),
581                sender: TestCheckpointDataBuilder::derive_address(0),
582                type_: StructTag::new_system_epoch_info_event(),
583                contents: bcs::to_bytes(&system_epoch_info_event).unwrap(),
584            }])
585        } else {
586            None
587        };
588
589        let transaction_events = events.map(TransactionEvents);
590
591        let effects = TransactionEffects::new_empty_v1_for_testing(*end_of_epoch_tx.digest());
592
593        // Similar to calling self.finish_transaction()
594        self.checkpoint_builder
595            .transactions
596            .push(CheckpointTransaction {
597                transaction: end_of_epoch_tx,
598                effects,
599                events: transaction_events,
600                input_objects: vec![],
601                output_objects: vec![],
602            });
603
604        // Call build_checkpoint() to finalize the checkpoint and then populate the
605        // checkpoint with additional end of epoch data.
606        let mut checkpoint = self.build_checkpoint();
607        let end_of_epoch_data = EndOfEpochData {
608            next_epoch_committee: committee.committee_members(),
609            next_epoch_protocol_version: protocol_config.version.as_u64(),
610            epoch_commitments: vec![],
611            // Do not simulate supply changes in tests.
612            epoch_supply_change: 0,
613        };
614        checkpoint.checkpoint_summary.end_of_epoch_data = Some(end_of_epoch_data);
615        self.checkpoint_builder.epoch += 1;
616        checkpoint
617    }
618
619    /// Build the checkpoint data using all the transactions added to the
620    /// builder so far. This will also increment the stored checkpoint
621    /// sequence number.
622    pub fn build_checkpoint(&mut self) -> CheckpointData {
623        assert!(self.checkpoint_builder.next_transaction.is_none());
624        let transactions = std::mem::take(&mut self.checkpoint_builder.transactions);
625        let contents = CheckpointContents::new_with_digests_only_for_tests(
626            transactions
627                .iter()
628                .map(|tx| ExecutionDigests::new(*tx.transaction.digest(), tx.effects.digest())),
629        );
630
631        self.checkpoint_builder.network_total_transactions += transactions.len() as u64;
632
633        let checkpoint_summary = CheckpointSummary::new_with_protocol_config(
634            &ProtocolConfig::get_for_max_version_UNSAFE(),
635            self.checkpoint_builder.epoch,
636            self.checkpoint_builder.checkpoint,
637            self.checkpoint_builder.network_total_transactions,
638            &contents,
639            None,
640            Default::default(),
641            None,
642            0,
643            vec![],
644        );
645
646        let (committee, keys) = Committee::new_simple_test_committee();
647
648        let checkpoint_cert = CertifiedCheckpointSummary::new_from_keypairs_for_testing(
649            checkpoint_summary,
650            &keys,
651            &committee,
652        );
653
654        self.checkpoint_builder.checkpoint += 1;
655        CheckpointData {
656            checkpoint_summary: checkpoint_cert,
657            checkpoint_contents: contents,
658            transactions,
659        }
660    }
661
662    /// Derive an object ID from an index. This is used to conveniently
663    /// represent an object's ID. We ensure that the bytes of object IDs
664    /// have a stable order that is the same as object_idx.
665    pub fn derive_object_id(object_idx: u64) -> ObjectId {
666        // We achieve this by setting the first 8 bytes of the object ID to the
667        // object_idx.
668        let mut bytes = [0; ObjectId::LENGTH];
669        bytes[0..8].copy_from_slice(&object_idx.to_le_bytes());
670        ObjectId::from_bytes(bytes).unwrap()
671    }
672
673    /// Derive an address from an index.
674    pub fn derive_address(address_idx: u8) -> Address {
675        dbg_addr(address_idx)
676    }
677
678    /// Add a shared input to the transaction, being accessed from the currently
679    /// recorded live version.
680    fn access_shared_object(mut self, object_idx: u64, mutable: bool) -> Self {
681        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
682        let object_id = Self::derive_object_id(object_idx);
683        let object = self
684            .live_objects
685            .get(&object_id)
686            .cloned()
687            .expect("Accessing a shared object that doesn't exist");
688        tx_builder
689            .shared_inputs
690            .insert(object_id, Shared { mutable, object });
691        self
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use std::str::FromStr;
698
699    use iota_sdk_types::Command;
700
701    use super::*;
702    use crate::{
703        ObjectId,
704        transaction::{TransactionDataAPI, TransactionKindExt},
705    };
706    #[test]
707    fn test_basic_checkpoint_builder() {
708        // Create a checkpoint with a single transaction that does nothing.
709        let checkpoint = TestCheckpointDataBuilder::new(1)
710            .with_epoch(5)
711            .start_transaction(0)
712            .finish_transaction()
713            .build_checkpoint();
714
715        assert_eq!(checkpoint.checkpoint_summary.sequence_number(), 1);
716        assert_eq!(checkpoint.checkpoint_summary.epoch, 5);
717        assert_eq!(checkpoint.transactions.len(), 1);
718        let tx = &checkpoint.transactions[0];
719        assert_eq!(
720            tx.transaction.sender_address(),
721            TestCheckpointDataBuilder::derive_address(0)
722        );
723        assert_eq!(tx.effects.mutated().len(), 1); // gas object
724        assert_eq!(tx.effects.deleted().len(), 0);
725        assert_eq!(tx.effects.created().len(), 0);
726        assert_eq!(tx.input_objects.len(), 1);
727        assert_eq!(tx.output_objects.len(), 1);
728    }
729
730    #[test]
731    fn test_multiple_transactions() {
732        let checkpoint = TestCheckpointDataBuilder::new(1)
733            .start_transaction(0)
734            .finish_transaction()
735            .start_transaction(1)
736            .finish_transaction()
737            .start_transaction(2)
738            .finish_transaction()
739            .build_checkpoint();
740
741        assert_eq!(checkpoint.transactions.len(), 3);
742
743        // Verify transactions have different senders (since we used 0, 1, 2 as sender
744        // indices above).
745        let senders: Vec<_> = checkpoint
746            .transactions
747            .iter()
748            .map(|tx| tx.transaction.transaction().sender())
749            .collect();
750        assert_eq!(
751            senders,
752            vec![
753                TestCheckpointDataBuilder::derive_address(0),
754                TestCheckpointDataBuilder::derive_address(1),
755                TestCheckpointDataBuilder::derive_address(2)
756            ]
757        );
758    }
759
760    #[test]
761    fn test_object_creation() {
762        let checkpoint = TestCheckpointDataBuilder::new(1)
763            .start_transaction(0)
764            .create_owned_object(0)
765            .finish_transaction()
766            .build_checkpoint();
767
768        let tx = &checkpoint.transactions[0];
769        let created_obj_id = TestCheckpointDataBuilder::derive_object_id(0);
770
771        // Verify the newly created object appears in output objects
772        assert!(
773            tx.output_objects
774                .iter()
775                .any(|obj| obj.id() == created_obj_id)
776        );
777
778        // Verify effects show object creation
779        assert!(
780            tx.effects
781                .created()
782                .iter()
783                .any(|(object_ref, owner)| object_ref.object_id == created_obj_id
784                    && owner.address_or_object().unwrap()
785                        == &TestCheckpointDataBuilder::derive_address(0))
786        );
787    }
788
789    #[test]
790    fn test_object_mutation() {
791        let checkpoint = TestCheckpointDataBuilder::new(1)
792            .start_transaction(0)
793            .create_owned_object(0)
794            .finish_transaction()
795            .start_transaction(0)
796            .mutate_owned_object(0)
797            .finish_transaction()
798            .build_checkpoint();
799
800        let tx = &checkpoint.transactions[1];
801        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
802
803        // Verify object appears in both input and output objects
804        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
805        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
806
807        // Verify effects show object mutation
808        assert!(
809            tx.effects
810                .mutated()
811                .iter()
812                .any(|(object_ref, _)| object_ref.object_id == obj_id)
813        );
814    }
815
816    #[test]
817    fn test_object_deletion() {
818        let checkpoint = TestCheckpointDataBuilder::new(1)
819            .start_transaction(0)
820            .create_owned_object(0)
821            .finish_transaction()
822            .start_transaction(0)
823            .delete_object(0)
824            .finish_transaction()
825            .build_checkpoint();
826
827        let tx = &checkpoint.transactions[1];
828        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
829
830        // Verify object appears in input objects but not output
831        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
832        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
833
834        // Verify effects show object deletion
835        assert!(
836            tx.effects
837                .deleted()
838                .iter()
839                .any(|object_ref| object_ref.object_id == obj_id)
840        );
841    }
842
843    #[test]
844    fn test_object_wrapping() {
845        let checkpoint = TestCheckpointDataBuilder::new(1)
846            .start_transaction(0)
847            .create_owned_object(0)
848            .finish_transaction()
849            .start_transaction(0)
850            .wrap_object(0)
851            .finish_transaction()
852            .start_transaction(0)
853            .unwrap_object(0)
854            .finish_transaction()
855            .build_checkpoint();
856
857        let tx = &checkpoint.transactions[1];
858        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
859
860        // Verify object appears in input objects but not output
861        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
862        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
863
864        // Verify effects show object wrapping
865        assert!(
866            tx.effects
867                .wrapped()
868                .iter()
869                .any(|object_ref| object_ref.object_id == obj_id)
870        );
871
872        let tx = &checkpoint.transactions[2];
873
874        // Verify object appears in output objects but not input
875        assert!(!tx.input_objects.iter().any(|obj| obj.id() == obj_id));
876        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
877
878        // Verify effects show object unwrapping
879        assert!(
880            tx.effects
881                .unwrapped()
882                .iter()
883                .any(|(object_ref, _owner)| object_ref.object_id == obj_id)
884        );
885    }
886
887    #[test]
888    fn test_object_transfer() {
889        let checkpoint = TestCheckpointDataBuilder::new(1)
890            .start_transaction(0)
891            .create_owned_object(0)
892            .finish_transaction()
893            .start_transaction(1)
894            .transfer_object(0, 1)
895            .finish_transaction()
896            .build_checkpoint();
897
898        let tx = &checkpoint.transactions[1];
899        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
900
901        // Verify object appears in input and output objects
902        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
903        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
904
905        // Verify effects show object transfer
906        assert!(
907            tx.effects
908                .mutated()
909                .iter()
910                .any(|(object_ref, owner)| object_ref.object_id == obj_id
911                    && owner.address_or_object().unwrap()
912                        == &TestCheckpointDataBuilder::derive_address(1))
913        );
914    }
915
916    #[test]
917    fn test_shared_object() {
918        let checkpoint = TestCheckpointDataBuilder::new(1)
919            .start_transaction(0)
920            .create_shared_object(0)
921            .finish_transaction()
922            .build_checkpoint();
923
924        let tx = &checkpoint.transactions[0];
925        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
926
927        // Verify object appears in output objects and is shared
928        assert!(
929            tx.output_objects
930                .iter()
931                .any(|obj| obj.id() == obj_id && obj.owner().is_shared())
932        );
933    }
934
935    #[test]
936    fn test_freeze_object() {
937        let checkpoint = TestCheckpointDataBuilder::new(1)
938            .start_transaction(0)
939            .create_owned_object(0)
940            .finish_transaction()
941            .start_transaction(0)
942            .change_object_owner(0, Owner::Immutable)
943            .finish_transaction()
944            .build_checkpoint();
945
946        let tx = &checkpoint.transactions[1];
947        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
948
949        // Verify object appears in output objects and is immutable
950        assert!(
951            tx.output_objects
952                .iter()
953                .any(|obj| obj.id() == obj_id && obj.owner().is_immutable())
954        );
955    }
956
957    #[test]
958    fn test_iota_balance_transfer() {
959        let checkpoint = TestCheckpointDataBuilder::new(1)
960            .start_transaction(0)
961            .create_iota_object(0, 100)
962            .finish_transaction()
963            .start_transaction(1)
964            .transfer_coin_balance(0, 1, 1, 10)
965            .finish_transaction()
966            .build_checkpoint();
967
968        let tx = &checkpoint.transactions[0];
969        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
970
971        // Verify the newly created object appears in output objects and is a gas coin
972        // with 100 NANOS.
973        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
974            && obj.is_gas_coin()
975            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 100));
976
977        let tx = &checkpoint.transactions[1];
978        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
979
980        // Verify the original IOTA coin now has 90 NANOS after the transfer.
981        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
982            && obj.is_gas_coin()
983            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 90));
984
985        // Verify the split out IOTA coin has 10 NANOS.
986        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
987            && obj.is_gas_coin()
988            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 10));
989    }
990
991    #[test]
992    fn test_coin_balance_transfer() {
993        let type_tag = TypeTag::from_str("0x100::a::b").unwrap();
994        let checkpoint = TestCheckpointDataBuilder::new(1)
995            .start_transaction(0)
996            .create_coin_object(0, 0, 100, type_tag.clone())
997            .finish_transaction()
998            .start_transaction(1)
999            .transfer_coin_balance(0, 1, 1, 10)
1000            .finish_transaction()
1001            .build_checkpoint();
1002
1003        let tx = &checkpoint.transactions[1];
1004        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
1005        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
1006
1007        // Verify the original coin now has 90 balance after the transfer.
1008        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
1009            && obj.coin_type_opt() == Some(&type_tag)
1010            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 90));
1011
1012        // Verify the split out coin has 10 balance, with the same type tag.
1013        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
1014            && obj.coin_type_opt() == Some(&type_tag)
1015            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 10));
1016    }
1017
1018    #[test]
1019    fn test_events() {
1020        let checkpoint = TestCheckpointDataBuilder::new(1)
1021            .start_transaction(0)
1022            .with_events(vec![Event {
1023                package_id: ObjectId::ZERO,
1024                module: Identifier::from_static("test"),
1025                sender: TestCheckpointDataBuilder::derive_address(0),
1026                type_: StructTag::new_gas(),
1027                contents: vec![],
1028            }])
1029            .finish_transaction()
1030            .build_checkpoint();
1031        let tx = &checkpoint.transactions[0];
1032
1033        // Verify the transaction has an events digest
1034        assert!(tx.effects.events_digest().is_some());
1035
1036        // Verify the transaction has a single event
1037        assert_eq!(tx.events.as_ref().unwrap().len(), 1);
1038    }
1039
1040    #[test]
1041    fn test_move_call() {
1042        let checkpoint = TestCheckpointDataBuilder::new(1)
1043            .start_transaction(0)
1044            .add_move_call(ObjectId::ZERO, "test", "test")
1045            .finish_transaction()
1046            .build_checkpoint();
1047        let tx = &checkpoint.transactions[0];
1048
1049        // Verify the transaction has a move call matching the arguments provided.
1050        assert!(
1051            tx.transaction
1052                .transaction()
1053                .kind()
1054                .iter_commands()
1055                .any(|cmd| {
1056                    cmd == &Command::new_move_call(
1057                        ObjectId::ZERO,
1058                        Identifier::new_unchecked("test"),
1059                        Identifier::new_unchecked("test"),
1060                        vec![],
1061                        vec![],
1062                    )
1063                })
1064        )
1065    }
1066
1067    #[test]
1068    fn test_multiple_checkpoints() {
1069        let mut builder = TestCheckpointDataBuilder::new(1)
1070            .start_transaction(0)
1071            .create_owned_object(0)
1072            .finish_transaction();
1073        let checkpoint1 = builder.build_checkpoint();
1074        builder = builder
1075            .start_transaction(0)
1076            .mutate_owned_object(0)
1077            .finish_transaction();
1078        let checkpoint2 = builder.build_checkpoint();
1079        builder = builder
1080            .start_transaction(0)
1081            .delete_object(0)
1082            .finish_transaction();
1083        let checkpoint3 = builder.build_checkpoint();
1084
1085        // Verify the sequence numbers are consecutive.
1086        assert_eq!(checkpoint1.checkpoint_summary.sequence_number, 1);
1087        assert_eq!(checkpoint2.checkpoint_summary.sequence_number, 2);
1088        assert_eq!(checkpoint3.checkpoint_summary.sequence_number, 3);
1089    }
1090}