1use 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
47pub trait SimulationInputLoader {
49 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
59pub 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 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 let input_object_kinds = transaction.input_objects()?;
103 let receiving_object_refs = transaction.receiving_objects();
104
105 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 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 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 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 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, 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 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}