REVM源码阅读-流程(2)

Validate上篇讲到了handler有五大步骤,Validate就是步骤一.这篇我们接着详细对它进行分析,了解它负责的内容.//crates/handler/src/handler.rs[inline]fnvalidate(&self,evm:&mutSe

Validate

上篇讲到了 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

我们先对 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部分的影响:

  • Max Fee: Sender愿意支付的最高GasPrice费用
  • Priority Fee: 矿工小费,用于加快交易
  • Base Fee: block 的基础费用,由,会被销毁
  • GasPrice = Priority Fee + min(Base Fee, Max Fee - Base Fee)

回到代码部分,这一大段看起来很长,但仔细看看.\ 除了 Eip4844Eip7702, 其他每个判断主要两个功能

  • 判断当前 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(())
}
  • 针对 EIP-4844 tx的处理,EIP-4844主要针对L2的,跳过
  • 针对 EIP-7702 tx的处理,验证是否有authorization_list

validate_initial_tx_gas

根据代码里的注释, validate_initial_tx_gas 负责的功能:

  • 根据交易类型和输入数据计算初始 gas 费用
  • 包括访问列表和授权列表的额外费用。
  • 验证初始费用是否超过交易 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&lt;impl Iterator&lt;Item = Self::AccessListItem&lt;'_>>>
// 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 中的 accountstorage_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 阶段的部分就到此结束.

点赞 1
收藏 0
分享

0 条评论

请先 登录 后评论
唐好酸
唐好酸
江湖只有他的大名,没有他的介绍。