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