iota_types/
test_checkpoint_data_builder.rs

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