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    let (inner_temp_store, effects, execution_result) = if execution_result.is_ok() {
232        let has_insufficient_object_funds = inner_temp_store
233            .accumulator_running_max_withdraws
234            .iter()
235            .filter(|(id, _)| !address_funds.contains(id))
236            .any(|(id, max_withdraw)| {
237                let balance = account_funds_read.get_latest_account_amount(id);
238                balance < *max_withdraw
239            });
240
241        if has_insufficient_object_funds {
242            let retry_gas_status = SuiGasStatus::new(
243                cloned_gas.budget,
244                cloned_gas.price,
245                validity_check_context.reference_gas_price,
246                protocol_config,
247            )?;
248            let (store, _, effects, result) = executor.dev_inspect_transaction(
249                &tracking_store,
250                protocol_config,
251                execution_metrics.clone(),
252                false,
253                ExecutionOrEarlyError::failed(
254                    NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
255                    None,
256                ),
257                &execution_epoch_id,
258                epoch_timestamp_ms,
259                cloned_input_objects,
260                system_object_versions,
261                cloned_gas,
262                retry_gas_status,
263                cloned_kind,
264                rewritten_inputs,
265                signer,
266                tx_digest,
267                dev_inspect,
268            );
269            (store, effects, result)
270        } else {
271            (inner_temp_store, effects, execution_result)
272        }
273    } else {
274        (inner_temp_store, effects, execution_result)
275    };
276
277    let loaded_runtime_objects = tracking_store.into_read_objects();
278    let unchanged_loaded_runtime_objects =
279        unchanged_loaded_runtime_objects(&transaction, &effects, &loaded_runtime_objects);
280
281    let object_set = {
282        let objects = {
283            let mut objects = loaded_runtime_objects;
284
285            for o in inner_temp_store
286                .input_objects
287                .into_values()
288                .chain(inner_temp_store.written.into_values())
289            {
290                objects.insert(o);
291            }
292
293            objects
294        };
295
296        let object_keys =
297            get_transaction_object_set(&transaction, &effects, &unchanged_loaded_runtime_objects);
298
299        let mut set = ObjectSet::default();
300        for k in object_keys {
301            if let Some(o) = objects.get(&k) {
302                set.insert(o.clone());
303            }
304        }
305
306        set
307    };
308
309    Ok(SimulateTransactionResult {
310        objects: object_set,
311        events: effects.events_digest().map(|_| inner_temp_store.events),
312        effects,
313        execution_result,
314        mock_gas_id,
315        unchanged_loaded_runtime_objects,
316        suggested_gas_price,
317    })
318}