sui_adapter_latest/
execution_engine.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4pub use checked::*;
5
6#[sui_macros::with_checked_arithmetic]
7mod checked {
8
9    use crate::adapter::new_move_runtime;
10    use crate::execution_mode::{self, ExecutionMode};
11    use crate::execution_value::SuiResolver;
12    use crate::gas_charger::{PaymentKind, PaymentMethod};
13    use move_binary_format::CompiledModule;
14    use move_trace_format::format::MoveTraceBuilder;
15    use move_vm_runtime::runtime::MoveRuntime;
16    use mysten_common::{assert_reachable, debug_fatal, in_test_configuration};
17    use std::collections::{BTreeMap, BTreeSet};
18    use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
19    use sui_types::accumulator_root::{ACCUMULATOR_ROOT_CREATE_FUNC, ACCUMULATOR_ROOT_MODULE};
20    use sui_types::balance::{
21        BALANCE_CREATE_REWARDS_FUNCTION_NAME, BALANCE_DESTROY_REBATES_FUNCTION_NAME,
22        BALANCE_MODULE_NAME,
23    };
24    use sui_types::coin_reservation::ParsedDigest;
25    use sui_types::execution_params::ExecutionOrEarlyError;
26    use sui_types::gas_coin::GAS;
27    use sui_types::messages_checkpoint::CheckpointTimestamp;
28    use sui_types::metrics::ExecutionMetrics;
29    use sui_types::object::OBJECT_START_VERSION;
30    use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
31    use sui_types::randomness_state::{
32        RANDOMNESS_MODULE_NAME, RANDOMNESS_STATE_CREATE_FUNCTION_NAME,
33        RANDOMNESS_STATE_UPDATE_FUNCTION_NAME,
34    };
35    use sui_types::{BRIDGE_ADDRESS, SUI_BRIDGE_OBJECT_ID, SUI_RANDOMNESS_STATE_OBJECT_ID};
36    use tracing::{info, instrument, trace, warn};
37
38    use crate::static_programmable_transactions as SPT;
39    use crate::sui_types::gas::SuiGasStatusAPI;
40    use crate::{gas_charger::GasCharger, temporary_store::TemporaryStore};
41    use move_core_types::ident_str;
42    use move_core_types::language_storage::TypeTag;
43    use sui_move_natives::all_natives;
44    use sui_protocol_config::{
45        Chain, LimitThresholdCrossed, PerObjectCongestionControlMode, ProtocolConfig,
46        check_limit_by_meter,
47    };
48    use sui_types::authenticator_state::{
49        AUTHENTICATOR_STATE_CREATE_FUNCTION_NAME, AUTHENTICATOR_STATE_EXPIRE_JWKS_FUNCTION_NAME,
50        AUTHENTICATOR_STATE_MODULE_NAME, AUTHENTICATOR_STATE_UPDATE_FUNCTION_NAME,
51    };
52    use sui_types::base_types::{ObjectID, SequenceNumber};
53    use sui_types::bridge::BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER;
54    use sui_types::bridge::{
55        BRIDGE_CREATE_FUNCTION_NAME, BRIDGE_INIT_COMMITTEE_FUNCTION_NAME, BRIDGE_MODULE_NAME,
56        BridgeChainId,
57    };
58    use sui_types::clock::{CLOCK_MODULE_NAME, CONSENSUS_COMMIT_PROLOGUE_FUNCTION_NAME};
59    use sui_types::committee::EpochId;
60    use sui_types::deny_list_v1::{DENY_LIST_CREATE_FUNC, DENY_LIST_MODULE};
61    use sui_types::digests::{
62        ChainIdentifier, get_mainnet_chain_identifier, get_testnet_chain_identifier,
63    };
64    use sui_types::effects::TransactionEffects;
65    use sui_types::error::{ExecutionError, ExecutionErrorTrait};
66    use sui_types::execution::{ExecutionTiming, ResultWithTimings, SharedInput};
67    use sui_types::execution_status::{ExecutionErrorKind, ExecutionStatus};
68    use sui_types::gas::GasCostSummary;
69    use sui_types::gas::SuiGasStatus;
70    use sui_types::id::UID;
71    use sui_types::inner_temporary_store::InnerTemporaryStore;
72    use sui_types::storage::BackingStore;
73    #[cfg(msim)]
74    use sui_types::sui_system_state::advance_epoch_result_injection::maybe_modify_result_for;
75    use sui_types::sui_system_state::{ADVANCE_EPOCH_SAFE_MODE_FUNCTION_NAME, AdvanceEpochParams};
76    use sui_types::transaction::{
77        Argument, AuthenticatorStateExpire, AuthenticatorStateUpdate, CallArg, ChangeEpoch,
78        Command, EndOfEpochTransactionKind, GasData, GenesisTransaction, ObjectArg,
79        ProgrammableTransaction, Reservation, StoredExecutionTimeObservations, TransactionKind,
80        WithdrawFrom, WriteAccumulatorStorageCost, is_gasless_transaction,
81    };
82    use sui_types::transaction::{CheckedInputObjects, RandomnessStateUpdate};
83    use sui_types::{
84        SUI_AUTHENTICATOR_STATE_OBJECT_ID, SUI_FRAMEWORK_ADDRESS, SUI_FRAMEWORK_PACKAGE_ID,
85        SUI_SYSTEM_PACKAGE_ID,
86        base_types::{SuiAddress, TransactionDigest, TxContext},
87        object::{Object, ObjectInner},
88        sui_system_state::{ADVANCE_EPOCH_FUNCTION_NAME, SUI_SYSTEM_MODULE_NAME},
89    };
90
91    /// Whether the *head* early error is `InsufficientFundsForWithdraw`. Used to gate the first
92    /// address-balance gas-payment pruning hotfix: the head error is the one surfaced as the
93    /// failure status, so keying off it (rather than any occurrence) keeps the pruning bit-for-bit
94    /// with the original single-error hotfix.
95    fn head_error_is_insufficient_funds_for_withdraw(
96        execution_params: &ExecutionOrEarlyError,
97    ) -> bool {
98        execution_params.early_errors().is_some_and(|errors| {
99            matches!(
100                errors.head,
101                ExecutionErrorKind::InsufficientFundsForWithdraw
102            )
103        })
104    }
105
106    fn payment_kind(
107        gas_data: &GasData,
108        transaction_kind: &TransactionKind,
109        protocol_config: &ProtocolConfig,
110    ) -> PaymentKind {
111        if gas_data.is_unmetered() || transaction_kind.is_system_tx() {
112            PaymentKind::unmetered()
113        } else if protocol_config.enable_gasless()
114            && is_gasless_transaction(gas_data, transaction_kind)
115        {
116            PaymentKind::gasless()
117        } else if gas_data.payment.is_empty() {
118            PaymentKind::smash(vec![PaymentMethod::AddressBalance(
119                gas_data.owner,
120                gas_data.budget,
121            )])
122            .expect("unable to create a payment kind with a single address balance")
123        } else {
124            let payment_methods = gas_data
125                .payment
126                .iter()
127                .map(|entry| {
128                    if let Ok(parsed) = ParsedDigest::try_from(entry.2) {
129                        PaymentMethod::AddressBalance(gas_data.owner, parsed.reservation_amount())
130                    } else {
131                        PaymentMethod::Coin(*entry)
132                    }
133                })
134                .collect();
135            PaymentKind::smash(payment_methods).expect(
136                "unable to create a payment kind from payment methods. \
137                 Should not be possible wit ha non-empty vector",
138            )
139        }
140    }
141
142    type ExecutionOutput<Mode> = (
143        InnerTemporaryStore,
144        SuiGasStatus,
145        TransactionEffects,
146        Vec<ExecutionTiming>,
147        Result<<Mode as ExecutionMode>::ExecutionResults, <Mode as ExecutionMode>::Error>,
148    );
149    #[instrument(name = "tx_execute_to_effects", level = "debug", skip_all)]
150    pub fn execute_transaction_to_effects<Mode: ExecutionMode>(
151        store: &dyn BackingStore,
152        input_objects: CheckedInputObjects,
153        gas_data: GasData,
154        gas_status: SuiGasStatus,
155        transaction_kind: TransactionKind,
156        rewritten_inputs: Option<Vec<bool>>,
157        transaction_signer: SuiAddress,
158        transaction_digest: TransactionDigest,
159        move_vm: &Arc<MoveRuntime>,
160        epoch_id: &EpochId,
161        epoch_timestamp_ms: u64,
162        protocol_config: &ProtocolConfig,
163        metrics: Arc<ExecutionMetrics>,
164        enable_expensive_checks: bool,
165        execution_params: ExecutionOrEarlyError,
166        trace_builder_opt: &mut Option<MoveTraceBuilder>,
167    ) -> ExecutionOutput<Mode> {
168        let input_objects = input_objects.into_inner();
169        let mutable_inputs = if enable_expensive_checks {
170            input_objects.all_mutable_inputs().keys().copied().collect()
171        } else {
172            HashSet::new()
173        };
174        let shared_object_refs = input_objects.filter_shared_objects();
175        let receiving_objects = transaction_kind.receiving_objects();
176        let transaction_dependencies = input_objects.transaction_dependencies();
177
178        let temporary_store = TemporaryStore::new(
179            store,
180            input_objects,
181            receiving_objects,
182            transaction_digest,
183            protocol_config,
184            *epoch_id,
185        );
186
187        // TODO: remove all `legacy` code on the next execution version cut
188        legacy::execute_transaction_inner::<Mode>(
189            store,
190            temporary_store,
191            gas_data,
192            gas_status,
193            transaction_kind,
194            rewritten_inputs,
195            transaction_signer,
196            transaction_digest,
197            move_vm,
198            epoch_id,
199            epoch_timestamp_ms,
200            protocol_config,
201            metrics,
202            enable_expensive_checks,
203            execution_params,
204            trace_builder_opt,
205            shared_object_refs,
206            transaction_dependencies,
207            mutable_inputs,
208        )
209    }
210
211    fn update_vm_telemetry_metrics(metrics: &ExecutionMetrics, move_vm: &MoveRuntime) {
212        metrics.vm_telemetry_metrics.try_update(|vm_metrics| {
213            let t = move_vm.get_telemetry_report();
214            vm_metrics
215                .move_vm_package_cache_count
216                .set(t.package_cache_count as i64);
217            vm_metrics
218                .move_vm_total_arena_size_bytes
219                .set(t.total_arena_size as i64);
220            vm_metrics.move_vm_module_count.set(t.module_count as i64);
221            vm_metrics
222                .move_vm_function_count
223                .set(t.function_count as i64);
224            vm_metrics.move_vm_type_count.set(t.type_count as i64);
225            vm_metrics.move_vm_interner_size.set(t.interner_size as i64);
226            vm_metrics
227                .move_vm_vtable_cache_count
228                .set(t.vtable_cache_count as i64);
229            vm_metrics
230                .move_vm_vtable_cache_hits
231                .set(t.vtable_cache_hits as i64);
232            vm_metrics
233                .move_vm_vtable_cache_misses
234                .set(t.vtable_cache_misses as i64);
235            vm_metrics
236                .move_vm_load_time_ms
237                .set(t.total_load_time as i64);
238            vm_metrics.move_vm_load_count.set(t.load_count as i64);
239            vm_metrics
240                .move_vm_validation_time_ms
241                .set(t.total_validation_time as i64);
242            vm_metrics
243                .move_vm_validation_count
244                .set(t.validation_count as i64);
245            vm_metrics.move_vm_jit_time_ms.set(t.total_jit_time as i64);
246            vm_metrics.move_vm_jit_count.set(t.jit_count as i64);
247            vm_metrics
248                .move_vm_execution_time_ms
249                .set(t.total_execution_time as i64);
250            vm_metrics
251                .move_vm_execution_count
252                .set(t.execution_count as i64);
253            vm_metrics
254                .move_vm_interpreter_time_ms
255                .set(t.total_interpreter_time as i64);
256            vm_metrics
257                .move_vm_interpreter_count
258                .set(t.interpreter_count as i64);
259            vm_metrics
260                .move_vm_max_callstack_size
261                .set(t.max_callstack_size as i64);
262            vm_metrics
263                .move_vm_max_valuestack_size
264                .set(t.max_valuestack_size as i64);
265            vm_metrics.move_vm_total_time_ms.set(t.total_time as i64);
266            vm_metrics.move_vm_total_count.set(t.total_count as i64);
267        });
268    }
269
270    pub fn execute_genesis_state_update(
271        store: &dyn BackingStore,
272        protocol_config: &ProtocolConfig,
273        metrics: Arc<ExecutionMetrics>,
274        move_vm: &Arc<MoveRuntime>,
275        tx_context: Rc<RefCell<TxContext>>,
276        input_objects: CheckedInputObjects,
277        pt: ProgrammableTransaction,
278    ) -> Result<InnerTemporaryStore, ExecutionError> {
279        let input_objects = input_objects.into_inner();
280        let mut temporary_store = TemporaryStore::new(
281            store,
282            input_objects,
283            vec![],
284            tx_context.borrow().digest(),
285            protocol_config,
286            0,
287        );
288        let mut gas_charger = GasCharger::new_unmetered(tx_context.borrow().digest());
289        SPT::execute::<execution_mode::Genesis>(
290            protocol_config,
291            metrics,
292            move_vm,
293            &mut temporary_store,
294            store.as_backing_package_store(),
295            tx_context,
296            &mut gas_charger,
297            None,
298            pt,
299            &mut None,
300        )
301        .map_err(|(e, _)| e)?;
302        temporary_store.update_object_version_and_prev_tx();
303        Ok(temporary_store.into_inner(BTreeMap::new()))
304    }
305
306    /// Frozen pre-v15 (`gas_model_version < 15`) execution, mirroring `origin/main`. Removed at the
307    /// next execution-version cut.
308    mod legacy {
309        use super::*;
310
311        // MAGIC CONSTANTS -- these are all mainnet-only hardcoded constants and should not be
312        // changed (but can be removed in future execution cuts).
313
314        /// Mainnet recovery point: the fix replays for transactions at/above this accumulator root
315        /// version and keeps the old behavior below it. A compiled constant (not a protocol flag)
316        /// because it had to take effect mid-epoch during recovery, when the network can't reconfigure.
317        pub(super) const ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION: SequenceNumber =
318            SequenceNumber::from_u64(692949576);
319
320        /// Mainnet settlement version at/above which an `InsufficientFundsForWithdraw` transaction
321        /// short-circuits execution entirely (zero-gas effects, mutable-input version bumps only),
322        /// superseding the address-balance gas-payment pruning hotfix. A compiled constant, not a
323        /// protocol flag, because it must take effect mid-epoch during recovery when the network cannot
324        /// reconfigure. Only consulted when an accumulator version is assigned (mainnet committed
325        /// execution); everywhere else the short-circuit is protocol gated (see
326        /// `should_short_circuit_insufficient_funds`).
327        ///
328        /// Value is the mainnet accumulator root version where the new binary was activated on the network.
329        pub(super) const ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION:
330            SequenceNumber = SequenceNumber::from_u64(693531074);
331
332        /// Whether to prune the address-balance leg of gas smashing for an IFFW transaction. This is
333        /// the mainnet-only accumulator backfill that replays the pre-flag incident hotfix below the
334        /// short-circuit rollout point; once `early_exit_on_iffw` is set the short-circuit handles IFFW
335        /// upstream, so reaching here implies the flag is off (asserted below).
336        pub(super) fn should_filter_address_balance_gas_smash(
337            execution_params: &ExecutionOrEarlyError,
338            protocol_config: &ProtocolConfig,
339        ) -> bool {
340            if !head_error_is_insufficient_funds_for_withdraw(execution_params) {
341                return false;
342            }
343            debug_assert!(
344                !protocol_config.early_exit_on_iffw(),
345                "Should not reach gas smashing filtering address balances if IFFW early exit is enabled"
346            );
347            // In test/debug builds, always apply the fix unconditionally to match the behaviour of
348            // the 1.72 mainnet release (where it was deployed as an ungated hotfix).
349            in_test_configuration()
350                || protocol_config.early_exit_on_iffw()
351                || (protocol_config.chain() == Chain::Mainnet
352                    && execution_params
353                        .accumulator_version()
354                        .is_some_and(|v| v >= ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION))
355        }
356
357        /// Whether to short-circuit an IFFW transaction. When an accumulator version is assigned
358        /// (mainnet committed execution) it gates on the settlement-version rollout point; otherwise
359        /// (every other chain and non-committed paths, where no accumulator version is assigned) the
360        /// short-circuit applies based on `early_exit_on_iffw`.
361        pub(super) fn should_short_circuit_insufficient_funds(
362            execution_params: &ExecutionOrEarlyError,
363            protocol_config: &ProtocolConfig,
364        ) -> bool {
365            // If no IFWWs, then does not apply
366            if !execution_params.early_errors().is_some_and(|errors| {
367                errors
368                    .iter()
369                    .any(|e| matches!(e, ExecutionErrorKind::InsufficientFundsForWithdraw))
370            }) {
371                return false;
372            }
373
374            // In test/debug builds, always short-circuit unconditionally to match the behaviour of
375            // the 1.72 mainnet release (where it was deployed as an ungated hotfix).
376            if in_test_configuration() {
377                return true;
378            }
379
380            // otherwise gate by accumulator version (if present) or protocol flag
381            protocol_config.early_exit_on_iffw()
382                || (protocol_config.chain() == Chain::Mainnet
383                    && execution_params.accumulator_version().is_some_and(|v| {
384                        v >= ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION
385                    }))
386        }
387
388        /// Frozen pre-v15 (`gas_model_version < 15`) execution; mirrors `origin/main`.
389        #[allow(clippy::too_many_arguments)]
390        pub(super) fn execute_transaction_inner<Mode: ExecutionMode>(
391            store: &dyn BackingStore,
392            mut temporary_store: TemporaryStore<'_>,
393            mut gas_data: GasData,
394            gas_status: SuiGasStatus,
395            transaction_kind: TransactionKind,
396            rewritten_inputs: Option<Vec<bool>>,
397            transaction_signer: SuiAddress,
398            transaction_digest: TransactionDigest,
399            move_vm: &Arc<MoveRuntime>,
400            epoch_id: &EpochId,
401            epoch_timestamp_ms: u64,
402            protocol_config: &ProtocolConfig,
403            metrics: Arc<ExecutionMetrics>,
404            enable_expensive_checks: bool,
405            execution_params: ExecutionOrEarlyError,
406            trace_builder_opt: &mut Option<MoveTraceBuilder>,
407            shared_object_refs: Vec<SharedInput>,
408            mut transaction_dependencies: BTreeSet<TransactionDigest>,
409            mutable_inputs: HashSet<ObjectID>,
410        ) -> ExecutionOutput<Mode> {
411            // Short-circuit on InsufficientFundsForWithdraw: the transaction is guaranteed to fail
412            // and has nothing to execute, so skip the executor pipeline. Bump versions of mutable
413            // inputs (so locks advance) and emit effects with a zero gas cost summary. On mainnet
414            // committed execution this is gated on the settlement-version rollout point (below it we
415            // fall through to the address-balance gas-payment pruning hotfix instead); everywhere else
416            // it applies based on `early_exit_on_iffw`.
417            if should_short_circuit_insufficient_funds(&execution_params, protocol_config) {
418                assert_reachable!("IFFW short-circuit fired");
419                temporary_store.ensure_active_inputs_mutated();
420                transaction_dependencies.remove(&TransactionDigest::genesis_marker());
421
422                let execution_error: Mode::Error =
423                    ExecutionError::from_kind(ExecutionErrorKind::InsufficientFundsForWithdraw)
424                        .into();
425                let status = ExecutionStatus::new_failure(execution_error.to_execution_failure());
426                let gas_meter = GasCharger::new(
427                    transaction_digest,
428                    PaymentKind::gasless(),
429                    gas_status,
430                    &mut temporary_store,
431                    protocol_config,
432                );
433
434                let gas_coin = gas_meter.gas_coin();
435                let (inner, effects) = temporary_store.into_effects(
436                    shared_object_refs,
437                    &transaction_digest,
438                    transaction_dependencies,
439                    GasCostSummary::default(),
440                    status,
441                    gas_coin,
442                    *epoch_id,
443                );
444
445                return (
446                    inner,
447                    gas_meter.into_gas_status(),
448                    effects,
449                    vec![],
450                    Err(execution_error),
451                );
452            }
453
454            let sponsor = {
455                let gas_owner = gas_data.owner;
456                if gas_owner == transaction_signer {
457                    None
458                } else {
459                    Some(gas_owner)
460                }
461            };
462            let gas_price = gas_status.gas_price();
463            let rgp = gas_status.reference_gas_price();
464
465            // On an IFFW abort, drop the address-balance gas payments (keeping real coins) so the
466            // pruned list flows into `payment_kind`/`compute_input_reservations` with no special
467            // handling. See `should_filter_address_balance_gas_smash` for when this applies.
468            if should_filter_address_balance_gas_smash(&execution_params, protocol_config)
469                && gas_data.payment.len() > 1
470                && ParsedDigest::try_from(gas_data.payment[0].2).is_err()
471            {
472                gas_data
473                    .payment
474                    .retain(|entry| ParsedDigest::try_from(entry.2).is_err());
475            }
476
477            let mut gas_charger = GasCharger::new(
478                transaction_digest,
479                payment_kind(&gas_data, &transaction_kind, protocol_config),
480                gas_status,
481                &mut temporary_store,
482                protocol_config,
483            );
484
485            let tx_ctx = TxContext::new_from_components(
486                &transaction_signer,
487                &transaction_digest,
488                epoch_id,
489                epoch_timestamp_ms,
490                rgp,
491                gas_price,
492                gas_data.budget,
493                sponsor,
494                protocol_config,
495            );
496            let tx_ctx = Rc::new(RefCell::new(tx_ctx));
497
498            let is_gasless = protocol_config.enable_gasless()
499                && is_gasless_transaction(&gas_data, &transaction_kind);
500            let is_epoch_change = transaction_kind.is_end_of_epoch_tx();
501
502            // Cache the transaction-derived invariant-check inputs (reservation budget, advance-epoch
503            // mint/burn, genesis flag) on the store, after the gas-smash filtering above. The
504            // conservation checks and the ownership-invariant check read them from there.
505            temporary_store.set_invariant_inputs(&transaction_kind, &gas_data, transaction_signer);
506
507            let (gas_cost_summary, mut execution_result, timings) = execute_transaction::<Mode>(
508                store,
509                &mut temporary_store,
510                transaction_kind,
511                rewritten_inputs,
512                &mut gas_charger,
513                tx_ctx,
514                move_vm,
515                protocol_config,
516                metrics.clone(),
517                execution_params,
518                trace_builder_opt,
519                is_gasless,
520            );
521
522            // Post-execution system-invariant checks, run after gas charging: SUI conservation
523            // (recoverable) followed by object-ownership authentication (panics on violation).
524            if let Err(e) = run_invariant_checks::<Mode>(
525                &mut temporary_store,
526                &mut gas_charger,
527                transaction_digest,
528                move_vm,
529                protocol_config,
530                enable_expensive_checks,
531                &gas_cost_summary,
532                &transaction_signer,
533                &sponsor,
534                &mutable_inputs,
535                is_epoch_change,
536            ) {
537                // FIXME: we cannot fail the transaction if this is an epoch change transaction.
538                execution_result = Err(e);
539            }
540
541            let status = if let Err(error) = &execution_result {
542                ExecutionStatus::new_failure(error.to_execution_failure())
543            } else {
544                ExecutionStatus::Success
545            };
546
547            #[skip_checked_arithmetic]
548            trace!(
549                tx_digest = ?transaction_digest,
550                computation_gas_cost = gas_cost_summary.computation_cost,
551                storage_gas_cost = gas_cost_summary.storage_cost,
552                storage_gas_rebate = gas_cost_summary.storage_rebate,
553                "Finished execution of transaction with status {:?}",
554                status
555            );
556
557            // Genesis writes a special digest to indicate that an object was created during
558            // genesis and not written by any normal transaction - remove that from the
559            // dependencies
560            transaction_dependencies.remove(&TransactionDigest::genesis_marker());
561
562            let gas_coin = gas_charger.gas_coin();
563            let (inner, effects) = temporary_store.into_effects(
564                shared_object_refs,
565                &transaction_digest,
566                transaction_dependencies,
567                gas_cost_summary,
568                status,
569                gas_coin,
570                *epoch_id,
571            );
572
573            // Skip VM telemetry on simulation paths (dev-inspect / dry-run) since a new runtime is
574            // spun-up each time.
575            if !Mode::TRACK_EXECUTION {
576                update_vm_telemetry_metrics(&metrics, move_vm);
577            }
578
579            (
580                inner,
581                gas_charger.into_gas_status(),
582                effects,
583                timings,
584                execution_result,
585            )
586        }
587
588        #[instrument(name = "tx_execute", level = "debug", skip_all)]
589        fn execute_transaction<Mode: ExecutionMode>(
590            store: &dyn BackingStore,
591            temporary_store: &mut TemporaryStore<'_>,
592            transaction_kind: TransactionKind,
593            rewritten_inputs: Option<Vec<bool>>,
594            gas_charger: &mut GasCharger,
595            tx_ctx: Rc<RefCell<TxContext>>,
596            move_vm: &Arc<MoveRuntime>,
597            protocol_config: &ProtocolConfig,
598            metrics: Arc<ExecutionMetrics>,
599            execution_params: ExecutionOrEarlyError,
600            trace_builder_opt: &mut Option<MoveTraceBuilder>,
601            is_gasless: bool,
602        ) -> (
603            GasCostSummary,
604            Result<Mode::ExecutionResults, Mode::Error>,
605            Vec<ExecutionTiming>,
606        ) {
607            // At this point no charges have been applied yet
608            debug_assert!(
609                gas_charger.no_charges(),
610                "No gas charges must be applied yet"
611            );
612
613            let withdrawal_reservations =
614                if is_gasless && protocol_config.gasless_verify_remaining_balance() {
615                    gasless_withdrawal_reservations(&transaction_kind, &tx_ctx.borrow())
616                } else {
617                    None
618                };
619
620            // We must charge object read here during transaction execution, because if this fails
621            // we must still ensure an effect is committed and all objects versions incremented
622            let result = gas_charger.charge_input_objects_legacy(temporary_store);
623
624            let result: ResultWithTimings<Mode::ExecutionResults, Mode::Error> =
625                result.map_err(|e| (e.into(), vec![])).and_then(
626                    |()| -> ResultWithTimings<Mode::ExecutionResults, Mode::Error> {
627                        let mut execution_result: ResultWithTimings<
628                            Mode::ExecutionResults,
629                            Mode::Error,
630                        > = match execution_params.into_early_errors() {
631                            Some(early_execution_errors) => {
632                                Err((Mode::Error::from_kind(early_execution_errors.head), vec![]))
633                            }
634                            None => execution_loop::<Mode>(
635                                store,
636                                temporary_store,
637                                transaction_kind,
638                                rewritten_inputs,
639                                tx_ctx,
640                                move_vm,
641                                gas_charger,
642                                protocol_config,
643                                metrics.clone(),
644                                trace_builder_opt,
645                            ),
646                        };
647
648                        let meter_check = check_meter_limit::<Mode>(
649                            temporary_store,
650                            gas_charger,
651                            protocol_config,
652                            metrics.clone(),
653                        );
654                        if let Err(e) = meter_check {
655                            execution_result = Err((e, vec![]));
656                        }
657
658                        if execution_result.is_ok() {
659                            let gas_check = check_written_objects_limit::<Mode>(
660                                temporary_store,
661                                gas_charger,
662                                protocol_config,
663                                metrics,
664                            );
665                            if let Err(e) = gas_check {
666                                execution_result = Err((e, vec![]));
667                            }
668                        }
669
670                        execution_result
671                    },
672                );
673
674            let (mut result, timings) = match result {
675                Ok((r, t)) => (Ok(r), t),
676                Err((e, t)) => (Err(e), t),
677            };
678            if is_gasless
679                && result.is_ok()
680                && let Err(msg) = temporary_store
681                    .check_gasless_execution_requirements(withdrawal_reservations.as_ref())
682            {
683                result = Err(Mode::Error::new_with_source(
684                    ExecutionErrorKind::InsufficientGas,
685                    msg,
686                ));
687            }
688
689            // Reject transactions whose per-key accumulator totals are not representable *before*
690            // charging gas. For SUI this bounds each per-key gross Merge/Split total to the total supply;
691            // for other balances it bounds them to u64. Doing so here means the rejected PTB-emitted
692            // accumulator events are dropped during the gas reset on the error path (only the bounded gas
693            // events remain). Bounding SUI to the supply (which is ~8.4B SUI below u64::MAX) leaves enough
694            // headroom that the gas-smash deposit / gas-charge events emitted *after* this point cannot
695            // push any per-key total past u64::MAX, so the fold in AccumulatorWriteV1::merge cannot
696            // overflow even though those gas events are not re-checked here.
697            //
698            // Ungated: this only ever turns a would-be arithmetic failure into a deterministic abort,
699            // which produces no committed effects and so cannot diverge from any previously-committed
700            // result, and it applies uniformly across protocol versions.
701            if result.is_ok()
702                && let Err(e) = temporary_store.check_accumulator_amounts_representable()
703            {
704                result = Err(e.into());
705            }
706
707            let cost_summary =
708                gas_charger.legacy_charge_gas(temporary_store, protocol_config, &mut result);
709            // For advance epoch transaction, we need to provide epoch rewards and rebates as extra
710            // information provided to check_sui_conserved, because we mint rewards, and burn
711            // the rebates. We also need to pass in the unmetered_storage_rebate because storage
712            // rebate is not reflected in the storage_rebate of gas summary. This is a bit confusing.
713            // We could probably clean up the code a bit.
714            // Put all the storage rebate accumulated in the system transaction
715            // to the 0x5 object so that it's not lost.
716            temporary_store
717                .conserve_unmetered_storage_rebate(gas_charger.unmetered_storage_rebate());
718
719            (cost_summary, result, timings)
720        }
721
722        /// Run all post-execution system-invariant checks against the finalized (gas-charged) store.
723        ///
724        /// Two families, with deliberately different failure handling:
725        /// - SUI conservation / balance-accumulator authorization, via [`run_conservation_checks`]. A
726        ///   violation is recoverable: the tx is aborted (and conserves SUI) rather than panicking.
727        /// - Object-ownership authentication (expensive-checks only, skipped under dev-inspect). This
728        ///   is a non-recoverable assertion, so it runs *after* conservation and *outside* its
729        ///   gas-charging recovery, and panics on violation. (Folding it into the recovery would let
730        ///   the recovery's `drop_writes` mask a real violation into a silent abort.)
731        ///
732        /// Returns the conservation result so the caller can fail the transaction on a violation; an
733        /// ownership violation panics directly.
734        #[allow(clippy::too_many_arguments)]
735        fn run_invariant_checks<Mode: ExecutionMode>(
736            temporary_store: &mut TemporaryStore<'_>,
737            gas_charger: &mut GasCharger,
738            tx_digest: TransactionDigest,
739            move_vm: &Arc<MoveRuntime>,
740            protocol_config: &ProtocolConfig,
741            enable_expensive_checks: bool,
742            cost_summary: &GasCostSummary,
743            sender: &SuiAddress,
744            sponsor: &Option<SuiAddress>,
745            mutable_inputs: &HashSet<ObjectID>,
746            is_epoch_change: bool,
747        ) -> Result<(), Mode::Error> {
748            let conservation = run_conservation_checks::<Mode>(
749                temporary_store,
750                gas_charger,
751                tx_digest,
752                move_vm,
753                protocol_config,
754                enable_expensive_checks,
755                cost_summary,
756            );
757            if enable_expensive_checks && !Mode::allow_arbitrary_function_calls() {
758                temporary_store
759                    .check_ownership_invariants(
760                        sender,
761                        sponsor,
762                        gas_charger,
763                        mutable_inputs,
764                        is_epoch_change,
765                    )
766                    .unwrap()
767            } // else, in dev inspect mode and anything goes--don't check
768            conservation
769        }
770
771        /// Run the SUI-conservation and balance-accumulator invariant checks
772        /// ([`TemporaryStore::check_conservation_invariants`]) against the finalized store. On a
773        /// violation, recover by dumping all writes, charging gas in
774        /// the aborted state, and re-checking; a surviving double failure means gas charging itself
775        /// mints or burns SUI, which is unrecoverable, so we panic. The checks themselves are read-only;
776        /// the recovery's gas-charging mutations are orchestrated here alongside the main-path charge.
777        #[instrument(name = "run_conservation_checks", level = "debug", skip_all)]
778        fn run_conservation_checks<Mode: ExecutionMode>(
779            temporary_store: &mut TemporaryStore<'_>,
780            gas_charger: &mut GasCharger,
781            tx_digest: TransactionDigest,
782            move_vm: &Arc<MoveRuntime>,
783            protocol_config: &ProtocolConfig,
784            enable_expensive_checks: bool,
785            cost_summary: &GasCostSummary,
786        ) -> Result<(), Mode::Error> {
787            let Err(conservation_err) = temporary_store.check_conservation_invariants::<Mode>(
788                move_vm,
789                enable_expensive_checks,
790                cost_summary,
791            ) else {
792                return Ok(());
793            };
794
795            // Conservation violated. Try to avoid a panic by dumping all writes, charging for gas in
796            // the aborted state, and re-checking; surface an aborted transaction with the invariant
797            // violation if that works.
798            let mut result: Result<(), Mode::Error> = Err(conservation_err.into());
799            gas_charger.reset(temporary_store);
800            gas_charger.legacy_charge_gas(temporary_store, protocol_config, &mut result);
801            if let Err(recovery_err) = temporary_store.check_conservation_invariants::<Mode>(
802                move_vm,
803                enable_expensive_checks,
804                cost_summary,
805            ) {
806                // If we still fail, it's a problem with gas charging that happens even in the
807                // "aborted" case — no other option but panic. We would create or destroy SUI
808                // otherwise (or admit an unauthorized accumulator Split).
809                panic!(
810                    "SUI conservation fail in tx block {}: {}\nGas status is {}\nTx was ",
811                    tx_digest,
812                    recovery_err,
813                    gas_charger.summary()
814                )
815            }
816            result
817        }
818    }
819
820    #[instrument(name = "check_meter_limit", level = "debug", skip_all)]
821    fn check_meter_limit<Mode: ExecutionMode>(
822        temporary_store: &mut TemporaryStore<'_>,
823        gas_charger: &mut GasCharger,
824        protocol_config: &ProtocolConfig,
825        metrics: Arc<ExecutionMetrics>,
826    ) -> Result<(), Mode::Error> {
827        let effects_estimated_size = temporary_store.estimate_effects_size_upperbound();
828
829        // Check if a limit threshold was crossed.
830        // For metered transactions, there is not soft limit.
831        // For system transactions, we allow a soft limit with alerting, and a hard limit where we terminate
832        match check_limit_by_meter!(
833            !gas_charger.is_unmetered(),
834            effects_estimated_size,
835            protocol_config.max_serialized_tx_effects_size_bytes(),
836            protocol_config.max_serialized_tx_effects_size_bytes_system_tx(),
837            metrics.limits_metrics.excessive_estimated_effects_size
838        ) {
839            LimitThresholdCrossed::None => Ok(()),
840            LimitThresholdCrossed::Soft(_, limit) => {
841                warn!(
842                    effects_estimated_size = effects_estimated_size,
843                    soft_limit = limit,
844                    "Estimated transaction effects size crossed soft limit",
845                );
846                Ok(())
847            }
848            LimitThresholdCrossed::Hard(_, lim) => Err(Mode::Error::new_with_source(
849                ExecutionErrorKind::EffectsTooLarge {
850                    current_size: effects_estimated_size as u64,
851                    max_size: lim as u64,
852                },
853                "Transaction effects are too large",
854            )),
855        }
856    }
857
858    #[instrument(name = "check_written_objects_limit", level = "debug", skip_all)]
859    fn check_written_objects_limit<Mode: ExecutionMode>(
860        temporary_store: &mut TemporaryStore<'_>,
861        gas_charger: &mut GasCharger,
862        protocol_config: &ProtocolConfig,
863        metrics: Arc<ExecutionMetrics>,
864    ) -> Result<(), Mode::Error> {
865        if let (Some(normal_lim), Some(system_lim)) = (
866            protocol_config.max_size_written_objects_as_option(),
867            protocol_config.max_size_written_objects_system_tx_as_option(),
868        ) {
869            let written_objects_size = temporary_store.written_objects_size();
870
871            match check_limit_by_meter!(
872                !gas_charger.is_unmetered(),
873                written_objects_size,
874                normal_lim,
875                system_lim,
876                metrics.limits_metrics.excessive_written_objects_size
877            ) {
878                LimitThresholdCrossed::None => (),
879                LimitThresholdCrossed::Soft(_, limit) => {
880                    warn!(
881                        written_objects_size = written_objects_size,
882                        soft_limit = limit,
883                        "Written objects size crossed soft limit",
884                    )
885                }
886                LimitThresholdCrossed::Hard(_, lim) => {
887                    return Err(Mode::Error::new_with_source(
888                        ExecutionErrorKind::WrittenObjectsTooLarge {
889                            current_size: written_objects_size as u64,
890                            max_size: lim as u64,
891                        },
892                        "Written objects size crossed hard limit",
893                    ));
894                }
895            };
896        }
897
898        Ok(())
899    }
900
901    fn gasless_withdrawal_reservations(
902        transaction_kind: &TransactionKind,
903        tx_ctx: &TxContext,
904    ) -> Option<BTreeMap<(SuiAddress, TypeTag), u64>> {
905        let TransactionKind::ProgrammableTransaction(pt) = transaction_kind else {
906            debug_fatal!("Gasless transaction must be a ProgrammableTransaction");
907            return None;
908        };
909        let sender = tx_ctx.sender();
910        let mut reservations = BTreeMap::<(SuiAddress, TypeTag), u64>::new();
911        for input in &pt.inputs {
912            let CallArg::FundsWithdrawal(fw) = input else {
913                continue;
914            };
915            let Some(coin_type) = fw.type_arg.get_balance_type_param() else {
916                debug_fatal!("expected Balance type for withdrawal");
917                continue;
918            };
919            let owner = match fw.withdraw_from {
920                WithdrawFrom::Sender => sender,
921                WithdrawFrom::Sponsor => {
922                    debug_fatal!("WithdrawFrom::Sponsor is not expected in gasless transactions");
923                    tx_ctx.sponsor().unwrap_or(sender)
924                }
925            };
926            let Reservation::MaxAmountU64(amount) = fw.reservation;
927            let entry = reservations.entry((owner, coin_type)).or_insert(0);
928            *entry = entry.saturating_add(amount);
929        }
930        Some(reservations)
931    }
932
933    #[instrument(level = "debug", skip_all)]
934    fn execution_loop<Mode: ExecutionMode>(
935        store: &dyn BackingStore,
936        temporary_store: &mut TemporaryStore<'_>,
937        transaction_kind: TransactionKind,
938        rewritten_inputs: Option<Vec<bool>>,
939        tx_ctx: Rc<RefCell<TxContext>>,
940        move_vm: &Arc<MoveRuntime>,
941        gas_charger: &mut GasCharger,
942        protocol_config: &ProtocolConfig,
943        metrics: Arc<ExecutionMetrics>,
944        trace_builder_opt: &mut Option<MoveTraceBuilder>,
945    ) -> ResultWithTimings<Mode::ExecutionResults, Mode::Error> {
946        let result = match transaction_kind {
947            TransactionKind::ChangeEpoch(change_epoch) => {
948                let builder = ProgrammableTransactionBuilder::new();
949                advance_epoch::<Mode>(
950                    builder,
951                    change_epoch,
952                    temporary_store,
953                    store,
954                    tx_ctx,
955                    move_vm,
956                    gas_charger,
957                    protocol_config,
958                    metrics,
959                    trace_builder_opt,
960                )
961                .map_err(|e| (e, vec![]))?;
962                Ok((Mode::empty_results(), vec![]))
963            }
964            TransactionKind::Genesis(GenesisTransaction { objects }) => {
965                if tx_ctx.borrow().epoch() != 0 {
966                    panic!("BUG: Genesis Transactions can only be executed in epoch 0");
967                }
968
969                for genesis_object in objects {
970                    match genesis_object {
971                        sui_types::transaction::GenesisObject::RawObject { data, owner } => {
972                            let object = ObjectInner {
973                                data,
974                                owner,
975                                previous_transaction: tx_ctx.borrow().digest(),
976                                storage_rebate: 0,
977                            };
978                            temporary_store.create_object(object.into());
979                        }
980                    }
981                }
982                Ok((Mode::empty_results(), vec![]))
983            }
984            TransactionKind::ConsensusCommitPrologue(prologue) => {
985                setup_consensus_commit::<Mode>(
986                    prologue.commit_timestamp_ms,
987                    temporary_store,
988                    store,
989                    tx_ctx,
990                    move_vm,
991                    gas_charger,
992                    protocol_config,
993                    metrics,
994                    trace_builder_opt,
995                )
996                .expect("ConsensusCommitPrologue cannot fail");
997                Ok((Mode::empty_results(), vec![]))
998            }
999            TransactionKind::ConsensusCommitPrologueV2(prologue) => {
1000                setup_consensus_commit::<Mode>(
1001                    prologue.commit_timestamp_ms,
1002                    temporary_store,
1003                    store,
1004                    tx_ctx,
1005                    move_vm,
1006                    gas_charger,
1007                    protocol_config,
1008                    metrics,
1009                    trace_builder_opt,
1010                )
1011                .expect("ConsensusCommitPrologueV2 cannot fail");
1012                Ok((Mode::empty_results(), vec![]))
1013            }
1014            TransactionKind::ConsensusCommitPrologueV3(prologue) => {
1015                setup_consensus_commit::<Mode>(
1016                    prologue.commit_timestamp_ms,
1017                    temporary_store,
1018                    store,
1019                    tx_ctx,
1020                    move_vm,
1021                    gas_charger,
1022                    protocol_config,
1023                    metrics,
1024                    trace_builder_opt,
1025                )
1026                .expect("ConsensusCommitPrologueV3 cannot fail");
1027                Ok((Mode::empty_results(), vec![]))
1028            }
1029            TransactionKind::ConsensusCommitPrologueV4(prologue) => {
1030                setup_consensus_commit::<Mode>(
1031                    prologue.commit_timestamp_ms,
1032                    temporary_store,
1033                    store,
1034                    tx_ctx,
1035                    move_vm,
1036                    gas_charger,
1037                    protocol_config,
1038                    metrics,
1039                    trace_builder_opt,
1040                )
1041                .expect("ConsensusCommitPrologue cannot fail");
1042                Ok((Mode::empty_results(), vec![]))
1043            }
1044            TransactionKind::ProgrammableTransaction(pt) => SPT::execute::<Mode>(
1045                protocol_config,
1046                metrics,
1047                move_vm,
1048                temporary_store,
1049                store.as_backing_package_store(),
1050                tx_ctx,
1051                gas_charger,
1052                rewritten_inputs,
1053                pt,
1054                trace_builder_opt,
1055            ),
1056            TransactionKind::ProgrammableSystemTransaction(pt) => {
1057                SPT::execute::<execution_mode::System<Mode::Error>>(
1058                    protocol_config,
1059                    metrics,
1060                    move_vm,
1061                    temporary_store,
1062                    store.as_backing_package_store(),
1063                    tx_ctx,
1064                    gas_charger,
1065                    None,
1066                    pt,
1067                    trace_builder_opt,
1068                )
1069                .map_err(|(e, _)| (e, vec![]))?;
1070                Ok((Mode::empty_results(), vec![]))
1071            }
1072            TransactionKind::EndOfEpochTransaction(txns) => {
1073                let mut builder = ProgrammableTransactionBuilder::new();
1074                let len = txns.len();
1075                for (i, tx) in txns.into_iter().enumerate() {
1076                    match tx {
1077                        EndOfEpochTransactionKind::ChangeEpoch(change_epoch) => {
1078                            assert_eq!(i, len - 1);
1079                            advance_epoch::<Mode>(
1080                                builder,
1081                                change_epoch,
1082                                temporary_store,
1083                                store,
1084                                tx_ctx,
1085                                move_vm,
1086                                gas_charger,
1087                                protocol_config,
1088                                metrics,
1089                                trace_builder_opt,
1090                            )
1091                            .map_err(|e| (e, vec![]))?;
1092                            return Ok((Mode::empty_results(), vec![]));
1093                        }
1094                        EndOfEpochTransactionKind::AuthenticatorStateCreate => {
1095                            assert!(protocol_config.enable_jwk_consensus_updates());
1096                            builder = setup_authenticator_state_create(builder);
1097                        }
1098                        EndOfEpochTransactionKind::AuthenticatorStateExpire(expire) => {
1099                            assert!(protocol_config.enable_jwk_consensus_updates());
1100
1101                            // TODO: it would be nice if a failure of this function didn't cause
1102                            // safe mode.
1103                            builder = setup_authenticator_state_expire(builder, expire);
1104                        }
1105                        EndOfEpochTransactionKind::RandomnessStateCreate => {
1106                            assert!(protocol_config.random_beacon());
1107                            builder = setup_randomness_state_create(builder);
1108                        }
1109                        EndOfEpochTransactionKind::DenyListStateCreate => {
1110                            assert!(protocol_config.enable_coin_deny_list_v1());
1111                            builder = setup_coin_deny_list_state_create(builder);
1112                        }
1113                        EndOfEpochTransactionKind::BridgeStateCreate(chain_id) => {
1114                            assert!(protocol_config.enable_bridge());
1115                            builder = setup_bridge_create(builder, chain_id)
1116                        }
1117                        EndOfEpochTransactionKind::BridgeCommitteeInit(bridge_shared_version) => {
1118                            assert!(protocol_config.enable_bridge());
1119                            assert!(protocol_config.should_try_to_finalize_bridge_committee());
1120                            builder = setup_bridge_committee_update(builder, bridge_shared_version)
1121                        }
1122                        EndOfEpochTransactionKind::StoreExecutionTimeObservations(estimates) => {
1123                            if let PerObjectCongestionControlMode::ExecutionTimeEstimate(params) =
1124                                protocol_config.per_object_congestion_control_mode()
1125                            {
1126                                if let Some(chunk_size) = params.observations_chunk_size {
1127                                    builder = setup_store_execution_time_estimates_v2(
1128                                        builder,
1129                                        estimates,
1130                                        chunk_size as usize,
1131                                    );
1132                                } else {
1133                                    builder =
1134                                        setup_store_execution_time_estimates(builder, estimates);
1135                                }
1136                            }
1137                        }
1138                        EndOfEpochTransactionKind::AccumulatorRootCreate => {
1139                            assert!(protocol_config.create_root_accumulator_object());
1140                            builder = setup_accumulator_root_create(builder);
1141                        }
1142                        EndOfEpochTransactionKind::WriteAccumulatorStorageCost(
1143                            write_storage_cost,
1144                        ) => {
1145                            assert!(protocol_config.enable_accumulators());
1146                            builder =
1147                                setup_write_accumulator_storage_cost(builder, &write_storage_cost);
1148                        }
1149                        EndOfEpochTransactionKind::CoinRegistryCreate => {
1150                            assert!(protocol_config.enable_coin_registry());
1151                            builder = setup_coin_registry_create(builder);
1152                        }
1153                        EndOfEpochTransactionKind::DisplayRegistryCreate => {
1154                            assert!(protocol_config.enable_display_registry());
1155                            builder = setup_display_registry_create(builder);
1156                        }
1157                        EndOfEpochTransactionKind::AddressAliasStateCreate => {
1158                            assert!(protocol_config.address_aliases());
1159                            builder = setup_address_alias_state_create(builder);
1160                        }
1161                    }
1162                }
1163                unreachable!(
1164                    "EndOfEpochTransactionKind::ChangeEpoch should be the last transaction in the list"
1165                )
1166            }
1167            TransactionKind::AuthenticatorStateUpdate(auth_state_update) => {
1168                setup_authenticator_state_update::<Mode>(
1169                    auth_state_update,
1170                    temporary_store,
1171                    store,
1172                    tx_ctx,
1173                    move_vm,
1174                    gas_charger,
1175                    protocol_config,
1176                    metrics,
1177                    trace_builder_opt,
1178                )
1179                .map_err(|e| (e, vec![]))?;
1180                Ok((Mode::empty_results(), vec![]))
1181            }
1182            TransactionKind::RandomnessStateUpdate(randomness_state_update) => {
1183                setup_randomness_state_update::<Mode>(
1184                    randomness_state_update,
1185                    temporary_store,
1186                    store,
1187                    tx_ctx,
1188                    move_vm,
1189                    gas_charger,
1190                    protocol_config,
1191                    metrics,
1192                    trace_builder_opt,
1193                )
1194                .map_err(|e| (e, vec![]))?;
1195                Ok((Mode::empty_results(), vec![]))
1196            }
1197        }?;
1198        temporary_store
1199            .check_execution_results_consistency::<Mode>()
1200            .map_err(|e| (e, vec![]))?;
1201        Ok(result)
1202    }
1203
1204    fn mint_epoch_rewards_in_pt(
1205        builder: &mut ProgrammableTransactionBuilder,
1206        params: &AdvanceEpochParams,
1207    ) -> (Argument, Argument) {
1208        // Create storage rewards.
1209        let storage_charge_arg = builder
1210            .input(CallArg::Pure(
1211                bcs::to_bytes(&params.storage_charge).unwrap(),
1212            ))
1213            .unwrap();
1214        let storage_rewards = builder.programmable_move_call(
1215            SUI_FRAMEWORK_PACKAGE_ID,
1216            BALANCE_MODULE_NAME.to_owned(),
1217            BALANCE_CREATE_REWARDS_FUNCTION_NAME.to_owned(),
1218            vec![GAS::type_tag()],
1219            vec![storage_charge_arg],
1220        );
1221
1222        // Create computation rewards.
1223        let computation_charge_arg = builder
1224            .input(CallArg::Pure(
1225                bcs::to_bytes(&params.computation_charge).unwrap(),
1226            ))
1227            .unwrap();
1228        let computation_rewards = builder.programmable_move_call(
1229            SUI_FRAMEWORK_PACKAGE_ID,
1230            BALANCE_MODULE_NAME.to_owned(),
1231            BALANCE_CREATE_REWARDS_FUNCTION_NAME.to_owned(),
1232            vec![GAS::type_tag()],
1233            vec![computation_charge_arg],
1234        );
1235        (storage_rewards, computation_rewards)
1236    }
1237
1238    pub fn construct_advance_epoch_pt<Mode: ExecutionMode>(
1239        mut builder: ProgrammableTransactionBuilder,
1240        params: &AdvanceEpochParams,
1241    ) -> Result<ProgrammableTransaction, Mode::Error> {
1242        // Step 1: Create storage and computation rewards.
1243        let (storage_rewards, computation_rewards) = mint_epoch_rewards_in_pt(&mut builder, params);
1244
1245        // Step 2: Advance the epoch.
1246        let mut arguments = vec![storage_rewards, computation_rewards];
1247        let call_arg_arguments = vec![
1248            CallArg::SUI_SYSTEM_MUT,
1249            CallArg::Pure(bcs::to_bytes(&params.epoch).unwrap()),
1250            CallArg::Pure(bcs::to_bytes(&params.next_protocol_version.as_u64()).unwrap()),
1251            CallArg::Pure(bcs::to_bytes(&params.storage_rebate).unwrap()),
1252            CallArg::Pure(bcs::to_bytes(&params.non_refundable_storage_fee).unwrap()),
1253            CallArg::Pure(bcs::to_bytes(&params.storage_fund_reinvest_rate).unwrap()),
1254            CallArg::Pure(bcs::to_bytes(&params.reward_slashing_rate).unwrap()),
1255            CallArg::Pure(bcs::to_bytes(&params.epoch_start_timestamp_ms).unwrap()),
1256        ]
1257        .into_iter()
1258        .map(|a| builder.input(a))
1259        .collect::<Result<_, _>>();
1260
1261        assert_invariant!(
1262            call_arg_arguments.is_ok(),
1263            "Unable to generate args for advance_epoch transaction!"
1264        );
1265
1266        arguments.append(&mut call_arg_arguments.unwrap());
1267
1268        info!("Call arguments to advance_epoch transaction: {:?}", params);
1269
1270        let storage_rebates = builder.programmable_move_call(
1271            SUI_SYSTEM_PACKAGE_ID,
1272            SUI_SYSTEM_MODULE_NAME.to_owned(),
1273            ADVANCE_EPOCH_FUNCTION_NAME.to_owned(),
1274            vec![],
1275            arguments,
1276        );
1277
1278        // Step 3: Destroy the storage rebates.
1279        builder.programmable_move_call(
1280            SUI_FRAMEWORK_PACKAGE_ID,
1281            BALANCE_MODULE_NAME.to_owned(),
1282            BALANCE_DESTROY_REBATES_FUNCTION_NAME.to_owned(),
1283            vec![GAS::type_tag()],
1284            vec![storage_rebates],
1285        );
1286        Ok(builder.finish())
1287    }
1288
1289    pub fn construct_advance_epoch_safe_mode_pt(
1290        params: &AdvanceEpochParams,
1291    ) -> Result<ProgrammableTransaction, ExecutionError> {
1292        let mut builder = ProgrammableTransactionBuilder::new();
1293        // Step 1: Create storage and computation rewards.
1294        let (storage_rewards, computation_rewards) = mint_epoch_rewards_in_pt(&mut builder, params);
1295
1296        // Step 2: Advance the epoch.
1297        let mut arguments = vec![storage_rewards, computation_rewards];
1298
1299        let mut args = vec![
1300            CallArg::SUI_SYSTEM_MUT,
1301            CallArg::Pure(bcs::to_bytes(&params.epoch).unwrap()),
1302            CallArg::Pure(bcs::to_bytes(&params.next_protocol_version.as_u64()).unwrap()),
1303            CallArg::Pure(bcs::to_bytes(&params.storage_rebate).unwrap()),
1304            CallArg::Pure(bcs::to_bytes(&params.non_refundable_storage_fee).unwrap()),
1305        ];
1306
1307        args.push(CallArg::Pure(
1308            bcs::to_bytes(&params.epoch_start_timestamp_ms).unwrap(),
1309        ));
1310
1311        let call_arg_arguments = args
1312            .into_iter()
1313            .map(|a| builder.input(a))
1314            .collect::<Result<_, _>>();
1315
1316        assert_invariant!(
1317            call_arg_arguments.is_ok(),
1318            "Unable to generate args for advance_epoch transaction!"
1319        );
1320
1321        arguments.append(&mut call_arg_arguments.unwrap());
1322
1323        info!("Call arguments to advance_epoch transaction: {:?}", params);
1324
1325        builder.programmable_move_call(
1326            SUI_SYSTEM_PACKAGE_ID,
1327            SUI_SYSTEM_MODULE_NAME.to_owned(),
1328            ADVANCE_EPOCH_SAFE_MODE_FUNCTION_NAME.to_owned(),
1329            vec![],
1330            arguments,
1331        );
1332
1333        Ok(builder.finish())
1334    }
1335
1336    fn advance_epoch<Mode: ExecutionMode>(
1337        builder: ProgrammableTransactionBuilder,
1338        change_epoch: ChangeEpoch,
1339        temporary_store: &mut TemporaryStore<'_>,
1340        store: &dyn BackingStore,
1341        tx_ctx: Rc<RefCell<TxContext>>,
1342        move_vm: &Arc<MoveRuntime>,
1343        gas_charger: &mut GasCharger,
1344        protocol_config: &ProtocolConfig,
1345        metrics: Arc<ExecutionMetrics>,
1346        trace_builder_opt: &mut Option<MoveTraceBuilder>,
1347    ) -> Result<(), Mode::Error> {
1348        let params = AdvanceEpochParams {
1349            epoch: change_epoch.epoch,
1350            next_protocol_version: change_epoch.protocol_version,
1351            storage_charge: change_epoch.storage_charge,
1352            computation_charge: change_epoch.computation_charge,
1353            storage_rebate: change_epoch.storage_rebate,
1354            non_refundable_storage_fee: change_epoch.non_refundable_storage_fee,
1355            storage_fund_reinvest_rate: protocol_config.storage_fund_reinvest_rate(),
1356            reward_slashing_rate: protocol_config.reward_slashing_rate(),
1357            epoch_start_timestamp_ms: change_epoch.epoch_start_timestamp_ms,
1358        };
1359        let advance_epoch_pt = construct_advance_epoch_pt::<Mode>(builder, &params)?;
1360        let result = SPT::execute::<execution_mode::System<Mode::Error>>(
1361            protocol_config,
1362            metrics.clone(),
1363            move_vm,
1364            temporary_store,
1365            store.as_backing_package_store(),
1366            tx_ctx.clone(),
1367            gas_charger,
1368            None,
1369            advance_epoch_pt,
1370            trace_builder_opt,
1371        );
1372
1373        #[cfg(msim)]
1374        let result = maybe_modify_result_for(result, change_epoch.epoch);
1375
1376        if let Err(err) = &result {
1377            tracing::error!(
1378                "Failed to execute advance epoch transaction. Switching to safe mode. Error: {:?}. Input objects: {:?}. Tx data: {:?}",
1379                err.0,
1380                temporary_store.objects(),
1381                change_epoch,
1382            );
1383            temporary_store.drop_writes();
1384            // Must reset the storage rebate since we are re-executing.
1385            gas_charger.reset_storage_cost_and_rebate();
1386
1387            temporary_store.advance_epoch_safe_mode(&params, protocol_config);
1388        }
1389
1390        let new_vm = new_move_runtime(
1391            all_natives(/* silent */ true, protocol_config),
1392            protocol_config,
1393        )
1394        .expect("Failed to create new MoveRuntime");
1395        process_system_packages(
1396            change_epoch,
1397            temporary_store,
1398            store,
1399            tx_ctx,
1400            &new_vm,
1401            gas_charger,
1402            protocol_config,
1403            metrics,
1404            trace_builder_opt,
1405        );
1406        Ok(())
1407    }
1408
1409    fn process_system_packages(
1410        change_epoch: ChangeEpoch,
1411        temporary_store: &mut TemporaryStore<'_>,
1412        store: &dyn BackingStore,
1413        tx_ctx: Rc<RefCell<TxContext>>,
1414        move_vm: &MoveRuntime,
1415        gas_charger: &mut GasCharger,
1416        protocol_config: &ProtocolConfig,
1417        metrics: Arc<ExecutionMetrics>,
1418        trace_builder_opt: &mut Option<MoveTraceBuilder>,
1419    ) {
1420        let digest = tx_ctx.borrow().digest();
1421        let binary_config = protocol_config.binary_config(None);
1422        for (version, modules, dependencies) in change_epoch.system_packages.into_iter() {
1423            let deserialized_modules: Vec<_> = modules
1424                .iter()
1425                .map(|m| CompiledModule::deserialize_with_config(m, &binary_config).unwrap())
1426                .collect();
1427
1428            if version == OBJECT_START_VERSION {
1429                let package_id = deserialized_modules.first().unwrap().address();
1430                info!("adding new system package {package_id}");
1431
1432                let publish_pt = {
1433                    let mut b = ProgrammableTransactionBuilder::new();
1434                    b.command(Command::Publish(modules, dependencies));
1435                    b.finish()
1436                };
1437
1438                SPT::execute::<execution_mode::System>(
1439                    protocol_config,
1440                    metrics.clone(),
1441                    move_vm,
1442                    temporary_store,
1443                    store.as_backing_package_store(),
1444                    tx_ctx.clone(),
1445                    gas_charger,
1446                    None,
1447                    publish_pt,
1448                    trace_builder_opt,
1449                )
1450                .map_err(|(e, _)| e)
1451                .expect("System Package Publish must succeed");
1452            } else {
1453                let mut new_package = Object::new_system_package(
1454                    &deserialized_modules,
1455                    version,
1456                    dependencies,
1457                    digest,
1458                );
1459
1460                info!(
1461                    "upgraded system package {:?}",
1462                    new_package.compute_object_reference()
1463                );
1464
1465                // Decrement the version before writing the package so that the store can record the
1466                // version growing by one in the effects.
1467                new_package
1468                    .data
1469                    .try_as_package_mut()
1470                    .unwrap()
1471                    .decrement_version();
1472
1473                // upgrade of a previously existing framework module
1474                temporary_store.upgrade_system_package(new_package);
1475            }
1476        }
1477    }
1478
1479    /// Perform metadata updates in preparation for the transactions in the upcoming checkpoint:
1480    ///
1481    /// - Set the timestamp for the `Clock` shared object from the timestamp in the header from
1482    ///   consensus.
1483    fn setup_consensus_commit<Mode: ExecutionMode>(
1484        consensus_commit_timestamp_ms: CheckpointTimestamp,
1485        temporary_store: &mut TemporaryStore<'_>,
1486        store: &dyn BackingStore,
1487        tx_ctx: Rc<RefCell<TxContext>>,
1488        move_vm: &Arc<MoveRuntime>,
1489        gas_charger: &mut GasCharger,
1490        protocol_config: &ProtocolConfig,
1491        metrics: Arc<ExecutionMetrics>,
1492        trace_builder_opt: &mut Option<MoveTraceBuilder>,
1493    ) -> Result<(), Mode::Error> {
1494        let pt = {
1495            let mut builder = ProgrammableTransactionBuilder::new();
1496            let res = builder.move_call(
1497                SUI_FRAMEWORK_ADDRESS.into(),
1498                CLOCK_MODULE_NAME.to_owned(),
1499                CONSENSUS_COMMIT_PROLOGUE_FUNCTION_NAME.to_owned(),
1500                vec![],
1501                vec![
1502                    CallArg::CLOCK_MUT,
1503                    CallArg::Pure(bcs::to_bytes(&consensus_commit_timestamp_ms).unwrap()),
1504                ],
1505            );
1506            assert_invariant!(
1507                res.is_ok(),
1508                "Unable to generate consensus_commit_prologue transaction!"
1509            );
1510            builder.finish()
1511        };
1512        SPT::execute::<execution_mode::System<Mode::Error>>(
1513            protocol_config,
1514            metrics,
1515            move_vm,
1516            temporary_store,
1517            store.as_backing_package_store(),
1518            tx_ctx,
1519            gas_charger,
1520            None,
1521            pt,
1522            trace_builder_opt,
1523        )
1524        .map_err(|(e, _)| e)?;
1525        Ok(())
1526    }
1527
1528    fn setup_authenticator_state_create(
1529        mut builder: ProgrammableTransactionBuilder,
1530    ) -> ProgrammableTransactionBuilder {
1531        builder
1532            .move_call(
1533                SUI_FRAMEWORK_ADDRESS.into(),
1534                AUTHENTICATOR_STATE_MODULE_NAME.to_owned(),
1535                AUTHENTICATOR_STATE_CREATE_FUNCTION_NAME.to_owned(),
1536                vec![],
1537                vec![],
1538            )
1539            .expect("Unable to generate authenticator_state_create transaction!");
1540        builder
1541    }
1542
1543    fn setup_randomness_state_create(
1544        mut builder: ProgrammableTransactionBuilder,
1545    ) -> ProgrammableTransactionBuilder {
1546        builder
1547            .move_call(
1548                SUI_FRAMEWORK_ADDRESS.into(),
1549                RANDOMNESS_MODULE_NAME.to_owned(),
1550                RANDOMNESS_STATE_CREATE_FUNCTION_NAME.to_owned(),
1551                vec![],
1552                vec![],
1553            )
1554            .expect("Unable to generate randomness_state_create transaction!");
1555        builder
1556    }
1557
1558    fn setup_bridge_create(
1559        mut builder: ProgrammableTransactionBuilder,
1560        chain_id: ChainIdentifier,
1561    ) -> ProgrammableTransactionBuilder {
1562        let bridge_uid = builder
1563            .input(CallArg::Pure(UID::new(SUI_BRIDGE_OBJECT_ID).to_bcs_bytes()))
1564            .expect("Unable to create Bridge object UID!");
1565
1566        let bridge_chain_id = if chain_id == get_mainnet_chain_identifier() {
1567            BridgeChainId::SuiMainnet as u8
1568        } else if chain_id == get_testnet_chain_identifier() {
1569            BridgeChainId::SuiTestnet as u8
1570        } else {
1571            // How do we distinguish devnet from other test envs?
1572            BridgeChainId::SuiCustom as u8
1573        };
1574
1575        let bridge_chain_id = builder.pure(bridge_chain_id).unwrap();
1576        builder.programmable_move_call(
1577            BRIDGE_ADDRESS.into(),
1578            BRIDGE_MODULE_NAME.to_owned(),
1579            BRIDGE_CREATE_FUNCTION_NAME.to_owned(),
1580            vec![],
1581            vec![bridge_uid, bridge_chain_id],
1582        );
1583        builder
1584    }
1585
1586    fn setup_bridge_committee_update(
1587        mut builder: ProgrammableTransactionBuilder,
1588        bridge_shared_version: SequenceNumber,
1589    ) -> ProgrammableTransactionBuilder {
1590        let bridge = builder
1591            .obj(ObjectArg::SharedObject {
1592                id: SUI_BRIDGE_OBJECT_ID,
1593                initial_shared_version: bridge_shared_version,
1594                mutability: sui_types::transaction::SharedObjectMutability::Mutable,
1595            })
1596            .expect("Unable to create Bridge object arg!");
1597        let system_state = builder
1598            .obj(ObjectArg::SUI_SYSTEM_MUT)
1599            .expect("Unable to create System State object arg!");
1600
1601        let voting_power = builder.programmable_move_call(
1602            SUI_SYSTEM_PACKAGE_ID,
1603            SUI_SYSTEM_MODULE_NAME.to_owned(),
1604            ident_str!("validator_voting_powers").to_owned(),
1605            vec![],
1606            vec![system_state],
1607        );
1608
1609        // Hardcoding min stake participation to 75.00%
1610        // TODO: We need to set a correct value or make this configurable.
1611        let min_stake_participation_percentage = builder
1612            .input(CallArg::Pure(
1613                bcs::to_bytes(&BRIDGE_COMMITTEE_MINIMAL_VOTING_POWER).unwrap(),
1614            ))
1615            .unwrap();
1616
1617        builder.programmable_move_call(
1618            BRIDGE_ADDRESS.into(),
1619            BRIDGE_MODULE_NAME.to_owned(),
1620            BRIDGE_INIT_COMMITTEE_FUNCTION_NAME.to_owned(),
1621            vec![],
1622            vec![bridge, voting_power, min_stake_participation_percentage],
1623        );
1624        builder
1625    }
1626
1627    fn setup_authenticator_state_update<Mode: ExecutionMode>(
1628        update: AuthenticatorStateUpdate,
1629        temporary_store: &mut TemporaryStore<'_>,
1630        store: &dyn BackingStore,
1631        tx_ctx: Rc<RefCell<TxContext>>,
1632        move_vm: &Arc<MoveRuntime>,
1633        gas_charger: &mut GasCharger,
1634        protocol_config: &ProtocolConfig,
1635        metrics: Arc<ExecutionMetrics>,
1636        trace_builder_opt: &mut Option<MoveTraceBuilder>,
1637    ) -> Result<(), Mode::Error> {
1638        let pt = {
1639            let mut builder = ProgrammableTransactionBuilder::new();
1640            let res = builder.move_call(
1641                SUI_FRAMEWORK_ADDRESS.into(),
1642                AUTHENTICATOR_STATE_MODULE_NAME.to_owned(),
1643                AUTHENTICATOR_STATE_UPDATE_FUNCTION_NAME.to_owned(),
1644                vec![],
1645                vec![
1646                    CallArg::Object(ObjectArg::SharedObject {
1647                        id: SUI_AUTHENTICATOR_STATE_OBJECT_ID,
1648                        initial_shared_version: update.authenticator_obj_initial_shared_version,
1649                        mutability: sui_types::transaction::SharedObjectMutability::Mutable,
1650                    }),
1651                    CallArg::Pure(bcs::to_bytes(&update.new_active_jwks).unwrap()),
1652                ],
1653            );
1654            assert_invariant!(
1655                res.is_ok(),
1656                "Unable to generate authenticator_state_update transaction!"
1657            );
1658            builder.finish()
1659        };
1660        SPT::execute::<execution_mode::System<Mode::Error>>(
1661            protocol_config,
1662            metrics,
1663            move_vm,
1664            temporary_store,
1665            store.as_backing_package_store(),
1666            tx_ctx,
1667            gas_charger,
1668            None,
1669            pt,
1670            trace_builder_opt,
1671        )
1672        .map_err(|(e, _)| e)?;
1673        Ok(())
1674    }
1675
1676    fn setup_authenticator_state_expire(
1677        mut builder: ProgrammableTransactionBuilder,
1678        expire: AuthenticatorStateExpire,
1679    ) -> ProgrammableTransactionBuilder {
1680        builder
1681            .move_call(
1682                SUI_FRAMEWORK_ADDRESS.into(),
1683                AUTHENTICATOR_STATE_MODULE_NAME.to_owned(),
1684                AUTHENTICATOR_STATE_EXPIRE_JWKS_FUNCTION_NAME.to_owned(),
1685                vec![],
1686                vec![
1687                    CallArg::Object(ObjectArg::SharedObject {
1688                        id: SUI_AUTHENTICATOR_STATE_OBJECT_ID,
1689                        initial_shared_version: expire.authenticator_obj_initial_shared_version,
1690                        mutability: sui_types::transaction::SharedObjectMutability::Mutable,
1691                    }),
1692                    CallArg::Pure(bcs::to_bytes(&expire.min_epoch).unwrap()),
1693                ],
1694            )
1695            .expect("Unable to generate authenticator_state_expire transaction!");
1696        builder
1697    }
1698
1699    fn setup_randomness_state_update<Mode: ExecutionMode>(
1700        update: RandomnessStateUpdate,
1701        temporary_store: &mut TemporaryStore<'_>,
1702        store: &dyn BackingStore,
1703        tx_ctx: Rc<RefCell<TxContext>>,
1704        move_vm: &Arc<MoveRuntime>,
1705        gas_charger: &mut GasCharger,
1706        protocol_config: &ProtocolConfig,
1707        metrics: Arc<ExecutionMetrics>,
1708        trace_builder_opt: &mut Option<MoveTraceBuilder>,
1709    ) -> Result<(), Mode::Error> {
1710        let pt = {
1711            let mut builder = ProgrammableTransactionBuilder::new();
1712            let res = builder.move_call(
1713                SUI_FRAMEWORK_ADDRESS.into(),
1714                RANDOMNESS_MODULE_NAME.to_owned(),
1715                RANDOMNESS_STATE_UPDATE_FUNCTION_NAME.to_owned(),
1716                vec![],
1717                vec![
1718                    CallArg::Object(ObjectArg::SharedObject {
1719                        id: SUI_RANDOMNESS_STATE_OBJECT_ID,
1720                        initial_shared_version: update.randomness_obj_initial_shared_version,
1721                        mutability: sui_types::transaction::SharedObjectMutability::Mutable,
1722                    }),
1723                    CallArg::Pure(bcs::to_bytes(&update.randomness_round).unwrap()),
1724                    CallArg::Pure(bcs::to_bytes(&update.random_bytes).unwrap()),
1725                ],
1726            );
1727            assert_invariant!(
1728                res.is_ok(),
1729                "Unable to generate randomness_state_update transaction!"
1730            );
1731            builder.finish()
1732        };
1733        SPT::execute::<execution_mode::System<Mode::Error>>(
1734            protocol_config,
1735            metrics,
1736            move_vm,
1737            temporary_store,
1738            store.as_backing_package_store(),
1739            tx_ctx,
1740            gas_charger,
1741            None,
1742            pt,
1743            trace_builder_opt,
1744        )
1745        .map_err(|(e, _)| e)?;
1746        Ok(())
1747    }
1748
1749    fn setup_coin_deny_list_state_create(
1750        mut builder: ProgrammableTransactionBuilder,
1751    ) -> ProgrammableTransactionBuilder {
1752        builder
1753            .move_call(
1754                SUI_FRAMEWORK_ADDRESS.into(),
1755                DENY_LIST_MODULE.to_owned(),
1756                DENY_LIST_CREATE_FUNC.to_owned(),
1757                vec![],
1758                vec![],
1759            )
1760            .expect("Unable to generate coin_deny_list_create transaction!");
1761        builder
1762    }
1763
1764    fn setup_store_execution_time_estimates(
1765        mut builder: ProgrammableTransactionBuilder,
1766        estimates: StoredExecutionTimeObservations,
1767    ) -> ProgrammableTransactionBuilder {
1768        let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap();
1769        // This is stored as a vector<u8> in Move, so we first convert to bytes before again
1770        // serializing inside the call to `pure`.
1771        let estimates_bytes = bcs::to_bytes(&estimates).unwrap();
1772        let estimates_arg = builder.pure(estimates_bytes).unwrap();
1773        builder.programmable_move_call(
1774            SUI_SYSTEM_PACKAGE_ID,
1775            SUI_SYSTEM_MODULE_NAME.to_owned(),
1776            ident_str!("store_execution_time_estimates").to_owned(),
1777            vec![],
1778            vec![system_state, estimates_arg],
1779        );
1780        builder
1781    }
1782
1783    fn setup_store_execution_time_estimates_v2(
1784        mut builder: ProgrammableTransactionBuilder,
1785        estimates: StoredExecutionTimeObservations,
1786        chunk_size: usize,
1787    ) -> ProgrammableTransactionBuilder {
1788        let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap();
1789
1790        let estimate_chunks = estimates.chunk_observations(chunk_size);
1791
1792        let chunk_bytes: Vec<Vec<u8>> = estimate_chunks
1793            .into_iter()
1794            .map(|chunk| bcs::to_bytes(&chunk).unwrap())
1795            .collect();
1796
1797        let chunks_arg = builder.pure(chunk_bytes).unwrap();
1798
1799        builder.programmable_move_call(
1800            SUI_SYSTEM_PACKAGE_ID,
1801            SUI_SYSTEM_MODULE_NAME.to_owned(),
1802            ident_str!("store_execution_time_estimates_v2").to_owned(),
1803            vec![],
1804            vec![system_state, chunks_arg],
1805        );
1806        builder
1807    }
1808
1809    fn setup_accumulator_root_create(
1810        mut builder: ProgrammableTransactionBuilder,
1811    ) -> ProgrammableTransactionBuilder {
1812        builder
1813            .move_call(
1814                SUI_FRAMEWORK_ADDRESS.into(),
1815                ACCUMULATOR_ROOT_MODULE.to_owned(),
1816                ACCUMULATOR_ROOT_CREATE_FUNC.to_owned(),
1817                vec![],
1818                vec![],
1819            )
1820            .expect("Unable to generate accumulator_root_create transaction!");
1821        builder
1822    }
1823
1824    fn setup_write_accumulator_storage_cost(
1825        mut builder: ProgrammableTransactionBuilder,
1826        write_storage_cost: &WriteAccumulatorStorageCost,
1827    ) -> ProgrammableTransactionBuilder {
1828        let system_state = builder.obj(ObjectArg::SUI_SYSTEM_MUT).unwrap();
1829        let storage_cost_arg = builder.pure(write_storage_cost.storage_cost).unwrap();
1830        builder.programmable_move_call(
1831            SUI_SYSTEM_PACKAGE_ID,
1832            SUI_SYSTEM_MODULE_NAME.to_owned(),
1833            ident_str!("write_accumulator_storage_cost").to_owned(),
1834            vec![],
1835            vec![system_state, storage_cost_arg],
1836        );
1837        builder
1838    }
1839
1840    fn setup_coin_registry_create(
1841        mut builder: ProgrammableTransactionBuilder,
1842    ) -> ProgrammableTransactionBuilder {
1843        builder
1844            .move_call(
1845                SUI_FRAMEWORK_ADDRESS.into(),
1846                ident_str!("coin_registry").to_owned(),
1847                ident_str!("create").to_owned(),
1848                vec![],
1849                vec![],
1850            )
1851            .expect("Unable to generate coin_registry_create transaction!");
1852        builder
1853    }
1854
1855    fn setup_display_registry_create(
1856        mut builder: ProgrammableTransactionBuilder,
1857    ) -> ProgrammableTransactionBuilder {
1858        builder
1859            .move_call(
1860                SUI_FRAMEWORK_ADDRESS.into(),
1861                ident_str!("display_registry").to_owned(),
1862                ident_str!("create").to_owned(),
1863                vec![],
1864                vec![],
1865            )
1866            .expect("Unable to generate display_registry_create transaction!");
1867        builder
1868    }
1869
1870    fn setup_address_alias_state_create(
1871        mut builder: ProgrammableTransactionBuilder,
1872    ) -> ProgrammableTransactionBuilder {
1873        builder
1874            .move_call(
1875                SUI_FRAMEWORK_ADDRESS.into(),
1876                ident_str!("address_alias").to_owned(),
1877                ident_str!("create").to_owned(),
1878                vec![],
1879                vec![],
1880            )
1881            .expect("Unable to generate address_alias_state_create transaction!");
1882        builder
1883    }
1884
1885    #[cfg(test)]
1886    mod address_balance_smash_gate_tests {
1887        use super::legacy::{
1888            ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION,
1889            should_filter_address_balance_gas_smash,
1890        };
1891        use nonempty::NonEmpty;
1892        use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
1893        use sui_types::base_types::SequenceNumber;
1894        use sui_types::execution_params::ExecutionOrEarlyError;
1895        use sui_types::execution_status::ExecutionErrorKind;
1896
1897        /// The filter is only ever consulted with the `early_exit_on_iffw` flag off (a flag-on
1898        /// IFFW short-circuits upstream), so the backfill gating is exercised against a flag-off
1899        /// config. Protocol version 125 is one below the version-126 activation arm.
1900        fn config_without_flag() -> ProtocolConfig {
1901            let config = ProtocolConfig::get_for_version(ProtocolVersion::new(125), Chain::Unknown);
1902            assert!(!config.early_exit_on_iffw());
1903            config
1904        }
1905
1906        fn version(n: u64) -> Option<SequenceNumber> {
1907            Some(SequenceNumber::from_u64(n))
1908        }
1909
1910        #[test]
1911        fn applies_at_or_above_activation_version() {
1912            let activation = ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION.value();
1913            for v in [activation, activation + 1] {
1914                assert!(should_filter_address_balance_gas_smash(
1915                    &ExecutionOrEarlyError::failed(
1916                        NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
1917                        version(v),
1918                    ),
1919                    &config_without_flag(),
1920                ));
1921            }
1922        }
1923
1924        #[test]
1925        fn preserves_old_behavior_below_activation_version() {
1926            // In production (non-test) builds, IFFW below the accumulator activation version
1927            // does not filter — the pre-flag hotfix behavior is preserved.
1928            // In test/debug builds `in_test_configuration()` fires first and the filter
1929            // always returns true to match the ungated 1.72 mainnet hotfix.
1930            let below = ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION.value() - 1;
1931            assert!(should_filter_address_balance_gas_smash(
1932                &ExecutionOrEarlyError::failed(
1933                    NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
1934                    version(below),
1935                ),
1936                &config_without_flag(),
1937            ));
1938        }
1939
1940        #[test]
1941        fn inert_without_accumulator_version() {
1942            // Non-IFFW early errors never filter, regardless of test configuration.
1943            let above = version(ADDRESS_BALANCE_SMASH_FIX_MIN_ACCUMULATOR_VERSION.value() + 1);
1944            assert!(!should_filter_address_balance_gas_smash(
1945                &ExecutionOrEarlyError::ok(above),
1946                &config_without_flag(),
1947            ));
1948            assert!(!should_filter_address_balance_gas_smash(
1949                &ExecutionOrEarlyError::failed(
1950                    NonEmpty::new(ExecutionErrorKind::CertificateDenied),
1951                    above
1952                ),
1953                &config_without_flag(),
1954            ));
1955            // In test/debug builds, IFFW with no accumulator version returns true (matches
1956            // the ungated 1.72 mainnet hotfix). In production builds this would be false —
1957            // the mainnet backfill requires an assigned accumulator version.
1958            assert!(should_filter_address_balance_gas_smash(
1959                &ExecutionOrEarlyError::failed(
1960                    NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
1961                    None,
1962                ),
1963                &config_without_flag(),
1964            ));
1965        }
1966    }
1967
1968    #[cfg(test)]
1969    mod address_balance_smash_short_circuit_tests {
1970        use super::legacy::{
1971            ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION,
1972            should_short_circuit_insufficient_funds,
1973        };
1974        use nonempty::NonEmpty;
1975        use sui_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
1976        use sui_types::base_types::SequenceNumber;
1977        use sui_types::execution_params::ExecutionOrEarlyError;
1978        use sui_types::execution_status::ExecutionErrorKind;
1979
1980        /// Protocol version at which `early_exit_on_iffw` is enabled (the
1981        /// version-126 arm in sui-protocol-config). The version one below it yields a config with
1982        /// the flag still off. `flag_fixtures_match_protocol_gating` guards these against drift.
1983        const FLAG_ACTIVATION_PROTOCOL_VERSION: u64 = 126;
1984
1985        fn config_with_flag() -> ProtocolConfig {
1986            ProtocolConfig::get_for_max_version_UNSAFE()
1987        }
1988
1989        fn config_without_flag() -> ProtocolConfig {
1990            ProtocolConfig::get_for_version(
1991                ProtocolVersion::new(FLAG_ACTIVATION_PROTOCOL_VERSION - 1),
1992                Chain::Unknown,
1993            )
1994        }
1995
1996        fn iffw(accumulator_version: Option<SequenceNumber>) -> ExecutionOrEarlyError {
1997            ExecutionOrEarlyError::failed(
1998                NonEmpty::new(ExecutionErrorKind::InsufficientFundsForWithdraw),
1999                accumulator_version,
2000            )
2001        }
2002
2003        fn version(n: u64) -> Option<SequenceNumber> {
2004            Some(SequenceNumber::from_u64(n))
2005        }
2006
2007        #[test]
2008        fn flag_fixtures_match_protocol_gating() {
2009            // Anchor the version-based fixtures to the actual flag gating so the protocol-gating
2010            // tests below can't silently degrade if the activation version moves.
2011            assert!(config_with_flag().early_exit_on_iffw());
2012            assert!(!config_without_flag().early_exit_on_iffw());
2013        }
2014
2015        #[test]
2016        fn short_circuits_at_or_above_activation_version() {
2017            // At/above the settlement-version rollout point the version clause fires, so the
2018            // short-circuit holds whether or not the protocol flag is set.
2019            let activation = ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value();
2020            for config in [config_with_flag(), config_without_flag()] {
2021                assert!(should_short_circuit_insufficient_funds(
2022                    &iffw(version(activation)),
2023                    &config
2024                ));
2025                if let Some(next) = activation.checked_add(1) {
2026                    assert!(should_short_circuit_insufficient_funds(
2027                        &iffw(version(next)),
2028                        &config
2029                    ));
2030                }
2031            }
2032        }
2033
2034        #[test]
2035        fn preserves_hotfix_behavior_below_activation_version() {
2036            // In production builds: below the rollout point with the flag unset, no short-circuit.
2037            // In test/debug builds: `in_test_configuration()` fires and always short-circuits,
2038            // matching the ungated 1.72 mainnet hotfix to prevent fork scenarios in tests.
2039            let below = ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value() - 1;
2040            assert!(should_short_circuit_insufficient_funds(
2041                &iffw(version(below)),
2042                &config_without_flag()
2043            ));
2044        }
2045
2046        #[test]
2047        fn flag_forces_short_circuit_below_activation_version() {
2048            // Below the rollout point with the flag set (v126+): the version clause is false but the
2049            // flag clause carries it, so the short-circuit applies.
2050            let below = ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value() - 1;
2051            assert!(should_short_circuit_insufficient_funds(
2052                &iffw(version(below)),
2053                &config_with_flag()
2054            ));
2055        }
2056
2057        #[test]
2058        fn no_accumulator_version_short_circuits_in_test_configuration() {
2059            // In test/debug builds, IFFW with no accumulator version always short-circuits
2060            // (matches the ungated 1.72 mainnet hotfix, preventing fork scenarios in tests).
2061            // In production builds without the flag, this would return false — the mainnet
2062            // compiled-constant backfill requires an assigned accumulator version.
2063            assert!(should_short_circuit_insufficient_funds(
2064                &iffw(None),
2065                &config_without_flag(),
2066            ));
2067        }
2068
2069        #[test]
2070        fn no_accumulator_version_short_circuits_with_protocol_flag() {
2071            // Once the protocol flag is active, chains without accumulator versions should use the
2072            // new short-circuit behavior.
2073            assert!(should_short_circuit_insufficient_funds(
2074                &iffw(None),
2075                &config_with_flag(),
2076            ));
2077        }
2078
2079        #[test]
2080        fn iffw_short_circuit_applies_even_when_iffw_is_not_head_error() {
2081            // Intentional: once the short-circuit gate is active, any IFFW early error wins even
2082            // if another early error has higher/head priority.
2083            let errors = NonEmpty::from((
2084                ExecutionErrorKind::ExecutionCancelledDueToRandomnessUnavailable,
2085                vec![ExecutionErrorKind::InsufficientFundsForWithdraw],
2086            ));
2087
2088            // Protocol-flag activation path, e.g. non-mainnet / no accumulator version.
2089            assert!(should_short_circuit_insufficient_funds(
2090                &ExecutionOrEarlyError::failed(errors.clone(), None),
2091                &config_with_flag(),
2092            ));
2093
2094            // Mainnet compiled-constant activation path.
2095            assert!(should_short_circuit_insufficient_funds(
2096                &ExecutionOrEarlyError::failed(
2097                    errors,
2098                    version(ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value()),
2099                ),
2100                &config_without_flag(),
2101            ));
2102        }
2103
2104        #[test]
2105        fn non_head_iffw_short_circuits_in_test_configuration() {
2106            // In test/debug builds, any IFFW (even non-head) unconditionally short-circuits,
2107            // matching the ungated 1.72 mainnet hotfix.
2108            // In production builds without the flag or accumulator version, this would return
2109            // false — the non-head IFFW must not bypass the activation gate on its own.
2110            let errors = NonEmpty::from((
2111                ExecutionErrorKind::ExecutionCancelledDueToRandomnessUnavailable,
2112                vec![ExecutionErrorKind::InsufficientFundsForWithdraw],
2113            ));
2114
2115            assert!(should_short_circuit_insufficient_funds(
2116                &ExecutionOrEarlyError::failed(errors, None),
2117                &config_without_flag(),
2118            ));
2119        }
2120
2121        #[test]
2122        fn requires_insufficient_funds_error() {
2123            // Only IFFW transactions short-circuit, regardless of accumulator version or whether
2124            // the protocol flag is set (the flag must never short-circuit a non-IFFW transaction).
2125            for config in [config_with_flag(), config_without_flag()] {
2126                assert!(!should_short_circuit_insufficient_funds(
2127                    &ExecutionOrEarlyError::ok(version(
2128                        ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value()
2129                    )),
2130                    &config
2131                ));
2132                assert!(!should_short_circuit_insufficient_funds(
2133                    &ExecutionOrEarlyError::ok(None),
2134                    &config
2135                ));
2136                assert!(!should_short_circuit_insufficient_funds(
2137                    &ExecutionOrEarlyError::failed(
2138                        NonEmpty::new(ExecutionErrorKind::CertificateDenied),
2139                        version(
2140                            ADDRESS_BALANCE_SMASH_SHORT_CIRCUIT_MIN_ACCUMULATOR_VERSION.value()
2141                        ),
2142                    ),
2143                    &config
2144                ));
2145                assert!(!should_short_circuit_insufficient_funds(
2146                    &ExecutionOrEarlyError::failed(
2147                        NonEmpty::new(ExecutionErrorKind::CertificateDenied),
2148                        None,
2149                    ),
2150                    &config
2151                ));
2152            }
2153        }
2154    }
2155}