use crate::{EvmTr, PrecompileProvider};
use bytecode::Bytecode;
use context_interface::{
journaled_state::{account::JournaledAccountTr, JournalCheckpoint, JournalTr},
result::InvalidTransaction,
transaction::{AccessListItemTr, AuthorizationTr, Transaction, TransactionType},
Block, Cfg, ContextTr, Database,
};
use core::cmp::Ordering;
use interpreter::GasTracker;
use primitives::{hardfork::SpecId, Address, AddressMap, HashSet, StorageKey, TxKind, U256};
use state::AccountInfo;
pub fn load_accounts<
EVM: EvmTr<Precompiles: PrecompileProvider<EVM::Context>>,
ERROR: From<<<EVM::Context as ContextTr>::Db as Database>::Error>,
>(
evm: &mut EVM,
) -> Result<(), ERROR> {
let (context, precompiles) = evm.ctx_precompiles();
let gen_spec = context.cfg().spec();
let spec = gen_spec.clone().into();
context.journal_mut().set_spec_id(spec);
let precompiles_changed = precompiles.set_spec(gen_spec);
let empty_warmed_precompiles = context.journal_mut().precompile_addresses().is_empty();
if precompiles_changed || empty_warmed_precompiles {
context
.journal_mut()
.warm_precompiles(precompiles.warm_addresses());
}
if spec.is_enabled_in(SpecId::SHANGHAI) {
let coinbase = context.block().beneficiary();
context.journal_mut().warm_coinbase_account(coinbase);
}
let (tx, journal) = context.tx_journal_mut();
if tx.tx_type() != TransactionType::Legacy {
if let Some(access_list) = tx.access_list() {
let mut map: AddressMap<HashSet<StorageKey>> = AddressMap::default();
for item in access_list {
map.entry(*item.address())
.or_default()
.extend(item.storage_slots().map(|key| U256::from_be_bytes(key.0)));
}
journal.warm_access_list(map);
}
}
Ok(())
}
#[inline]
pub fn validate_account_nonce_and_code_with_components(
caller_info: &AccountInfo,
tx: impl Transaction,
cfg: impl Cfg,
) -> Result<(), InvalidTransaction> {
validate_account_nonce_and_code(
caller_info,
tx.nonce(),
cfg.is_eip3607_disabled(),
cfg.is_nonce_check_disabled(),
)
}
#[inline]
pub fn validate_account_nonce_and_code(
caller_info: &AccountInfo,
tx_nonce: u64,
is_eip3607_disabled: bool,
is_nonce_check_disabled: bool,
) -> Result<(), InvalidTransaction> {
if !is_eip3607_disabled {
let bytecode = match caller_info.code.as_ref() {
Some(code) => code,
None => &Bytecode::default(),
};
if !bytecode.is_empty() && !bytecode.is_eip7702() {
return Err(InvalidTransaction::RejectCallerWithCode);
}
}
if !is_nonce_check_disabled {
let tx = tx_nonce;
let state = caller_info.nonce;
if tx == u64::MAX && state == u64::MAX {
return Err(InvalidTransaction::NonceOverflowInTransaction);
}
match tx.cmp(&state) {
Ordering::Greater => {
return Err(InvalidTransaction::NonceTooHigh { tx, state });
}
Ordering::Less => {
return Err(InvalidTransaction::NonceTooLow { tx, state });
}
_ => {}
}
}
Ok(())
}
#[inline]
pub fn calculate_caller_fee(
balance: U256,
tx: impl Transaction,
block: impl Block,
cfg: impl Cfg,
) -> Result<U256, InvalidTransaction> {
if cfg.is_fee_charge_disabled() {
return Ok(balance);
}
let basefee = block.basefee() as u128;
let blob_price = block.blob_gasprice().unwrap_or_default();
let is_balance_check_disabled = cfg.is_balance_check_disabled();
if !is_balance_check_disabled {
tx.ensure_enough_balance(balance)?;
}
let effective_balance_spending = tx
.effective_balance_spending(basefee, blob_price)
.expect("effective balance is always smaller than max balance so it can't overflow");
let gas_balance_spending = effective_balance_spending - tx.value();
let mut new_balance = balance.saturating_sub(gas_balance_spending);
if is_balance_check_disabled {
new_balance = new_balance.max(tx.value());
}
Ok(new_balance)
}
#[inline]
pub fn validate_against_state_and_deduct_caller<
CTX: ContextTr,
ERROR: From<InvalidTransaction> + From<<CTX::Db as Database>::Error>,
>(
context: &mut CTX,
) -> Result<(), ERROR> {
let (block, tx, cfg, journal, _, _) = context.all_mut();
let mut caller = journal.load_account_with_code_mut(tx.caller())?.data;
validate_account_nonce_and_code_with_components(&caller.account().info, tx, cfg)?;
let new_balance = calculate_caller_fee(*caller.balance(), tx, block, cfg)?;
caller.set_balance(new_balance);
if tx.kind().is_call() {
caller.bump_nonce();
}
Ok(())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PreExecutionOutput {
pub eip7702_refund: u64,
pub checkpoint: JournalCheckpoint,
}
#[inline]
pub fn apply_eip7702_auth_list<
CTX: ContextTr,
ERROR: From<InvalidTransaction> + From<<CTX::Db as Database>::Error>,
>(
context: &mut CTX,
gas: &mut GasTracker,
) -> Result<Option<u64>, ERROR> {
if context.cfg().is_amsterdam_eip2780_enabled() {
if context.tx().tx_type() != TransactionType::Eip7702 {
return Ok(Some(0));
}
let chain_id = context.cfg().chain_id();
let is_eip8037 = context.cfg().is_amsterdam_eip8037_enabled();
let params = context.cfg().gas_params();
let account_write_cost = params.tx_account_write_cost();
let new_account_state_gas = if is_eip8037 {
params.new_account_state_gas()
} else {
0
};
let delegation_bytes_state_gas = if is_eip8037 {
params.tx_eip7702_state_gas_bytecode()
} else {
0
};
let (tx, journal) = context.tx_journal_mut();
let mut written_accounts: HashSet<Address> = HashSet::default();
written_accounts.insert(tx.caller());
if let TxKind::Call(target) = tx.kind() {
if !tx.value().is_zero() {
written_accounts.insert(target);
}
}
let oog = apply_auth_list_eip2780::<_, ERROR>(
chain_id,
tx.authorization_list(),
journal,
account_write_cost,
new_account_state_gas,
delegation_bytes_state_gas,
&mut written_accounts,
gas,
)?;
return Ok(if oog { None } else { Some(0) });
}
let chain_id = context.cfg().chain_id();
let (tx, journal) = context.tx_journal_mut();
if tx.tx_type() != TransactionType::Eip7702 {
return Ok(Some(0));
}
let number_of_refunded_accounts =
apply_auth_list::<_, ERROR>(chain_id, tx.authorization_list(), journal)?;
let params = context.cfg().gas_params();
let regular_gas_refund = params
.tx_eip7702_auth_refund_regular()
.saturating_mul(number_of_refunded_accounts);
Ok(Some(regular_gas_refund))
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn apply_auth_list_eip2780<
JOURNAL: JournalTr,
ERROR: From<InvalidTransaction> + From<<JOURNAL::Database as Database>::Error>,
>(
chain_id: u64,
auth_list: impl Iterator<Item = impl AuthorizationTr>,
journal: &mut JOURNAL,
account_write_cost: u64,
new_account_state_gas: u64,
delegation_bytes_state_gas: u64,
written_accounts: &mut HashSet<Address>,
gas: &mut GasTracker,
) -> Result<bool, ERROR> {
let mut charged_delegation_bytes: HashSet<Address> = HashSet::default();
for authorization in auth_list {
let auth_chain_id = authorization.chain_id();
if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) {
continue;
}
if authorization.nonce() == u64::MAX {
continue;
}
let Some(authority) = authorization.authority() else {
continue;
};
let mut authority_acc = journal.load_account_with_code_mut(authority)?;
let authority_acc_info = &authority_acc.account().info;
if let Some(bytecode) = &authority_acc_info.code {
if !bytecode.is_empty() && !bytecode.is_eip7702() {
continue;
}
}
if authorization.nonce() != authority_acc_info.nonce {
continue;
}
let existed = !(authority_acc_info.is_empty()
&& authority_acc
.account()
.is_loaded_as_not_existing_not_touched());
let delegated_now = !authority_acc_info.is_code_hash_empty_or_zero();
let delegated_before_tx = !authority_acc
.account()
.original_info()
.is_code_hash_empty_or_zero();
let clearing = authorization.address().is_zero();
if !existed && !gas.record_state_cost(new_account_state_gas) {
return Ok(true);
}
if !written_accounts.contains(&authority) {
if !gas.record_regular_cost(account_write_cost) {
return Ok(true);
}
written_accounts.insert(authority);
}
if !clearing
&& !delegated_now
&& !delegated_before_tx
&& !charged_delegation_bytes.contains(&authority)
{
if !gas.record_state_cost(delegation_bytes_state_gas) {
return Ok(true);
}
charged_delegation_bytes.insert(authority);
}
authority_acc.delegate(authorization.address());
}
Ok(false)
}
#[inline]
pub fn apply_auth_list<
JOURNAL: JournalTr,
ERROR: From<InvalidTransaction> + From<<JOURNAL::Database as Database>::Error>,
>(
chain_id: u64,
auth_list: impl Iterator<Item = impl AuthorizationTr>,
journal: &mut JOURNAL,
) -> Result<u64, ERROR> {
let mut refunded_accounts = 0;
for authorization in auth_list {
let auth_chain_id = authorization.chain_id();
if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) {
continue;
}
if authorization.nonce() == u64::MAX {
continue;
}
let Some(authority) = authorization.authority() else {
continue;
};
let mut authority_acc = journal.load_account_with_code_mut(authority)?;
let authority_acc_info = &authority_acc.account().info;
if let Some(bytecode) = &authority_acc_info.code {
if !bytecode.is_empty() && !bytecode.is_eip7702() {
continue;
}
}
if authorization.nonce() != authority_acc_info.nonce {
continue;
}
let existed = !(authority_acc_info.is_empty()
&& authority_acc
.account()
.is_loaded_as_not_existing_not_touched());
if existed {
refunded_accounts += 1;
}
authority_acc.delegate(authorization.address());
}
Ok(refunded_accounts)
}
#[cfg(test)]
mod tests {
use super::validate_account_nonce_and_code;
use context_interface::result::InvalidTransaction;
use state::AccountInfo;
#[test]
fn rejects_transactions_when_sender_nonce_is_max() {
let caller_info = AccountInfo {
nonce: u64::MAX,
..AccountInfo::default()
};
let err = validate_account_nonce_and_code(&caller_info, u64::MAX, false, false)
.expect_err("nonce-max sender should be rejected before execution");
assert_eq!(err, InvalidTransaction::NonceOverflowInTransaction);
}
#[test]
fn allows_matching_non_max_nonce() {
let caller_info = AccountInfo {
nonce: 7,
..AccountInfo::default()
};
assert!(validate_account_nonce_and_code(&caller_info, 7, false, false).is_ok());
}
}