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