Skip to main content

sui_core/
transaction_simulation.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeSet, HashSet},
6    sync::Arc,
7};
8
9use nonempty::NonEmpty;
10use sui_config::{
11    transaction_deny_config::TransactionDenyConfig, verifier_signing_config::VerifierSigningConfig,
12};
13use sui_execution::Executor;
14use sui_transaction_checks::{check_dev_inspect_input, check_transaction_input};
15use sui_types::{
16    base_types::{EpochId, ObjectID, ObjectRef, SystemObjectVersions},
17    coin_reservation::{CoinReservationResolverTrait, ParsedDigest},
18    digests::{ChainIdentifier, TransactionDigest},
19    effects::TransactionEffectsAPI,
20    error::{SuiErrorKind, SuiResult},
21    execution_params::{ExecutionOrEarlyError, FundsWithdrawStatus, get_early_execution_error},
22    execution_status::ExecutionErrorKind,
23    full_checkpoint_content::ObjectSet,
24    messages_checkpoint::CheckpointTimestamp,
25    metrics::{BytecodeVerifierMetrics, ExecutionMetrics},
26    object::{MoveObject, OBJECT_START_VERSION, Object, Owner},
27    storage::{
28        BackingPackageStore, BackingStore, TrackingBackingStore, get_transaction_object_set,
29    },
30    transaction::{
31        InputObjectKind, InputObjects, ObjectReadResult, ReceivingObjects, TransactionData,
32        TransactionDataAPI, TxValidityCheckContext,
33    },
34    transaction_executor::{SimulateTransactionResult, TransactionChecks},
35};
36
37use crate::{
38    accumulators::{
39        funds_read::AccountFundsRead,
40        transaction_rewriting::rewrite_transaction_for_coin_reservations,
41    },
42    authority::{DEV_INSPECT_GAS_COIN_VALUE, pre_object_load_checks},
43    transaction_outputs::unchanged_loaded_runtime_objects,
44};
45
46/// Load transaction inputs for simulation without preparing them for committed execution.
47pub trait SimulationInputLoader {
48    /// Load the input and receiving objects at the state used for simulation.
49    fn read_objects_for_simulation(
50        &self,
51        transaction_digest: &TransactionDigest,
52        input_object_kinds: &[InputObjectKind],
53        receiving_object_refs: &[ObjectRef],
54        epoch_id: EpochId,
55    ) -> SuiResult<(InputObjects, ReceivingObjects)>;
56}
57
58/// Simulate a transaction without committing its outputs.
59pub fn simulate_transaction(
60    mut transaction: TransactionData,
61    checks: TransactionChecks,
62    allow_mock_gas_coin: bool,
63    suggested_gas_price: Option<u64>,
64    validity_check_context: TxValidityCheckContext<'_>,
65    execution_epoch_id: EpochId,
66    epoch_timestamp_ms: CheckpointTimestamp,
67    chain_identifier: ChainIdentifier,
68    transaction_deny_config: &TransactionDenyConfig,
69    certificate_deny_set: &HashSet<TransactionDigest>,
70    input_loader: &dyn SimulationInputLoader,
71    backing_store: &(dyn BackingStore + Send + Sync),
72    backing_package_store: &(dyn BackingPackageStore + Send + Sync),
73    executor: &(dyn Executor + Send + Sync),
74    coin_reservation_resolver: &dyn CoinReservationResolverTrait,
75    account_funds_read: &dyn AccountFundsRead,
76    verifier_signing_config: &VerifierSigningConfig,
77    bytecode_verifier_metrics: &Arc<BytecodeVerifierMetrics>,
78    execution_metrics: &Arc<ExecutionMetrics>,
79) -> SuiResult<SimulateTransactionResult> {
80    let dev_inspect = checks.disabled();
81
82    // Reject coin reservations in gas payment when the execution engine
83    // doesn't support them.
84    let protocol_config = validity_check_context.config;
85    if !protocol_config.enable_coin_reservation_obj_refs()
86        && transaction
87            .gas()
88            .iter()
89            .any(|obj_ref| ParsedDigest::is_coin_reservation_digest(&obj_ref.2))
90    {
91        return Err(SuiErrorKind::UnsupportedFeatureError {
92            error: "coin reservations in gas payment are not supported at this protocol version"
93                .to_string(),
94        }
95        .into());
96    }
97
98    // Compute input/receiving object kinds before mock gas injection so the mock
99    // gas reference is not included in input_object_kinds (it is added to
100    // input_objects directly after object loading).
101    let input_object_kinds = transaction.input_objects()?;
102    let receiving_object_refs = transaction.receiving_objects();
103
104    // Inject mock gas coin before validity_check so that on protocol versions
105    // where address-balance gas payments are not yet enabled, the non-empty
106    // payment check in validity_check passes for simulate/dev-inspect requests
107    // submitted without explicit gas.
108    // Also required before pre_object_load_checks so that funds-withdrawal
109    // processing sees non-empty payment and doesn't create an address-balance
110    // withdrawal for gas.
111    // Skip mock gas for gasless transactions — they don't use gas coins.
112    let is_gasless = protocol_config.enable_gasless() && transaction.is_gasless_transaction();
113    let mock_gas_object = if allow_mock_gas_coin && transaction.gas().is_empty() && !is_gasless {
114        let obj = Object::new_move(
115            MoveObject::new_gas_coin(
116                OBJECT_START_VERSION,
117                ObjectID::MAX,
118                DEV_INSPECT_GAS_COIN_VALUE,
119            ),
120            Owner::AddressOwner(transaction.gas_data().owner),
121            TransactionDigest::genesis_marker(),
122        );
123        transaction.gas_data_mut().payment = vec![obj.compute_object_reference()];
124        Some(obj)
125    } else {
126        None
127    };
128
129    // Full validity check including gas budget and price.
130    transaction.validity_check(&validity_check_context)?;
131
132    let declared_withdrawals = pre_object_load_checks(
133        &transaction,
134        &[],
135        &input_object_kinds,
136        &receiving_object_refs,
137        protocol_config,
138        transaction_deny_config,
139        backing_package_store,
140        chain_identifier,
141        coin_reservation_resolver,
142        account_funds_read,
143    )?;
144    let address_funds: BTreeSet<_> = declared_withdrawals.keys().cloned().collect();
145
146    let transaction_digest = transaction.digest();
147    let (mut input_objects, receiving_objects) = input_loader.read_objects_for_simulation(
148        &transaction_digest,
149        &input_object_kinds,
150        &receiving_object_refs,
151        validity_check_context.epoch,
152    )?;
153
154    // Add mock gas to input objects after loading (it doesn't exist in the store).
155    let mock_gas_id = mock_gas_object.map(|obj| {
156        let id = obj.id();
157        input_objects.push(ObjectReadResult::new_from_gas_object(&obj));
158        id
159    });
160
161    let checked_input_objects = if dev_inspect {
162        check_dev_inspect_input(
163            protocol_config,
164            &transaction,
165            input_objects,
166            receiving_objects,
167            validity_check_context.reference_gas_price,
168        )?
169    } else {
170        check_transaction_input(
171            protocol_config,
172            validity_check_context.reference_gas_price,
173            &transaction,
174            input_objects,
175            &receiving_objects,
176            bytecode_verifier_metrics,
177            verifier_signing_config,
178        )?
179    };
180
181    let (mut kind, signer, gas_data) = transaction.execution_parts();
182    let rewritten_inputs = rewrite_transaction_for_coin_reservations(
183        chain_identifier,
184        coin_reservation_resolver,
185        signer,
186        &mut kind,
187        None,
188    )?;
189    let early_execution_error = get_early_execution_error(
190        &transaction.digest(),
191        &checked_input_objects,
192        certificate_deny_set,
193        &FundsWithdrawStatus::MaybeSufficient,
194    );
195    // Dev-inspect/simulation path (not committed): no assigned accumulator version here, so the
196    // IFFW short-circuit applies unconditionally (`None`), matching non-mainnet execution.
197    let execution_params = match early_execution_error {
198        None => ExecutionOrEarlyError::ok(None),
199        Some(errors) => ExecutionOrEarlyError::failed(errors, None),
200    };
201
202    let tracking_store = TrackingBackingStore::new(backing_store);
203
204    // Clone inputs for potential retry if object funds check fails post-execution.
205    let cloned_input_objects = checked_input_objects.clone();
206    let cloned_gas = gas_data.clone();
207    let cloned_kind = kind.clone();
208    let tx_digest = transaction_digest;
209    let system_object_versions = SystemObjectVersions::from_inputs_or_latest_in_store(
210        checked_input_objects.inner(),
211        backing_store,
212    );
213    let (inner_temp_store, _, effects, execution_result) = executor.dev_inspect_transaction(
214        &tracking_store,
215        protocol_config,
216        execution_metrics.clone(),
217        false, // expensive_checks
218        execution_params,
219        &execution_epoch_id,
220        epoch_timestamp_ms,
221        checked_input_objects,
222        system_object_versions.clone(),
223        gas_data,
224        validity_check_context.reference_gas_price,
225        kind,
226        rewritten_inputs.clone(),
227        signer,
228        tx_digest,
229        dev_inspect,
230    );
231
232    // Post-execution: check object funds (non-address withdrawals discovered during execution).
233    // TODO: Remove this code once check_object_funds_withdraw_in_execution is enabled on all
234    // production networks.
235    let (inner_temp_store, effects, execution_result) = if !protocol_config
236        .check_object_funds_withdraw_in_execution()
237        && execution_result.is_ok()
238    {
239        let has_insufficient_object_funds = inner_temp_store
240            .accumulator_running_max_withdraws
241            .iter()
242            .filter(|(id, _)| !address_funds.contains(id))
243            .any(|(id, max_withdraw)| {
244                let balance = account_funds_read.get_latest_account_amount(id);
245                balance < *max_withdraw
246            });
247
248        if has_insufficient_object_funds {
249            let (store, _, effects, result) = executor.dev_inspect_transaction(
250                &tracking_store,
251                protocol_config,
252                execution_metrics.clone(),
253                false,
254                ExecutionOrEarlyError::failed(
255                    NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
256                    None,
257                ),
258                &execution_epoch_id,
259                epoch_timestamp_ms,
260                cloned_input_objects,
261                system_object_versions,
262                cloned_gas,
263                validity_check_context.reference_gas_price,
264                cloned_kind,
265                rewritten_inputs,
266                signer,
267                tx_digest,
268                dev_inspect,
269            );
270            (store, effects, result)
271        } else {
272            (inner_temp_store, effects, execution_result)
273        }
274    } else {
275        (inner_temp_store, effects, execution_result)
276    };
277
278    let loaded_runtime_objects = tracking_store.into_read_objects();
279    let unchanged_loaded_runtime_objects =
280        unchanged_loaded_runtime_objects(&transaction, &effects, &loaded_runtime_objects);
281
282    let object_set = {
283        let objects = {
284            let mut objects = loaded_runtime_objects;
285
286            for o in inner_temp_store
287                .input_objects
288                .into_values()
289                .chain(inner_temp_store.written.into_values())
290            {
291                objects.insert(o);
292            }
293
294            objects
295        };
296
297        let object_keys =
298            get_transaction_object_set(&transaction, &effects, &unchanged_loaded_runtime_objects);
299
300        let mut set = ObjectSet::default();
301        for k in object_keys {
302            if let Some(o) = objects.get(&k) {
303                set.insert(o.clone());
304            }
305        }
306
307        set
308    };
309
310    Ok(SimulateTransactionResult {
311        objects: object_set,
312        events: effects.events_digest().map(|_| inner_temp_store.events),
313        effects,
314        execution_result,
315        mock_gas_id,
316        unchanged_loaded_runtime_objects,
317        suggested_gas_price,
318    })
319}