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