use crate::{exec::ExecError, Config, Error};
use core::marker::PhantomData;
use frame_support::{
dispatch::{DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo},
weights::Weight,
DefaultNoBound,
};
use sp_core::Get;
use sp_runtime::{traits::Zero, 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(T::Schedule::get().ref_time_by_fuel()),
_phantom: PhantomData,
}
}
fn set_fuel(&mut self, fuel: u64) -> Weight {
let consumed = self
.fuel
.saturating_sub(fuel)
.saturating_mul(T::Schedule::get().ref_time_by_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(T::Schedule::get().ref_time_by_fuel())
.ok_or(Error::<T>::InvalidSchedule)?;
self.fuel.checked_sub(amount).ok_or_else(|| Error::<T>::OutOfGas)?;
Ok(Syncable(self.fuel))
}
}
#[must_use]
pub struct RefTimeLeft(u64);
#[must_use]
pub struct Syncable(u64);
impl From<Syncable> for u64 {
fn from(from: Syncable) -> u64 {
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(&mut self, amount: Weight) -> Self {
let amount = Weight::from_parts(
if amount.ref_time().is_zero() {
self.gas_left().ref_time()
} else {
amount.ref_time()
},
if amount.proof_size().is_zero() {
self.gas_left().proof_size()
} else {
amount.proof_size()
},
)
.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: u64) -> Result<RefTimeLeft, DispatchError> {
let weight_consumed = self.engine_meter.set_fuel(engine_fuel);
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 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 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());
}
}