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