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    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                ..Default::default()
579            };
580            Some(vec![Event {
581                package_id: ObjectId::SYSTEM,
582                module: Identifier::from_static("iota_system_state_inner"),
583                sender: TestCheckpointDataBuilder::derive_address(0),
584                struct_tag: StructTag::new_system_epoch_info_event(),
585                contents: bcs::to_bytes(&system_epoch_info_event).unwrap(),
586            }])
587        } else {
588            None
589        };
590
591        let transaction_events = events.map(TransactionEvents);
592
593        let effects = TransactionEffects::new_empty_v1_for_testing(*end_of_epoch_tx.digest());
594
595        // Similar to calling self.finish_transaction()
596        self.checkpoint_builder
597            .transactions
598            .push(CheckpointTransaction {
599                transaction: end_of_epoch_tx,
600                effects,
601                events: transaction_events,
602                input_objects: vec![],
603                output_objects: vec![],
604            });
605
606        // Call build_checkpoint() to finalize the checkpoint and then populate the
607        // checkpoint with additional end of epoch data.
608        let mut checkpoint = self.build_checkpoint();
609        let end_of_epoch_data = EndOfEpochData {
610            next_epoch_committee: committee.committee_members(),
611            next_epoch_protocol_version: protocol_config.version.as_u64(),
612            epoch_commitments: vec![],
613            // Do not simulate supply changes in tests.
614            epoch_supply_change: 0,
615        };
616        checkpoint.checkpoint_summary.end_of_epoch_data = Some(end_of_epoch_data);
617        self.checkpoint_builder.epoch += 1;
618        checkpoint
619    }
620
621    /// Build the checkpoint data using all the transactions added to the
622    /// builder so far. This will also increment the stored checkpoint
623    /// sequence number.
624    pub fn build_checkpoint(&mut self) -> CheckpointData {
625        assert!(self.checkpoint_builder.next_transaction.is_none());
626        let transactions = std::mem::take(&mut self.checkpoint_builder.transactions);
627        let contents = CheckpointContents::new_with_digests_only_for_tests(
628            transactions
629                .iter()
630                .map(|tx| ExecutionDigests::new(*tx.transaction.digest(), tx.effects.digest())),
631        );
632
633        self.checkpoint_builder.network_total_transactions += transactions.len() as u64;
634
635        let checkpoint_summary = CheckpointSummary::new_with_protocol_config(
636            &ProtocolConfig::get_for_max_version_UNSAFE(),
637            self.checkpoint_builder.epoch,
638            self.checkpoint_builder.checkpoint,
639            self.checkpoint_builder.network_total_transactions,
640            &contents,
641            None,
642            Default::default(),
643            None,
644            0,
645            vec![],
646        );
647
648        let (committee, keys) = Committee::new_simple_test_committee();
649
650        let checkpoint_cert = CertifiedCheckpointSummary::new_from_keypairs_for_testing(
651            checkpoint_summary,
652            &keys,
653            &committee,
654        );
655
656        self.checkpoint_builder.checkpoint += 1;
657        CheckpointData {
658            checkpoint_summary: checkpoint_cert,
659            checkpoint_contents: contents,
660            transactions,
661        }
662    }
663
664    /// Derive an object ID from an index. This is used to conveniently
665    /// represent an object's ID. We ensure that the bytes of object IDs
666    /// have a stable order that is the same as object_idx.
667    pub fn derive_object_id(object_idx: u64) -> ObjectId {
668        // We achieve this by setting the first 8 bytes of the object ID to the
669        // object_idx.
670        let mut bytes = [0; ObjectId::LENGTH];
671        bytes[0..8].copy_from_slice(&object_idx.to_le_bytes());
672        ObjectId::from_bytes(bytes).unwrap()
673    }
674
675    /// Derive an address from an index.
676    pub fn derive_address(address_idx: u8) -> Address {
677        dbg_addr(address_idx)
678    }
679
680    /// Add a shared input to the transaction, being accessed from the currently
681    /// recorded live version.
682    fn access_shared_object(mut self, object_idx: u64, mutable: bool) -> Self {
683        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
684        let object_id = Self::derive_object_id(object_idx);
685        let object = self
686            .live_objects
687            .get(&object_id)
688            .cloned()
689            .expect("Accessing a shared object that doesn't exist");
690        tx_builder
691            .shared_inputs
692            .insert(object_id, Shared { mutable, object });
693        self
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use std::str::FromStr;
700
701    use iota_sdk_types::Command;
702
703    use super::*;
704    use crate::{
705        ObjectId,
706        transaction::{TransactionAPI, TransactionKindExt},
707    };
708    #[test]
709    fn test_basic_checkpoint_builder() {
710        // Create a checkpoint with a single transaction that does nothing.
711        let checkpoint = TestCheckpointDataBuilder::new(1)
712            .with_epoch(5)
713            .start_transaction(0)
714            .finish_transaction()
715            .build_checkpoint();
716
717        assert_eq!(checkpoint.checkpoint_summary.sequence_number(), 1);
718        assert_eq!(checkpoint.checkpoint_summary.epoch, 5);
719        assert_eq!(checkpoint.transactions.len(), 1);
720        let tx = &checkpoint.transactions[0];
721        assert_eq!(
722            tx.transaction.sender_address(),
723            TestCheckpointDataBuilder::derive_address(0)
724        );
725        assert_eq!(tx.effects.mutated().len(), 1); // gas object
726        assert_eq!(tx.effects.deleted().len(), 0);
727        assert_eq!(tx.effects.created().len(), 0);
728        assert_eq!(tx.input_objects.len(), 1);
729        assert_eq!(tx.output_objects.len(), 1);
730    }
731
732    #[test]
733    fn test_multiple_transactions() {
734        let checkpoint = TestCheckpointDataBuilder::new(1)
735            .start_transaction(0)
736            .finish_transaction()
737            .start_transaction(1)
738            .finish_transaction()
739            .start_transaction(2)
740            .finish_transaction()
741            .build_checkpoint();
742
743        assert_eq!(checkpoint.transactions.len(), 3);
744
745        // Verify transactions have different senders (since we used 0, 1, 2 as sender
746        // indices above).
747        let senders: Vec<_> = checkpoint
748            .transactions
749            .iter()
750            .map(|tx| tx.transaction.transaction().sender())
751            .collect();
752        assert_eq!(
753            senders,
754            vec![
755                TestCheckpointDataBuilder::derive_address(0),
756                TestCheckpointDataBuilder::derive_address(1),
757                TestCheckpointDataBuilder::derive_address(2)
758            ]
759        );
760    }
761
762    #[test]
763    fn test_object_creation() {
764        let checkpoint = TestCheckpointDataBuilder::new(1)
765            .start_transaction(0)
766            .create_owned_object(0)
767            .finish_transaction()
768            .build_checkpoint();
769
770        let tx = &checkpoint.transactions[0];
771        let created_obj_id = TestCheckpointDataBuilder::derive_object_id(0);
772
773        // Verify the newly created object appears in output objects
774        assert!(
775            tx.output_objects
776                .iter()
777                .any(|obj| obj.id() == created_obj_id)
778        );
779
780        // Verify effects show object creation
781        assert!(
782            tx.effects
783                .created()
784                .iter()
785                .any(|created| created.reference.object_id == created_obj_id
786                    && created.owner.address_or_object().unwrap()
787                        == &TestCheckpointDataBuilder::derive_address(0))
788        );
789    }
790
791    #[test]
792    fn test_object_mutation() {
793        let checkpoint = TestCheckpointDataBuilder::new(1)
794            .start_transaction(0)
795            .create_owned_object(0)
796            .finish_transaction()
797            .start_transaction(0)
798            .mutate_owned_object(0)
799            .finish_transaction()
800            .build_checkpoint();
801
802        let tx = &checkpoint.transactions[1];
803        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
804
805        // Verify object appears in both input and output objects
806        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
807        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
808
809        // Verify effects show object mutation
810        assert!(
811            tx.effects
812                .mutated()
813                .iter()
814                .any(|changed| changed.reference.object_id == obj_id)
815        );
816    }
817
818    #[test]
819    fn test_object_deletion() {
820        let checkpoint = TestCheckpointDataBuilder::new(1)
821            .start_transaction(0)
822            .create_owned_object(0)
823            .finish_transaction()
824            .start_transaction(0)
825            .delete_object(0)
826            .finish_transaction()
827            .build_checkpoint();
828
829        let tx = &checkpoint.transactions[1];
830        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
831
832        // Verify object appears in input objects but not output
833        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
834        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
835
836        // Verify effects show object deletion
837        assert!(
838            tx.effects
839                .deleted()
840                .iter()
841                .any(|object_ref| object_ref.object_id == obj_id)
842        );
843    }
844
845    #[test]
846    fn test_object_wrapping() {
847        let checkpoint = TestCheckpointDataBuilder::new(1)
848            .start_transaction(0)
849            .create_owned_object(0)
850            .finish_transaction()
851            .start_transaction(0)
852            .wrap_object(0)
853            .finish_transaction()
854            .start_transaction(0)
855            .unwrap_object(0)
856            .finish_transaction()
857            .build_checkpoint();
858
859        let tx = &checkpoint.transactions[1];
860        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
861
862        // Verify object appears in input objects but not output
863        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
864        assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id));
865
866        // Verify effects show object wrapping
867        assert!(
868            tx.effects
869                .wrapped()
870                .iter()
871                .any(|object_ref| object_ref.object_id == obj_id)
872        );
873
874        let tx = &checkpoint.transactions[2];
875
876        // Verify object appears in output objects but not input
877        assert!(!tx.input_objects.iter().any(|obj| obj.id() == obj_id));
878        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
879
880        // Verify effects show object unwrapping
881        assert!(
882            tx.effects
883                .unwrapped()
884                .iter()
885                .any(|changed| changed.reference.object_id == obj_id)
886        );
887    }
888
889    #[test]
890    fn test_object_transfer() {
891        let checkpoint = TestCheckpointDataBuilder::new(1)
892            .start_transaction(0)
893            .create_owned_object(0)
894            .finish_transaction()
895            .start_transaction(1)
896            .transfer_object(0, 1)
897            .finish_transaction()
898            .build_checkpoint();
899
900        let tx = &checkpoint.transactions[1];
901        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
902
903        // Verify object appears in input and output objects
904        assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id));
905        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id));
906
907        // Verify effects show object transfer
908        assert!(
909            tx.effects
910                .mutated()
911                .iter()
912                .any(|changed| changed.reference.object_id == obj_id
913                    && changed.owner.address_or_object().unwrap()
914                        == &TestCheckpointDataBuilder::derive_address(1))
915        );
916    }
917
918    #[test]
919    fn test_shared_object() {
920        let checkpoint = TestCheckpointDataBuilder::new(1)
921            .start_transaction(0)
922            .create_shared_object(0)
923            .finish_transaction()
924            .build_checkpoint();
925
926        let tx = &checkpoint.transactions[0];
927        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
928
929        // Verify object appears in output objects and is shared
930        assert!(
931            tx.output_objects
932                .iter()
933                .any(|obj| obj.id() == obj_id && obj.owner().is_shared())
934        );
935    }
936
937    #[test]
938    fn test_freeze_object() {
939        let checkpoint = TestCheckpointDataBuilder::new(1)
940            .start_transaction(0)
941            .create_owned_object(0)
942            .finish_transaction()
943            .start_transaction(0)
944            .change_object_owner(0, Owner::Immutable)
945            .finish_transaction()
946            .build_checkpoint();
947
948        let tx = &checkpoint.transactions[1];
949        let obj_id = TestCheckpointDataBuilder::derive_object_id(0);
950
951        // Verify object appears in output objects and is immutable
952        assert!(
953            tx.output_objects
954                .iter()
955                .any(|obj| obj.id() == obj_id && obj.owner().is_immutable())
956        );
957    }
958
959    #[test]
960    fn test_iota_balance_transfer() {
961        let checkpoint = TestCheckpointDataBuilder::new(1)
962            .start_transaction(0)
963            .create_iota_object(0, 100)
964            .finish_transaction()
965            .start_transaction(1)
966            .transfer_coin_balance(0, 1, 1, 10)
967            .finish_transaction()
968            .build_checkpoint();
969
970        let tx = &checkpoint.transactions[0];
971        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
972
973        // Verify the newly created object appears in output objects and is a gas coin
974        // with 100 NANOS.
975        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
976            && obj.is_gas_coin()
977            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 100));
978
979        let tx = &checkpoint.transactions[1];
980        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
981
982        // Verify the original IOTA coin now has 90 NANOS after the transfer.
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() == 90));
986
987        // Verify the split out IOTA coin has 10 NANOS.
988        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
989            && obj.is_gas_coin()
990            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 10));
991    }
992
993    #[test]
994    fn test_coin_balance_transfer() {
995        let type_tag = TypeTag::from_str("0x100::a::b").unwrap();
996        let checkpoint = TestCheckpointDataBuilder::new(1)
997            .start_transaction(0)
998            .create_coin_object(0, 0, 100, type_tag.clone())
999            .finish_transaction()
1000            .start_transaction(1)
1001            .transfer_coin_balance(0, 1, 1, 10)
1002            .finish_transaction()
1003            .build_checkpoint();
1004
1005        let tx = &checkpoint.transactions[1];
1006        let obj_id0 = TestCheckpointDataBuilder::derive_object_id(0);
1007        let obj_id1 = TestCheckpointDataBuilder::derive_object_id(1);
1008
1009        // Verify the original coin now has 90 balance after the transfer.
1010        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0
1011            && obj.opt_coin_type() == Some(&type_tag)
1012            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 90));
1013
1014        // Verify the split out coin has 10 balance, with the same type tag.
1015        assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1
1016            && obj.opt_coin_type() == Some(&type_tag)
1017            && obj.data.as_opt_struct().unwrap().get_coin_value_unchecked() == 10));
1018    }
1019
1020    #[test]
1021    fn test_events() {
1022        let checkpoint = TestCheckpointDataBuilder::new(1)
1023            .start_transaction(0)
1024            .with_events(vec![Event {
1025                package_id: ObjectId::ZERO,
1026                module: Identifier::from_static("test"),
1027                sender: TestCheckpointDataBuilder::derive_address(0),
1028                struct_tag: StructTag::new_gas(),
1029                contents: vec![],
1030            }])
1031            .finish_transaction()
1032            .build_checkpoint();
1033        let tx = &checkpoint.transactions[0];
1034
1035        // Verify the transaction has an events digest
1036        assert!(tx.effects.events_digest().is_some());
1037
1038        // Verify the transaction has a single event
1039        assert_eq!(tx.events.as_ref().unwrap().len(), 1);
1040    }
1041
1042    #[test]
1043    fn test_move_call() {
1044        let checkpoint = TestCheckpointDataBuilder::new(1)
1045            .start_transaction(0)
1046            .add_move_call(ObjectId::ZERO, "test", "test")
1047            .finish_transaction()
1048            .build_checkpoint();
1049        let tx = &checkpoint.transactions[0];
1050
1051        // Verify the transaction has a move call matching the arguments provided.
1052        assert!(
1053            tx.transaction
1054                .transaction()
1055                .kind()
1056                .iter_commands()
1057                .any(|cmd| {
1058                    cmd == &Command::new_move_call(
1059                        ObjectId::ZERO,
1060                        Identifier::new_unchecked("test"),
1061                        Identifier::new_unchecked("test"),
1062                        vec![],
1063                        vec![],
1064                    )
1065                })
1066        )
1067    }
1068
1069    #[test]
1070    fn test_multiple_checkpoints() {
1071        let mut builder = TestCheckpointDataBuilder::new(1)
1072            .start_transaction(0)
1073            .create_owned_object(0)
1074            .finish_transaction();
1075        let checkpoint1 = builder.build_checkpoint();
1076        builder = builder
1077            .start_transaction(0)
1078            .mutate_owned_object(0)
1079            .finish_transaction();
1080        let checkpoint2 = builder.build_checkpoint();
1081        builder = builder
1082            .start_transaction(0)
1083            .delete_object(0)
1084            .finish_transaction();
1085        let checkpoint3 = builder.build_checkpoint();
1086
1087        // Verify the sequence numbers are consecutive.
1088        assert_eq!(checkpoint1.checkpoint_summary.sequence_number, 1);
1089        assert_eq!(checkpoint2.checkpoint_summary.sequence_number, 2);
1090        assert_eq!(checkpoint3.checkpoint_summary.sequence_number, 3);
1091    }
1092}