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