REVM源码阅读-Frame(1)

前言前一系列的文章是为了快速走个流程,让大家对REVM有个概念.让源码阅读起来没那么恐惧.忽略了很多细节和概念.在后面的文章中会尽量讲详细点.对Rust的语法能解释的我也会尽量解释.Rust的泛型看着真是头痛,对于刚入门的我来说真是天书.还得查资料理解后才能写出来.当然,我会尽

前言

前一系列的文章是为了快速走个流程,让大家对 REVM 有个概念.\ 让源码阅读起来没那么恐惧.忽略了很多细节和概念.\ 在后面的文章中会尽量讲详细点.

Rust 的语法能解释的我也会尽量解释.\ Rust 的泛型看着真是头痛,对于刚入门的我来说真是天书.\ 还得查资料理解后才能写出来.当然,我会尽量保证正确.

Frame 里面包含了多个其他 EVM 的类.我们顺便在这章把它们讲了

作用

Frame 的源码在前面 流程(4) 中已经介绍了很大一部分.\ 在流程中我们讲解了 make_call_framemake_create_frame 的执行过程.\ 两个函数都是用来创建执行合约的 Frame .

在REVM中, 每一次的合约调用都会创建一个 Frame.\ 如果只是 EOA 账户之间的转账,不会创建 Frame.\ 合约调用合约(CALLSTATICCALLDELEGATECALL) 就会生成一个新的 Frame

Frame 之间的执行环境都是隔离的,都拥有自己独立的:

  • 字节码
  • Stack
  • 合约地址
  • PC(指令位置)
  • Gas剩余
  • Memory(共享一个大内存块,但是每个都有自己的范围.)
  • Journaled State 变更记录

Frame 定义

我们先来看下 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 派生 CloneDebug .\ 它不是直接作用于 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 {
        
    }
}

Frame实现

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;
    }
}

这一段看起来比较绕的只有第一段.表达式左右两边都是相同的.\ 分别点进去定义跳转还是的不同地方.

先看 FrameTrFrameResult 的定义.\ 这里的 type FrameResult: From<FrameResult>,可以分成两部分来理解.\ type FrameResultFrameResult: 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消耗

OutFrame

上面5个参数中,其他的都比较容易理解.就OutFrame 一下看不出是做啥的.\ 跳转到定义:

#[allow(missing_debug_implementations)], 定义 pub 类型时,忽略没有实现 Debug Trait的警告

总共3个参数.

  • ptr: *mut T 存放Frame数据的内存地址
  • init: bool ptr指向的内容是否初始化为有效的T
  • lt: core::marker::PhantomData<&'a mut T> 标记T的借用所有权和生命周期.原因是因为ptr的类型是 *mut T 而不是 &mut T,这是裸指针而不是引用.裸指针没有生命周期和所有权.这样写能让裸指针更安全.
// crates/context/interface/src/local.rs
// 一个可能已初始化的帧。用于在主循环中初始化新帧时。
#[allow(missing_debug_implementations)]
pub struct OutFrame&lt;'a, T> {
    ptr: *mut T,
    init: bool,
    lt: core::marker::PhantomData&lt;&'a mut T>,
}
impl&lt;'a, T> OutFrame&lt;'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&lt;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 &lt; self.stack.len())\ 获取 FrameStack 的起始指针 + idx 个位置,判断 idx 是否小于 self.stack.len().\ 如果是第一次调用, FrameStack 的len肯定为0,所以 这里的idx &lt; 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&lt;'_, 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&lt;'_, T> {
        unsafe {
            OutFrame::new_maybe_uninit(self.stack.as_mut_ptr().add(idx), idx &lt; self.stack.len())
        }
    }

回到 init_with_context ,可以看到只是将 OutFrame 类型的 this 传进去给两个 CreateFrame 函数\ 点进去 make_call_frame 并找到 this的使用位置.\ 终于找到了它的使用位置. this.getthis.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.getthis.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");
    }
}

我们重新整理下流程.

  1. crates/handler/src/evm.rs 中的 Evm 有一个字段 frame_stack, 它保存所有生成的Frame
  2. frame_init 中,会调用 frame_stack.start_init 或者 self.frame_stack.get_next() 生成一个OutFrame类型的newFrame.里面裸指针保存了 framefram_stack 中的位置.
  3. 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 是预编译合约集合,但是还没深入进去讲下.\ 这里的 PRECOMPILES 是泛型了,接受实现了 PrecompileProvider&lt;CTX, Output = InterpreterResult> Trait的类型.

进去看下 PrecompileProvider&lt;CTX: ContextTr> 的定义.

// crates/handler/src/precompile_provider.rs
#[auto_impl(&mut, Box)]
pub trait PrecompileProvider&lt;CTX: ContextTr> {
    type Output;
    fn set_spec(&mut self, spec: &lt;CTX::Cfg as Cfg>::Spec) -> bool;
    fn run(
        &mut self,
        context: &mut CTX,
        inputs: &CallInputs,
    ) -> Result&lt;Option&lt;Self::Output>, String>;
    fn warm_addresses(&self) -> Box&lt;impl Iterator&lt;Item = Address>>;
    fn contains(&self, address: &Address) -> bool;
}
  • set_spec 设置 spec_id,如果跟之前不同则返回 true.
  • run 运行指定的precompile
  • warm_addresses 获取预热地址列表,这里是获取预编译合约
  • contains 判断地址是否预编译合约 precompile

功能挺明显了,就是获取根据 EVM 版本获取所有的预编译合约保存.再提供接口可以运行指定预编译合约、获取合约列表、判断是否预编译合约.

crates/handler/src/precompile_provider.rs 这个文件中只有一个 EthPrecompiles 实现了 Trait.\ 我们就直接看下 EthPrecompiles 的内容.

好多内容都比较简单,我们挑着讲.\ &'static Precompiles , 这里的 &'static 是生命周期,表示 Precompiles 在整个程序运行期间都有效.

pub fn warm_addresses(&self) -> Box&lt;impl Iterator&lt;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&lt;impl Iterator&lt;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&lt;CTX: ContextTr> PrecompileProvider&lt;CTX> for EthPrecompiles {
    type Output = InterpreterResult;

    fn set_spec(&mut self, spec: &lt;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&lt;Option&lt;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&lt;impl Iterator&lt;Item = Address>> {
        Self::warm_addresses(self)
    }
    fn contains(&self, address: &Address) -> bool {
        Self::contains(self, address)
    }
}

看一下 Run 的内容.统一将输入数据 &inputs.input 转成 &[u8] 切片.

range 是一个包含 startIndexendIndex 的范围.\ shared_memory_buffer_slice 返回 startIndexendIndex 之间的切片(这里不展开,属于其他章节).

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());
  }
 }
}

FrameInit

带有初始化 Frame 时的所有参数.\ 如果是第一个 Frame,也就是根 Frame. 是由 handlerfirst_frame_input 中生成并返回.\ 如果不是,则由上一个 Frame 执行产生,\ 来源 handler->frame_run->frame.process_next_action.\ 前面流程已经介绍过,这里不再介绍.

// crates/handler/src/frame.rs
pub fn process_next_action&lt;
        CTX: ContextTr,
        ERROR: From&lt;ContextTrDbError&lt;CTX>> + FromStringError,
    >(
        &mut self,
        context: &mut CTX,
        next_action: InterpreterAction,
    ) -> Result&lt;FrameInitOrResult&lt;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 分为两种 CallInputsCreateInputs

    • 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 调用的类型

      • Call
      • CallCode
      • DelegateCall
      • StaticCall
    • is_static 是否只读交易,设置了后,storage只读,不能修改

    • CreateInputs

    • caller

    • scheme 创建合约的方法,两者的参数和计算方法都不同,计算出来的地址会不同.

      • CREATE
      • CREATE2
      • Custom
    • 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,
}

GasParams

EVM OpCode执行过程中 Gas 分成两部分,一部分是静态Gas,每个OpCode会有一个固定的消耗值.\ 一部分是动态Gas,例如内存扩展、是否预热地址、LOGx 的 data 长度.

在之前的 流程(4) 我写错了,没仔细看,以为只保存了静态部分.\ 但其实这里不止静态部分.静态部分是保存在了数组中.\ 动态部分通过调用函数的形式返回.

看下定义:\ table 是 Arc&lt;[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&lt;[u64; 256]>,
    /// Pointer to the table.
    ptr: *const u64,
}

具体代码就不贴了.解释下 impl 的方法:

  • pub fn new(table: Arc&lt;[u64; 256]>) -> Self 将现有的table替换成传入的table,设置裸指针指向table,并返回新的 GasParams
  • pub fn override_gas(&mut self, values: impl IntoIterator&lt;Item = (GasId, u64)>) 替换指定的opcode的gas
  • pub 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 成本后的额外 gas
  • sstore_reset_without_cold_load_cost() 返回 SSTORE “重置已有槽”时扣除 cold load 后的额外 gas
  • sstore_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,决定是否收取新账户创建 gas
  • new_account_cost_for_selfdestruct() 返回 SELFDESTRUCT 目标为新账户时的额外创建 gas
  • warm_storage_read_cost() 返回访问已热(warm)存储槽的基础读取 gas 成本
  • copy_cost(len) 计算通用内存/代码拷贝操作的动态 gas 消耗(按 word 计算)
  • copy_per_word_cost(word_num) 计算每 word 拷贝的 gas 成本乘以 word 数量的结果

SharedMemory

之前说过了好多次关于SharedMemory.\ 所有 Frame 共享一大块 Buffer, 每个Frame都有自己的 offset 和 len.\ 这样做能减少每次新建和销毁 frame 时的内存申请和释放.

父 Frame 调用 子Frame 的时候通过 SharedMemory 进行参数的传递.\ 子Frame 返回结果给 父Frame 也通过SharedMemory.

跳到 SharedMemory 的定义

  • buffer Frame存放数据的地方.

    • Rc&lt;RefCell&lt;Vec&lt;u8>>> 由内到外,一个Vec&lt;u8>类型的Vec.
    • RefCell 让数据可以通过不可变引用进行可变访问
    • Rc 引用计数智能指针,让被包围的变量支持多个所有者但数据不可修改.
    • 最终 buffer 可以拥有多个所用者,并且每个所用者都可修改数据.
  • my_checkpoint 当前 Frame 的 检查点,相当于当前Frame数据的起始位置.

  • child_checkpoint 子 Frame 数据的起始位置.因为没有保存Frame数据的长度,所以要保存child_checkpoint,方便在子Frame释放数据的时候回滚.

  • memory_limit 内存最大限制.当前EVM版本内存是无限制的.字段暂时没用.

pub struct SharedMemory {
    /// The underlying buffer.
    buffer: Option&lt;Rc&lt;RefCell&lt;Vec&lt;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&lt;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 .\ 设置了 当前Framechild_checkpointbuffer 总长度.\ 并返回新创建的 SharedMemory 类型.\ 注意: 这里返回的数据不是返回当前Frame的数据.而是返回用于 子Frame 中的内存,所以 my_checkpoint 等于 父Framechild_checkpoint,而 child_checkpoint 为 None.

pub fn new_with_buffer(buffer: Rc&lt;RefCell&lt;Vec&lt;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&lt;'_, Vec&lt;u8>> {
 self.buffer().dbg_borrow()
}

上面是创建相关的函数.接着看下其他的函数.

  • free_child_context 释放内存,只是修改长度为 子frame 检查点的位置.后面有新Frame需要内存直接进行修改.避免重新申请和释放

  • resize 内存增加指定长度.在OpCode执行过程中,会有内存不够申请内存的操作.

  • slice_len 返回该 Frame 中指定区间的数据切片.

  • slice_range 返回该 Frame 中指定区间的数据切片.

    • self.buffer_refmut 返回 buffer 的可修改引用.类型是`RefMut<', Vec<u8>>,这里的_是生命周期.便于理解可以当成RefMut<Vec<u8>>`
    • Ref::map(buffer),将 buffer 转换成 RefMut&lt;[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&lt;'_, [u8]> {
 self.slice_range(offset..offset + size)
}
pub fn slice_range(&self, range: Range&lt;usize>) -> Ref&lt;'_, [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&lt;usize>) -> Ref&lt;'_, [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&lt;'_, [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::&lt;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&lt;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&lt;'_, [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&lt;'_, [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&lt;Memory: MemoryTr>(
    gas: &mut crate::Gas,
    memory: &mut Memory,
    gas_table: &GasParams,
    offset: usize,
    len: usize,
) -> Result&lt;(), 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&lt;Memory: MemoryTr>(
    gas: &mut crate::Gas,
    memory: &mut Memory,
    gas_table: &GasParams,
    new_num_words: usize,
) -> Result&lt;(), 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(())
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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