iota_types/gas_model/
tables.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use move_binary_format::errors::{PartialVMError, PartialVMResult};
8use move_core_types::{
9    gas_algebra::{AbstractMemorySize, InternalGas},
10    vm_status::StatusCode,
11};
12use move_vm_profiler::GasProfiler;
13use once_cell::sync::Lazy;
14
15use crate::gas_model::units_types::{CostTable, Gas, GasCost};
16
17/// VM flat fee
18pub const VM_FLAT_FEE: Gas = Gas::new(8_000);
19
20/// The size in bytes for a non-string or address constant on the stack
21pub const CONST_SIZE: AbstractMemorySize = AbstractMemorySize::new(16);
22
23/// The size in bytes for a reference on the stack
24pub const REFERENCE_SIZE: AbstractMemorySize = AbstractMemorySize::new(8);
25
26/// The size of a struct in bytes
27pub const STRUCT_SIZE: AbstractMemorySize = AbstractMemorySize::new(2);
28
29/// The size of a vector (without its containing data) in bytes
30pub const VEC_SIZE: AbstractMemorySize = AbstractMemorySize::new(8);
31
32/// For exists checks on data that doesn't exists this is the multiplier that is
33/// used.
34pub const MIN_EXISTS_DATA_SIZE: AbstractMemorySize = AbstractMemorySize::new(100);
35
36pub static ZERO_COST_SCHEDULE: Lazy<CostTable> = Lazy::new(zero_cost_schedule);
37
38pub static INITIAL_COST_SCHEDULE: Lazy<CostTable> = Lazy::new(initial_cost_schedule_v1);
39
40/// The Move VM implementation of state for gas metering.
41///
42/// Initialize with a `CostTable` and the gas provided to the transaction.
43/// Provide all the proper guarantees about gas metering in the Move VM.
44///
45/// Every client must use an instance of this type to interact with the Move VM.
46#[derive(Debug)]
47pub struct GasStatus {
48    pub gas_model_version: u64,
49    cost_table: CostTable,
50    pub gas_left: InternalGas,
51    gas_price: u64,
52    initial_budget: InternalGas,
53    pub charge: bool,
54
55    // The current height of the operand stack, and the maximal height that it has reached.
56    stack_height_high_water_mark: u64,
57    stack_height_current: u64,
58    stack_height_next_tier_start: Option<u64>,
59    stack_height_current_tier_mult: u64,
60
61    // The current (abstract) size  of the operand stack and the maximal size that it has reached.
62    stack_size_high_water_mark: u64,
63    stack_size_current: u64,
64    stack_size_next_tier_start: Option<u64>,
65    stack_size_current_tier_mult: u64,
66
67    // The total number of bytecode instructions that have been executed in the transaction.
68    instructions_executed: u64,
69    instructions_next_tier_start: Option<u64>,
70    instructions_current_tier_mult: u64,
71
72    pub profiler: Option<GasProfiler>,
73    pub num_native_calls: u64,
74}
75
76impl GasStatus {
77    /// Initialize the gas state with metering enabled.
78    ///
79    /// Charge for every operation and fail when there is no more gas to pay for
80    /// operations. This is the instantiation that must be used when
81    /// executing a user script.
82    pub fn new(cost_table: CostTable, budget: u64, gas_price: u64, gas_model_version: u64) -> Self {
83        assert!(gas_price > 0, "gas price cannot be 0");
84        let budget_in_unit = budget / gas_price;
85        let gas_left = Self::to_internal_units(budget_in_unit);
86
87        let (stack_height_current_tier_mult, stack_height_next_tier_start) =
88            cost_table.stack_height_tier(0);
89        let (stack_size_current_tier_mult, stack_size_next_tier_start) =
90            cost_table.stack_size_tier(0);
91        let (instructions_current_tier_mult, instructions_next_tier_start) =
92            cost_table.instruction_tier(0);
93        Self {
94            gas_model_version,
95            gas_left,
96            gas_price,
97            initial_budget: gas_left,
98            cost_table,
99            charge: true,
100            stack_height_high_water_mark: 0,
101            stack_height_current: 0,
102            stack_size_high_water_mark: 0,
103            stack_size_current: 0,
104            instructions_executed: 0,
105            stack_height_current_tier_mult,
106            stack_size_current_tier_mult,
107            instructions_current_tier_mult,
108            stack_height_next_tier_start,
109            stack_size_next_tier_start,
110            instructions_next_tier_start,
111            profiler: None,
112            num_native_calls: 0,
113        }
114    }
115
116    /// Initialize the gas state with metering disabled.
117    ///
118    /// It should be used by clients in very specific cases and when executing
119    /// system code that does not have to charge the user.
120    pub fn new_unmetered() -> Self {
121        Self {
122            gas_model_version: 1,
123            gas_left: InternalGas::new(0),
124            gas_price: 1,
125            initial_budget: InternalGas::new(0),
126            cost_table: ZERO_COST_SCHEDULE.clone(),
127            charge: false,
128            stack_height_high_water_mark: 0,
129            stack_height_current: 0,
130            stack_size_high_water_mark: 0,
131            stack_size_current: 0,
132            instructions_executed: 0,
133            stack_height_current_tier_mult: 0,
134            stack_size_current_tier_mult: 0,
135            instructions_current_tier_mult: 0,
136            stack_height_next_tier_start: None,
137            stack_size_next_tier_start: None,
138            instructions_next_tier_start: None,
139            profiler: None,
140            num_native_calls: 0,
141        }
142    }
143
144    const INTERNAL_UNIT_MULTIPLIER: u64 = 1000;
145
146    fn to_internal_units(val: u64) -> InternalGas {
147        InternalGas::new(val * Self::INTERNAL_UNIT_MULTIPLIER)
148    }
149
150    #[expect(dead_code)]
151    fn to_nanos(&self, val: InternalGas) -> u64 {
152        let gas: Gas = InternalGas::to_unit_round_down(val);
153        u64::from(gas) * self.gas_price
154    }
155
156    pub fn push_stack(&mut self, pushes: u64) -> PartialVMResult<()> {
157        match self.stack_height_current.checked_add(pushes) {
158            // We should never hit this.
159            None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
160            Some(new_height) => {
161                if new_height > self.stack_height_high_water_mark {
162                    self.stack_height_high_water_mark = new_height;
163                }
164                self.stack_height_current = new_height;
165            }
166        }
167
168        if let Some(stack_height_tier_next) = self.stack_height_next_tier_start {
169            if self.stack_height_current > stack_height_tier_next {
170                let (next_mul, next_tier) =
171                    self.cost_table.stack_height_tier(self.stack_height_current);
172                self.stack_height_current_tier_mult = next_mul;
173                self.stack_height_next_tier_start = next_tier;
174            }
175        }
176
177        Ok(())
178    }
179
180    pub fn pop_stack(&mut self, pops: u64) {
181        self.stack_height_current = self.stack_height_current.saturating_sub(pops);
182    }
183
184    pub fn increase_instruction_count(&mut self, amount: u64) -> PartialVMResult<()> {
185        match self.instructions_executed.checked_add(amount) {
186            None => return Err(PartialVMError::new(StatusCode::PC_OVERFLOW)),
187            Some(new_pc) => {
188                self.instructions_executed = new_pc;
189            }
190        }
191
192        if let Some(instr_tier_next) = self.instructions_next_tier_start {
193            if self.instructions_executed > instr_tier_next {
194                let (instr_cost, next_tier) =
195                    self.cost_table.instruction_tier(self.instructions_executed);
196                self.instructions_current_tier_mult = instr_cost;
197                self.instructions_next_tier_start = next_tier;
198            }
199        }
200
201        Ok(())
202    }
203
204    pub fn increase_stack_size(&mut self, size_amount: u64) -> PartialVMResult<()> {
205        match self.stack_size_current.checked_add(size_amount) {
206            None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
207            Some(new_size) => {
208                if new_size > self.stack_size_high_water_mark {
209                    self.stack_size_high_water_mark = new_size;
210                }
211                self.stack_size_current = new_size;
212            }
213        }
214
215        if let Some(stack_size_tier_next) = self.stack_size_next_tier_start {
216            if self.stack_size_current > stack_size_tier_next {
217                let (next_mul, next_tier) =
218                    self.cost_table.stack_size_tier(self.stack_size_current);
219                self.stack_size_current_tier_mult = next_mul;
220                self.stack_size_next_tier_start = next_tier;
221            }
222        }
223
224        Ok(())
225    }
226
227    pub fn decrease_stack_size(&mut self, size_amount: u64) {
228        let new_size = self.stack_size_current.saturating_sub(size_amount);
229        if new_size > self.stack_size_high_water_mark {
230            self.stack_size_high_water_mark = new_size;
231        }
232        self.stack_size_current = new_size;
233    }
234
235    /// Given: pushes + pops + increase + decrease in size for an instruction
236    /// charge for the execution of the instruction.
237    pub fn charge(
238        &mut self,
239        num_instructions: u64,
240        pushes: u64,
241        pops: u64,
242        incr_size: u64,
243        _decr_size: u64,
244    ) -> PartialVMResult<()> {
245        self.push_stack(pushes)?;
246        self.increase_instruction_count(num_instructions)?;
247        self.increase_stack_size(incr_size)?;
248
249        self.deduct_gas(
250            GasCost::new(
251                self.instructions_current_tier_mult
252                    .checked_mul(num_instructions)
253                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
254                self.stack_size_current_tier_mult
255                    .checked_mul(incr_size)
256                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
257                self.stack_height_current_tier_mult
258                    .checked_mul(pushes)
259                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
260            )
261            .total_internal(),
262        )?;
263
264        // self.decrease_stack_size(decr_size);
265        self.pop_stack(pops);
266        Ok(())
267    }
268
269    /// Return the `CostTable` behind this `GasStatus`.
270    pub fn cost_table(&self) -> &CostTable {
271        &self.cost_table
272    }
273
274    /// Return the gas left.
275    pub fn remaining_gas(&self) -> Gas {
276        self.gas_left.to_unit_round_down()
277    }
278
279    /// Charge a given amount of gas and fail if not enough gas units are left.
280    pub fn deduct_gas(&mut self, amount: InternalGas) -> PartialVMResult<()> {
281        if !self.charge {
282            return Ok(());
283        }
284
285        match self.gas_left.checked_sub(amount) {
286            Some(gas_left) => {
287                self.gas_left = gas_left;
288                Ok(())
289            }
290            None => {
291                self.gas_left = InternalGas::new(0);
292                Err(PartialVMError::new(StatusCode::OUT_OF_GAS))
293            }
294        }
295    }
296
297    pub fn record_native_call(&mut self) {
298        self.num_native_calls = self.num_native_calls.saturating_add(1);
299    }
300
301    // Deduct the amount provided with no conversion, as if it was InternalGasUnit
302    fn deduct_units(&mut self, amount: u64) -> PartialVMResult<()> {
303        self.deduct_gas(InternalGas::new(amount))
304    }
305
306    pub fn set_metering(&mut self, enabled: bool) {
307        self.charge = enabled
308    }
309
310    // The amount of gas used, it does not include the multiplication for the gas
311    // price
312    pub fn gas_used_pre_gas_price(&self) -> u64 {
313        let gas: Gas = match self.initial_budget.checked_sub(self.gas_left) {
314            Some(val) => InternalGas::to_unit_round_down(val),
315            None => InternalGas::to_unit_round_down(self.initial_budget),
316        };
317        u64::from(gas)
318    }
319
320    // Charge the number of bytes with the cost per byte value
321    // As more bytes are read throughout the computation the cost per bytes is
322    // increased.
323    pub fn charge_bytes(&mut self, size: usize, cost_per_byte: u64) -> PartialVMResult<()> {
324        let computation_cost = size as u64 * cost_per_byte;
325        self.deduct_units(computation_cost)
326    }
327
328    pub fn gas_price(&self) -> u64 {
329        self.gas_price
330    }
331
332    pub fn stack_height_high_water_mark(&self) -> u64 {
333        self.stack_height_high_water_mark
334    }
335
336    pub fn stack_size_high_water_mark(&self) -> u64 {
337        self.stack_size_high_water_mark
338    }
339
340    pub fn instructions_executed(&self) -> u64 {
341        self.instructions_executed
342    }
343}
344
345pub fn zero_cost_schedule() -> CostTable {
346    let mut zero_tier = BTreeMap::new();
347    zero_tier.insert(0, 0);
348    CostTable {
349        instruction_tiers: zero_tier.clone(),
350        stack_size_tiers: zero_tier.clone(),
351        stack_height_tiers: zero_tier,
352    }
353}
354
355pub fn unit_cost_schedule() -> CostTable {
356    let mut unit_tier = BTreeMap::new();
357    unit_tier.insert(0, 1);
358    CostTable {
359        instruction_tiers: unit_tier.clone(),
360        stack_size_tiers: unit_tier.clone(),
361        stack_height_tiers: unit_tier,
362    }
363}
364
365pub fn initial_cost_schedule_v1() -> CostTable {
366    let instruction_tiers: BTreeMap<u64, u64> = vec![
367        (0, 1),
368        (20_000, 2),
369        (50_000, 10),
370        (100_000, 50),
371        (200_000, 100),
372        (10_000_000, 1000),
373    ]
374    .into_iter()
375    .collect();
376
377    let stack_height_tiers: BTreeMap<u64, u64> =
378        vec![(0, 1), (1_000, 2), (10_000, 10)].into_iter().collect();
379
380    let stack_size_tiers: BTreeMap<u64, u64> = vec![
381        (0, 1),
382        (100_000, 2),        // ~100K
383        (500_000, 5),        // ~500K
384        (1_000_000, 100),    // ~1M
385        (100_000_000, 1000), // ~100M
386    ]
387    .into_iter()
388    .collect();
389
390    CostTable {
391        instruction_tiers,
392        stack_size_tiers,
393        stack_height_tiers,
394    }
395}
396
397// Convert from our representation of gas costs to the type that the MoveVM
398// expects for unit tests. We don't want our gas depending on the MoveVM test
399// utils and we don't want to fix our representation to whatever is there, so
400// instead we perform this translation from our gas units and cost schedule to
401// the one expected by the Move unit tests.
402pub fn initial_cost_schedule_for_unit_tests() -> move_vm_test_utils::gas_schedule::CostTable {
403    let table = initial_cost_schedule_v1();
404    move_vm_test_utils::gas_schedule::CostTable {
405        instruction_tiers: table.instruction_tiers,
406        stack_height_tiers: table.stack_height_tiers,
407        stack_size_tiers: table.stack_size_tiers,
408    }
409}