Skip to main content

iota_types/
gas.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6pub use checked::*;
7
8#[iota_macros::with_checked_arithmetic]
9pub mod checked {
10
11    use std::collections::HashMap;
12
13    use enum_dispatch::enum_dispatch;
14    use iota_protocol_config::ProtocolConfig;
15    use iota_sdk_types::{GasPayment, ObjectReference, Transaction, gas::GasCostSummary};
16
17    use crate::{
18        ObjectId,
19        error::{ExecutionError, IotaResult, UserInputError, UserInputResult},
20        gas_model::{gas_v1::IotaGasStatus as IotaGasStatusV1, tables::GasStatus},
21        object::{MoveStructExt, Object},
22        transaction::{InputObjects, ObjectReadResult, TransactionAPI},
23    };
24
25    #[enum_dispatch]
26    pub trait IotaGasStatusAPI {
27        fn is_unmetered(&self) -> bool;
28        fn move_gas_status(&self) -> &GasStatus;
29        fn move_gas_status_mut(&mut self) -> &mut GasStatus;
30        fn bucketize_computation(&mut self) -> Result<(), ExecutionError>;
31        fn summary(&self) -> GasCostSummary;
32        fn gas_budget(&self) -> u64;
33        fn gas_price(&self) -> u64;
34        fn reference_gas_price(&self) -> u64;
35        fn storage_gas_units(&self) -> u64;
36        fn storage_rebate(&self) -> u64;
37        fn unmetered_storage_rebate(&self) -> u64;
38        fn gas_used(&self) -> u64;
39        fn reset_storage_cost_and_rebate(&mut self);
40        fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError>;
41        fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError>;
42        fn track_storage_mutation(
43            &mut self,
44            object_id: ObjectId,
45            new_size: usize,
46            storage_rebate: u64,
47        ) -> u64;
48        fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError>;
49        fn adjust_computation_on_out_of_gas(&mut self);
50    }
51
52    /// Version aware enum for gas status.
53    #[enum_dispatch(IotaGasStatusAPI)]
54    #[derive(Debug)]
55    pub enum IotaGasStatus {
56        V1(IotaGasStatusV1),
57    }
58
59    impl IotaGasStatus {
60        pub fn new(
61            gas_budget: u64,
62            gas_price: u64,
63            reference_gas_price: u64,
64            config: &ProtocolConfig,
65        ) -> IotaResult<Self> {
66            Self::check_gas_preconditions(gas_price, reference_gas_price, config)?;
67
68            Ok(Self::V1(IotaGasStatusV1::new_with_budget(
69                gas_budget,
70                gas_price,
71                reference_gas_price,
72                config,
73            )))
74        }
75
76        pub fn new_unmetered() -> Self {
77            // Always return V1 as unmetered gas status is identical from V1 to V2.
78            // This is only used for system transactions which do not pay gas.
79            Self::V1(IotaGasStatusV1::new_unmetered())
80        }
81
82        // This is the only public API on IotaGasStatus, all other gas related
83        // operations should go through `GasCharger`
84        pub fn check_gas_balance(
85            &self,
86            gas_objs: &[&ObjectReadResult],
87            gas_budget: u64,
88        ) -> UserInputResult {
89            match self {
90                Self::V1(status) => status.check_gas_balance(gas_objs, gas_budget),
91            }
92        }
93
94        fn check_gas_preconditions(
95            gas_price: u64,
96            reference_gas_price: u64,
97            config: &ProtocolConfig,
98        ) -> IotaResult<()> {
99            // Common checks. We may pull them into version specific status as needed, but
100            // they are unlikely to change.
101
102            // The gas price must be greater than or equal to the reference gas price.
103            if gas_price < reference_gas_price {
104                return Err(UserInputError::GasPriceUnderRGP {
105                    gas_price,
106                    reference_gas_price,
107                }
108                .into());
109            }
110            if gas_price > config.max_gas_price() {
111                return Err(UserInputError::GasPriceTooHigh {
112                    max_gas_price: config.max_gas_price(),
113                }
114                .into());
115            }
116
117            Ok(())
118        }
119    }
120
121    // Helper functions to deal with gas coins operations.
122
123    pub fn deduct_gas(gas_object: &mut Object, charge_or_rebate: i64) {
124        // The object must be a gas coin as we have checked in transaction handle phase.
125        let gas_coin = gas_object.data.as_opt_mut_struct().unwrap();
126        let balance = gas_coin.get_coin_value_unchecked();
127        let new_balance = if charge_or_rebate < 0 {
128            balance + (-charge_or_rebate as u64)
129        } else {
130            assert!(balance >= charge_or_rebate as u64);
131            balance - charge_or_rebate as u64
132        };
133        gas_coin.set_coin_value_unchecked(new_balance)
134    }
135
136    pub fn get_gas_balance(gas_object: &Object) -> UserInputResult<u64> {
137        if let Some(move_obj) = gas_object.data.as_opt_struct() {
138            if !move_obj.struct_tag().is_gas_coin() {
139                return Err(UserInputError::InvalidGasObject {
140                    object_id: gas_object.id(),
141                });
142            }
143            Ok(move_obj.get_coin_value_unchecked())
144        } else {
145            Err(UserInputError::InvalidGasObject {
146                object_id: gas_object.id(),
147            })
148        }
149    }
150
151    /// Fills in the gas a simulated transaction leaves unset: a zero price
152    /// becomes `reference_gas_price`, and a zero budget as much as the gas
153    /// coins can back, up to the protocol maximum.
154    pub fn fill_in_unset_simulation_gas(
155        transaction: &mut Transaction,
156        input_objects: &InputObjects,
157        reference_gas_price: u64,
158        protocol_config: &ProtocolConfig,
159    ) {
160        if transaction.gas_price() == 0 {
161            transaction.gas_data_mut().price = reference_gas_price;
162        }
163        if transaction.gas_budget() == 0 {
164            let min_gas_budget = protocol_config
165                .base_tx_cost_fixed()
166                .saturating_mul(transaction.gas_price());
167
168            // The gas budget is capped at the gas coins' combined balance rather than left
169            // at the protocol maximum, so that coins holding less than `max_tx_gas`
170            // still produce an estimate instead of being rejected for not covering a
171            // budget the caller never asked for.
172            let gas_balance = gas_coins_balance(input_objects, transaction.gas());
173
174            // The cap is raised back to the minimum budget a transaction may declare
175            // when the balance falls below it, so a balance too small to transact at
176            // all is still reported against the balance by the gas checks, rather than
177            // against a budget the caller never set.
178            let budget = std::cmp::min(protocol_config.max_tx_gas() as u128, gas_balance)
179                .max(min_gas_budget as u128);
180
181            transaction.gas_data_mut().budget = budget as u64;
182        }
183    }
184
185    /// Sums the balance of the gas coins `gas` refers to among `input_objects`
186    /// and skips all non-gas coins.
187    fn gas_coins_balance(input_objects: &InputObjects, gas: &[ObjectReference]) -> u128 {
188        let objects: HashMap<_, _> = input_objects
189            .iter()
190            .map(|object| (object.id(), object))
191            .collect();
192
193        gas.iter()
194            .filter_map(|gas_ref| objects.get(&gas_ref.object_id)?.as_object())
195            .filter_map(|object| get_gas_balance(object).ok())
196            .map(u128::from)
197            .sum()
198    }
199
200    /// Reports the gas a simulation ran with in `reported`, in place of what
201    /// the caller left unset — the mirror of [`fill_in_unset_simulation_gas`],
202    /// for the response rather than the run.
203    ///
204    /// A zero budget asks what the transaction costs, so it comes back as
205    /// `gas_used` rather than as the caller's own zero, which would say
206    /// nothing.
207    ///
208    /// Note what that costs: `gas_used` is not the budget the run
209    /// metered against, so a transaction reported this way does not hash to the
210    /// digest the effects are keyed by.
211    pub fn report_simulation_gas(reported: &mut GasPayment, simulated: &GasPayment, gas_used: u64) {
212        let estimating = reported.budget == 0;
213        *reported = simulated.clone();
214        if estimating {
215            reported.budget = gas_used;
216        }
217    }
218
219    /// Checks that every object `gas` refers to is an address-owned gas coin
220    /// present in `input_objects`, and that their combined balance covers
221    /// `gas_budget`.
222    pub fn check_gas_coins_cover_budget_in_simulation(
223        input_objects: &InputObjects,
224        gas: &[ObjectReference],
225        gas_budget: u64,
226    ) -> UserInputResult {
227        let objects: HashMap<_, _> = input_objects
228            .iter()
229            .map(|object| (object.id(), object))
230            .collect();
231
232        let mut gas_balance = 0u128;
233        for gas_ref in gas {
234            let read = objects
235                .get(&gas_ref.object_id)
236                .ok_or(UserInputError::ObjectNotFound {
237                    object_id: gas_ref.object_id,
238                    version: Some(gas_ref.version),
239                })?;
240            // `as_object` returning `None` means the object was deleted, which makes
241            // it a shared one, and gas cannot be shared.
242            let object = read.as_object().ok_or(UserInputError::MissingGasPayment)?;
243            if !object.is_address_owned() {
244                return Err(UserInputError::GasObjectNotOwnedObject {
245                    owner: object.owner,
246                });
247            }
248            gas_balance += get_gas_balance(object)? as u128;
249        }
250
251        if gas_balance < gas_budget as u128 {
252            return Err(UserInputError::GasBalanceTooLow {
253                gas_balance,
254                needed_gas_amount: gas_budget as u128,
255            });
256        }
257
258        Ok(())
259    }
260}