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    #[derive(Debug, Clone, Copy)]
170    enum GasRoundingMode {
171        /// Bucketize the computation cost according to predefined buckets.
172        Bucketize,
173        /// Rounding value to round up gas charges.
174        Stepped(u64),
175        /// Round by keeping just over half digits
176        KeepHalfDigits,
177    }
178
179    #[allow(dead_code)]
180    #[derive(Debug)]
181    pub struct SuiGasStatus {
182        // GasStatus as used by the VM, that is all the VM sees
183        pub gas_status: GasStatus,
184        // Cost table contains a set of constant/config for the gas model/charging
185        cost_table: SuiCostTable,
186        // Gas budget for this gas status instance.
187        // Typically the gas budget as defined in the `TransactionData::GasData`
188        gas_budget: u64,
189        // Computation cost after execution. This is the result of the gas used by the `GasStatus`
190        // properly bucketized.
191        // Starts at 0 and it is assigned in `bucketize_computation`.
192        computation_cost: u64,
193        // Whether to charge or go unmetered
194        charge: bool,
195        // Gas price for computation.
196        // This is a multiplier on the final charge as related to the RGP (reference gas price).
197        // Checked at signing: `gas_price >= reference_gas_price`
198        // and then conceptually
199        // `final_computation_cost = total_computation_cost * gas_price / reference_gas_price`
200        gas_price: u64,
201        // RGP as defined in the protocol config.
202        reference_gas_price: u64,
203        // Gas price for storage. This is a multiplier on the final charge
204        // as related to the storage gas price defined in the system
205        // (`ProtocolConfig::storage_gas_price`).
206        // Conceptually, given a constant `obj_data_cost_refundable`
207        // (defined in `ProtocolConfig::obj_data_cost_refundable`)
208        // `total_storage_cost = storage_bytes * obj_data_cost_refundable`
209        // `final_storage_cost = total_storage_cost * storage_gas_price`
210        storage_gas_price: u64,
211        /// Per Object Storage Cost and Storage Rebate, used to get accumulated values at the
212        /// end of execution to determine storage charges and rebates.
213        per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
214        // storage rebate rate as defined in the ProtocolConfig
215        rebate_rate: u64,
216        /// Amount of storage rebate accumulated when we are running in unmetered mode (i.e. system transaction).
217        /// This allows us to track how much storage rebate we need to retain in system transactions.
218        unmetered_storage_rebate: u64,
219        /// Rounding mode for gas charges.
220        gas_rounding_mode: GasRoundingMode,
221    }
222
223    impl SuiGasStatus {
224        fn new(
225            move_gas_status: GasStatus,
226            gas_budget: u64,
227            charge: bool,
228            gas_price: u64,
229            reference_gas_price: u64,
230            storage_gas_price: u64,
231            rebate_rate: u64,
232            gas_rounding_mode: GasRoundingMode,
233            cost_table: SuiCostTable,
234        ) -> SuiGasStatus {
235            let gas_rounding_mode = match gas_rounding_mode {
236                GasRoundingMode::Bucketize => GasRoundingMode::Bucketize,
237                GasRoundingMode::Stepped(val) => GasRoundingMode::Stepped(val.max(1)),
238                GasRoundingMode::KeepHalfDigits => GasRoundingMode::KeepHalfDigits,
239            };
240            SuiGasStatus {
241                gas_status: move_gas_status,
242                gas_budget,
243                charge,
244                computation_cost: 0,
245                gas_price,
246                reference_gas_price,
247                storage_gas_price,
248                per_object_storage: Vec::new(),
249                rebate_rate,
250                unmetered_storage_rebate: 0,
251                gas_rounding_mode,
252                cost_table,
253            }
254        }
255
256        pub(crate) fn new_with_budget(
257            gas_budget: u64,
258            gas_price: u64,
259            reference_gas_price: u64,
260            config: &ProtocolConfig,
261        ) -> SuiGasStatus {
262            let storage_gas_price = config.storage_gas_price();
263            let max_computation_budget = config.max_gas_computation_bucket() * gas_price;
264            let computation_budget = if gas_budget > max_computation_budget {
265                max_computation_budget
266            } else {
267                gas_budget
268            };
269            let sui_cost_table = SuiCostTable::new(config, gas_price);
270            let gas_rounding_mode = if config.gas_rounding_halve_digits() {
271                GasRoundingMode::KeepHalfDigits
272            } else if let Some(step) = config.gas_rounding_step_as_option() {
273                GasRoundingMode::Stepped(step)
274            } else {
275                GasRoundingMode::Bucketize
276            };
277            Self::new(
278                GasStatus::new(
279                    sui_cost_table.execution_cost_table.clone(),
280                    computation_budget,
281                    gas_price,
282                    config.gas_model_version(),
283                ),
284                gas_budget,
285                true,
286                gas_price,
287                reference_gas_price,
288                storage_gas_price,
289                config.storage_rebate_rate(),
290                gas_rounding_mode,
291                sui_cost_table,
292            )
293        }
294
295        pub fn new_unmetered() -> SuiGasStatus {
296            Self::new(
297                GasStatus::new_unmetered(),
298                0,
299                false,
300                0,
301                0,
302                0,
303                0,
304                GasRoundingMode::Bucketize,
305                SuiCostTable::unmetered(),
306            )
307        }
308
309        pub fn reference_gas_price(&self) -> u64 {
310            self.reference_gas_price
311        }
312
313        // Check whether gas arguments are legit:
314        // 1. Gas object has an address owner.
315        // 2. Gas budget is between min and max budget allowed
316        // 3. Gas balance (all gas coins together) is bigger or equal to budget
317        pub(crate) fn check_gas_balance(
318            &self,
319            gas_objs: &[&ObjectReadResult],
320            gas_budget: u64,
321        ) -> UserInputResult {
322            // 1. All gas objects have an address owner
323            for gas_object in gas_objs {
324                // if as_object() returns None, it means the object has been deleted (and therefore
325                // must be a shared object).
326                if let Some(obj) = gas_object.as_object() {
327                    if !obj.is_address_owned() {
328                        return Err(UserInputError::GasObjectNotOwnedObject {
329                            owner: obj.owner.clone(),
330                        });
331                    }
332                } else {
333                    // This case should never happen (because gas can't be a shared object), but we
334                    // handle this case for future-proofing
335                    return Err(UserInputError::MissingGasPayment);
336                }
337            }
338
339            // 2. Gas budget is between min and max budget allowed
340            if gas_budget > self.cost_table.max_gas_budget {
341                return Err(UserInputError::GasBudgetTooHigh {
342                    gas_budget,
343                    max_budget: self.cost_table.max_gas_budget,
344                });
345            }
346            if gas_budget < self.cost_table.min_transaction_cost {
347                return Err(UserInputError::GasBudgetTooLow {
348                    gas_budget,
349                    min_budget: self.cost_table.min_transaction_cost,
350                });
351            }
352
353            // 3. Gas balance (all gas coins together) is bigger or equal to budget
354            let mut gas_balance = 0u128;
355            for gas_obj in gas_objs {
356                // expect is safe because we already checked that all gas objects have an address owner
357                gas_balance +=
358                    gas::get_gas_balance(gas_obj.as_object().expect("object must be owned"))?
359                        as u128;
360            }
361            if gas_balance < gas_budget as u128 {
362                Err(UserInputError::GasBalanceTooLow {
363                    gas_balance,
364                    needed_gas_amount: gas_budget as u128,
365                })
366            } else {
367                Ok(())
368            }
369        }
370
371        fn storage_cost(&self) -> u64 {
372            self.storage_gas_units()
373        }
374
375        pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
376            &self.per_object_storage
377        }
378    }
379
380    impl SuiGasStatusAPI for SuiGasStatus {
381        fn is_unmetered(&self) -> bool {
382            !self.charge
383        }
384
385        fn move_gas_status(&self) -> &GasStatus {
386            &self.gas_status
387        }
388
389        fn move_gas_status_mut(&mut self) -> &mut GasStatus {
390            &mut self.gas_status
391        }
392
393        fn bucketize_computation(&mut self, aborted: Option<bool>) -> Result<(), ExecutionError> {
394            let gas_used = self.gas_status.gas_used_pre_gas_price();
395            let effective_gas_price = if self
396                .cost_table
397                .max_gas_price_rgp_factor_for_aborted_transactions
398                .is_some()
399                && aborted.unwrap_or(false)
400            {
401                // For aborts, cap at max but don't exceed user's price
402                // This minimizes the risk of competing for priority execution in the case that the txn may be aborted.
403                let max_gas_price_for_aborted_txns = self
404                    .cost_table
405                    .max_gas_price_rgp_factor_for_aborted_transactions
406                    .unwrap()
407                    * self.reference_gas_price;
408                self.gas_price.min(max_gas_price_for_aborted_txns)
409            } else {
410                // For all other cases, use the user's gas price
411                self.gas_price
412            };
413            let gas_used = match self.gas_rounding_mode {
414                GasRoundingMode::KeepHalfDigits => {
415                    half_digits_rounding(gas_used) * effective_gas_price
416                }
417                GasRoundingMode::Stepped(gas_rounding) => {
418                    if gas_used > 0 && gas_used % gas_rounding == 0 {
419                        gas_used * effective_gas_price
420                    } else {
421                        ((gas_used / gas_rounding) + 1) * gas_rounding * effective_gas_price
422                    }
423                }
424                GasRoundingMode::Bucketize => {
425                    let bucket_cost =
426                        get_bucket_cost(&self.cost_table.computation_bucket, gas_used);
427                    // charge extra on top of `computation_cost` to make the total computation
428                    // cost a bucket value
429                    bucket_cost * effective_gas_price
430                }
431            };
432            if self.gas_budget <= gas_used {
433                self.computation_cost = self.gas_budget;
434                Err(ExecutionErrorKind::InsufficientGas.into())
435            } else {
436                self.computation_cost = gas_used;
437                Ok(())
438            }
439        }
440
441        /// Returns the final (computation cost, storage cost, storage rebate) of the gas meter.
442        /// We use initial budget, combined with remaining gas and storage cost to derive
443        /// computation cost.
444        fn summary(&self) -> GasCostSummary {
445            // compute storage rebate, both rebate and non refundable fee
446            let storage_rebate = self.storage_rebate();
447            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
448            assert!(sender_rebate <= storage_rebate);
449            let non_refundable_storage_fee = storage_rebate - sender_rebate;
450            GasCostSummary {
451                computation_cost: self.computation_cost,
452                storage_cost: self.storage_cost(),
453                storage_rebate: sender_rebate,
454                non_refundable_storage_fee,
455            }
456        }
457
458        fn gas_budget(&self) -> u64 {
459            self.gas_budget
460        }
461
462        fn gas_price(&self) -> u64 {
463            self.gas_price
464        }
465
466        fn reference_gas_price(&self) -> u64 {
467            self.reference_gas_price
468        }
469
470        fn storage_gas_units(&self) -> u64 {
471            self.per_object_storage
472                .iter()
473                .map(|(_, per_object)| per_object.storage_cost)
474                .sum()
475        }
476
477        fn storage_rebate(&self) -> u64 {
478            self.per_object_storage
479                .iter()
480                .map(|(_, per_object)| per_object.storage_rebate)
481                .sum()
482        }
483
484        fn unmetered_storage_rebate(&self) -> u64 {
485            self.unmetered_storage_rebate
486        }
487
488        fn gas_used(&self) -> u64 {
489            self.gas_status.gas_used_pre_gas_price()
490        }
491
492        fn reset_storage_cost_and_rebate(&mut self) {
493            self.per_object_storage = Vec::new();
494            self.unmetered_storage_rebate = 0;
495        }
496
497        fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
498            self.gas_status
499                .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
500                .map_err(|e| {
501                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
502                    ExecutionErrorKind::InsufficientGas.into()
503                })
504        }
505
506        fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
507            self.gas_status
508                .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
509                .map_err(|e| {
510                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
511                    ExecutionErrorKind::InsufficientGas.into()
512                })
513        }
514
515        /// Update `storage_rebate` and `storage_gas_units` for each object in the transaction.
516        /// There is no charge in this function. Charges will all be applied together at the end
517        /// (`track_storage_mutation`).
518        /// Return the new storage rebate (cost of object storage) according to `new_size`.
519        fn track_storage_mutation(
520            &mut self,
521            object_id: ObjectID,
522            new_size: usize,
523            storage_rebate: u64,
524        ) -> u64 {
525            if self.is_unmetered() {
526                self.unmetered_storage_rebate += storage_rebate;
527                return 0;
528            }
529
530            // compute and track cost (based on size)
531            let new_size = new_size as u64;
532            let storage_cost =
533                new_size * self.cost_table.storage_per_byte_cost * self.storage_gas_price;
534            // track rebate
535
536            self.per_object_storage.push((
537                object_id,
538                PerObjectStorage {
539                    storage_cost,
540                    storage_rebate,
541                    new_size,
542                },
543            ));
544            // return the new object rebate (object storage cost)
545            storage_cost
546        }
547
548        fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
549            let storage_rebate = self.storage_rebate();
550            let storage_cost = self.storage_cost();
551            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
552            assert!(sender_rebate <= storage_rebate);
553            if sender_rebate >= storage_cost {
554                // there is more rebate than cost, when deducting gas we are adding
555                // to whatever is the current amount charged so we are `Ok`
556                Ok(())
557            } else {
558                let gas_left = self.gas_budget - self.computation_cost;
559                // we have to charge for storage and may go out of gas, check
560                if gas_left < storage_cost - sender_rebate {
561                    // Running out of gas would cause the temporary store to reset
562                    // and zero storage and rebate.
563                    // The remaining_gas will be 0 and we will charge all in computation
564                    Err(ExecutionErrorKind::InsufficientGas.into())
565                } else {
566                    Ok(())
567                }
568            }
569        }
570
571        fn adjust_computation_on_out_of_gas(&mut self) {
572            self.per_object_storage = Vec::new();
573            self.computation_cost = self.gas_budget;
574        }
575
576        fn gas_usage_report(&self) -> GasUsageReport {
577            GasUsageReport {
578                cost_summary: self.summary(),
579                gas_used: self.gas_used(),
580                gas_price: self.gas_price(),
581                reference_gas_price: self.reference_gas_price(),
582                per_object_storage: self.per_object_storage().clone(),
583                gas_budget: self.gas_budget(),
584                storage_gas_price: self.storage_gas_price,
585                rebate_rate: self.rebate_rate,
586            }
587        }
588    }
589
590    fn half_digits_rounding(n: u64) -> u64 {
591        if n < 1000 {
592            return 1000;
593        }
594        let digits = n.ilog10();
595        let drop = digits / 2;
596        let base = 10u64.pow(drop);
597        n.div_ceil(base) * base
598    }
599
600    #[test]
601    fn test_half_digits_rounding() {
602        assert_eq!(half_digits_rounding(0), 1000);
603        assert_eq!(half_digits_rounding(1), 1000);
604        assert_eq!(half_digits_rounding(999), 1000);
605        assert_eq!(half_digits_rounding(1000), 1000);
606        assert_eq!(half_digits_rounding(1001), 1010);
607        assert_eq!(half_digits_rounding(1050), 1050);
608        assert_eq!(half_digits_rounding(1999), 2000);
609        assert_eq!(half_digits_rounding(20_000), 20_000);
610        assert_eq!(half_digits_rounding(20_001), 20_100);
611        assert_eq!(half_digits_rounding(20_500), 20_500);
612        assert_eq!(half_digits_rounding(29_999), 30_000);
613        assert_eq!(half_digits_rounding(300_000), 300_000);
614        assert_eq!(half_digits_rounding(300_001), 300_100);
615        assert_eq!(half_digits_rounding(305_500), 305_500);
616        assert_eq!(half_digits_rounding(305_501), 305_600);
617        assert_eq!(half_digits_rounding(999_999), 1_000_000);
618        assert_eq!(half_digits_rounding(1_000_000), 1_000_000);
619        assert_eq!(half_digits_rounding(1_000_001), 1_001_000);
620        assert_eq!(half_digits_rounding(1_005_000), 1_005_000);
621        assert_eq!(half_digits_rounding(1_005_001), 1_006_000);
622        assert_eq!(half_digits_rounding(1_999_999), 2_000_000);
623        assert_eq!(half_digits_rounding(10_000_001), 10_001_000);
624        assert_eq!(half_digits_rounding(100_000_001), 100_010_000);
625    }
626}