Skip to main content

sui_core/authority/
authority_test_utils.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use fastcrypto::hash::MultisetHash;
6use fastcrypto::traits::KeyPair;
7use sui_types::base_types::FullObjectRef;
8use sui_types::crypto::{AccountKeyPair, AuthorityKeyPair};
9use sui_types::utils::to_sender_signed_transaction;
10
11use super::shared_object_version_manager::AssignedVersions;
12use super::test_authority_builder::TestAuthorityBuilder;
13use super::*;
14use sui_types::transaction_executor::{SimulateTransactionResult, TransactionChecks};
15
16#[cfg(test)]
17use super::shared_object_version_manager::Schedulable;
18#[cfg(test)]
19use mysten_common::ZipDebugEqIteratorExt;
20#[cfg(test)]
21use std::collections::HashMap;
22#[cfg(test)]
23use sui_types::transaction::TransactionKey;
24
25/// Simulate `transaction_kind` the way the removed dev-inspect API used to: the
26/// transaction is synthesized from the given gas parameters (defaulting to the
27/// reference gas price, the maximum gas budget, and the sender as sponsor), and
28/// a mock gas coin is injected when no gas payment is provided. Pass
29/// `TransactionChecks::Disabled` for the classic dev-inspect semantics.
30#[allow(clippy::too_many_arguments)]
31pub fn dev_inspect_for_testing(
32    state: &AuthorityState,
33    sender: SuiAddress,
34    transaction_kind: TransactionKind,
35    gas_price: Option<u64>,
36    gas_budget: Option<u64>,
37    gas_sponsor: Option<SuiAddress>,
38    gas_objects: Option<Vec<ObjectRef>>,
39    checks: TransactionChecks,
40) -> SuiResult<SimulateTransactionResult> {
41    let epoch_store = state.epoch_store_for_testing();
42    let transaction = TransactionData::new_with_gas_coins_allow_sponsor(
43        transaction_kind,
44        sender,
45        gas_objects.unwrap_or_default(),
46        gas_budget.unwrap_or(epoch_store.protocol_config().max_tx_gas()),
47        gas_price.unwrap_or(epoch_store.reference_gas_price()),
48        gas_sponsor.unwrap_or(sender),
49    );
50    state.simulate_transaction(transaction, checks, /* allow_mock_gas_coin */ true)
51}
52
53// =============================================================================
54// MFP (Mysticeti Fast Path) Test Helpers
55//
56// The MFP transaction flow is:
57//   1. Client signs transaction and submits to a validator.
58//   2. The validator validates transaction and submits it to consensus.
59//   3. Consensus finalizes the transaction and outputs it in a commit.
60//   4. Transactions in the commit are filtered, sequenced and processed. Then they are sent to execution.
61//
62// =============================================================================
63
64/// Validates a transaction.
65/// This is the MFP "voting" phase - similar to what happens when a validator
66/// receives a transaction before submitting to consensus.
67///
68/// Returns the verified transaction ready for consensus submission.
69pub fn vote_transaction(
70    authority: &AuthorityState,
71    transaction: Transaction,
72) -> Result<VerifiedTransaction, SuiError> {
73    let epoch_store = authority.load_epoch_store_one_call_per_task();
74    transaction.validity_check(&epoch_store.tx_validity_check_context())?;
75    let verified_tx = epoch_store
76        .verify_transaction_require_no_aliases(transaction)?
77        .into_tx();
78
79    // Validate the transaction.
80    authority.handle_vote_transaction(&epoch_store, verified_tx.clone())?;
81
82    Ok(verified_tx)
83}
84
85/// Creates a VerifiedExecutableTransaction from a signed transaction.
86/// This validates the transaction, votes on it, and creates an executable
87/// as if it came out of consensus.
88pub fn create_executable_transaction(
89    authority: &AuthorityState,
90    transaction: Transaction,
91) -> Result<VerifiedExecutableTransaction, SuiError> {
92    let epoch_store = authority.load_epoch_store_one_call_per_task();
93    let verified_tx = vote_transaction(authority, transaction)?;
94    Ok(VerifiedExecutableTransaction::new_from_consensus(
95        verified_tx,
96        epoch_store.epoch(),
97    ))
98}
99
100/// Submits a transaction to consensus for ordering and version assignment.
101/// This only simulates the consensus submission process by assigning versions
102/// to shared objects.
103///
104/// Returns the executable transaction (now certified by consensus) and assigned versions.
105/// The transaction is NOT automatically executed - use `execute_from_consensus` for that.
106pub async fn submit_to_consensus(
107    authority: &AuthorityState,
108    transaction: Transaction,
109) -> Result<(VerifiedExecutableTransaction, AssignedVersions), SuiError> {
110    let epoch_store = authority.load_epoch_store_one_call_per_task();
111
112    // First validate and vote
113    let verified_tx = vote_transaction(authority, transaction)?;
114
115    // Create executable - the transaction is now "certified" by consensus
116    let executable =
117        VerifiedExecutableTransaction::new_from_consensus(verified_tx, epoch_store.epoch());
118
119    // Assign shared object versions
120    let assigned_versions = authority
121        .epoch_store_for_testing()
122        .assign_shared_object_versions_for_tests(
123            authority.get_object_cache_reader().as_ref(),
124            std::slice::from_ref(&executable.clone()),
125        )?;
126
127    let versions = assigned_versions
128        .into_map()
129        .get(&executable.key())
130        .cloned()
131        .unwrap_or_else(AssignedVersions::empty);
132
133    Ok((executable, versions))
134}
135
136/// Executes a transaction that has already been sequenced through consensus.
137pub async fn execute_from_consensus(
138    authority: &AuthorityState,
139    executable: VerifiedExecutableTransaction,
140    assigned_versions: AssignedVersions,
141) -> (TransactionEffects, Option<ExecutionError>) {
142    let env = ExecutionEnv::new().with_assigned_versions(assigned_versions);
143    authority.execution_scheduler.enqueue(
144        vec![(executable.clone().into(), env.clone())],
145        &authority.epoch_store_for_testing(),
146    );
147
148    let (result, execution_error_opt) = authority
149        .try_execute_executable_for_test(&executable, env)
150        .await;
151    let effects = result.inner().data().clone();
152    (effects, execution_error_opt)
153}
154
155/// This is the primary test helper for executing transactions end-to-end.
156///
157/// Returns the executable transaction and signed effects.
158pub async fn submit_and_execute(
159    authority: &AuthorityState,
160    transaction: Transaction,
161) -> Result<(VerifiedExecutableTransaction, SignedTransactionEffects), SuiError> {
162    submit_and_execute_with_options(authority, None, transaction).await
163}
164
165/// Options:
166/// - `fullnode`: Optionally sync and execute on a fullnode as well
167pub async fn submit_and_execute_with_options(
168    authority: &AuthorityState,
169    fullnode: Option<&AuthorityState>,
170    transaction: Transaction,
171) -> Result<(VerifiedExecutableTransaction, SignedTransactionEffects), SuiError> {
172    let (exec, effects, _) =
173        submit_and_execute_with_error(authority, fullnode, transaction).await?;
174    Ok((exec, effects))
175}
176
177/// Complete MFP flow returning execution error if any.
178pub async fn submit_and_execute_with_error(
179    authority: &AuthorityState,
180    fullnode: Option<&AuthorityState>,
181    transaction: Transaction,
182) -> Result<
183    (
184        VerifiedExecutableTransaction,
185        SignedTransactionEffects,
186        Option<ExecutionError>,
187    ),
188    SuiError,
189> {
190    let epoch_store = authority.load_epoch_store_one_call_per_task();
191
192    // Vote on the transaction.
193    let verified_tx = vote_transaction(authority, transaction)?;
194
195    // Create executable - transaction is now certified by consensus
196    let executable =
197        VerifiedExecutableTransaction::new_from_consensus(verified_tx, epoch_store.epoch());
198
199    let versions = authority
200        .epoch_store_for_testing()
201        .assign_shared_object_versions_for_tests(
202            authority.get_object_cache_reader().as_ref(),
203            std::slice::from_ref(&executable.clone()),
204        )?;
205    let assigned_versions = versions
206        .into_map()
207        .get(&executable.key())
208        .cloned()
209        .unwrap_or_else(AssignedVersions::empty);
210    // This executes directly, so it attaches the accumulator root version itself: the
211    // helper assigns none, and execution reads the root implicitly for object funds
212    // withdraws, even when the transaction has no shared inputs.
213    let assigned_versions = match authority.accumulator_version_for_testing() {
214        Some(version) => assigned_versions.with_accumulator_version_for_testing(version),
215        None => assigned_versions,
216    };
217
218    // State accumulator for validation
219    let state_acc =
220        GlobalStateHasher::new_for_tests(authority.get_global_state_hash_store().clone());
221    let include_wrapped_tombstone = !authority
222        .epoch_store_for_testing()
223        .protocol_config()
224        .simplified_unwrap_then_delete();
225    let mut state =
226        state_acc.accumulate_cached_live_object_set_for_testing(include_wrapped_tombstone);
227
228    // Execute
229    let env = ExecutionEnv::new().with_assigned_versions(assigned_versions.clone());
230    let (result, mut execution_error_opt) = authority
231        .try_execute_executable_for_test(&executable, env.clone())
232        .await;
233
234    // Validate state accumulation
235    let state_after =
236        state_acc.accumulate_cached_live_object_set_for_testing(include_wrapped_tombstone);
237    let effects_acc = state_acc.accumulate_effects(
238        &[result.inner().data().clone()],
239        epoch_store.protocol_config(),
240    );
241    state.union(&effects_acc);
242    assert_eq!(state_after.digest(), state.digest());
243
244    // Execute on fullnode if provided, use its error which includes source error
245    if let Some(fullnode) = fullnode {
246        let (_, fullnode_execution_error_opt) = fullnode
247            .try_execute_executable_for_test(&executable, env)
248            .await;
249        execution_error_opt = fullnode_execution_error_opt;
250    }
251
252    Ok((executable, result.into_inner(), execution_error_opt))
253}
254
255/// Enqueues multiple transactions for execution after they've been through consensus.
256pub async fn enqueue_and_execute_all(
257    authority: &AuthorityState,
258    executables: Vec<(VerifiedExecutableTransaction, ExecutionEnv)>,
259) -> Result<Vec<TransactionEffects>, SuiError> {
260    authority.execution_scheduler.enqueue(
261        executables
262            .iter()
263            .map(|(exec, env)| (exec.clone().into(), env.clone()))
264            .collect(),
265        &authority.epoch_store_for_testing(),
266    );
267    let mut output = Vec::new();
268    for (exec, _) in executables {
269        let effects = authority
270            .notify_read_effects_for_testing("", *exec.digest())
271            .await;
272        output.push(effects);
273    }
274    Ok(output)
275}
276
277/// Submits a transaction to consensus and schedules for execution.
278/// Returns assigned versions. Execution happens asynchronously.
279pub async fn submit_and_schedule(
280    authority: &AuthorityState,
281    transaction: Transaction,
282) -> Result<AssignedVersions, SuiError> {
283    let (executable, versions) = submit_to_consensus(authority, transaction).await?;
284
285    let env = ExecutionEnv::new().with_assigned_versions(versions.clone());
286    authority.execution_scheduler().enqueue_transactions(
287        vec![(executable, env)],
288        &authority.epoch_store_for_testing(),
289    );
290
291    Ok(versions)
292}
293
294pub async fn init_state_validator_with_fullnode() -> (Arc<AuthorityState>, Arc<AuthorityState>) {
295    use sui_types::crypto::get_authority_key_pair;
296
297    let validator = TestAuthorityBuilder::new().build().await;
298    let fullnode_key_pair = get_authority_key_pair().1;
299    let fullnode = TestAuthorityBuilder::new()
300        .with_keypair(&fullnode_key_pair)
301        .build()
302        .await;
303    (validator, fullnode)
304}
305
306pub async fn init_state_with_committee(
307    genesis: &Genesis,
308    authority_key: &AuthorityKeyPair,
309) -> Arc<AuthorityState> {
310    TestAuthorityBuilder::new()
311        .with_genesis_and_keypair(genesis, authority_key)
312        .build()
313        .await
314}
315
316pub async fn init_state_with_ids<I: IntoIterator<Item = (SuiAddress, ObjectID)>>(
317    objects: I,
318) -> Arc<AuthorityState> {
319    let state = TestAuthorityBuilder::new().build().await;
320    for (address, object_id) in objects {
321        let obj = Object::with_id_owner_for_testing(object_id, address);
322        state.insert_genesis_object(obj);
323    }
324    state
325}
326
327pub async fn init_state_with_ids_and_versions<
328    I: IntoIterator<Item = (SuiAddress, ObjectID, SequenceNumber)>,
329>(
330    objects: I,
331) -> Arc<AuthorityState> {
332    let state = TestAuthorityBuilder::new().build().await;
333    for (address, object_id, version) in objects {
334        let obj = Object::with_id_owner_version_for_testing(
335            object_id,
336            version,
337            Owner::AddressOwner(address),
338        );
339        state.insert_genesis_object(obj);
340    }
341    state
342}
343
344pub async fn init_state_with_objects<I: IntoIterator<Item = Object>>(
345    objects: I,
346) -> Arc<AuthorityState> {
347    let dir = tempfile::TempDir::new().unwrap();
348    let network_config = sui_swarm_config::network_config_builder::ConfigBuilder::new(&dir).build();
349    let genesis = network_config.genesis;
350    let keypair = network_config.validator_configs[0]
351        .protocol_key_pair()
352        .copy();
353    init_state_with_objects_and_committee(objects, &genesis, &keypair).await
354}
355
356pub async fn init_state_with_objects_and_committee<I: IntoIterator<Item = Object>>(
357    objects: I,
358    genesis: &Genesis,
359    authority_key: &AuthorityKeyPair,
360) -> Arc<AuthorityState> {
361    let state = init_state_with_committee(genesis, authority_key).await;
362    for o in objects {
363        state.insert_genesis_object(o);
364    }
365    state
366}
367
368pub async fn init_state_with_object_id(
369    address: SuiAddress,
370    object: ObjectID,
371) -> Arc<AuthorityState> {
372    init_state_with_ids(std::iter::once((address, object))).await
373}
374
375pub async fn init_state_with_ids_and_expensive_checks<
376    I: IntoIterator<Item = (SuiAddress, ObjectID)>,
377>(
378    objects: I,
379    config: ExpensiveSafetyCheckConfig,
380) -> Arc<AuthorityState> {
381    let state = TestAuthorityBuilder::new()
382        .with_expensive_safety_checks(config)
383        .build()
384        .await;
385    for (address, object_id) in objects {
386        let obj = Object::with_id_owner_for_testing(object_id, address);
387        state.insert_genesis_object(obj);
388    }
389    state
390}
391
392pub fn init_transfer_transaction(
393    authority_state: &AuthorityState,
394    sender: SuiAddress,
395    secret: &AccountKeyPair,
396    recipient: SuiAddress,
397    object_ref: ObjectRef,
398    gas_object_ref: ObjectRef,
399    gas_budget: u64,
400    gas_price: u64,
401) -> VerifiedTransaction {
402    let data = TransactionData::new_transfer(
403        recipient,
404        FullObjectRef::from_fastpath_ref(object_ref),
405        sender,
406        gas_object_ref,
407        gas_budget,
408        gas_price,
409    );
410    let tx = to_sender_signed_transaction(data, secret);
411    authority_state
412        .epoch_store_for_testing()
413        .verify_transaction_require_no_aliases(tx)
414        .unwrap()
415        .into_tx()
416}
417
418#[cfg(test)]
419pub async fn submit_batch_to_consensus<C>(
420    authority: &AuthorityState,
421    transactions: &[Transaction],
422    consensus_handler: &mut crate::consensus_handler::ConsensusHandler<C>,
423    captured_transactions: &crate::consensus_test_utils::CapturedTransactions,
424) -> (Vec<Schedulable>, HashMap<TransactionKey, AssignedVersions>)
425where
426    C: crate::checkpoints::CheckpointServiceNotify + Send + Sync + 'static,
427{
428    use crate::consensus_test_utils::TestConsensusCommit;
429    use sui_types::messages_consensus::ConsensusTransaction;
430    use sui_types::transaction::PlainTransactionWithClaims;
431
432    let consensus_transactions: Vec<ConsensusTransaction> = transactions
433        .iter()
434        .map(|tx| {
435            ConsensusTransaction::new_user_transaction_v2_message(
436                &authority.name,
437                PlainTransactionWithClaims::no_aliases(tx.clone()),
438            )
439        })
440        .collect();
441
442    let epoch_store = authority.epoch_store_for_testing();
443    let round = epoch_store.get_highest_pending_checkpoint_height() + 1;
444    let timestamp_ms = epoch_store.epoch_start_state().epoch_start_timestamp_ms();
445    let sub_dag_index = 0;
446
447    let commit =
448        TestConsensusCommit::new(consensus_transactions, round, timestamp_ms, sub_dag_index);
449
450    consensus_handler
451        .handle_consensus_commit_for_test(commit)
452        .await;
453
454    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
455
456    let (scheduled_txns, assigned_tx_and_versions) = {
457        let mut captured = captured_transactions.lock();
458        assert!(
459            !captured.is_empty(),
460            "Expected transactions to be scheduled"
461        );
462        let (paired, _) = captured.remove(0);
463        let (schedulables, versions): (Vec<_>, Vec<_>) = paired.into_iter().unzip();
464        let assigned_versions = schedulables
465            .iter()
466            .map(|s| s.key())
467            .zip_debug_eq(versions)
468            .collect();
469        (schedulables, assigned_versions)
470    };
471
472    (scheduled_txns, assigned_tx_and_versions)
473}
474
475pub async fn assign_versions_and_schedule(
476    authority: &AuthorityState,
477    executable: &VerifiedExecutableTransaction,
478) -> AssignedVersions {
479    let assigned_versions = authority
480        .epoch_store_for_testing()
481        .assign_shared_object_versions_for_tests(
482            authority.get_object_cache_reader().as_ref(),
483            std::slice::from_ref(&executable.clone()),
484        )
485        .unwrap();
486
487    let versions = assigned_versions
488        .into_map()
489        .get(&executable.key())
490        .cloned()
491        .unwrap_or_else(AssignedVersions::empty);
492
493    let env = ExecutionEnv::new().with_assigned_versions(versions.clone());
494    authority.execution_scheduler().enqueue_transactions(
495        vec![(executable.clone(), env)],
496        &authority.epoch_store_for_testing(),
497    );
498
499    versions
500}
501
502/// Assigns shared object versions for an executable without scheduling for execution.
503/// This is used when you need version assignment but want to control execution separately.
504pub async fn assign_shared_object_versions(
505    authority: &AuthorityState,
506    executable: &VerifiedExecutableTransaction,
507) -> AssignedVersions {
508    let assigned_versions = authority
509        .epoch_store_for_testing()
510        .assign_shared_object_versions_for_tests(
511            authority.get_object_cache_reader().as_ref(),
512            std::slice::from_ref(&executable.clone()),
513        )
514        .unwrap();
515
516    assigned_versions
517        .into_map()
518        .get(&executable.key())
519        .cloned()
520        .unwrap_or_else(AssignedVersions::empty)
521}