Skip to main content

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