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