**REVM源码阅读-流程(1)**

REVM源码阅读-流程(1)

前言

炒币炒了几年,亏亏赚赚,越炒越累,又不想离开币圈. 想来想去决定去看下 RETH

REVM源码阅读-流程(1)

前言

炒币炒了几年,亏亏赚赚,越炒越累,又不想离开币圈. 想来想去决定去看下 RETH 的源码. REVM 作为 RETH 的最重要的模块,搞懂它就算完成一大任务. 之前没有学过 RUST ,看过教程,但没有在实际项目中写过.决定配合着 AI 进行答疑来看. REVM官方库

目录结构

主要关注3个目录:

  • book

  • revm的文档

  • crates

  • 实际源码所在

  • examples

  • 例子

开始

想学一个项目,最好的方法就是从例子开始. 这里我们选择从 examples/erc20_gas 这个项目入手. 打开项目中的src.

// examples/erc20_gas/src/main.rs
let rpc_url = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
let provider = ProviderBuilder::new().connect(rpc_url).await?.erased();
let alloy_db = WrapDatabaseAsync::new(AlloyDB::new(provider, BlockId::latest())).unwrap();
let mut cache_db = CacheDB::new(alloy_db);

前两句比较好理解,就是创建一个provider用来获取链上数据.

后面两句涉及到3个新的概念:

  • AlloyDB : 让 evm 能按需通过 RPC 直接获取链上数据

  • WrapDatabaseAsync : 将AlloyDB从 Async 转成 Sync .

  • CacheDB: 作为中间层,缓存读取的数据,并暂存写入的修改

也就是创建了CacheDB,用于缓存链上读取到的数据,当 EVM 需要的时候,直接从CacheDB中读取,而不是再次从链上获取. 这三个类主要用于Fork主网进行交易模拟和测试,所以我们不深入它的具体实现.

上面的代码往下都是一些配置,所以我们直接跳到 transfer 函数的实现.

fn transfer(from: Address, to: Address, amount: U256, cache_db: &mut AlloyCacheDB) -> Result<()> {
    let mut ctx = Context::mainnet()
        .with_db(cache_db)
        .modify_cfg_chained(|cfg| {
            cfg.spec = SpecId::CANCUN;
        })
        .with_tx(
            TxEnv::builder()
                .caller(from)
                .kind(TxKind::Call(to))
                .value(amount)
                .gas_price(2)
                .build()
                .unwrap(),
        )
        .modify_block_chained(|b| {
            b.basefee = 1;
        })
        .build_mainnet();
    transact_erc20evm_commit(&mut ctx).unwrap();
    Ok(())
}

Context::mainnet(), 创建一个主网的上下文(Context). Context 是一个很重要的概念,会贯穿整个 evm. 了解 Context 的功能会让后面的源码阅读起来轻松一些. 跳转到 Context::mainnet 的实现.

//crates/handler/src/mainnet_builder.rs
impl MainContext for Context<BlockEnv, TxEnv, CfgEnv, EmptyDB, Journal<EmptyDB>, ()> {
    fn mainnet() -> Self {
        Context::new(EmptyDB::new(), SpecId::default())
    }
}

没啥有用的,就是使用了一个MainContext Trait

继续往下跳

// crates/context/src/context.rs
impl<
        BLOCK: Block + Default,
        TX: Transaction + Default,
        DB: Database,
        JOURNAL: JournalTr<Database = DB>,
        CHAIN: Default,
        LOCAL: LocalContextTr + Default,
        SPEC: Default + Copy + Into<SpecId>,
    > Context<BLOCK, TX, CfgEnv<SPEC>, DB, JOURNAL, CHAIN, LOCAL>
{
    /// Creates a new context with a new database type.
    ///
    /// This will create a new [`Journal`] object.
    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(()),
        }
    }
}

新建了一个Context并进行配置返回.Context包含了以下内容

  • tx 创建一个新的Transaction(每个Transaction都会新建一个evm,所以一个evm只会有一个tx)

  • block 区块

  • cfg

  • chain_id 链id

  • tx_chain_id_check 是否检查chain_id(发送Transction的时候会携带chain_id,这里就是检查Transaction带的chain_id是否和当前evm的chain_id相同)

  • spec 当前EVM版本

  • limit_contract_code_size 合约代码大小限制

  • limit_contract_initcode_size 合约初始化代码大小限制

  • disable_nonce_check 是否检查nonce(每个transaction会携带sender的nonce,等于发送过的transaction数量)

  • max_blobs_per_tx L2用的

  • blob_base_fee_update_fraction L2用的

  • tx_gas_limit_cap Transacton的Gas上限

  • memory_limit EVM使用的最大内存限制

  • disable_balance_check

  • disable_block_gas_limit

  • disable_eip3541

  • disable_eip3607

  • disable_eip7623

  • disable_base_fee

  • disable_priority_fee_check

  • disable_fee_charge

  • local localContext, EVM 执行过程中由解释器/执行逻辑逐步填充的本地临时上下文

  • journaled_state Journaled 状态(跟踪账户修改、日志、revert 等)

  • chain 占位符,暂时没啥用

从字段我们可以看出, Context 包含了 EVM 执行交易时所需要的数据和状态.

看完了Context,我们再次回到main.rs

//***examples/erc20_gas/src/main.rs***
 .with_db(cache_db)
        .modify_cfg_chained(|cfg| {
            cfg.spec = SpecId::CANCUN;
        })
        .with_tx(
            TxEnv::builder()
                .caller(from)
                .kind(TxKind::Call(to))
                .value(amount)
                .gas_price(2)
                .build()
                .unwrap(),
        )
        .modify_block_chained(|b| {
            b.basefee = 1;
        })
        .build_mainnet();

前面没啥好说的,就是设置链版本、当前tx、block的basefee. 我们直接看 build_mainnet ,跳转进去

//crates/handler/src/mainnet_builder.rs
fn build_mainnet(self) -> MainnetEvm<Self::Context> {
        Evm {
            ctx: self,
            inspector: (),
            instruction: EthInstructions::default(),
            precompiles: EthPrecompiles::default(),
            frame_stack: FrameStack::new_prealloc(8),
        }
    }

就是直接创建了一个 Evm 对象.这里对其他几个字段进行解释下. inspector: 可以理解为监视器(钩子),主要用在调试中.可以在 opcode 执行前后 hook ,查看此时内存、栈、gas消耗. precompiles: 预编译合约集合(sha256,ecrecover等) instruction: 指令表.保存着每个 opcode 消耗的 gas,每个opcode 对应的函数 frame_stack: 调用栈,跨合约调用就会创建一个frame.用于合约间的调用.

这里面涉及到了很多新的概念,这些概念是 EVM 的重要组成部分,记住他们的作用,后续才能更容易搞懂 EVM 的流程.

继续回到 examples/erc20_gas/src/main.rs 中的 Transfer 函数

transact_erc20evm_commit(&mut ctx).unwrap();

跳转进去,就一个函数调用,关键在于 transact_erc20evm

pub fn transact_erc20evm_commit<EVM>(
    evm: &mut EVM,
) -> Result<ExecutionResult<HaltReason>, Erc20Error<EVM::Context>>
where
    EVM: EvmTr<
        Context: ContextTr<Journal: JournalTr<State = EvmState>, Db: DatabaseCommit>,
        Precompiles: PrecompileProvider<EVM::Context, Output = InterpreterResult>,
        Instructions: InstructionProvider<
            Context = EVM::Context,
            InterpreterTypes = EthInterpreter,
        >,
        Frame = EthFrame<EthInterpreter>,
    >,
{
    transact_erc20evm(evm).map(|(result, state)| {
        evm.ctx().db_mut().commit(state);
        result
    })
}

我们继续跳转 transact_erc20evm:

pub fn transact_erc20evm<EVM>(
    evm: &mut EVM,
) -> Result<(ExecutionResult<HaltReason>, EvmState), Erc20Error<EVM::Context>>
where
    EVM: EvmTr<
        Context: ContextTr<Journal: JournalTr<State = EvmState>>,
        Precompiles: PrecompileProvider<EVM::Context, Output = InterpreterResult>,
        Instructions: InstructionProvider<
            Context = EVM::Context,
            InterpreterTypes = EthInterpreter,
        >,
        Frame = EthFrame<EthInterpreter>,
    >,
{
    Erc20MainnetHandler::new().run(evm).map(|r| {
        let state = evm.ctx().journal_mut().finalize();
        (r, state)
    })
}

暂时只需要关注 Erc20MainnetHandler::new().run(evm) 前面就是创建一个 Erc20MainnetHandler , 但这其实不是我们今天的重点. 虽然从 erc20_gas 入手,但只是为了找一个入口点.虽然我们直接跳到 run

// crates/handler/src/handler.rs
 #[inline]
    fn run(
        &mut self,
        evm: &mut Self::Evm,
    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
        self.configure(evm);
        // Run inner handler and catch all errors to handle cleanup.
        match self.run_without_catch_error(evm) {
            Ok(output) => Ok(output),
            Err(e) => self.catch_error(evm, e),
        }
    }

又是只有一个函数调用,继续往下:

// crates/handler/src/handler.rs
#[inline]
    fn run_without_catch_error(
        &mut self,
        evm: &mut Self::Evm,
    ) -> Result<ExecutionResult<Self::HaltReason>, Self::Error> {
        let init_and_floor_gas = self.validate(evm)?;
        let eip7702_refund = self.pre_execution(evm)? as i64;
        let mut exec_result = self.execution(evm, &init_and_floor_gas)?;
        self.post_execution(evm, &mut exec_result, init_and_floor_gas, eip7702_refund)?;
        // Prepare the output
        self.execution_result(evm, exec_result)
    }

然后就来到了今天的重点 run_without_catch_error run_without_catch_errorhandler 中重要的函数,用于执行evm的完整流程.

Handler

就像文章最开始, main.rs 是项目的入口. Handler 就是整个 EVM 的入口. 核心作用: 定义和组织 EVM 执行流程的各个阶段(如交易验证、Gas 消耗、指令执行、Log 和 Selfdestruct 处理等),并允许用户在这些阶段中插入自定义逻辑(Hooks)。

run_without_catch_error 就是 REVM 五大阶段执行的地方.

  • Validation: 确保交易符合协议规则,并在执行前锁住费用。

  • pre_execution: 为特定协议(如 EIP-7702)或自定义逻辑做准备。

  • execution: 执行合约逻辑,完成所有的状态修改和日志记录。

  • post_execution: 基于执行结果,完成最终的 Gas 结算和所有的状态清理工作。

  • execution_result: 提供一个标准化的、包含所有最终数据的交易执行结果。

讲得比较粗略,因为概念性的东西讲了也记不住.会在后面专门的 handler 文章中再详细进行讲解. 只要记住主要有五大步骤.

点赞 0
收藏 0
分享

0 条评论

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