use std::collections::BTreeMap;
use move_binary_format::errors::{PartialVMError, PartialVMResult};
use move_core_types::{
gas_algebra::{AbstractMemorySize, InternalGas, NumArgs, NumBytes},
language_storage::ModuleId,
vm_status::StatusCode,
};
use move_vm_profiler::GasProfiler;
use move_vm_types::{
gas::{GasMeter, SimpleInstruction},
loaded_data::runtime_types::Type,
views::{TypeView, ValueView},
};
use once_cell::sync::Lazy;
use crate::gas_model::units_types::{CostTable, Gas, GasCost};
pub const VM_FLAT_FEE: Gas = Gas::new(8_000);
pub const CONST_SIZE: AbstractMemorySize = AbstractMemorySize::new(16);
pub const REFERENCE_SIZE: AbstractMemorySize = AbstractMemorySize::new(8);
pub const STRUCT_SIZE: AbstractMemorySize = AbstractMemorySize::new(2);
pub const VEC_SIZE: AbstractMemorySize = AbstractMemorySize::new(8);
pub const MIN_EXISTS_DATA_SIZE: AbstractMemorySize = AbstractMemorySize::new(100);
pub static ZERO_COST_SCHEDULE: Lazy<CostTable> = Lazy::new(zero_cost_schedule);
pub static INITIAL_COST_SCHEDULE: Lazy<CostTable> = Lazy::new(initial_cost_schedule_v1);
#[allow(dead_code)]
#[derive(Debug)]
pub struct GasStatus {
pub gas_model_version: u64,
cost_table: CostTable,
gas_left: InternalGas,
gas_price: u64,
initial_budget: InternalGas,
charge: bool,
stack_height_high_water_mark: u64,
stack_height_current: u64,
stack_height_next_tier_start: Option<u64>,
stack_height_current_tier_mult: u64,
stack_size_high_water_mark: u64,
stack_size_current: u64,
stack_size_next_tier_start: Option<u64>,
stack_size_current_tier_mult: u64,
instructions_executed: u64,
instructions_next_tier_start: Option<u64>,
instructions_current_tier_mult: u64,
profiler: Option<GasProfiler>,
}
impl GasStatus {
pub fn new(cost_table: CostTable, budget: u64, gas_price: u64, gas_model_version: u64) -> Self {
assert!(gas_price > 0, "gas price cannot be 0");
let budget_in_unit = budget / gas_price;
let gas_left = Self::to_internal_units(budget_in_unit);
let (stack_height_current_tier_mult, stack_height_next_tier_start) =
cost_table.stack_height_tier(0);
let (stack_size_current_tier_mult, stack_size_next_tier_start) =
cost_table.stack_size_tier(0);
let (instructions_current_tier_mult, instructions_next_tier_start) =
cost_table.instruction_tier(0);
Self {
gas_model_version,
gas_left,
gas_price,
initial_budget: gas_left,
cost_table,
charge: true,
stack_height_high_water_mark: 0,
stack_height_current: 0,
stack_size_high_water_mark: 0,
stack_size_current: 0,
instructions_executed: 0,
stack_height_current_tier_mult,
stack_size_current_tier_mult,
instructions_current_tier_mult,
stack_height_next_tier_start,
stack_size_next_tier_start,
instructions_next_tier_start,
profiler: None,
}
}
pub fn new_unmetered() -> Self {
Self {
gas_model_version: 1,
gas_left: InternalGas::new(0),
gas_price: 1,
initial_budget: InternalGas::new(0),
cost_table: ZERO_COST_SCHEDULE.clone(),
charge: false,
stack_height_high_water_mark: 0,
stack_height_current: 0,
stack_size_high_water_mark: 0,
stack_size_current: 0,
instructions_executed: 0,
stack_height_current_tier_mult: 0,
stack_size_current_tier_mult: 0,
instructions_current_tier_mult: 0,
stack_height_next_tier_start: None,
stack_size_next_tier_start: None,
instructions_next_tier_start: None,
profiler: None,
}
}
const INTERNAL_UNIT_MULTIPLIER: u64 = 1000;
fn to_internal_units(val: u64) -> InternalGas {
InternalGas::new(val * Self::INTERNAL_UNIT_MULTIPLIER)
}
#[allow(dead_code)]
fn to_nanos(&self, val: InternalGas) -> u64 {
let gas: Gas = InternalGas::to_unit_round_down(val);
u64::from(gas) * self.gas_price
}
pub fn push_stack(&mut self, pushes: u64) -> PartialVMResult<()> {
match self.stack_height_current.checked_add(pushes) {
None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
Some(new_height) => {
if new_height > self.stack_height_high_water_mark {
self.stack_height_high_water_mark = new_height;
}
self.stack_height_current = new_height;
}
}
if let Some(stack_height_tier_next) = self.stack_height_next_tier_start {
if self.stack_height_current > stack_height_tier_next {
let (next_mul, next_tier) =
self.cost_table.stack_height_tier(self.stack_height_current);
self.stack_height_current_tier_mult = next_mul;
self.stack_height_next_tier_start = next_tier;
}
}
Ok(())
}
pub fn pop_stack(&mut self, pops: u64) {
self.stack_height_current = self.stack_height_current.saturating_sub(pops);
}
pub fn increase_instruction_count(&mut self, amount: u64) -> PartialVMResult<()> {
match self.instructions_executed.checked_add(amount) {
None => return Err(PartialVMError::new(StatusCode::PC_OVERFLOW)),
Some(new_pc) => {
self.instructions_executed = new_pc;
}
}
if let Some(instr_tier_next) = self.instructions_next_tier_start {
if self.instructions_executed > instr_tier_next {
let (instr_cost, next_tier) =
self.cost_table.instruction_tier(self.instructions_executed);
self.instructions_current_tier_mult = instr_cost;
self.instructions_next_tier_start = next_tier;
}
}
Ok(())
}
pub fn increase_stack_size(&mut self, size_amount: u64) -> PartialVMResult<()> {
match self.stack_size_current.checked_add(size_amount) {
None => return Err(PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW)),
Some(new_size) => {
if new_size > self.stack_size_high_water_mark {
self.stack_size_high_water_mark = new_size;
}
self.stack_size_current = new_size;
}
}
if let Some(stack_size_tier_next) = self.stack_size_next_tier_start {
if self.stack_size_current > stack_size_tier_next {
let (next_mul, next_tier) =
self.cost_table.stack_size_tier(self.stack_size_current);
self.stack_size_current_tier_mult = next_mul;
self.stack_size_next_tier_start = next_tier;
}
}
Ok(())
}
pub fn decrease_stack_size(&mut self, size_amount: u64) {
let new_size = self.stack_size_current.saturating_sub(size_amount);
if new_size > self.stack_size_high_water_mark {
self.stack_size_high_water_mark = new_size;
}
self.stack_size_current = new_size;
}
pub fn charge(
&mut self,
num_instructions: u64,
pushes: u64,
pops: u64,
incr_size: u64,
_decr_size: u64,
) -> PartialVMResult<()> {
self.push_stack(pushes)?;
self.increase_instruction_count(num_instructions)?;
self.increase_stack_size(incr_size)?;
self.deduct_gas(
GasCost::new(
self.instructions_current_tier_mult
.checked_mul(num_instructions)
.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
self.stack_size_current_tier_mult
.checked_mul(incr_size)
.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
self.stack_height_current_tier_mult
.checked_mul(pushes)
.ok_or_else(|| PartialVMError::new(StatusCode::ARITHMETIC_OVERFLOW))?,
)
.total_internal(),
)?;
self.pop_stack(pops);
Ok(())
}
pub fn cost_table(&self) -> &CostTable {
&self.cost_table
}
pub fn remaining_gas(&self) -> Gas {
self.gas_left.to_unit_round_down()
}
pub fn deduct_gas(&mut self, amount: InternalGas) -> PartialVMResult<()> {
if !self.charge {
return Ok(());
}
match self.gas_left.checked_sub(amount) {
Some(gas_left) => {
self.gas_left = gas_left;
Ok(())
}
None => {
self.gas_left = InternalGas::new(0);
Err(PartialVMError::new(StatusCode::OUT_OF_GAS))
}
}
}
fn deduct_units(&mut self, amount: u64) -> PartialVMResult<()> {
self.deduct_gas(InternalGas::new(amount))
}
pub fn set_metering(&mut self, enabled: bool) {
self.charge = enabled
}
pub fn gas_used_pre_gas_price(&self) -> u64 {
let gas: Gas = match self.initial_budget.checked_sub(self.gas_left) {
Some(val) => InternalGas::to_unit_round_down(val),
None => InternalGas::to_unit_round_down(self.initial_budget),
};
u64::from(gas)
}
pub fn charge_bytes(&mut self, size: usize, cost_per_byte: u64) -> PartialVMResult<()> {
let computation_cost = size as u64 * cost_per_byte;
self.deduct_units(computation_cost)
}
fn abstract_memory_size(&self, val: impl ValueView) -> AbstractMemorySize {
val.abstract_memory_size()
}
pub fn gas_price(&self) -> u64 {
self.gas_price
}
pub fn stack_height_high_water_mark(&self) -> u64 {
self.stack_height_high_water_mark
}
pub fn stack_size_high_water_mark(&self) -> u64 {
self.stack_size_high_water_mark
}
pub fn instructions_executed(&self) -> u64 {
self.instructions_executed
}
}
fn get_simple_instruction_stack_change(
instr: SimpleInstruction,
) -> (u64, u64, AbstractMemorySize, AbstractMemorySize) {
use SimpleInstruction::*;
match instr {
Nop | Ret => (0, 0, 0.into(), 0.into()),
BrTrue | BrFalse => (1, 0, Type::Bool.size(), 0.into()),
Branch => (0, 0, 0.into(), 0.into()),
LdU8 => (0, 1, 0.into(), Type::U8.size()),
LdU16 => (0, 1, 0.into(), Type::U16.size()),
LdU32 => (0, 1, 0.into(), Type::U32.size()),
LdU64 => (0, 1, 0.into(), Type::U64.size()),
LdU128 => (0, 1, 0.into(), Type::U128.size()),
LdU256 => (0, 1, 0.into(), Type::U256.size()),
LdTrue | LdFalse => (0, 1, 0.into(), Type::Bool.size()),
FreezeRef => (1, 1, REFERENCE_SIZE, REFERENCE_SIZE),
ImmBorrowLoc | MutBorrowLoc => (0, 1, 0.into(), REFERENCE_SIZE),
ImmBorrowField | MutBorrowField | ImmBorrowFieldGeneric | MutBorrowFieldGeneric => {
(1, 1, REFERENCE_SIZE, REFERENCE_SIZE)
}
CastU8 => (1, 1, Type::U8.size(), Type::U8.size()),
CastU16 => (1, 1, Type::U8.size(), Type::U16.size()),
CastU32 => (1, 1, Type::U8.size(), Type::U32.size()),
CastU64 => (1, 1, Type::U8.size(), Type::U64.size()),
CastU128 => (1, 1, Type::U8.size(), Type::U128.size()),
CastU256 => (1, 1, Type::U8.size(), Type::U256.size()),
Add | Sub | Mul | Mod | Div => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()),
BitOr | BitAnd | Xor => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()),
Shl | Shr => (2, 1, Type::U8.size() + Type::U8.size(), Type::U256.size()),
Or | And => (
2,
1,
Type::Bool.size() + Type::Bool.size(),
Type::Bool.size(),
),
Lt | Gt | Le | Ge => (2, 1, Type::U8.size() + Type::U8.size(), Type::Bool.size()),
Not => (1, 1, Type::Bool.size(), Type::Bool.size()),
Abort => (1, 0, Type::U64.size(), 0.into()),
}
}
impl GasMeter for GasStatus {
fn charge_simple_instr(&mut self, instr: SimpleInstruction) -> PartialVMResult<()> {
let (pops, pushes, pop_size, push_size) = get_simple_instruction_stack_change(instr);
self.charge(1, pushes, pops, push_size.into(), pop_size.into())
}
fn charge_pop(&mut self, popped_val: impl ValueView) -> PartialVMResult<()> {
self.charge(1, 0, 1, 0, self.abstract_memory_size(popped_val).into())
}
fn charge_native_function(
&mut self,
amount: InternalGas,
ret_vals: Option<impl ExactSizeIterator<Item = impl ValueView>>,
) -> PartialVMResult<()> {
let pushes = ret_vals
.as_ref()
.map(|ret_vals| ret_vals.len())
.unwrap_or(0) as u64;
let size_increase = ret_vals
.map(|ret_vals| {
ret_vals.fold(AbstractMemorySize::zero(), |acc, elem| {
acc + self.abstract_memory_size(elem)
})
})
.unwrap_or_else(AbstractMemorySize::zero);
self.charge(0, pushes, 0, size_increase.into(), 0)?;
self.deduct_gas(amount)
}
fn charge_native_function_before_execution(
&mut self,
_ty_args: impl ExactSizeIterator<Item = impl TypeView>,
args: impl ExactSizeIterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
let pops = args.len() as u64;
let stack_reduction_size = args.fold(AbstractMemorySize::new(pops), |acc, elem| {
acc + self.abstract_memory_size(elem)
});
self.charge(1, 0, pops, 0, stack_reduction_size.into())
}
fn charge_call(
&mut self,
_module_id: &ModuleId,
_func_name: &str,
args: impl ExactSizeIterator<Item = impl ValueView>,
_num_locals: NumArgs,
) -> PartialVMResult<()> {
let pops = args.len() as u64;
let stack_reduction_size = args.fold(AbstractMemorySize::new(0), |acc, elem| {
acc + self.abstract_memory_size(elem)
});
self.charge(1, 0, pops, 0, stack_reduction_size.into())
}
fn charge_call_generic(
&mut self,
_module_id: &ModuleId,
_func_name: &str,
_ty_args: impl ExactSizeIterator<Item = impl TypeView>,
args: impl ExactSizeIterator<Item = impl ValueView>,
_num_locals: NumArgs,
) -> PartialVMResult<()> {
let pops = args.len() as u64;
let stack_reduction_size = args.fold(AbstractMemorySize::new(0), |acc, elem| {
acc + self.abstract_memory_size(elem)
});
self.charge(1, 0, pops, 0, stack_reduction_size.into())
}
fn charge_ld_const(&mut self, size: NumBytes) -> PartialVMResult<()> {
self.charge(1, 1, 0, u64::from(size), 0)
}
fn charge_ld_const_after_deserialization(
&mut self,
_val: impl ValueView,
) -> PartialVMResult<()> {
Ok(())
}
fn charge_copy_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> {
self.charge(1, 1, 0, self.abstract_memory_size(val).into(), 0)
}
fn charge_move_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> {
self.charge(1, 1, 0, self.abstract_memory_size(val).into(), 0)
}
fn charge_store_loc(&mut self, val: impl ValueView) -> PartialVMResult<()> {
self.charge(1, 0, 1, 0, self.abstract_memory_size(val).into())
}
fn charge_pack(
&mut self,
_is_generic: bool,
args: impl ExactSizeIterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
let num_fields = args.len() as u64;
self.charge(1, 1, num_fields, STRUCT_SIZE.into(), 0)
}
fn charge_unpack(
&mut self,
_is_generic: bool,
args: impl ExactSizeIterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
let num_fields = args.len() as u64;
self.charge(1, num_fields, 1, 0, STRUCT_SIZE.into())
}
fn charge_variant_switch(&mut self, val: impl ValueView) -> PartialVMResult<()> {
self.charge(1, 0, 1, 0, self.abstract_memory_size(val).into())
}
fn charge_read_ref(&mut self, ref_val: impl ValueView) -> PartialVMResult<()> {
self.charge(
1,
1,
1,
self.abstract_memory_size(ref_val).into(),
REFERENCE_SIZE.into(),
)
}
fn charge_write_ref(
&mut self,
new_val: impl ValueView,
old_val: impl ValueView,
) -> PartialVMResult<()> {
self.charge(
1,
1,
2,
self.abstract_memory_size(new_val).into(),
self.abstract_memory_size(old_val).into(),
)
}
fn charge_eq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()> {
let size_reduction = self.abstract_memory_size(lhs) + self.abstract_memory_size(rhs);
self.charge(
1,
1,
2,
(Type::Bool.size() + size_reduction).into(),
size_reduction.into(),
)
}
fn charge_neq(&mut self, lhs: impl ValueView, rhs: impl ValueView) -> PartialVMResult<()> {
let size_reduction = self.abstract_memory_size(lhs) + self.abstract_memory_size(rhs);
self.charge(1, 1, 2, Type::Bool.size().into(), size_reduction.into())
}
fn charge_vec_pack<'a>(
&mut self,
_ty: impl TypeView + 'a,
args: impl ExactSizeIterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
let num_args = args.len() as u64;
self.charge(1, 1, num_args, VEC_SIZE.into(), 0)
}
fn charge_vec_len(&mut self, _ty: impl TypeView) -> PartialVMResult<()> {
self.charge(1, 1, 1, Type::U64.size().into(), REFERENCE_SIZE.into())
}
fn charge_vec_borrow(
&mut self,
_is_mut: bool,
_ty: impl TypeView,
_is_success: bool,
) -> PartialVMResult<()> {
self.charge(
1,
1,
2,
REFERENCE_SIZE.into(),
(REFERENCE_SIZE + Type::U64.size()).into(),
)
}
fn charge_vec_push_back(
&mut self,
_ty: impl TypeView,
_val: impl ValueView,
) -> PartialVMResult<()> {
self.charge(1, 0, 2, 0, REFERENCE_SIZE.into())
}
fn charge_vec_pop_back(
&mut self,
_ty: impl TypeView,
_val: Option<impl ValueView>,
) -> PartialVMResult<()> {
self.charge(1, 1, 1, 0, REFERENCE_SIZE.into())
}
fn charge_vec_unpack(
&mut self,
_ty: impl TypeView,
expect_num_elements: NumArgs,
_elems: impl ExactSizeIterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
let pushes = u64::from(expect_num_elements);
self.charge(1, pushes, 1, 0, VEC_SIZE.into())
}
fn charge_vec_swap(&mut self, _ty: impl TypeView) -> PartialVMResult<()> {
let size_decrease = REFERENCE_SIZE + Type::U64.size() + Type::U64.size();
self.charge(1, 1, 1, 0, size_decrease.into())
}
fn charge_drop_frame(
&mut self,
_locals: impl Iterator<Item = impl ValueView>,
) -> PartialVMResult<()> {
Ok(())
}
fn remaining_gas(&self) -> InternalGas {
if !self.charge {
return InternalGas::new(u64::MAX);
}
self.gas_left
}
fn get_profiler_mut(&mut self) -> Option<&mut GasProfiler> {
self.profiler.as_mut()
}
fn set_profiler(&mut self, profiler: GasProfiler) {
self.profiler = Some(profiler);
}
}
pub fn zero_cost_schedule() -> CostTable {
let mut zero_tier = BTreeMap::new();
zero_tier.insert(0, 0);
CostTable {
instruction_tiers: zero_tier.clone(),
stack_size_tiers: zero_tier.clone(),
stack_height_tiers: zero_tier,
}
}
pub fn unit_cost_schedule() -> CostTable {
let mut unit_tier = BTreeMap::new();
unit_tier.insert(0, 1);
CostTable {
instruction_tiers: unit_tier.clone(),
stack_size_tiers: unit_tier.clone(),
stack_height_tiers: unit_tier,
}
}
pub fn initial_cost_schedule_v1() -> CostTable {
let instruction_tiers: BTreeMap<u64, u64> = vec![
(0, 1),
(20_000, 2),
(50_000, 10),
(100_000, 50),
(200_000, 100),
(10_000_000, 1000),
]
.into_iter()
.collect();
let stack_height_tiers: BTreeMap<u64, u64> =
vec![(0, 1), (1_000, 2), (10_000, 10)].into_iter().collect();
let stack_size_tiers: BTreeMap<u64, u64> = vec![
(0, 1),
(100_000, 2), (500_000, 5), (1_000_000, 100), (100_000_000, 1000), ]
.into_iter()
.collect();
CostTable {
instruction_tiers,
stack_size_tiers,
stack_height_tiers,
}
}
pub fn initial_cost_schedule_for_unit_tests() -> move_vm_test_utils::gas_schedule::CostTable {
let table = initial_cost_schedule_v1();
move_vm_test_utils::gas_schedule::CostTable {
instruction_tiers: table.instruction_tiers,
stack_height_tiers: table.stack_height_tiers,
stack_size_tiers: table.stack_size_tiers,
}
}