REVM源码阅读-流程(1)
前言
炒币炒了几年,亏亏赚赚,越炒越累,又不想离开币圈. 想来想去决定去看下 RETH
炒币炒了几年,亏亏赚赚,越炒越累,又不想离开币圈. 想来想去决定去看下 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_error 是 handler 中重要的函数,用于执行evm的完整流程.
就像文章最开始, 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 文章中再详细进行讲解. 只要记住主要有五大步骤.
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!