iota_types/gas_model/
gas_v1.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]
9mod checked {
10    use iota_protocol_config::*;
11    use move_core_types::vm_status::StatusCode;
12
13    use crate::{
14        ObjectID,
15        error::{ExecutionError, ExecutionErrorKind, UserInputError, UserInputResult},
16        gas::{self, GasCostSummary, IotaGasStatusAPI},
17        gas_model::{
18            gas_predicates::cost_table_for_version,
19            tables::{GasStatus, ZERO_COST_SCHEDULE},
20            units_types::CostTable,
21        },
22        transaction::ObjectReadResult,
23    };
24
25    /// A bucket defines a range of units that will be priced the same.
26    /// After execution a call to `GasStatus::bucketize` will round the
27    /// computation cost to `cost` for the bucket ([`min`, `max`]) the gas
28    /// used falls into.
29    #[expect(dead_code)]
30    pub(crate) struct ComputationBucket {
31        min: u64,
32        max: u64,
33        cost: u64,
34    }
35
36    impl ComputationBucket {
37        fn new(min: u64, max: u64, cost: u64) -> Self {
38            ComputationBucket { min, max, cost }
39        }
40
41        fn simple(min: u64, max: u64) -> Self {
42            Self::new(min, max, max)
43        }
44    }
45
46    fn get_bucket_cost(table: &[ComputationBucket], computation_cost: u64) -> u64 {
47        for bucket in table {
48            if bucket.max >= computation_cost {
49                return bucket.cost;
50            }
51        }
52        match table.last() {
53            // maybe not a literal here could be better?
54            None => 5_000_000,
55            Some(bucket) => bucket.cost,
56        }
57    }
58
59    // define the bucket table for computation charging
60    // If versioning defines multiple functions and
61    fn computation_bucket(max_bucket_cost: u64) -> Vec<ComputationBucket> {
62        assert!(max_bucket_cost >= 5_000_000);
63        vec![
64            ComputationBucket::simple(0, 1_000),
65            ComputationBucket::simple(1_000, 5_000),
66            ComputationBucket::simple(5_000, 10_000),
67            ComputationBucket::simple(10_000, 20_000),
68            ComputationBucket::simple(20_000, 50_000),
69            ComputationBucket::simple(50_000, 200_000),
70            ComputationBucket::simple(200_000, 1_000_000),
71            ComputationBucket::simple(1_000_000, max_bucket_cost),
72        ]
73    }
74
75    /// Portion of the storage rebate that gets passed on to the transaction
76    /// sender. The remainder will be burned, then re-minted + added to the
77    /// storage fund at the next epoch change
78    fn sender_rebate(storage_rebate: u64, storage_rebate_rate: u64) -> u64 {
79        // we round storage rebate such that `>= x.5` goes to x+1 (rounds up) and
80        // `< x.5` goes to x (truncates). We replicate `f32/64::round()`
81        const BASIS_POINTS: u128 = 10000;
82        (((storage_rebate as u128 * storage_rebate_rate as u128)
83        + (BASIS_POINTS / 2)) // integer rounding adds half of the BASIS_POINTS (denominator)
84        / BASIS_POINTS) as u64
85    }
86
87    /// A list of constant costs of various operations in IOTA.
88    pub struct IotaCostTable {
89        /// A flat fee charged for every transaction. This is also the minimum
90        /// amount of gas charged for a transaction.
91        pub(crate) min_transaction_cost: u64,
92        /// Maximum allowable budget for a transaction.
93        pub(crate) max_gas_budget: u64,
94        /// Computation cost per byte charged for package publish. This cost is
95        /// primarily determined by the cost to verify and link a
96        /// package. Note that this does not include the cost of writing
97        /// the package to the store.
98        package_publish_per_byte_cost: u64,
99        /// Per byte cost to read objects from the store. This is computation
100        /// cost instead of storage cost because it does not change the
101        /// amount of data stored on the db.
102        object_read_per_byte_cost: u64,
103        /// Unit cost of a byte in the storage. This will be used both for
104        /// charging for new storage as well as rebating for deleting
105        /// storage. That is, we expect users to get full refund on the
106        /// object storage when it's deleted.
107        storage_per_byte_cost: u64,
108        /// Execution cost table to be used.
109        pub execution_cost_table: CostTable,
110        /// Computation buckets to cost transaction in price groups
111        computation_bucket: Vec<ComputationBucket>,
112    }
113
114    impl std::fmt::Debug for IotaCostTable {
115        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116            // TODO: dump the fields.
117            write!(f, "IotaCostTable(...)")
118        }
119    }
120
121    impl IotaCostTable {
122        pub(crate) fn new(c: &ProtocolConfig, gas_price: u64) -> Self {
123            // gas_price here is the Reference Gas Price, however we may decide
124            // to change it to be the price passed in the transaction
125            let min_transaction_cost = c.base_tx_cost_fixed() * gas_price;
126            Self {
127                min_transaction_cost,
128                max_gas_budget: c.max_tx_gas(),
129                package_publish_per_byte_cost: c.package_publish_cost_per_byte(),
130                object_read_per_byte_cost: c.obj_access_cost_read_per_byte(),
131                storage_per_byte_cost: c.obj_data_cost_refundable(),
132                execution_cost_table: cost_table_for_version(c.gas_model_version()),
133                computation_bucket: computation_bucket(c.max_gas_computation_bucket()),
134            }
135        }
136
137        pub(crate) fn unmetered() -> Self {
138            Self {
139                min_transaction_cost: 0,
140                max_gas_budget: u64::MAX,
141                package_publish_per_byte_cost: 0,
142                object_read_per_byte_cost: 0,
143                storage_per_byte_cost: 0,
144                execution_cost_table: ZERO_COST_SCHEDULE.clone(),
145                // should not matter
146                computation_bucket: computation_bucket(5_000_000),
147            }
148        }
149    }
150
151    #[derive(Debug)]
152    pub struct PerObjectStorage {
153        /// storage_cost is the total storage gas to charge. This is computed
154        /// at the end of execution while determining storage charges.
155        /// It tracks `storage_bytes * obj_data_cost_refundable` as
156        /// described in `storage_gas_price`
157        /// It has been multiplied by the storage gas price. This is the new
158        /// storage rebate.
159        pub storage_cost: u64,
160        /// storage_rebate is the storage rebate (in IOTA) for in this object.
161        /// This is computed at the end of execution while determining storage
162        /// charges. The value is in IOTA.
163        pub storage_rebate: u64,
164        /// The object size post-transaction in bytes
165        pub new_size: u64,
166    }
167
168    #[derive(Debug)]
169    pub struct IotaGasStatus {
170        /// GasStatus as used by the VM, that is all the VM sees
171        pub gas_status: GasStatus,
172        /// Cost table contains a set of constant/config for the gas
173        /// model/charging
174        cost_table: IotaCostTable,
175        /// Gas budget for this gas status instance.
176        /// Typically the gas budget as defined in the
177        /// `TransactionData::GasData`
178        gas_budget: u64,
179        /// Computation cost after execution. This is the result of the gas used
180        /// by the `GasStatus` properly bucketized.
181        /// Starts at 0 and it is assigned in `bucketize_computation`.
182        computation_cost: u64,
183        /// Whether to charge or go unmetered
184        charge: bool,
185        /// Gas price for computation.
186        /// This is a multiplier on the final charge as related to the RGP
187        /// (reference gas price). Checked at signing: `gas_price >=
188        /// reference_gas_price` and then conceptually
189        /// `final_computation_cost = total_computation_cost * gas_price /
190        /// reference_gas_price`
191        gas_price: u64,
192        // Reference gas price as defined in protocol config.
193        // If `protocol_defined_base_fee' is enabled, this is a mandatory base fee paid to the
194        // protocol.
195        reference_gas_price: u64,
196        /// Gas price for storage. This is a multiplier on the final charge
197        /// as related to the storage gas price defined in the system
198        /// (`ProtocolConfig::storage_gas_price`).
199        /// Conceptually, given a constant `obj_data_cost_refundable`
200        /// (defined in `ProtocolConfig::obj_data_cost_refundable`)
201        /// `total_storage_cost = storage_bytes * obj_data_cost_refundable`
202        /// `final_storage_cost = total_storage_cost * storage_gas_price`
203        storage_gas_price: u64,
204        /// Per Object Storage Cost and Storage Rebate, used to get accumulated
205        /// values at the end of execution to determine storage charges
206        /// and rebates.
207        per_object_storage: Vec<(ObjectID, PerObjectStorage)>,
208        // storage rebate rate as defined in the ProtocolConfig
209        rebate_rate: u64,
210        /// Amount of storage rebate accumulated when we are running in
211        /// unmetered mode (i.e. system transaction). This allows us to
212        /// track how much storage rebate we need to retain in system
213        /// transactions.
214        unmetered_storage_rebate: u64,
215        /// Rounding value to round up gas charges.
216        gas_rounding_step: Option<u64>,
217        /// Flag to indicate whether the protocol-defined base fee is enabled,
218        /// in which case the reference gas price is burned.
219        protocol_defined_base_fee: bool,
220    }
221
222    impl IotaGasStatus {
223        fn new(
224            move_gas_status: GasStatus,
225            gas_budget: u64,
226            charge: bool,
227            gas_price: u64,
228            reference_gas_price: u64,
229            storage_gas_price: u64,
230            rebate_rate: u64,
231            gas_rounding_step: Option<u64>,
232            cost_table: IotaCostTable,
233            protocol_defined_base_fee: bool,
234        ) -> IotaGasStatus {
235            let gas_rounding_step = gas_rounding_step.map(|val| val.max(1));
236            IotaGasStatus {
237                gas_status: move_gas_status,
238                gas_budget,
239                charge,
240                computation_cost: 0,
241                gas_price,
242                reference_gas_price,
243                storage_gas_price,
244                per_object_storage: Vec::new(),
245                rebate_rate,
246                unmetered_storage_rebate: 0,
247                gas_rounding_step,
248                cost_table,
249                protocol_defined_base_fee,
250            }
251        }
252
253        pub(crate) fn new_with_budget(
254            gas_budget: u64,
255            gas_price: u64,
256            reference_gas_price: u64,
257            config: &ProtocolConfig,
258        ) -> IotaGasStatus {
259            let storage_gas_price = config.storage_gas_price();
260            let computation_budget = computation_budget(gas_budget, gas_price, config);
261            let iota_cost_table = IotaCostTable::new(config, gas_price);
262            let gas_rounding_step = config.gas_rounding_step_as_option();
263            Self::new(
264                GasStatus::new(
265                    iota_cost_table.execution_cost_table.clone(),
266                    computation_budget,
267                    gas_price,
268                    config.gas_model_version(),
269                ),
270                gas_budget,
271                true,
272                gas_price,
273                reference_gas_price,
274                storage_gas_price,
275                config.storage_rebate_rate(),
276                gas_rounding_step,
277                iota_cost_table,
278                config.protocol_defined_base_fee(),
279            )
280        }
281
282        pub fn new_unmetered() -> IotaGasStatus {
283            Self::new(
284                GasStatus::new_unmetered(),
285                0,
286                false,
287                0,
288                0,
289                0,
290                0,
291                None,
292                IotaCostTable::unmetered(),
293                false,
294            )
295        }
296
297        pub fn reference_gas_price(&self) -> u64 {
298            self.reference_gas_price
299        }
300
301        // Check whether gas arguments are legit:
302        // 1. Gas object has an address owner.
303        // 2. Gas budget is between min and max budget allowed
304        // 3. Gas balance (all gas coins together) is bigger or equal to budget
305        pub(crate) fn check_gas_balance(
306            &self,
307            gas_objs: &[&ObjectReadResult],
308            gas_budget: u64,
309        ) -> UserInputResult {
310            // 1. All gas objects have an address owner
311            for gas_object in gas_objs {
312                // if as_object() returns None, it means the object has been deleted (and
313                // therefore must be a shared object).
314                if let Some(obj) = gas_object.as_object() {
315                    if !obj.is_address_owned() {
316                        return Err(UserInputError::GasObjectNotOwnedObject { owner: obj.owner });
317                    }
318                } else {
319                    // This case should never happen (because gas can't be a shared object), but we
320                    // handle this case for future-proofing
321                    return Err(UserInputError::MissingGasPayment);
322                }
323            }
324
325            // 2. Gas budget is between min and max budget allowed
326            if gas_budget > self.cost_table.max_gas_budget {
327                return Err(UserInputError::GasBudgetTooHigh {
328                    gas_budget,
329                    max_budget: self.cost_table.max_gas_budget,
330                });
331            }
332            if gas_budget < self.cost_table.min_transaction_cost {
333                return Err(UserInputError::GasBudgetTooLow {
334                    gas_budget,
335                    min_budget: self.cost_table.min_transaction_cost,
336                });
337            }
338
339            // 3. Gas balance (all gas coins together) is bigger or equal to budget
340            let mut gas_balance = 0u128;
341            for gas_obj in gas_objs {
342                // expect is safe because we already checked that all gas objects have an
343                // address owner
344                gas_balance +=
345                    gas::get_gas_balance(gas_obj.as_object().expect("object must be owned"))?
346                        as u128;
347            }
348            if gas_balance < gas_budget as u128 {
349                Err(UserInputError::GasBalanceTooLow {
350                    gas_balance,
351                    needed_gas_amount: gas_budget as u128,
352                })
353            } else {
354                Ok(())
355            }
356        }
357
358        fn storage_cost(&self) -> u64 {
359            self.storage_gas_units()
360        }
361
362        pub fn per_object_storage(&self) -> &Vec<(ObjectID, PerObjectStorage)> {
363            &self.per_object_storage
364        }
365    }
366
367    impl IotaGasStatusAPI for IotaGasStatus {
368        fn is_unmetered(&self) -> bool {
369            !self.charge
370        }
371
372        fn move_gas_status(&self) -> &GasStatus {
373            &self.gas_status
374        }
375
376        fn move_gas_status_mut(&mut self) -> &mut GasStatus {
377            &mut self.gas_status
378        }
379
380        fn bucketize_computation(&mut self) -> Result<(), ExecutionError> {
381            let mut computation_units = self.gas_status.gas_used_pre_gas_price();
382            if let Some(gas_rounding) = self.gas_rounding_step {
383                if gas_rounding > 0
384                    && (computation_units == 0 || computation_units % gas_rounding > 0)
385                {
386                    computation_units = ((computation_units / gas_rounding) + 1) * gas_rounding;
387                }
388            } else {
389                // use the max value of the bucket that the computation_units falls into.
390                computation_units =
391                    get_bucket_cost(&self.cost_table.computation_bucket, computation_units);
392            };
393            let computation_cost = computation_units * self.gas_price;
394            if self.gas_budget <= computation_cost {
395                self.computation_cost = self.gas_budget;
396                Err(ExecutionErrorKind::InsufficientGas.into())
397            } else {
398                self.computation_cost = computation_cost;
399                Ok(())
400            }
401        }
402
403        /// Returns the final (computation cost, storage cost, storage rebate)
404        /// of the gas meter. We use initial budget, combined with
405        /// remaining gas and storage cost to derive computation cost.
406        fn summary(&self) -> GasCostSummary {
407            // compute computation cost burned and storage rebate, both rebate and non
408            // refundable fee
409            let computation_cost_burned = if self.protocol_defined_base_fee {
410                // when protocol_defined_base_fee is enabled, the computation cost burned is
411                // computed as follows:
412                // computation_cost_burned = computation_units * reference_gas_price.
413                // = (computation_cost / gas_price) * reference_gas_price
414                self.computation_cost * self.reference_gas_price / self.gas_price
415            } else {
416                // when protocol_defined_base_fee is disabled, the entire computation cost is
417                // burned.
418                self.computation_cost
419            };
420            let storage_rebate = self.storage_rebate();
421            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
422            assert!(sender_rebate <= storage_rebate);
423            let non_refundable_storage_fee = storage_rebate - sender_rebate;
424            GasCostSummary {
425                computation_cost: self.computation_cost,
426                computation_cost_burned,
427                storage_cost: self.storage_cost(),
428                storage_rebate: sender_rebate,
429                non_refundable_storage_fee,
430            }
431        }
432
433        fn gas_budget(&self) -> u64 {
434            self.gas_budget
435        }
436
437        fn gas_price(&self) -> u64 {
438            self.gas_price
439        }
440
441        fn storage_gas_units(&self) -> u64 {
442            self.per_object_storage
443                .iter()
444                .map(|(_, per_object)| per_object.storage_cost)
445                .sum()
446        }
447
448        fn storage_rebate(&self) -> u64 {
449            self.per_object_storage
450                .iter()
451                .map(|(_, per_object)| per_object.storage_rebate)
452                .sum()
453        }
454
455        fn unmetered_storage_rebate(&self) -> u64 {
456            self.unmetered_storage_rebate
457        }
458
459        fn gas_used(&self) -> u64 {
460            self.gas_status.gas_used_pre_gas_price()
461        }
462
463        fn reset_storage_cost_and_rebate(&mut self) {
464            self.per_object_storage = Vec::new();
465            self.unmetered_storage_rebate = 0;
466        }
467
468        fn charge_storage_read(&mut self, size: usize) -> Result<(), ExecutionError> {
469            self.gas_status
470                .charge_bytes(size, self.cost_table.object_read_per_byte_cost)
471                .map_err(|e| {
472                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
473                    ExecutionErrorKind::InsufficientGas.into()
474                })
475        }
476
477        fn charge_publish_package(&mut self, size: usize) -> Result<(), ExecutionError> {
478            self.gas_status
479                .charge_bytes(size, self.cost_table.package_publish_per_byte_cost)
480                .map_err(|e| {
481                    debug_assert_eq!(e.major_status(), StatusCode::OUT_OF_GAS);
482                    ExecutionErrorKind::InsufficientGas.into()
483                })
484        }
485
486        /// Update `storage_rebate` and `storage_gas_units` for each object in
487        /// the transaction. There is no charge in this function.
488        /// Charges will all be applied together at the end
489        /// (`track_storage_mutation`).
490        /// Return the new storage rebate (cost of object storage) according to
491        /// `new_size`.
492        fn track_storage_mutation(
493            &mut self,
494            object_id: ObjectID,
495            new_size: usize,
496            storage_rebate: u64,
497        ) -> u64 {
498            if self.is_unmetered() {
499                self.unmetered_storage_rebate += storage_rebate;
500                return 0;
501            }
502
503            // compute and track cost (based on size)
504            let new_size = new_size as u64;
505            let storage_cost =
506                new_size * self.cost_table.storage_per_byte_cost * self.storage_gas_price;
507            // track rebate
508
509            self.per_object_storage.push((
510                object_id,
511                PerObjectStorage {
512                    storage_cost,
513                    storage_rebate,
514                    new_size,
515                },
516            ));
517            // return the new object rebate (object storage cost)
518            storage_cost
519        }
520
521        fn charge_storage_and_rebate(&mut self) -> Result<(), ExecutionError> {
522            let storage_rebate = self.storage_rebate();
523            let storage_cost = self.storage_cost();
524            let sender_rebate = sender_rebate(storage_rebate, self.rebate_rate);
525            assert!(sender_rebate <= storage_rebate);
526            if sender_rebate >= storage_cost {
527                // there is more rebate than cost, when deducting gas we are adding
528                // to whatever is the current amount charged so we are `Ok`
529                Ok(())
530            } else {
531                let gas_left = self.gas_budget - self.computation_cost;
532                // we have to charge for storage and may go out of gas, check
533                if gas_left < storage_cost - sender_rebate {
534                    // Running out of gas would cause the temporary store to reset
535                    // and zero storage and rebate.
536                    // The remaining_gas will be 0 and we will charge all in computation
537                    Err(ExecutionErrorKind::InsufficientGas.into())
538                } else {
539                    Ok(())
540                }
541            }
542        }
543
544        fn adjust_computation_on_out_of_gas(&mut self) {
545            self.per_object_storage = Vec::new();
546            self.computation_cost = self.gas_budget;
547        }
548    }
549
550    pub fn computation_budget(gas_budget: u64, gas_price: u64, config: &ProtocolConfig) -> u64 {
551        let max_computation_budget = config.max_gas_computation_bucket() * gas_price;
552
553        if gas_budget > max_computation_budget {
554            max_computation_budget
555        } else {
556            gas_budget
557        }
558    }
559}