前言前一系列的文章是为了快速走个流程,让大家对REVM有个概念.让源码阅读起来没那么恐惧.忽略了很多细节和概念.在后面的文章中会尽量讲详细点.对Rust的语法能解释的我也会尽量解释.Rust的泛型看着真是头痛,对于刚入门的我来说真是天书.还得查资料理解后才能写出来.当然,我会尽
前一系列的文章是为了快速走个流程,让大家对 REVM 有个概念.\
让源码阅读起来没那么恐惧.忽略了很多细节和概念.\
在后面的文章中会尽量讲详细点.
对 Rust 的语法能解释的我也会尽量解释.\
Rust 的泛型看着真是头痛,对于刚入门的我来说真是天书.\
还得查资料理解后才能写出来.当然,我会尽量保证正确.
Frame 里面包含了多个其他 EVM 的类.我们顺便在这章把它们讲了
Frame 的源码在前面 流程(4) 中已经介绍了很大一部分.\ 在流程中我们讲解了 make_call_frame 和 make_create_frame 的执行过程.\ 两个函数都是用来创建执行合约的 Frame .
在REVM中, 每一次的合约调用都会创建一个 Frame.\
如果只是 EOA 账户之间的转账,不会创建 Frame.\
合约调用合约(CALL、STATICCALL、DELEGATECALL) 就会生成一个新的 Frame
Frame 之间的执行环境都是隔离的,都拥有自己独立的:
Memory(共享一个大内存块,但是每个都有自己的范围.)Journaled State 变更记录我们先来看下 Frame 结构体的定义:
#[derive_where(Clone, Debug; IW,
<IW as InterpreterTypes>::Stack,
<IW as InterpreterTypes>::Memory,
<IW as InterpreterTypes>::Bytecode,
<IW as InterpreterTypes>::ReturnData,
<IW as InterpreterTypes>::Input,
<IW as InterpreterTypes>::RuntimeFlag,
<IW as InterpreterTypes>::Extend,
)]
pub struct EthFrame<IW: InterpreterTypes = EthInterpreter> {
pub data: FrameData,
pub input: FrameInput,
pub depth: usize,
pub checkpoint: JournalCheckpoint,
pub interpreter: Interpreter<IW>,
pub is_finished: bool,
}
讲一下各个字段的定义:
data
Call
return_memory_range
Create
created_address
input FrameInput在后面有详细的解释
depth 当前 Frame 的深度,每次生成frame都会压入栈中,你可以理解为在栈中的index.
checkpoint 检查点. revert 时用来回滚状态.
interpreter 解释器,用来执行字节码
is_finished 当前 Frame 是否已经执行完成并返回结果.
上面的 derive_where 是一个第三方库的宏.表示我们要为结构体 EthFrame 派生 Clone 和 Debug .\ 它不是直接作用于 struct , 而是直接作用于 impl. 展开后变成这样.
impl<IW: InterpreterTypes + Clone + Debug> Clone for EthFrame<IW>
where
<IW as InterpreterTypes>::Stack: Clone,
<IW as InterpreterTypes>::Memory: Clone,
<IW as InterpreterTypes>::Bytecode: Clone,
<IW as InterpreterTypes>::ReturnData: Clone,
<IW as InterpreterTypes>::Input: Clone,
<IW as InterpreterTypes>::RuntimeFlag: Clone,
<IW as InterpreterTypes>::Extend: Clone,
{
fn clone(&self) -> Self {
}
}
impl<IW: InterpreterTypes + Debug> Debug for EthFrame<IW>
where
<IW as InterpreterTypes>::Stack: Debug,
<IW as InterpreterTypes>::Memory: Debug,
<IW as InterpreterTypes>::Bytecode: Debug,
<IW as InterpreterTypes>::ReturnData: Debug,
<IW as InterpreterTypes>::Input: Debug,
<IW as InterpreterTypes>::RuntimeFlag: Debug,
<IW as InterpreterTypes>::Extend: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
}
}
impl<IT: InterpreterTypes> FrameTr for EthFrame<IT> {
type FrameResult = FrameResult;
type FrameInit = FrameInit;
}
impl Default for EthFrame<EthInterpreter> {
fn default() -> Self {
Self::do_default(Interpreter::default())
}
}
impl EthFrame<EthInterpreter> {
pub fn invalid() -> Self {
Self::do_default(Interpreter::invalid())
}
fn do_default(interpreter: Interpreter<EthInterpreter>) -> Self {
Self {
data: FrameData::Call(CallFrame {
return_memory_range: 0..0,
}),
input: FrameInput::Empty,
depth: 0,
checkpoint: JournalCheckpoint::default(),
interpreter,
is_finished: false,
}
}
pub fn is_finished(&self) -> bool {
self.is_finished
}
pub fn set_finished(&mut self, finished: bool) {
self.is_finished = finished;
}
}
这一段看起来比较绕的只有第一段.表达式左右两边都是相同的.\ 分别点进去定义跳转还是的不同地方.
先看 FrameTr 中 FrameResult 的定义.\
这里的 type FrameResult: From<FrameResult>,可以分成两部分来理解.\
type FrameResult 和 FrameResult: From<FrameResult>.
如果只有 type FrameResult 表示不指定类型, impl 实现时再来指定具体类型.
FrameResult: From<FrameResult> 是 Trait 限定,限定 FrameResult 只能由 FrameResult 类型转换而来.注意这里是冒号.
#[auto_impl(&mut, Box)] 是一个第三方宏.\
auto_impl 宏会自动为 &mut 和 Box 指针类型实现这个 trait。这意味着,如果你有一个类型 T 实现了 FrameTr,那么 &mut T 和 Box<T> 也会自动实现 FrameTr.
在看回 type FrameResult = FrameResult, 这里其实也就是让左边 关联类型 FrameResult 等于右边 具体类型 FrameResult. 注意这里是等号.\
这里看着绕是因为它让两个名字相等了.
impl<IT: InterpreterTypes> FrameTr for EthFrame<IT> {
type FrameResult = FrameResult;
type FrameInit = FrameInit;
}
// crates/handler/src/evm.rs
// 等式左边
#[auto_impl(&mut, Box)]
pub trait FrameTr {
/// The result type returned when a frame completes execution.
type FrameResult: From<FrameResult>;
/// The initialization type used to create a new frame.
type FrameInit: From<FrameInit>;
}
// crates/handler/src/frame_data.rs
// 等式右边
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone)]
pub enum FrameResult {
/// Call frame result.
Call(CallOutcome),
/// Create frame result.
Create(CreateOutcome),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FrameInit {
/// depth of the next frame
pub depth: usize,
/// shared memory set to this shared context
pub memory: SharedMemory,
/// Data needed as input for Interpreter.
pub frame_input: FrameInput,
}
继续看下看.大部分的内容在之前的 Execute 流程中都介绍过了.
pub type ContextTrDbError<CTX> = <<CTX as ContextTr>::Db as Database>::Error;
impl EthFrame<EthInterpreter> {
/// Clear and initialize a frame.
#[allow(clippy::too_many_arguments)]
#[inline(always)]
pub fn clear(
&mut self,
data: FrameData,
input: FrameInput,
depth: usize,
memory: SharedMemory,
bytecode: ExtBytecode,
inputs: InputsImpl,
is_static: bool,
spec_id: SpecId,
gas_limit: u64,
checkpoint: JournalCheckpoint,
gas_params: GasParams,
) {
let Self {
data: data_ref,
input: input_ref,
depth: depth_ref,
interpreter,
checkpoint: checkpoint_ref,
is_finished: is_finished_ref,
} = self;
*data_ref = data;
*input_ref = input;
*depth_ref = depth;
*is_finished_ref = false;
interpreter.clear(
memory, bytecode, inputs, is_static, spec_id, gas_limit, gas_params,
);
*checkpoint_ref = checkpoint;
}
#[inline]
pub fn make_call_frame<
CTX: ContextTr,
PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
ERROR: From<ContextTrDbError<CTX>> + FromStringError,
>(
mut this: OutFrame<'_, Self>,
ctx: &mut CTX,
precompiles: &mut PRECOMPILES,
depth: usize,
memory: SharedMemory,
inputs: Box<CallInputs>,
gas_params: GasParams,
) -> Result<ItemOrResult<FrameToken, FrameResult>, ERROR> {}
#[inline]
pub fn make_create_frame<
CTX: ContextTr,
ERROR: From<ContextTrDbError<CTX>> + FromStringError,
>(
mut this: OutFrame<'_, Self>,
context: &mut CTX,
depth: usize,
memory: SharedMemory,
inputs: Box<CreateInputs>,
gas_params: GasParams,
) -> Result<ItemOrResult<FrameToken, FrameResult>, ERROR> {}
pub fn init_with_context<
CTX: ContextTr,
PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
>(
this: OutFrame<'_, Self>,
ctx: &mut CTX,
precompiles: &mut PRECOMPILES,
frame_init: FrameInit,
gas_params: GasParams,
) -> Result<
ItemOrResult<FrameToken, FrameResult>,
ContextError<<<CTX as ContextTr>::Db as Database>::Error>,
> {}
}
impl EthFrame<EthInterpreter> {
/// Processes the next interpreter action, either creating a new frame or returning a result.
pub fn process_next_action<
CTX: ContextTr,
ERROR: From<ContextTrDbError<CTX>> + FromStringError,
>(
&mut self,
context: &mut CTX,
next_action: InterpreterAction,
) -> Result<FrameInitOrResult<Self>, ERROR> {}
pub fn return_result<CTX: ContextTr, ERROR: From<ContextTrDbError<CTX>> + FromStringError>(
&mut self,
ctx: &mut CTX,
result: FrameResult,
) -> Result<(), ERROR> {}
}
pub fn return_create<JOURNAL: JournalTr>(
journal: &mut JOURNAL,
checkpoint: JournalCheckpoint,
interpreter_result: &mut InterpreterResult,
address: Address,
max_code_size: usize,
is_eip3541_disabled: bool,
spec_id: SpecId,
) {}
既然大部分的内容都在前面讲过了,为什么还要单独为 Frame 写一章?\
因为很多细节的东西还没讲,只是过个流程,对整个 EVM 执行的理解还是东一块西一块.\
后面的部分我们重点讲一下之前没涉及到的细节部分.
init_with_context 可以说是 Frame 的入口.\
我们就从 init_with_context 再讲一遍.
// 使用给定的context上下文和预编译合约来初始化一个Frame
pub fn init_with_context<
CTX: ContextTr,
PRECOMPILES: PrecompileProvider<CTX, Output = InterpreterResult>,
>(
this: OutFrame<'_, Self>,
ctx: &mut CTX,
precompiles: &mut PRECOMPILES,
frame_init: FrameInit,
gas_params: GasParams,
) -> Result<
ItemOrResult<FrameToken, FrameResult>,
ContextError<<<CTX as ContextTr>::Db as Database>::Error>,
> {}
总共有5个参数:
this: OutFrame<'_, Self> Frame创建过程中的临时类型.ctx: &mutCTX 限定实现了 ContextTr Trait的context上下文.precompiles: &mut PRECOMPILES 预编译合约集合frame_init: FrameInit Frame初始化的参数gas_params: GasParams 保存的是opcode的基础gas消耗上面5个参数中,其他的都比较容易理解.就OutFrame 一下看不出是做啥的.\ 跳转到定义:
#[allow(missing_debug_implementations)], 定义 pub 类型时,忽略没有实现 Debug Trait的警告
总共3个参数.
*mut T 存放Frame数据的内存地址*mut T 而不是 &mut T,这是裸指针而不是引用.裸指针没有生命周期和所有权.这样写能让裸指针更安全.// crates/context/interface/src/local.rs
// 一个可能已初始化的帧。用于在主循环中初始化新帧时。
#[allow(missing_debug_implementations)]
pub struct OutFrame<'a, T> {
ptr: *mut T,
init: bool,
lt: core::marker::PhantomData<&'a mut T>,
}
impl<'a, T> OutFrame<'a, T> {
pub fn new_init(slot: &'a mut T) -> Self {
unsafe { Self::new_maybe_uninit(slot, true) }
}
pub fn new_uninit(slot: &'a mut core::mem::MaybeUninit<T>) -> Self {
unsafe { Self::new_maybe_uninit(slot.as_mut_ptr(), false) }
}
pub unsafe fn new_maybe_uninit(ptr: *mut T, init: bool) -> Self {
Self {
ptr,
init,
lt: Default::default(),
}
}
pub unsafe fn get_unchecked(&mut self) -> &mut T {
debug_assert!(self.init, "OutFrame must be initialized before use");
unsafe { &mut *self.ptr }
}
pub fn consume(self) -> FrameToken {
FrameToken(self.init)
}
}
我们找一下 init_with_context 调用的位置, 并跳到 OutFrame 的生成.\
关键在 unsafe { OutFrame::new_maybe_uninit(self.stack.as_mut_ptr().add(idx), idx < self.stack.len())\
获取 FrameStack 的起始指针 + idx 个位置,判断 idx 是否小于 self.stack.len().\
如果是第一次调用, FrameStack 的len肯定为0,所以 这里的idx < self.stack.len()必定为false\
调用 new_maybe_uninit ,new_maybe_uninit 的定义在上面.\
只是新建了一个 OutFrame 并返回.
// crates/handler/src/evm.rs
let new_frame = if is_first_init {
self.frame_stack.start_init()
} else {
self.frame_stack.get_next()
};
// crates/context/interface/src/local.rs
#[inline]
pub fn start_init(&mut self) -> OutFrame<'_, T> {
self.index = None;
if self.stack.is_empty() {
self.stack.reserve(8);
}
self.out_frame_at(0)
}
fn out_frame_at(&mut self, idx: usize) -> OutFrame<'_, T> {
unsafe {
OutFrame::new_maybe_uninit(self.stack.as_mut_ptr().add(idx), idx < self.stack.len())
}
}
回到 init_with_context ,可以看到只是将 OutFrame 类型的 this 传进去给两个 CreateFrame 函数\ 点进去 make_call_frame 并找到 this的使用位置.\ 终于找到了它的使用位置. this.get 、this.consume
this.get(EthFrame::invalid).clear(
FrameData::Call(CallFrame {
return_memory_range: inputs.return_memory_offset.clone(),
}),
FrameInput::Call(inputs),
depth,
memory,
ExtBytecode::new_with_hash(bytecode, bytecode_hash),
interpreter_input,
is_static,
ctx.cfg().spec().into(),
gas_limit,
checkpoint,
gas_params,
);
Ok(ItemOrResult::Item(this.consume()))
点进去看下 this.get 和 this.consume 的实现.\
很明了了,就是调用传入的函数 EthFrame::invalid ,生成一个新的非法Frame.\
并返回生成的 Frame 的可变引用.\
为什么这里有 unsafe ,因为涉及到了裸指针的解引用 *`self.ptr`**
// crates/context/interface/src/local.rs
pub fn get(&mut self, f: impl FnOnce() -> T) -> &mut T {
if !self.init {
self.do_init(f);
}
unsafe { &mut *self.ptr }
}
#[inline(never)]
#[cold]
fn do_init(&mut self, f: impl FnOnce() -> T) {
unsafe {
self.init = true;
self.ptr.write(f());
}
}
pub fn consume(self) -> FrameToken {
FrameToken(self.init)
}
pub struct FrameToken(bool);
impl FrameToken {
/// Asserts that the frame token is initialized.
#[cfg_attr(debug_assertions, track_caller)]
pub fn assert(self) {
assert!(self.0, "FrameToken must be initialized before use");
}
}
我们重新整理下流程.
crates/handler/src/evm.rs 中的 Evm 有一个字段 frame_stack, 它保存所有生成的Frameframe_init 中,会调用 frame_stack.start_init 或者 self.frame_stack.get_next() 生成一个OutFrame类型的newFrame.里面裸指针保存了 frame 在 fram_stack 中的位置.init_with_context 中传入 newFrame , 最终在 MakeXXFrame 的中传递Frame的创建函数到 OutFrame 中的get,并创建初始化Frame.绕一大圈就是为了创建一个 Frame.\
那为什么不直接创建 Frame ,而是要通过一个中间的 OutFrame.\
想了好久,问了好多遍多个AI这块的细节.也没得到满意的答案.
一开始以为是Rust 所有权的问题.\
直接 Frame::new ,需要将 self 的一些成员字段传进去.后面self.frame_stack.push(new_frame) 就会涉及到所有权的问题.\
结果回到上面看Frame的结构,并没有涉及到任何引用借用.
那应该就是性能问题.\
REVM 在执行过程中会频繁创建 Frame.如果每次都直接 New 一个 Frame,在结束的时候又销毁.反复申请和释放内存会带来消耗.\
这样 FrameStack 就可以当成一个对象池.
另一个原因是延迟创建.在 MakeXXFrame 中,会有很多判断失败的情况.\
如果判断失败的话没必要创建Frame.避免创建没必要的Frame
之前说过 precompiles 是预编译合约集合,但是还没深入进去讲下.\
这里的 PRECOMPILES 是泛型了,接受实现了 PrecompileProvider<CTX, Output = InterpreterResult> Trait的类型.
进去看下 PrecompileProvider<CTX: ContextTr> 的定义.
// crates/handler/src/precompile_provider.rs
#[auto_impl(&mut, Box)]
pub trait PrecompileProvider<CTX: ContextTr> {
type Output;
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool;
fn run(
&mut self,
context: &mut CTX,
inputs: &CallInputs,
) -> Result<Option<Self::Output>, String>;
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>>;
fn contains(&self, address: &Address) -> bool;
}
set_spec 设置 spec_id,如果跟之前不同则返回 true.run 运行指定的precompilewarm_addresses 获取预热地址列表,这里是获取预编译合约contains 判断地址是否预编译合约 precompile功能挺明显了,就是获取根据 EVM 版本获取所有的预编译合约保存.再提供接口可以运行指定预编译合约、获取合约列表、判断是否预编译合约.
在 crates/handler/src/precompile_provider.rs 这个文件中只有一个 EthPrecompiles 实现了 Trait.\
我们就直接看下 EthPrecompiles 的内容.
好多内容都比较简单,我们挑着讲.\
&'static Precompiles , 这里的 &'static 是生命周期,表示 Precompiles 在整个程序运行期间都有效.
pub fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> 返回一个实现了迭代器 Iterator Trait的类型,该迭代器产生 Address 类型的元素.
Precompiles::new 内容挺长的,就不贴了.\ 逻辑很简单,就是根据 spec_id , 返回所有的预编译合约
pub struct EthPrecompiles {
pub precompiles: &'static Precompiles,
pub spec: SpecId,
}
impl EthPrecompiles {
pub fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
Box::new(self.precompiles.addresses().cloned())
}
pub fn contains(&self, address: &Address) -> bool {
self.precompiles.contains(address)
}
}
impl Clone for EthPrecompiles {
fn clone(&self) -> Self {
Self {
precompiles: self.precompiles,
spec: self.spec,
}
}
}
impl Default for EthPrecompiles {
fn default() -> Self {
let spec = SpecId::default();
Self {
precompiles: Precompiles::new(PrecompileSpecId::from_spec_id(spec)),
spec,
}
}
}
impl<CTX: ContextTr> PrecompileProvider<CTX> for EthPrecompiles {
type Output = InterpreterResult;
fn set_spec(&mut self, spec: <CTX::Cfg as Cfg>::Spec) -> bool {
let spec = spec.into();
if spec == self.spec {
return false;
}
self.precompiles = Precompiles::new(PrecompileSpecId::from_spec_id(spec));
self.spec = spec;
true
}
fn run(
&mut self,
context: &mut CTX,
inputs: &CallInputs,
) -> Result<Option<InterpreterResult>, String> {
let Some(precompile) = self.precompiles.get(&inputs.bytecode_address) else {
return Ok(None);
};
let mut result = InterpreterResult {
result: InstructionResult::Return,
gas: Gas::new(inputs.gas_limit),
output: Bytes::new(),
};
let exec_result = {
let r;
let input_bytes = match &inputs.input {
CallInput::SharedBuffer(range) => {
if let Some(slice) = context.local().shared_memory_buffer_slice(range.clone()) {
r = slice;
r.as_ref()
} else {
&[]
}
}
CallInput::Bytes(bytes) => bytes.0.iter().as_slice(),
};
precompile.execute(input_bytes, inputs.gas_limit)
};
match exec_result {
Ok(output) => {
result.gas.record_refund(output.gas_refunded);
let underflow = result.gas.record_cost(output.gas_used);
assert!(underflow, "Gas underflow is not possible");
result.result = if output.reverted {
InstructionResult::Revert
} else {
InstructionResult::Return
};
result.output = output.bytes;
}
Err(PrecompileError::Fatal(e)) => return Err(e),
Err(e) => {
result.result = if e.is_oog() {
InstructionResult::PrecompileOOG
} else {
InstructionResult::PrecompileError
};
if !e.is_oog() && context.journal().depth() == 1 {
context
.local_mut()
.set_precompile_error_context(e.to_string());
}
}
}
Ok(Some(result))
}
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
Self::warm_addresses(self)
}
fn contains(&self, address: &Address) -> bool {
Self::contains(self, address)
}
}
看一下 Run 的内容.统一将输入数据 &inputs.input 转成 &[u8] 切片.
range 是一个包含 startIndex、endIndex 的范围.\
shared_memory_buffer_slice 返回 startIndex 和 endIndex 之间的切片(这里不展开,属于其他章节).
Bytes 类型是 pub struct Bytes(pub bytes::Bytes),字段是匿名字段,所以用bytes.0获取内容.
let exec_result = {
let r;
let input_bytes = match &inputs.input {
CallInput::SharedBuffer(range) => {
if let Some(slice) = context.local().shared_memory_buffer_slice(range.clone()) {
r = slice;
r.as_ref()
} else {
&[]
}
}
CallInput::Bytes(bytes) => bytes.0.iter().as_slice(),
};
precompile.execute(input_bytes, inputs.gas_limit)
};
对返回结果进行处理.这里没有啥比较复杂的逻辑.\
oog 是 out of gas, gas不够.\
Revert 和 Return 对于 EVM 来说都是执行成功.不同的是对Caller, Return 算成功,Revert 算失败.
match exec_result {
Ok(output) => {
result.gas.record_refund(output.gas_refunded);
let underflow = result.gas.record_cost(output.gas_used);
assert!(underflow, "Gas underflow is not possible");
result.result = if output.reverted {
InstructionResult::Revert
} else {
InstructionResult::Return
};
result.output = output.bytes;
}
Err(PrecompileError::Fatal(e)) => return Err(e),
Err(e) => {
result.result = if e.is_oog() {
InstructionResult::PrecompileOOG
} else {
InstructionResult::PrecompileError
};
if !e.is_oog() && context.journal().depth() == 1 {
context
.local_mut()
.set_precompile_error_context(e.to_string());
}
}
}
带有初始化 Frame 时的所有参数.\
如果是第一个 Frame,也就是根 Frame. 是由 handler 在 first_frame_input 中生成并返回.\
如果不是,则由上一个 Frame 执行产生,\
来源 handler->frame_run->frame.process_next_action.\
前面流程已经介绍过,这里不再介绍.
// crates/handler/src/frame.rs
pub fn process_next_action<
CTX: ContextTr,
ERROR: From<ContextTrDbError<CTX>> + FromStringError,
>(
&mut self,
context: &mut CTX,
next_action: InterpreterAction,
) -> Result<FrameInitOrResult<Self>, ERROR> {
let spec = context.cfg().spec().into();
// Run interpreter
let mut interpreter_result = match next_action {
InterpreterAction::NewFrame(frame_input) => {
let depth = self.depth + 1;
return Ok(ItemOrResult::Item(FrameInit {
frame_input,
depth,
memory: self.interpreter.memory.new_child_context(),
}));
}
InterpreterAction::Return(result) => result,
};
继续讲下 FrameInit 的定义\
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 如果在 Cargo.toml 中启用了 serde , 则为结构体派生序列化、反序列化的能力.
depth 当前frame的深度.
memory 共享内存.所有 frame 共享一大块预分配内存,每个Frame有自己的offset和len.后面再具体介绍 ShareMemory
frame_input 分为两种 CallInputs 和 CreateInputs
CallInputs
input 调用frame的数据.通过内存传递.
return_memory_offset 返回数据在内存中的offset,前面说了所有frame共享内存,输入输出都是通过这块内存传递
gas_limit
bytecode_address 要调用的合约的地址
known_bytecode 如果之前有提前加载过合约,bytecode会存放在这里
target_address 指示谁的地址storage被修改.如果你熟悉solidity的话,这里理解起来会简单点.主要有三种合约调用方法,Call、DelegateCall、StaticCall. Call 和 StaticCall 在调用的时候修改的是被调用方合约的storage.此时target_address 和bytecode_address是相同的. DelegateCall 在调用时修改的是调用方的Storage.大多数代理合约就用的DelegateCall,调用其他的合约,但实际修改自己的数据.
caller 调用该Frame合约的sender,不是整个交易的sender.用户调用A合约,A合约调用B合约,B的frame中的caller是A
value 要发送的值
scheme 调用的类型
CallCallCodeDelegateCallStaticCallis_static 是否只读交易,设置了后,storage只读,不能修改
CreateInputs
caller
scheme 创建合约的方法,两者的参数和计算方法都不同,计算出来的地址会不同.
value
init_code 创建合约的 initcode.部署合约的时候分为两部分.构造函数中的就属于initcode. 其他的部分属于 runcode .部署只用到 initcode ,调用Call交易的时候运行的runcode.
gas_limit
cached_address 合约部署后生成的地址.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FrameInit {
/// depth of the next frame
pub depth: usize,
/// shared memory set to this shared context
pub memory: SharedMemory,
/// Data needed as input for Interpreter.
pub frame_input: FrameInput,
}
EVM OpCode执行过程中 Gas 分成两部分,一部分是静态Gas,每个OpCode会有一个固定的消耗值.\ 一部分是动态Gas,例如内存扩展、是否预热地址、LOGx 的 data 长度.
在之前的 流程(4) 我写错了,没仔细看,以为只保存了静态部分.\ 但其实这里不止静态部分.静态部分是保存在了数组中.\ 动态部分通过调用函数的形式返回.
看下定义:\
table 是 Arc<[u64; 256]> 类型, Arc 是共享引用计数.用于线程间所有权共享.\
[u64; 256] 是包含 256 个 u64 类型的数组.\
table 里保存的是就是每个 OpCode 的基础 Gas。消耗.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct GasParams {
/// Table of gas costs for operations
table: Arc<[u64; 256]>,
/// Pointer to the table.
ptr: *const u64,
}
具体代码就不贴了.解释下 impl 的方法:
pub fn new(table: Arc<[u64; 256]>) -> Self 将现有的table替换成传入的table,设置裸指针指向table,并返回新的 GasParamspub fn override_gas(&mut self, values: impl IntoIterator<Item = (GasId, u64)>) 替换指定的opcode的gaspub fn table(&self) -> &[u64; 256] 返回table的引用pub fn new_spec(spec: SpecId) -> Self 根据specId生成opcode的gas消耗table.pub const fn get(&self, id: GasId) -> u64 获取指定 opcode 的gas消耗.pub fn exp_cost(&self, power: U256) -> u64 exp opcode 的gas 消耗pub fn selfdestruct_refund(&self) -> i64 合约调用 Selfdestruct 时 Gas 的退款selfdestruct_cold_cost() 计算 SELFDESTRUCT 针对冷账户的额外 gas 成本(cold + warm 的组合)selfdestruct_cost(should_charge_topup, is_cold) 综合计算 SELFDESTRUCT 的总 gas 成本,包括新账户创建和冷访问费用extcodecopy(len) 计算 EXTCODECOPY 复制指定长度代码的动态 gas 消耗(按 word 计算)mcopy_cost(len) 计算 MCOPY 复制指定长度内存的动态 gas 消耗(按 word 计算)sstore_static_gas() 返回 SSTORE 操作的基础静态 gas 成本sstore_set_without_load_cost() 返回 SSTORE “设置新槽”时扣除 load 成本后的额外 gassstore_reset_without_cold_load_cost() 返回 SSTORE “重置已有槽”时扣除 cold load 后的额外 gassstore_clearing_slot_refund() 返回 SSTORE 清空存储槽(非0→0)时可获得的退款金额sstore_dynamic_gas(is_istanbul, vals, is_cold) 根据存储值变化和冷热状态,计算 SSTORE 的动态 gas 部分sstore_refund(is_istanbul, vals) 根据 SSTORE 前后值变化,计算该操作可获得的存储退款金额(可能为负)log_cost(n, len) 计算 LOGx 操作的总 gas,包括每个 topic 的固定成本 + data 长度的线性成本keccak256_cost(len) 计算 KECCAK256 对指定长度输入的动态 gas 消耗(按 word 计算)memory_cost(len) 根据内存扩展到指定长度计算总 gas,使用线性 + 二次项公式initcode_cost(len) 计算 CREATE/CREATE2 中 initcode 长度对应的 gas 消耗(每 word 固定成本)create_cost() 返回 CREATE 操作的基础 gas 成本create2_cost(len) 计算 CREATE2 的总 gas 成本(基础 + keccak256 hash 输入长度成本)call_stipend() 返回子调用(带 value)时额外赠送的固定 gas stipend(通常 2300)call_stipend_reduction(gas_limit) 计算子调用 gas 转发时扣除的 1/64 部分transfer_value_cost() 返回带 value 的 CALL 操作额外收取的转账 gas 成本cold_account_additional_cost() 返回访问冷账户时额外收取的 gas(EIP-2929)cold_storage_additional_cost() 返回访问冷存储槽时额外收取的 gas 差值cold_storage_cost() 返回访问冷存储槽的总 gas 成本(warm + additional)new_account_cost(is_spurious_dragon, transfers_value) 根据分叉和是否转账 value,决定是否收取新账户创建 gasnew_account_cost_for_selfdestruct() 返回 SELFDESTRUCT 目标为新账户时的额外创建 gaswarm_storage_read_cost() 返回访问已热(warm)存储槽的基础读取 gas 成本copy_cost(len) 计算通用内存/代码拷贝操作的动态 gas 消耗(按 word 计算)copy_per_word_cost(word_num) 计算每 word 拷贝的 gas 成本乘以 word 数量的结果之前说过了好多次关于SharedMemory.\ 所有 Frame 共享一大块 Buffer, 每个Frame都有自己的 offset 和 len.\ 这样做能减少每次新建和销毁 frame 时的内存申请和释放.
父 Frame 调用 子Frame 的时候通过 SharedMemory 进行参数的传递.\
子Frame 返回结果给 父Frame 也通过SharedMemory.
跳到 SharedMemory 的定义
buffer Frame存放数据的地方.
Rc<RefCell<Vec<u8>>> 由内到外,一个Vec<u8>类型的Vec.RefCell 让数据可以通过不可变引用进行可变访问Rc 引用计数智能指针,让被包围的变量支持多个所有者但数据不可修改.my_checkpoint 当前 Frame 的 检查点,相当于当前Frame数据的起始位置.
child_checkpoint 子 Frame 数据的起始位置.因为没有保存Frame数据的长度,所以要保存child_checkpoint,方便在子Frame释放数据的时候回滚.
memory_limit 内存最大限制.当前EVM版本内存是无限制的.字段暂时没用.
pub struct SharedMemory {
/// The underlying buffer.
buffer: Option<Rc<RefCell<Vec<u8>>>>,
/// Memory checkpoints for each depth.
/// Invariant: these are always in bounds of `data`.
my_checkpoint: usize,
/// Child checkpoint that we need to free context to.
child_checkpoint: Option<usize>,
/// Memory limit. See [`Cfg`](context_interface::Cfg).
#[cfg(feature = "memory_limit")]
memory_limit: u64,
}
看下 SharedMemory的 函数,只截取了部分.\
SharedMemory 的中虽然有new(),但是实际并没有使用.\
第一次创建(根Frame)使用的是 new_with_buffer.\
就是用传进来的 buffer 设置 buffer 字段而已.
我们找下调用的位置.\
传入的是从 ctx.local().shared_memory_buffer() 获取的buffer.\
这部分属于 context 的部分,后面再讲
// crates/handler/src/handler.rs
//first_frame_input
let mut memory = SharedMemory::new_with_buffer(ctx.local().shared_memory_buffer().clone());
后面所有的 子Frame 都调用的 new_child_context .\
设置了 当前Frame 的 child_checkpoint 为 buffer 总长度.\
并返回新创建的 SharedMemory 类型.\
注意: 这里返回的数据不是返回当前Frame的数据.而是返回用于 子Frame 中的内存,所以 my_checkpoint 等于 父Frame 的 child_checkpoint,而 child_checkpoint 为 None.
pub fn new_with_buffer(buffer: Rc<RefCell<Vec<u8>>>) -> Self {
Self {
buffer: Some(buffer),
my_checkpoint: 0,
child_checkpoint: None,
#[cfg(feature = "memory_limit")]
memory_limit: u64::MAX,
}
}
pub fn new_child_context(&mut self) -> SharedMemory {
if self.child_checkpoint.is_some() {
panic!("new_child_context was already called without freeing child context");
}
let new_checkpoint = self.full_len();
self.child_checkpoint = Some(new_checkpoint);
SharedMemory {
buffer: Some(self.buffer().clone()),
my_checkpoint: new_checkpoint,
// child_checkpoint is same as my_checkpoint
child_checkpoint: None,
#[cfg(feature = "memory_limit")]
memory_limit: self.memory_limit,
}
}
fn full_len(&self) -> usize {
self.buffer_ref().len()
}
fn buffer_ref(&self) -> Ref<'_, Vec<u8>> {
self.buffer().dbg_borrow()
}
上面是创建相关的函数.接着看下其他的函数.
free_child_context 释放内存,只是修改长度为 子frame 检查点的位置.后面有新Frame需要内存直接进行修改.避免重新申请和释放
resize 内存增加指定长度.在OpCode执行过程中,会有内存不够申请内存的操作.
slice_len 返回该 Frame 中指定区间的数据切片.
slice_range 返回该 Frame 中指定区间的数据切片.
,这里的_是生命周期.便于理解可以当成RefMut<Vec<u8>>`Ref::map(buffer),将 buffer 转换成 RefMut<[u8]>切片.|b| {match b.get_mut(self.my_checkpoint + offset..self.my_checkpoint + offset + size) 用于从 slice 中获取指定范围的 slice.global_slice_range 返回整个 buffer (所有frame共享的而不是当前frame) 指定范围数据.
slice_mut 跟 slice_range 一样, 只不过参数是分开的.
pub fn free_child_context(&mut self) {
let Some(child_checkpoint) = self.child_checkpoint.take() else {
return;
};
unsafe {
self.buffer_ref_mut().set_len(child_checkpoint);
}
}
pub fn resize(&mut self, new_size: usize) {
self.buffer()
.dbg_borrow_mut()
.resize(self.my_checkpoint + new_size, 0);
}
pub fn slice_len(&self, offset: usize, size: usize) -> Ref<'_, [u8]> {
self.slice_range(offset..offset + size)
}
pub fn slice_range(&self, range: Range<usize>) -> Ref<'_, [u8]> {
let buffer = self.buffer_ref();
Ref::map(buffer, |b| {
match b.get(range.start + self.my_checkpoint..range.end + self.my_checkpoint) {
Some(slice) => slice,
None => debug_unreachable!("slice OOB: range; len: {}", self.len()),
}
})
}
pub fn global_slice_range(&self, range: Range<usize>) -> Ref<'_, [u8]> {
let buffer = self.buffer_ref();
Ref::map(buffer, |b| match b.get(range) {
Some(slice) => slice,
None => debug_unreachable!("slice OOB: range; len: {}", self.len()),
})
}
pub fn slice_mut(&mut self, offset: usize, size: usize) -> RefMut<'_, [u8]> {
let buffer = self.buffer_ref_mut();
RefMut::map(buffer, |b| {
match b.get_mut(self.my_checkpoint + offset..self.my_checkpoint + offset + size) {
Some(slice) => slice,
None => debug_unreachable!("slice OOB: {offset}..{}", offset + size),
}
})
}
继续看下保存和获取指定数据类型的函数\ 好像没有什么需要什么特别讲的.
pub fn get_byte(&self, offset: usize) -> u8 {
self.slice_len(offset, 1)[0]
}
pub fn get_word(&self, offset: usize) -> B256 {
(*self.slice_len(offset, 32)).try_into().unwrap()
}
pub fn get_u256(&self, offset: usize) -> U256 {
self.get_word(offset).into()
}
pub fn set_byte(&mut self, offset: usize, byte: u8) {
self.set(offset, &[byte]);
}
fn set_word(&mut self, offset: usize, value: &B256) {
self.set(offset, &value[..]);
}
pub fn set_u256(&mut self, offset: usize, value: U256) {
self.set(offset, &value.to_be_bytes::<32>());
}
pub fn set(&mut self, offset: usize, value: &[u8]) {
if !value.is_empty() {
self.slice_mut(offset, value.len()).copy_from_slice(value);
}
}
pub fn set_data(&mut self, memory_offset: usize, data_offset: usize, len: usize, data: &[u8]) {
let mut dst = self.context_memory_mut();
unsafe { set_data(dst.as_mut(), data, memory_offset, data_offset, len) };
}
pub fn global_to_local_set_data(
&mut self,
memory_offset: usize,
data_offset: usize,
len: usize,
data_range: Range<usize>,
) {
let mut buffer = self.buffer_ref_mut();
let (src, dst) = buffer.split_at_mut(self.my_checkpoint);
let src = if data_range.is_empty() {
&mut []
} else {
src.get_mut(data_range).unwrap()
};
unsafe { set_data(dst, src, memory_offset, data_offset, len) };
}
pub fn copy(&mut self, dst: usize, src: usize, len: usize) {
self.context_memory_mut().copy_within(src..src + len, dst);
}
pub fn context_memory(&self) -> Ref<'_, [u8]> {
let buffer = self.buffer_ref();
Ref::map(buffer, |b| match b.get(self.my_checkpoint..) {
Some(slice) => slice,
None => debug_unreachable!("Context memory should be always valid"),
})
}
pub fn context_memory_mut(&mut self) -> RefMut<'_, [u8]> {
let buffer = self.buffer_ref_mut();
RefMut::map(buffer, |b| match b.get_mut(self.my_checkpoint..) {
Some(slice) => slice,
None => debug_unreachable!("Context memory should be always valid"),
})
}
在文件的最下面,提供了两个函数,用于增加内存.\ 可以看到增加内存都是以 32个字节 的倍数来增加的.
#[inline]
pub fn resize_memory<Memory: MemoryTr>(
gas: &mut crate::Gas,
memory: &mut Memory,
gas_table: &GasParams,
offset: usize,
len: usize,
) -> Result<(), InstructionResult> {
#[cfg(feature = "memory_limit")]
if memory.limit_reached(offset, len) {
return Err(InstructionResult::MemoryLimitOOG);
}
let new_num_words = num_words(offset.saturating_add(len));
if new_num_words > gas.memory().words_num {
return resize_memory_cold(gas, memory, gas_table, new_num_words);
}
Ok(())
}
#[cold]
#[inline(never)]
fn resize_memory_cold<Memory: MemoryTr>(
gas: &mut crate::Gas,
memory: &mut Memory,
gas_table: &GasParams,
new_num_words: usize,
) -> Result<(), InstructionResult> {
let cost = gas_table.memory_cost(new_num_words);
let cost = unsafe {
gas.memory_mut()
.set_words_num(new_num_words, cost)
.unwrap_unchecked()
};
if !gas.record_cost(cost) {
return Err(InstructionResult::MemoryOOG);
}
memory.resize(new_num_words * 32);
Ok(())
} 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!