iota_adapter_latest/
gas_charger.rs

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