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