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