Skip to content

Commit

Permalink
feat: separate initial checks (#486)
Browse files Browse the repository at this point in the history
* wip separate initial checks

* tests passing, consolidate some checks

* test

* feat: add transfer test

* temp

* Update crates/interpreter/src/gas/calc.rs

* / Initial gas that is deducted from the transaction gas limit.

* fmt
  • Loading branch information
rakita authored May 9, 2023
1 parent c3b0312 commit f8ff6b3
Show file tree
Hide file tree
Showing 12 changed files with 493 additions and 367 deletions.
3 changes: 3 additions & 0 deletions bins/revm-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ name = "analysis"

[[bin]]
name = "snailtracer"

[[bin]]
name = "transfer"
1 change: 0 additions & 1 deletion bins/revm-test/src/bin/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ fn main() {
);
//evm.env.tx.data = Bytes::from(hex::decode("30627b7c").unwrap());
evm.env.tx.data = Bytes::from(hex::decode("8035F0CE").unwrap());
evm.env.cfg.perf_all_precompiles_have_balance = true;

let bytecode_raw = Bytecode::new_raw(contract_data.clone());
let bytecode_checked = Bytecode::new_raw(contract_data.clone()).to_checked();
Expand Down
39 changes: 39 additions & 0 deletions bins/revm-test/src/bin/transfer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use revm::{
db::BenchmarkDB,
primitives::{Bytecode, TransactTo, U256},
};
use std::time::{Duration, Instant};
extern crate alloc;

fn main() {
// BenchmarkDB is dummy state that implements Database trait.
let mut evm = revm::new();

// execution globals block hash/gas_limit/coinbase/timestamp..
evm.env.tx.caller = "0x0000000000000000000000000000000000000001"
.parse()
.unwrap();
evm.env.tx.value = U256::from(10);
evm.env.tx.transact_to = TransactTo::Call(
"0x0000000000000000000000000000000000000000"
.parse()
.unwrap(),
);
//evm.env.tx.data = Bytes::from(hex::decode("30627b7c").unwrap());

evm.database(BenchmarkDB::new_bytecode(Bytecode::new()));

// Microbenchmark
let bench_options = microbench::Options::default().time(Duration::from_secs(1));

microbench::bench(&bench_options, "Simple value transfer", || {
let _ = evm.transact().unwrap();
});

let time = Instant::now();
for _ in 0..10000 {
let _ = evm.transact().unwrap();
}
let elapsed = time.elapsed();
println!("10k runs in {:?}", elapsed.as_nanos() / 10_000);
}
48 changes: 48 additions & 0 deletions crates/interpreter/src/gas/calc.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use super::constants::*;
use crate::alloc::vec::Vec;
use crate::{
inner_models::SelfDestructResult,
primitives::Spec,
primitives::{SpecId::*, U256},
};
use revm_primitives::{Bytes, B160};

#[allow(clippy::collapsible_else_if)]
pub fn sstore_refund<SPEC: Spec>(original: U256, current: U256, new: U256) -> i64 {
Expand Down Expand Up @@ -325,3 +327,49 @@ pub fn memory_gas(a: usize) -> u64 {
.saturating_mul(a)
.saturating_add(a.saturating_mul(a) / 512)
}

/// Initial gas that is deducted for transaction to be included.
/// Initial gas contains initial stipend gas, gas for access list and input data.
pub fn initial_tx_gas<SPEC: Spec>(
input: &Bytes,
is_create: bool,
access_list: &[(B160, Vec<U256>)],
) -> u64 {
let mut initial_gas = 0;
let zero_data_len = input.iter().filter(|v| **v == 0).count() as u64;
let non_zero_data_len = input.len() as u64 - zero_data_len;

// initdate stipend
initial_gas += zero_data_len * TRANSACTION_ZERO_DATA;
// EIP-2028: Transaction data gas cost reduction
initial_gas += non_zero_data_len * if SPEC::enabled(ISTANBUL) { 16 } else { 68 };

// get number of access list account and storages.
if SPEC::enabled(BERLIN) {
let accessed_slots = access_list
.iter()
.fold(0, |slot_count, (_, slots)| slot_count + slots.len() as u64);
initial_gas += access_list.len() as u64 * ACCESS_LIST_ADDRESS;
initial_gas += accessed_slots * ACCESS_LIST_STORAGE_KEY;
}

// base stipend
initial_gas += if is_create {
if SPEC::enabled(HOMESTEAD) {
// EIP-2: Homestead Hard-fork Changes
53000
} else {
21000
}
} else {
21000
};

// EIP-3860: Limit and meter initcode
// Initcode stipend for bytecode analysis
if SPEC::enabled(SHANGHAI) && is_create {
initial_gas += initcode_cost(input.len() as u64)
}

initial_gas
}
13 changes: 13 additions & 0 deletions crates/primitives/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Interpreter stack limit
pub const STACK_LIMIT: u64 = 1024;
/// EVM call stack limit
pub const CALL_STACK_LIMIT: u64 = 1024;

/// EIP-170: Contract code size limit
/// By default limit is 0x6000 (~25kb)
pub const MAX_CODE_SIZE: usize = 0x6000;

/// EIP-3860: Limit and meter initcode
///
/// Limit of maximum initcode size is 2 * MAX_CODE_SIZE
pub const MAX_INITCODE_SIZE: usize = 2 * MAX_CODE_SIZE;
167 changes: 160 additions & 7 deletions crates/primitives/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::{alloc::vec::Vec, SpecId, B160, B256, U256};
use crate::{
alloc::vec::Vec, Account, EVMError, InvalidTransaction, Spec, SpecId, B160, B256, KECCAK_EMPTY,
MAX_INITCODE_SIZE, U256,
};
use bytes::Bytes;
use core::cmp::min;
use core::cmp::{min, Ordering};

#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -78,10 +81,6 @@ pub enum CreateScheme {
pub struct CfgEnv {
pub chain_id: U256,
pub spec_id: SpecId,
/// If all precompiles have some balance we can skip initially fetching them from the database.
/// This is is not really needed on mainnet, and defaults to false, but in most cases it is
/// safe to be set to `true`, depending on the chain.
pub perf_all_precompiles_have_balance: bool,
/// Bytecode that is created with CREATE/CREATE2 is by default analysed and jumptable is created.
/// This is very benefitial for testing and speeds up execution of that bytecode if called multiple times.
///
Expand Down Expand Up @@ -121,6 +120,58 @@ pub struct CfgEnv {
pub disable_base_fee: bool,
}

impl CfgEnv {
#[cfg(feature = "optional_eip3607")]
pub fn is_eip3607_disabled(&self) -> bool {
self.disable_eip3607
}

#[cfg(not(feature = "optional_eip3607"))]
pub fn is_eip3607_disabled(&self) -> bool {
false
}

#[cfg(feature = "optional_balance_check")]
pub fn is_balance_check_disabled(&self) -> bool {
self.disable_balance_check
}

#[cfg(not(feature = "optional_balance_check"))]
pub fn is_balance_check_disabled(&self) -> bool {
false
}

#[cfg(feature = "optional_gas_refund")]
pub fn is_gas_refund_disabled(&self) -> bool {
self.disable_gas_refund
}

#[cfg(not(feature = "optional_gas_refund"))]
pub fn is_gas_refund_disabled(&self) -> bool {
false
}

#[cfg(feature = "optional_no_base_fee")]
pub fn is_base_fee_check_disabled(&self) -> bool {
self.disable_base_fee
}

#[cfg(not(feature = "optional_no_base_fee"))]
pub fn is_base_fee_check_disabled(&self) -> bool {
false
}

#[cfg(feature = "optional_block_gas_limit")]
pub fn is_block_gas_limit_disabled(&self) -> bool {
self.disable_block_gas_limit
}

#[cfg(not(feature = "optional_block_gas_limit"))]
pub fn is_block_gas_limit_disabled(&self) -> bool {
false
}
}

#[derive(Clone, Default, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AnalysisKind {
Expand All @@ -135,7 +186,6 @@ impl Default for CfgEnv {
CfgEnv {
chain_id: U256::from(1),
spec_id: SpecId::LATEST,
perf_all_precompiles_have_balance: false,
perf_analyse_created_bytecodes: Default::default(),
limit_contract_code_size: None,
#[cfg(feature = "memory_limit")]
Expand Down Expand Up @@ -196,4 +246,107 @@ impl Env {
)
}
}

/// Validate ENV data of the block.
///
/// It can be skip if you are sure that PREVRANDAO is set.
#[inline]
pub fn validate_block_env<SPEC: Spec, T>(&self) -> Result<(), EVMError<T>> {
// Prevrandao is required for merge
if SPEC::enabled(SpecId::MERGE) && self.block.prevrandao.is_none() {
return Err(EVMError::PrevrandaoNotSet);
}
Ok(())
}

/// Validate transaction data that is set inside ENV and return error if something is wrong.
///
/// Return inital spend gas (Gas needed to execute transaction).
#[inline]
pub fn validate_tx<SPEC: Spec>(&self) -> Result<(), InvalidTransaction> {
let gas_limit = self.tx.gas_limit;
let effective_gas_price = self.effective_gas_price();
let is_create = self.tx.transact_to.is_create();

// BASEFEE tx check
if SPEC::enabled(SpecId::LONDON) {
if let Some(priority_fee) = self.tx.gas_priority_fee {
if priority_fee > self.tx.gas_price {
// or gas_max_fee for eip1559
return Err(InvalidTransaction::GasMaxFeeGreaterThanPriorityFee);
}
}
let basefee = self.block.basefee;

// check minimal cost against basefee
if !self.cfg.is_base_fee_check_disabled() && effective_gas_price < basefee {
return Err(InvalidTransaction::GasPriceLessThanBasefee);
}
}

// Check if gas_limit is more than block_gas_limit
if !self.cfg.is_block_gas_limit_disabled() && U256::from(gas_limit) > self.block.gas_limit {
return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
}

// EIP-3860: Limit and meter initcode
if SPEC::enabled(SpecId::SHANGHAI) && is_create && self.tx.data.len() > MAX_INITCODE_SIZE {
return Err(InvalidTransaction::CreateInitcodeSizeLimit);
}

// Check if the transaction's chain id is correct
if let Some(tx_chain_id) = self.tx.chain_id {
if U256::from(tx_chain_id) != self.cfg.chain_id {
return Err(InvalidTransaction::InvalidChainId);
}
}

// Check if the transaction's chain id is correct
if !SPEC::enabled(SpecId::BERLIN) && !self.tx.access_list.is_empty() {
return Err(InvalidTransaction::AccessListNotSupported);
}

Ok(())
}

/// Validate transaction agains state.
#[inline]
pub fn validate_tx_agains_state(&self, account: &Account) -> Result<(), InvalidTransaction> {
// EIP-3607: Reject transactions from senders with deployed code
// This EIP is introduced after london but there was no collision in past
// so we can leave it enabled always
if !self.cfg.is_eip3607_disabled() && account.info.code_hash != KECCAK_EMPTY {
return Err(InvalidTransaction::RejectCallerWithCode);
}

// Check that the transaction's nonce is correct
if let Some(tx) = self.tx.nonce {
let state = account.info.nonce;
match tx.cmp(&state) {
Ordering::Greater => {
return Err(InvalidTransaction::NonceTooHigh { tx, state });
}
Ordering::Less => {
return Err(InvalidTransaction::NonceTooLow { tx, state });
}
_ => {}
}
}

let balance_check = U256::from(self.tx.gas_limit)
.checked_mul(self.tx.gas_price)
.and_then(|gas_cost| gas_cost.checked_add(self.tx.value))
.ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;

// Check if account has enough balance for gas_limit*gas_price and value transfer.
// Transfer will be done inside `*_inner` functions.
if !self.cfg.is_balance_check_disabled() && balance_check > account.info.balance {
return Err(InvalidTransaction::LackOfFundForMaxFee {
fee: self.tx.gas_limit,
balance: account.info.balance,
});
}

Ok(())
}
}
4 changes: 3 additions & 1 deletion crates/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

pub mod bits;
pub mod bytecode;
pub mod constants;
pub mod db;
pub mod env;
pub mod log;
Expand All @@ -26,8 +27,9 @@ pub type Hash = B256;

pub use bitvec;
pub use bytecode::*;
pub use constants::*;
pub use env::*;
pub use hashbrown::{hash_map, HashMap};
pub use hashbrown::{hash_map, hash_set, HashMap, HashSet};
pub use log::Log;
pub use precompile::*;
pub use result::*;
Expand Down
15 changes: 9 additions & 6 deletions crates/primitives/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use alloc::vec::Vec;
use bytes::Bytes;
use ruint::aliases::U256;

pub type EVMResult<DB> = core::result::Result<ResultAndState, EVMError<DB>>;
pub type EVMResult<DBError> = core::result::Result<ResultAndState, EVMError<DBError>>;

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -91,14 +91,14 @@ impl Output {

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EVMError<DB> {
pub enum EVMError<DBError> {
Transaction(InvalidTransaction),
/// REVM specific and related to environment.
PrevrandaoNotSet,
Database(DB),
Database(DBError),
}

impl<DB> From<InvalidTransaction> for EVMError<DB> {
impl<DBError> From<InvalidTransaction> for EVMError<DBError> {
fn from(invalid: InvalidTransaction) -> Self {
EVMError::Transaction(invalid)
}
Expand All @@ -114,8 +114,8 @@ pub enum InvalidTransaction {
/// EIP-3607 Reject transactions from senders with deployed code
RejectCallerWithCode,
/// Transaction account does not have enough amount of ether to cover transferred value and gas_limit*gas_price.
LackOfFundForGasLimit {
gas_limit: U256,
LackOfFundForMaxFee {
fee: u64,
balance: U256,
},
/// Overflow payment in transaction.
Expand All @@ -133,6 +133,9 @@ pub enum InvalidTransaction {
/// EIP-3860: Limit and meter initcode
CreateInitcodeSizeLimit,
InvalidChainId,
/// Access list is not supported is not supported
/// for blocks before Berlin hardfork.
AccessListNotSupported,
}

/// When transaction return successfully without halts.
Expand Down
Loading

0 comments on commit f8ff6b3

Please sign in to comment.