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_inputs_or_latest_in_store(
211        checked_input_objects.inner(),
212        backing_store,
213    );
214    let (inner_temp_store, _, effects, execution_result) = executor.dev_inspect_transaction(
215        &tracking_store,
216        protocol_config,
217        execution_metrics.clone(),
218        false, // expensive_checks
219        execution_params,
220        &execution_epoch_id,
221        epoch_timestamp_ms,
222        checked_input_objects,
223        system_object_versions,
224        gas_data,
225        gas_status,
226        kind,
227        rewritten_inputs.clone(),
228        signer,
229        tx_digest,
230        dev_inspect,
231    );
232
233    // Post-execution: check object funds (non-address withdrawals discovered during execution).
234    // TODO: Remove this code once check_object_funds_withdraw_in_execution is enabled on all
235    // production networks.
236    let (inner_temp_store, effects, execution_result) = if !protocol_config
237        .check_object_funds_withdraw_in_execution()
238        && execution_result.is_ok()
239    {
240        let has_insufficient_object_funds = inner_temp_store
241            .accumulator_running_max_withdraws
242            .iter()
243            .filter(|(id, _)| !address_funds.contains(id))
244            .any(|(id, max_withdraw)| {
245                let balance = account_funds_read.get_latest_account_amount(id);
246                balance < *max_withdraw
247            });
248
249        if has_insufficient_object_funds {
250            let retry_gas_status = SuiGasStatus::new(
251                cloned_gas.budget,
252                cloned_gas.price,
253                validity_check_context.reference_gas_price,
254                protocol_config,
255            )?;
256            let (store, _, effects, result) = executor.dev_inspect_transaction(
257                &tracking_store,
258                protocol_config,
259                execution_metrics.clone(),
260                false,
261                ExecutionOrEarlyError::failed(
262                    NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
263                    None,
264                ),
265                &execution_epoch_id,
266                epoch_timestamp_ms,
267                cloned_input_objects,
268                system_object_versions,
269                cloned_gas,
270                retry_gas_status,
271                cloned_kind,
272                rewritten_inputs,
273                signer,
274                tx_digest,
275                dev_inspect,
276            );
277            (store, effects, result)
278        } else {
279            (inner_temp_store, effects, execution_result)
280        }
281    } else {
282        (inner_temp_store, effects, execution_result)
283    };
284
285    let loaded_runtime_objects = tracking_store.into_read_objects();
286    let unchanged_loaded_runtime_objects =
287        unchanged_loaded_runtime_objects(&transaction, &effects, &loaded_runtime_objects);
288
289    let object_set = {
290        let objects = {
291            let mut objects = loaded_runtime_objects;
292
293            for o in inner_temp_store
294                .input_objects
295                .into_values()
296                .chain(inner_temp_store.written.into_values())
297            {
298                objects.insert(o);
299            }
300
301            objects
302        };
303
304        let object_keys =
305            get_transaction_object_set(&transaction, &effects, &unchanged_loaded_runtime_objects);
306
307        let mut set = ObjectSet::default();
308        for k in object_keys {
309            if let Some(o) = objects.get(&k) {
310                set.insert(o.clone());
311            }
312        }
313
314        set
315    };
316
317    Ok(SimulateTransactionResult {
318        objects: object_set,
319        events: effects.events_digest().map(|_| inner_temp_store.events),
320        effects,
321        execution_result,
322        mock_gas_id,
323        unchanged_loaded_runtime_objects,
324        suggested_gas_price,
325    })
326}