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