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        let (stack_height_current_tier_mult, stack_height_next_tier_start) =
87            cost_table.stack_height_tier(0);
88        let (stack_size_current_tier_mult, stack_size_next_tier_start) =
89            cost_table.stack_size_tier(0);
90        let (instructions_current_tier_mult, instructions_next_tier_start) =
91            cost_table.instruction_tier(0);
92        Self {
93            gas_model_version,
94            gas_left,
95            gas_price,
96            initial_budget: gas_left,
97            cost_table,
98            charge: true,
99            stack_height_high_water_mark: 0,
100            stack_height_current: 0,
101            stack_size_high_water_mark: 0,
102            stack_size_current: 0,
103            instructions_executed: 0,
104            stack_height_current_tier_mult,
105            stack_size_current_tier_mult,
106            instructions_current_tier_mult,
107            stack_height_next_tier_start,
108            stack_size_next_tier_start,
109            instructions_next_tier_start,
110            profiler: None,
111            num_native_calls: 0,
112        }
113    }
114
115    /// Initialize the gas state with metering disabled.
116    ///
117    /// It should be used by clients in very specific cases and when executing
118    /// system code that does not have to charge the user.
119    pub fn new_unmetered() -> Self {
120        Self {
121            gas_model_version: 1,
122            gas_left: InternalGas::new(0),
123            gas_price: 1,
124            initial_budget: InternalGas::new(0),
125            cost_table: ZERO_COST_SCHEDULE.clone(),
126            charge: false,
127            stack_height_high_water_mark: 0,
128            stack_height_current: 0,
129            stack_size_high_water_mark: 0,
130            stack_size_current: 0,
131            instructions_executed: 0,
132            stack_height_current_tier_mult: 0,
133            stack_size_current_tier_mult: 0,
134            instructions_current_tier_mult: 0,
135            stack_height_next_tier_start: None,
136            stack_size_next_tier_start: None,
137            instructions_next_tier_start: None,
138            profiler: None,
139            num_native_calls: 0,
140        }
141    }
142
143    const INTERNAL_UNIT_MULTIPLIER: u64 = 1000;
144
145    fn to_internal_units(val: u64) -> InternalGas {
146        InternalGas::new(val * Self::INTERNAL_UNIT_MULTIPLIER)
147    }
148
149    #[expect(dead_code)]
150    fn to_nanos(&self, val: InternalGas) -> u64 {
151        let gas: Gas = InternalGas::to_unit_round_down(val);
152        u64::from(gas) * self.gas_price
153    }
154
155    pub fn push_stack(&mut self, pushes: u64) -> PartialVMResult<()> {
156        match self.stack_height_current.checked_add(pushes) {
157            // We should never hit this.
158            None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
159            Some(new_height) => {
160                if new_height > self.stack_height_high_water_mark {
161                    self.stack_height_high_water_mark = new_height;
162                }
163                self.stack_height_current = new_height;
164            }
165        }
166
167        if let Some(stack_height_tier_next) = self.stack_height_next_tier_start {
168            if self.stack_height_current > stack_height_tier_next {
169                let (next_mul, next_tier) =
170                    self.cost_table.stack_height_tier(self.stack_height_current);
171                self.stack_height_current_tier_mult = next_mul;
172                self.stack_height_next_tier_start = next_tier;
173            }
174        }
175
176        Ok(())
177    }
178
179    pub fn pop_stack(&mut self, pops: u64) {
180        self.stack_height_current = self.stack_height_current.saturating_sub(pops);
181    }
182
183    pub fn increase_instruction_count(&mut self, amount: u64) -> PartialVMResult<()> {
184        match self.instructions_executed.checked_add(amount) {
185            None => return Err(PartialVMError::new(StatusCode::PC_OVERFLOW)),
186            Some(new_pc) => {
187                self.instructions_executed = new_pc;
188            }
189        }
190
191        if let Some(instr_tier_next) = self.instructions_next_tier_start {
192            if self.instructions_executed > instr_tier_next {
193                let (instr_cost, next_tier) =
194                    self.cost_table.instruction_tier(self.instructions_executed);
195                self.instructions_current_tier_mult = instr_cost;
196                self.instructions_next_tier_start = next_tier;
197            }
198        }
199
200        Ok(())
201    }
202
203    pub fn increase_stack_size(&mut self, size_amount: u64) -> PartialVMResult<()> {
204        match self.stack_size_current.checked_add(size_amount) {
205            None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
206            Some(new_size) => {
207                if new_size > self.stack_size_high_water_mark {
208                    self.stack_size_high_water_mark = new_size;
209                }
210                self.stack_size_current = new_size;
211            }
212        }
213
214        if let Some(stack_size_tier_next) = self.stack_size_next_tier_start {
215            if self.stack_size_current > stack_size_tier_next {
216                let (next_mul, next_tier) =
217                    self.cost_table.stack_size_tier(self.stack_size_current);
218                self.stack_size_current_tier_mult = next_mul;
219                self.stack_size_next_tier_start = next_tier;
220            }
221        }
222
223        Ok(())
224    }
225
226    pub fn decrease_stack_size(&mut self, size_amount: u64) {
227        let new_size = self.stack_size_current.saturating_sub(size_amount);
228        if new_size > self.stack_size_high_water_mark {
229            self.stack_size_high_water_mark = new_size;
230        }
231        self.stack_size_current = new_size;
232    }
233
234    /// Given: pushes + pops + increase + decrease in size for an instruction
235    /// charge for the execution of the instruction.
236    pub fn charge(
237        &mut self,
238        num_instructions: u64,
239        pushes: u64,
240        pops: u64,
241        incr_size: u64,
242        _decr_size: u64,
243    ) -> PartialVMResult<()> {
244        self.push_stack(pushes)?;
245        self.increase_instruction_count(num_instructions)?;
246        self.increase_stack_size(incr_size)?;
247
248        self.deduct_gas(
249            GasCost::new(
250                self.instructions_current_tier_mult
251                    .checked_mul(num_instructions)
252                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
253                self.stack_size_current_tier_mult
254                    .checked_mul(incr_size)
255                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
256                self.stack_height_current_tier_mult
257                    .checked_mul(pushes)
258                    .ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
259            )
260            .total_internal(),
261        )?;
262
263        // self.decrease_stack_size(decr_size);
264        self.pop_stack(pops);
265        Ok(())
266    }
267
268    /// Return the `CostTable` behind this `GasStatus`.
269    pub fn cost_table(&self) -> &CostTable {
270        &self.cost_table
271    }
272
273    /// Return the gas left.
274    pub fn remaining_gas(&self) -> Gas {
275        self.gas_left.to_unit_round_down()
276    }
277
278    /// Charge a given amount of gas and fail if not enough gas units are left.
279    pub fn deduct_gas(&mut self, amount: InternalGas) -> PartialVMResult<()> {
280        if !self.charge {
281            return Ok(());
282        }
283
284        match self.gas_left.checked_sub(amount) {
285            Some(gas_left) => {
286                self.gas_left = gas_left;
287                Ok(())
288            }
289            None => {
290                self.gas_left = InternalGas::new(0);
291                Err(PartialVMError::new(StatusCode::OUT_OF_GAS))
292            }
293        }
294    }
295
296    pub fn record_native_call(&mut self) {
297        self.num_native_calls = self.num_native_calls.saturating_add(1);
298    }
299
300    // Deduct the amount provided with no conversion, as if it was InternalGasUnit
301    fn deduct_units(&mut self, amount: u64) -> PartialVMResult<()> {
302        self.deduct_gas(InternalGas::new(amount))
303    }
304
305    pub fn set_metering(&mut self, enabled: bool) {
306        self.charge = enabled
307    }
308
309    // The amount of gas used, it does not include the multiplication for the gas
310    // price
311    pub fn gas_used_pre_gas_price(&self) -> u64 {
312        let gas: Gas = match self.initial_budget.checked_sub(self.gas_left) {
313            Some(val) => InternalGas::to_unit_round_down(val),
314            None => InternalGas::to_unit_round_down(self.initial_budget),
315        };
316        u64::from(gas)
317    }
318
319    // Charge the number of bytes with the cost per byte value
320    // As more bytes are read throughout the computation the cost per bytes is
321    // increased.
322    pub fn charge_bytes(&mut self, size: usize, cost_per_byte: u64) -> PartialVMResult<()> {
323        let computation_cost = size as u64 * cost_per_byte;
324        self.deduct_units(computation_cost)
325    }
326
327    pub fn gas_price(&self) -> u64 {
328        self.gas_price
329    }
330
331    pub fn stack_height_high_water_mark(&self) -> u64 {
332        self.stack_height_high_water_mark
333    }
334
335    pub fn stack_size_high_water_mark(&self) -> u64 {
336        self.stack_size_high_water_mark
337    }
338
339    pub fn instructions_executed(&self) -> u64 {
340        self.instructions_executed
341    }
342}
343
344pub fn zero_cost_schedule() -> CostTable {
345    let mut zero_tier = BTreeMap::new();
346    zero_tier.insert(0, 0);
347    CostTable {
348        instruction_tiers: zero_tier.clone(),
349        stack_size_tiers: zero_tier.clone(),
350        stack_height_tiers: zero_tier,
351    }
352}
353
354pub fn unit_cost_schedule() -> CostTable {
355    let mut unit_tier = BTreeMap::new();
356    unit_tier.insert(0, 1);
357    CostTable {
358        instruction_tiers: unit_tier.clone(),
359        stack_size_tiers: unit_tier.clone(),
360        stack_height_tiers: unit_tier,
361    }
362}
363
364pub fn initial_cost_schedule_v1() -> CostTable {
365    let instruction_tiers: BTreeMap<u64, u64> = vec![
366        (0, 1),
367        (20_000, 2),
368        (50_000, 10),
369        (100_000, 50),
370        (200_000, 100),
371        (10_000_000, 1000),
372    ]
373    .into_iter()
374    .collect();
375
376    let stack_height_tiers: BTreeMap<u64, u64> =
377        vec![(0, 1), (1_000, 2), (10_000, 10)].into_iter().collect();
378
379    let stack_size_tiers: BTreeMap<u64, u64> = vec![
380        (0, 1),
381        (100_000, 2),        // ~100K
382        (500_000, 5),        // ~500K
383        (1_000_000, 100),    // ~1M
384        (100_000_000, 1000), // ~100M
385    ]
386    .into_iter()
387    .collect();
388
389    CostTable {
390        instruction_tiers,
391        stack_size_tiers,
392        stack_height_tiers,
393    }
394}
395
396// Convert from our representation of gas costs to the type that the MoveVM
397// expects for unit tests. We don't want our gas depending on the MoveVM test
398// utils and we don't want to fix our representation to whatever is there, so
399// instead we perform this translation from our gas units and cost schedule to
400// the one expected by the Move unit tests.
401pub fn initial_cost_schedule_for_unit_tests() -> move_vm_test_utils::gas_schedule::CostTable {
402    let table = initial_cost_schedule_v1();
403    move_vm_test_utils::gas_schedule::CostTable {
404        instruction_tiers: table.instruction_tiers,
405        stack_height_tiers: table.stack_height_tiers,
406        stack_size_tiers: table.stack_size_tiers,
407    }
408}