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