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