sui_adapter_latest/static_programmable_transactions/execution/
interpreter.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{
5    execution_mode::ExecutionMode,
6    gas_charger::GasCharger,
7    object_runtime, sp,
8    static_programmable_transactions::{
9        env::Env,
10        execution::{
11            context::{Context, CtxValue, GasCoinTransfer},
12            trace_utils,
13        },
14        typing::{ast as T, verify::input_arguments::is_coin_send_funds},
15    },
16};
17use move_core_types::account_address::AccountAddress;
18use move_trace_format::format::MoveTraceBuilder;
19use mysten_common::ZipDebugEqIteratorExt;
20use std::{
21    cell::RefCell,
22    collections::BTreeMap,
23    rc::Rc,
24    sync::Arc,
25    time::{Duration, Instant},
26};
27use sui_types::{
28    base_types::TxContext,
29    error::ExecutionError,
30    execution::{ExecutionTiming, ResultWithTimings},
31    execution_status::{ExecutionErrorKind, PackageUpgradeError},
32    metrics::ExecutionMetrics,
33    move_package::MovePackage,
34    object::Owner,
35};
36use tracing::instrument;
37
38pub fn execute<'env, 'pc, 'vm, 'state, 'linkage, 'extension, Mode: ExecutionMode>(
39    env: &'env mut Env<'pc, 'vm, 'state, 'linkage, 'extension>,
40    metrics: Arc<ExecutionMetrics>,
41    tx_context: Rc<RefCell<TxContext>>,
42    gas_charger: &mut GasCharger,
43    ast: T::Transaction,
44    trace_builder_opt: &mut Option<MoveTraceBuilder>,
45) -> ResultWithTimings<Mode::ExecutionResults, ExecutionError>
46where
47    'pc: 'state,
48    'env: 'state,
49{
50    let original_command_len = ast.original_command_len;
51    let mut indexed_timings = IndexedExecutionTimings::new(original_command_len);
52    let result = execute_inner::<Mode>(
53        &mut indexed_timings,
54        env,
55        metrics,
56        tx_context,
57        gas_charger,
58        ast,
59        trace_builder_opt,
60    );
61    let timings = indexed_timings.into_coalesced();
62    debug_assert!(
63        timings.len() <= original_command_len,
64        "coalesced timings length {} exceeds original command length {}",
65        timings.len(),
66        original_command_len
67    );
68
69    match result {
70        Ok(result) => Ok((result, timings)),
71        Err(e) => {
72            trace_utils::trace_execution_error(trace_builder_opt, e.to_string());
73            Err((e, timings))
74        }
75    }
76}
77
78fn execute_inner<'env, 'pc, 'vm, 'state, 'linkage, 'extension, Mode: ExecutionMode>(
79    timings: &mut IndexedExecutionTimings,
80    env: &'env mut Env<'pc, 'vm, 'state, 'linkage, 'extension>,
81    metrics: Arc<ExecutionMetrics>,
82    tx_context: Rc<RefCell<TxContext>>,
83    gas_charger: &mut GasCharger,
84    ast: T::Transaction,
85    trace_builder_opt: &mut Option<MoveTraceBuilder>,
86) -> Result<Mode::ExecutionResults, ExecutionError>
87where
88    'pc: 'state,
89{
90    debug_assert_eq!(gas_charger.move_gas_status().stack_height_current(), 0);
91    let T::Transaction {
92        gas_payment,
93        bytes,
94        objects,
95        withdrawals,
96        pure,
97        receiving,
98        withdrawal_compatibility_conversions: _,
99        original_command_len: _,
100        commands,
101    } = ast;
102    let mut context = Context::new(
103        env,
104        metrics,
105        tx_context,
106        gas_charger,
107        gas_payment,
108        bytes,
109        objects,
110        withdrawals,
111        pure,
112        receiving,
113    )?;
114
115    trace_utils::trace_ptb_summary(&mut context, trace_builder_opt, &commands)?;
116
117    let mut mode_results = Mode::empty_results();
118    for sp!(annotated_index, c) in commands {
119        let annotated_index = annotated_index as usize;
120        let start = Instant::now();
121        if let Err(err) =
122            execute_command::<Mode>(&mut context, &mut mode_results, c, trace_builder_opt)
123        {
124            // We still need to record the loaded child objects for replay
125            let loaded_runtime_objects = object_runtime!(context)?.loaded_runtime_objects();
126            // we do not save the wrapped objects since on error, they should not be modified
127            drop(context);
128            // TODO wtf is going on with the borrow checker here. 'state is bound into the object
129            // runtime, but its since been dropped. what gives with this error?
130            env.state_view
131                .save_loaded_runtime_objects(loaded_runtime_objects);
132            timings.error(annotated_index, start.elapsed());
133            return Err(err.with_command_index(annotated_index));
134        };
135        timings.executed(annotated_index, start.elapsed());
136    }
137    // Save loaded objects table in case we fail in post execution
138    //
139    // We still need to record the loaded child objects for replay
140    // Record the objects loaded at runtime (dynamic fields + received) for
141    // storage rebate calculation.
142    let loaded_runtime_objects = object_runtime!(context)?.loaded_runtime_objects();
143    // We record what objects were contained in at the start of the transaction
144    // for expensive invariant checks
145    let wrapped_object_containers = object_runtime!(context)?.wrapped_object_containers();
146    // We record the generated object IDs for expensive invariant checks
147    let generated_object_ids = object_runtime!(context)?.generated_object_ids();
148
149    // apply changes
150    let finished = context.finish::<Mode>();
151    // Save loaded objects for debug. We dont want to lose the info
152    env.state_view
153        .save_loaded_runtime_objects(loaded_runtime_objects);
154    env.state_view
155        .save_wrapped_object_containers(wrapped_object_containers);
156    env.state_view.record_execution_results(finished?)?;
157    env.state_view
158        .record_generated_object_ids(generated_object_ids);
159    Ok(mode_results)
160}
161
162/// Execute a single command
163#[instrument(level = "trace", skip_all)]
164fn execute_command<Mode: ExecutionMode>(
165    context: &mut Context,
166    mode_results: &mut Mode::ExecutionResults,
167    c: T::Command_,
168    trace_builder_opt: &mut Option<MoveTraceBuilder>,
169) -> Result<(), ExecutionError> {
170    let T::Command_ {
171        command,
172        result_type,
173        drop_values,
174        consumed_shared_objects: _,
175    } = c;
176    assert_invariant!(
177        context.gas_charger.move_gas_status().stack_height_current() == 0,
178        "stack height did not start at 0"
179    );
180    let is_move_call = matches!(command, T::Command__::MoveCall(_));
181    let num_args = command.arguments_len();
182    let mut args_to_update = vec![];
183    let result = match command {
184        T::Command__::MoveCall(move_call) => {
185            trace_utils::trace_move_call_start(trace_builder_opt);
186            let T::MoveCall {
187                function,
188                arguments,
189            } = *move_call;
190            // Detect send_funds with gas coin
191            let is_gas_coin_send_funds = is_coin_send_funds(&function)
192                && arguments.first().is_some_and(|arg| {
193                    matches!(
194                        &arg.value.0,
195                        T::Argument__::Use(T::Usage::Move(T::Location::GasCoin))
196                    )
197                });
198            if Mode::TRACK_EXECUTION {
199                args_to_update.extend(
200                    arguments
201                        .iter()
202                        .filter(|arg| matches!(&arg.value.1, T::Type::Reference(/* mut */ true, _)))
203                        .cloned(),
204                )
205            }
206            let arguments: Vec<CtxValue> = context.arguments(arguments)?;
207            if is_gas_coin_send_funds {
208                assert_invariant!(arguments.len() == 2, "coin::send_funds should have 2 args");
209                let recipient = arguments.last().unwrap().to_address()?;
210                context.record_gas_coin_transfer(GasCoinTransfer::SendFunds { recipient })?;
211            }
212            let res = context.vm_move_call(function, arguments, trace_builder_opt);
213            trace_utils::trace_move_call_end(trace_builder_opt);
214            res?
215        }
216        T::Command__::TransferObjects(objects, recipient) => {
217            // Check if any object is the gas coin moved by value before consuming
218            let has_gas_coin_move = objects.iter().any(|arg| {
219                matches!(
220                    &arg.value.0,
221                    T::Argument__::Use(T::Usage::Move(T::Location::GasCoin))
222                )
223            });
224            if has_gas_coin_move {
225                context.record_gas_coin_transfer(GasCoinTransfer::TransferObjects)?;
226            }
227            let object_tys = objects
228                .iter()
229                .map(|sp!(_, (_, ty))| ty.clone())
230                .collect::<Vec<_>>();
231            let object_values: Vec<CtxValue> = context.arguments(objects)?;
232            let recipient: AccountAddress = context.argument(recipient)?;
233            assert_invariant!(
234                object_values.len() == object_tys.len(),
235                "object values and types mismatch"
236            );
237            trace_utils::trace_transfer(context, trace_builder_opt, &object_values, &object_tys)?;
238            for (object_value, ty) in object_values.into_iter().zip_debug_eq(object_tys) {
239                // TODO should we just call a Move function?
240                let recipient = Owner::AddressOwner(recipient.into());
241                context.transfer_object(recipient, ty, object_value)?;
242            }
243            vec![]
244        }
245        T::Command__::SplitCoins(ty, coin, amounts) => {
246            let mut trace_values = vec![];
247            // TODO should we just call a Move function?
248            if Mode::TRACK_EXECUTION {
249                args_to_update.push(coin.clone());
250            }
251            let coin_ref: CtxValue = context.argument(coin)?;
252            let amount_values: Vec<u64> = context.arguments(amounts)?;
253            let mut total: u64 = 0;
254            for amount in &amount_values {
255                let Some(new_total) = total.checked_add(*amount) else {
256                    return Err(ExecutionError::from_kind(
257                        ExecutionErrorKind::CoinBalanceOverflow,
258                    ));
259                };
260                total = new_total;
261            }
262            trace_utils::add_move_value_info_from_ctx_value(
263                context,
264                trace_builder_opt,
265                &mut trace_values,
266                &ty,
267                &coin_ref,
268            )?;
269            let coin_value = context.copy_value(&coin_ref)?.coin_ref_value()?;
270            fp_ensure!(
271                coin_value >= total,
272                ExecutionError::new_with_source(
273                    ExecutionErrorKind::InsufficientCoinBalance,
274                    format!("balance: {coin_value} required: {total}")
275                )
276            );
277            coin_ref.coin_ref_subtract_balance(total)?;
278            let amounts = amount_values
279                .into_iter()
280                .map(|a| context.new_coin(a))
281                .collect::<Result<Vec<_>, _>>()?;
282            trace_utils::trace_split_coins(
283                context,
284                trace_builder_opt,
285                &ty,
286                trace_values,
287                &amounts,
288                total,
289            )?;
290
291            amounts
292        }
293        T::Command__::MergeCoins(ty, target, coins) => {
294            let mut trace_values = vec![];
295            // TODO should we just call a Move function?
296            if Mode::TRACK_EXECUTION {
297                args_to_update.push(target.clone());
298            }
299            let target_ref: CtxValue = context.argument(target)?;
300            trace_utils::add_move_value_info_from_ctx_value(
301                context,
302                trace_builder_opt,
303                &mut trace_values,
304                &ty,
305                &target_ref,
306            )?;
307            let coins = context.arguments(coins)?;
308            let amounts = coins
309                .into_iter()
310                .map(|coin| {
311                    trace_utils::add_move_value_info_from_ctx_value(
312                        context,
313                        trace_builder_opt,
314                        &mut trace_values,
315                        &ty,
316                        &coin,
317                    )?;
318                    context.destroy_coin(coin)
319                })
320                .collect::<Result<Vec<_>, _>>()?;
321            let mut additional: u64 = 0;
322            for amount in amounts {
323                let Some(new_additional) = additional.checked_add(amount) else {
324                    return Err(ExecutionError::from_kind(
325                        ExecutionErrorKind::CoinBalanceOverflow,
326                    ));
327                };
328                additional = new_additional;
329            }
330            let target_value = context.copy_value(&target_ref)?.coin_ref_value()?;
331            fp_ensure!(
332                target_value.checked_add(additional).is_some(),
333                ExecutionError::from_kind(ExecutionErrorKind::CoinBalanceOverflow,)
334            );
335            target_ref.coin_ref_add_balance(additional)?;
336            trace_utils::trace_merge_coins(
337                context,
338                trace_builder_opt,
339                &ty,
340                trace_values,
341                additional,
342            )?;
343            vec![]
344        }
345        T::Command__::MakeMoveVec(ty, items) => {
346            let items: Vec<CtxValue> = context.arguments(items)?;
347            trace_utils::trace_make_move_vec(context, trace_builder_opt, &items, &ty)?;
348            vec![CtxValue::vec_pack(ty, items)?]
349        }
350        T::Command__::Publish(module_bytes, dep_ids, linkage) => {
351            trace_utils::trace_publish_event(trace_builder_opt)?;
352            let modules =
353                context.deserialize_modules(&module_bytes, /* is upgrade */ false)?;
354
355            let original_id = context.publish_and_init_package::<Mode>(
356                modules,
357                &dep_ids,
358                linkage,
359                trace_builder_opt,
360            )?;
361
362            if <Mode>::packages_are_predefined() {
363                // no upgrade cap for genesis modules
364                std::vec![]
365            } else {
366                std::vec![context.new_upgrade_cap(original_id)?]
367            }
368        }
369        T::Command__::Upgrade(
370            module_bytes,
371            dep_ids,
372            current_package_id,
373            upgrade_ticket,
374            linkage,
375        ) => {
376            trace_utils::trace_upgrade_event(trace_builder_opt)?;
377            let upgrade_ticket = context
378                .argument::<CtxValue>(upgrade_ticket)?
379                .into_upgrade_ticket()?;
380            // Make sure the passed-in package ID matches the package ID in the `upgrade_ticket`.
381            if current_package_id != upgrade_ticket.package.bytes {
382                return Err(ExecutionError::from_kind(
383                    ExecutionErrorKind::PackageUpgradeError {
384                        upgrade_error: PackageUpgradeError::PackageIDDoesNotMatch {
385                            package_id: current_package_id,
386                            ticket_id: upgrade_ticket.package.bytes,
387                        },
388                    },
389                ));
390            }
391            // deserialize modules and charge gas
392            let modules = context.deserialize_modules(&module_bytes, /* is upgrade */ true)?;
393
394            let computed_digest = MovePackage::compute_digest_for_modules_and_deps(
395                &module_bytes,
396                &dep_ids,
397                /* hash_modules */ true,
398            )
399            .to_vec();
400            if computed_digest != upgrade_ticket.digest {
401                return Err(ExecutionError::from_kind(
402                    ExecutionErrorKind::PackageUpgradeError {
403                        upgrade_error: PackageUpgradeError::DigestDoesNotMatch {
404                            digest: computed_digest,
405                        },
406                    },
407                ));
408            }
409
410            let upgraded_package_id = context.upgrade(
411                modules,
412                &dep_ids,
413                current_package_id,
414                upgrade_ticket.policy,
415                linkage,
416            )?;
417
418            vec![context.upgrade_receipt(upgrade_ticket, upgraded_package_id)]
419        }
420    };
421    if Mode::TRACK_EXECUTION {
422        let argument_updates = context.argument_updates(args_to_update)?;
423        let command_result = context.tracked_results(&result, &result_type)?;
424        Mode::finish_command_v2(mode_results, argument_updates, command_result)?;
425    }
426    assert_invariant!(
427        result.len() == drop_values.len(),
428        "result values and drop values mismatch"
429    );
430    context.charge_command(is_move_call, num_args, result.len())?;
431    let result = result
432        .into_iter()
433        .zip_debug_eq(drop_values)
434        .map(|(value, drop)| if !drop { Some(value) } else { None })
435        .collect::<Vec<_>>();
436    context.result(result)?;
437    assert_invariant!(
438        context.gas_charger.move_gas_status().stack_height_current() == 0,
439        "stack height did not end at 0"
440    );
441    Ok(())
442}
443
444/// Struct to track execution timings, coalesced into the annotated command indices.
445struct IndexedExecutionTimings {
446    /// The maximum index in the original command vector. All annotated indices will be capped at
447    /// this value.
448    max_allowed_index: usize,
449    /// Mapping from the command's annotated index to its duration. Multiple commands may share
450    /// the same annotated index, in which case their durations will be added together.
451    executed_commands: BTreeMap<usize, Duration>,
452    /// `Some` if an error occurred, stopping execution.
453    /// `usize` is the annotated index of the command.
454    error_command: Option<(usize, Duration)>,
455}
456
457impl IndexedExecutionTimings {
458    fn new(original_command_len: usize) -> Self {
459        let max_allowed_index = original_command_len.saturating_sub(1);
460        Self {
461            max_allowed_index,
462            executed_commands: BTreeMap::new(),
463            error_command: None,
464        }
465    }
466
467    /// Records the execution of a successful command.
468    fn executed(&mut self, annotated_index: usize, duration: Duration) {
469        debug_assert!(
470            self.error_command.is_none(),
471            "command executed after an error occurred"
472        );
473        let index = annotated_index.min(self.max_allowed_index);
474        let existing = self
475            .executed_commands
476            .entry(index)
477            .or_insert(Duration::ZERO);
478        *existing = existing.saturating_add(duration);
479    }
480
481    /// Record the execution of a failed command that errored and stopped the execution of the PTB.
482    fn error(&mut self, annotated_index: usize, duration: Duration) {
483        debug_assert!(self.error_command.is_none(), "multiple errors recorded");
484        let index = annotated_index.min(self.max_allowed_index);
485        debug_assert!(
486            self.executed_commands
487                .last_key_value()
488                .is_none_or(|(last, _)| *last <= index),
489            "execution timings recorded for command index {:?} after error at index {}",
490            self.executed_commands
491                .last_key_value()
492                .map(|(last, _)| *last),
493            index,
494        );
495
496        let existing_opt = self.executed_commands.remove(&index);
497        let total_duration = existing_opt
498            .unwrap_or(Duration::ZERO)
499            .saturating_add(duration);
500        self.error_command = Some((index, total_duration));
501    }
502
503    /// Coalesces timings by each commands annotated index to align with the original command count.
504    /// Extra commands may have been injected during typing (e.g., withdrawal compatibility).
505    /// Timings sharing an `annotated_index` have their durations summed. An error, if present,
506    /// is always last.
507    fn into_coalesced(self) -> Vec<ExecutionTiming> {
508        let Self {
509            max_allowed_index,
510            executed_commands,
511            error_command,
512        } = self;
513
514        let max_executed_index = executed_commands.keys().last().copied();
515        let error_index = error_command.as_ref().map(|(idx, _)| *idx);
516        let max_used_index = match (max_executed_index, error_index) {
517            (Some(exec), Some(err)) => exec.max(err),
518            (Some(idx), None) | (None, Some(idx)) => idx,
519            (None, None) => return vec![],
520        };
521        debug_assert!(
522            max_used_index <= max_allowed_index,
523            "max used index {} exceeds max allowed index {}",
524            max_used_index,
525            max_allowed_index
526        );
527        let size = max_used_index.saturating_add(1);
528
529        // We initialize a vector of `Success` timings with zero duration, since we have no
530        // guarantee at this point that there are no gaps in the annotated indices. Presently,
531        // there should be no gaps, but there is nothing inherent to the annotation scheme that
532        // guarantees they are not sparse.
533        let mut coalesced = vec![ExecutionTiming::Success(Duration::ZERO); size];
534        for (index, duration) in executed_commands {
535            let Some(entry) = coalesced.get_mut(index) else {
536                debug_assert!(
537                    false,
538                    "failed to initialize coalesced timings at index {}",
539                    index
540                );
541                continue;
542            };
543            debug_assert!(matches!(entry, ExecutionTiming::Success(d) if d.is_zero()));
544            *entry = ExecutionTiming::Success(duration);
545        }
546
547        if let Some((index, error_duration)) = error_command {
548            debug_assert!(
549                index == coalesced.len().saturating_sub(1),
550                "error index should be last"
551            );
552            if let Some(entry) = coalesced.get_mut(index) {
553                debug_assert!(matches!(entry, ExecutionTiming::Success(d) if d.is_zero()));
554                *entry = ExecutionTiming::Abort(error_duration);
555            } else {
556                debug_assert!(
557                    false,
558                    "failed to initialize coalesced timings at index {}",
559                    index
560                );
561            };
562        }
563
564        coalesced
565    }
566}