sui_types/
test_checkpoint_data_builder.rs

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