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