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        /// Charge reading a package input. Earlier gas models charge packages as other inputs.
84        pub fn charge_package_object_read(&mut self, size: usize) -> Result<(), ExecutionError> {
85            match self {
86                Self::V2(status) => status.charge_storage_read(size),
87                Self::V3(status) => status.charge_package_object_read(size),
88            }
89        }
90
91        pub fn new(
92            gas_budget: u64,
93            gas_price: u64,
94            reference_gas_price: u64,
95            config: &ProtocolConfig,
96        ) -> SuiResult<Self> {
97            // Common checks. We may pull them into version specific status as needed, but they
98            // are unlikely to change.
99
100            // gas price must be bigger or equal to reference gas price
101            if gas_price < reference_gas_price {
102                return Err(UserInputError::GasPriceUnderRGP {
103                    gas_price,
104                    reference_gas_price,
105                }
106                .into());
107            }
108            if check_for_gas_price_too_high(config.gas_model_version())
109                && gas_price >= config.max_gas_price()
110            {
111                return Err(UserInputError::GasPriceTooHigh {
112                    max_gas_price: config.max_gas_price(),
113                }
114                .into());
115            }
116
117            // Dispatch by gas model version: v15+ uses the clean gas_v3 pipeline;
118            // everything older keeps the legacy gas_v2 path so replay determinism holds.
119            if config.gas_model_version() >= 15 {
120                Ok(Self::V3(SuiGasStatusV3::new_with_budget(
121                    gas_budget,
122                    gas_price,
123                    reference_gas_price,
124                    config,
125                )))
126            } else {
127                Ok(Self::V2(SuiGasStatusV2::new_with_budget(
128                    gas_budget,
129                    gas_price,
130                    reference_gas_price,
131                    config,
132                )))
133            }
134        }
135
136        pub fn new_unmetered(config: &ProtocolConfig) -> Self {
137            // Same dispatch as `new`: v15+ uses gas_v3, everything older keeps gas_v2 so
138            // replay determinism holds.
139            if config.gas_model_version() >= 15 {
140                Self::V3(SuiGasStatusV3::new_unmetered())
141            } else {
142                Self::V2(SuiGasStatusV2::new_unmetered())
143            }
144        }
145    }
146
147    /// Summary of the charges in a transaction.
148    /// Storage is charged independently of computation.
149    /// There are 3 parts to the storage charges:
150    /// `storage_cost`: it is the charge of storage at the time the transaction is executed.
151    ///                 The cost of storage is the number of bytes of the objects being mutated
152    ///                 multiplied by a variable storage cost per byte
153    /// `storage_rebate`: this is the amount a user gets back when manipulating an object.
154    ///                   The `storage_rebate` is the `storage_cost` for an object minus fees.
155    /// `non_refundable_storage_fee`: not all the value of the object storage cost is
156    ///                               given back to user and there is a small fraction that
157    ///                               is kept by the system. This value tracks that charge.
158    ///
159    /// When looking at a gas cost summary the amount charged to the user is
160    /// `computation_cost + storage_cost - storage_rebate`
161    /// and that is the amount that is deducted from the gas coins.
162    /// `non_refundable_storage_fee` is collected from the objects being mutated/deleted
163    /// and it is tracked by the system in storage funds.
164    ///
165    /// Objects deleted, including the older versions of objects mutated, have the storage field
166    /// on the objects added up to a pool of "potential rebate". This rebate then is reduced
167    /// by the "nonrefundable rate" such that:
168    /// `potential_rebate(storage cost of deleted/mutated objects) =
169    /// storage_rebate + non_refundable_storage_fee`
170
171    #[serde_as]
172    #[derive(Eq, PartialEq, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
173    #[serde(rename_all = "camelCase")]
174    pub struct GasCostSummary {
175        /// Cost of computation/execution
176        #[schemars(with = "BigInt<u64>")]
177        #[serde_as(as = "Readable<BigInt<u64>, _>")]
178        pub computation_cost: u64,
179        /// Storage cost, it's the sum of all storage cost for all objects created or mutated.
180        #[schemars(with = "BigInt<u64>")]
181        #[serde_as(as = "Readable<BigInt<u64>, _>")]
182        pub storage_cost: u64,
183        /// The amount of storage cost refunded to the user for all objects deleted or mutated in the
184        /// transaction.
185        #[schemars(with = "BigInt<u64>")]
186        #[serde_as(as = "Readable<BigInt<u64>, _>")]
187        pub storage_rebate: u64,
188        /// The fee for the rebate. The portion of the storage rebate kept by the system.
189        #[schemars(with = "BigInt<u64>")]
190        #[serde_as(as = "Readable<BigInt<u64>, _>")]
191        pub non_refundable_storage_fee: u64,
192    }
193
194    impl GasCostSummary {
195        pub fn new(
196            computation_cost: u64,
197            storage_cost: u64,
198            storage_rebate: u64,
199            non_refundable_storage_fee: u64,
200        ) -> GasCostSummary {
201            GasCostSummary {
202                computation_cost,
203                storage_cost,
204                storage_rebate,
205                non_refundable_storage_fee,
206            }
207        }
208
209        pub fn gas_used(&self) -> u64 {
210            self.computation_cost + self.storage_cost
211        }
212
213        /// Portion of the storage rebate that gets passed on to the transaction sender. The remainder
214        /// will be burned, then re-minted + added to the storage fund at the next epoch change
215        pub fn sender_rebate(&self, storage_rebate_rate: u64) -> u64 {
216            // we round storage rebate such that `>= x.5` goes to x+1 (rounds up) and
217            // `< x.5` goes to x (truncates). We replicate `f32/64::round()`
218            const BASIS_POINTS: u128 = 10000;
219            (((self.storage_rebate as u128 * storage_rebate_rate as u128)
220            + (BASIS_POINTS / 2)) // integer rounding adds half of the BASIS_POINTS (denominator)
221            / BASIS_POINTS) as u64
222        }
223
224        /// Get net gas usage, positive number means used gas; negative number means refund.
225        pub fn net_gas_usage(&self) -> i64 {
226            self.gas_used() as i64 - self.storage_rebate as i64
227        }
228
229        pub fn new_from_txn_effects<'a>(
230            transactions: impl Iterator<Item = &'a TransactionEffects>,
231        ) -> GasCostSummary {
232            let (storage_costs, computation_costs, storage_rebates, non_refundable_storage_fee): (
233                Vec<u64>,
234                Vec<u64>,
235                Vec<u64>,
236                Vec<u64>,
237            ) = transactions
238                .map(|e| {
239                    (
240                        e.gas_cost_summary().storage_cost,
241                        e.gas_cost_summary().computation_cost,
242                        e.gas_cost_summary().storage_rebate,
243                        e.gas_cost_summary().non_refundable_storage_fee,
244                    )
245                })
246                .multiunzip();
247
248            GasCostSummary {
249                storage_cost: storage_costs.iter().sum(),
250                computation_cost: computation_costs.iter().sum(),
251                storage_rebate: storage_rebates.iter().sum(),
252                non_refundable_storage_fee: non_refundable_storage_fee.iter().sum(),
253            }
254        }
255    }
256
257    impl std::fmt::Display for GasCostSummary {
258        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259            write!(
260                f,
261                "computation_cost: {}, storage_cost: {},  storage_rebate: {}, non_refundable_storage_fee: {}",
262                self.computation_cost,
263                self.storage_cost,
264                self.storage_rebate,
265                self.non_refundable_storage_fee,
266            )
267        }
268    }
269
270    impl std::ops::AddAssign<&Self> for GasCostSummary {
271        fn add_assign(&mut self, other: &Self) {
272            self.computation_cost += other.computation_cost;
273            self.storage_cost += other.storage_cost;
274            self.storage_rebate += other.storage_rebate;
275            self.non_refundable_storage_fee += other.non_refundable_storage_fee;
276        }
277    }
278
279    impl std::ops::AddAssign<Self> for GasCostSummary {
280        fn add_assign(&mut self, other: Self) {
281            self.add_assign(&other)
282        }
283    }
284
285    //
286    // Helper functions to deal with gas coins operations.
287    //
288
289    pub fn deduct_gas(gas_object: &mut Object, charge_or_rebate: i64) {
290        // The object must be a gas coin as we have checked in transaction handle phase.
291        let gas_coin = gas_object.data.try_as_move_mut().unwrap();
292        let balance = gas_coin.get_coin_value_unsafe();
293        let new_balance = if charge_or_rebate < 0 {
294            balance + (-charge_or_rebate as u64)
295        } else {
296            assert!(balance >= charge_or_rebate as u64);
297            balance - charge_or_rebate as u64
298        };
299        gas_coin.set_coin_value_unsafe(new_balance)
300    }
301
302    pub fn get_gas_balance(gas_object: &Object) -> UserInputResult<u64> {
303        if let Some(move_obj) = gas_object.data.try_as_move() {
304            if !move_obj.type_().is_gas_coin() {
305                return Err(UserInputError::InvalidGasObject {
306                    object_id: gas_object.id(),
307                });
308            }
309            Ok(move_obj.get_coin_value_unsafe())
310        } else {
311            Err(UserInputError::InvalidGasObject {
312                object_id: gas_object.id(),
313            })
314        }
315    }
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct GasUsageReport {
320    pub cost_summary: GasCostSummary,
321    pub gas_used: u64,
322    pub gas_budget: u64,
323    pub gas_price: u64,
324    pub reference_gas_price: u64,
325    pub storage_gas_price: u64,
326    pub rebate_rate: u64,
327    pub per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
328}