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