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
90pub fn half_digits_rounding(n: u64) -> u64 {
91    if n < 1000 {
92        return 1000;
93    }
94    let digits = n.ilog10();
95    let drop = digits / 2;
96    let base = 10u64.pow(drop);
97    n.div_ceil(base).saturating_mul(base)
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct PerObjectStorage {
102    /// The new "value" for this object storage. Computed
103    /// at the end of execution while determining storage charges.
104    /// This will be the new storage rebate.
105    pub storage_cost: u64,
106    /// storage_rebate is the value of this object.
107    /// This is computed at the end of execution while determining storage charges.
108    /// The value is in Sui.
109    pub storage_rebate: u64,
110    /// The object size post-transaction in bytes
111    pub new_size: u64,
112}
113
114/// Per-object storage-gas accumulator, shared by both gas models. Pure data + arithmetic; the Move
115/// meter stays on the outer `SuiGasStatus`, which passes the `unmetered` flag into `track_mutation`.
116#[derive(Debug)]
117pub struct StorageGas {
118    /// Per-object storage cost + rebate, accumulated during execution.
119    per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
120    /// Running total of per-object storage cost. Metered path only.
121    total_storage_cost: u64,
122    /// Running total of per-object storage rebate. Metered path only.
123    total_storage_rebate: u64,
124    /// Storage rebate accrued while running unmetered (system transactions), retained in effects
125    /// and parked onto 0x5. Kept separate from `total_storage_rebate`: it must read 0 on metered
126    /// txns (its consumer `conserve_unmetered_storage_rebate` runs unconditionally).
127    unmetered_storage_rebate: u64,
128    /// Multiplier applied to the storage byte cost (`ProtocolConfig::storage_gas_price`).
129    pub storage_gas_price: u64,
130    /// Refundable per-byte storage cost (`ProtocolConfig::obj_data_cost_refundable`).
131    storage_per_byte_cost: u64,
132}
133
134impl StorageGas {
135    pub fn new(storage_gas_price: u64, storage_per_byte_cost: u64) -> Self {
136        Self {
137            per_object_storage: Vec::new(),
138            total_storage_cost: 0,
139            total_storage_rebate: 0,
140            unmetered_storage_rebate: 0,
141            storage_gas_price,
142            storage_per_byte_cost,
143        }
144    }
145
146    pub fn storage_gas_units(&self) -> u64 {
147        self.total_storage_cost
148    }
149
150    pub fn storage_rebate(&self) -> u64 {
151        self.total_storage_rebate
152    }
153
154    pub fn unmetered_storage_rebate(&self) -> u64 {
155        self.unmetered_storage_rebate
156    }
157
158    pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
159        &self.per_object_storage
160    }
161
162    pub fn reset(&mut self) {
163        self.per_object_storage = Vec::new();
164        self.total_storage_cost = 0;
165        self.total_storage_rebate = 0;
166        self.unmetered_storage_rebate = 0;
167    }
168
169    /// Update the running storage cost/rebate totals for the object.
170    /// Returns the new object storage cost (based on `new_size`), or `None` on overflow.
171    pub fn track_mutation(
172        &mut self,
173        object_id: ObjectID,
174        new_size: usize,
175        storage_rebate: u64,
176        unmetered: bool,
177    ) -> Option<u64> {
178        if unmetered {
179            let total = self.unmetered_storage_rebate.checked_add(storage_rebate)?;
180            self.unmetered_storage_rebate = total;
181            return Some(0);
182        }
183
184        let new_size = new_size as u64;
185        let storage_cost = new_size
186            .checked_mul(self.storage_per_byte_cost)?
187            .checked_mul(self.storage_gas_price)?;
188        self.total_storage_cost = self.total_storage_cost.checked_add(storage_cost)?;
189        self.total_storage_rebate = self.total_storage_rebate.checked_add(storage_rebate)?;
190        self.per_object_storage.push((
191            object_id,
192            PerObjectStorage {
193                storage_cost,
194                storage_rebate,
195                new_size,
196            },
197        ));
198        Some(storage_cost)
199    }
200}
201
202#[test]
203fn test_half_digits_rounding() {
204    assert_eq!(half_digits_rounding(0), 1000);
205    assert_eq!(half_digits_rounding(1), 1000);
206    assert_eq!(half_digits_rounding(999), 1000);
207    assert_eq!(half_digits_rounding(1000), 1000);
208    assert_eq!(half_digits_rounding(1001), 1010);
209    assert_eq!(half_digits_rounding(1050), 1050);
210    assert_eq!(half_digits_rounding(1999), 2000);
211    assert_eq!(half_digits_rounding(20_000), 20_000);
212    assert_eq!(half_digits_rounding(20_001), 20_100);
213    assert_eq!(half_digits_rounding(20_500), 20_500);
214    assert_eq!(half_digits_rounding(29_999), 30_000);
215    assert_eq!(half_digits_rounding(300_000), 300_000);
216    assert_eq!(half_digits_rounding(300_001), 300_100);
217    assert_eq!(half_digits_rounding(305_500), 305_500);
218    assert_eq!(half_digits_rounding(305_501), 305_600);
219    assert_eq!(half_digits_rounding(999_999), 1_000_000);
220    assert_eq!(half_digits_rounding(1_000_000), 1_000_000);
221    assert_eq!(half_digits_rounding(1_000_001), 1_001_000);
222    assert_eq!(half_digits_rounding(1_005_000), 1_005_000);
223    assert_eq!(half_digits_rounding(1_005_001), 1_006_000);
224    assert_eq!(half_digits_rounding(1_999_999), 2_000_000);
225    assert_eq!(half_digits_rounding(10_000_001), 10_001_000);
226    assert_eq!(half_digits_rounding(100_000_001), 100_010_000);
227}