Skip to main content

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