Skip to main content

sui_types/gas_model/
gas_v2.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::*;
6
7#[sui_macros::with_checked_arithmetic]
8mod checked {
9    use crate::error::UserInputResult;
10    use crate::gas::{GasCostSummary, GasUsageReport, SuiGasStatusAPI};
11    pub use crate::gas_model::gas_common::PerObjectStorage;
12    use crate::gas_model::gas_common::{
13        StorageGas, check_gas_data, check_gas_objects, half_digits_rounding, sender_rebate,
14    };
15    use crate::gas_model::gas_predicates::{cost_table_for_version, txn_base_cost_as_multiplier};
16    use crate::gas_model::units_types::CostTable;
17    use crate::transaction::ObjectReadResult;
18    use crate::{
19        ObjectID,
20        error::ExecutionError,
21        execution_status::ExecutionErrorKind,
22        gas_model::tables::{GasStatus, ZERO_COST_SCHEDULE},
23    };
24    use move_core_types::vm_status::StatusCode;
25    use sui_protocol_config::*;
26
27    /// A bucket defines a range of units that will be priced the same.
28    /// After execution a call to `GasStatus::bucketize` will round the computation
29    /// cost to `cost` for the bucket ([`min`, `max`]) the gas used falls into.
30    #[allow(dead_code)]
31    pub(crate) struct ComputationBucket {
32        min: u64,
33        max: u64,
34        cost: u64,
35    }
36
37    impl ComputationBucket {
38        fn new(min: u64, max: u64, cost: u64) -> Self {
39            ComputationBucket { min, max, cost }
40        }
41
42        fn simple(min: u64, max: u64) -> Self {
43            Self::new(min, max, max)
44        }
45    }
46
47    fn get_bucket_cost(table: &[ComputationBucket], computation_cost: u64) -> u64 {
48        for bucket in table {
49            if bucket.max >= computation_cost {
50                return bucket.cost;
51            }
52        }
53        match table.last() {
54            // maybe not a literal here could be better?
55            None => 5_000_000,
56            Some(bucket) => bucket.cost,
57        }
58    }
59
60    // define the bucket table for computation charging
61    // If versioning defines multiple functions and
62    fn computation_bucket(max_bucket_cost: u64) -> Vec<ComputationBucket> {
63        assert!(max_bucket_cost >= 5_000_000);
64        vec![
65            ComputationBucket::simple(0, 1_000),
66            ComputationBucket::simple(1_000, 5_000),
67            ComputationBucket::simple(5_000, 10_000),
68            ComputationBucket::simple(10_000, 20_000),
69            ComputationBucket::simple(20_000, 50_000),
70            ComputationBucket::simple(50_000, 200_000),
71            ComputationBucket::simple(200_000, 1_000_000),
72            ComputationBucket::simple(1_000_000, max_bucket_cost),
73        ]
74    }
75
76    /// A list of constant costs of various operations in Sui.
77    pub struct SuiCostTable {
78        /// A flat fee charged for every transaction. This is also the minimum amount of
79        /// gas charged for a transaction.
80        pub(crate) min_transaction_cost: u64,
81        /// Maximum allowable budget for a transaction.
82        pub(crate) max_gas_budget: u64,
83        /// Computation cost per byte charged for package publish. This cost is primarily
84        /// determined by the cost to verify and link a package. Note that this does not
85        /// include the cost of writing the package to the store.
86        package_publish_per_byte_cost: u64,
87        /// Per byte cost to read objects from the store. This is computation cost instead of
88        /// storage cost because it does not change the amount of data stored on the db.
89        object_read_per_byte_cost: u64,
90        /// Unit cost of a byte in the storage. This will be used both for charging for
91        /// new storage as well as rebating for deleting storage. That is, we expect users to
92        /// get full refund on the object storage when it's deleted.
93        storage_per_byte_cost: u64,
94        /// Execution cost table to be used.
95        pub execution_cost_table: CostTable,
96        /// Computation buckets to cost transaction in price groups
97        computation_bucket: Vec<ComputationBucket>,
98        /// Max gas price for aborted transactions.
99        max_gas_price_rgp_factor_for_aborted_transactions: Option<u64>,
100    }
101
102    impl std::fmt::Debug for SuiCostTable {
103        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104            // TODO: dump the fields.
105            write!(f, "SuiCostTable(...)")
106        }
107    }
108
109    impl SuiCostTable {
110        pub(crate) fn new(c: &ProtocolConfig, gas_price: u64) -> Self {
111            // gas_price here is the Reference Gas Price, however we may decide
112            // to change it to be the price passed in the transaction
113            let min_transaction_cost = if txn_base_cost_as_multiplier(c) {
114                c.base_tx_cost_fixed() * gas_price
115            } else {
116                c.base_tx_cost_fixed()
117            };
118            Self {
119                min_transaction_cost,
120                max_gas_budget: c.max_tx_gas(),
121                package_publish_per_byte_cost: c.package_publish_cost_per_byte(),
122                object_read_per_byte_cost: c.obj_access_cost_read_per_byte(),
123                storage_per_byte_cost: c.obj_data_cost_refundable(),
124                execution_cost_table: cost_table_for_version(c.gas_model_version()),
125                computation_bucket: computation_bucket(c.max_gas_computation_bucket()),
126                max_gas_price_rgp_factor_for_aborted_transactions: c
127                    .max_gas_price_rgp_factor_for_aborted_transactions_as_option(),
128            }
129        }
130
131        pub(crate) fn unmetered() -> Self {
132            Self {
133                min_transaction_cost: 0,
134                max_gas_budget: u64::MAX,
135                package_publish_per_byte_cost: 0,
136                object_read_per_byte_cost: 0,
137                storage_per_byte_cost: 0,
138                execution_cost_table: ZERO_COST_SCHEDULE.clone(),
139                // should not matter
140                computation_bucket: computation_bucket(5_000_000),
141                max_gas_price_rgp_factor_for_aborted_transactions: None,
142            }
143        }
144    }
145
146    #[derive(Debug, Clone, Copy)]
147    enum GasRoundingMode {
148        /// Bucketize the computation cost according to predefined buckets.
149        Bucketize,
150        /// Rounding value to round up gas charges.
151        Stepped(u64),
152        /// Round by keeping just over half digits
153        KeepHalfDigits,
154    }
155
156    #[allow(dead_code)]
157    #[derive(Debug)]
158    pub struct SuiGasStatus {
159        // GasStatus as used by the VM, that is all the VM sees
160        pub gas_status: GasStatus,
161        // Cost table contains a set of constant/config for the gas model/charging
162        cost_table: SuiCostTable,
163        // Gas budget for this gas status instance.
164        // Typically the gas budget as defined in the `TransactionData::GasData`
165        gas_budget: u64,
166        // Computation cost after execution. This is the result of the gas used by the `GasStatus`
167        // properly bucketized.
168        // Starts at 0 and it is assigned in `bucketize_computation`.
169        computation_cost: u64,
170        // Whether to charge or go unmetered
171        charge: bool,
172        // Gas price for computation.
173        // This is a multiplier on the final charge as related to the RGP (reference gas price).
174        // Checked at signing: `gas_price >= reference_gas_price`
175        // and then conceptually
176        // `final_computation_cost = total_computation_cost * gas_price / reference_gas_price`
177        gas_price: u64,
178        // RGP as defined in the protocol config.
179        reference_gas_price: u64,
180        // storage rebate rate as defined in the ProtocolConfig
181        rebate_rate: u64,
182        /// Per-object storage accounting (accumulated costs/rebates + the storage config it needs),
183        /// shared with gas_v3 via `gas_common::StorageGas`.
184        storage: StorageGas,
185        /// Rounding mode for gas charges.
186        gas_rounding_mode: GasRoundingMode,
187    }
188
189    impl SuiGasStatus {
190        fn new(
191            move_gas_status: GasStatus,
192            gas_budget: u64,
193            charge: bool,
194            gas_price: u64,
195            reference_gas_price: u64,
196            storage_gas_price: u64,
197            rebate_rate: u64,
198            gas_rounding_mode: GasRoundingMode,
199            cost_table: SuiCostTable,
200        ) -> SuiGasStatus {
201            let gas_rounding_mode = match gas_rounding_mode {
202                GasRoundingMode::Bucketize => GasRoundingMode::Bucketize,
203                GasRoundingMode::Stepped(val) => GasRoundingMode::Stepped(val.max(1)),
204                GasRoundingMode::KeepHalfDigits => GasRoundingMode::KeepHalfDigits,
205            };
206            SuiGasStatus {
207                gas_status: move_gas_status,
208                gas_budget,
209                charge,
210                computation_cost: 0,
211                gas_price,
212                reference_gas_price,
213                rebate_rate,
214                storage: StorageGas::new(storage_gas_price, cost_table.storage_per_byte_cost),
215                gas_rounding_mode,
216                cost_table,
217            }
218        }
219
220        pub(crate) fn new_with_budget(
221            gas_budget: u64,
222            gas_price: u64,
223            reference_gas_price: u64,
224            config: &ProtocolConfig,
225        ) -> SuiGasStatus {
226            let storage_gas_price = config.storage_gas_price();
227            let max_computation_budget = config.max_gas_computation_bucket() * gas_price;
228            let computation_budget = if gas_budget > max_computation_budget {
229                max_computation_budget
230            } else {
231                gas_budget
232            };
233            let sui_cost_table = SuiCostTable::new(config, gas_price);
234            let gas_rounding_mode = if config.gas_rounding_halve_digits() {
235                GasRoundingMode::KeepHalfDigits
236            } else if let Some(step) = config.gas_rounding_step_as_option() {
237                GasRoundingMode::Stepped(step)
238            } else {
239                GasRoundingMode::Bucketize
240            };
241            Self::new(
242                GasStatus::new(
243                    sui_cost_table.execution_cost_table.clone(),
244                    computation_budget,
245                    gas_price,
246                    config.gas_model_version(),
247                ),
248                gas_budget,
249                true,
250                gas_price,
251                reference_gas_price,
252                storage_gas_price,
253                config.storage_rebate_rate(),
254                gas_rounding_mode,
255                sui_cost_table,
256            )
257        }
258
259        pub fn new_unmetered() -> SuiGasStatus {
260            Self::new(
261                GasStatus::new_unmetered(),
262                0,
263                false,
264                0,
265                0,
266                0,
267                0,
268                GasRoundingMode::Bucketize,
269                SuiCostTable::unmetered(),
270            )
271        }
272
273        pub fn reference_gas_price(&self) -> u64 {
274            self.reference_gas_price
275        }
276
277        fn storage_cost(&self) -> u64 {
278            self.storage_gas_units()
279        }
280    }
281
282    impl SuiGasStatusAPI for SuiGasStatus {
283        fn is_unmetered(&self) -> bool {
284            !self.charge
285        }
286
287        fn move_gas_status(&self) -> &GasStatus {
288            &self.gas_status
289        }
290
291        fn move_gas_status_mut(&mut self) -> &mut GasStatus {
292            &mut self.gas_status
293        }
294
295        fn bucketize_computation(&mut self, aborted: Option<bool>) -> Result<(), ExecutionError> {
296            let gas_used = self.gas_status.gas_used_pre_gas_price();
297            let effective_gas_price = if let Some(max_gas_price_rgp_factor_for_aborted_transactions) =
298                self.cost_table
299                    .max_gas_price_rgp_factor_for_aborted_transactions
300                && aborted.unwrap_or(false)
301            {
302                // For aborts, cap at max but don't exceed user's price
303                // This minimizes the risk of competing for priority execution in the case that the txn may be aborted.
304                let max_gas_price_for_aborted_txns =
305                    max_gas_price_rgp_factor_for_aborted_transactions * self.reference_gas_price;
306                self.gas_price.min(max_gas_price_for_aborted_txns)
307            } else {
308                // For all other cases, use the user's gas price
309                self.gas_price
310            };
311            let gas_used = match self.gas_rounding_mode {
312                GasRoundingMode::KeepHalfDigits => {
313                    half_digits_rounding(gas_used) * effective_gas_price
314                }
315                GasRoundingMode::Stepped(gas_rounding) => {
316                    if gas_used > 0 && gas_used % gas_rounding == 0 {
317                        gas_used * effective_gas_price
318                    } else {
319                        ((gas_used / gas_rounding) + 1) * gas_rounding * effective_gas_price
320                    }
321                }
322                GasRoundingMode::Bucketize => {
323                    let bucket_cost =
324                        get_bucket_cost(&self.cost_table.computation_bucket, gas_used);
325                    // charge extra on top of `computation_cost` to make the total computation
326                    // cost a bucket value
327                    bucket_cost * effective_gas_price
328                }
329            };
330            if self.gas_budget <= gas_used {
331                self.computation_cost = self.gas_budget;
332                Err(ExecutionErrorKind::InsufficientGas.into())
333            } else {
334                self.computation_cost = gas_used;
335                Ok(())
336            }
337        }
338
339        /// Returns the final (computation cost, storage cost, storage rebate) of the gas meter.
340        /// We use initial budget, combined with remaining gas and storage cost to derive
341        /// computation cost.
342        fn summary(&self) -> GasCostSummary {
343            // compute storage rebate, both rebate and non refundable fee
344            let storage_rebate = self.storage_rebate();
345            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
346            assert!(sender_rebate <= storage_rebate);
347            let non_refundable_storage_fee = storage_rebate - sender_rebate;
348            GasCostSummary {
349                computation_cost: self.computation_cost,
350                storage_cost: self.storage_cost(),
351                storage_rebate: sender_rebate,
352                non_refundable_storage_fee,
353            }
354        }
355
356        fn gas_budget(&self) -> u64 {
357            self.gas_budget
358        }
359
360        fn gas_price(&self) -> u64 {
361            self.gas_price
362        }
363
364        fn reference_gas_price(&self) -> u64 {
365            self.reference_gas_price
366        }
367
368        fn storage_gas_units(&self) -> u64 {
369            self.storage.storage_gas_units()
370        }
371
372        fn storage_rebate(&self) -> u64 {
373            self.storage.storage_rebate()
374        }
375
376        fn unmetered_storage_rebate(&self) -> u64 {
377            self.storage.unmetered_storage_rebate()
378        }
379
380        fn gas_used(&self) -> u64 {
381            self.gas_status.gas_used_pre_gas_price()
382        }
383
384        fn reset_storage_cost_and_rebate(&mut self) {
385            self.storage.reset();
386        }
387
388        fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
389            self.gas_status
390                .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
391                .map_err(|e| {
392                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
393                    ExecutionErrorKind::InsufficientGas.into()
394                })
395        }
396
397        fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
398            self.gas_status
399                .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
400                .map_err(|e| {
401                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
402                    ExecutionErrorKind::InsufficientGas.into()
403                })
404        }
405
406        /// Update `storage_rebate` and `storage_gas_units` for each object in the transaction.
407        /// There is no charge in this function. Charges will all be applied together at the end
408        /// (`track_storage_mutation`).
409        /// Return the new storage rebate (cost of object storage) according to `new_size`.
410        fn track_storage_mutation(
411            &mut self,
412            object_id: ObjectID,
413            new_size: usize,
414            storage_rebate: u64,
415        ) -> Option<u64> {
416            let unmetered = self.is_unmetered();
417            self.storage
418                .track_mutation(object_id, new_size, storage_rebate, unmetered)
419        }
420
421        fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
422            let storage_rebate = self.storage_rebate();
423            let storage_cost = self.storage_cost();
424            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
425            assert!(sender_rebate <= storage_rebate);
426            if sender_rebate >= storage_cost {
427                // there is more rebate than cost, when deducting gas we are adding
428                // to whatever is the current amount charged so we are `Ok`
429                Ok(())
430            } else {
431                let gas_left = self.gas_budget - self.computation_cost;
432                // we have to charge for storage and may go out of gas, check
433                if gas_left < storage_cost - sender_rebate {
434                    // Running out of gas would cause the temporary store to reset
435                    // and zero storage and rebate.
436                    // The remaining_gas will be 0 and we will charge all in computation
437                    Err(ExecutionErrorKind::InsufficientGas.into())
438                } else {
439                    Ok(())
440                }
441            }
442        }
443
444        fn adjust_computation_on_out_of_gas(&mut self) {
445            self.storage.reset();
446            self.computation_cost = self.gas_budget;
447        }
448
449        fn gas_usage_report(&self) -> GasUsageReport {
450            GasUsageReport {
451                cost_summary: self.summary(),
452                gas_used: self.gas_used(),
453                gas_price: self.gas_price(),
454                reference_gas_price: self.reference_gas_price(),
455                per_object_storage: self.per_object_storage().clone(),
456                gas_budget: self.gas_budget(),
457                storage_gas_price: self.storage.storage_gas_price,
458                rebate_rate: self.rebate_rate,
459            }
460        }
461
462        // Check whether gas arguments are legit:
463        // 1. Gas object has an address owner.
464        // 2. Gas budget is between min and max budget allowed
465        // 3. Gas balance (all gas coins together) is bigger or equal to budget
466        fn check_gas_balance(
467            &self,
468            gas_objs: &[&ObjectReadResult],
469            gas_budget: u64,
470            available_address_balance_gas: u64,
471        ) -> UserInputResult {
472            self.check_gas_objects(gas_objs)?;
473            check_gas_data(
474                gas_objs,
475                gas_budget,
476                available_address_balance_gas,
477                self.cost_table.min_transaction_cost,
478                self.cost_table.max_gas_budget,
479            )
480        }
481
482        fn check_gas_objects(&self, gas_objs: &[&ObjectReadResult]) -> UserInputResult {
483            check_gas_objects(gas_objs)
484        }
485
486        fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
487            self.storage.per_object_storage()
488        }
489    }
490}