Validate上篇讲到了handler有五大步骤,Validate就是步骤一.这篇我们接着详细对它进行分析,了解它负责的内容.//crates/handler/src/handler.rs[inline]fnvalidate(&self,evm:&mutSe
上篇讲到了 handler 有五大步骤, Validate 就是步骤一.\ 这篇我们接着详细对它进行分析,了解它负责的内容.
// crates/handler/src/handler.rs
#[inline]
fn validate(&self, evm: &mut Self::Evm) -> Result<InitialAndFloorGas, Self::Error> {
self.validate_env(evm)?;
self.validate_initial_tx_gas(evm)
}
从函数和内部的函数调用名字可以看出, Validate 主要负责两部分:\ validate_env: 验证环境.\ validate_initial_tx_gas: 验证初始化的tx的gas.
我们先对 validate_env 进行分析,跳转进去:
// crates/handler/src/handler.rs
#[inline]
fn validate_env(&self, evm: &mut Self::Evm) -> Result<(), Self::Error> {
validation::validate_env(evm.ctx())
}
没啥有用的,只是函数调用,继续跳转:
// crates/handler/src/validation.rs
pub fn validate_env<CTX: ContextTr, ERROR: From<InvalidHeader> + From<InvalidTransaction>>(
context: CTX,
) -> Result<(), ERROR> {
let spec = context.cfg().spec().into();
// `prevrandao` is required for the merge
if spec.is_enabled_in(SpecId::MERGE) && context.block().prevrandao().is_none() {
return Err(InvalidHeader::PrevrandaoNotSet.into());
}
// `excess_blob_gas` is required for Cancun
if spec.is_enabled_in(SpecId::CANCUN) && context.block().blob_excess_gas_and_price().is_none() {
return Err(InvalidHeader::ExcessBlobGasNotSet.into());
}
validate_tx_env::<CTX>(context, spec).map_err(Into::into)
}
let spec = context.cfg().spec().into()\ 获取当前上下文 context 中的 spec,可以理解为当前 EVM 的版本.返回的是一个 enum 类型.
is_enabled_in()\ 将当前spec与传入的spec进行比较,如果当前spec 大于等于 传入的spec,则返回true
context.block().prevrandao().is_none()\ 判断是否在context中设置了 prevrandao
context.block().blob_excess_gas_and_price().is_none()\ 判断是否在context中设置了 blob_excess_gas_and_price
可以看出这段主要是判断, 如果在 context 设置了指定版本的 EVM,是否实现了该版本的功能和升级.
接着往下就到这句,从函数名可以看出,是检查tx的环境:
validate_tx_env::<CTX>(context, spec).map_err(Into::into)
我们直接跳转到函数实现:
// crates/handler/src/validation.rs
pub fn validate_tx_env<CTX: ContextTr>(
context: CTX,
spec_id: SpecId,
) -> Result<(), InvalidTransaction> {
// Check if the transaction's chain id is correct
let tx_type = context.tx().tx_type();
let tx = context.tx();
let base_fee = if context.cfg().is_base_fee_check_disabled() {
None
} else {
Some(context.block().basefee() as u128)
};
let tx_type = TransactionType::from(tx_type);
// Check chain_id if config is enabled.
// EIP-155: Simple replay attack protection
if context.cfg().tx_chain_id_check() {
if let Some(chain_id) = tx.chain_id() {
if chain_id != context.cfg().chain_id() {
return Err(InvalidTransaction::InvalidChainId);
}
} else if !tx_type.is_legacy() && !tx_type.is_custom() {
// Legacy transaction are the only one that can omit chain_id.
return Err(InvalidTransaction::MissingChainId);
}
}
// EIP-7825: Transaction Gas Limit Cap
let cap = context.cfg().tx_gas_limit_cap();
if tx.gas_limit() > cap {
return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
gas_limit: tx.gas_limit(),
cap,
});
}
let disable_priority_fee_check = context.cfg().is_priority_fee_check_disabled();
match tx_type {
TransactionType::Legacy => {
validate_legacy_gas_price(tx.gas_price(), base_fee)?;
}
TransactionType::Eip2930 => {
// Enabled in BERLIN hardfork
if !spec_id.is_enabled_in(SpecId::BERLIN) {
return Err(InvalidTransaction::Eip2930NotSupported);
}
validate_legacy_gas_price(tx.gas_price(), base_fee)?;
}
TransactionType::Eip1559 => {
if !spec_id.is_enabled_in(SpecId::LONDON) {
return Err(InvalidTransaction::Eip1559NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
}
TransactionType::Eip4844 => {
if !spec_id.is_enabled_in(SpecId::CANCUN) {
return Err(InvalidTransaction::Eip4844NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
validate_eip4844_tx(
tx.blob_versioned_hashes(),
tx.max_fee_per_blob_gas(),
context.block().blob_gasprice().unwrap_or_default(),
context.cfg().max_blobs_per_tx(),
)?;
}
TransactionType::Eip7702 => {
// Check if EIP-7702 transaction is enabled.
if !spec_id.is_enabled_in(SpecId::PRAGUE) {
return Err(InvalidTransaction::Eip7702NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
let auth_list_len = tx.authorization_list_len();
// The transaction is considered invalid if the length of authorization_list is zero.
if auth_list_len == 0 {
return Err(InvalidTransaction::EmptyAuthorizationList);
}
}
TransactionType::Custom => {
// Custom transaction type check is not done here.
}
};
// Check if gas_limit is more than block_gas_limit
if !context.cfg().is_block_gas_limit_disabled() && tx.gas_limit() > context.block().gas_limit()
{
return Err(InvalidTransaction::CallerGasLimitMoreThanBlock);
}
// EIP-3860: Limit and meter initcode. Still valid with EIP-7907 and increase of initcode size.
if spec_id.is_enabled_in(SpecId::SHANGHAI)
&& tx.kind().is_create()
&& context.tx().input().len() > context.cfg().max_initcode_size()
{
return Err(InvalidTransaction::CreateInitCodeSizeLimit);
}
Ok(())
}
看着很长,但其实都是各种判断.我们拆分成一段一段
// crates/handler/src/validation.rs
// Check chain_id if config is enabled.
// EIP-155: Simple replay attack protection
if context.cfg().tx_chain_id_check() {
if let Some(chain_id) = tx.chain_id() {
if chain_id != context.cfg().chain_id() {
return Err(InvalidTransaction::InvalidChainId);
}
} else if !tx_type.is_legacy() && !tx_type.is_custom() {
// Legacy transaction are the only one that can omit chain_id.
return Err(InvalidTransaction::MissingChainId);
}
}
检查 Context 中是否启用了 tx_chain_id_check .\ 如果启用了 tx_chain_id_check ,则判断当前 tx 的 chain_id 是否和 context 的一致.
加 chain_id 验证是为了防止 重放攻击.\ 如果没带 chain_id ,你在 ETH 发送的成功交易,其他人可以在其他的 EVM 链再发送一遍.导致你其他链的财产收到损失.(例如你在 BSC 和 ETH 各有10个币,你只在 ETH 给 A 发了 1 ETH.其他人拿到了你在ETH发送的raw Transaction,可以在BSC上进行广播.你在BSC又会给A发送一个BNB.)
// EIP-7825: Transaction Gas Limit Cap
let cap = context.cfg().tx_gas_limit_cap();
if tx.gas_limit() > cap {
return Err(InvalidTransaction::TxGasLimitGreaterThanCap {
gas_limit: tx.gas_limit(),
cap,
});
}
判断当前tx的 GasLimit 是否超过 Context 中设置的值.\ EIP-7825 主要还是为了防止攻击.\ 如果不进行限制,单个tx把块撑爆,一个块能打包的交易就变少.会造成网络拥堵.\ GasLimit 越大,逻辑越复杂,处理时 CPU/内存 也会爆炸,节点同步也会变慢.
match tx_type {
TransactionType::Legacy => {
validate_legacy_gas_price(tx.gas_price(), base_fee)?;
}
TransactionType::Eip2930 => {
// Enabled in BERLIN hardfork
if !spec_id.is_enabled_in(SpecId::BERLIN) {
return Err(InvalidTransaction::Eip2930NotSupported);
}
validate_legacy_gas_price(tx.gas_price(), base_fee)?;
}
TransactionType::Eip1559 => {
if !spec_id.is_enabled_in(SpecId::LONDON) {
return Err(InvalidTransaction::Eip1559NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
}
TransactionType::Eip4844 => {
if !spec_id.is_enabled_in(SpecId::CANCUN) {
return Err(InvalidTransaction::Eip4844NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
validate_eip4844_tx(
tx.blob_versioned_hashes(),
tx.max_fee_per_blob_gas(),
context.block().blob_gasprice().unwrap_or_default(),
context.cfg().max_blobs_per_tx(),
)?;
}
TransactionType::Eip7702 => {
// Check if EIP-7702 transaction is enabled.
if !spec_id.is_enabled_in(SpecId::PRAGUE) {
return Err(InvalidTransaction::Eip7702NotSupported);
}
validate_priority_fee_for_tx(tx, base_fee, disable_priority_fee_check)?;
let auth_list_len = tx.authorization_list_len();
// The transaction is considered invalid if the length of authorization_list is zero.
if auth_list_len == 0 {
return Err(InvalidTransaction::EmptyAuthorizationList);
}
}
TransactionType::Custom => {
// Custom transaction type check is not done here.
}
开始前先讲下 EIP-1559 , EIP-1559 之后 GasPrice 受到3部分的影响:
回到代码部分,这一大段看起来很长,但仔细看看.\ 除了 Eip4844 和 Eip7702, 其他每个判断主要两个功能
判断当前 EVM 版本是否支持 该交易类型.
判断 GasPrice 的设置是否正确.
validate_legacy_gas_price
确保 tx 设置的 GasPrice 大于 Context 中设置的 Base_Fee
validate_priority_fee_for_tx
实际调用的 validate_priority_fee_tx,具体解释在代码中了
pub fn validate_priority_fee_tx(
max_fee: u128,
max_priority_fee: u128,
base_fee: Option<u128>,
disable_priority_fee_check: bool,
) -> Result<(), InvalidTransaction> {
// 确保 max_priority_fee 不会大于 max_fee
if !disable_priority_fee_check && max_priority_fee > max_fee {
return Err(InvalidTransaction::PriorityFeeGreaterThanMaxFee);
}
// 就是上面的公式,确保Base fee + Max Priority Fee 不会大于Max Fee
if let Some(base_fee) = base_fee {
let effective_gas_price = cmp::min(max_fee, base_fee.saturating_add(max_priority_fee));
if effective_gas_price < base_fee {
return Err(InvalidTransaction::GasPriceLessThanBasefee);
}
}
Ok(())
}
根据代码里的注释, validate_initial_tx_gas 负责的功能:
我们接着看代码部分:
// crates/handler/src/handler.rs
#[inline]
fn validate_initial_tx_gas(
&self,
evm: &mut Self::Evm,
) -> Result<InitialAndFloorGas, Self::Error> {
let ctx = evm.ctx_ref();
validation::validate_initial_tx_gas(
ctx.tx(),
ctx.cfg().spec().into(),
ctx.cfg().is_eip7623_disabled(),
)
.map_err(From::from)
}
主要调用的 validation::validate_initial_tx_gas,接着进去跳转进去
// crates/handler/src/validation.rs
pub fn validate_initial_tx_gas(
tx: impl Transaction,
spec: SpecId,
is_eip7623_disabled: bool,
) -> Result<InitialAndFloorGas, InvalidTransaction> {
let mut gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);
if is_eip7623_disabled {
gas.floor_gas = 0
}
// Additional check to see if limit is big enough to cover initial gas.
if gas.initial_gas > tx.gas_limit() {
return Err(InvalidTransaction::CallGasCostMoreThanGasLimit {
gas_limit: tx.gas_limit(),
initial_gas: gas.initial_gas,
});
}
// EIP-7623: Increase calldata cost
// floor gas should be less than gas limit.
if spec.is_enabled_in(SpecId::PRAGUE) && gas.floor_gas > tx.gas_limit() {
return Err(InvalidTransaction::GasFloorMoreThanGasLimit {
gas_floor: gas.floor_gas,
gas_limit: tx.gas_limit(),
});
};
Ok(gas)
}
一步一步进行分析,先关注第一句,从名字可以看出是计算tx的初始gas
let mut gas = gas::calculate_initial_tx_gas_for_tx(&tx, spec);
跳转进去看下,具体实现:
pub fn calculate_initial_tx_gas_for_tx(tx: impl Transaction, spec: SpecId) -> InitialAndFloorGas {
let mut accounts = 0;
let mut storages = 0;
// legacy is only tx type that does not have access list.
if tx.tx_type() != TransactionType::Legacy {
(accounts, storages) = tx
.access_list()
.map(|al| {
al.fold((0, 0), |(mut num_accounts, mut num_storage_slots), item| {
num_accounts += 1;
num_storage_slots += item.storage_slots().count();
(num_accounts, num_storage_slots)
})
})
.unwrap_or_default();
}
calculate_initial_tx_gas(
spec,
tx.input(),
tx.kind().is_create(),
accounts as u64,
storages as u64,
tx.authorization_list_len() as u64,
)
}
前面一大段是对 access_list 的处理.\ access_list: 在发送tx的时候,可以预先声明tx在执行过程中要读取或者写入的地址和存储键列表.\ 在tx中声明的地址和存储键会被提前读取并预热,可以降低Gas消耗.
前面一大段看着是真不舒服,我们一步一步来进行分析.\ 我们先看 access_list 的返回类型,返回的是\ Option\<impl Iterator\<Item = Self::AccessListItem<'_>>>
fn access_list(&self) -> Option<impl Iterator<Item = Self::AccessListItem<'_>>>
// access_list是一个Option, None的时候没有操作,Some的时候则执行map里面的闭包函数
tx.access_list().map()
// fold 对 iterator 的元素进行一个累加操作,
// (0, 0)初始值
// (mut num_accounts, mut num_storage_slots) 在累加过程中用于保存结果的tmp
// item iterator的元素
|al| {al.fold((0, 0), |(mut num_accounts, mut num_storage_slots), item| {
num_accounts += 1;
num_storage_slots += item.storage_slots().count();
(num_accounts, num_storage_slots)
}) }
结合起来就是,如果 access_list 不为空,则对 access_list 中的 account 和 storage_slot 进行累加,计算出实际使用的 account 数量和 storage_slot 数量.
接着看 calculate_initial_tx_gas 的部分,跳转到具体实现
// crates/interpreter/src/gas/calc.rs
pub fn calculate_initial_tx_gas(
spec_id: SpecId,
input: &[u8],
is_create: bool,
access_list_accounts: u64,
access_list_storages: u64,
authorization_list_num: u64,
) -> InitialAndFloorGas {
let mut gas = InitialAndFloorGas::default();
// Initdate stipend
let tokens_in_calldata = get_tokens_in_calldata(input, spec_id.is_enabled_in(SpecId::ISTANBUL));
gas.initial_gas += tokens_in_calldata * STANDARD_TOKEN_COST;
// Get number of access list account and storages.
gas.initial_gas += access_list_accounts * ACCESS_LIST_ADDRESS;
gas.initial_gas += access_list_storages * ACCESS_LIST_STORAGE_KEY;
// Base stipend
gas.initial_gas += if is_create {
if spec_id.is_enabled_in(SpecId::HOMESTEAD) {
// EIP-2: Homestead Hard-fork Changes
53000
} else {
21000
}
} else {
21000
};
// EIP-3860: Limit and meter initcode
// Init code stipend for bytecode analysis
if spec_id.is_enabled_in(SpecId::SHANGHAI) && is_create {
gas.initial_gas += initcode_cost(input.len())
}
// EIP-7702
if spec_id.is_enabled_in(SpecId::PRAGUE) {
gas.initial_gas += authorization_list_num * eip7702::PER_EMPTY_ACCOUNT_COST;
// Calculate gas floor for EIP-7623
gas.floor_gas = calc_tx_floor_cost(tokens_in_calldata);
}
gas
}
继续跳转到 get_tokens_in_calldata 的实现:
#[inline]
pub fn get_tokens_in_calldata(input: &[u8], is_istanbul: bool) -> u64 {
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;
let non_zero_data_multiplier = if is_istanbul {
// EIP-2028: Transaction data gas cost reduction
NON_ZERO_BYTE_MULTIPLIER_ISTANBUL
} else {
NON_ZERO_BYTE_MULTIPLIER
};
zero_data_len + non_zero_data_len * non_zero_data_multiplier
}
逻辑很简单,就是找出tx中 calldata 数据中 0 和 非0 的数量,对 非0 的数量乘以一个固定数值.
这么做的原因是因为 calldata 会永久保存在链上, 大量字节为 0 的数据可以进行高效压缩存储,存储成本低.而 非0 的数据不容易压缩,存储成本高.
calculate_initial_tx_gas 后面的逻辑都比较明显,就不细说了.
跳回 validate_initial_tx_gas, 后面的逻辑也比较明显,也不继续了.
validate 阶段的部分就到此结束.
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!