Skip to main content

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