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            BTreeSet::new(),
760            end_of_epoch_tx_signed.digest(),
761            lamport_version,
762            changed_objects,
763            None,
764            events_digest,
765            vec![],
766        );
767        self.checkpoint_builder
768            .transactions
769            .push(CheckpointTransaction {
770                transaction: end_of_epoch_tx,
771                effects,
772                events: transaction_events,
773                input_objects: vec![],
774                output_objects,
775            });
776        let mut checkpoint = self.build_checkpoint();
777        let end_of_epoch_data = EndOfEpochData {
778            next_epoch_committee: committee.voting_rights.clone(),
779            next_epoch_protocol_version: protocol_version,
780            epoch_commitments,
781        };
782        checkpoint.summary.end_of_epoch_data = Some(end_of_epoch_data);
783        self.checkpoint_builder.epoch += 1;
784        checkpoint
785    }
786
787    /// Derive an object ID from an index. This is used to conveniently represent an object's ID.
788    /// We ensure that the bytes of object IDs have a stable order that is the same as object_idx.
789    pub fn derive_object_id(object_idx: u64) -> ObjectID {
790        // We achieve this by setting the first 8 bytes of the object ID to the object_idx.
791        let mut bytes = [0; ObjectID::LENGTH];
792        bytes[0..8].copy_from_slice(&object_idx.to_le_bytes());
793        ObjectID::from_bytes(bytes).unwrap()
794    }
795
796    /// Derive an address from an index.
797    pub fn derive_address(address_idx: u8) -> SuiAddress {
798        dbg_addr(address_idx)
799    }
800
801    /// Add a shared input to the transaction, being accessed from the currently recorded live
802    /// version.
803    fn access_shared_object(mut self, object_idx: u64, mutability: bool) -> Self {
804        let tx_builder = self.checkpoint_builder.next_transaction.as_mut().unwrap();
805        let object_id = Self::derive_object_id(object_idx);
806        let object = self
807            .live_objects
808            .get(&object_id)
809            .cloned()
810            .expect("Accessing a shared object that doesn't exist");
811        tx_builder.shared_inputs.insert(
812            object_id,
813            Shared {
814                mutable: mutability,
815                object,
816            },
817        );
818        self
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use std::str::FromStr;
825
826    use move_core_types::ident_str;
827
828    use crate::transaction::{Command, ProgrammableMoveCall, TransactionDataAPI};
829
830    use super::*;
831    #[test]
832    fn test_basic_checkpoint_builder() {
833        // Create a checkpoint with a single transaction that does nothing.
834        let checkpoint = TestCheckpointBuilder::new(1)
835            .with_epoch(5)
836            .start_transaction(0)
837            .finish_transaction()
838            .build_checkpoint();
839
840        assert_eq!(*checkpoint.summary.sequence_number(), 1);
841        assert_eq!(checkpoint.summary.epoch, 5);
842        assert_eq!(checkpoint.transactions.len(), 1);
843        let tx = &checkpoint.transactions[0];
844        assert_eq!(
845            tx.transaction.sender(),
846            TestCheckpointBuilder::derive_address(0)
847        );
848        assert_eq!(tx.effects.mutated().len(), 1); // gas object
849        assert_eq!(tx.effects.deleted().len(), 0);
850        assert_eq!(tx.effects.created().len(), 0);
851        // object_set contains both input and output versions (2 total: input gas + output gas)
852        assert_eq!(checkpoint.object_set.iter().count(), 2);
853    }
854
855    #[test]
856    fn test_multiple_transactions() {
857        let checkpoint = TestCheckpointBuilder::new(1)
858            .start_transaction(0)
859            .finish_transaction()
860            .start_transaction(1)
861            .finish_transaction()
862            .start_transaction(2)
863            .finish_transaction()
864            .build_checkpoint();
865
866        assert_eq!(checkpoint.transactions.len(), 3);
867
868        // Verify transactions have different senders (since we used 0, 1, 2 as sender indices above).
869        let senders: Vec<_> = checkpoint
870            .transactions
871            .iter()
872            .map(|tx| tx.transaction.sender())
873            .collect();
874        assert_eq!(
875            senders,
876            vec![
877                TestCheckpointBuilder::derive_address(0),
878                TestCheckpointBuilder::derive_address(1),
879                TestCheckpointBuilder::derive_address(2)
880            ]
881        );
882    }
883
884    #[test]
885    fn test_object_creation() {
886        let checkpoint = TestCheckpointBuilder::new(1)
887            .start_transaction(0)
888            .create_owned_object(0)
889            .finish_transaction()
890            .build_checkpoint();
891
892        let tx = &checkpoint.transactions[0];
893        let created_obj_id = TestCheckpointBuilder::derive_object_id(0);
894
895        // Verify the newly created object appears in the object set
896        assert!(
897            checkpoint
898                .object_set
899                .iter()
900                .any(|obj| obj.id() == created_obj_id)
901        );
902
903        // Verify effects show object creation
904        assert!(
905            tx.effects
906                .created()
907                .iter()
908                .any(|((id, ..), owner)| *id == created_obj_id
909                    && owner.get_owner_address().unwrap()
910                        == TestCheckpointBuilder::derive_address(0))
911        );
912    }
913
914    #[test]
915    fn test_object_mutation() {
916        let checkpoint = TestCheckpointBuilder::new(1)
917            .start_transaction(0)
918            .create_owned_object(0)
919            .finish_transaction()
920            .start_transaction(0)
921            .mutate_owned_object(0)
922            .finish_transaction()
923            .build_checkpoint();
924
925        let obj_id = TestCheckpointBuilder::derive_object_id(0);
926
927        // Verify object is in the object set
928        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id));
929
930        // Verify effects show object mutation
931        let tx = &checkpoint.transactions[1];
932        assert!(
933            tx.effects
934                .mutated()
935                .iter()
936                .any(|((id, ..), _)| *id == obj_id)
937        );
938    }
939
940    #[test]
941    fn test_object_deletion() {
942        let checkpoint = TestCheckpointBuilder::new(1)
943            .start_transaction(0)
944            .create_owned_object(0)
945            .finish_transaction()
946            .start_transaction(0)
947            .delete_object(0)
948            .finish_transaction()
949            .build_checkpoint();
950
951        let obj_id = TestCheckpointBuilder::derive_object_id(0);
952
953        // The deleted object is still in object_set (it contains both inputs and outputs)
954        // We verify deletion via the effects instead
955        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id));
956
957        // Verify effects show object deletion
958        let tx = &checkpoint.transactions[1];
959        assert!(tx.effects.deleted().iter().any(|(id, ..)| *id == obj_id));
960    }
961
962    #[test]
963    fn test_object_wrapping() {
964        let checkpoint = TestCheckpointBuilder::new(1)
965            .start_transaction(0)
966            .create_owned_object(0)
967            .finish_transaction()
968            .start_transaction(0)
969            .wrap_object(0)
970            .finish_transaction()
971            .start_transaction(0)
972            .unwrap_object(0)
973            .finish_transaction()
974            .build_checkpoint();
975
976        let obj_id = TestCheckpointBuilder::derive_object_id(0);
977
978        // After wrap and unwrap, object should be in the final object set
979        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id));
980
981        // Verify effects show object wrapping
982        let tx = &checkpoint.transactions[1];
983        assert!(tx.effects.wrapped().iter().any(|(id, ..)| *id == obj_id));
984
985        // Verify effects show object unwrapping
986        let tx = &checkpoint.transactions[2];
987        assert!(
988            tx.effects
989                .unwrapped()
990                .iter()
991                .any(|((id, ..), _)| *id == obj_id)
992        );
993    }
994
995    #[test]
996    fn test_object_transfer() {
997        let checkpoint = TestCheckpointBuilder::new(1)
998            .start_transaction(0)
999            .create_owned_object(0)
1000            .finish_transaction()
1001            .start_transaction(1)
1002            .transfer_object(0, 1)
1003            .finish_transaction()
1004            .build_checkpoint();
1005
1006        let obj_id = TestCheckpointBuilder::derive_object_id(0);
1007
1008        // Object should be in the final object set
1009        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id));
1010
1011        // Verify effects show object transfer
1012        let tx = &checkpoint.transactions[1];
1013        assert!(
1014            tx.effects
1015                .mutated()
1016                .iter()
1017                .any(|((id, ..), owner)| *id == obj_id
1018                    && owner.get_owner_address().unwrap()
1019                        == TestCheckpointBuilder::derive_address(1))
1020        );
1021    }
1022
1023    #[test]
1024    fn test_shared_object() {
1025        let checkpoint = TestCheckpointBuilder::new(1)
1026            .start_transaction(0)
1027            .create_shared_object(0)
1028            .finish_transaction()
1029            .build_checkpoint();
1030
1031        let obj_id = TestCheckpointBuilder::derive_object_id(0);
1032
1033        // Verify object is in object set and is shared
1034        assert!(
1035            checkpoint
1036                .object_set
1037                .iter()
1038                .any(|obj| obj.id() == obj_id && obj.owner().is_shared())
1039        );
1040    }
1041
1042    #[test]
1043    fn test_freeze_object() {
1044        let checkpoint = TestCheckpointBuilder::new(1)
1045            .start_transaction(0)
1046            .create_owned_object(0)
1047            .finish_transaction()
1048            .start_transaction(0)
1049            .change_object_owner(0, Owner::Immutable)
1050            .finish_transaction()
1051            .build_checkpoint();
1052
1053        let obj_id = TestCheckpointBuilder::derive_object_id(0);
1054
1055        // Verify object is in object set and is immutable
1056        assert!(
1057            checkpoint
1058                .object_set
1059                .iter()
1060                .any(|obj| obj.id() == obj_id && obj.owner().is_immutable())
1061        );
1062    }
1063
1064    #[test]
1065    fn test_sui_balance_transfer() {
1066        let checkpoint = TestCheckpointBuilder::new(1)
1067            .start_transaction(0)
1068            .create_sui_object(0, 100)
1069            .finish_transaction()
1070            .start_transaction(1)
1071            .transfer_coin_balance(0, 1, 1, 10)
1072            .finish_transaction()
1073            .build_checkpoint();
1074
1075        let obj_id0 = TestCheckpointBuilder::derive_object_id(0);
1076        let obj_id1 = TestCheckpointBuilder::derive_object_id(1);
1077
1078        // Verify both coins are in the final object set with correct balances
1079        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id0
1080            && obj.is_gas_coin()
1081            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90));
1082
1083        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id1
1084            && obj.is_gas_coin()
1085            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10));
1086    }
1087
1088    #[test]
1089    fn test_coin_balance_transfer() {
1090        let type_tag = TypeTag::from_str("0x100::a::b").unwrap();
1091        let checkpoint = TestCheckpointBuilder::new(1)
1092            .start_transaction(0)
1093            .create_coin_object(0, 0, 100, type_tag.clone())
1094            .finish_transaction()
1095            .start_transaction(1)
1096            .transfer_coin_balance(0, 1, 1, 10)
1097            .finish_transaction()
1098            .build_checkpoint();
1099
1100        let obj_id0 = TestCheckpointBuilder::derive_object_id(0);
1101        let obj_id1 = TestCheckpointBuilder::derive_object_id(1);
1102
1103        // Verify both coins are in the final object set with correct balances
1104        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id0
1105            && obj.coin_type_maybe().unwrap() == type_tag
1106            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90));
1107
1108        assert!(checkpoint.object_set.iter().any(|obj| obj.id() == obj_id1
1109            && obj.coin_type_maybe().unwrap() == type_tag
1110            && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10));
1111    }
1112
1113    #[test]
1114    fn test_events() {
1115        let checkpoint = TestCheckpointBuilder::new(1)
1116            .start_transaction(0)
1117            .with_events(vec![Event::new(
1118                &ObjectID::ZERO,
1119                ident_str!("test"),
1120                TestCheckpointBuilder::derive_address(0),
1121                GAS::type_(),
1122                vec![],
1123            )])
1124            .finish_transaction()
1125            .build_checkpoint();
1126        let tx = &checkpoint.transactions[0];
1127
1128        // Verify the transaction has an events digest
1129        assert!(tx.effects.events_digest().is_some());
1130
1131        // Verify the transaction has a single event
1132        assert_eq!(tx.events.as_ref().unwrap().data.len(), 1);
1133    }
1134
1135    #[test]
1136    fn test_move_call() {
1137        let checkpoint = TestCheckpointBuilder::new(1)
1138            .start_transaction(0)
1139            .add_move_call(ObjectID::ZERO, "test", "test")
1140            .finish_transaction()
1141            .build_checkpoint();
1142        let tx = &checkpoint.transactions[0];
1143
1144        // Verify the transaction has a move call matching the arguments provided.
1145        assert!(tx.transaction.kind().iter_commands().any(|cmd| {
1146            cmd == &Command::MoveCall(Box::new(ProgrammableMoveCall {
1147                package: ObjectID::ZERO,
1148                module: "test".to_string(),
1149                function: "test".to_string(),
1150                type_arguments: vec![],
1151                arguments: vec![],
1152            }))
1153        }));
1154    }
1155
1156    #[test]
1157    fn test_multiple_checkpoints() {
1158        let mut builder = TestCheckpointBuilder::new(1)
1159            .start_transaction(0)
1160            .create_owned_object(0)
1161            .finish_transaction();
1162        let checkpoint1 = builder.build_checkpoint();
1163        builder = builder
1164            .start_transaction(0)
1165            .mutate_owned_object(0)
1166            .finish_transaction();
1167        let checkpoint2 = builder.build_checkpoint();
1168        builder = builder
1169            .start_transaction(0)
1170            .delete_object(0)
1171            .finish_transaction();
1172        let checkpoint3 = builder.build_checkpoint();
1173
1174        // Verify the sequence numbers are consecutive.
1175        assert_eq!(checkpoint1.summary.sequence_number, 1);
1176        assert_eq!(checkpoint2.summary.sequence_number, 2);
1177        assert_eq!(checkpoint3.summary.sequence_number, 3);
1178    }
1179}