Skip to main content

sui_types/gas_model/
gas_v3.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5#![deny(clippy::arithmetic_side_effects)]
6#![deny(clippy::cast_possible_truncation)]
7#![deny(clippy::indexing_slicing)]
8#![deny(clippy::cast_possible_wrap)]
9#![deny(clippy::cast_sign_loss)]
10
11use crate::error::UserInputResult;
12use crate::gas::{GasCostSummary, GasUsageReport, SuiGasStatusAPI};
13use crate::gas_model::gas_common::{
14    StorageGas, check_gas_data, check_gas_objects, half_digits_rounding, sender_rebate,
15};
16use crate::gas_model::gas_predicates::cost_table_for_version;
17use crate::gas_model::units_types::CostTable;
18use crate::transaction::ObjectReadResult;
19use crate::{
20    ObjectID,
21    error::ExecutionError,
22    execution_status::ExecutionErrorKind,
23    gas_model::tables::{GasStatus, ZERO_COST_SCHEDULE},
24};
25use move_core_types::vm_status::StatusCode;
26use sui_protocol_config::*;
27
28/// A list of constant costs of various operations in Sui.
29pub struct SuiCostTable {
30    /// A flat fee charged for every transaction.
31    pub(crate) min_transaction_cost: u64,
32    /// Maximum allowable budget for a transaction.
33    pub(crate) max_gas_budget: u64,
34    /// Computation cost per byte charged for package publish.
35    package_publish_per_byte_cost: u64,
36    /// Per byte cost to read objects from the store.
37    object_read_per_byte_cost: u64,
38    /// Unit cost of a byte in the storage.
39    storage_per_byte_cost: u64,
40    /// Execution cost table to be used.
41    pub execution_cost_table: CostTable,
42    /// RGP-multiplier cap on the effective gas price for aborted transactions.
43    max_gas_price_rgp_factor_for_aborted_transactions: Option<u64>,
44}
45
46impl std::fmt::Debug for SuiCostTable {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        // TODO: dump the fields.
49        write!(f, "SuiCostTable(...)")
50    }
51}
52
53impl SuiCostTable {
54    pub(crate) fn new(c: &ProtocolConfig, gas_price: u64) -> Self {
55        // gas_price here is the Reference Gas Price, however we may decide
56        // to change it to be the price passed in the transaction
57        let min_transaction_cost = c
58            .base_tx_cost_fixed()
59            .checked_mul(gas_price)
60            .expect("base tx cost cannot overflow: gas_price is bounded by max_gas_price");
61        Self {
62            min_transaction_cost,
63            max_gas_budget: c.max_tx_gas(),
64            package_publish_per_byte_cost: c.package_publish_cost_per_byte(),
65            object_read_per_byte_cost: c.obj_access_cost_read_per_byte(),
66            storage_per_byte_cost: c.obj_data_cost_refundable(),
67            execution_cost_table: cost_table_for_version(c.gas_model_version()),
68            max_gas_price_rgp_factor_for_aborted_transactions: c
69                .max_gas_price_rgp_factor_for_aborted_transactions_as_option(),
70        }
71    }
72
73    pub(crate) fn unmetered() -> Self {
74        Self {
75            min_transaction_cost: 0,
76            max_gas_budget: u64::MAX,
77            package_publish_per_byte_cost: 0,
78            object_read_per_byte_cost: 0,
79            storage_per_byte_cost: 0,
80            execution_cost_table: ZERO_COST_SCHEDULE.clone(),
81            max_gas_price_rgp_factor_for_aborted_transactions: None,
82        }
83    }
84}
85
86pub use crate::gas_model::gas_common::PerObjectStorage;
87
88#[allow(dead_code)]
89#[derive(Debug)]
90pub struct SuiGasStatus {
91    // GasStatus as used by the VM, that is all the VM sees
92    pub gas_status: GasStatus,
93    // Cost table contains a set of constant/config for the gas model/charging
94    cost_table: SuiCostTable,
95    // Gas budget for this gas status instance.
96    gas_budget: u64,
97    // Whether to charge or go unmetered
98    charge: bool,
99    // Price used to convert gas units to MIST; starts at `user_gas_price`, lowered to the abort cap
100    // by `bucketize_computation` on a Move abort.
101    effective_gas_price: u64,
102    // The gas price the user signed for (>= reference_gas_price).
103    user_gas_price: u64,
104    // RGP as defined in the protocol config.
105    reference_gas_price: u64,
106    // storage rebate rate as defined in the ProtocolConfig
107    rebate_rate: u64,
108    /// Per-object storage accounting .
109    storage: StorageGas,
110    /// When set, computation cost is reported as exactly `gas_budget`.
111    force_computation_cost_to_budget: bool,
112}
113
114impl SuiGasStatus {
115    fn new(
116        move_gas_status: GasStatus,
117        gas_budget: u64,
118        charge: bool,
119        user_gas_price: u64,
120        reference_gas_price: u64,
121        storage_gas_price: u64,
122        rebate_rate: u64,
123        cost_table: SuiCostTable,
124    ) -> SuiGasStatus {
125        SuiGasStatus {
126            gas_status: move_gas_status,
127            gas_budget,
128            charge,
129            effective_gas_price: user_gas_price,
130            user_gas_price,
131            reference_gas_price,
132            rebate_rate,
133            storage: StorageGas::new(storage_gas_price, cost_table.storage_per_byte_cost),
134            cost_table,
135            force_computation_cost_to_budget: false,
136        }
137    }
138
139    pub(crate) fn new_with_budget(
140        gas_budget: u64,
141        gas_price: u64,
142        reference_gas_price: u64,
143        config: &ProtocolConfig,
144    ) -> SuiGasStatus {
145        let storage_gas_price = config.storage_gas_price();
146        let max_computation_budget = config
147            .max_gas_computation_bucket()
148            .checked_mul(gas_price)
149            .expect("computation budget cannot overflow: gas_price is bounded by max_gas_price");
150        let computation_budget = if gas_budget > max_computation_budget {
151            max_computation_budget
152        } else {
153            gas_budget
154        };
155        let sui_cost_table = SuiCostTable::new(config, gas_price);
156        Self::new(
157            GasStatus::new(
158                sui_cost_table.execution_cost_table.clone(),
159                computation_budget,
160                gas_price,
161                config.gas_model_version(),
162            ),
163            gas_budget,
164            true,
165            gas_price,
166            reference_gas_price,
167            storage_gas_price,
168            config.storage_rebate_rate(),
169            sui_cost_table,
170        )
171    }
172
173    pub fn new_unmetered() -> SuiGasStatus {
174        Self::new(
175            GasStatus::new_unmetered(),
176            0,
177            false,
178            0,
179            0,
180            0,
181            0,
182            SuiCostTable::unmetered(),
183        )
184    }
185
186    pub fn reference_gas_price(&self) -> u64 {
187        self.reference_gas_price
188    }
189
190    /// Meter-derived computation cost in MIST (bucketed units * effective_gas_price).
191    fn uncapped_computation_cost(&self) -> u64 {
192        if self.force_computation_cost_to_budget {
193            return self.gas_budget;
194        }
195        let raw_units = self.gas_status.gas_used_pre_gas_price();
196        let bucketed_units = half_digits_rounding(raw_units);
197        bucketed_units.saturating_mul(self.effective_gas_price)
198    }
199
200    /// Computation cost reported in `summary()`, capped so
201    /// `computation + storage_cost - sender_rebate <= gas_budget`
202    /// storage charges stick, computation absorbs whatever budget remains.
203    fn derived_computation_cost(&self) -> u64 {
204        let uncapped_cost = self.uncapped_computation_cost();
205        let storage_rebate = self.storage_rebate();
206        let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
207        let net_storage = self.storage_cost().saturating_sub(sender_rebate);
208        let max_computation = self.gas_budget.saturating_sub(net_storage);
209        uncapped_cost.min(max_computation)
210    }
211
212    fn storage_cost(&self) -> u64 {
213        self.storage_gas_units()
214    }
215}
216
217impl SuiGasStatusAPI for SuiGasStatus {
218    fn is_unmetered(&self) -> bool {
219        !self.charge
220    }
221
222    fn move_gas_status(&self) -> &GasStatus {
223        &self.gas_status
224    }
225
226    fn move_gas_status_mut(&mut self) -> &mut GasStatus {
227        &mut self.gas_status
228    }
229
230    fn bucketize_computation(&mut self, aborted: Option<bool>) -> Result<(), ExecutionError> {
231        self.effective_gas_price = match self
232            .cost_table
233            .max_gas_price_rgp_factor_for_aborted_transactions
234        {
235            Some(factor) if aborted.unwrap_or(false) => {
236                let cap = factor
237                    .checked_mul(self.reference_gas_price)
238                    .ok_or_else(|| {
239                        ExecutionError::from_kind(ExecutionErrorKind::InvariantViolation)
240                    })?;
241                self.user_gas_price.min(cap)
242            }
243            _ => self.user_gas_price,
244        };
245        if self.uncapped_computation_cost() >= self.gas_budget {
246            return Err(ExecutionErrorKind::InsufficientGas.into());
247        }
248        Ok(())
249    }
250
251    /// Returns the gas cost summary for the transaction.
252    fn summary(&self) -> GasCostSummary {
253        let storage_rebate = self.storage_rebate();
254        let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
255        let non_refundable_storage_fee = storage_rebate
256            .checked_sub(sender_rebate)
257            .expect("sender rebate must not exceed storage rebate");
258        GasCostSummary {
259            computation_cost: self.derived_computation_cost(),
260            storage_cost: self.storage_cost(),
261            storage_rebate: sender_rebate,
262            non_refundable_storage_fee,
263        }
264    }
265
266    fn gas_budget(&self) -> u64 {
267        self.gas_budget
268    }
269
270    fn gas_price(&self) -> u64 {
271        self.user_gas_price
272    }
273
274    fn reference_gas_price(&self) -> u64 {
275        self.reference_gas_price
276    }
277
278    fn storage_gas_units(&self) -> u64 {
279        self.storage.storage_gas_units()
280    }
281
282    fn storage_rebate(&self) -> u64 {
283        self.storage.storage_rebate()
284    }
285
286    fn unmetered_storage_rebate(&self) -> u64 {
287        self.storage.unmetered_storage_rebate()
288    }
289
290    fn gas_used(&self) -> u64 {
291        self.gas_status.gas_used_pre_gas_price()
292    }
293
294    fn reset_storage_cost_and_rebate(&mut self) {
295        self.storage.reset();
296    }
297
298    fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
299        self.gas_status
300            .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
301            .map_err(|e| {
302                debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
303                ExecutionErrorKind::InsufficientGas.into()
304            })
305    }
306
307    fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
308        self.gas_status
309            .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
310            .map_err(|e| {
311                debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
312                ExecutionErrorKind::InsufficientGas.into()
313            })
314    }
315
316    /// Update `storage_rebate` and `storage_gas_units` for each object in the transaction.
317    /// Return the new storage rebate (cost of object storage) according to `new_size` or
318    /// `None` on arithmetic errors.
319    fn track_storage_mutation(
320        &mut self,
321        object_id: ObjectID,
322        new_size: usize,
323        storage_rebate: u64,
324    ) -> Option<u64> {
325        let unmetered = self.is_unmetered();
326        self.storage
327            .track_mutation(object_id, new_size, storage_rebate, unmetered)
328    }
329
330    fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
331        let storage_rebate = self.storage.storage_rebate();
332        let storage_cost = self.storage.storage_gas_units();
333        let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
334        assert!(sender_rebate <= storage_rebate);
335        let net_storage_cost = storage_cost.saturating_sub(sender_rebate);
336        let gas_left = self
337            .gas_budget
338            .saturating_sub(self.uncapped_computation_cost());
339        if net_storage_cost > gas_left {
340            return Err(ExecutionErrorKind::InsufficientGas.into());
341        }
342        Ok(())
343    }
344
345    /// Drop accumulated storage and force `summary()` to report `computation_cost == gas_budget`
346    fn adjust_computation_on_out_of_gas(&mut self) {
347        self.storage.reset();
348        self.force_computation_cost_to_budget = true;
349    }
350
351    fn gas_usage_report(&self) -> GasUsageReport {
352        GasUsageReport {
353            cost_summary: self.summary(),
354            gas_used: self.gas_used(),
355            gas_price: self.gas_price(),
356            reference_gas_price: self.reference_gas_price(),
357            per_object_storage: self.per_object_storage().clone(),
358            gas_budget: self.gas_budget(),
359            storage_gas_price: self.storage.storage_gas_price,
360            rebate_rate: self.rebate_rate,
361        }
362    }
363
364    // Check whether gas arguments are legit:
365    // 1. Gas object has an address owner.
366    // 2. Gas budget is between min and max budget allowed
367    // 3. Gas balance (all gas coins together) is bigger or equal to budget
368    fn check_gas_balance(
369        &self,
370        gas_objs: &[&ObjectReadResult],
371        gas_budget: u64,
372        available_address_balance_gas: u64,
373    ) -> UserInputResult {
374        self.check_gas_objects(gas_objs)?;
375        check_gas_data(
376            gas_objs,
377            gas_budget,
378            available_address_balance_gas,
379            self.cost_table.min_transaction_cost,
380            self.cost_table.max_gas_budget,
381        )
382    }
383
384    fn check_gas_objects(&self, gas_objs: &[&ObjectReadResult]) -> UserInputResult {
385        check_gas_objects(gas_objs)
386    }
387
388    fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
389        self.storage.per_object_storage()
390    }
391}