REVM源码阅读-Context

前言Context用于提供本次交易执行过程中的全局上下文.包括本次TX信息、当前区块配置、状态变更日志、外部DB交互.Context直接跳到结构体定义部分.derive_where宏之前说过了.就是为Context分别派生Debug和Clone.block:区块环境,就是

前言

Context 用于提供本次交易执行过程中的全局上下文.
包括本次TX信息、当前区块配置、状态变更日志、外部DB交互.

Context

直接跳到结构体定义部分.

derive_where 宏之前说过了.就是为 Context 分别派生 Debug 和Clone.

  • block: 区块环境,就是打包这次TX的Block配置
  • tx: 包含这次TX交易的所有信息
  • cfg: EVM 配置信息、例如硬分叉版本、 链ID等
  • DB: 数据库接口,用于保存本次TX的变更.由外部调用 EVM 的对象提供.
  • journaled_state: 在提交到数据库前,所有状态变更保存在JOURNAL中,并提供回撤机制.
  • chain: 扩展链的时候用的,主网用不到.
  • local: 提供SharedMemory共享内存使用的Buffer,保存预编译合约调用过程中的错误.
  • error: 执行过程中的错误信息.
// crates/context/src/context.rs
#[derive_where(Clone, Debug; BLOCK, CFG, CHAIN, TX, DB, JOURNAL, <DB as Database>::Error, LOCAL)]
pub struct Context<
    BLOCK = BlockEnv,
    TX = TxEnv,
    CFG = CfgEnv,
    DB: Database = EmptyDB,
    JOURNAL: JournalTr<Database = DB> = Journal<DB>,
    CHAIN = (),
    LOCAL: LocalContextTr = LocalContext,
> {
    pub block: BLOCK,
    pub tx: TX,
    pub cfg: CFG,
    pub journaled_state: JOURNAL,
    pub chain: CHAIN,
    pub local: LOCAL,
    pub error: Result<(), ContextError<DB::Error>>,
}

继续看实现部分.基本上都很简单.就不贴函数具体内容了.
Context 就是起一个管理作用,配置和操作都是由他的成员来提供.
allall_mut 的区别是 all_mut 后面三个返回值是 可变引用 &mut

// crates/context/src/context.rs
#[inline]
    fn all(&self,) -> (&Self::Block,&Self::Tx,&Self::Cfg,&Self::Db,&Self::Journal,&Self::Chain,&Self::Local,
    ) {}

#[inline]
    fn all_mut(&self,) -> (&Self::Block,&Self::Tx,&Self::Cfg,&Self::Db,&mut Self::Journal,&mut Self::Chain,&mut Self::Local,
    ) {}

#[inline]
fn error(&mut self) -> &mut Result<(), ContextError<<Self::Db as Database>::Error>> {
    &mut self.error
}
fn set_tx(&mut self, tx: Self::Tx) {
    self.tx = tx;
}
fn set_block(&mut self, block: Self::Block) {
    self.block = block;
}

后面的函数也很简单.都是 New 实例化部分,或者对成员字段内部函数的包装.
就不贴了.感兴趣的直接去看下.

讲一下里面的 selfdestruct 里面的 cold_path.
点进起来看实现,是一个空函数.不过加了 #[cold] 宏.

这是用于编译器的.它是告诉 编译器/CPU ,这段代码极少会被执行到.将它放到生成的二进制文件相对偏僻的位置.
这样CPU在加载执行的时候可以只加载执行正常流程,提高运行速度.

pub fn new(db: DB, spec: SPEC) -> Self {
    let mut journaled_state = JOURNAL::new(db);
    journaled_state.set_spec_id(spec.into());
    Self {
        tx: TX::default(),
        block: BLOCK::default(),
        cfg: CfgEnv {
            spec,
            ..Default::default()
        },
        local: LOCAL::default(),
        journaled_state,
        chain: Default::default(),
        error: Ok(()),
    }
}
#[inline]
fn selfdestruct(
    &mut self,
    address: Address,
    target: Address,
    skip_cold_load: bool,
) -> Result<StateLoad<SelfDestructResult>, LoadError> {
    self.journal_mut()
        .selfdestruct(address, target, skip_cold_load)
        .map_err(|e| {
            cold_path();
            let (ret, err) = e.into_parts();
            if let Some(err) = err {
                *self.error() = Err(err.into());
            }
            ret
        })
}
// crates/primitives/src/hints_util.rs
#[cold]
pub fn cold_path() {}

BlockEnv

上面讲到 Block 是区块环境,就是打包这次TX的Block配置.
EVM 只负责执行部分,没有保存 Block 信息.
所以 Block 信息都是由外部提供, 例如 ETH.
我们看下结构体

  • number 当前区块高度
  • beneficiary 受益人,打包这个区块的矿工
  • timestamp 打包时的时间戳
  • basefee GasPrice分成了两部分,BaseFee和PriorityFee.这里设置的BaseFee
  • difficulty POW 时代的难度值.Paris 硬分叉后弃用了.
  • prevrandao 由共识层提供的上一区块 RANDAO mix 值,可用于链上生成随机数.
  • blob_excess_gas_and_price 超额 blob gasblob gas 价格,
    • blob 主要是 L2 在用, 这里就不深入了
pub struct BlockEnv {
    pub number: U256,
    pub beneficiary: Address,
    pub timestamp: U256,
    pub gas_limit: u64,
    pub basefee: u64,
    pub difficulty: U256,
    pub prevrandao: Option<B256>,
    pub blob_excess_gas_and_price: Option<BlobExcessGasAndPrice>,
}

因为只是保存外部提供的数据,并提供给 EVM 使用.
所以并没有什么复杂的操作. 实现的部分都是提供 Setter 供外部调用.

#[inline]
    fn number(&self) -> U256 {
        self.number
    }
    #[inline]
    fn beneficiary(&self) -> Address {
        self.beneficiary
    }
    #[inline]
    fn timestamp(&self) -> U256 {
        self.timestamp
    }
    #[inline]
    fn gas_limit(&self) -> u64 {
        self.gas_limit
    }
    #[inline]
    fn basefee(&self) -> u64 {
        self.basefee
    }
    #[inline]
    fn difficulty(&self) -> U256 {
        self.difficulty
    }
    #[inline]
    fn prevrandao(&self) -> Option<B256> {
        self.prevrandao
    }
    #[inline]
    fn blob_excess_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
        self.blob_excess_gas_and_price
    }

TxEnv

上面讲到 tx 包含这次交易的所有信息.
Block 一样也是由外部提供信息. EVM 只负责执行.

TX 部分的内容看起来比 Block
原因就是 EVM 出现得早, 补丁打得多, 历史包袱重,得分别处理每次升级带来的变化.
简单来说就是 老块老办法, 新块新办法

TX 部分的内容分为两段.

  • TxEnv, 负责返回tx的数据,相当于 Getter 部分
  • TxEnvBuilder, 负责构建 Tx ,相当于 Setter 部分

先看下 TxEnv

  • tx_type tx类型
    • Legacy
    • Eip2930
    • Eip1559
    • Eip4844
    • Eip7702
    • Custom
  • caller 本次tx的发起者、tx的签名人
  • gas_limit tx的gas最大使用限制
  • gas_price gas的每单位价格.EIP-1559之后表示max_gas_fee.
  • kind 是 Create 交易(部署合约)还是 Call (调用)交易,如果是 Call 交易, 会包含 to
  • value 本次交易发送的value
  • data 本次交易发送到to的数据
  • nonce EOA账户的Nonce,等于发送上链的交易数
  • chain_id 链ID,用于防止重放攻击
  • access_list access列表,用于提前预热地址列表,后面访问可以减少Gas消耗
  • gas_priority_fee Gas优先费用,用于贿赂矿工提前打包tx
  • blob_hashes blob的版本号哈希列表
  • max_fee_per_blob_gas 本次交易中的每个 blob gas 愿意支付的最大费用
  • authorization_list 授权列表,用于授权EOA账户智能合约账户
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TxEnv {
    pub tx_type: u8,
    pub caller: Address,
    pub gas_limit: u64,
    pub gas_price: u128,
    pub kind: TxKind,
    pub value: U256,
    pub data: Bytes,
    pub nonce: u64,
    pub chain_id: Option<u64>,
    pub access_list: AccessList,
    pub gas_priority_fee: Option<u128>,
    pub blob_hashes: Vec<B256>,
    pub max_fee_per_blob_gas: u128,
    pub authorization_list: Vec<Either<SignedAuthorization, RecoveredAuthorization>>,
}

大部分实现都一目了然,只讲个特定的.

pub fn derive_tx_type(&mut self) 根据 Tx 中的字段来设置 tx_type
这里 EIP-2930EIP-1559 的判断会让人有些迷惑.不应该是先 EIP-1159 再到 EIP-2930
但现实是 EIP-2930EIP-1559 的提案要早.
Eip2930 的交易类型并不包含gas_priority_fee.

其他没啥讲的, TxEnvBuilder 也是.

pub fn derive_tx_type(&mut self) -> Result<(), DeriveTxTypeError> {
    if !self.access_list.0.is_empty() {
        self.tx_type = TransactionType::Eip2930 as u8;
    }

    if self.gas_priority_fee.is_some() {
        self.tx_type = TransactionType::Eip1559 as u8;
    }

    if !self.blob_hashes.is_empty() || self.max_fee_per_blob_gas > 0 {
        if let TxKind::Call(_) = self.kind {
            self.tx_type = TransactionType::Eip4844 as u8;
            return Ok(());
        } else {
            return Err(DeriveTxTypeError::MissingTargetForEip4844);
        }
    }

    if !self.authorization_list.is_empty() {
        if let TxKind::Call(_) = self.kind {
            self.tx_type = TransactionType::Eip7702 as u8;
            return Ok(());
        } else {
            return Err(DeriveTxTypeError::MissingTargetForEip7702);
        }
    }
    Ok(())
}

CFG

这里用来设置或获取 EVM 配置. 也是由外部传入设置.
内容比较简单,就不详细展开.简单说下各个属性的作用.
blob 的主要是 L2 在用.

  • chain_id 链ID,每个 EVM 链有自己的chain id,用于防止重放攻击.
  • tx_chain_id_check 配置是否检查 chain_id
  • spec 当前硬分叉版本
  • limit_contract_code_size 合约代码大小限制
  • limit_contract_initcode_size 合约初始化代码大小限制
  • disable_nonce_check 禁用 tx 中的 nonce 检查.
  • max_blobs_per_tx tx中blobs最大限制
  • blob_base_fee_update_fraction Blob 基本费用更新分数
  • tx_gas_limit_cap 限制单笔交易的Gas Limit
  • memory_limit 内存限制,当前 ETH主网 默认无限制.
  • disable_balance_check 禁用余额检查,测试使用.
  • disable_block_gas_limit 禁用区块 gas limit 检查
  • disable_eip3541 (EIP-3541: 禁止以 0xEF 字节开头的新合约部署)
  • disable_eip3607 (EIP-3607:禁止部署了合约的地址作为交易的发起者)
  • disable_eip7623 (EIP-7623: calldata 非零字节成本从 16 gas -> 更高)
  • disable_base_fee 禁用 BaseFee
  • disable_priority_fee_check 禁用 PriorityFee
  • disable_fee_charge 禁用费用收取.相当于 免Gas 交易,交易不扣 Gas
// crates/context/src/cfg.rs
pub struct CfgEnv<SPEC = SpecId> {
    pub chain_id: u64,
    pub tx_chain_id_check: bool,
    pub spec: SPEC,
    pub limit_contract_code_size: Option<usize>,
    pub limit_contract_initcode_size: Option<usize>,
    pub disable_nonce_check: bool,
    pub max_blobs_per_tx: Option<u64>,
    pub blob_base_fee_update_fraction: Option<u64>,
    pub tx_gas_limit_cap: Option<u64>,
    pub memory_limit: u64,
    pub disable_balance_check: bool,
    pub disable_block_gas_limit: bool,
    pub disable_eip3541: bool,
    pub disable_eip3607: bool,
    pub disable_eip7623: bool,
    pub disable_base_fee: bool,
    pub disable_priority_fee_check: bool,
    pub disable_fee_charge: bool,
}

Database

REVM 只负责执行部分, 自然没有持久化保存数据的部分,也就是没有 DB.
REVM 只提供 Trait 约束, 然后由外部实现并提供给 REVM.
REVM 在交易执行完成后可以选择是否通过接口提交到外部数据库.

这一节就只讲下设计到 DB 相关的 Trait.
后续有时间可以讲下 AlloyDb 和 CacheDb, 它们是用来 Fork 主网数据.
使用主网数据,模拟交易在主网环境下运行的结果.

  • basic 获取指定地址的基本信息.不区分 EOA合约账户.
    • balance 余额
    • nonce
    • code_hash 如果是EOA账户,是个固定值.如果是合约账户,是代码的哈希
    • account_id REVM 内部实现,不是 ETH 协议层面的东西.
    • code 账户字节码(为 None 才用 code_hash 去获取)
  • code_by_hash 根据 code_hash 获取对应的合约字节码
  • storage 获取地址在 index 的值
  • storage_by_account_id 通过 account_id 获取storage,account_id 为空则走storage函数
  • block_hash 获取指定 blockhash
// crates/database/interface/src/lib.rs
pub trait Database {
    type Error: DBErrorMarker;
    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error>;
    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error>;
    fn storage(&mut self, address: Address, index: StorageKey)
        -> Result<StorageValue, Self::Error>;
    fn storage_by_account_id(
        &mut self,
        address: Address,
        account_id: usize,
        storage_key: StorageKey,
    ) -> Result<StorageValue, Self::Error> {
        let _ = account_id;
        self.storage(address, storage_key)
    }

    /// Gets block hash by block number.
    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error>;
}

两者不同的只是传入的参数不通. commit_iter 还是转换参数类型后还是调用的 commit

// crates/database/interface/src/lib.rs
#[auto_impl(&mut, Box)]
pub trait DatabaseCommit {
    fn commit(&mut self, changes: HashMap<Address, Account>);
    fn commit_iter(&mut self, changes: impl IntoIterator<Item = (Address, Account)>) {
        let changes: HashMap<Address, Account> = changes.into_iter().collect();
        self.commit(changes);
    }
}

DatabaseRefDatabase Trait 的函数功能是一样的.
DatabaseRef 函数的第一个参数是&self, Database 是可变引用 &mut self.
名字后面加了_ref.
至于为什么这样做.接下来会说.

继续往下看, WrapDatabaseRef.
对实现了 DatabaseRef Trait 的 泛型T 添加一层外包装.

一直没懂这个包装 WrapDatabaseRef 的使用场景在哪.
唯一的调用是 context 中的 with_ref_db.

这里加了外包装算是对 Database 做降级处理.
本来调用函数的都是 可变引用 &mut , 现在降级成引用 &.
可能是为了兼容, Rust Trait 系统不允许 &mut self方法直接调用 &self 方法
加个中间层,你想用哪个用哪个

#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct WrapDatabaseRef<T: DatabaseRef>(pub T);
impl<F: DatabaseRef> From<F> for WrapDatabaseRef<F> {
    #[inline]
    fn from(f: F) -> Self {
        WrapDatabaseRef(f)
    }
}
impl<T: DatabaseRef> Database for WrapDatabaseRef<T> {
    type Error = T::Error;

    #[inline]
    fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
        self.0.basic_ref(address)
    }

    #[inline]
    fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
        self.0.code_by_hash_ref(code_hash)
    }

    #[inline]
    fn storage(
        &mut self,
        address: Address,
        index: StorageKey,
    ) -> Result<StorageValue, Self::Error> {
        self.0.storage_ref(address, index)
    }

    #[inline]
    fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
        self.0.block_hash_ref(number)
    }
}

其他的都是差不多的 Trait ,跳过了.
直接看 DatabaseCommitExt 部分.

  • increment_balances 批量增加余额
  • drain_balances 批量清空余额
  • 两个函数逻辑差不多,简单讲下 drain_balances
    addresses_iter.size_hint 返回原组,左边是最小长度.
    core::mem::take 取出原有的值,并替换为类型的默认值(相当于置零).

    • 函数流程就是接受一个 item 为地址的迭代器,遍历并将 Balance 置零.
    • 使用 commit_iter 进行提交
pub trait DatabaseCommitExt: Database + DatabaseCommit {
    fn increment_balances(
        &mut self,
        balances: impl IntoIterator<Item = (Address, u128)>,
    ) -> Result<(), Self::Error> {
        // Make transition and update cache state
        let transitions = balances
            .into_iter()
            .map(|(address, balance)| {
                let mut original_account = match self.basic(address)? {
                    Some(acc_info) => Account::from(acc_info),
                    None => Account::new_not_existing(0),
                };
                original_account.info.balance = original_account
                    .info
                    .balance
                    .saturating_add(U256::from(balance));
                original_account.mark_touch();
                Ok((address, original_account))
            })
            // Unfortunately must collect here to short circuit on error
            .collect::<Result<Vec<_>, _>>()?;

        self.commit_iter(transitions);
        Ok(())
    }

    fn drain_balances(
        &mut self,
        addresses: impl IntoIterator<Item = Address>,
    ) -> Result<Vec<u128>, Self::Error> {
        // Make transition and update cache state
        let addresses_iter = addresses.into_iter();
        let (lower, _) = addresses_iter.size_hint();
        let mut transitions = Vec::with_capacity(lower);
        let balances = addresses_iter
            .map(|address| {
                let mut original_account = match self.basic(address)? {
                    Some(acc_info) => Account::from(acc_info),
                    None => Account::new_not_existing(0),
                };
                let balance = core::mem::take(&mut original_account.info.balance);
                original_account.mark_touch();
                transitions.push((address, original_account));
                Ok(balance.try_into().unwrap())
            })
            .collect::<Result<Vec<_>, _>>()?;

        self.commit_iter(transitions);
        Ok(balances)
    }
}

LocalContext

LocalContext 负责的内容蛮简单的.就两个功能:
提供一大块内存作为共享供 Frame 使用.只负责提供,不负责管理.

提供接口让 OpCode 在执行过程中保存 调用预编译合约 时的错误.
至于为什么不和普通 OpCode 执行一样,放到内存中进行返回?

是因为 预编译合约 和 OpCode 属于两个不同层面的东西. OpCode 由EVM 中 解释器 运行执行的,有完整的Stack、Memory、Return Data.

而 Precompile 相当于直接调用 Rust 函数. 这些函数并不是访问当前 Frame 的状态.只能根据传入的值进行计算.自然也无法访问内存,将错误信息写入到内存中的返回数据中.

看下 LocalContext 的属性和实现.
非常简单,就不多解释了.
Rc<RefCell<Vec<u8>>> 前面说过,就是让 Vec<u8> 可以被多个对象持有并修改.

pub struct LocalContext {
    pub shared_memory_buffer: Rc<RefCell<Vec<u8>>>,
    pub precompile_error_message: Option<String>,
}

impl Default for LocalContext {
    fn default() -> Self {
        Self {
            shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(1024 * 4))),
            precompile_error_message: None,
        }
    }
}

impl LocalContextTr for LocalContext {
    fn clear(&mut self) {
        unsafe { self.shared_memory_buffer.borrow_mut().set_len(0) };
        self.precompile_error_message = None;
    }
    fn shared_memory_buffer(&self) -> &Rc<RefCell<Vec<u8>>> {
        &self.shared_memory_buffer
    }
    fn set_precompile_error_context(&mut self, output: String) {
        self.precompile_error_message = Some(output);
    }
    fn take_precompile_error_context(&mut self) -> Option<String> {
        self.precompile_error_message.take()
    }
}
impl LocalContext {
    pub fn new() -> Self {
        Self::default()
    }
}

Journal-1

在 EVM 执行过程中,会产生很多临时状态变更.例如 NonceChange、BalanceTransfer、BalanceChange、CodeChange等待.
这些临时状态不会马上存储到外部永久数据库中,而是结束后再统一 Commit.

在上节中, 我们讲了 Database 的部分,但是Context并不直接持有 Database 的实例.而是由 Journal 持有并管理.

Journal 相当于 REVMETH 之间的缓存层,缓存从外部 DB 读来的数据.
缓存将来要写入 DB 的数据.

看下 Journal 的结构体.
很简单,就两个成员变量,一个 database, 一个 inner.
database 是持有的外部数据库引用,用来获取和保存链上数据.
inner 是 Journal 逻辑的真正部分, Journal 的大部分操作都是对 inner 的一层封装.

// crates/context/interface/src/journaled_state/entry.rs
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Journal<DB, ENTRY = JournalEntry>
where
    ENTRY: JournalEntryTr,
{
    pub database: DB,
    pub inner: JournalInner<ENTRY>,
}

直接跳转到 JournalInner 的部分.
解释下各个变量的作用.

  • state 缓存当前交易已 Touched 过 或者 修改过 的账户
  • transient_storage 瞬态存储.和内存的作用很像,都是临时存储.不同的是内存 不能跨Frame 访问,Frame 结束就删除. 而transient storage是所有 Frame 可访问,生命周期在整个交易内.
  • logs 交易执行过程,产生的所有事件Log.包含所有 Frame 的,也就是所有的合约调用.
  • depth 当前调用 Frame 的深度,和 Frame 中的 depth 是一样的.配合 journal 使用.
  • journal 交易执行过程中,产生的所有临时状态变更栈.
  • transaction_id 当前 Tx 在 当前 Block 中的 位置索引 Index.
  • spec EVM硬分叉版本
  • warm_addresses 预热地址列表.包含AccesList、矿工Coinbase、预编译合约Precompiles
// crates/context/src/journal/inner.rs
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct JournalInner<ENTRY> {
    pub state: EvmState,
    pub transient_storage: TransientStorage,
    pub logs: Vec<Log>,
    pub depth: usize,
    pub journal: Vec<ENTRY>,
    pub transaction_id: usize,
    pub spec: SpecId,
    pub warm_addresses: WarmAddresses,
}

看下 JournalInner 的部分实现.

  • commit_tx 每个 TX 执行成功调用
    • transient_storage 清空
    • depth 置零
    • journal 清空
    • warm_addresses 清空
    • transaction_id 加1
    • logs 清空
  • discard_tx 丢弃当前 TX, Revert 时调用.和 commit_tx 不同的是
    • journal 不是清空,而是从后向前回滚所有的条目.
    • 回滚的逻辑之前在 流程(5) 已经解释过,但是 entry.revert没说,在后面再具体解释.
  • finalize 整个 Block 的所有交易结束时调用.和 commit_tx 不同的是
    • transaction_id 置零,而不是加1
    • 返回 state,也就是所有Touched和修改过的账户,由外部决定是否提交到数据库.
// crates/context/src/journal/inner.rs
pub fn commit_tx(&mut self) {
    // Clears all field from JournalInner. Doing it this way to avoid
    // missing any field.
    let Self {
        state,
        transient_storage,
        logs,
        depth,
        journal,
        transaction_id,
        spec,
        warm_addresses,
    } = self;
    // Spec, precompiles, BAL and state are not changed. It is always set again execution.
    let _ = spec;
    let _ = state;
    transient_storage.clear();
    *depth = 0;

    // Do nothing with journal history so we can skip cloning present journal.
    journal.clear();

    // Clear coinbase address warming for next tx
    warm_addresses.clear_coinbase_and_access_list();
    // increment transaction id.
    *transaction_id += 1;

    logs.clear();
}
pub fn discard_tx(&mut self) {
    ~~~
    journal.drain(..).rev().for_each(|entry| {
            entry.revert(state, None, is_spurious_dragon_enabled);
        });
    ~~~
}
pub fn finalize(&mut self) -> EvmState {}

Account

没有继续讲 JournalInner 而是跳到这里的原因是
后面 JournalInner 的内容基本都是跟 AccountENTRY 相关.
后面的内容都是对它们的封装,直接讲只知道怎么用而不知道为什么这样写.

上面讲到的 state 的类型是 EvmState. pub type EvmState = HashMap<Address, Account>.
这就是为什么要讲 Account 的原因.
先看下结构体

  • info 账户的所有信息,具体内容在上一章的 Database 有介绍过
  • original_info 账户在交易开始时,首次加载后的初始值.
  • transaction_id TxBlock 中的 Index
  • storage 地址对应的存储,合约的属性字段就是保存在 storage 中.
    • value 的类型是 EvmStorageSlot,这是它的字段
      • original_value storage 的原始值
      • present_value storage 的现值
      • transaction_id txblock 中的 index
      • is_cold 是否冷加载(第一次加载,Gas 扣费不一样)
  • status 账户状态(数值按位表示,方便叠加和移除)
    • Created:0b00000001账户在 本次交易 中被创建.(标记后不会去查数据库)
    • CreatedLocal:0b10000000 账户在 当前Frame 被创建
    • SelfDestructed:0b00000010 账户 本次交易被标记为自毁.(交易结束后会被从数据库删除)
    • SelfDestructedLocal:0b01000000 账户在 当前Frame 中被标记为自毁.
    • Touched:0b00000100 账户被读或者写过.(被标记过的账户才会考虑写入数据库)
    • LoadedAsNotExisting:0b00001000 账户被加载为不存在(去DB查,没找到)
    • Cold:0b00010000 冷账户,和上面的 is_cold 功能一样
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Account {
    pub info: AccountInfo,
    pub original_info: Box<AccountInfo>,
    pub transaction_id: usize,
    pub storage: EvmStorage,
    pub status: AccountStatus,
}

看下实现

  • new_not_existing 创建一个新的 Account 并标记为不存在.
    这里使用了 OnceLock. OneLock 不是读写锁,而是单例锁.
    用于保证这个对象只会被初始化一次.

    EVM 执行过程中可能会频繁创建新 Account
    新建一个static 模板在全局内存中用 Clone 的方式,可以减少内存分配开销.

  • caller_initial_modificationcaller 设置新的金额并标记为 Touched

  • state_clear_aware_is_empty 判断一个账户是否为空.

    • 如果一个账户的 Nonce 为0、Balance 为0、CodeHash也为0,这就是空账户
    • 在EIP-161(SpecId::SPURIOUS_DRAGON硬分叉)之后,是要被从数据库中删除的.
    • 删除是因为有些垃圾地址会大量占用外部DB存储空间(例如大量被转了0 ETH的空地址)
  • changed_storage_slots 判断 storage 是否被修改过

    • 通过对比所有 storageoriginal_valuepresent_value 是否相等.
    • 这里是用于保存到数据库中,所以并不关心中间是否修改过几次.
  • is_cold_transaction_id 判断

    • 虽然EVM一次只执行一次交易.但是 Journal 实例是会被多个交易共用的.
    • 同一个地址会被不同交易访问.第一次访问后会一直保存在 Journal 中
    • 但对于每笔交易,如果这个地址你是第一次访问.这个地址对于你还是 Cold
  • mark_warm_with_transaction_id 标志该地址对于该交易是 Warm

  • 剩下的都是 SetterGetter,就不细讲了.

pub fn new_not_existing(transaction_id: usize) -> Self {
    static DEFAULT: OnceLock<Account> = OnceLock::new();
    DEFAULT
        .get_or_init(|| Self {
            info: AccountInfo::default(),
            storage: HashMap::default(),
            transaction_id,
            status: AccountStatus::LoadedAsNotExisting,
            original_info: Box::new(AccountInfo::default()),
        })
        .clone()
}
pub fn caller_initial_modification(&mut self, new_balance: U256, is_call: bool) -> U256 {
        self.mark_touch();
        if is_call {
            self.info.nonce = self.info.nonce.saturating_add(1);
        }
        core::mem::replace(&mut self.info.balance, new_balance)
    }

pub fn state_clear_aware_is_empty(&self, spec: SpecId) -> bool {
    if SpecId::is_enabled_in(spec, SpecId::SPURIOUS_DRAGON) {
        self.is_empty()
    } else {
        self.is_loaded_as_not_existing_not_touched()
    }
}
pub fn is_cold_transaction_id(&self, transaction_id: usize) -> bool {
        self.transaction_id != transaction_id || self.status.contains(AccountStatus::Cold)
    }
#[inline]
pub fn mark_warm_with_transaction_id(&mut self, transaction_id: usize) -> bool {
    let is_cold = self.is_cold_transaction_id(transaction_id);
    self.status -= AccountStatus::Cold;
    self.transaction_id = transaction_id;
    is_cold
}

JournalEntry

之前说 Journal 会保存 EVM 运行过程中的临时状态变更列表.
这里的 JournalEntry 就是变更条目,每次的变更记录都用 JournalEntry 进行保存.

和前面的类型不同, JournalEntry 不是结构体 Struct , 是枚举 Enum

  • AccountWarmed 标记地址预热
    • Action: 将地址添加 state 中
    • Revert: 将地址从 state 中移除
  • AccountDestroyed 标记地址销毁并将 Journal 中的余额回滚
    • Action: 标记账户并转移余额
    • Revert: 取消标记并返还余额
  • AccountTouched 标记地址被Touched
    • Action: 标记地址为 Touched
    • Revert: 取消标记地址为 Touched
  • BalanceChange 地址Balance被修改
    • Action: 地址的Balance被修改
    • Rvert: 地址的Balance设置回保存的值
  • BalanceTransfer 转账操作
    • Action: 转账
    • Revert: 回滚双方余额
  • NonceChange Nonce 变化(主要用于调试)
    • Action: 修改地址 Nonce
    • Revert: 修改地址 Nonce 回保存的值
  • NonceBump Nonce递增1(正常EVM流程)
    • Action: 地址 Nonce 递增1
    • Revert 地址 Nonce 递减1
  • AccountCreated 账户创建
    • Action: 标记地址创建
    • Revert: 取消标记地址并将 Nonce 置零
  • StorageChanged Storage改变
    • Action: Storage改版
    • Revert: 回滚会之前的值
  • StorageWarmed Storage预热
    • Action: 地址Storage预热
    • Revert: 取消地址Storage预测
  • TransientStorageChange 瞬态Storage改变
    • Action: 瞬态Storage改变
    • Revert: 瞬态Storage回滚到之前的值
  • CodeChange Code改变
    • Action: 地址Code改变
    • Revert: 回滚到之前的Bytecode
pub enum JournalEntry {
    AccountWarmed {
        address: Address,
    },
    AccountDestroyed {
        had_balance: U256,
        address: Address,
        target: Address,
        destroyed_status: SelfdestructionRevertStatus,
    },
    AccountTouched {
        address: Address,
    },
    BalanceChange {
        old_balance: U256,
        address: Address,
    },
    BalanceTransfer {
        balance: U256,
        from: Address,
        to: Address,
    },
    NonceChange {
        address: Address,
        previous_nonce: u64,
    },
    NonceBump {
        address: Address,
    },
    AccountCreated {
        address: Address,
        is_created_globally: bool,
    },
    StorageChanged {
        key: StorageKey,
        had_value: StorageValue,
        address: Address,
    },
    StorageWarmed {
        key: StorageKey,
        address: Address,
    },
    TransientStorageChange {
        key: StorageKey,
        had_value: StorageValue,
        address: Address,
    },
    CodeChange {
        address: Address,
    },
}

看下 Revert 的部分.
针对每种改变日志进行处理并回滚之前保存的值或者状态.
都挺简单的,就是繁琐了点,就不解释了

fn revert(
    self,
    state: &mut EvmState,
    transient_storage: Option<&mut TransientStorage>,
    is_spurious_dragon_enabled: bool,
) {
    match self {
        JournalEntry::AccountWarmed { address } => {
            state.get_mut(&address).unwrap().mark_cold();
        }
        JournalEntry::AccountTouched { address } => {
            if is_spurious_dragon_enabled && address == PRECOMPILE3 {
                return;
            }
            // remove touched status
            state.get_mut(&address).unwrap().unmark_touch();
        }
        JournalEntry::AccountDestroyed {
            address,
            target,
            destroyed_status,
            had_balance,
        } => {
            let account = state.get_mut(&address).unwrap();
            // set previous state of selfdestructed flag, as there could be multiple
            // selfdestructs in one transaction.
            match destroyed_status {
                SelfdestructionRevertStatus::GloballySelfdestroyed => {
                    account.unmark_selfdestruct();
                    account.unmark_selfdestructed_locally();
                }
                SelfdestructionRevertStatus::LocallySelfdestroyed => {
                    account.unmark_selfdestructed_locally();
                }
                // do nothing on repeated selfdestruction
                SelfdestructionRevertStatus::RepeatedSelfdestruction => (),
            }

            account.info.balance += had_balance;

            if address != target {
                let target = state.get_mut(&target).unwrap();
                target.info.balance -= had_balance;
            }
        }
        JournalEntry::BalanceChange {
            address,
            old_balance,
        } => {
            let account = state.get_mut(&address).unwrap();
            account.info.balance = old_balance;
        }
        JournalEntry::BalanceTransfer { from, to, balance } => {
            // we don't need to check overflow and underflow when adding and subtracting the balance.
            let from = state.get_mut(&from).unwrap();
            from.info.balance += balance;
            let to = state.get_mut(&to).unwrap();
            to.info.balance -= balance;
        }
        JournalEntry::NonceChange {
            address,
            previous_nonce,
        } => {
            state.get_mut(&address).unwrap().info.nonce = previous_nonce;
        }
        JournalEntry::NonceBump { address } => {
            let nonce = &mut state.get_mut(&address).unwrap().info.nonce;
            *nonce = nonce.saturating_sub(1);
        }
        JournalEntry::AccountCreated {
            address,
            is_created_globally,
        } => {
            let account = &mut state.get_mut(&address).unwrap();
            account.unmark_created_locally();
            if is_created_globally {
                account.unmark_created();
            }
            // only account that have nonce == 0 can be created so it is safe to set it to 0.
            account.info.nonce = 0;
        }
        JournalEntry::StorageWarmed { address, key } => {
            state
                .get_mut(&address)
                .unwrap()
                .storage
                .get_mut(&key)
                .unwrap()
                .mark_cold();
        }
        JournalEntry::StorageChanged {
            address,
            key,
            had_value,
        } => {
            state
                .get_mut(&address)
                .unwrap()
                .storage
                .get_mut(&key)
                .unwrap()
                .present_value = had_value;
        }
        JournalEntry::TransientStorageChange {
            address,
            key,
            had_value,
        } => {
            let Some(transient_storage) = transient_storage else {
                return;
            };
            let tkey = (address, key);
            if had_value.is_zero() {
                // if previous value is zero, remove it
                transient_storage.remove(&tkey);
            } else {
                // if not zero, reinsert old value to transient storage.
                transient_storage.insert(tkey, had_value);
            }
        }
        JournalEntry::CodeChange { address } => {
            let acc = state.get_mut(&address).unwrap();
            acc.info.code_hash = KECCAK_EMPTY;
            acc.info.code = None;
        }
    }
}

Journal-2

继续回到 Journal 剩下的部分,只挑几个函数来讲讲.

检查点相关

checkpoint 创建检查点.

  • 创建逻辑很简单.新建 JournalCheckpoint 对象.
  • 在对象中设置当前 logs 和 journal 的长度并返回
    checkpoint_commit 提交检查点
  • 这里只是简单 depth - 1.因为这只代表当前 Frame 运行成功.
  • 如果它的上层 Frame Revert,回滚的时候还需要这些条目进行回滚.
    checkpoint_revert 回滚到指定检查点
  • checkpoint 日志开始的位置进行截断.
  • drain 截断checkpoint.journal_i之后的元素并返回.
  • rev 反转截断返回的元素列表
  • 调用前面说的 Revert 进行回滚操作.
pub fn checkpoint(&mut self) -> JournalCheckpoint {
    let checkpoint = JournalCheckpoint {
        log_i: self.logs.len(),
        journal_i: self.journal.len(),
    };
    self.depth += 1;
    checkpoint
}

/// Commits the checkpoint.
#[inline]
pub fn checkpoint_commit(&mut self) {
    self.depth = self.depth.saturating_sub(1);
}

/// Reverts all changes to state until given checkpoint.
#[inline]
pub fn checkpoint_revert(&mut self, checkpoint: JournalCheckpoint) {
    let is_spurious_dragon_enabled = self.spec.is_enabled_in(SPURIOUS_DRAGON);
    let state = &mut self.state;
    let transient_storage = &mut self.transient_storage;
    self.depth = self.depth.saturating_sub(1);
    self.logs.truncate(checkpoint.log_i);

    // iterate over last N journals sets and revert our global state
    if checkpoint.journal_i < self.journal.len() {
        self.journal
            .drain(checkpoint.journal_i..)
            .rev()
            .for_each(|entry| {
                entry.revert(state, Some(transient_storage), is_spurious_dragon_enabled);
            });
    }
}
加载账户
  • load_account_mut_optional 所有加载账户函数的核心
    • 从 state 中查找当前地址,判断是否存在
      • 存在
        • 判断当前 transaction_id 是否第一次加载此 Account
        • 如果是第一次则标志该 Account 对 当前 transaction_id 为 warm.
        • selfdestruct 相关处理
        • 插入 AccountWarmed 状态变更日志到 journal 中
      • 不存在
        • 从DB中进行加载并设置transaction_id 为 当前 transaction_id.
        • 插入 AccountWarmed 状态变更日志到 journal 中
        • 插入 state 中
  • load_account_delegated 加载 EIP-7702委托账户
    • 底层也是调用的load_account_mut_optional
    • 不同的是会判断是否 EIP-7702,是的话会加载Code
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();

            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)?;

                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));
            }
            (account, is_cold)
        }
        Entry::Vacant(vac) => {
            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)
            };

            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,
    ))
}
pub fn load_account_delegated<DB: Database>(
    &mut self,
    db: &mut DB,
    address: Address,
) -> Result<StateLoad<AccountLoad>, DB::Error> {
    let spec = self.spec;
    let is_eip7702_enabled = spec.is_enabled_in(SpecId::PRAGUE);
    let account = self
        .load_account_optional(db, address, is_eip7702_enabled, false)
        .map_err(JournalLoadError::unwrap_db_error)?;
    let is_empty = account.state_clear_aware_is_empty(spec);

    let mut account_load = StateLoad::new(
        AccountLoad {
            is_delegate_account_cold: None,
            is_empty,
        },
        account.is_cold,
    );

    if let Some(Bytecode::Eip7702(code)) = &account.info.code {
        let address = code.address();
        let delegate_account = self
            .load_account_optional(db, address, true, false)
            .map_err(JournalLoadError::unwrap_db_error)?;
        account_load.data.is_delegate_account_cold = Some(delegate_account.is_cold);
    }

    Ok(account_load)
}

剩下的函数逻辑都蛮简单的.感兴趣的可以自己去看一下.

  • touch() 将账户标记为已触碰
  • touch_account() 内部函数,标记账户已触碰并添加日志条目
  • account() 返回已加载账户的只读引用
  • set_code_with_hash() 为账户设置代码和哈希
  • set_code() 为账户设置代码(自动计算哈希)
  • caller_accounting_journal_entry() 为调用者添加余额/nonce变更的日志条目
  • balance_incr() 增加账户余额
  • nonce_bump_journal_entry() 添加 nonce 递增的日志条目
  • transfer_loaded() 在两个已加载账户之间转账
  • transfer() 加载账户并执行转账
  • create_account_checkpoint() 创建账户时的检查点处理(检测碰撞、转移余额等)
  • selfdestruct() 执行 SELFDESTRUCT 操作
  • load_account() 加载账户到内存,返回冷/热状态
  • load_code() 加载账户及其代码
  • load_account_optional() 可选加载账户(可跳过冷加载)
  • load_account_mut() 加载账户并返回可变引用
  • load_account_mut_optional_code() 可选加载账户代码的可变引用版本
  • get_account_mut() 获取账户可变引用(不标记冷/热状态)
  • sload() 加载存储槽值
  • sload_assume_account_present() 加载存储槽值(假设账户已存在)
  • sstore() 写入存储槽值
  • sstore_assume_account_present() 写入存储槽值(假设账户已存在)
  • tload() 读取瞬态存储值
  • tstore() 写入瞬态存储值
  • log() 将日志推入日志列表
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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