use crate::{exec::ExecError, weights::WeightInfo, Config, Error};
use core::marker::PhantomData;
use frame_support::{
dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo},
weights::Weight,
DefaultNoBound,
};
use sp_runtime::DispatchError;
#[cfg(test)]
use std::{any::Any, fmt::Debug};
#[derive(Debug, PartialEq, Eq)]
pub struct ChargedAmount(Weight);
impl ChargedAmount {
pub fn amount(&self) -> Weight {
self.0
}
}
#[derive(DefaultNoBound)]
struct EngineMeter<T: Config> {
fuel: u64,
_phantom: PhantomData<T>,
}
impl<T: Config> EngineMeter<T> {
fn new(limit: Weight) -> Self {
Self {
fuel: limit.ref_time().saturating_div(Self::ref_time_per_fuel()),
_phantom: PhantomData,
}
}
fn set_fuel(&mut self, fuel: u64) -> Weight {
let consumed = self.fuel.saturating_sub(fuel).saturating_mul(Self::ref_time_per_fuel());
self.fuel = fuel;
Weight::from_parts(consumed, 0)
}
fn charge_ref_time(&mut self, ref_time: u64) -> Result<Syncable, DispatchError> {
let amount = ref_time
.checked_div(Self::ref_time_per_fuel())
.ok_or(Error::<T>::InvalidSchedule)?;
self.fuel.checked_sub(amount).ok_or_else(|| Error::<T>::OutOfGas)?;
Ok(Syncable(self.fuel.try_into().map_err(|_| Error::<T>::OutOfGas)?))
}
fn ref_time_per_fuel() -> u64 {
let loop_iteration =
T::WeightInfo::instr(1).saturating_sub(T::WeightInfo::instr(0)).ref_time();
let empty_loop_iteration = T::WeightInfo::instr_empty_loop(1)
.saturating_sub(T::WeightInfo::instr_empty_loop(0))
.ref_time();
loop_iteration.saturating_sub(empty_loop_iteration)
}
}
#[must_use]
pub struct RefTimeLeft(u64);
#[must_use]
pub struct Syncable(polkavm::Gas);
impl From<Syncable> for polkavm::Gas {
fn from(from: Syncable) -> Self {
from.0
}
}
#[cfg(not(test))]
pub trait TestAuxiliaries {}
#[cfg(not(test))]
impl<T> TestAuxiliaries for T {}
#[cfg(test)]
pub trait TestAuxiliaries: Any + Debug + PartialEq + Eq {}
#[cfg(test)]
impl<T: Any + Debug + PartialEq + Eq> TestAuxiliaries for T {}
pub trait Token<T: Config>: Copy + Clone + TestAuxiliaries {
fn weight(&self) -> Weight;
fn influence_lowest_gas_limit(&self) -> bool {
true
}
}
#[cfg(test)]
pub struct ErasedToken {
pub description: String,
pub token: Box<dyn Any>,
}
#[derive(DefaultNoBound)]
pub struct GasMeter<T: Config> {
gas_limit: Weight,
gas_left: Weight,
gas_left_lowest: Weight,
engine_meter: EngineMeter<T>,
_phantom: PhantomData<T>,
#[cfg(test)]
tokens: Vec<ErasedToken>,
}
impl<T: Config> GasMeter<T> {
pub fn new(gas_limit: Weight) -> Self {
GasMeter {
gas_limit,
gas_left: gas_limit,
gas_left_lowest: gas_limit,
engine_meter: EngineMeter::new(gas_limit),
_phantom: PhantomData,
#[cfg(test)]
tokens: Vec::new(),
}
}
pub fn nested_take_all(&mut self) -> Self {
let gas_left = self.gas_left;
self.gas_left -= gas_left;
GasMeter::new(gas_left)
}
pub fn nested(&mut self, amount: Weight) -> Self {
let amount = amount.min(self.gas_left);
self.gas_left -= amount;
GasMeter::new(amount)
}
pub fn absorb_nested(&mut self, nested: Self) {
self.gas_left_lowest = (self.gas_left + nested.gas_limit)
.saturating_sub(nested.gas_required())
.min(self.gas_left_lowest);
self.gas_left += nested.gas_left;
}
#[inline]
pub fn charge<Tok: Token<T>>(&mut self, token: Tok) -> Result<ChargedAmount, DispatchError> {
#[cfg(test)]
{
let erased_tok =
ErasedToken { description: format!("{:?}", token), token: Box::new(token) };
self.tokens.push(erased_tok);
}
let amount = token.weight();
self.gas_left = self.gas_left.checked_sub(&amount).ok_or_else(|| Error::<T>::OutOfGas)?;
Ok(ChargedAmount(amount))
}
pub fn adjust_gas<Tok: Token<T>>(&mut self, charged_amount: ChargedAmount, token: Tok) {
if token.influence_lowest_gas_limit() {
self.gas_left_lowest = self.gas_left_lowest();
}
let adjustment = charged_amount.0.saturating_sub(token.weight());
self.gas_left = self.gas_left.saturating_add(adjustment).min(self.gas_limit);
}
pub fn sync_from_executor(
&mut self,
engine_fuel: polkavm::Gas,
) -> Result<RefTimeLeft, DispatchError> {
let weight_consumed = self
.engine_meter
.set_fuel(engine_fuel.try_into().map_err(|_| Error::<T>::OutOfGas)?);
self.gas_left
.checked_reduce(weight_consumed)
.ok_or_else(|| Error::<T>::OutOfGas)?;
Ok(RefTimeLeft(self.gas_left.ref_time()))
}
pub fn sync_to_executor(&mut self, before: RefTimeLeft) -> Result<Syncable, DispatchError> {
let ref_time_consumed = before.0.saturating_sub(self.gas_left().ref_time());
self.engine_meter.charge_ref_time(ref_time_consumed)
}
pub fn gas_required(&self) -> Weight {
self.gas_limit.saturating_sub(self.gas_left_lowest())
}
pub fn gas_consumed(&self) -> Weight {
self.gas_limit.saturating_sub(self.gas_left)
}
pub fn gas_left(&self) -> Weight {
self.gas_left
}
pub fn engine_fuel_left(&self) -> Result<polkavm::Gas, DispatchError> {
self.engine_meter.fuel.try_into().map_err(|_| <Error<T>>::OutOfGas.into())
}
pub fn into_dispatch_result<R, E>(
self,
result: Result<R, E>,
base_weight: Weight,
) -> DispatchResultWithPostInfo
where
E: Into<ExecError>,
{
let post_info = PostDispatchInfo {
actual_weight: Some(self.gas_consumed().saturating_add(base_weight)),
pays_fee: Default::default(),
};
result
.map(|_| post_info)
.map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into().error })
}
fn gas_left_lowest(&self) -> Weight {
self.gas_left_lowest.min(self.gas_left)
}
#[cfg(test)]
pub fn tokens(&self) -> &[ErasedToken] {
&self.tokens
}
}
#[cfg(test)]
mod tests {
use super::{GasMeter, Token, Weight};
use crate::tests::Test;
macro_rules! match_tokens {
($tokens_iter:ident,) => {
};
($tokens_iter:ident, $x:expr, $($rest:tt)*) => {
{
let next = ($tokens_iter).next().unwrap();
let pattern = $x;
let mut _pattern_typed_next_ref = &pattern;
_pattern_typed_next_ref = match next.token.downcast_ref() {
Some(p) => {
assert_eq!(p, &pattern);
p
}
None => {
panic!("expected type {} got {}", stringify!($x), next.description);
}
};
}
match_tokens!($tokens_iter, $($rest)*);
};
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct SimpleToken(u64);
impl Token<Test> for SimpleToken {
fn weight(&self) -> Weight {
Weight::from_parts(self.0, 0)
}
}
#[test]
fn it_works() {
let gas_meter = GasMeter::<Test>::new(Weight::from_parts(50000, 0));
assert_eq!(gas_meter.gas_left(), Weight::from_parts(50000, 0));
}
#[test]
fn tracing() {
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(50000, 0));
assert!(!gas_meter.charge(SimpleToken(1)).is_err());
let mut tokens = gas_meter.tokens().iter();
match_tokens!(tokens, SimpleToken(1),);
}
#[test]
fn refuse_to_execute_anything_if_zero() {
let mut gas_meter = GasMeter::<Test>::new(Weight::zero());
assert!(gas_meter.charge(SimpleToken(1)).is_err());
}
#[test]
fn nested_zero_gas_requested() {
let test_weight = 50000.into();
let mut gas_meter = GasMeter::<Test>::new(test_weight);
let gas_for_nested_call = gas_meter.nested(0.into());
assert_eq!(gas_meter.gas_left(), 50000.into());
assert_eq!(gas_for_nested_call.gas_left(), 0.into())
}
#[test]
fn nested_some_gas_requested() {
let test_weight = 50000.into();
let mut gas_meter = GasMeter::<Test>::new(test_weight);
let gas_for_nested_call = gas_meter.nested(10000.into());
assert_eq!(gas_meter.gas_left(), 40000.into());
assert_eq!(gas_for_nested_call.gas_left(), 10000.into())
}
#[test]
fn nested_all_gas_requested() {
let test_weight = Weight::from_parts(50000, 50000);
let mut gas_meter = GasMeter::<Test>::new(test_weight);
let gas_for_nested_call = gas_meter.nested(test_weight);
assert_eq!(gas_meter.gas_left(), Weight::from_parts(0, 0));
assert_eq!(gas_for_nested_call.gas_left(), 50_000.into())
}
#[test]
fn nested_excess_gas_requested() {
let test_weight = Weight::from_parts(50000, 50000);
let mut gas_meter = GasMeter::<Test>::new(test_weight);
let gas_for_nested_call = gas_meter.nested(test_weight + 10000.into());
assert_eq!(gas_meter.gas_left(), Weight::from_parts(0, 0));
assert_eq!(gas_for_nested_call.gas_left(), 50_000.into())
}
#[test]
fn overcharge_does_not_charge() {
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(200, 0));
assert!(gas_meter.charge(SimpleToken(300)).is_err());
assert!(gas_meter.charge(SimpleToken(200)).is_ok());
}
#[test]
fn charge_exact_amount() {
let mut gas_meter = GasMeter::<Test>::new(Weight::from_parts(25, 0));
assert!(!gas_meter.charge(SimpleToken(25)).is_err());
}
}