Skip to main content

sui_adapter_latest/
gas_charger.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5pub use checked::*;
6
7#[sui_macros::with_checked_arithmetic]
8pub mod checked {
9
10    use crate::sui_types::gas::SuiGasStatusAPI;
11    use crate::temporary_store::TemporaryStore;
12    use either::Either;
13    use indexmap::IndexMap;
14    use mysten_common::assert_reachable;
15    use sui_protocol_config::ProtocolConfig;
16    use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
17    use sui_types::digests::TransactionDigest;
18    use sui_types::error::ExecutionErrorTrait;
19    use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas};
20    use sui_types::gas_model::gas_predicates::refresh_gas_payment_location;
21    use sui_types::{
22        accumulator_event::AccumulatorEvent,
23        base_types::{ObjectID, ObjectRef, SuiAddress},
24        error::ExecutionError,
25        gas_model::tables::GasStatus,
26        is_system_package,
27        object::Data,
28    };
29    use tracing::trace;
30
31    /// Encapsulates the gas metering state (`SuiGasStatus`) and the payment source metadata,
32    /// whether it is from a smashed list (coin objects or address-balance withdrawals) or
33    /// un-metered. In other words, this serves the point of interaction between the on-chain data
34    /// (coins and address balances) and the gas meter.
35    #[derive(Debug)]
36    pub struct GasCharger {
37        tx_digest: TransactionDigest,
38        gas_model_version: u64,
39        payment: PaymentMetadata,
40        gas_status: SuiGasStatus,
41    }
42
43    /// Internal representation of how a transaction's gas is being paid.
44    /// `Unmetered` for no payment (dev inspect and system transactions).
45    /// `Gasless` for metered-but-free transactions (gas is metered but not charged).
46    /// `Smash` when one or more user-provided payment methods have been combined into a single
47    /// source.
48    #[derive(Debug)]
49    enum PaymentMetadata {
50        Unmetered,
51        Gasless,
52        /// Contains the list of payments (coins and address balances) and additional metadata
53        Smash(SmashMetadata),
54    }
55
56    /// State produced by smashing multiple gas payment sources into one.
57    /// Tracks the combined balance (`total_smashed`), the target location where the
58    /// smashed value lives, and the original payment methods for bookkeeping.
59    /// Note that the target location (`gas_charge_location`) may differ from the first payment
60    /// method in the list if it has been overridden during execution.
61    #[derive(Debug)]
62    struct SmashMetadata {
63        /// The location to charge gas from at the end of execution. Starts with the primary
64        /// payment method but may be overridden.
65        gas_charge_location: PaymentLocation,
66        /// The total balance of all smashed payment methods.
67        total_smashed: u64,
68        /// The "primary" payment method that serves as the recipient of the `total_smashed`. Also,
69        /// provides the initial location of the `gas_charge_location` before any overrides.
70        smash_target: PaymentMethod,
71        /// The original payment methods to be smashed into the `smash_target`. It does not include
72        /// the `smash_target` itself. Keyed by location to guarantee uniqueness.
73        smashed_payments: IndexMap<PaymentLocation, PaymentMethod>,
74    }
75
76    /// Public wrapper that describes how gas will be paid before smashing occurs.
77    /// Constructed via `PaymentKind::unmetered()` or `PaymentKind::smash(methods)` and
78    /// consumed by `GasCharger::new`.
79    #[derive(Debug)]
80    pub struct PaymentKind(PaymentKind_);
81
82    /// Inner representation for `PaymentKind`. Kept private so construction is forced through
83    /// the validation in `PaymentKind::smash`.
84    #[derive(Debug)]
85    enum PaymentKind_ {
86        Unmetered,
87        Gasless,
88        /// A non-empty map of gas coins or address balance withdrawals, keyed by location.
89        /// The first entry is the smash target; all others are smashed into it.
90        Smash(IndexMap<PaymentLocation, PaymentMethod>),
91    }
92
93    /// A single source of SUI used to pay for gas: either a coin object or a withdrawal
94    /// reservation from an address balance.
95    #[derive(Debug)]
96    pub enum PaymentMethod {
97        Coin(ObjectRef),
98        AddressBalance(SuiAddress, /* withdrawal reservation */ u64),
99    }
100
101    /// Identifies where a gas payment lives, independent of its value (`ObjectRef` or reservation).
102    /// Used often as a key, e.g. during smashing and during gas final charging.
103    #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
104    pub enum PaymentLocation {
105        Coin(ObjectID),
106        AddressBalance(SuiAddress),
107    }
108
109    /// A resolved gas payment: the location that will receive the final charge or refund,
110    /// paired with the total SUI available after smashing. Produced by
111    /// `GasCharger::gas_payment_amount` and consumed by PTB execution to set up the
112    /// runtime gas coin.
113    #[derive(Debug, Clone, Copy)]
114    pub struct GasPayment {
115        /// The location of the gas payment (coin or address balance), which also serves as the
116        /// target for smashed gas payments.
117        pub location: PaymentLocation,
118        /// The total amount available for gas payment after smashing
119        pub amount: u64,
120    }
121
122    impl GasCharger {
123        pub fn new(
124            tx_digest: TransactionDigest,
125            payment_kind: PaymentKind,
126            gas_status: SuiGasStatus,
127            temporary_store: &mut TemporaryStore<'_>,
128            protocol_config: &ProtocolConfig,
129        ) -> Self {
130            let gas_model_version = protocol_config.gas_model_version();
131            let payment = match payment_kind.0 {
132                PaymentKind_::Unmetered => PaymentMetadata::Unmetered,
133                PaymentKind_::Gasless => PaymentMetadata::Gasless,
134                PaymentKind_::Smash(mut payment_methods) => {
135                    let (_, smash_target) = payment_methods.shift_remove_index(0).unwrap();
136                    let mut metadata = SmashMetadata {
137                        // dummy value set below in smash_gas
138                        total_smashed: 0,
139                        gas_charge_location: smash_target.location(),
140                        smash_target,
141                        smashed_payments: payment_methods,
142                    };
143                    metadata.smash_gas(&tx_digest, temporary_store);
144                    PaymentMetadata::Smash(metadata)
145                }
146            };
147            Self {
148                tx_digest,
149                gas_model_version,
150                payment,
151                gas_status,
152            }
153        }
154
155        pub fn new_unmetered(
156            tx_digest: TransactionDigest,
157            protocol_config: &ProtocolConfig,
158        ) -> Self {
159            Self {
160                tx_digest,
161                gas_model_version: protocol_config.gas_model_version(),
162                payment: PaymentMetadata::Unmetered,
163                gas_status: SuiGasStatus::new_unmetered(protocol_config),
164            }
165        }
166
167        // TODO: there is only one caller to this function that should not exist otherwise.
168        //       Explore way to remove it.
169        pub(crate) fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
170            match &self.payment {
171                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => {
172                    Either::Left(std::iter::empty())
173                }
174                PaymentMetadata::Smash(metadata) => Either::Right(metadata.used_coins()),
175            }
176        }
177
178        // Override the gas payment location for smashing
179        pub fn override_gas_charge_location(
180            &mut self,
181            location: PaymentLocation,
182        ) -> Result<(), ExecutionError> {
183            if let PaymentMetadata::Smash(metadata) = &mut self.payment {
184                metadata.gas_charge_location = location;
185                Ok(())
186            } else {
187                invariant_violation!("Can only override gas charge location in the smash-gas case")
188            }
189        }
190
191        /// Return the amount available at the given input payment location.
192        /// For unmetered, this is None.
193        /// For smashed gas payments, this is the payment location and the total amount smashed.
194        /// This information feels a bit brittle but should be used only by PTB execution.
195        /// This might also differ from the final charge location, if override_gas_charge_location
196        /// is used.
197        pub fn gas_payment_amount(&self) -> Option<GasPayment> {
198            match &self.payment {
199                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
200                PaymentMetadata::Smash(metadata) => Some(GasPayment {
201                    location: metadata.smash_target.location(),
202                    amount: metadata.total_smashed,
203                }),
204            }
205        }
206
207        /// The coin that receives the final gas charge, or `None` when gas is paid from an address
208        /// balance (or there is no payment, e.g. unmetered/gasless).
209        pub fn gas_coin(&self) -> Option<ObjectID> {
210            self.gas_payment_amount().and_then(|gp| match gp.location {
211                PaymentLocation::Coin(coin_id) => Some(coin_id),
212                PaymentLocation::AddressBalance(_) => None,
213            })
214        }
215
216        pub(crate) fn gas_payment_location(&self) -> Option<PaymentLocation> {
217            match &self.payment {
218                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => None,
219                PaymentMetadata::Smash(metadata) => Some(metadata.gas_charge_location),
220            }
221        }
222
223        pub fn gas_budget(&self) -> u64 {
224            self.gas_status.gas_budget()
225        }
226
227        pub fn unmetered_storage_rebate(&self) -> u64 {
228            self.gas_status.unmetered_storage_rebate()
229        }
230
231        pub fn no_charges(&self) -> bool {
232            self.gas_status.gas_used() == 0
233                && self.gas_status.storage_rebate() == 0
234                && self.gas_status.storage_gas_units() == 0
235        }
236
237        pub fn is_unmetered(&self) -> bool {
238            self.gas_status.is_unmetered()
239        }
240
241        pub fn set_computation_to_budget(&mut self) {
242            self.gas_status.adjust_computation_on_out_of_gas();
243        }
244
245        pub fn move_gas_status(&self) -> &GasStatus {
246            self.gas_status.move_gas_status()
247        }
248
249        pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
250            self.gas_status.move_gas_status_mut()
251        }
252
253        pub fn into_gas_status(self) -> SuiGasStatus {
254            self.gas_status
255        }
256
257        pub fn summary(&self) -> GasCostSummary {
258            self.gas_status.summary()
259        }
260
261        // This function is called when the transaction is about to be executed.
262        // It will smash all gas coins into a single one and set the logical gas coin
263        // to be the first one in the list.
264        // After this call, `gas_coin` will return it id of the gas coin.
265        // This function panics if errors are found while operation on the gas coins.
266        // Transaction and certificate input checks must have insured that all gas coins
267        // are correct.
268        fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
269            match &mut self.payment {
270                PaymentMetadata::Unmetered | PaymentMetadata::Gasless => (),
271                PaymentMetadata::Smash(smash_metadata) => {
272                    smash_metadata.smash_gas(&self.tx_digest, temporary_store);
273                }
274            }
275        }
276
277        //
278        // Gas charging operations
279        //
280
281        pub fn track_storage_mutation(
282            &mut self,
283            object_id: ObjectID,
284            new_size: usize,
285            storage_rebate: u64,
286        ) -> Option<u64> {
287            self.gas_status
288                .track_storage_mutation(object_id, new_size, storage_rebate)
289        }
290
291        pub fn reset_storage_cost_and_rebate(&mut self) {
292            self.gas_status.reset_storage_cost_and_rebate();
293        }
294
295        pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
296            self.gas_status.charge_publish_package(size)
297        }
298
299        /// Charge `storage_read` for each input object (system packages excepted).
300        pub fn charge_input_objects(
301            &mut self,
302            temporary_store: &TemporaryStore<'_>,
303        ) -> Result<(), ExecutionError> {
304            temporary_store
305                .objects()
306                .iter()
307                // don't charge for loading Sui Framework or Move stdlib
308                .filter(|(id, _)| !is_system_package(**id))
309                .try_for_each(|(_, obj)| {
310                    let size = obj.object_size_for_gas_metering();
311                    if obj.is_package() {
312                        // This covers every package input: packages named by Move calls, publish
313                        // dependencies, and the package and dependencies of an upgrade. The bytes
314                        // of newly published code are charged separately by `charge_publish_package`.
315                        self.gas_status.charge_package_object_read(size)
316                    } else {
317                        self.gas_status.charge_storage_read(size)
318                    }
319                })
320        }
321
322        pub fn charge_coin_transfers(
323            &mut self,
324            protocol_config: &ProtocolConfig,
325            num_non_gas_coin_owners: u64,
326        ) -> Result<(), ExecutionError> {
327            // times two for the global pause and per-address settings
328            // this "overcharges" slightly since it does not check the global pause for each owner
329            // but rather each coin type.
330            let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
331            // associate the cost with dynamic field access so that it will increase if/when this
332            // cost increases
333            let cost_per_byte =
334                protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
335            let cost_per_owner = bytes_read_per_owner * cost_per_byte;
336            let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
337            self.gas_status.charge_storage_read(owner_cost)
338        }
339
340        /// Restore the store + gas state to the post-input shape: drop execution writes, clear
341        /// storage cost/rebate, re-smash gas, and re-touch mutable inputs so their versions still
342        /// bump on the err path.
343        pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
344            temporary_store.drop_writes();
345            self.gas_status.reset_storage_cost_and_rebate();
346            self.smash_gas(temporary_store);
347            temporary_store.ensure_active_inputs_mutated();
348        }
349
350        pub fn round_computation<T, E: ExecutionErrorTrait>(
351            &mut self,
352            result: Result<T, E>,
353        ) -> Result<T, E> {
354            debug_assert!(self.gas_status.storage_rebate() == 0);
355            debug_assert!(self.gas_status.storage_gas_units() == 0);
356
357            if matches!(&self.payment, PaymentMetadata::Unmetered) {
358                return result;
359            }
360            let is_move_abort = matches!(
361                result.as_ref().err().map(|e| e.kind()),
362                Some(sui_types::execution_status::ExecutionErrorKind::MoveAbort(
363                    ..
364                ))
365            );
366            let round_res = self.gas_status.bucketize_computation(Some(is_move_abort));
367            match result {
368                Ok(v) => round_res.map(|_| v).map_err(Into::into),
369                Err(e) => Err(e),
370            }
371        }
372
373        /// Meter storage: collect per-object storage cost/rebate and verify it fits the remaining
374        /// budget. Payment is applied later, by `charge`. For gasless, the execution requirements
375        /// must be validated before `ensure_active_inputs_mutated` populates `written_objects`.
376        pub fn meter_storage(
377            &mut self,
378            temporary_store: &mut TemporaryStore<'_>,
379        ) -> Result<(), ExecutionError> {
380            match &self.payment {
381                PaymentMetadata::Unmetered => {
382                    temporary_store.ensure_active_inputs_mutated();
383                    temporary_store.collect_storage_and_rebate(self)
384                }
385                PaymentMetadata::Gasless => {
386                    temporary_store
387                        .check_gasless_execution_requirements()
388                        .map_err(|msg| {
389                            ExecutionError::new_with_source(
390                                sui_types::execution_status::ExecutionErrorKind::InsufficientGas,
391                                msg,
392                            )
393                        })?;
394                    temporary_store.ensure_active_inputs_mutated();
395                    temporary_store.collect_storage_and_rebate(self)
396                }
397                PaymentMetadata::Smash(_) => {
398                    temporary_store.ensure_active_inputs_mutated();
399                    temporary_store.collect_storage_and_rebate(self)?;
400                    self.gas_status.charge_storage_and_rebate()
401                }
402            }
403        }
404
405        pub(crate) fn handle_error(
406            &mut self,
407            temporary_store: &mut TemporaryStore<'_>,
408        ) -> Result<(), ExecutionError> {
409            self.reset(temporary_store);
410            self.meter_storage(temporary_store).or_else(|_| {
411                // Even input-only storage doesn't fit: full budget for computation, rebates only.
412                self.reset(temporary_store);
413                self.set_computation_to_budget();
414                temporary_store.collect_rebate(self)
415            })
416        }
417
418        /// Apply the final gas charge derived from the current `SuiGasStatus`, per payment kind
419        /// (`Unmetered` / `Gasless` / `Smash`).
420        pub fn charge<T, E: ExecutionErrorTrait>(
421            &mut self,
422            temporary_store: &mut TemporaryStore<'_>,
423            execution_result: &Result<T, E>,
424        ) -> GasCostSummary {
425            match &self.payment {
426                PaymentMetadata::Unmetered => {
427                    // Park unmetered (system-tx) storage rebate into 0x5 so SUI is not dropped.
428                    let unmetered_storage_rebate = self.gas_status.unmetered_storage_rebate();
429                    temporary_store.conserve_unmetered_storage_rebate(unmetered_storage_rebate);
430                    GasCostSummary::default()
431                }
432                PaymentMetadata::Gasless => {
433                    if execution_result.is_err() {
434                        return GasCostSummary::default();
435                    }
436                    let cost_summary = self.gas_status.summary();
437                    let storage_cost = cost_summary.storage_cost;
438                    assert!(
439                        storage_cost == 0,
440                        "Gasless transaction must not incur storage cost, got {storage_cost}"
441                    );
442                    let sender_rebate = cost_summary.storage_rebate;
443                    GasCostSummary {
444                        computation_cost: sender_rebate,
445                        storage_cost: 0,
446                        storage_rebate: sender_rebate,
447                        non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
448                    }
449                }
450                PaymentMetadata::Smash(metadata) => {
451                    if let PaymentLocation::Coin(_) = metadata.gas_charge_location {
452                        #[skip_checked_arithmetic]
453                        trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
454                    }
455                    let cost_summary = self.gas_status.summary();
456                    self.apply_payment(temporary_store, cost_summary, metadata.gas_charge_location)
457                }
458            }
459        }
460
461        /// Apply the net gas charge or refund: to the gas coin (coin payment) or via an
462        /// accumulator event (address balance).
463        fn apply_payment(
464            &mut self,
465            temporary_store: &mut TemporaryStore<'_>,
466            cost_summary: GasCostSummary,
467            gas_payment_location: PaymentLocation,
468        ) -> GasCostSummary {
469            let net_change = cost_summary.net_gas_usage();
470            match gas_payment_location {
471                PaymentLocation::AddressBalance(payer_address) => {
472                    if net_change != 0 {
473                        let balance_type = sui_types::balance::Balance::type_tag(
474                            sui_types::gas_coin::GAS::type_tag(),
475                        );
476                        let event = AccumulatorEvent::from_balance_change(
477                            payer_address,
478                            balance_type,
479                            net_change
480                                .checked_neg()
481                                .expect("net gas usage is never i64::MIN"),
482                        )
483                        .expect("Failed to create accumulator event for gas charging");
484                        temporary_store.add_accumulator_event(event);
485                    }
486                }
487                PaymentLocation::Coin(gas_object_id) => {
488                    let mut gas_object = temporary_store
489                        .read_object(&gas_object_id)
490                        .expect("gas coin is an input object and present after smashing")
491                        .clone();
492                    deduct_gas(&mut gas_object, net_change);
493                    #[skip_checked_arithmetic]
494                    trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
495                    temporary_store.mutate_new_or_input_object(gas_object);
496                }
497            }
498            cost_summary
499        }
500
501        // === legacy methods below - see `mod legacy` for the full bodies ===
502    }
503
504    /// Pre-refactor gas charging: a single monolithic `charge_gas` plus the storage-OOG
505    /// retry helpers. Kept here so transactions executed at `gas_model_version < 15`
506    /// (i.e. before the v15+ pipeline activates) replay bit-for-bit identically to
507    /// what `origin/main` produces today, including the SUIPR-753 surgical fix gated on
508    /// `refresh_gas_payment_location`. Will be removed when execution_version bumps past 4.
509    mod legacy {
510        use super::*;
511
512        impl super::GasCharger {
513            /// Pre-v15 input charging: sum all input sizes and charge
514            /// `storage_read` once. Bit-for-bit identical to origin/main for
515            /// replay determinism; goes away when execution_version > 4.
516            pub fn charge_input_objects_legacy(
517                &mut self,
518                temporary_store: &TemporaryStore<'_>,
519            ) -> Result<(), ExecutionError> {
520                let objects = temporary_store.objects();
521                // TODO: Charge input object count.
522                let _object_count = objects.len();
523                // Charge bytes read
524                let total_size = temporary_store
525                    .objects()
526                    .iter()
527                    // don't charge for loading Sui Framework or Move stdlib
528                    .filter(|(id, _)| !is_system_package(**id))
529                    .map(|(_, obj)| obj.object_size_for_gas_metering())
530                    .sum();
531                self.gas_status.charge_storage_read(total_size)
532            }
533
534            /// Entry point for legacy gas charging.
535            /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field
536            /// of mutated objects
537            /// 2. Deduct computation gas costs and storage costs, credit storage rebates.
538            /// The happy path of this function follows (1) + (2) and is fairly simple.
539            /// Most of the complexity is in the unhappy paths:
540            /// - if execution aborted before calling this function, we have to dump all writes +
541            ///   re-smash gas, then charge for storage
542            /// - if we run out of gas while charging for storage, we have to dump all writes +
543            ///   re-smash gas, then charge for storage again
544            pub(crate) fn legacy_charge_gas<T, E: ExecutionErrorTrait>(
545                &mut self,
546                temporary_store: &mut TemporaryStore<'_>,
547                protocol_config: &ProtocolConfig,
548                execution_result: &mut Result<T, E>,
549            ) -> GasCostSummary {
550                // at this point, we have done *all* charging for computation,
551                // but have not yet set the storage rebate or storage gas units
552                debug_assert!(self.gas_status.storage_rebate() == 0);
553                debug_assert!(self.gas_status.storage_gas_units() == 0);
554
555                if !matches!(&self.payment, PaymentMetadata::Unmetered) {
556                    // bucketize computation cost
557                    let is_move_abort = execution_result
558                        .as_ref()
559                        .err()
560                        .map(|err| {
561                            matches!(
562                                err.kind(),
563                                sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
564                            )
565                        })
566                        .unwrap_or(false);
567                    // bucketize computation cost
568                    if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
569                        && execution_result.is_ok()
570                    {
571                        *execution_result = Err(err.into());
572                    }
573
574                    // On error we need to dump writes, deletes, etc before charging storage gas
575                    if execution_result.is_err() {
576                        self.reset(temporary_store);
577                    }
578                }
579
580                // compute and collect storage charges
581                temporary_store.ensure_active_inputs_mutated();
582                temporary_store
583                    .collect_storage_and_rebate(self)
584                    .expect("storage gas overflow");
585
586                if matches!(&self.payment, PaymentMetadata::Unmetered) {
587                    return GasCostSummary::default();
588                }
589                let gas_payment_location = self.gas_payment_location();
590                if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
591                    #[skip_checked_arithmetic]
592                    trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
593                }
594
595                if execution_result
596                .as_ref()
597                .err()
598                .map(|err| {
599                    matches!(
600                        err.kind(),
601                        sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
602                    )
603                })
604                .unwrap_or(false)
605                && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
606                    debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
607                    // If we don't have enough balance to withdraw, don't charge for gas
608                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
609                    return GasCostSummary::default();
610            }
611
612                self.compute_storage_and_rebate(temporary_store, execution_result);
613
614                let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
615                    self.gas_payment_location()
616                } else {
617                    gas_payment_location
618                };
619
620                let cost_summary = self.gas_status.summary();
621
622                let Some(gas_payment_location) = gas_payment_location else {
623                    // Gasless: sender pays nothing.
624                    assert!(
625                        matches!(self.payment, PaymentMetadata::Gasless),
626                        "Only gasless transactions should reach this point without a payment location"
627                    );
628                    if execution_result.is_err() {
629                        return GasCostSummary::default();
630                    }
631                    // Any storage rebate from destroyed input coins is absorbed as
632                    // network fees, not returned to sender.
633                    let storage_cost = cost_summary.storage_cost;
634                    assert!(
635                        storage_cost == 0,
636                        "Gasless transaction must not incur storage cost, got {storage_cost}"
637                    );
638                    let sender_rebate = cost_summary.storage_rebate;
639                    return GasCostSummary {
640                        computation_cost: sender_rebate,
641                        storage_cost: 0,
642                        storage_rebate: sender_rebate,
643                        non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
644                    };
645                };
646
647                let net_change = cost_summary.net_gas_usage();
648
649                match gas_payment_location {
650                    PaymentLocation::AddressBalance(payer_address) => {
651                        // TODO tracing?
652                        if net_change != 0 {
653                            let balance_type = sui_types::balance::Balance::type_tag(
654                                sui_types::gas_coin::GAS::type_tag(),
655                            );
656                            let event = AccumulatorEvent::from_balance_change(
657                                payer_address,
658                                balance_type,
659                                net_change.checked_neg().unwrap(),
660                            )
661                            .expect("Failed to create accumulator event for gas charging");
662                            temporary_store.add_accumulator_event(event);
663                        }
664                    }
665                    PaymentLocation::Coin(gas_object_id) => {
666                        let mut gas_object =
667                            temporary_store.read_object(&gas_object_id).unwrap().clone();
668                        deduct_gas(&mut gas_object, net_change);
669                        #[skip_checked_arithmetic]
670                        trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
671                        temporary_store.mutate_new_or_input_object(gas_object);
672                    }
673                }
674                cost_summary
675            }
676
677            /// Calculate total gas cost considering storage and rebate.
678            ///
679            /// First, we net computation, storage, and rebate to determine total gas to charge.
680            ///
681            /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
682            /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
683            ///
684            /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
685            /// v2: we charge (computation + storage costs for input objects - storage_rebates)
686            ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
687            ///     so we charge net (gas_budget - storage_rebates)
688            fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
689                &mut self,
690                temporary_store: &mut TemporaryStore<'_>,
691                execution_result: &mut Result<T, E>,
692            ) {
693                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
694                    // we run out of gas charging storage, reset and try charging for storage again.
695                    // Input objects are touched and so they have a storage cost
696                    // Attempt to charge just for computation + input object storage costs - storage_rebate
697                    self.reset(temporary_store);
698                    temporary_store.ensure_active_inputs_mutated();
699                    temporary_store
700                        .collect_storage_and_rebate(self)
701                        .expect("storage gas overflow");
702                    if let Err(err) = self.gas_status.charge_storage_and_rebate() {
703                        // we run out of gas attempting to charge for the input objects exclusively,
704                        // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
705                        self.reset(temporary_store);
706                        self.gas_status.adjust_computation_on_out_of_gas();
707                        temporary_store.ensure_active_inputs_mutated();
708                        temporary_store
709                            .collect_rebate(self)
710                            .expect("storage gas overflow");
711                        if execution_result.is_ok() {
712                            *execution_result = Err(err.into());
713                        }
714                    } else if execution_result.is_ok() {
715                        *execution_result = Err(err.into());
716                    }
717                }
718            }
719        }
720    }
721
722    impl SmashMetadata {
723        /// Iterates over all payment methods: the smash target followed by the smashed payments.
724        fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
725            std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
726        }
727
728        fn smash_gas(
729            &mut self,
730            tx_digest: &TransactionDigest,
731            temporary_store: &mut TemporaryStore<'_>,
732        ) {
733            // set gas charge location
734            self.gas_charge_location = self.smash_target.location();
735
736            // sum the value of all gas coins
737            let total_smashed = self
738                .payment_methods()
739                .map(|payment| match payment {
740                    PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
741                    PaymentMethod::Coin(obj_ref) => {
742                        let obj_data = temporary_store
743                            .objects()
744                            .get(&obj_ref.0)
745                            .map(|obj| &obj.data);
746                        let Some(Data::Move(move_obj)) = obj_data else {
747                            return Err(ExecutionError::invariant_violation(
748                                "Provided non-gas coin object as input for gas!",
749                            ));
750                        };
751                        if !move_obj.type_().is_gas_coin() {
752                            return Err(ExecutionError::invariant_violation(
753                                "Provided non-gas coin object as input for gas!",
754                            ));
755                        }
756                        Ok(move_obj.get_coin_value_unsafe())
757                    }
758                })
759                .collect::<Result<Vec<u64>, ExecutionError>>()
760                // transaction and certificate input checks must have insured that all gas coins
761                // are valid
762                .unwrap_or_else(|_| {
763                    panic!(
764                        "Unable to process gas payments for transaction {}",
765                        tx_digest
766                    )
767                })
768                .iter()
769                .sum();
770            // If it is 0, then we are smashing for the first time (at the beginning of execution).
771            // If it is non-zero, then we are re-smashing after a reset (due to some sort of
772            // failure in charging for gas), and the total should not change.
773            debug_assert!(
774                self.total_smashed == 0 || self.total_smashed == total_smashed,
775                "Gas smashing should not change after a reset"
776            );
777            self.total_smashed = total_smashed;
778
779            let smash_location = self.smash_target.location();
780            // delete all gas objects except the smash target
781            for payment_method in self.smashed_payments.values() {
782                let location = payment_method.location();
783                assert_ne!(location, smash_location, "Payment methods must be unique");
784                match payment_method {
785                    PaymentMethod::AddressBalance(sui_address, reservation) => {
786                        assert_reachable!("smashed payment is address-balance reservation");
787                        let balance_type = sui_types::balance::Balance::type_tag(
788                            sui_types::gas_coin::GAS::type_tag(),
789                        );
790                        let event = AccumulatorEvent::from_balance_change(
791                            *sui_address,
792                            balance_type,
793                            i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
794                        )
795                        .expect("Failed to create accumulator event for gas smashing");
796                        temporary_store.add_accumulator_event(event);
797                    }
798                    PaymentMethod::Coin((id, _, _)) => {
799                        assert_reachable!("smashed payment is coin object");
800                        temporary_store.delete_input_object(id);
801                    }
802                }
803            }
804            match &self.smash_target {
805                PaymentMethod::AddressBalance(sui_address, reservation) => {
806                    assert_reachable!("smash target is address-balance reservation");
807                    // The reservation here is only a maximal withdrawal from this address balance
808                    // We do not need to withdraw here unless necessary, which will be done during
809                    // gas charging
810                    let deposit = total_smashed - *reservation;
811                    if deposit != 0 {
812                        let balance_type = sui_types::balance::Balance::type_tag(
813                            sui_types::gas_coin::GAS::type_tag(),
814                        );
815                        let event = AccumulatorEvent::from_balance_change(
816                            *sui_address,
817                            balance_type,
818                            i64::try_from(deposit).unwrap(),
819                        )
820                        .expect("Failed to create accumulator event for gas smashing");
821                        temporary_store.add_accumulator_event(event);
822                    }
823                }
824                PaymentMethod::Coin((gas_coin_id, _, _)) => {
825                    let mut primary_gas_object = temporary_store
826                        .objects()
827                        .get(gas_coin_id)
828                        // unwrap should be safe because we checked that this exists in `self.objects()` above
829                        .unwrap_or_else(|| {
830                            panic!(
831                                "Invariant violation: gas coin not found in store in txn {}",
832                                tx_digest
833                            )
834                        })
835                        .clone();
836                    primary_gas_object
837                        .data
838                        .try_as_move_mut()
839                        // unwrap should be safe because we checked that the primary gas object was a coin object above.
840                        .unwrap_or_else(|| {
841                            panic!(
842                                "Invariant violation: invalid coin object in txn {}",
843                                tx_digest
844                            )
845                        })
846                        .set_coin_value_unsafe(total_smashed);
847                    temporary_store.mutate_input_object(primary_gas_object);
848                }
849            }
850        }
851
852        fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
853            self.payment_methods().filter_map(|method| match method {
854                PaymentMethod::Coin(obj_ref) => Some(obj_ref),
855                PaymentMethod::AddressBalance(_, _) => None,
856            })
857        }
858    }
859
860    impl PaymentKind {
861        pub fn unmetered() -> Self {
862            Self(PaymentKind_::Unmetered)
863        }
864
865        /// Metered-but-free.
866        pub fn gasless() -> Self {
867            Self(PaymentKind_::Gasless)
868        }
869
870        /// `None` on an invalid payment set: empty, a duplicate gas coin, or an overflowing
871        /// address-balance reservation sum.
872        pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
873            if payment_methods.is_empty() {
874                return None;
875            }
876            let mut unique_methods = IndexMap::new();
877            for payment_method in payment_methods {
878                match (
879                    unique_methods.entry(payment_method.location()),
880                    payment_method,
881                ) {
882                    (indexmap::map::Entry::Vacant(entry), payment_method) => {
883                        entry.insert(payment_method);
884                    }
885                    (
886                        indexmap::map::Entry::Occupied(mut occupied),
887                        PaymentMethod::AddressBalance(other, additional),
888                    ) => {
889                        let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
890                            unreachable!("Payment method does not match location")
891                        };
892                        assert_eq!(*addr, other, "Payment method does not match location");
893                        *amount = amount.checked_add(additional)?;
894                    }
895                    // Duplicate gas coin; input checks should have rejected it.
896                    (indexmap::map::Entry::Occupied(_), _) => return None,
897                }
898            }
899            Some(Self(PaymentKind_::Smash(unique_methods)))
900        }
901    }
902
903    impl PaymentMethod {
904        pub fn location(&self) -> PaymentLocation {
905            match self {
906                PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
907                PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
908            }
909        }
910    }
911}