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 就是起一个管理作用,配置和操作都是由他的成员来提供.
all 和 all_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 gas和blob 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_typetx类型LegacyEip2930Eip1559Eip4844Eip7702Custom
caller本次tx的发起者、tx的签名人gas_limittx的gas最大使用限制gas_pricegas的每单位价格.EIP-1559之后表示max_gas_fee.kind是 Create 交易(部署合约)还是 Call (调用)交易,如果是 Call 交易, 会包含 tovalue本次交易发送的valuedata本次交易发送到to的数据nonceEOA账户的Nonce,等于发送上链的交易数chain_id链ID,用于防止重放攻击access_listaccess列表,用于提前预热地址列表,后面访问可以减少Gas消耗gas_priority_feeGas优先费用,用于贿赂矿工提前打包txblob_hashesblob的版本号哈希列表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-2930 和 EIP-1559 的判断会让人有些迷惑.不应该是先 EIP-1159 再到 EIP-2930吗
但现实是 EIP-2930 比 EIP-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_idspec当前硬分叉版本limit_contract_code_size合约代码大小限制limit_contract_initcode_size合约初始化代码大小限制disable_nonce_check禁用 tx 中的 nonce 检查.max_blobs_per_txtx中blobs最大限制blob_base_fee_update_fractionBlob 基本费用更新分数tx_gas_limit_cap限制单笔交易的Gas Limitmemory_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禁用BaseFeedisable_priority_fee_check禁用PriorityFeedisable_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余额noncecode_hash如果是EOA账户,是个固定值.如果是合约账户,是代码的哈希account_idREVM 内部实现,不是 ETH 协议层面的东西.code账户字节码(为 None 才用 code_hash 去获取)
code_by_hash根据 code_hash 获取对应的合约字节码storage获取地址在 index 的值storage_by_account_id通过 account_id 获取storage,account_id为空则走storage函数block_hash获取指定block的hash
// 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);
}
}
DatabaseRef 和 Database 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 相当于 REVM 和 ETH 之间的缓存层,缓存从外部 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加1logs清空
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 的内容基本都是跟 Account 和 ENTRY 相关.
后面的内容都是对它们的封装,直接讲只知道怎么用而不知道为什么这样写.
上面讲到的 state 的类型是 EvmState. pub type EvmState = HashMap<Address, Account>.
这就是为什么要讲 Account 的原因.
先看下结构体
info账户的所有信息,具体内容在上一章的 Database 有介绍过original_info账户在交易开始时,首次加载后的初始值.transaction_idTx在Block中的Indexstorage地址对应的存储,合约的属性字段就是保存在storage中.value的类型是EvmStorageSlot,这是它的字段original_valuestorage的原始值present_valuestorage的现值transaction_idtx在block中的indexis_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_modification为caller设置新的金额并标记为Touched -
state_clear_aware_is_empty判断一个账户是否为空.- 如果一个账户的 Nonce 为0、Balance 为0、CodeHash也为0,这就是空账户
- 在EIP-161(SpecId::SPURIOUS_DRAGON硬分叉)之后,是要被从数据库中删除的.
- 删除是因为有些垃圾地址会大量占用外部DB存储空间(例如大量被转了0 ETH的空地址)
-
changed_storage_slots 判断
storage是否被修改过- 通过对比所有
storage中original_value和present_value是否相等. - 这里是用于保存到数据库中,所以并不关心中间是否修改过几次.
- 通过对比所有
-
is_cold_transaction_id判断- 虽然EVM一次只执行一次交易.但是
Journal实例是会被多个交易共用的. - 同一个地址会被不同交易访问.第一次访问后会一直保存在 Journal 中
- 但对于每笔交易,如果这个地址你是第一次访问.这个地址对于你还是
Cold
- 虽然EVM一次只执行一次交易.但是
-
mark_warm_with_transaction_id 标志该地址对于该交易是
Warm -
剩下的都是
Setter和Getter,就不细讲了.
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: 回滚双方余额
NonceChangeNonce 变化(主要用于调试)- Action: 修改地址 Nonce
- Revert: 修改地址 Nonce 回保存的值
NonceBumpNonce递增1(正常EVM流程)- Action: 地址 Nonce 递增1
- Revert 地址 Nonce 递减1
AccountCreated账户创建- Action: 标记地址创建
- Revert: 取消标记地址并将 Nonce 置零
StorageChangedStorage改变- Action: Storage改版
- Revert: 回滚会之前的值
StorageWarmedStorage预热- Action: 地址Storage预热
- Revert: 取消地址Storage预测
TransientStorageChange瞬态Storage改变- Action: 瞬态Storage改变
- Revert: 瞬态Storage回滚到之前的值
CodeChangeCode改变- 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 中
- 存在
- 从 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()将日志推入日志列表