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