Skip to main content

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