Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify gas metering #5121

Merged
merged 24 commits into from Nov 8, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
132 changes: 99 additions & 33 deletions runtime/near-vm-logic/src/gas_counter.rs
Expand Up @@ -24,15 +24,35 @@ pub fn with_ext_cost_counter(f: impl FnOnce(&mut HashMap<ExtCosts, u64>)) {

type Result<T> = ::std::result::Result<T, VMLogicError>;

/// Fast gas counter with very simple structure, could be exposed to compiled code in the VM.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FastGasCounter {
/// The following three fields must be put next to another to make sure
/// generated gas counting code can use and adjust them.
/// We will share counter to ensure we never miss synchronization.
/// This could change and in such a case synchronization required between compiled WASM code
/// and the host code.

/// The amount of gas that was irreversibly used for contract execution.
burnt_gas: u64,
/// Hard gas limit for execution
gas_limit: u64,
/// Single WASM opcode cost
opcode_cost: u64,
}

/// Gas counter (a part of VMlogic)
pub struct GasCounter {
/// The amount of gas that was irreversibly used for contract execution.
burnt_gas: Gas,
/// `burnt_gas` + gas that was attached to the promises.
used_gas: Gas,
/// Gas limit for execution
/// Shared gas counter data.
fast_counter: FastGasCounter,
/// Gas that was attached to the promises.
promises_gas: Gas,
/// Hard gas limit for execution
max_gas_burnt: Gas,
/// Amount of prepaid gas, we can never burn more than prepaid amount
prepaid_gas: Gas,
/// If this is a view-only call.
is_view: bool,
ext_costs_config: ExtCostsConfig,
/// Where to store profile data, if needed.
Expand All @@ -49,48 +69,98 @@ impl GasCounter {
pub fn new(
ext_costs_config: ExtCostsConfig,
max_gas_burnt: Gas,
opcode_cost: u32,
prepaid_gas: Gas,
is_view: bool,
) -> Self {
use std::cmp::min;
Self {
ext_costs_config,
burnt_gas: 0,
used_gas: 0,
max_gas_burnt,
fast_counter: FastGasCounter {
burnt_gas: 0,
gas_limit: if is_view {
// Ignore prepaid gas limit and promises.
max_gas_burnt
} else {
min(max_gas_burnt, prepaid_gas)
},
opcode_cost: Gas::from(opcode_cost),
},
max_gas_burnt: max_gas_burnt,
promises_gas: 0,
prepaid_gas,
is_view,
profile: Default::default(),
}
}

fn deduct_gas(&mut self, burn_gas: Gas, use_gas: Gas) -> Result<()> {
use std::cmp::min;
assert!(burn_gas <= use_gas);
let promise_gas = use_gas - burn_gas;
let new_promises_gas =
self.promises_gas.checked_add(promise_gas).ok_or(HostError::IntegerOverflow)?;
let new_burnt_gas =
self.burnt_gas.checked_add(burn_gas).ok_or(HostError::IntegerOverflow)?;
let new_used_gas = self.used_gas.checked_add(use_gas).ok_or(HostError::IntegerOverflow)?;
self.fast_counter.burnt_gas.checked_add(burn_gas).ok_or(HostError::IntegerOverflow)?;
let new_used_gas =
new_burnt_gas.checked_add(new_promises_gas).ok_or(HostError::IntegerOverflow)?;
if new_burnt_gas <= self.max_gas_burnt && (self.is_view || new_used_gas <= self.prepaid_gas)
{
self.burnt_gas = new_burnt_gas;
self.used_gas = new_used_gas;
if promise_gas != 0 {
self.fast_counter.gas_limit =
min(self.max_gas_burnt, self.prepaid_gas - new_promises_gas);
matklad marked this conversation as resolved.
Show resolved Hide resolved
}
self.fast_counter.burnt_gas = new_burnt_gas;
self.promises_gas = new_promises_gas;
Ok(())
} else {
use std::cmp::min;
let res = if new_burnt_gas > self.max_gas_burnt {
// if max_gas_burnt == prepaid_gas we must use GasExceeded error.
if new_burnt_gas > self.max_gas_burnt && self.max_gas_burnt != self.prepaid_gas {
self.fast_counter.burnt_gas = self.max_gas_burnt;
Err(HostError::GasLimitExceeded.into())
} else if new_used_gas > self.prepaid_gas {
Err(HostError::GasExceeded.into())
} else {
unreachable!()
};

let max_burnt_gas = min(self.max_gas_burnt, self.prepaid_gas);
self.burnt_gas = min(new_burnt_gas, max_burnt_gas);
self.used_gas = min(new_used_gas, self.prepaid_gas);
self.fast_counter.burnt_gas = min(new_burnt_gas, self.prepaid_gas);
// Technically we shall do `self.promises_gas = 0;` or error paths, as in this case
// no promises will be kept, but that would mean protocol change.
// TODO: consider making this change!
olonho marked this conversation as resolved.
Show resolved Hide resolved
assert!(self.prepaid_gas >= self.fast_counter.burnt_gas);
self.promises_gas = self.prepaid_gas - self.fast_counter.burnt_gas;
Err(HostError::GasExceeded.into())
}
}
}

res
// Optimized version of above function for cases where no promises involved.
pub fn burn_gas(&mut self, value: Gas) -> Result<()> {
let new_burnt_gas =
self.fast_counter.burnt_gas.checked_add(value).ok_or(HostError::IntegerOverflow)?;
if new_burnt_gas <= self.fast_counter.gas_limit {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically in view mode we could have promised something close to u64::max() gas by now and with new burnt gas that could have overflown integer in deduct_gas. Not sure if we care enough about this case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think that (checked) overflow in view mode is critical/important.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, fixed it.

mina86 marked this conversation as resolved.
Show resolved Hide resolved
self.fast_counter.burnt_gas = new_burnt_gas;
Ok(())
} else {
// if max_gas_burnt == prepaid_gas we must use GasExceeded error.
if new_burnt_gas > self.max_gas_burnt && self.max_gas_burnt != self.prepaid_gas {
self.fast_counter.burnt_gas = self.max_gas_burnt;
Err(HostError::GasLimitExceeded.into())
} else {
use std::cmp::min;
// Now we limit burnt gas with prepaid amount.
self.fast_counter.burnt_gas = min(new_burnt_gas, self.prepaid_gas);
// Technically we shall do `self.promises_gas = 0;` or error paths, as in this case
// no promises will be kept.
// TODO: consider making this change and fix tests/protocol!
assert!(self.prepaid_gas >= self.fast_counter.burnt_gas);
self.promises_gas = self.prepaid_gas - self.fast_counter.burnt_gas;
Err(HostError::GasExceeded.into())
olonho marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

pub fn pay_wasm_gas(&mut self, opcodes: u32) -> Result<()> {
let value = Gas::from(opcodes) * self.fast_counter.opcode_cost;
self.burn_gas(value)
}

#[inline]
fn inc_ext_costs_counter(&mut self, cost: ExtCosts, value: u64) {
with_ext_cost_counter(|cc| *cc.entry(cost).or_default() += value)
Expand All @@ -106,10 +176,6 @@ impl GasCounter {
self.profile.add_action_cost(action, value)
}

pub fn pay_wasm_gas(&mut self, value: u64) -> Result<()> {
self.deduct_gas(value, value)
}

/// A helper function to pay a multiple of a cost.
pub fn pay_per(&mut self, cost: ExtCosts, num: u64) -> Result<()> {
let use_gas = num
Expand All @@ -118,15 +184,15 @@ impl GasCounter {

self.inc_ext_costs_counter(cost, num);
self.update_profile_host(cost, use_gas);
self.deduct_gas(use_gas, use_gas)
self.burn_gas(use_gas)
}

/// A helper function to pay base cost gas.
pub fn pay_base(&mut self, cost: ExtCosts) -> Result<()> {
let base_fee = cost.value(&self.ext_costs_config);
self.inc_ext_costs_counter(cost, 1);
self.update_profile_host(cost, base_fee);
self.deduct_gas(base_fee, base_fee)
self.burn_gas(base_fee)
}

/// A helper function to pay per byte gas fee for batching an action.
Expand Down Expand Up @@ -191,10 +257,10 @@ impl GasCounter {
}

pub fn burnt_gas(&self) -> Gas {
self.burnt_gas
self.fast_counter.burnt_gas
}
pub fn used_gas(&self) -> Gas {
self.used_gas
self.promises_gas + self.fast_counter.burnt_gas
}

pub fn profile_data(&self) -> ProfileData {
Expand All @@ -209,7 +275,7 @@ mod tests {

#[test]
fn test_deduct_gas() {
let mut counter = GasCounter::new(ExtCostsConfig::default(), 10, 10, false);
let mut counter = GasCounter::new(ExtCostsConfig::default(), 10, 1, 10, false);
counter.deduct_gas(5, 10).expect("deduct_gas should work");
assert_eq!(counter.burnt_gas(), 5);
assert_eq!(counter.used_gas(), 10);
Expand All @@ -218,7 +284,7 @@ mod tests {
#[test]
#[should_panic]
fn test_prepaid_gas_min() {
let mut counter = GasCounter::new(ExtCostsConfig::default(), 100, 10, false);
let mut counter = GasCounter::new(ExtCostsConfig::default(), 100, 1, 10, false);
counter.deduct_gas(10, 5).unwrap();
}
}
6 changes: 3 additions & 3 deletions runtime/near-vm-logic/src/logic.rs
Expand Up @@ -120,6 +120,7 @@ impl<'a> VMLogic<'a> {
let gas_counter = GasCounter::new(
config.ext_costs.clone(),
max_gas_burnt,
config.regular_op_cost,
context.prepaid_gas,
context.is_view(),
);
Expand Down Expand Up @@ -1052,9 +1053,8 @@ impl<'a> VMLogic<'a> {
/// * If passed gas amount somehow overflows internal gas counters returns `IntegerOverflow`;
/// * If we exceed usage limit imposed on burnt gas returns `GasLimitExceeded`;
/// * If we exceed the `prepaid_gas` then returns `GasExceeded`.
pub fn gas(&mut self, gas_amount: u32) -> Result<()> {
let value = Gas::from(gas_amount) * Gas::from(self.config.regular_op_cost);
self.gas_counter.pay_wasm_gas(value)
pub fn gas(&mut self, opcodes: u32) -> Result<()> {
self.gas_counter.pay_wasm_gas(opcodes)
}

// ################
Expand Down
6 changes: 3 additions & 3 deletions runtime/near-vm-logic/tests/test_promises.rs
Expand Up @@ -96,9 +96,9 @@ fn test_limit_wasm_gas_after_attaching_gas() {
.expect("should add action to receipt");
logic.gas((op_limit / 2) as u32).expect_err("should fail with gas limit");
let outcome = logic.outcome();

assert!(outcome.used_gas == limit);
assert!(limit / 2 < outcome.burnt_gas && outcome.burnt_gas < limit);
assert_eq!(outcome.used_gas, limit);
assert!(outcome.burnt_gas > limit / 2);
assert!(outcome.burnt_gas < limit);
}

#[test]
Expand Down