Skip to main content

sui_types/gas_model/
gas_common.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4#![deny(clippy::arithmetic_side_effects)]
5#![deny(clippy::cast_possible_truncation)]
6#![deny(clippy::indexing_slicing)]
7#![deny(clippy::cast_possible_wrap)]
8#![deny(clippy::cast_sign_loss)]
9
10use crate::error::{UserInputError, UserInputResult};
11use crate::transaction::ObjectReadResult;
12use crate::{ObjectID, gas};
13use serde::{Deserialize, Serialize};
14
15pub fn check_gas_objects(gas_objs: &[&ObjectReadResult]) -> UserInputResult {
16    // All gas objects have an address owner
17    // Note: because of address balance payments, gas_objs may be empty.
18    for gas_object in gas_objs {
19        // if as_object() returns None, it means the object has been deleted (and therefore
20        // must be a shared object).
21        if let Some(obj) = gas_object.as_object() {
22            if !obj.is_address_owned() {
23                return Err(UserInputError::GasObjectNotOwnedObject {
24                    owner: obj.owner.clone(),
25                });
26            }
27        } else {
28            // This case should never happen (because gas can't be a shared object), but we
29            // handle this case for future-proofing
30            return Err(UserInputError::MissingGasPayment);
31        }
32    }
33    Ok(())
34}
35
36pub fn check_gas_data(
37    gas_objs: &[&ObjectReadResult],
38    gas_budget: u64,
39    available_address_balance_gas: u64,
40    min_transaction_cost: u64,
41    max_gas_budget: u64,
42) -> UserInputResult {
43    // Gas budget is between min and max budget allowed
44    if gas_budget > max_gas_budget {
45        return Err(UserInputError::GasBudgetTooHigh {
46            gas_budget,
47            max_budget: max_gas_budget,
48        });
49    }
50    if gas_budget < min_transaction_cost {
51        return Err(UserInputError::GasBudgetTooLow {
52            gas_budget,
53            min_budget: min_transaction_cost,
54        });
55    }
56
57    // Gas balance (all gas coins + address balance together) is bigger or equal to budget
58    let mut gas_balance = available_address_balance_gas as u128;
59    for gas_obj in gas_objs {
60        // Saturation is unreachable: a sum of u64 coin balances cannot overflow u128.
61        gas_balance = gas_balance.saturating_add(gas::get_gas_balance(gas_obj.as_object().ok_or(
62            UserInputError::InvalidGasObject {
63                object_id: gas_obj.id(),
64            },
65        )?)? as u128);
66    }
67    if gas_balance < gas_budget as u128 {
68        Err(UserInputError::GasBalanceTooLow {
69            gas_balance,
70            needed_gas_amount: gas_budget as u128,
71        })
72    } else {
73        Ok(())
74    }
75}
76
77/// Portion of the storage rebate that gets passed on to the transaction sender. The remainder
78/// will be burned, then re-minted + added to the storage fund at the next epoch change
79pub fn sender_rebate(storage_rebate: u64, storage_rebate_rate: u64) -> u64 {
80    // we round storage rebate such that `>= x.5` goes to x+1 (rounds up) and
81    // `< x.5` goes to x (truncates). We replicate `f32/64::round()`
82    const BASIS_POINTS: u128 = 10000;
83    let rebate = (storage_rebate as u128)
84        .saturating_mul(storage_rebate_rate as u128)
85        .saturating_add(BASIS_POINTS / 2) // integer rounding adds half of the denominator
86        / BASIS_POINTS;
87    u64::try_from(rebate).unwrap_or(u64::MAX)
88}
89
90/// Internal gas for reading `size` bytes of package inputs at `cost_per_kb` per KiB (1,024 bytes),
91/// rounded up. Saturates at `u64::MAX` instead of overflowing.
92pub fn package_read_internal_gas(size: usize, cost_per_kb: u64) -> u64 {
93    // The product of two values below 2^64 always fits in a u128.
94    let cost = (size as u128)
95        .saturating_mul(u128::from(cost_per_kb))
96        .div_ceil(1024);
97    u64::try_from(cost).unwrap_or(u64::MAX)
98}
99
100pub fn half_digits_rounding(n: u64) -> u64 {
101    if n < 1000 {
102        return 1000;
103    }
104    let digits = n.ilog10();
105    let drop = digits / 2;
106    let base = 10u64.pow(drop);
107    n.div_ceil(base).saturating_mul(base)
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct PerObjectStorage {
112    /// The new "value" for this object storage. Computed
113    /// at the end of execution while determining storage charges.
114    /// This will be the new storage rebate.
115    pub storage_cost: u64,
116    /// storage_rebate is the value of this object.
117    /// This is computed at the end of execution while determining storage charges.
118    /// The value is in Sui.
119    pub storage_rebate: u64,
120    /// The object size post-transaction in bytes
121    pub new_size: u64,
122}
123
124/// Per-object storage-gas accumulator, shared by both gas models. Pure data + arithmetic; the Move
125/// meter stays on the outer `SuiGasStatus`, which passes the `unmetered` flag into `track_mutation`.
126#[derive(Debug)]
127pub struct StorageGas {
128    /// Per-object storage cost + rebate, accumulated during execution.
129    per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
130    /// Running total of per-object storage cost. Metered path only.
131    total_storage_cost: u64,
132    /// Running total of per-object storage rebate. Metered path only.
133    total_storage_rebate: u64,
134    /// Storage rebate accrued while running unmetered (system transactions), retained in effects
135    /// and parked onto 0x5. Kept separate from `total_storage_rebate`: it must read 0 on metered
136    /// txns (its consumer `conserve_unmetered_storage_rebate` runs unconditionally).
137    unmetered_storage_rebate: u64,
138    /// Multiplier applied to the storage byte cost (`ProtocolConfig::storage_gas_price`).
139    pub storage_gas_price: u64,
140    /// Refundable per-byte storage cost (`ProtocolConfig::obj_data_cost_refundable`).
141    storage_per_byte_cost: u64,
142}
143
144impl StorageGas {
145    pub fn new(storage_gas_price: u64, storage_per_byte_cost: u64) -> Self {
146        Self {
147            per_object_storage: Vec::new(),
148            total_storage_cost: 0,
149            total_storage_rebate: 0,
150            unmetered_storage_rebate: 0,
151            storage_gas_price,
152            storage_per_byte_cost,
153        }
154    }
155
156    pub fn storage_gas_units(&self) -> u64 {
157        self.total_storage_cost
158    }
159
160    pub fn storage_rebate(&self) -> u64 {
161        self.total_storage_rebate
162    }
163
164    pub fn unmetered_storage_rebate(&self) -> u64 {
165        self.unmetered_storage_rebate
166    }
167
168    pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
169        &self.per_object_storage
170    }
171
172    pub fn reset(&mut self) {
173        self.per_object_storage = Vec::new();
174        self.total_storage_cost = 0;
175        self.total_storage_rebate = 0;
176        self.unmetered_storage_rebate = 0;
177    }
178
179    /// Update the running storage cost/rebate totals for the object.
180    /// Returns the new object storage cost (based on `new_size`), or `None` on overflow.
181    pub fn track_mutation(
182        &mut self,
183        object_id: ObjectID,
184        new_size: usize,
185        storage_rebate: u64,
186        unmetered: bool,
187    ) -> Option<u64> {
188        if unmetered {
189            let total = self.unmetered_storage_rebate.checked_add(storage_rebate)?;
190            self.unmetered_storage_rebate = total;
191            return Some(0);
192        }
193
194        let new_size = new_size as u64;
195        let storage_cost = new_size
196            .checked_mul(self.storage_per_byte_cost)?
197            .checked_mul(self.storage_gas_price)?;
198        self.total_storage_cost = self.total_storage_cost.checked_add(storage_cost)?;
199        self.total_storage_rebate = self.total_storage_rebate.checked_add(storage_rebate)?;
200        self.per_object_storage.push((
201            object_id,
202            PerObjectStorage {
203                storage_cost,
204                storage_rebate,
205                new_size,
206            },
207        ));
208        Some(storage_cost)
209    }
210}
211
212#[test]
213fn test_half_digits_rounding() {
214    assert_eq!(half_digits_rounding(0), 1000);
215    assert_eq!(half_digits_rounding(1), 1000);
216    assert_eq!(half_digits_rounding(999), 1000);
217    assert_eq!(half_digits_rounding(1000), 1000);
218    assert_eq!(half_digits_rounding(1001), 1010);
219    assert_eq!(half_digits_rounding(1050), 1050);
220    assert_eq!(half_digits_rounding(1999), 2000);
221    assert_eq!(half_digits_rounding(20_000), 20_000);
222    assert_eq!(half_digits_rounding(20_001), 20_100);
223    assert_eq!(half_digits_rounding(20_500), 20_500);
224    assert_eq!(half_digits_rounding(29_999), 30_000);
225    assert_eq!(half_digits_rounding(300_000), 300_000);
226    assert_eq!(half_digits_rounding(300_001), 300_100);
227    assert_eq!(half_digits_rounding(305_500), 305_500);
228    assert_eq!(half_digits_rounding(305_501), 305_600);
229    assert_eq!(half_digits_rounding(999_999), 1_000_000);
230    assert_eq!(half_digits_rounding(1_000_000), 1_000_000);
231    assert_eq!(half_digits_rounding(1_000_001), 1_001_000);
232    assert_eq!(half_digits_rounding(1_005_000), 1_005_000);
233    assert_eq!(half_digits_rounding(1_005_001), 1_006_000);
234    assert_eq!(half_digits_rounding(1_999_999), 2_000_000);
235    assert_eq!(half_digits_rounding(10_000_001), 10_001_000);
236    assert_eq!(half_digits_rounding(100_000_001), 100_010_000);
237}