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_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, 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 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}