#![allow(clippy::comparison_chain)]
pub use evm::backend::Basic as Account;
use frame_support::{sp_runtime::traits::UniqueSaturatedInto, weights::Weight};
use sp_core::{H160, H256, U256};
use sp_std::vec::Vec;
#[derive(Debug)]
pub struct CheckEvmTransactionInput {
pub chain_id: Option<u64>,
pub to: Option<H160>,
pub input: Vec<u8>,
pub nonce: U256,
pub gas_limit: U256,
pub gas_price: Option<U256>,
pub max_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>,
pub value: U256,
pub access_list: Vec<(H160, Vec<H256>)>,
}
#[derive(Debug)]
pub struct CheckEvmTransactionConfig<'config> {
pub evm_config: &'config evm::Config,
pub block_gas_limit: U256,
pub base_fee: U256,
pub chain_id: u64,
pub is_transactional: bool,
}
#[derive(Debug)]
pub struct CheckEvmTransaction<'config, E: From<TransactionValidationError>> {
pub config: CheckEvmTransactionConfig<'config>,
pub transaction: CheckEvmTransactionInput,
pub weight_limit: Option<Weight>,
pub proof_size_base_cost: Option<u64>,
_marker: sp_std::marker::PhantomData<E>,
}
#[repr(u8)]
#[derive(num_enum::FromPrimitive, num_enum::IntoPrimitive, Debug)]
pub enum TransactionValidationError {
GasLimitTooLow,
GasLimitTooHigh,
GasPriceTooLow,
PriorityFeeTooHigh,
BalanceTooLow,
TxNonceTooLow,
TxNonceTooHigh,
InvalidFeeInput,
InvalidChainId,
InvalidSignature,
#[num_enum(default)]
UnknownError,
}
impl<'config, E: From<TransactionValidationError>> CheckEvmTransaction<'config, E> {
pub fn new(
config: CheckEvmTransactionConfig<'config>,
transaction: CheckEvmTransactionInput,
weight_limit: Option<Weight>,
proof_size_base_cost: Option<u64>,
) -> Self {
CheckEvmTransaction {
config,
transaction,
weight_limit,
proof_size_base_cost,
_marker: Default::default(),
}
}
pub fn validate_in_pool_for(&self, who: &Account) -> Result<&Self, E> {
if self.transaction.nonce < who.nonce {
return Err(TransactionValidationError::TxNonceTooLow.into());
}
self.validate_common()
}
pub fn validate_in_block_for(&self, who: &Account) -> Result<&Self, E> {
if self.transaction.nonce > who.nonce {
return Err(TransactionValidationError::TxNonceTooHigh.into());
} else if self.transaction.nonce < who.nonce {
return Err(TransactionValidationError::TxNonceTooLow.into());
}
self.validate_common()
}
pub fn with_chain_id(&self) -> Result<&Self, E> {
if let Some(chain_id) = self.transaction.chain_id {
if chain_id != self.config.chain_id {
return Err(TransactionValidationError::InvalidChainId.into());
}
}
Ok(self)
}
pub fn with_base_fee(&self) -> Result<&Self, E> {
let (gas_price, _) = self.transaction_fee_input()?;
if self.config.is_transactional || gas_price > U256::zero() {
if gas_price < self.config.base_fee {
return Err(TransactionValidationError::GasPriceTooLow.into());
}
}
Ok(self)
}
pub fn with_balance_for(&self, who: &Account) -> Result<&Self, E> {
let (max_fee_per_gas, _) = self.transaction_fee_input()?;
let fee = max_fee_per_gas.saturating_mul(self.transaction.gas_limit);
if self.config.is_transactional || fee > U256::zero() {
let total_payment = self.transaction.value.saturating_add(fee);
if who.balance < total_payment {
return Err(TransactionValidationError::BalanceTooLow.into());
}
}
Ok(self)
}
fn transaction_fee_input(&self) -> Result<(U256, Option<U256>), E> {
match (
self.transaction.gas_price,
self.transaction.max_fee_per_gas,
self.transaction.max_priority_fee_per_gas,
) {
(Some(gas_price), None, None) => Ok((gas_price, Some(gas_price))),
(None, Some(max_fee_per_gas), None) => {
Ok((max_fee_per_gas, Some(self.config.base_fee)))
}
(None, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => {
if max_priority_fee_per_gas > max_fee_per_gas {
return Err(TransactionValidationError::PriorityFeeTooHigh.into());
}
let effective_gas_price = self
.config
.base_fee
.checked_add(max_priority_fee_per_gas)
.unwrap_or_else(U256::max_value)
.min(max_fee_per_gas);
Ok((max_fee_per_gas, Some(effective_gas_price)))
}
_ => {
if self.config.is_transactional {
Err(TransactionValidationError::InvalidFeeInput.into())
} else {
Ok((U256::zero(), None))
}
}
}
}
pub fn validate_common(&self) -> Result<&Self, E> {
if self.config.is_transactional {
if let (Some(weight_limit), Some(proof_size_base_cost)) =
(self.weight_limit, self.proof_size_base_cost)
{
let _ = weight_limit
.proof_size()
.checked_sub(proof_size_base_cost)
.ok_or(TransactionValidationError::GasLimitTooLow)?;
}
let mut gasometer = evm::gasometer::Gasometer::new(
self.transaction.gas_limit.unique_saturated_into(),
self.config.evm_config,
);
let transaction_cost = if self.transaction.to.is_some() {
evm::gasometer::call_transaction_cost(
&self.transaction.input,
&self.transaction.access_list,
)
} else {
evm::gasometer::create_transaction_cost(
&self.transaction.input,
&self.transaction.access_list,
)
};
if gasometer.record_transaction(transaction_cost).is_err() {
return Err(TransactionValidationError::GasLimitTooLow.into());
}
if self.transaction.gas_limit > self.config.block_gas_limit {
return Err(TransactionValidationError::GasLimitTooHigh.into());
}
}
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
pub enum TestError {
GasLimitTooLow,
GasLimitTooHigh,
GasPriceTooLow,
PriorityFeeTooHigh,
BalanceTooLow,
TxNonceTooLow,
TxNonceTooHigh,
InvalidFeeInput,
InvalidChainId,
InvalidSignature,
UnknownError,
}
static SHANGHAI_CONFIG: evm::Config = evm::Config::shanghai();
impl From<TransactionValidationError> for TestError {
fn from(e: TransactionValidationError) -> Self {
match e {
TransactionValidationError::GasLimitTooLow => TestError::GasLimitTooLow,
TransactionValidationError::GasLimitTooHigh => TestError::GasLimitTooHigh,
TransactionValidationError::GasPriceTooLow => TestError::GasPriceTooLow,
TransactionValidationError::PriorityFeeTooHigh => TestError::PriorityFeeTooHigh,
TransactionValidationError::BalanceTooLow => TestError::BalanceTooLow,
TransactionValidationError::TxNonceTooLow => TestError::TxNonceTooLow,
TransactionValidationError::TxNonceTooHigh => TestError::TxNonceTooHigh,
TransactionValidationError::InvalidFeeInput => TestError::InvalidFeeInput,
TransactionValidationError::InvalidChainId => TestError::InvalidChainId,
TransactionValidationError::InvalidSignature => TestError::InvalidSignature,
TransactionValidationError::UnknownError => TestError::UnknownError,
}
}
}
struct TestCase {
pub blockchain_gas_limit: U256,
pub blockchain_base_fee: U256,
pub blockchain_chain_id: u64,
pub is_transactional: bool,
pub chain_id: Option<u64>,
pub nonce: U256,
pub gas_limit: U256,
pub gas_price: Option<U256>,
pub max_fee_per_gas: Option<U256>,
pub max_priority_fee_per_gas: Option<U256>,
pub value: U256,
pub weight_limit: Option<Weight>,
pub proof_size_base_cost: Option<u64>,
}
impl Default for TestCase {
fn default() -> Self {
TestCase {
blockchain_gas_limit: U256::max_value(),
blockchain_base_fee: U256::from(1_000_000_000u128),
blockchain_chain_id: 42u64,
is_transactional: true,
chain_id: Some(42u64),
nonce: U256::zero(),
gas_limit: U256::from(21_000u64),
gas_price: None,
max_fee_per_gas: Some(U256::from(1_000_000_000u128)),
max_priority_fee_per_gas: Some(U256::from(1_000_000_000u128)),
value: U256::from(1u8),
weight_limit: None,
proof_size_base_cost: None,
}
}
}
fn test_env<'config>(input: TestCase) -> CheckEvmTransaction<'config, TestError> {
let TestCase {
blockchain_gas_limit,
blockchain_base_fee,
blockchain_chain_id,
is_transactional,
chain_id,
nonce,
gas_limit,
gas_price,
max_fee_per_gas,
max_priority_fee_per_gas,
value,
weight_limit,
proof_size_base_cost,
} = input;
CheckEvmTransaction::<TestError>::new(
CheckEvmTransactionConfig {
evm_config: &SHANGHAI_CONFIG,
block_gas_limit: blockchain_gas_limit,
base_fee: blockchain_base_fee,
chain_id: blockchain_chain_id,
is_transactional,
},
CheckEvmTransactionInput {
chain_id,
to: Some(H160::default()),
input: vec![],
nonce,
gas_limit,
gas_price,
max_fee_per_gas,
max_priority_fee_per_gas,
value,
access_list: vec![],
},
weight_limit,
proof_size_base_cost,
)
}
fn default_transaction<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
is_transactional,
..Default::default()
})
}
fn transaction_gas_limit_low<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
gas_limit: U256::from(1u8),
is_transactional,
..Default::default()
})
}
fn transaction_gas_limit_low_proof_size<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
weight_limit: Some(Weight::from_parts(1, 1)),
proof_size_base_cost: Some(2),
is_transactional,
..Default::default()
})
}
fn transaction_gas_limit_high<'config>() -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
blockchain_gas_limit: U256::from(1u8),
..Default::default()
})
}
fn transaction_nonce_high<'config>() -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
nonce: U256::from(10u8),
..Default::default()
})
}
fn transaction_invalid_chain_id<'config>() -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
chain_id: Some(555u64),
..Default::default()
})
}
fn transaction_none_fee<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
is_transactional,
..Default::default()
})
}
fn transaction_max_fee_low<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
max_fee_per_gas: Some(U256::from(1u8)),
max_priority_fee_per_gas: None,
is_transactional,
..Default::default()
})
}
fn transaction_priority_fee_high<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
max_priority_fee_per_gas: Some(U256::from(1_100_000_000)),
is_transactional,
..Default::default()
})
}
fn transaction_max_fee_high<'config>(tip: bool) -> CheckEvmTransaction<'config, TestError> {
let mut input = TestCase {
max_fee_per_gas: Some(U256::from(5_000_000_000u128)),
..Default::default()
};
if !tip {
input.max_priority_fee_per_gas = None;
}
test_env(input)
}
fn legacy_transaction<'config>() -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
gas_price: Some(U256::from(1_000_000_000u128)),
max_fee_per_gas: None,
max_priority_fee_per_gas: None,
..Default::default()
})
}
fn invalid_transaction_mixed_fees<'config>(
is_transactional: bool,
) -> CheckEvmTransaction<'config, TestError> {
test_env(TestCase {
gas_price: Some(U256::from(1_000_000_000u128)),
max_fee_per_gas: Some(U256::from(1_000_000_000u128)),
max_priority_fee_per_gas: None,
is_transactional,
..Default::default()
})
}
#[test]
fn validate_in_pool_and_block_succeeds() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let test = default_transaction(true);
assert!(test.validate_in_pool_for(&who).is_ok());
assert!(test.validate_in_block_for(&who).is_ok());
}
#[test]
fn validate_in_pool_and_block_fails_nonce_too_low() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::from(1u8),
};
let test = default_transaction(true);
let res = test.validate_in_pool_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::TxNonceTooLow);
let res = test.validate_in_block_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::TxNonceTooLow);
}
#[test]
fn validate_in_pool_succeeds_nonce_too_high() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::from(1u8),
};
let test = transaction_nonce_high();
let res = test.validate_in_pool_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_in_block_fails_nonce_too_high() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::from(1u8),
};
let test = transaction_nonce_high();
let res = test.validate_in_block_for(&who);
assert!(res.is_err());
}
#[test]
fn validate_in_pool_and_block_transactional_fails_gas_limit_too_low() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let is_transactional = true;
let test = transaction_gas_limit_low(is_transactional);
let res = test.validate_in_pool_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow);
let res = test.validate_in_block_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow);
}
#[test]
fn validate_in_pool_and_block_non_transactional_succeeds_gas_limit_too_low() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let is_transactional = false;
let test = transaction_gas_limit_low(is_transactional);
let res = test.validate_in_pool_for(&who);
assert!(res.is_ok());
let res = test.validate_in_block_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_in_pool_and_block_transactional_fails_gas_limit_too_low_proof_size() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let is_transactional = true;
let test = transaction_gas_limit_low_proof_size(is_transactional);
let res = test.validate_in_pool_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow);
let res = test.validate_in_block_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooLow);
}
#[test]
fn validate_in_pool_and_block_non_transactional_succeeds_gas_limit_too_low_proof_size() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let is_transactional = false;
let test = transaction_gas_limit_low_proof_size(is_transactional);
let res = test.validate_in_pool_for(&who);
assert!(res.is_ok());
let res = test.validate_in_block_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_in_pool_for_fails_gas_limit_too_high() {
let who = Account {
balance: U256::from(1_000_000u128),
nonce: U256::zero(),
};
let test = transaction_gas_limit_high();
let res = test.validate_in_pool_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooHigh);
let res = test.validate_in_block_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasLimitTooHigh);
}
#[test]
fn validate_chain_id_succeeds() {
let test = default_transaction(true);
let res = test.with_chain_id();
assert!(res.is_ok());
}
#[test]
fn validate_chain_id_fails() {
let test = transaction_invalid_chain_id();
let res = test.with_chain_id();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::InvalidChainId);
}
#[test]
fn validate_base_fee_succeeds() {
let test = default_transaction(true);
let res = test.with_base_fee();
assert!(res.is_ok());
let test = default_transaction(false);
let res = test.with_base_fee();
assert!(res.is_ok());
}
#[test]
fn validate_base_fee_with_none_fee_fails() {
let test = transaction_none_fee(true);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::InvalidFeeInput);
}
#[test]
fn validate_base_fee_with_none_fee_non_transactional_succeeds() {
let test = transaction_none_fee(false);
let res = test.with_base_fee();
assert!(res.is_ok());
}
#[test]
fn validate_base_fee_with_max_fee_too_low_fails() {
let test = transaction_max_fee_low(true);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasPriceTooLow);
let test = transaction_max_fee_low(false);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::GasPriceTooLow);
}
#[test]
fn validate_base_fee_with_priority_fee_too_high_fails() {
let test = transaction_priority_fee_high(true);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::PriorityFeeTooHigh);
let test = transaction_priority_fee_high(false);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::PriorityFeeTooHigh);
}
#[test]
fn validate_balance_succeeds() {
let who = Account {
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let test = default_transaction(true);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
let test = default_transaction(false);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_insufficient_balance_fails() {
let who = Account {
balance: U256::from(21_000_000_000_000u128),
nonce: U256::zero(),
};
let test = default_transaction(true);
let res = test.with_balance_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::BalanceTooLow);
let test = default_transaction(false);
let res = test.with_balance_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::BalanceTooLow);
}
#[test]
fn validate_non_fee_transactional_fails() {
let who = Account {
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let test = transaction_none_fee(true);
let res = test.with_balance_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::InvalidFeeInput);
}
#[test]
fn validate_non_fee_non_transactional_succeeds() {
let who = Account {
balance: U256::from(0u8),
nonce: U256::zero(),
};
let test = transaction_none_fee(false);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_balance_regardless_of_base_fee() {
let who = Account {
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let with_tip = false;
let test = transaction_max_fee_high(with_tip);
let res = test.with_balance_for(&who);
assert!(res.is_err());
}
#[test]
fn validate_balance_regardless_of_effective_gas_price() {
let who = Account {
balance: U256::from(42_000_000_000_001u128),
nonce: U256::zero(),
};
let with_tip = true;
let test = transaction_max_fee_high(with_tip);
let res = test.with_balance_for(&who);
assert!(res.is_err());
}
#[test]
fn validate_balance_for_legacy_transaction_succeeds() {
let who = Account {
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let test = legacy_transaction();
let res = test.with_balance_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_balance_for_legacy_transaction_fails() {
let who = Account {
balance: U256::from(21_000_000_000_000u128),
nonce: U256::zero(),
};
let test = legacy_transaction();
let res = test.with_balance_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::BalanceTooLow);
}
#[test]
fn validate_balance_with_invalid_fee_input() {
let who = Account {
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let is_transactional = true;
let test = invalid_transaction_mixed_fees(is_transactional);
let res = test.with_balance_for(&who);
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::InvalidFeeInput);
let is_transactional = false;
let test = invalid_transaction_mixed_fees(is_transactional);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
}
#[test]
fn validate_base_fee_with_invalid_fee_input() {
let is_transactional = true;
let test = invalid_transaction_mixed_fees(is_transactional);
let res = test.with_base_fee();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), TestError::InvalidFeeInput);
let is_transactional = false;
let test = invalid_transaction_mixed_fees(is_transactional);
let res = test.with_base_fee();
assert!(res.is_ok());
}
}