iota_types/
gas.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 enum_dispatch::enum_dispatch;
12    use iota_protocol_config::ProtocolConfig;
13    use itertools::MultiUnzip;
14    use schemars::JsonSchema;
15    use serde::{Deserialize, Serialize};
16    use serde_with::serde_as;
17
18    use crate::{
19        ObjectID,
20        effects::{TransactionEffects, TransactionEffectsAPI},
21        error::{ExecutionError, IotaResult, UserInputError, UserInputResult},
22        gas_model::{gas_v1::IotaGasStatus as IotaGasStatusV1, tables::GasStatus},
23        iota_serde::{BigInt, Readable},
24        object::Object,
25        transaction::ObjectReadResult,
26    };
27
28    #[enum_dispatch]
29    pub trait IotaGasStatusAPI {
30        fn is_unmetered(&self) -> bool;
31        fn move_gas_status(&self) -> &GasStatus;
32        fn move_gas_status_mut(&mut self) -> &mut GasStatus;
33        fn bucketize_computation(&mut self) -> Result<(), ExecutionError>;
34        fn summary(&self) -> GasCostSummary;
35        fn gas_budget(&self) -> u64;
36        fn gas_price(&self) -> u64;
37        fn reference_gas_price(&self) -> u64;
38        fn storage_gas_units(&self) -> u64;
39        fn storage_rebate(&self) -> u64;
40        fn unmetered_storage_rebate(&self) -> u64;
41        fn gas_used(&self) -> u64;
42        fn reset_storage_cost_and_rebate(&mut self);
43        fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError>;
44        fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError>;
45        fn track_storage_mutation(
46            &mut self,
47            object_id: ObjectID,
48            new_size: usize,
49            storage_rebate: u64,
50        ) -> u64;
51        fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError>;
52        fn adjust_computation_on_out_of_gas(&mut self);
53    }
54
55    /// Version aware enum for gas status.
56    #[enum_dispatch(IotaGasStatusAPI)]
57    #[derive(Debug)]
58    pub enum IotaGasStatus {
59        V1(IotaGasStatusV1),
60    }
61
62    impl IotaGasStatus {
63        pub fn new(
64            gas_budget: u64,
65            gas_price: u64,
66            reference_gas_price: u64,
67            config: &ProtocolConfig,
68        ) -> IotaResult<Self> {
69            Self::check_gas_preconditions(gas_price, reference_gas_price, config)?;
70
71            Ok(Self::V1(IotaGasStatusV1::new_with_budget(
72                gas_budget,
73                gas_price,
74                reference_gas_price,
75                config,
76            )))
77        }
78
79        pub fn new_unmetered() -> Self {
80            // Always return V1 as unmetered gas status is identical from V1 to V2.
81            // This is only used for system transactions which do not pay gas.
82            Self::V1(IotaGasStatusV1::new_unmetered())
83        }
84
85        // This is the only public API on IotaGasStatus, all other gas related
86        // operations should go through `GasCharger`
87        pub fn check_gas_balance(
88            &self,
89            gas_objs: &[&ObjectReadResult],
90            gas_budget: u64,
91        ) -> UserInputResult {
92            match self {
93                Self::V1(status) => status.check_gas_balance(gas_objs, gas_budget),
94            }
95        }
96
97        fn check_gas_preconditions(
98            gas_price: u64,
99            reference_gas_price: u64,
100            config: &ProtocolConfig,
101        ) -> IotaResult<()> {
102            // Common checks. We may pull them into version specific status as needed, but
103            // they are unlikely to change.
104
105            // The gas price must be greater than or equal to the reference gas price.
106            if gas_price < reference_gas_price {
107                return Err(UserInputError::GasPriceUnderRGP {
108                    gas_price,
109                    reference_gas_price,
110                }
111                .into());
112            }
113            if gas_price > config.max_gas_price() {
114                return Err(UserInputError::GasPriceTooHigh {
115                    max_gas_price: config.max_gas_price(),
116                }
117                .into());
118            }
119
120            Ok(())
121        }
122    }
123
124    /// Summary of the charges in a transaction.
125    /// Storage is charged independently of computation.
126    /// There are 3 parts to the storage charges:
127    /// `storage_cost`: it is the charge of storage at the time the transaction
128    /// is executed.                 The cost of storage is the number of
129    /// bytes of the objects being mutated                 multiplied by a
130    /// variable storage cost per byte `storage_rebate`: this is the amount
131    /// a user gets back when manipulating an object.                   The
132    /// `storage_rebate` is the `storage_cost` for an object minus fees.
133    /// `non_refundable_storage_fee`: not all the value of the object storage
134    /// cost is                               given back to user and there
135    /// is a small fraction that                               is kept by
136    /// the system. This value tracks that charge.
137    ///
138    /// When looking at a gas cost summary the amount charged to the user is
139    /// `computation_cost + storage_cost - storage_rebate`
140    /// and that is the amount that is deducted from the gas coins.
141    /// `non_refundable_storage_fee` is collected from the objects being
142    /// mutated/deleted and it is tracked by the system in storage funds.
143    ///
144    /// Objects deleted, including the older versions of objects mutated, have
145    /// the storage field on the objects added up to a pool of "potential
146    /// rebate". This rebate then is reduced by the "nonrefundable rate"
147    /// such that: `potential_rebate(storage cost of deleted/mutated
148    /// objects) = storage_rebate + non_refundable_storage_fee`
149
150    #[serde_as]
151    #[derive(Eq, PartialEq, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
152    #[serde(rename_all = "camelCase")]
153    pub struct GasCostSummary {
154        /// Cost of computation/execution
155        #[schemars(with = "BigInt<u64>")]
156        #[serde_as(as = "Readable<BigInt<u64>, _>")]
157        pub computation_cost: u64,
158        /// The burned component of the computation/execution costs
159        #[schemars(with = "BigInt<u64>")]
160        #[serde_as(as = "Readable<BigInt<u64>, _>")]
161        pub computation_cost_burned: u64,
162        /// Storage cost, it's the sum of all storage cost for all objects
163        /// created or mutated.
164        #[schemars(with = "BigInt<u64>")]
165        #[serde_as(as = "Readable<BigInt<u64>, _>")]
166        pub storage_cost: u64,
167        /// The amount of storage cost refunded to the user for all objects
168        /// deleted or mutated in the transaction.
169        #[schemars(with = "BigInt<u64>")]
170        #[serde_as(as = "Readable<BigInt<u64>, _>")]
171        pub storage_rebate: u64,
172        /// The fee for the rebate. The portion of the storage rebate kept by
173        /// the system.
174        #[schemars(with = "BigInt<u64>")]
175        #[serde_as(as = "Readable<BigInt<u64>, _>")]
176        pub non_refundable_storage_fee: u64,
177    }
178
179    impl GasCostSummary {
180        pub fn new(
181            computation_cost: u64,
182            computation_cost_burned: u64,
183            storage_cost: u64,
184            storage_rebate: u64,
185            non_refundable_storage_fee: u64,
186        ) -> GasCostSummary {
187            GasCostSummary {
188                computation_cost,
189                computation_cost_burned,
190                storage_cost,
191                storage_rebate,
192                non_refundable_storage_fee,
193            }
194        }
195
196        pub fn gas_used(&self) -> u64 {
197            self.computation_cost + self.storage_cost
198        }
199
200        /// Portion of the storage rebate that gets passed on to the transaction
201        /// sender. The remainder will be burned, then re-minted + added
202        /// to the storage fund at the next epoch change
203        pub fn sender_rebate(&self, storage_rebate_rate: u64) -> u64 {
204            // we round storage rebate such that `>= x.5` goes to x+1 (rounds up) and
205            // `< x.5` goes to x (truncates). We replicate `f32/64::round()`
206            const BASIS_POINTS: u128 = 10000;
207            (((self.storage_rebate as u128 * storage_rebate_rate as u128)
208            + (BASIS_POINTS / 2)) // integer rounding adds half of the BASIS_POINTS (denominator)
209            / BASIS_POINTS) as u64
210        }
211
212        /// Get net gas usage, positive number means used gas; negative number
213        /// means refund.
214        pub fn net_gas_usage(&self) -> i64 {
215            self.gas_used() as i64 - self.storage_rebate as i64
216        }
217
218        #[expect(clippy::type_complexity)]
219        pub fn new_from_txn_effects<'a>(
220            transactions: impl Iterator<Item = &'a TransactionEffects>,
221        ) -> GasCostSummary {
222            let (
223                storage_costs,
224                computation_costs,
225                computation_costs_burned,
226                storage_rebates,
227                non_refundable_storage_fee,
228            ): (Vec<u64>, Vec<u64>, Vec<u64>, Vec<u64>, Vec<u64>) = transactions
229                .map(|e| {
230                    (
231                        e.gas_cost_summary().storage_cost,
232                        e.gas_cost_summary().computation_cost,
233                        e.gas_cost_summary().computation_cost_burned,
234                        e.gas_cost_summary().storage_rebate,
235                        e.gas_cost_summary().non_refundable_storage_fee,
236                    )
237                })
238                .multiunzip();
239
240            GasCostSummary {
241                storage_cost: storage_costs.iter().sum(),
242                computation_cost: computation_costs.iter().sum(),
243                computation_cost_burned: computation_costs_burned.iter().sum(),
244                storage_rebate: storage_rebates.iter().sum(),
245                non_refundable_storage_fee: non_refundable_storage_fee.iter().sum(),
246            }
247        }
248    }
249
250    impl std::fmt::Display for GasCostSummary {
251        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252            write!(
253                f,
254                "computation_cost: {}, computation_cost_burned: {}, storage_cost: {},  storage_rebate: {}, non_refundable_storage_fee: {}",
255                self.computation_cost,
256                self.computation_cost_burned,
257                self.storage_cost,
258                self.storage_rebate,
259                self.non_refundable_storage_fee,
260            )
261        }
262    }
263
264    impl std::ops::AddAssign<&Self> for GasCostSummary {
265        fn add_assign(&mut self, other: &Self) {
266            self.computation_cost += other.computation_cost;
267            self.computation_cost_burned += other.computation_cost_burned;
268            self.storage_cost += other.storage_cost;
269            self.storage_rebate += other.storage_rebate;
270            self.non_refundable_storage_fee += other.non_refundable_storage_fee;
271        }
272    }
273
274    impl std::ops::AddAssign<Self> for GasCostSummary {
275        fn add_assign(&mut self, other: Self) {
276            self.add_assign(&other)
277        }
278    }
279
280    // Helper functions to deal with gas coins operations.
281    //
282
283    pub fn deduct_gas(gas_object: &mut Object, charge_or_rebate: i64) {
284        // The object must be a gas coin as we have checked in transaction handle phase.
285        let gas_coin = gas_object.data.try_as_move_mut().unwrap();
286        let balance = gas_coin.get_coin_value_unsafe();
287        let new_balance = if charge_or_rebate < 0 {
288            balance + (-charge_or_rebate as u64)
289        } else {
290            assert!(balance >= charge_or_rebate as u64);
291            balance - charge_or_rebate as u64
292        };
293        gas_coin.set_coin_value_unsafe(new_balance)
294    }
295
296    pub fn get_gas_balance(gas_object: &Object) -> UserInputResult<u64> {
297        if let Some(move_obj) = gas_object.data.try_as_move() {
298            if !move_obj.type_().is_gas_coin() {
299                return Err(UserInputError::InvalidGasObject {
300                    object_id: gas_object.id(),
301                });
302            }
303            Ok(move_obj.get_coin_value_unsafe())
304        } else {
305            Err(UserInputError::InvalidGasObject {
306                object_id: gas_object.id(),
307            })
308        }
309    }
310}