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                .map(|(_, obj)| obj.object_size_for_gas_metering())
310                .try_for_each(|size| self.gas_status.charge_storage_read(size))
311        }
312
313        pub fn charge_coin_transfers(
314            &mut self,
315            protocol_config: &ProtocolConfig,
316            num_non_gas_coin_owners: u64,
317        ) -> Result<(), ExecutionError> {
318            // times two for the global pause and per-address settings
319            // this "overcharges" slightly since it does not check the global pause for each owner
320            // but rather each coin type.
321            let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
322            // associate the cost with dynamic field access so that it will increase if/when this
323            // cost increases
324            let cost_per_byte =
325                protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
326            let cost_per_owner = bytes_read_per_owner * cost_per_byte;
327            let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
328            self.gas_status.charge_storage_read(owner_cost)
329        }
330
331        /// Restore the store + gas state to the post-input shape: drop execution writes, clear
332        /// storage cost/rebate, re-smash gas, and re-touch mutable inputs so their versions still
333        /// bump on the err path.
334        pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
335            temporary_store.drop_writes();
336            self.gas_status.reset_storage_cost_and_rebate();
337            self.smash_gas(temporary_store);
338            temporary_store.ensure_active_inputs_mutated();
339        }
340
341        pub fn round_computation<T, E: ExecutionErrorTrait>(
342            &mut self,
343            result: Result<T, E>,
344        ) -> Result<T, E> {
345            debug_assert!(self.gas_status.storage_rebate() == 0);
346            debug_assert!(self.gas_status.storage_gas_units() == 0);
347
348            if matches!(&self.payment, PaymentMetadata::Unmetered) {
349                return result;
350            }
351            let is_move_abort = matches!(
352                result.as_ref().err().map(|e| e.kind()),
353                Some(sui_types::execution_status::ExecutionErrorKind::MoveAbort(
354                    ..
355                ))
356            );
357            let round_res = self.gas_status.bucketize_computation(Some(is_move_abort));
358            match result {
359                Ok(v) => round_res.map(|_| v).map_err(Into::into),
360                Err(e) => Err(e),
361            }
362        }
363
364        /// Meter storage: collect per-object storage cost/rebate and verify it fits the remaining
365        /// budget. Payment is applied later, by `charge`. For gasless, the execution requirements
366        /// must be validated before `ensure_active_inputs_mutated` populates `written_objects`.
367        pub fn meter_storage(
368            &mut self,
369            temporary_store: &mut TemporaryStore<'_>,
370        ) -> Result<(), ExecutionError> {
371            match &self.payment {
372                PaymentMetadata::Unmetered => {
373                    temporary_store.ensure_active_inputs_mutated();
374                    temporary_store.collect_storage_and_rebate(self)
375                }
376                PaymentMetadata::Gasless => {
377                    temporary_store
378                        .check_gasless_execution_requirements()
379                        .map_err(|msg| {
380                            ExecutionError::new_with_source(
381                                sui_types::execution_status::ExecutionErrorKind::InsufficientGas,
382                                msg,
383                            )
384                        })?;
385                    temporary_store.ensure_active_inputs_mutated();
386                    temporary_store.collect_storage_and_rebate(self)
387                }
388                PaymentMetadata::Smash(_) => {
389                    temporary_store.ensure_active_inputs_mutated();
390                    temporary_store.collect_storage_and_rebate(self)?;
391                    self.gas_status.charge_storage_and_rebate()
392                }
393            }
394        }
395
396        pub(crate) fn handle_error(
397            &mut self,
398            temporary_store: &mut TemporaryStore<'_>,
399        ) -> Result<(), ExecutionError> {
400            self.reset(temporary_store);
401            self.meter_storage(temporary_store).or_else(|_| {
402                // Even input-only storage doesn't fit: full budget for computation, rebates only.
403                self.reset(temporary_store);
404                self.set_computation_to_budget();
405                temporary_store.collect_rebate(self)
406            })
407        }
408
409        /// Apply the final gas charge derived from the current `SuiGasStatus`, per payment kind
410        /// (`Unmetered` / `Gasless` / `Smash`).
411        pub fn charge<T, E: ExecutionErrorTrait>(
412            &mut self,
413            temporary_store: &mut TemporaryStore<'_>,
414            execution_result: &Result<T, E>,
415        ) -> GasCostSummary {
416            match &self.payment {
417                PaymentMetadata::Unmetered => {
418                    // Park unmetered (system-tx) storage rebate into 0x5 so SUI is not dropped.
419                    let unmetered_storage_rebate = self.gas_status.unmetered_storage_rebate();
420                    temporary_store.conserve_unmetered_storage_rebate(unmetered_storage_rebate);
421                    GasCostSummary::default()
422                }
423                PaymentMetadata::Gasless => {
424                    if execution_result.is_err() {
425                        return GasCostSummary::default();
426                    }
427                    let cost_summary = self.gas_status.summary();
428                    let storage_cost = cost_summary.storage_cost;
429                    assert!(
430                        storage_cost == 0,
431                        "Gasless transaction must not incur storage cost, got {storage_cost}"
432                    );
433                    let sender_rebate = cost_summary.storage_rebate;
434                    GasCostSummary {
435                        computation_cost: sender_rebate,
436                        storage_cost: 0,
437                        storage_rebate: sender_rebate,
438                        non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
439                    }
440                }
441                PaymentMetadata::Smash(metadata) => {
442                    if let PaymentLocation::Coin(_) = metadata.gas_charge_location {
443                        #[skip_checked_arithmetic]
444                        trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
445                    }
446                    let cost_summary = self.gas_status.summary();
447                    self.apply_payment(temporary_store, cost_summary, metadata.gas_charge_location)
448                }
449            }
450        }
451
452        /// Apply the net gas charge or refund: to the gas coin (coin payment) or via an
453        /// accumulator event (address balance).
454        fn apply_payment(
455            &mut self,
456            temporary_store: &mut TemporaryStore<'_>,
457            cost_summary: GasCostSummary,
458            gas_payment_location: PaymentLocation,
459        ) -> GasCostSummary {
460            let net_change = cost_summary.net_gas_usage();
461            match gas_payment_location {
462                PaymentLocation::AddressBalance(payer_address) => {
463                    if net_change != 0 {
464                        let balance_type = sui_types::balance::Balance::type_tag(
465                            sui_types::gas_coin::GAS::type_tag(),
466                        );
467                        let event = AccumulatorEvent::from_balance_change(
468                            payer_address,
469                            balance_type,
470                            net_change
471                                .checked_neg()
472                                .expect("net gas usage is never i64::MIN"),
473                        )
474                        .expect("Failed to create accumulator event for gas charging");
475                        temporary_store.add_accumulator_event(event);
476                    }
477                }
478                PaymentLocation::Coin(gas_object_id) => {
479                    let mut gas_object = temporary_store
480                        .read_object(&gas_object_id)
481                        .expect("gas coin is an input object and present after smashing")
482                        .clone();
483                    deduct_gas(&mut gas_object, net_change);
484                    #[skip_checked_arithmetic]
485                    trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
486                    temporary_store.mutate_new_or_input_object(gas_object);
487                }
488            }
489            cost_summary
490        }
491
492        // === legacy methods below - see `mod legacy` for the full bodies ===
493    }
494
495    /// Pre-refactor gas charging: a single monolithic `charge_gas` plus the storage-OOG
496    /// retry helpers. Kept here so transactions executed at `gas_model_version < 15`
497    /// (i.e. before the v15+ pipeline activates) replay bit-for-bit identically to
498    /// what `origin/main` produces today, including the SUIPR-753 surgical fix gated on
499    /// `refresh_gas_payment_location`. Will be removed when execution_version bumps past 4.
500    mod legacy {
501        use super::*;
502
503        impl super::GasCharger {
504            /// Pre-v15 input charging: sum all input sizes and charge
505            /// `storage_read` once. Bit-for-bit identical to origin/main for
506            /// replay determinism; goes away when execution_version > 4.
507            pub fn charge_input_objects_legacy(
508                &mut self,
509                temporary_store: &TemporaryStore<'_>,
510            ) -> Result<(), ExecutionError> {
511                let objects = temporary_store.objects();
512                // TODO: Charge input object count.
513                let _object_count = objects.len();
514                // Charge bytes read
515                let total_size = temporary_store
516                    .objects()
517                    .iter()
518                    // don't charge for loading Sui Framework or Move stdlib
519                    .filter(|(id, _)| !is_system_package(**id))
520                    .map(|(_, obj)| obj.object_size_for_gas_metering())
521                    .sum();
522                self.gas_status.charge_storage_read(total_size)
523            }
524
525            /// Entry point for legacy gas charging.
526            /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field
527            /// of mutated objects
528            /// 2. Deduct computation gas costs and storage costs, credit storage rebates.
529            /// The happy path of this function follows (1) + (2) and is fairly simple.
530            /// Most of the complexity is in the unhappy paths:
531            /// - if execution aborted before calling this function, we have to dump all writes +
532            ///   re-smash gas, then charge for storage
533            /// - if we run out of gas while charging for storage, we have to dump all writes +
534            ///   re-smash gas, then charge for storage again
535            pub(crate) fn legacy_charge_gas<T, E: ExecutionErrorTrait>(
536                &mut self,
537                temporary_store: &mut TemporaryStore<'_>,
538                protocol_config: &ProtocolConfig,
539                execution_result: &mut Result<T, E>,
540            ) -> GasCostSummary {
541                // at this point, we have done *all* charging for computation,
542                // but have not yet set the storage rebate or storage gas units
543                debug_assert!(self.gas_status.storage_rebate() == 0);
544                debug_assert!(self.gas_status.storage_gas_units() == 0);
545
546                if !matches!(&self.payment, PaymentMetadata::Unmetered) {
547                    // bucketize computation cost
548                    let is_move_abort = execution_result
549                        .as_ref()
550                        .err()
551                        .map(|err| {
552                            matches!(
553                                err.kind(),
554                                sui_types::execution_status::ExecutionErrorKind::MoveAbort(_, _)
555                            )
556                        })
557                        .unwrap_or(false);
558                    // bucketize computation cost
559                    if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
560                        && execution_result.is_ok()
561                    {
562                        *execution_result = Err(err.into());
563                    }
564
565                    // On error we need to dump writes, deletes, etc before charging storage gas
566                    if execution_result.is_err() {
567                        self.reset(temporary_store);
568                    }
569                }
570
571                // compute and collect storage charges
572                temporary_store.ensure_active_inputs_mutated();
573                temporary_store
574                    .collect_storage_and_rebate(self)
575                    .expect("storage gas overflow");
576
577                if matches!(&self.payment, PaymentMetadata::Unmetered) {
578                    return GasCostSummary::default();
579                }
580                let gas_payment_location = self.gas_payment_location();
581                if let Some(PaymentLocation::Coin(_)) = gas_payment_location {
582                    #[skip_checked_arithmetic]
583                    trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
584                }
585
586                if execution_result
587                .as_ref()
588                .err()
589                .map(|err| {
590                    matches!(
591                        err.kind(),
592                        sui_types::execution_status::ExecutionErrorKind::InsufficientFundsForWithdraw
593                    )
594                })
595                .unwrap_or(false)
596                && matches!(gas_payment_location, Some(PaymentLocation::AddressBalance(_))) {
597                    debug_assert!(!protocol_config.early_exit_on_iffw(), "Should have not reached charge gas in this case with IFFW");
598                    // If we don't have enough balance to withdraw, don't charge for gas
599                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
600                    return GasCostSummary::default();
601            }
602
603                self.compute_storage_and_rebate(temporary_store, execution_result);
604
605                let gas_payment_location = if refresh_gas_payment_location(self.gas_model_version) {
606                    self.gas_payment_location()
607                } else {
608                    gas_payment_location
609                };
610
611                let cost_summary = self.gas_status.summary();
612
613                let Some(gas_payment_location) = gas_payment_location else {
614                    // Gasless: sender pays nothing.
615                    assert!(
616                        matches!(self.payment, PaymentMetadata::Gasless),
617                        "Only gasless transactions should reach this point without a payment location"
618                    );
619                    if execution_result.is_err() {
620                        return GasCostSummary::default();
621                    }
622                    // Any storage rebate from destroyed input coins is absorbed as
623                    // network fees, not returned to sender.
624                    let storage_cost = cost_summary.storage_cost;
625                    assert!(
626                        storage_cost == 0,
627                        "Gasless transaction must not incur storage cost, got {storage_cost}"
628                    );
629                    let sender_rebate = cost_summary.storage_rebate;
630                    return GasCostSummary {
631                        computation_cost: sender_rebate,
632                        storage_cost: 0,
633                        storage_rebate: sender_rebate,
634                        non_refundable_storage_fee: cost_summary.non_refundable_storage_fee,
635                    };
636                };
637
638                let net_change = cost_summary.net_gas_usage();
639
640                match gas_payment_location {
641                    PaymentLocation::AddressBalance(payer_address) => {
642                        // TODO tracing?
643                        if net_change != 0 {
644                            let balance_type = sui_types::balance::Balance::type_tag(
645                                sui_types::gas_coin::GAS::type_tag(),
646                            );
647                            let event = AccumulatorEvent::from_balance_change(
648                                payer_address,
649                                balance_type,
650                                net_change.checked_neg().unwrap(),
651                            )
652                            .expect("Failed to create accumulator event for gas charging");
653                            temporary_store.add_accumulator_event(event);
654                        }
655                    }
656                    PaymentLocation::Coin(gas_object_id) => {
657                        let mut gas_object =
658                            temporary_store.read_object(&gas_object_id).unwrap().clone();
659                        deduct_gas(&mut gas_object, net_change);
660                        #[skip_checked_arithmetic]
661                        trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
662                        temporary_store.mutate_new_or_input_object(gas_object);
663                    }
664                }
665                cost_summary
666            }
667
668            /// Calculate total gas cost considering storage and rebate.
669            ///
670            /// First, we net computation, storage, and rebate to determine total gas to charge.
671            ///
672            /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
673            /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
674            ///
675            /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
676            /// v2: we charge (computation + storage costs for input objects - storage_rebates)
677            ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
678            ///     so we charge net (gas_budget - storage_rebates)
679            fn compute_storage_and_rebate<T, E: ExecutionErrorTrait>(
680                &mut self,
681                temporary_store: &mut TemporaryStore<'_>,
682                execution_result: &mut Result<T, E>,
683            ) {
684                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
685                    // we run out of gas charging storage, reset and try charging for storage again.
686                    // Input objects are touched and so they have a storage cost
687                    // Attempt to charge just for computation + input object storage costs - storage_rebate
688                    self.reset(temporary_store);
689                    temporary_store.ensure_active_inputs_mutated();
690                    temporary_store
691                        .collect_storage_and_rebate(self)
692                        .expect("storage gas overflow");
693                    if let Err(err) = self.gas_status.charge_storage_and_rebate() {
694                        // we run out of gas attempting to charge for the input objects exclusively,
695                        // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
696                        self.reset(temporary_store);
697                        self.gas_status.adjust_computation_on_out_of_gas();
698                        temporary_store.ensure_active_inputs_mutated();
699                        temporary_store
700                            .collect_rebate(self)
701                            .expect("storage gas overflow");
702                        if execution_result.is_ok() {
703                            *execution_result = Err(err.into());
704                        }
705                    } else if execution_result.is_ok() {
706                        *execution_result = Err(err.into());
707                    }
708                }
709            }
710        }
711    }
712
713    impl SmashMetadata {
714        /// Iterates over all payment methods: the smash target followed by the smashed payments.
715        fn payment_methods(&self) -> impl Iterator<Item = &'_ PaymentMethod> {
716            std::iter::once(&self.smash_target).chain(self.smashed_payments.values())
717        }
718
719        fn smash_gas(
720            &mut self,
721            tx_digest: &TransactionDigest,
722            temporary_store: &mut TemporaryStore<'_>,
723        ) {
724            // set gas charge location
725            self.gas_charge_location = self.smash_target.location();
726
727            // sum the value of all gas coins
728            let total_smashed = self
729                .payment_methods()
730                .map(|payment| match payment {
731                    PaymentMethod::AddressBalance(_, reservation) => Ok(*reservation),
732                    PaymentMethod::Coin(obj_ref) => {
733                        let obj_data = temporary_store
734                            .objects()
735                            .get(&obj_ref.0)
736                            .map(|obj| &obj.data);
737                        let Some(Data::Move(move_obj)) = obj_data else {
738                            return Err(ExecutionError::invariant_violation(
739                                "Provided non-gas coin object as input for gas!",
740                            ));
741                        };
742                        if !move_obj.type_().is_gas_coin() {
743                            return Err(ExecutionError::invariant_violation(
744                                "Provided non-gas coin object as input for gas!",
745                            ));
746                        }
747                        Ok(move_obj.get_coin_value_unsafe())
748                    }
749                })
750                .collect::<Result<Vec<u64>, ExecutionError>>()
751                // transaction and certificate input checks must have insured that all gas coins
752                // are valid
753                .unwrap_or_else(|_| {
754                    panic!(
755                        "Unable to process gas payments for transaction {}",
756                        tx_digest
757                    )
758                })
759                .iter()
760                .sum();
761            // If it is 0, then we are smashing for the first time (at the beginning of execution).
762            // If it is non-zero, then we are re-smashing after a reset (due to some sort of
763            // failure in charging for gas), and the total should not change.
764            debug_assert!(
765                self.total_smashed == 0 || self.total_smashed == total_smashed,
766                "Gas smashing should not change after a reset"
767            );
768            self.total_smashed = total_smashed;
769
770            let smash_location = self.smash_target.location();
771            // delete all gas objects except the smash target
772            for payment_method in self.smashed_payments.values() {
773                let location = payment_method.location();
774                assert_ne!(location, smash_location, "Payment methods must be unique");
775                match payment_method {
776                    PaymentMethod::AddressBalance(sui_address, reservation) => {
777                        assert_reachable!("smashed payment is address-balance reservation");
778                        let balance_type = sui_types::balance::Balance::type_tag(
779                            sui_types::gas_coin::GAS::type_tag(),
780                        );
781                        let event = AccumulatorEvent::from_balance_change(
782                            *sui_address,
783                            balance_type,
784                            i64::try_from(*reservation).unwrap().checked_neg().unwrap(),
785                        )
786                        .expect("Failed to create accumulator event for gas smashing");
787                        temporary_store.add_accumulator_event(event);
788                    }
789                    PaymentMethod::Coin((id, _, _)) => {
790                        assert_reachable!("smashed payment is coin object");
791                        temporary_store.delete_input_object(id);
792                    }
793                }
794            }
795            match &self.smash_target {
796                PaymentMethod::AddressBalance(sui_address, reservation) => {
797                    assert_reachable!("smash target is address-balance reservation");
798                    // The reservation here is only a maximal withdrawal from this address balance
799                    // We do not need to withdraw here unless necessary, which will be done during
800                    // gas charging
801                    let deposit = total_smashed - *reservation;
802                    if deposit != 0 {
803                        let balance_type = sui_types::balance::Balance::type_tag(
804                            sui_types::gas_coin::GAS::type_tag(),
805                        );
806                        let event = AccumulatorEvent::from_balance_change(
807                            *sui_address,
808                            balance_type,
809                            i64::try_from(deposit).unwrap(),
810                        )
811                        .expect("Failed to create accumulator event for gas smashing");
812                        temporary_store.add_accumulator_event(event);
813                    }
814                }
815                PaymentMethod::Coin((gas_coin_id, _, _)) => {
816                    let mut primary_gas_object = temporary_store
817                        .objects()
818                        .get(gas_coin_id)
819                        // unwrap should be safe because we checked that this exists in `self.objects()` above
820                        .unwrap_or_else(|| {
821                            panic!(
822                                "Invariant violation: gas coin not found in store in txn {}",
823                                tx_digest
824                            )
825                        })
826                        .clone();
827                    primary_gas_object
828                        .data
829                        .try_as_move_mut()
830                        // unwrap should be safe because we checked that the primary gas object was a coin object above.
831                        .unwrap_or_else(|| {
832                            panic!(
833                                "Invariant violation: invalid coin object in txn {}",
834                                tx_digest
835                            )
836                        })
837                        .set_coin_value_unsafe(total_smashed);
838                    temporary_store.mutate_input_object(primary_gas_object);
839                }
840            }
841        }
842
843        fn used_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
844            self.payment_methods().filter_map(|method| match method {
845                PaymentMethod::Coin(obj_ref) => Some(obj_ref),
846                PaymentMethod::AddressBalance(_, _) => None,
847            })
848        }
849    }
850
851    impl PaymentKind {
852        pub fn unmetered() -> Self {
853            Self(PaymentKind_::Unmetered)
854        }
855
856        /// Metered-but-free.
857        pub fn gasless() -> Self {
858            Self(PaymentKind_::Gasless)
859        }
860
861        /// `None` on an invalid payment set: empty, a duplicate gas coin, or an overflowing
862        /// address-balance reservation sum.
863        pub fn smash(payment_methods: Vec<PaymentMethod>) -> Option<Self> {
864            if payment_methods.is_empty() {
865                return None;
866            }
867            let mut unique_methods = IndexMap::new();
868            for payment_method in payment_methods {
869                match (
870                    unique_methods.entry(payment_method.location()),
871                    payment_method,
872                ) {
873                    (indexmap::map::Entry::Vacant(entry), payment_method) => {
874                        entry.insert(payment_method);
875                    }
876                    (
877                        indexmap::map::Entry::Occupied(mut occupied),
878                        PaymentMethod::AddressBalance(other, additional),
879                    ) => {
880                        let PaymentMethod::AddressBalance(addr, amount) = occupied.get_mut() else {
881                            unreachable!("Payment method does not match location")
882                        };
883                        assert_eq!(*addr, other, "Payment method does not match location");
884                        *amount = amount.checked_add(additional)?;
885                    }
886                    // Duplicate gas coin; input checks should have rejected it.
887                    (indexmap::map::Entry::Occupied(_), _) => return None,
888                }
889            }
890            Some(Self(PaymentKind_::Smash(unique_methods)))
891        }
892    }
893
894    impl PaymentMethod {
895        pub fn location(&self) -> PaymentLocation {
896            match self {
897                PaymentMethod::Coin(obj_ref) => PaymentLocation::Coin(obj_ref.0),
898                PaymentMethod::AddressBalance(addr, _) => PaymentLocation::AddressBalance(*addr),
899            }
900        }
901    }
902}