From 094faa433aa28209ee2b91fe90b5e3fe05c7d57b Mon Sep 17 00:00:00 2001 From: Xun Li Date: Fri, 27 Dec 2024 18:17:03 -0800 Subject: [PATCH] Add test checkpoint data builder --- .../src/authority/authority_test_utils.rs | 6 +- .../unit_tests/writeback_cache_tests.rs | 13 +- .../src/unit_tests/authority_tests.rs | 14 +- .../unit_tests/transaction_manager_tests.rs | 47 +- .../src/effects/test_effects_builder.rs | 113 +++- crates/sui-types/src/lib.rs | 1 + crates/sui-types/src/object.rs | 4 +- .../src/test_checkpoint_data_builder.rs | 578 ++++++++++++++++++ 8 files changed, 738 insertions(+), 38 deletions(-) create mode 100644 crates/sui-types/src/test_checkpoint_data_builder.rs diff --git a/crates/sui-core/src/authority/authority_test_utils.rs b/crates/sui-core/src/authority/authority_test_utils.rs index a55c820334da5..6b1a25798b858 100644 --- a/crates/sui-core/src/authority/authority_test_utils.rs +++ b/crates/sui-core/src/authority/authority_test_utils.rs @@ -218,7 +218,11 @@ pub async fn init_state_with_ids_and_versions< ) -> Arc { let state = TestAuthorityBuilder::new().build().await; for (address, object_id, version) in objects { - let obj = Object::with_id_owner_version_for_testing(object_id, version, address); + let obj = Object::with_id_owner_version_for_testing( + object_id, + version, + Owner::AddressOwner(address), + ); state.insert_genesis_object(obj).await; } state diff --git a/crates/sui-core/src/execution_cache/unit_tests/writeback_cache_tests.rs b/crates/sui-core/src/execution_cache/unit_tests/writeback_cache_tests.rs index af0b1213e2541..86398b1ad06ba 100644 --- a/crates/sui-core/src/execution_cache/unit_tests/writeback_cache_tests.rs +++ b/crates/sui-core/src/execution_cache/unit_tests/writeback_cache_tests.rs @@ -1244,7 +1244,11 @@ async fn latest_object_cache_race_test() { std::thread::spawn(move || { let mut version = OBJECT_START_VERSION; while start.elapsed() < Duration::from_secs(2) { - let object = Object::with_id_owner_version_for_testing(object_id, version, owner); + let object = Object::with_id_owner_version_for_testing( + object_id, + version, + Owner::AddressOwner(owner), + ); cache .write_object_entry(&object_id, version, object.into()) @@ -1284,8 +1288,11 @@ async fn latest_object_cache_race_test() { std::thread::sleep(Duration::from_micros(1)); } - let object = - Object::with_id_owner_version_for_testing(object_id, latest_version, owner); + let object = Object::with_id_owner_version_for_testing( + object_id, + latest_version, + Owner::AddressOwner(owner), + ); // because we obtained the ticket before reading the object, we will not write a stale // version to the cache. diff --git a/crates/sui-core/src/unit_tests/authority_tests.rs b/crates/sui-core/src/unit_tests/authority_tests.rs index a7cc0b5590bc3..fe958d294e03f 100644 --- a/crates/sui-core/src/unit_tests/authority_tests.rs +++ b/crates/sui-core/src/unit_tests/authority_tests.rs @@ -1094,7 +1094,7 @@ async fn test_dry_run_dev_inspect_max_gas_version() { let gas_object = Object::with_id_owner_version_for_testing( gas_object_id, SequenceNumber::from_u64(SequenceNumber::MAX.value() - 1), - sender, + Owner::AddressOwner(sender), ); let gas_object_ref = gas_object.compute_object_reference(); validator.insert_genesis_object(gas_object.clone()).await; @@ -5949,8 +5949,16 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { let gas_objects = create_gas_objects(3, sender); let gas_objects_cancelled_txn = create_gas_objects(1, sender); let owned_objects_cancelled_txn = vec![ - Object::with_id_owner_version_for_testing(ObjectID::random(), 1.into(), sender), - Object::with_id_owner_version_for_testing(ObjectID::random(), 2.into(), sender), + Object::with_id_owner_version_for_testing( + ObjectID::random(), + 1.into(), + Owner::AddressOwner(sender), + ), + Object::with_id_owner_version_for_testing( + ObjectID::random(), + 2.into(), + Owner::AddressOwner(sender), + ), ]; // Create the cluster with controlled per object congestion control and cancellation. diff --git a/crates/sui-core/src/unit_tests/transaction_manager_tests.rs b/crates/sui-core/src/unit_tests/transaction_manager_tests.rs index f1b04cee8247e..c5cce05433349 100644 --- a/crates/sui-core/src/unit_tests/transaction_manager_tests.rs +++ b/crates/sui-core/src/unit_tests/transaction_manager_tests.rs @@ -5,6 +5,7 @@ use std::{time::Duration, vec}; use sui_test_transaction_builder::TestTransactionBuilder; use sui_types::executable_transaction::VerifiedExecutableTransaction; +use sui_types::object::Owner; use sui_types::transaction::VerifiedTransaction; use sui_types::{ base_types::{ObjectID, SequenceNumber}, @@ -120,8 +121,11 @@ async fn transaction_manager_basics() { transaction_manager.check_empty_for_testing(); // Enqueue a transaction with a new gas object, empty input. - let gas_object_new = - Object::with_id_owner_version_for_testing(ObjectID::random(), 0.into(), owner); + let gas_object_new = Object::with_id_owner_version_for_testing( + ObjectID::random(), + 0.into(), + Owner::AddressOwner(owner), + ); let transaction = make_transaction(gas_object_new.clone(), vec![]); let tx_start_time = Instant::now(); transaction_manager.enqueue(vec![transaction.clone()], &state.epoch_store_for_testing()); @@ -386,7 +390,11 @@ async fn transaction_manager_receiving_notify_commit() { let obj_id = ObjectID::random(); let object_arguments: Vec<_> = (0..10) .map(|i| { - let object = Object::with_id_owner_version_for_testing(obj_id, i.into(), owner); + let object = Object::with_id_owner_version_for_testing( + obj_id, + i.into(), + Owner::AddressOwner(owner), + ); // Every other transaction receives the object, and we create a run of multiple receives in // a row at the beginning to test that the TM doesn't get stuck in either configuration of: // ImmOrOwnedObject => Receiving, @@ -475,8 +483,10 @@ async fn transaction_manager_receiving_object_ready_notifications() { transaction_manager.check_empty_for_testing(); let obj_id = ObjectID::random(); - let receiving_object_new0 = Object::with_id_owner_version_for_testing(obj_id, 0.into(), owner); - let receiving_object_new1 = Object::with_id_owner_version_for_testing(obj_id, 1.into(), owner); + let receiving_object_new0 = + Object::with_id_owner_version_for_testing(obj_id, 0.into(), Owner::AddressOwner(owner)); + let receiving_object_new1 = + Object::with_id_owner_version_for_testing(obj_id, 1.into(), Owner::AddressOwner(owner)); let receiving_object_arg0 = ObjectArg::Receiving(receiving_object_new0.compute_object_reference()); let receive_object_transaction0 = make_transaction( @@ -561,8 +571,10 @@ async fn transaction_manager_receiving_object_ready_notifications_multiple_of_sa transaction_manager.check_empty_for_testing(); let obj_id = ObjectID::random(); - let receiving_object_new0 = Object::with_id_owner_version_for_testing(obj_id, 0.into(), owner); - let receiving_object_new1 = Object::with_id_owner_version_for_testing(obj_id, 1.into(), owner); + let receiving_object_new0 = + Object::with_id_owner_version_for_testing(obj_id, 0.into(), Owner::AddressOwner(owner)); + let receiving_object_new1 = + Object::with_id_owner_version_for_testing(obj_id, 1.into(), Owner::AddressOwner(owner)); let receiving_object_arg0 = ObjectArg::Receiving(receiving_object_new0.compute_object_reference()); let receive_object_transaction0 = make_transaction( @@ -661,8 +673,11 @@ async fn transaction_manager_receiving_object_ready_if_current_version_greater() Object::with_id_owner_for_testing(gas_object_id, owner) }) .collect(); - let receiving_object = - Object::with_id_owner_version_for_testing(ObjectID::random(), 10.into(), owner); + let receiving_object = Object::with_id_owner_version_for_testing( + ObjectID::random(), + 10.into(), + Owner::AddressOwner(owner), + ); gas_objects.push(receiving_object.clone()); let state = init_state_with_objects(gas_objects.clone()).await; @@ -674,10 +689,16 @@ async fn transaction_manager_receiving_object_ready_if_current_version_greater() // TM should be empty at the beginning. transaction_manager.check_empty_for_testing(); - let receiving_object_new0 = - Object::with_id_owner_version_for_testing(receiving_object.id(), 0.into(), owner); - let receiving_object_new1 = - Object::with_id_owner_version_for_testing(receiving_object.id(), 1.into(), owner); + let receiving_object_new0 = Object::with_id_owner_version_for_testing( + receiving_object.id(), + 0.into(), + Owner::AddressOwner(owner), + ); + let receiving_object_new1 = Object::with_id_owner_version_for_testing( + receiving_object.id(), + 1.into(), + Owner::AddressOwner(owner), + ); let receiving_object_arg0 = ObjectArg::Receiving(receiving_object_new0.compute_object_reference()); let receive_object_transaction0 = make_transaction( diff --git a/crates/sui-types/src/effects/test_effects_builder.rs b/crates/sui-types/src/effects/test_effects_builder.rs index 027a131e0c18a..77d952525842c 100644 --- a/crates/sui-types/src/effects/test_effects_builder.rs +++ b/crates/sui-types/src/effects/test_effects_builder.rs @@ -19,6 +19,10 @@ pub struct TestEffectsBuilder { /// Provide the assigned versions for all shared objects. shared_input_versions: BTreeMap, events_digest: Option, + created_objects: Vec<(ObjectID, Owner)>, + /// Objects that are mutated, old version and new owners. + mutated_objects: Vec<(ObjectID, SequenceNumber, Owner)>, + deleted_objects: Vec<(ObjectID, SequenceNumber)>, } impl TestEffectsBuilder { @@ -28,6 +32,9 @@ impl TestEffectsBuilder { status: None, shared_input_versions: BTreeMap::new(), events_digest: None, + created_objects: vec![], + mutated_objects: vec![], + deleted_objects: vec![], } } @@ -50,7 +57,33 @@ impl TestEffectsBuilder { self } + pub fn with_created_objects( + mut self, + objects: impl IntoIterator, + ) -> Self { + self.created_objects.extend(objects); + self + } + + pub fn with_mutated_objects( + mut self, + // Object ID, old version, and new owner. + objects: impl IntoIterator, + ) -> Self { + self.mutated_objects.extend(objects); + self + } + + pub fn with_deleted_objects( + mut self, + objects: impl IntoIterator, + ) -> Self { + self.deleted_objects.extend(objects); + self + } + pub fn build(self) -> TransactionEffects { + let lamport_version = self.get_lamport_version(); let status = self.status.unwrap_or_else(|| ExecutionStatus::Success); // TODO: This does not yet support deleted shared objects. let shared_objects = self @@ -59,22 +92,6 @@ impl TestEffectsBuilder { .map(|(id, version)| SharedInput::Existing((*id, *version, ObjectDigest::MIN))) .collect(); let executed_epoch = 0; - let lamport_version = SequenceNumber::lamport_increment( - self.transaction - .transaction_data() - .input_objects() - .unwrap() - .iter() - .filter_map(|kind| kind.version()) - .chain( - self.transaction - .transaction_data() - .receiving_objects() - .iter() - .map(|oref| oref.1), - ) - .chain(self.shared_input_versions.values().copied()), - ); let sender = self.transaction.transaction_data().sender(); // TODO: Include receiving objects in the object changes as well. let changed_objects = self @@ -130,6 +147,49 @@ impl TestEffectsBuilder { }, )), }) + .chain(self.created_objects.into_iter().map(|(id, owner)| { + ( + id, + EffectsObjectChange { + input_state: ObjectIn::NotExist, + output_state: ObjectOut::ObjectWrite((ObjectDigest::random(), owner)), + id_operation: IDOperation::Created, + }, + ) + })) + .chain( + self.mutated_objects + .into_iter() + .map(|(id, version, owner)| { + ( + id, + EffectsObjectChange { + input_state: ObjectIn::Exist(( + (version, ObjectDigest::random()), + Owner::AddressOwner(sender), + )), + output_state: ObjectOut::ObjectWrite(( + ObjectDigest::random(), + owner, + )), + id_operation: IDOperation::None, + }, + ) + }), + ) + .chain(self.deleted_objects.into_iter().map(|(id, version)| { + ( + id, + EffectsObjectChange { + input_state: ObjectIn::Exist(( + (version, ObjectDigest::random()), + Owner::AddressOwner(sender), + )), + output_state: ObjectOut::NotExist, + id_operation: IDOperation::Deleted, + }, + ) + })) .collect(); let gas_object_id = self.transaction.transaction_data().gas()[0].0; let event_digest = self.events_digest; @@ -148,4 +208,25 @@ impl TestEffectsBuilder { dependencies, ) } + + fn get_lamport_version(&self) -> SequenceNumber { + SequenceNumber::lamport_increment( + self.transaction + .transaction_data() + .input_objects() + .unwrap() + .iter() + .filter_map(|kind| kind.version()) + .chain( + self.transaction + .transaction_data() + .receiving_objects() + .iter() + .map(|oref| oref.1), + ) + .chain(self.shared_input_versions.values().copied()) + .chain(self.mutated_objects.iter().map(|(_, v, _)| *v)) + .chain(self.deleted_objects.iter().map(|(_, v)| *v)), + ) + } } diff --git a/crates/sui-types/src/lib.rs b/crates/sui-types/src/lib.rs index f9b6d0abf477e..c815c98822274 100644 --- a/crates/sui-types/src/lib.rs +++ b/crates/sui-types/src/lib.rs @@ -80,6 +80,7 @@ pub mod sui_sdk_types_conversions; pub mod sui_serde; pub mod sui_system_state; pub mod supported_protocol_versions; +pub mod test_checkpoint_data_builder; pub mod traffic_control; pub mod transaction; pub mod transaction_executor; diff --git a/crates/sui-types/src/object.rs b/crates/sui-types/src/object.rs index db9971c1192bd..65bf9a7bea5ae 100644 --- a/crates/sui-types/src/object.rs +++ b/crates/sui-types/src/object.rs @@ -1079,7 +1079,7 @@ impl Object { pub fn with_id_owner_version_for_testing( id: ObjectID, version: SequenceNumber, - owner: SuiAddress, + owner: Owner, ) -> Self { let data = Data::Move(MoveObject { type_: GasCoin::type_().into(), @@ -1088,7 +1088,7 @@ impl Object { contents: GasCoin::new(id, GAS_VALUE_FOR_TESTING).to_bcs_bytes(), }); ObjectInner { - owner: Owner::AddressOwner(owner), + owner, data, previous_transaction: TransactionDigest::genesis_marker(), storage_rebate: 0, diff --git a/crates/sui-types/src/test_checkpoint_data_builder.rs b/crates/sui-types/src/test_checkpoint_data_builder.rs new file mode 100644 index 0000000000000..fb72a826fe543 --- /dev/null +++ b/crates/sui-types/src/test_checkpoint_data_builder.rs @@ -0,0 +1,578 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; + +use move_core_types::language_storage::TypeTag; +use sui_protocol_config::ProtocolConfig; + +use crate::{ + base_types::{dbg_addr, ExecutionDigests, ObjectID, ObjectRef, SequenceNumber, SuiAddress}, + coin::Coin, + committee::Committee, + digests::TransactionDigest, + effects::{TestEffectsBuilder, TransactionEffectsAPI, TransactionEvents}, + event::Event, + full_checkpoint_content::{CheckpointData, CheckpointTransaction}, + gas_coin::GAS, + message_envelope::Message, + messages_checkpoint::{CertifiedCheckpointSummary, CheckpointContents, CheckpointSummary}, + object::{MoveObject, Object, Owner, GAS_VALUE_FOR_TESTING}, + programmable_transaction_builder::ProgrammableTransactionBuilder, + transaction::{SenderSignedData, Transaction, TransactionData, TransactionKind}, +}; + +/// A builder for creating test checkpoint data. +/// Once initialized, the builder can be used to create transactions and add them to the checkpoint. +/// Call `start_transaction` to begin creating a new transaction. +/// Call `finish_transaction` to complete the current transaction and add it to the checkpoint. +/// After all transactions are added, call `build` to get the final checkpoint data. +pub struct TestCheckpointDataBuilder { + checkpoint: u64, + epoch: u64, + object_map: HashMap, + gas_map: HashMap, + transactions: Vec, + next_transaction: Option, +} + +struct TransactionBuilder { + sender_idx: u8, + gas: ObjectRef, + created_objects: Vec, + mutated_objects: Vec, + deleted_objects: Vec, + events: Option>, +} + +impl TransactionBuilder { + pub fn new(sender_idx: u8, gas: ObjectRef) -> Self { + Self { + sender_idx, + gas, + created_objects: vec![], + mutated_objects: vec![], + deleted_objects: vec![], + events: None, + } + } +} + +impl TestCheckpointDataBuilder { + pub fn new(checkpoint: u64) -> Self { + Self { + checkpoint, + epoch: 0, + object_map: HashMap::new(), + gas_map: HashMap::new(), + transactions: vec![], + next_transaction: None, + } + } + + /// Set the epoch for the checkpoint. + pub fn with_epoch(mut self, epoch: u64) -> Self { + self.epoch = epoch; + self + } + + /// Start creating a new transaction. + /// `sender_idx` is a convenient representation of the sender's address. + /// A proper SuiAddress will be derived from it. + pub fn start_transaction(mut self, sender_idx: u8) -> Self { + assert!(self.next_transaction.is_none()); + let sender = dbg_addr(sender_idx); + let gas_id = self.gas_map.entry(sender).or_insert_with(|| { + let gas = Object::with_owner_for_testing(sender); + let id = gas.id(); + self.object_map.insert(id, gas); + id + }); + let gas_ref = self + .object_map + .get(gas_id) + .cloned() + .unwrap() + .compute_object_reference(); + self.next_transaction = Some(TransactionBuilder::new(sender_idx, gas_ref)); + self + } + + /// Create a new object in the transaction. + /// `object_idx` is a convenient representation of the object's ID. + /// The object will be created as a SUI coin object, with default balance, + /// and the transaction sender as its owner. + pub fn create_object(self, object_idx: u64) -> Self { + self.create_sui_object(object_idx, GAS_VALUE_FOR_TESTING) + } + + /// Create a new SUI coin object in the transaction. + /// `object_idx` is a convenient representation of the object's ID. + /// `balance` is the amount of SUI to be created. + pub fn create_sui_object(self, object_idx: u64, balance: u64) -> Self { + let sender_idx = self.next_transaction.as_ref().unwrap().sender_idx; + self.create_coin_object(object_idx, sender_idx, balance, GAS::type_tag()) + } + + /// Create a new coin object in the transaction. + /// `object_idx` is a convenient representation of the object's ID. + /// `owner_idx` is a convenient representation of the object's owner's address. + /// `balance` is the amount of SUI to be created. + /// `coin_type` is the type of the coin to be created. + pub fn create_coin_object( + mut self, + object_idx: u64, + owner_idx: u8, + balance: u64, + coin_type: TypeTag, + ) -> Self { + let tx_builder = self.next_transaction.as_mut().unwrap(); + let object_id = derive_object_id(object_idx); + let move_object = MoveObject::new_coin( + Coin::type_(coin_type).into(), + SequenceNumber::MIN, + object_id, + balance, + ); + let object = Object::new_move( + move_object, + Owner::AddressOwner(dbg_addr(owner_idx)), + TransactionDigest::ZERO, + ); + tx_builder.created_objects.push(object); + self + } + + /// Mutate an existing object in the transaction. + /// `object_idx` is a convenient representation of the object's ID. + pub fn mutate_object(mut self, object_idx: u64) -> Self { + let tx_builder = self.next_transaction.as_mut().unwrap(); + let object_id = derive_object_id(object_idx); + let object = self + .object_map + .get(&object_id) + .cloned() + .expect("Mutating an object that doesn't exist"); + tx_builder.mutated_objects.push(object); + self + } + + /// Transfer an existing object to a new owner. + /// `object_idx` is a convenient representation of the object's ID. + /// `recipient_idx` is a convenient representation of the recipient's address. + pub fn transfer_object(mut self, object_idx: u64, recipient_idx: u8) -> Self { + let tx_builder = self.next_transaction.as_mut().unwrap(); + let object_id = derive_object_id(object_idx); + let mut object = self + .object_map + .get(&object_id) + .cloned() + .expect("Transferring an object that doesn't exist"); + object.owner = Owner::AddressOwner(dbg_addr(recipient_idx)); + tx_builder.mutated_objects.push(object); + self + } + + /// Transfer part of an existing coin object's balance to a new owner. + /// `object_idx` is a convenient representation of the object's ID. + /// `new_object_idx` is a convenient representation of the new object's ID. + /// `recipient_idx` is a convenient representation of the recipient's address. + /// `amount` is the amount of balance to be transferred. + pub fn transfer_coin_balance( + mut self, + object_idx: u64, + new_object_idx: u64, + recipient_idx: u8, + amount: u64, + ) -> Self { + let tx_builder = self.next_transaction.as_mut().unwrap(); + let object_id = derive_object_id(object_idx); + let mut object = self + .object_map + .get(&object_id) + .cloned() + .expect("Mutating an object that does not exist"); + let coin_type = object.coin_type_maybe().unwrap(); + // Withdraw balance from coin object. + let move_object = object.data.try_as_move_mut().unwrap(); + let old_balance = move_object.get_coin_value_unsafe(); + let new_balance = old_balance - amount; + move_object.set_coin_value_unsafe(new_balance); + tx_builder.mutated_objects.push(object); + + // Deposit balance into new coin object. + self.create_coin_object(new_object_idx, recipient_idx, amount, coin_type) + } + + /// Delete an existing object from the transaction. + /// `object_idx` is a convenient representation of the object's ID. + pub fn delete_object(mut self, object_idx: u64) -> Self { + let tx_builder = self.next_transaction.as_mut().unwrap(); + let object_id = derive_object_id(object_idx); + assert!(self.object_map.contains_key(&object_id)); + tx_builder.deleted_objects.push(object_id); + self + } + + /// Add events to the transaction. + /// `events` is a vector of events to be added to the transaction. + pub fn with_events(mut self, events: Vec) -> Self { + self.next_transaction.as_mut().unwrap().events = Some(events); + self + } + + /// Complete the current transaction and add it to the checkpoint. + pub fn finish_transaction(mut self) -> Self { + let TransactionBuilder { + sender_idx, + gas, + created_objects, + mutated_objects, + deleted_objects, + events, + } = self.next_transaction.take().unwrap(); + let sender = dbg_addr(sender_idx); + let events = events.map(|events| TransactionEvents { data: events }); + let events_digest = events.as_ref().map(|events| events.digest()); + let pt = ProgrammableTransactionBuilder::new().finish(); + let tx_data = TransactionData::new( + TransactionKind::ProgrammableTransaction(pt), + sender, + gas, + 1, + 1, + ); + let tx = Transaction::new(SenderSignedData::new(tx_data, vec![])); + let mut effects_builder = TestEffectsBuilder::new(tx.data()) + .with_created_objects(created_objects.iter().map(|o| (o.id(), o.owner().clone()))) + .with_mutated_objects( + mutated_objects + .iter() + .map(|o| (o.id(), o.version(), o.owner().clone())), + ) + .with_deleted_objects( + deleted_objects + .clone() + .into_iter() + .map(|id| (id, self.object_map.get(&id).unwrap().version())), + ); + if let Some(events_digest) = &events_digest { + effects_builder = effects_builder.with_events_digest(*events_digest); + } + let effects = effects_builder.build(); + let lamport_version = effects.lamport_version(); + let input_objects: Vec<_> = mutated_objects + .iter() + .map(|o| o.id()) + .chain(deleted_objects.clone()) + .chain(std::iter::once(gas.0)) + .map(|id| self.object_map.get(&id).cloned().unwrap()) + .collect(); + let output_objects: Vec<_> = created_objects + .into_iter() + .chain(mutated_objects) + .chain(std::iter::once( + self.object_map.get(&gas.0).cloned().unwrap(), + )) + .map(|mut o| { + o.data + .try_as_move_mut() + .unwrap() + .increment_version_to(lamport_version); + o + }) + .collect(); + self.object_map + .extend(output_objects.iter().map(|o| (o.id(), o.clone()))); + self.transactions.push(CheckpointTransaction { + transaction: tx, + effects, + events, + input_objects, + output_objects, + }); + self + } + + /// Build the checkpoint data. + pub fn build_checkpoint(&mut self) -> CheckpointData { + assert!(self.next_transaction.is_none()); + let transactions = std::mem::take(&mut self.transactions); + let contents = CheckpointContents::new_with_digests_only_for_tests( + transactions + .iter() + .map(|tx| ExecutionDigests::new(*tx.transaction.digest(), tx.effects.digest())), + ); + let checkpoint_summary = CheckpointSummary::new( + &ProtocolConfig::get_for_max_version_UNSAFE(), + self.epoch, + self.checkpoint, + transactions.len() as u64, + &contents, + None, + Default::default(), + None, + 0, + vec![], + ); + let (committee, keys) = Committee::new_simple_test_committee(); + let checkpoint_cert = CertifiedCheckpointSummary::new_from_keypairs_for_testing( + checkpoint_summary, + &keys, + &committee, + ); + self.checkpoint += 1; + CheckpointData { + checkpoint_summary: checkpoint_cert, + checkpoint_contents: contents, + transactions, + } + } +} + +fn derive_object_id(object_idx: u64) -> ObjectID { + ObjectID::derive_id(TransactionDigest::ZERO, object_idx) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use move_core_types::ident_str; + + use crate::transaction::TransactionDataAPI; + + use super::*; + #[test] + fn test_basic_checkpoint_builder() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .with_epoch(5) + .start_transaction(0) + .finish_transaction() + .build_checkpoint(); + + assert_eq!(*checkpoint.checkpoint_summary.sequence_number(), 1); + assert_eq!(checkpoint.checkpoint_summary.epoch, 5); + assert_eq!(checkpoint.transactions.len(), 1); + let tx = &checkpoint.transactions[0]; + assert_eq!(tx.transaction.sender_address(), dbg_addr(0)); + assert_eq!(tx.effects.mutated().len(), 1); // gas object + assert_eq!(tx.effects.deleted().len(), 0); + assert_eq!(tx.effects.created().len(), 0); + assert_eq!(tx.input_objects.len(), 1); + assert_eq!(tx.output_objects.len(), 1); + } + + #[test] + fn test_multiple_transactions() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .finish_transaction() + .start_transaction(1) + .finish_transaction() + .start_transaction(2) + .finish_transaction() + .build_checkpoint(); + + assert_eq!(checkpoint.transactions.len(), 3); + + // Verify transactions have different senders + let senders: Vec<_> = checkpoint + .transactions + .iter() + .map(|tx| tx.transaction.transaction_data().sender()) + .collect(); + assert_eq!(senders, vec![dbg_addr(0), dbg_addr(1), dbg_addr(2)]); + } + + #[test] + fn test_object_creation() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_object(0) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[0]; + let created_obj_id = derive_object_id(0); + + // Verify object appears in output objects + assert!(tx + .output_objects + .iter() + .any(|obj| obj.id() == created_obj_id)); + + // Verify effects show object creation + assert!(tx + .effects + .created() + .iter() + .any(|((id, ..), owner)| *id == created_obj_id + && owner.get_owner_address().unwrap() == dbg_addr(0))); + } + + #[test] + fn test_object_mutation() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_object(0) + .finish_transaction() + .start_transaction(0) + .mutate_object(0) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[1]; + let obj_id = derive_object_id(0); + + // Verify object appears in input and output objects + assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id)); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id)); + + // Verify effects show object mutation + assert!(tx + .effects + .mutated() + .iter() + .any(|((id, ..), _)| *id == obj_id)); + } + + #[test] + fn test_object_deletion() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_object(0) + .finish_transaction() + .start_transaction(0) + .delete_object(0) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[1]; + let obj_id = derive_object_id(0); + + // Verify object appears in input objects but not output + assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id)); + assert!(!tx.output_objects.iter().any(|obj| obj.id() == obj_id)); + + // Verify effects show object deletion + assert!(tx.effects.deleted().iter().any(|(id, ..)| *id == obj_id)); + } + + #[test] + fn test_object_transfer() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_object(0) + .finish_transaction() + .start_transaction(1) + .transfer_object(0, 1) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[1]; + let obj_id = derive_object_id(0); + + // Verify object appears in input and output objects + assert!(tx.input_objects.iter().any(|obj| obj.id() == obj_id)); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id)); + + // Verify effects show object transfer + assert!(tx + .effects + .mutated() + .iter() + .any(|((id, ..), owner)| *id == obj_id + && owner.get_owner_address().unwrap() == dbg_addr(1))); + } + + #[test] + fn test_sui_balance_transfer() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_sui_object(0, 100) + .finish_transaction() + .start_transaction(1) + .transfer_coin_balance(0, 1, 1, 10) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[0]; + let obj_id0 = derive_object_id(0); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0 + && obj.is_gas_coin() + && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 100)); + + let tx = &checkpoint.transactions[1]; + let obj_id1 = derive_object_id(1); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0 + && obj.is_gas_coin() + && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90)); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1 + && obj.is_gas_coin() + && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10)); + } + + #[test] + fn test_coin_balance_transfer() { + let type_tag = TypeTag::from_str("0x100::a::b").unwrap(); + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_coin_object(0, 0, 100, type_tag.clone()) + .finish_transaction() + .start_transaction(1) + .transfer_coin_balance(0, 1, 1, 10) + .finish_transaction() + .build_checkpoint(); + + let tx = &checkpoint.transactions[1]; + let obj_id0 = derive_object_id(0); + let obj_id1 = derive_object_id(1); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id0 + && obj.coin_type_maybe().unwrap() == type_tag + && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 90)); + assert!(tx.output_objects.iter().any(|obj| obj.id() == obj_id1 + && obj.coin_type_maybe().unwrap() == type_tag + && obj.data.try_as_move().unwrap().get_coin_value_unsafe() == 10)); + } + + #[test] + fn test_events() { + let checkpoint = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .with_events(vec![Event::new( + &ObjectID::ZERO, + &ident_str!("test"), + dbg_addr(0), + GAS::type_(), + vec![], + )]) + .finish_transaction() + .build_checkpoint(); + let tx = &checkpoint.transactions[0]; + assert!(tx.effects.events_digest().is_some()); + assert_eq!(tx.events.as_ref().unwrap().data.len(), 1); + } + + #[test] + fn test_multiple_checkpoints() { + let mut builder = TestCheckpointDataBuilder::new(1) + .start_transaction(0) + .create_object(0) + .finish_transaction(); + let checkpoint1 = builder.build_checkpoint(); + builder = builder + .start_transaction(0) + .mutate_object(0) + .finish_transaction(); + let checkpoint2 = builder.build_checkpoint(); + builder = builder + .start_transaction(0) + .delete_object(0) + .finish_transaction(); + let checkpoint3 = builder.build_checkpoint(); + assert_eq!(checkpoint1.checkpoint_summary.sequence_number, 1); + assert_eq!(checkpoint2.checkpoint_summary.sequence_number, 2); + assert_eq!(checkpoint3.checkpoint_summary.sequence_number, 3); + } +}