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 sui_protocol_config::ProtocolConfig;
14    use sui_types::deny_list_v2::CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
15    use sui_types::gas::{GasCostSummary, SuiGasStatus, deduct_gas};
16    use sui_types::gas_model::gas_predicates::{
17        charge_upgrades, dont_charge_budget_on_storage_oog,
18    };
19    use sui_types::{
20        accumulator_event::AccumulatorEvent,
21        base_types::{ObjectID, ObjectRef, SuiAddress},
22        digests::TransactionDigest,
23        error::ExecutionError,
24        gas_model::tables::GasStatus,
25        is_system_package,
26        object::Data,
27    };
28    use tracing::trace;
29
30    /// Tracks all gas operations for a single transaction.
31    /// This is the main entry point for gas accounting.
32    /// All the information about gas is stored in this object.
33    /// The objective here is two-fold:
34    /// 1- Isolate al version info into a single entry point. This file and the other gas
35    ///    related files are the only one that check for gas version.
36    /// 2- Isolate all gas accounting into a single implementation. Gas objects are not
37    ///    passed around, and they are retrieved from this instance.
38    #[derive(Debug)]
39    pub struct GasCharger {
40        tx_digest: TransactionDigest,
41        gas_model_version: u64,
42        payment_method: PaymentMethod,
43        // this is the first gas coin in `gas_coins` and the one that all others will
44        // be smashed into. It can be None for system transactions when `gas_coins` is empty.
45        smashed_gas_coin: Option<ObjectID>,
46        //smashed_gas_coin_bud
47        gas_status: SuiGasStatus,
48    }
49
50    #[derive(Debug)]
51    pub enum PaymentMethod {
52        Unmetered,
53        Coins(Vec<ObjectRef>),
54        AddressBalance(SuiAddress),
55    }
56
57    impl PaymentMethod {
58        pub fn is_unmetered(&self) -> bool {
59            matches!(self, PaymentMethod::Unmetered)
60        }
61        pub fn is_address_balance(&self) -> bool {
62            matches!(self, PaymentMethod::AddressBalance(_))
63        }
64        pub fn is_coins(&self) -> bool {
65            matches!(self, PaymentMethod::Coins(_))
66        }
67    }
68
69    impl GasCharger {
70        pub fn new(
71            tx_digest: TransactionDigest,
72            payment_method: PaymentMethod,
73            gas_status: SuiGasStatus,
74            protocol_config: &ProtocolConfig,
75        ) -> Self {
76            let gas_model_version = protocol_config.gas_model_version();
77            Self {
78                tx_digest,
79                gas_model_version,
80                payment_method,
81                smashed_gas_coin: None,
82                gas_status,
83            }
84        }
85
86        pub fn new_unmetered(tx_digest: TransactionDigest) -> Self {
87            Self {
88                tx_digest,
89                gas_model_version: 6, // pick any of the latest, it should not matter
90                payment_method: PaymentMethod::Unmetered,
91                smashed_gas_coin: None,
92                gas_status: SuiGasStatus::new_unmetered(),
93            }
94        }
95
96        // TODO: there is only one caller to this function that should not exist otherwise.
97        //       Explore way to remove it.
98        pub(crate) fn gas_coins(&self) -> impl Iterator<Item = &'_ ObjectRef> {
99            match &self.payment_method {
100                PaymentMethod::Coins(gas_coins) => Either::Left(gas_coins.iter()),
101                PaymentMethod::AddressBalance(_) | PaymentMethod::Unmetered => {
102                    Either::Right(std::iter::empty())
103                }
104            }
105        }
106
107        // Return the logical gas coin for this transactions or None if no gas coin was present
108        // (system transactions).
109        pub fn gas_coin(&self) -> Option<ObjectID> {
110            self.smashed_gas_coin
111        }
112
113        pub fn gas_budget(&self) -> u64 {
114            self.gas_status.gas_budget()
115        }
116
117        pub fn unmetered_storage_rebate(&self) -> u64 {
118            self.gas_status.unmetered_storage_rebate()
119        }
120
121        pub fn no_charges(&self) -> bool {
122            self.gas_status.gas_used() == 0
123                && self.gas_status.storage_rebate() == 0
124                && self.gas_status.storage_gas_units() == 0
125        }
126
127        pub fn is_unmetered(&self) -> bool {
128            self.gas_status.is_unmetered()
129        }
130
131        pub fn move_gas_status(&self) -> &GasStatus {
132            self.gas_status.move_gas_status()
133        }
134
135        pub fn move_gas_status_mut(&mut self) -> &mut GasStatus {
136            self.gas_status.move_gas_status_mut()
137        }
138
139        pub fn into_gas_status(self) -> SuiGasStatus {
140            self.gas_status
141        }
142
143        pub fn summary(&self) -> GasCostSummary {
144            self.gas_status.summary()
145        }
146
147        // This function is called when the transaction is about to be executed.
148        // It will smash all gas coins into a single one and set the logical gas coin
149        // to be the first one in the list.
150        // After this call, `gas_coin` will return it id of the gas coin.
151        // This function panics if errors are found while operation on the gas coins.
152        // Transaction and certificate input checks must have insured that all gas coins
153        // are correct.
154        pub fn smash_gas(&mut self, temporary_store: &mut TemporaryStore<'_>) {
155            if let PaymentMethod::Coins(gas_coins) = &mut self.payment_method {
156                let gas_coin_count = gas_coins.len();
157                if gas_coin_count == 0 || (gas_coin_count == 1 && gas_coins[0].0 == ObjectID::ZERO)
158                {
159                    return; // self.smashed_gas_coin is None
160                }
161                // set the first coin to be the transaction only gas coin.
162                // All others will be smashed into this one.
163                let gas_coin_id = gas_coins[0].0;
164                self.smashed_gas_coin = Some(gas_coin_id);
165                if gas_coin_count == 1 {
166                    return;
167                }
168
169                // sum the value of all gas coins
170                let new_balance = gas_coins
171                    .iter()
172                    .map(|obj_ref| {
173                        let obj = temporary_store.objects().get(&obj_ref.0).unwrap();
174                        let Data::Move(move_obj) = &obj.data else {
175                            return Err(ExecutionError::invariant_violation(
176                                "Provided non-gas coin object as input for gas!",
177                            ));
178                        };
179                        if !move_obj.type_().is_gas_coin() {
180                            return Err(ExecutionError::invariant_violation(
181                                "Provided non-gas coin object as input for gas!",
182                            ));
183                        }
184                        Ok(move_obj.get_coin_value_unsafe())
185                    })
186                    .collect::<Result<Vec<u64>, ExecutionError>>()
187                    // transaction and certificate input checks must have insured that all gas coins
188                    // are valid
189                    .unwrap_or_else(|_| {
190                        panic!(
191                            "Invariant violation: non-gas coin object as input for gas in txn {}",
192                            self.tx_digest
193                        )
194                    })
195                    .iter()
196                    .sum();
197                let mut primary_gas_object = temporary_store
198                    .objects()
199                    .get(&gas_coin_id)
200                    // unwrap should be safe because we checked that this exists in `self.objects()` above
201                    .unwrap_or_else(|| {
202                        panic!(
203                            "Invariant violation: gas coin not found in store in txn {}",
204                            self.tx_digest
205                        )
206                    })
207                    .clone();
208                // delete all gas objects except the primary_gas_object
209                for (id, _version, _digest) in &gas_coins[1..] {
210                    debug_assert_ne!(*id, primary_gas_object.id());
211                    temporary_store.delete_input_object(id);
212                }
213                primary_gas_object
214                    .data
215                    .try_as_move_mut()
216                    // unwrap should be safe because we checked that the primary gas object was a coin object above.
217                    .unwrap_or_else(|| {
218                        panic!(
219                            "Invariant violation: invalid coin object in txn {}",
220                            self.tx_digest
221                        )
222                    })
223                    .set_coin_value_unsafe(new_balance);
224                temporary_store.mutate_input_object(primary_gas_object);
225            }
226        }
227
228        //
229        // Gas charging operations
230        //
231
232        pub fn track_storage_mutation(
233            &mut self,
234            object_id: ObjectID,
235            new_size: usize,
236            storage_rebate: u64,
237        ) -> u64 {
238            self.gas_status
239                .track_storage_mutation(object_id, new_size, storage_rebate)
240        }
241
242        pub fn reset_storage_cost_and_rebate(&mut self) {
243            self.gas_status.reset_storage_cost_and_rebate();
244        }
245
246        pub fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
247            self.gas_status.charge_publish_package(size)
248        }
249
250        pub fn charge_upgrade_package(&mut self, size: usize) -> Result<(), ExecutionError> {
251            if charge_upgrades(self.gas_model_version) {
252                self.gas_status.charge_publish_package(size)
253            } else {
254                Ok(())
255            }
256        }
257
258        pub fn charge_input_objects(
259            &mut self,
260            temporary_store: &TemporaryStore<'_>,
261        ) -> Result<(), ExecutionError> {
262            let objects = temporary_store.objects();
263            // TODO: Charge input object count.
264            let _object_count = objects.len();
265            // Charge bytes read
266            let total_size = temporary_store
267                .objects()
268                .iter()
269                // don't charge for loading Sui Framework or Move stdlib
270                .filter(|(id, _)| !is_system_package(**id))
271                .map(|(_, obj)| obj.object_size_for_gas_metering())
272                .sum();
273            self.gas_status.charge_storage_read(total_size)
274        }
275
276        pub fn charge_coin_transfers(
277            &mut self,
278            protocol_config: &ProtocolConfig,
279            num_non_gas_coin_owners: u64,
280        ) -> Result<(), ExecutionError> {
281            // times two for the global pause and per-address settings
282            // this "overcharges" slightly since it does not check the global pause for each owner
283            // but rather each coin type.
284            let bytes_read_per_owner = CONFIG_SETTING_DYNAMIC_FIELD_SIZE_FOR_GAS;
285            // associate the cost with dynamic field access so that it will increase if/when this
286            // cost increases
287            let cost_per_byte =
288                protocol_config.dynamic_field_borrow_child_object_type_cost_per_byte() as usize;
289            let cost_per_owner = bytes_read_per_owner * cost_per_byte;
290            let owner_cost = cost_per_owner * (num_non_gas_coin_owners as usize);
291            self.gas_status.charge_storage_read(owner_cost)
292        }
293
294        /// Resets any mutations, deletions, and events recorded in the store, as well as any storage costs and
295        /// rebates, then Re-runs gas smashing. Effects on store are now as if we were about to begin execution
296        pub fn reset(&mut self, temporary_store: &mut TemporaryStore<'_>) {
297            temporary_store.drop_writes();
298            self.gas_status.reset_storage_cost_and_rebate();
299            self.smash_gas(temporary_store);
300        }
301
302        /// Entry point for gas charging.
303        /// 1. Compute tx storage gas costs and tx storage rebates, update storage_rebate field of
304        /// mutated objects
305        /// 2. Deduct computation gas costs and storage costs, credit storage rebates.
306        /// The happy path of this function follows (1) + (2) and is fairly simple.
307        /// Most of the complexity is in the unhappy paths:
308        /// - if execution aborted before calling this function, we have to dump all writes +
309        ///   re-smash gas, then charge for storage
310        /// - if we run out of gas while charging for storage, we have to dump all writes +
311        ///   re-smash gas, then charge for storage again
312        pub fn charge_gas<T>(
313            &mut self,
314            temporary_store: &mut TemporaryStore<'_>,
315            execution_result: &mut Result<T, ExecutionError>,
316        ) -> GasCostSummary {
317            // at this point, we have done *all* charging for computation,
318            // but have not yet set the storage rebate or storage gas units
319            debug_assert!(self.gas_status.storage_rebate() == 0);
320            debug_assert!(self.gas_status.storage_gas_units() == 0);
321
322            if self.smashed_gas_coin.is_some() || self.payment_method.is_address_balance() {
323                // bucketize computation cost
324                let is_move_abort = execution_result
325                    .as_ref()
326                    .err()
327                    .map(|err| {
328                        matches!(
329                            err.kind(),
330                            sui_types::execution_status::ExecutionFailureStatus::MoveAbort(_, _)
331                        )
332                    })
333                    .unwrap_or(false);
334                // bucketize computation cost
335                if let Err(err) = self.gas_status.bucketize_computation(Some(is_move_abort))
336                    && execution_result.is_ok()
337                {
338                    *execution_result = Err(err);
339                }
340
341                // On error we need to dump writes, deletes, etc before charging storage gas
342                if execution_result.is_err() {
343                    self.reset(temporary_store);
344                }
345            }
346
347            // compute and collect storage charges
348            temporary_store.ensure_active_inputs_mutated();
349            temporary_store.collect_storage_and_rebate(self);
350
351            if self.smashed_gas_coin.is_some() {
352                #[skip_checked_arithmetic]
353                trace!(target: "replay_gas_info", "Gas smashing has occurred for this transaction");
354            }
355
356            if self.payment_method.is_unmetered() {
357                return GasCostSummary::default();
358            }
359
360            if execution_result
361                .as_ref()
362                .err()
363                .map(|err| {
364                    matches!(
365                        err.kind(),
366                        sui_types::execution_status::ExecutionFailureStatus::InsufficientFundsForWithdraw
367                    )
368                })
369                .unwrap_or(false)
370                && self.payment_method.is_address_balance() {
371                    // If we don't have enough balance to withdraw, don't charge for gas
372                    // TODO: consider charging gas if we have enough to reserve but not enough to cover all withdraws
373                    return GasCostSummary::default();
374            }
375
376            self.compute_storage_and_rebate(temporary_store, execution_result);
377            let cost_summary = self.gas_status.summary();
378            let net_change = cost_summary.net_gas_usage();
379
380            match self.payment_method {
381                PaymentMethod::AddressBalance(payer_address) => {
382                    if net_change != 0 {
383                        let balance_type = sui_types::balance::Balance::type_tag(
384                            sui_types::gas_coin::GAS::type_tag(),
385                        );
386                        let accumulator_event = AccumulatorEvent::from_balance_change(
387                            payer_address,
388                            balance_type,
389                            net_change,
390                        )
391                        .expect("Failed to create accumulator event for gas balance");
392
393                        temporary_store.add_accumulator_event(accumulator_event);
394                    }
395
396                    cost_summary
397                }
398                PaymentMethod::Coins(_) => {
399                    let gas_object_id = self.smashed_gas_coin.unwrap();
400
401                    let mut gas_object =
402                        temporary_store.read_object(&gas_object_id).unwrap().clone();
403                    deduct_gas(&mut gas_object, net_change);
404                    #[skip_checked_arithmetic]
405                    trace!(net_change, gas_obj_id =? gas_object.id(), gas_obj_ver =? gas_object.version(), "Updated gas object");
406
407                    temporary_store.mutate_input_object(gas_object);
408                    cost_summary
409                }
410                PaymentMethod::Unmetered => unreachable!(),
411            }
412        }
413
414        /// Calculate total gas cost considering storage and rebate.
415        ///
416        /// First, we net computation, storage, and rebate to determine total gas to charge.
417        ///
418        /// If we exceed gas_budget, we set execution_result to InsufficientGas, failing the tx.
419        /// If we have InsufficientGas, we determine how much gas to charge for the failed tx:
420        ///
421        /// v1: we set computation_cost = gas_budget, so we charge net (gas_budget - storage_rebates)
422        /// v2: we charge (computation + storage costs for input objects - storage_rebates)
423        ///     if the gas balance is still insufficient, we fall back to set computation_cost = gas_budget
424        ///     so we charge net (gas_budget - storage_rebates)
425        fn compute_storage_and_rebate<T>(
426            &mut self,
427            temporary_store: &mut TemporaryStore<'_>,
428            execution_result: &mut Result<T, ExecutionError>,
429        ) {
430            if dont_charge_budget_on_storage_oog(self.gas_model_version) {
431                self.handle_storage_and_rebate_v2(temporary_store, execution_result)
432            } else {
433                self.handle_storage_and_rebate_v1(temporary_store, execution_result)
434            }
435        }
436
437        fn handle_storage_and_rebate_v1<T>(
438            &mut self,
439            temporary_store: &mut TemporaryStore<'_>,
440            execution_result: &mut Result<T, ExecutionError>,
441        ) {
442            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
443                self.reset(temporary_store);
444                self.gas_status.adjust_computation_on_out_of_gas();
445                temporary_store.ensure_active_inputs_mutated();
446                temporary_store.collect_rebate(self);
447                if execution_result.is_ok() {
448                    *execution_result = Err(err);
449                }
450            }
451        }
452
453        fn handle_storage_and_rebate_v2<T>(
454            &mut self,
455            temporary_store: &mut TemporaryStore<'_>,
456            execution_result: &mut Result<T, ExecutionError>,
457        ) {
458            if let Err(err) = self.gas_status.charge_storage_and_rebate() {
459                // we run out of gas charging storage, reset and try charging for storage again.
460                // Input objects are touched and so they have a storage cost
461                // Attempt to charge just for computation + input object storage costs - storage_rebate
462                self.reset(temporary_store);
463                temporary_store.ensure_active_inputs_mutated();
464                temporary_store.collect_storage_and_rebate(self);
465                if let Err(err) = self.gas_status.charge_storage_and_rebate() {
466                    // we run out of gas attempting to charge for the input objects exclusively,
467                    // deal with this edge case by not charging for storage: we charge (gas_budget - rebates).
468                    self.reset(temporary_store);
469                    self.gas_status.adjust_computation_on_out_of_gas();
470                    temporary_store.ensure_active_inputs_mutated();
471                    temporary_store.collect_rebate(self);
472                    if execution_result.is_ok() {
473                        *execution_result = Err(err);
474                    }
475                } else if execution_result.is_ok() {
476                    *execution_result = Err(err);
477                }
478            }
479        }
480    }
481}