REVM源码阅读-流程(3)

PreExecute根据代码里的注释,PreExecute负责的功能:准备EVM状态以供执行加载受益人账户(EIP-3651:预热COINBASE)以及访问列表中的所有账户/存储(EIP-2929)从调用者的余额中扣除最大可能费用对于EIP-7702交易,应用授权列表并委托

PreExecute

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

  • 准备 EVM 状态以供执行
  • 加载受益人账户(EIP-3651:预热 COINBASE)以及访问列表中的所有账户/存储(EIP-2929)
  • 从调用者的余额中扣除最大可能费用
  • 对于 EIP-7702 交易,应用授权列表并委托成功的授权
  • 返回 EIP-7702 的 gas 退款金额。授权在执行开始前应用

说的比较虚,我们直接跳到源码部分:

// crates/handler/src/handler.rs
    #[inline]
    fn pre_execution(&self, evm: &mut Self::Evm) -> Result<u64, Self::Error> {
        self.validate_against_state_and_deduct_caller(evm)?;
        self.load_accounts(evm)?;

        let gas = self.apply_eip7702_auth_list(evm)?;
        Ok(gas)
    }

总共分成3个步骤,我们先看步骤1,validate_against_state_and_deduct_caller\ 跳转到源码实现:

1. validate_against_state_and_deduct_caller

// crates/handler/src/handler.rs
    #[inline]
    fn validate_against_state_and_deduct_caller(
        &self,
        evm: &mut Self::Evm,
    ) -> Result<(), Self::Error> {
        pre_execution::validate_against_state_and_deduct_caller(evm.ctx())
    }

继续跳转

// crates/handler/src/pre_execution.rs
#[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();

    // Load caller's account.
    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(())
}

看下 context.all_mut() 的定义:

// crates/context/interface/src/context.rs
    fn all_mut(
        &mut self,
    ) -> (
        &Self::Block,
        &Self::Tx,
        &Self::Cfg,
        &mut Self::Journal,
        &mut Self::Chain,
        &mut Self::Local,
    );

返回的这几个变量的功能,我们在前面已经介绍过,这里不再复述了.

接着继续往下看

let mut caller = journal.load_account_with_code_mut(tx.caller())?.data;

这里实际调用的是下面的函数,略过查找跳转的部分:

// crates/context/src/journal/inner.rs
/// Loads account into memory. If account is already loaded it will be marked as warm.
 #[inline]
    pub fn load_account_mut_optional_code<'a, 'db, DB: Database>(
        &'a mut self,
        db: &'db mut DB,
        address: Address,
        load_code: bool,
        skip_cold_load: bool,
    ) -> Result<StateLoad<JournaledAccount<'a, DB, ENTRY>>, JournalLoadError<DB::Error>>
    where
        'db: 'a,
    {
        let mut load = self.load_account_mut_optional(db, address, skip_cold_load)?;
        if load_code {
            load.data.load_code_preserve_error()?;
        }
        Ok(load)
    }

Database 在前面没有出现过,他负责的是数据持久化的部分.提供 EVM 执行所需的所有底层状态数据.在这里负责加载账户信息、账户字节码.

先跳转到 load_account_mut_optional 查看实现部分.

// crates/context/src/journal/inner.rs
#[inline(never)]
    pub fn load_account_mut_optional<'a, 'db, DB: Database>(
        &'a mut self,
        db: &'db mut DB,
        address: Address,
        skip_cold_load: bool,
    ) -> Result<StateLoad<JournaledAccount<'a, DB, ENTRY>>, JournalLoadError<DB::Error>>
    where
        'db: 'a,
    {
        let (account, is_cold) = match self.state.entry(address) {
            Entry::Occupied(entry) => {
                let account = entry.into_mut();

                // skip load if account is cold.
                let mut is_cold = account.is_cold_transaction_id(self.transaction_id);

                if unlikely(is_cold) {
                    is_cold = self
                        .warm_addresses
                        .check_is_cold(&address, skip_cold_load)?;

                    // mark it warm.
                    account.mark_warm_with_transaction_id(self.transaction_id);

                    // if it is cold loaded and we have selfdestructed locally it means that
                    // account was selfdestructed in previous transaction and we need to clear its information and storage.
                    if account.is_selfdestructed_locally() {
                        account.selfdestruct();
                        account.unmark_selfdestructed_locally();
                    }
                    // set original info to current info.
                    *account.original_info = account.info.clone();

                    // unmark locally created
                    account.unmark_created_locally();

                    // journal loading of cold account.
                    self.journal.push(ENTRY::account_warmed(address));
                }
                (account, is_cold)
            }
            Entry::Vacant(vac) => {
                // Precompiles,  among some other account(access list and coinbase included)
                // are warm loaded so we need to take that into account
                let is_cold = self
                    .warm_addresses
                    .check_is_cold(&address, skip_cold_load)?;

                let account = if let Some(account) = db.basic(address)? {
                    let mut account: Account = account.into();
                    account.transaction_id = self.transaction_id;
                    account
                } else {
                    Account::new_not_existing(self.transaction_id)
                };

                // journal loading of cold account.
                if is_cold {
                    self.journal.push(ENTRY::account_warmed(address));
                }

                (vac.insert(account), is_cold)
            }
        };

        Ok(StateLoad::new(
            JournaledAccount::new(
                address,
                account,
                &mut self.journal,
                db,
                self.warm_addresses.access_list(),
                self.transaction_id,
            ),
            is_cold,
        ))
    }

self.state 是一个 HashMap, KeyAddress, ValueAccount\ 这里是查找 Account 之前是否已经加载过,并处理各自的逻辑.\ 虽然在代码中,先判断的找到的部分,但是这里我们先看未找到的部分

Entry::Vacant(vac)


// 检测warm_address 中是否存在当前地址
// warm_addresses 包含预编译、AccessList 和 Coinbase
let is_cold = self
                    .warm_addresses
                    .check_is_cold(&address, skip_cold_load)?;

下面是 warm_addresses 的定义,

// crates/context/src/journal/warm_addresses.rs
pub struct WarmAddresses {
    /// Set of warm loaded precompile addresses.
    precompile_set: HashSet<Address>,
    /// Bit vector of precompile short addresses. If address is shorter than [`SHORT_ADDRESS_CAP`] it
    /// will be stored in this bit vector for faster access.
    precompile_short_addresses: BitVec,
    /// `true` if all precompiles are short addresses.
    precompile_all_short_addresses: bool,
    /// Coinbase address.
    coinbase: Option<Address>,
    /// Access list
    access_list: HashMap<Address, HashSet<StorageKey>>,
}

check_is_cold 是检查所有的属性中是否有当前地址.\ 扩展太多,这里就不代码了.\ 继续往下看 Entry::Vacant(vac) 后面的部分

let account = if let Some(account) = db.basic(address)? {
                    let mut account: Account = account.into();
                    account.transaction_id = self.transaction_id;
                    account
                } else {
                    Account::new_not_existing(self.transaction_id)
                };

从数据库中查找该地址.如果找到则设置account 的 transaction_id 并返回.\ 这里的 transaction_id 不是 transaction_hash\ 而是当前 tx 在 block 中的 index, 就是tx在当前block排的位置.\ 记住这里,为什么先看没找到的部分就是因为 transaction_id 的设置.

接着往下比较容易理解,如果 is_cold ,则插入 journalself.state

if is_cold {
                    self.journal.push(ENTRY::account_warmed(address));
                }

                (vac.insert(account), is_cold)

接着看找到的部分\ Entry::Occupied(entry)

let account = entry.into_mut();
let mut is_cold = account.is_cold_transaction_id(self.transaction_id);

我们跳转到 is_cold_transaction_id 实现进行查看

    #[inline]
    pub fn is_cold_transaction_id(&self, transaction_id: usize) -> bool {
        self.transaction_id != transaction_id || self.status.contains(AccountStatus::Cold)
    }

这里为什么能用 self.transaction_id != transaction_id 能判断 Account 是否 cold\ 就是因为在前面没找到的时候设置了 self.transaction_id\ 如果相等的话说明是这次 tx 执行的时候获取的 Account\ 如果不相等的话说明是之前的 tx 执行的时候就获取了 Account, Account 已经 Warm

继续往下.

// 这里跟Rust的分支判断有关,感兴趣的自己去了解下,这里就不扩展了.
if unlikely(is_cold){
 // 前面说过了 
 is_cold = self
  .warm_addresses
  .check_is_cold(&address, skip_cold_load)?;

 // 标记成热地址
 account.mark_warm_with_transaction_id(self.transaction_id);
 // 合约销毁相关代码,暂时略过
 if account.is_selfdestructed_locally() {
  account.selfdestruct();
  account.unmark_selfdestructed_locally();
 }
 // 都比较明显,就不细讲了
 *account.original_info = account.info.clone();
 account.unmark_created_locally();
 self.journal.push(ENTRY::account_warmed(address));
}

讲完 load_account_mut_optional\ 跳回 crates/context/src/journal/inner.rs -> load_account_mut_optional_code\ 接着往下

 if load_code {
 load.data.load_code_preserve_error()?;
}

跳转进去\ 逻辑还是比较清晰,还没需要特别讲的.\ 这里 code_hash 是 Account 代码 的哈希值\ EOA账户没有代码,code_hash 为空\ 合约账户抽象账户都会有代码,code_hash不为空

// crates/context/interface/src/journaled_state/account.rs
 pub fn load_code_preserve_error(&mut self) -> Result<&Bytecode, JournalLoadError<DB::Error>> {
 if self.account.info.code.is_none() {
  let hash = *self.code_hash();
  let code = if hash == KECCAK_EMPTY {
   Bytecode::default()
  } else {
   self.db.code_by_hash(hash)?
  };
  self.account.info.code = Some(code);
 }

 Ok(self.account.info.code.as_ref().unwrap())
}

讲完了 load_account_mut_optional_code\ 我们跳转回到crates/handler/src/pre_execution.rs -> validate_against_state_and_deduct_caller\ 接着往下来到

validate_account_nonce_and_code_with_components(&caller.account().info, tx, cfg)?;

实际调用的 validate_account_nonce_and_code,我们直接跳转查看\ 逻辑很简单,两个判断

  • eip3607

    • 这个提案是禁止合约能用私钥像EOA账户一样签署交易
    • 这个提案是为了防止地址碰撞攻击,感兴趣的可以去了解下
  • nonce

    • 这个是为了验证tx中的 nonce 是否设置正常.
    • nonce 等于你签署上链过的交易数量.
// crates/handler/src/pre_execution.rs
#[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);
        }
    }

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

继续回到crates/handler/src/pre_execution.rs -> validate_against_state_and_deduct_caller\ 接着来到

let new_balance = calculate_caller_fee(*caller.balance(), tx, block, cfg)?;
caller.set_balance(new_balance);

跳转到具体实现

// crates/handler/src/pre_execution.rs
#[inline]
pub fn calculate_caller_fee(
    balance: U256,
    tx: impl Transaction,
    block: impl Block,
    cfg: impl Cfg,
) -> Result<U256, InvalidTransaction> {
    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)
}

前面都是获取参数,我们直接跳到 effective_balance_spending,并点进去看他实现\ 主要关注第一段,这里是估算 tx 会花费的最大费用\ gas_limit * max_fee + value + additional_gas_cost

// crates/context/interface/src/transaction.rs
#[inline]
    fn effective_balance_spending(
        &self,
        base_fee: u128,
        blob_price: u128,
    ) -> Result<U256, InvalidTransaction> {
        let mut effective_balance_spending = (self.gas_limit() as u128)
            .checked_mul(self.effective_gas_price(base_fee))
            .and_then(|gas_cost| U256::from(gas_cost).checked_add(self.value()))
            .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;

        // add blob fee
        if self.tx_type() == TransactionType::Eip4844 {
            let blob_gas = self.total_blob_gas() as u128;
            effective_balance_spending = effective_balance_spending
                .checked_add(U256::from(blob_price.saturating_mul(blob_gas)))
                .ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;
        }

        Ok(effective_balance_spending)
    }

跳转到 effective_gas_price\ Gas Price跟上一章的 validate_tx_env 差不多,这里就不细讲了.\ 可以对照着看下

// crates/context/interface/src/transaction.rs
fn effective_gas_price(&self, base_fee: u128) -> u128 {
        if self.tx_type() == TransactionType::Legacy as u8
            || self.tx_type() == TransactionType::Eip2930 as u8
        {
            return self.gas_price();
        }

        // for EIP-1559 tx and onwards gas_price represents maximum price.
        let max_price = self.gas_price();
        let Some(max_priority_fee) = self.max_priority_fee_per_gas() else {
            return max_price;
        };
        min(max_price, base_fee.saturating_add(max_priority_fee))
    }

继续回到 calculate_caller_fee\ 逻辑都比较简单,就是将 callerBalance 减去 Gas 费用并确保剩下的余额大于** tx.value**

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)

继续回到 crates/handler/src/pre_execution.rs -> validate_against_state_and_deduct_caller

if tx.kind().is_call() {
 caller.bump_nonce();
}

tx.kind 分为两种

  • Call 转账和调用合约都属于这种
  • Create 创建合约\ bump_nonce 是对 Callernonce + 1, 这里只对 Call 的情况进行处理\ CreateCall 都是 Caller 发起的交易, 正常应该都要 nonce + 1\ 这里没处理 Create, 是将它留到 Execute 阶段,在合约创建成功后才 nonce + 1\ 只有上链的tx才会 nonce + 1, 如果创建合约在 constructor阶段 失败就不会上链.

2. load_accounts

跳转回 crates/handler/src/handler.rs -> pre_execution

self.load_accounts(evm)?;

直接跳到具体实现

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();
    // sets eth spec id in journal
    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 {
        // load new precompile addresses into journal.
        // When precompiles addresses are changed we reset the warmed hashmap to those new addresses.
        context
            .journal_mut()
            .warm_precompiles(precompiles.warm_addresses().collect());
    }

    // Load coinbase
    // EIP-3651: Warm COINBASE. Starts the `COINBASE` address warm
    if spec.is_enabled_in(SpecId::SHANGHAI) {
        let coinbase = context.block().beneficiary();
        context.journal_mut().warm_coinbase_account(coinbase);
    }

    // Load access list
    let (tx, journal) = context.tx_journal_mut();
    // legacy is only tx type that does not have access list.
    if tx.tx_type() != TransactionType::Legacy {
        if let Some(access_list) = tx.access_list() {
            let mut map: HashMap<Address, HashSet<StorageKey>> = HashMap::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(())
}

看着长, 但是功能都差不多.\ 加载 Precompiles 地址、CoinBase(矿工) 地址、 上一章提到的 AccessList 地址\ 直接看加载的源码\ 除了 Precompiles ,其他两个直接设置而已\ Precompiles 的初始地址是下面这样的形式

  • 0x0000000000000000000000000000000000000001
  • 0x0000000000000000000000000000000000000002
  • 0x0000000000000000000000000000000000000003\ 短地址后只需要最后两位不为0的
/// crates/context/src/journal/warm_addresses.rs
    #[inline]
    pub fn set_precompile_addresses(&mut self, addresses: HashSet<Address>) {
        self.precompile_short_addresses.fill(false);

        let mut all_short_addresses = true;
        for address in addresses.iter() {
            if let Some(short_address) = short_address(address) {
                self.precompile_short_addresses.set(short_address, true);
            } else {
                all_short_addresses = false;
            }
        }

        self.precompile_all_short_addresses = all_short_addresses;
        self.precompile_set = addresses;
    }

    /// Set the coinbase address.
    #[inline]
    pub fn set_coinbase(&mut self, address: Address) {
        self.coinbase = Some(address);
    }

    /// Set the access list.
    #[inline]
    pub fn set_access_list(&mut self, access_list: HashMap<Address, HashSet<StorageKey>>) {
        self.access_list = access_list;
    }

3. apply_eip7702_auth_list

跳转回 crates/handler/src/handler.rs -> pre_execution

let gas = self.apply_eip7702_auth_list(evm)?;

这是对交易类型为 EIP7702 的处理.\ 不是 EIP7702 类型则直接跳过.

开始前介绍下 EIP7702\ 核心功能是让 ** EOA 账户** 能像智能合约一样,执行一些复杂逻辑.\ 原理是通过私钥签名,将 code 字段设置为一串特殊的字节码.指向一个委托的地址.\ 后续 EOA账户 可以选择通过这个委托地址进行其他的合约调用.\ 委托后的账户称为 智能合约账户, 委托的这个过程称为 账户抽象

直接跳转到函数实现.\ 这里是设置 EOA账户智能合约账户,不是 Sender 通过委托地址调用合约.\ 再说明一点, 账户抽象 的账户不一定是 tx.caller(这里不区分tx.origin) 发出来的\ 可以由 想 账户抽象 的账户 签名,再由其他人广播出去.

// crates/handler/src/pre_execution.rs
#[inline]
pub fn apply_eip7702_auth_list<
    CTX: ContextTr,
    ERROR: From<InvalidTransaction> + From<<CTX::Db as Database>::Error>,
>(
    context: &mut CTX,
) -> Result<u64, ERROR> {
    let chain_id = context.cfg().chain_id();
    let (tx, journal) = context.tx_journal_mut();

    // Return if not EIP-7702 transaction.
    if tx.tx_type() != TransactionType::Eip7702 {
        return Ok(0);
    }
    apply_auth_list(chain_id, tx.authorization_list(), journal)
}

继续跳,下面的逻辑注释比较清晰,就不逐一解释,直接在代码里注释说明

// crates/handler/src/pre_execution.rs
#[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 {
        // 1. 确认链 ID 为 0 或链的当前 ID。
        let auth_chain_id = authorization.chain_id();
        if !auth_chain_id.is_zero() && auth_chain_id != U256::from(chain_id) {
            continue;
        }
        // 2. 验证 `nonce` 是否小于 `2**64 - 1`。
        if authorization.nonce() == u64::MAX {
            continue;
        }

        // 恢复权限和授权地址。
        // 这里的authority指的就是要账户抽象的地址
        // 因为不一定是tx的caller,所这里必须从签名中恢复
        // 3. `authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s]`
        let Some(authority) = authorization.authority() else {
            continue;
        };
        // 4. 预热authority并检查nonce
        // load_account_with_code_mut前面已经深入说过,加载account的信息
        let mut authority_acc = journal.load_account_with_code_mut(authority)?;
        let authority_acc_info = &authority_acc.account().info;
        // 5. 验证 `authority` 的代码是否为空或已被委托。
        if let Some(bytecode) = &authority_acc_info.code {
            // if it is not empty and it is not eip7702
            if !bytecode.is_empty() && !bytecode.is_eip7702() {
                continue;
            }
        }
        // 6. 验证 `authority` 的 nonce 是否等于 `nonce`。如果 `authority` 不存在于 trie 树中,则验证 `nonce` 是否等于 `0`。
        // 这里authorization.nonce就是authority这个原本eoa账户的nonce
        // 如果sender和authority相同,sender的nonce就是authority的nonce
        if authorization.nonce() != authority_acc_info.nonce {
            continue;
        }

        // 7. 如果 trie 中存在 `authority`,则将 `PER_EMPTY_ACCOUNT_COST - PER_AUTH_BASE_COST` gas 添加到全局退款计数器。
        // 这里是计算退款,EIP7702规定,如果authority不是空账户,则退还部分auth退款
        // auth的费用计算在crates/interpreter/src/gas/calc.rs
        // ->calculate_initial_tx_gas 中,在上一章节讲过了
        if !(authority_acc_info.is_empty()
            && authority_acc
                .account()
                .is_loaded_as_not_existing_not_touched())
        {
            refunded_accounts += 1;
        }

        // 8. 将 `authority` 的代码设置为 `0xef0100 || address`。这是一个委托指定。
   // * 特殊情况下,如果 `address` 为`0x000000000000000000000000000000000000000000`,则不写入指定。
// 清除帐户代码并将帐户代码哈希重置为空哈希 `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`。
// 9. 将 `authority` 的 nonce 值加 1。
        authority_acc.delegate(authorization.address());
    }

    let refunded_gas =
        refunded_accounts * (eip7702::PER_EMPTY_ACCOUNT_COST - eip7702::PER_AUTH_BASE_COST);

    Ok(refunded_gas)
}
点赞 0
收藏 0
分享

0 条评论

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