REVM源码阅读- Interpreter(2)

前言年前又爆仓,加上过年事情多.一直没有时间继续往下写.过年回来调整了下状态,也为了爆仓后重新找份工作,接着写完这篇.上一篇介绍了Interpreter的成员属性.这一篇写执行流程和Opcode.

前言

年前又爆仓,加上过年事情多.一直没有时间继续往下写.
过年回来调整了下状态,也为了爆仓后重新找份工作,接着写完这篇.

上一篇介绍了 Interpreter 的成员属性.这一篇写执行流程和Opcode.
为了配合这篇文章,写了个简单的网站来模拟 EVM 的执行过程.
这篇文章跟着模拟走,执行到哪就讲涉及到的Opcode.
网站:http://localhost:3000/
交易:https://etherscan.io/tx/0x005347d49f1fbcb3f0409dff3ef5e1c6f0186a2efb6be97a91facb0e8fdcbdd3
主合约:https://etherscan.io/address/0xa842927c9c9712394c57264716593db383c3a5cf#code
副合约:https://etherscan.io/address/0x0fb8f3bae4005f8ee539c20e6273b2aac25e5ad2#code

网站介绍

website.png

  1. 每次 CallCreate 都会创建一个 Frame.这里就是对应的 Frame.
    1. : 为了方便理解才会每次都生成一个新的 Frame,实际可能调用的都是同一个地址的合约.
  2. Opcode 列表,也就是调用的合约的字节码对应的Opcode列表.左边是 PC, 右边是具体OpCode.
  3. Stack Interpreter 执行过程中的栈数据.
  4. Memory Interpreter 执行过程中的内存数据.
  5. Storage 合约的Storage数据,这里的数据我暂时没写.
  6. ReturnData 每一次合约 Call 后的返回数据.
  7. 执行到哪一步时,Opcode 的作用解释.这里的 index界面3的index 是相反的,这里的 index是从顶开始算
  8. Context 对应的 Frame Id.calldata 是这次交易发送的数据.

执行过程

instruction_table_impl 中可以看到.
PUSH、DUP、SWAP、POP类的指令,直接调用stack的操作.
后续会直接跳过相关的操作.

// crates/interpreter/src/instructions.rs
const fn instruction_table_impl<WIRE: InterpreterTypes, H: Host>() -> [Instruction<WIRE, H>; 256] {
    use bytecode::opcode::*;
    let mut table = [Instruction::unknown(); 256];
    table[POP as usize] = Instruction::new(stack::pop, 2);

    table[PUSH0 as usize] = Instruction::new(stack::push0, 2);
    table[PUSH1 as usize] = Instruction::new(stack::push::<1, _, _>, 3);

    table[DUP1 as usize] = Instruction::new(stack::dup::<1, _, _>, 3);
    table[DUP2 as usize] = Instruction::new(stack::dup::<2, _, _>, 3);

    table[SWAP1 as usize] = Instruction::new(stack::swap::<1, _, _>, 3);
    table[SWAP2 as usize] = Instruction::new(stack::swap::<2, _, _>, 3);
}

在网站中左上角,查看Opcodes的前3步.
分别是 PUSH1 0x80PUSH1 0x40MSTORE.
这三个 OpCode 合起来是在内存中 0x40 的位置 写入0x80.
为什么这样做呢?这是因为 Solidity 的规定:

  • 0x00 - 0x3f 这64个字节的位置为临时读写空间.主要勇于哈希计算.
  • 0x40 - 0x5f 这32个字节为自由内存指针.告诉EVM,在这位置之后的空间你可以使用.
    • 前面的三个Opcode对应的就是这里,指示 0x80 后面的空间都可以使用.
  • 0x60-0x7f 零值空间,不被使用,永远为0.

当然,你在合约的 solidity 源码和 REVM 代码里都找不到关于这些的定义.
因为这部分写在编译器里了.
在将 solidity 源码编译成字节码的过程中,会自动进行内存指针的管理并写到字节码中.
REVM 只负责执行编译好的字节码.

常用宏

在讲其他 Opcode 前,先讲下几个宏

popn

功能很简单,就是从 interpreter 的栈中弹出 n 个元素.
#[macro_export] 指示当前宏可以在其他crate中使用.
([ $($x:ident),* ],$interpreter:expr $(,$ret:expr)? ) 这段不了解Rust宏的模式匹配看着是真头疼.
了解正则的话,可能会好点.一步一步拆分它

  • [ $($x:ident),* ]
    • $x:ident, 相当于捕获一个 ident(变量名、函数名) 类型的语法片段,命名为 $x.
    • [ $($x:ident),* ] 这里的 * 是重复捕获.匹配零次或者多次.
  • $interpreter:expr 捕获一个 expr(表达式) 类型的语法片段.
  • $(,$ret:expr)? 可选捕获,也就是0 或者 1个.
  • let Some([$( $x ),*]) 前面匹配到的 $x 会传入到数组中.
  • $interpreter.stack.popn() 会根据匹配到的数组数量弹出相同的数量.如果数量不足,则返回halt_underflow.

popn!([offset, value], context.interpreter) 大部分 popn 宏的使用形式.从context.interpreter 弹出两个元素,赋值到 offsetvalue.

// crates/interpreter/src/instructions/macros.rs
#[macro_export]
#[collapse_debuginfo(yes)]
macro_rules! popn {
    ([ $($x:ident),* ],$interpreter:expr $(,$ret:expr)? ) => {
        let Some([$( $x ),*]) = $interpreter.stack.popn() else {
            $interpreter.halt_underflow();
            return $($ret)?;
        };
    };
}
// crates/interpreter/src/interpreter/stack.rs
#[inline]
fn popn<const N: usize>(&mut self) -> Option<[U256; N]> {
    if self.len() < N {
        return None;
    }
    // SAFETY: Stack length is checked above.
    Some(unsafe { self.popn::<N>() })
}
popn_top

也是从 stack 弹出 n 个元素.不同的是这里会返回 栈顶 的指针.
不深入讲解了.和上面的逻辑差不多.

#[macro_export]
#[collapse_debuginfo(yes)]
macro_rules! popn_top {
    ([ $($x:ident),* ], $top:ident, $interpreter:expr $(,$ret:expr)? ) => {
        /*
        let Some(([$( $x ),*], $top)) = $interpreter.stack.popn_top() else {
            $interpreter.halt($crate::InstructionResult::StackUnderflow);
            return $($ret)?;
        };
        */

        // Workaround for https://github.com/rust-lang/rust/issues/144329.
        if $interpreter.stack.len() < (1 + $crate::_count!($($x)*)) {
            $interpreter.halt_underflow();
            return $($ret)?;
        }
        let ([$( $x ),*], $top) = unsafe { $interpreter.stack.popn_top().unwrap_unchecked() };
    };
}
resize_memory

如果有需要的话会调整内存的大小.
$crate 指的是当前宏所在的 crate.
如果传入的是 3 个参数,加一个()再调用一遍宏.
如果是 4 个参数则调用 interpreter 下的 resize_memory.
let new_num_words = num_words(offset.saturating_add(len)); 这里是用(offset + len)/32 并向上取整来计算需要的 words.

例如我在 0x65(101) 写入 32 个字节. new_num_words = (101 + 32)/32 = 5.

gas 消耗的计算公式在前面说过了$$Cost = (size^2 / 512) + (3 \times size)$$
memory.resize(new_num_words * 32) 来实际调整大小.

// crates/interpreter/src/instructions/macros.rs
macro_rules! resize_memory {
    ($interpreter:expr, $offset:expr, $len:expr) => {
        $crate::resize_memory!($interpreter, $offset, $len, ())
    };
    ($interpreter:expr, $offset:expr, $len:expr, $ret:expr) => {
        if let Err(result) = $crate::interpreter::resize_memory(
            &mut $interpreter.gas,
            &mut $interpreter.memory,
            &$interpreter.gas_params,
            $offset,
            $len,
        ) {
            $interpreter.halt(result);
            return $ret;
        }
    };
}
//crates/interpreter/src/interpreter.rs
pub fn resize_memory(&mut self, offset: usize, len: usize) -> bool {
    if let Err(result) = resize_memory(
        &mut self.gas,
        &mut self.memory,
        &self.gas_params,
        offset,
        len,
    ) {
        self.halt(result);
        return false;
    }
    true
}
// crates/interpreter/src/interpreter/shared_memory.rs
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(())
}
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(())
}
gas

判断 Gas 是否足够支付,并记录Gas消耗.

// crates/interpreter/src/instructions/macros.rs
#[macro_export]
#[collapse_debuginfo(yes)]
macro_rules! gas {
    ($interpreter:expr, $gas:expr) => {
        $crate::gas!($interpreter, $gas, ())
    };
    ($interpreter:expr, $gas:expr, $ret:expr) => {
        if !$interpreter.gas.record_cost($gas) {
            $interpreter.halt_oog();
            return $ret;
        }
    };
}
// crates/interpreter/src/gas.rs
pub fn record_cost(&mut self, cost: u64) -> bool {
        if let Some(new_remaining) = self.remaining.checked_sub(cost) {
            self.remaining = new_remaining;
            return true;
        }
        false
    }

MSTORE

看下代码逻辑:
popn!([offset, value], context.interpreter)Stack 中弹出两个元素.
栈顶是 offset,接着是 value.
as_usize_or_fail! 用于将 U256 类型转换成 usize.这里不进行内部解释了.
resize_memory!(context.interpreter, offset, 32); 检测是否需要调整内存大小.
context.interpreter.memory.set(offset, &value.to_be_bytes::<32>());offset 中填入 value.
在网站中跳到第4步(第三步执行完成).可以看到内存中出现了数据,最后一位为 80.

这篇文章是为了跟随网站的模拟过程,所以暂时不讨论其他的内存相关的Opcode.
感兴趣的自己可以先看下,跟这个逻辑类似.

// crates/interpreter/src/instructions/memory.rs
pub fn mstore<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
    popn!([offset, value], context.interpreter);
    let offset = as_usize_or_fail!(context.interpreter, offset);
    resize_memory!(context.interpreter, offset, 32);
    context
        .interpreter
        .memory
        .set(offset, &value.to_be_bytes::<32>());
}

MLOAD

从内存中加载数据.

逻辑比较简单,就不解释了.

pub fn mload<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
    popn_top!([], top, context.interpreter);
    let offset = as_usize_or_fail!(context.interpreter, top);
    resize_memory!(context.interpreter, offset, 32);
    *top =
        U256::try_from_be_slice(context.interpreter.memory.slice_len(offset, 32).as_ref()).unwrap()
}

CALLDATASIZE

清晰明了,计算出 inputlenpushstack 中.
执行到 第8步 ,栈中插入 0x24.
因为 calldata 是0x769c02d50000000000000000000000000000000000000000000000000000000000000001.
36个字节,前面是函数的abi encode,后面是函数参数.转成16进制就是0x24.

pub fn calldatasize<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
    push!(
        context.interpreter,
        U256::from(context.interpreter.input.input().len())
    );
}

LT

比较栈顶两个元素的大小.
为了方便理解,后面栈顶用 index 为 1 表示,次栈顶用 index 为 2 表示,以此类推.
popn_top 弹出两个元素并返回栈顶的指针.
假设栈顶为a,次栈顶为b, if a < b, then 1, else 0.
并将比较结果插入栈顶.
执行第6,7步,栈中元素从上到下是 0x24 , 0x4, 0x24 > 0x4, 所以结果是0.

// crates/interpreter/src/instructions/bitwise.rs
pub fn lt<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
    popn_top!([op1], op2, context.interpreter);
    *op2 = U256::from(op1 < *op2);
}

JUMPI

条件跳转Op.
栈顶为跳转目的地,次栈顶为跳转条件.
如果次栈顶为1,则跳转到目的地.如果为 0,则PC + 1

jump_table 我在上一章中可能写错,改了但是不确定有没有改完.
jump_table 是 用 u8 实现的位图. 是 bitvec<u8>. jumpdestpc 对应的第几位(bit),而不是第几个byte.

is_valid_legacy_jump 最终调用 JUMPTABLE中的 is_valid.
table_ptr: jump_table 指针. pc >> 3, 右移3位, 相当于除以8. 因为一个字节就是 8 位.这里是找出位于第几个字节. 1 << (pc & 7),pc & 7这里的功能相当于% 8, 找出字节中第几位是 dest.结果是0-7. 1 <<` 是因为上一章我们说过JumpTable的排序在字节之间是顺序的,在字节内部是逆序的.也就是从低位到高位,而不是符合阅读习惯的从高到低.这个操作同时保证了一个字节中只有一位是1.

在网站中,这次跳转条件不满足,PC + 1往下运行.

// crates/interpreter/src/instructions/control.rs
pub fn jumpi&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn!([target, cond], context.interpreter);
    if !cond.is_zero() {
        jump_inner(context.interpreter, target);
    }
}
fn jump_inner&lt;WIRE: InterpreterTypes>(interpreter: &mut Interpreter&lt;WIRE>, target: U256) {
    let target = as_usize_or_fail!(interpreter, target, InstructionResult::InvalidJump);
    if !interpreter.bytecode.is_valid_legacy_jump(target) {
        interpreter.halt(InstructionResult::InvalidJump);
        return;
    }
    // SAFETY: `is_valid_jump` ensures that `dest` is in bounds.
    interpreter.bytecode.absolute_jump(target);
}
//crates/bytecode/src/legacy/jump_map.rs
pub fn is_valid(&self, pc: usize) -> bool {
        pc &lt; self.len && unsafe { *self.table_ptr.add(pc >> 3) & (1 &lt;&lt; (pc & 7)) != 0 }
    }

CALLDATALOAD

offset_ptr 保存的是栈顶指针.
let offset = as_usize_saturated!(offset_ptr) 这里传入的是指针,但会自动解引用获取栈顶的元素,并转成 usize. offset 保存的是要读取 calldata 的偏移位置.
let count = 32.min(input_len - offset); 默认读取32字节,如果剩余不足则读取剩余的.
判断 input 类型

  • Bytes 类型,直接复制 count 字节的数据到 word 中.
  • SharedBuffer 类型,从内存中读取数据后再复制到word中.
    *offset_ptr = word.into()word 的数据写入到栈顶.
    网站中运行到11步,可以看到栈中数据为:
    0x769c02d500000000000000000000000000000000000000000000000000000000.
// crates/interpreter/src/instructions/system.rs
pub fn calldataload&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn_top!([], offset_ptr, context.interpreter);
    let mut word = B256::ZERO;
    let offset = as_usize_saturated!(offset_ptr);
    let input = context.interpreter.input.input();
    let input_len = input.len();
    if offset &lt; input_len {
        let count = 32.min(input_len - offset);

        match context.interpreter.input.input() {
            CallInput::Bytes(bytes) => {
                unsafe {
                    ptr::copy_nonoverlapping(bytes.as_ptr().add(offset), word.as_mut_ptr(), count)
                };
            }
            CallInput::SharedBuffer(range) => {
                let input_slice = context.interpreter.memory.global_slice(range.clone());
                unsafe {
                    ptr::copy_nonoverlapping(
                        input_slice.as_ptr().add(offset),
                        word.as_mut_ptr(),
                        count,
                    )
                };
            }
        }
    }
    *offset_ptr = word.into();
}

SHR

右移操作.将数据向右移动 n 位.移出的低位将被丢弃.高位部分补0.
这里是按位进行计算,不是按字节.

check!(context.interpreter, CONSTANTINOPLE) check宏检测指定硬分叉版本是否启用.
popn_top!([op1], op2, context.interpreter) 弹出栈顶到 op1 , 保存的是要移动的位数.
op2 保存的是弹出移动位数后的栈顶指针.
判断要移动的位数是否小于 256.是则右移指定位数并保存回栈顶.

网站执行11步将 0xe0 写入栈顶.执行12步进行右移操作.
0x769c02d500000000000000000000000000000000000000000000000000000000 右移 0xe0(224位,28个字节).结果是 0x769c02d5.再将 0x769c02d5 写入栈顶.

// crates/interpreter/src/instructions/bitwise.rs
pub fn shr&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    check!(context.interpreter, CONSTANTINOPLE);
    popn_top!([op1], op2, context.interpreter);
    let shift = as_usize_saturated!(op1);
    *op2 = if shift &lt; 256 {
        *op2 >> shift
    } else {
        U256::ZERO
    }
}

一直执行到17步,前面那几步是为了找到 0x769c02d5(runFullOpcodeSuite) 函数对应的PC位置.
这里开始进行跳转.
跳转后指定函数后,还会进行一系列的检查和加载数据.
加载函数的参数、检测payable等等.我们的任务不是学习反编译.所以这部分都跳过.
直接跳到 104 步,这里是函数主体开始的地方.

ADDRESS

target_address 地址入栈.
如果你去看网站对应的 solidity 源码,你要搞清楚.
这里 ADDRESS 出现是因为 this,而不是因为address(this) 中的 address.
solidity 中 address 只是类型转换. this 是获取 target_address.

// crates/interpreter/src/instructions/system.rs
pub fn address&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    push!(
        context.interpreter,
        context
            .interpreter
            .input
            .target_address()
            .into_word()
            .into()
    );
}

CALLVALUE

Transaction 发送时带的 Value 压入栈.

网站发送的交易我发送了 0.00001 ETH(10000000000000 WEI, 0x9184e72a000).
执行108步,将 0x9184e72a000 压入栈.

// crates/interpreter/src/instructions/system.rs
pub fn callvalue&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    push!(context.interpreter, context.interpreter.input.call_value());
}

GASPRICE

将 Transacton 发送时带的 GasPrice 压入栈.

真实交易的 GasPrice2032443413 Wei
网站上112步模拟的交易入栈的是 0x79aadd5e(2041240926)
是因为模拟的时候可能 BlockBasefeeTxPriorityFee 会有不同.

// crates/interpreter/src/instructions/tx_info.rs
pub fn gasprice&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(
    context: InstructionContext&lt;'_, H, WIRE>,
) {
    push!(context.interpreter, context.host.effective_gas_price());
}

ADDMOD

将两个数相加并对另一个数取模

源码中是将 msg.value 和 函数参数进行相加,并对 100 就行取模.
网站中拖到 125 步. 此时栈顶自上而下是

  • 0x0000000000000000000000000000000000000000000000000000000000000001(1)
  • 0x000000000000000000000000000000000000000000000000000009184e72a000(10000000000000)
  • 0x0000000000000000000000000000000000000000000000000000000000000064(100)
    相当于 (1 + 10000000000000)%100 = 1
    执行125后,栈顶变成 0x0000000000000000000000000000000000000000000000000000000000000001.
//crates/interpreter/src/instructions/arithmetic.rs
pub fn addmod&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn_top!([op1, op2], op3, context.interpreter);
    *op3 = op1.add_mod(op2, *op3)
}

AND

执行到 162 步会遇到 AND 操作.
比较简单就不解释了

// crates/interpreter/src/instructions/bitwise.rs
pub fn bitand&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn_top!([op1], op2, context.interpreter);
    *op2 = op1 & *op2;
}

SSTORE

将值写入存储槽.

EIP-2200 优化 Gas 消耗,之前重复对一个槽位修改n次会消耗固定Gas * n.之后改成第一次消耗固定Gas,后面的每一次只消耗少部分gas.
BERLIN升级(EIP-2929) 增加了Cold、Warm概念, AddressStorage 第一次访问属于 冷访问, Gas消耗会高些,后续访问属于热访问,Gas消耗会少些.

require_non_staticcall 判断当前 CALL 是否是 STATICCALL, STATICCALL 只能访问不能修改数据.
target 要实际修改的合约地址.(CALL和DELEGATECALL的关键就是这里不一样)

每个OpCode都有静态固定Gas消耗,这里是判断是否足够.

gas!(
        context.interpreter,
        context.interpreter.gas_params.sstore_static_gas()
    );

cold_storage_additional_cost 冷访问的额外消耗.
这里这样计算是因为不管冷热都会扣除 WARM_STORAGE_READ_COST,会在其他地方统一扣除WARM_STORAGE_READ_COST,所以这里减去.
COLD_SLOAD_COST = 2100, WARM_STORAGE_READ_COST =100

table[GasId::cold_storage_additional_cost().as_usize()] =
                gas::COLD_SLOAD_COST - gas::WARM_STORAGE_READ_COST;

skip_cold 这里的判断会有些奇怪.正常 Gas不足 是直接报错,这里当参数传入.
这是因为这里保存已加载过的 地址Storage 是在 journal 中,这里并不知道有没有加载过,所以不能直接报错.

pub fn sstore&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    require_non_staticcall!(context.interpreter);
    popn!([index, value], context.interpreter);

    let target = context.interpreter.input.target_address();
    let spec_id = context.interpreter.runtime_flag.spec_id();

    if spec_id.is_enabled_in(ISTANBUL)
        && context.interpreter.gas.remaining() &lt;= context.interpreter.gas_params.call_stipend()
    {
        context
            .interpreter
            .halt(InstructionResult::ReentrancySentryOOG);
        return;
    }

    gas!(
        context.interpreter,
        context.interpreter.gas_params.sstore_static_gas()
    );

    let state_load = if spec_id.is_enabled_in(BERLIN) {
        let additional_cold_cost = context
            .interpreter
            .gas_params
            .cold_storage_additional_cost();
        let skip_cold = context.interpreter.gas.remaining() &lt; additional_cold_cost;
        let res = context
            .host
            .sstore_skip_cold_load(target, index, value, skip_cold);
        match res {
            Ok(load) => load,
            Err(LoadError::ColdLoadSkipped) => return context.interpreter.halt_oog(),
            Err(LoadError::DBError) => return context.interpreter.halt_fatal(),
        }
    } else {
        let Some(load) = context.host.sstore(target, index, value) else {
            return context.interpreter.halt_fatal();
        };
        load
    };

    let is_istanbul = spec_id.is_enabled_in(ISTANBUL);

    // dynamic gas
    gas!(
        context.interpreter,
        context.interpreter.gas_params.sstore_dynamic_gas(
            is_istanbul,
            &state_load.data,
            state_load.is_cold
        )
    );

    // refund
    context.interpreter.gas.record_refund(
        context
            .interpreter
            .gas_params
            .sstore_refund(is_istanbul, &state_load.data),
    );
}

后面调用了 sstore_dynamic_gas 进行 动态Gas 计算,这里深入进去看下具体逻辑.
解释在注释里面.

pub fn sstore_dynamic_gas(&self, is_istanbul: bool, vals: &SStoreResult, is_cold: bool) -> u64 {
        // istanbul 升级前的 Gas 计算,历史兼容性,老链就是兼容多.
        if !is_istanbul {
            // 当前值是0,但新值不是0,相当于新建.
            // sstore_set_without_load_cost = SSTORE_SET - SSTORE_RESET = 20000-500
            if vals.is_present_zero() && !vals.is_new_zero() {
                return self.sstore_set_without_load_cost();
            // 其他的情况,包括非0写非0,非0写0
            // sstore_reset_without_cold_load_cost = SSTORE_RESET - ISTANBUL_SLOAD_GAS = 5000 - 800
            } else {
                return self.sstore_reset_without_cold_load_cost();
            }
        }

        let mut gas = 0;

        // 冷访问,第一次访问要额外 + cold_storage_cost(2100)
        if is_cold {
            gas += self.cold_storage_cost();
        }

        // 这里蛮绕的,storage 分三个状态:Original(原始值)、Present(当前值)、New(新值)
        // is_original_eq_present 判断当前值是否等于原始值,判断是否第一次修改
        // new_values_changes_present 判断新写入的值是否真的修改了现值
        // 所以这段就是本次交易中第一次修改槽位的逻辑
        if vals.new_values_changes_present() && vals.is_original_eq_present() {
            // 初始值为0,相当于新写入
            // SSTORE_SET - SSTORE_RESET = 20000 - 5000
            gas += if vals.is_original_zero() {
                self.sstore_set_without_load_cost()
            // 覆盖写入
            // SSTORE_RESET - ISTANBUL_SLOAD_GAS = 5000 - 800
            } else {
                self.sstore_reset_without_cold_load_cost()
            };
        }
        gas
    }

SSTORE 的逻辑继续往下, 到 sstore_refund,进去看下具体逻辑
这是关于 Storage 的退款部分.解释写在注释里.

pub fn sstore_refund(&self, is_istanbul: bool, vals: &SStoreResult) -> i64 {
        // sstore_clearing_slot_refund = 15000 
        let sstore_clearing_slot_refund = self.sstore_clearing_slot_refund() as i64;
        // istanbul 升级前
        if !is_istanbul {
            // 之前非0,新值0,相当于清空槽位,直接返现
            if !vals.is_present_zero() && vals.is_new_zero() {
                return sstore_clearing_slot_refund;
            }
            return 0;
        }
        // 新值和现值相等
        if vals.is_new_eq_present() {
            return 0;
        }
        // istanbul 升级后清空槽位返现要判断本次交易中初始值没有修改过
        // 防止黑客攻击,反复退款
        if vals.is_original_eq_present() && vals.is_new_zero() {
            return sstore_clearing_slot_refund;
        }
        let mut refund = 0;
        // 初始值非0
        if !vals.is_original_zero() {
            // 之前判断过现值(present)和新值(new)是否相等,所以这里新值(new)不为0
            // 也就是这里针对的是original修改为0,然后给你补偿了,但是你又重新写入不为0.
            // 我收回补偿
            if vals.is_present_zero() {
                refund -= sstore_clearing_slot_refund;
            // 就是上面说的你修改new为0,我给你补偿
            } else if vals.is_new_zero() {
                refund += sstore_clearing_slot_refund;
            }
        }
        // 原始值(original)和新值(new)相等的情况
        // 这里有个前提,无论你这次交易中,修改了多少次storage,只要你最后原始值和新值相等.
        // 就认为你没有对状态树造成影响.会给你退还set和reset费用.
        // 这两者的费用在上面的sstore_dynamic_gas最后部分.
        if vals.is_original_eq_new() {
            // 两者为0的情况
            if vals.is_original_zero() {
                refund += self.sstore_set_without_load_cost() as i64;
            // 两者不为0的情况.
            } else {
                refund += self.sstore_reset_without_cold_load_cost() as i64;
            }
        }
        refund
    }

SSTORE 的部分就讲到这里吧,后面再进行总结.

在网站中接着往下运行,碰到了 MSTOREMSTORE8.
MSTORE 我们在前面讲过了, MSTORE8MSTORE 逻辑差不多.
不同的是 MSTORE 写入的是 32 个字节,MSTORE8 写入的是单个字节.
可以自己去看下,这里就不展开了.

SLOAD

Storage 中加载数据.

虽然功能不同,但实现和SSTORE差不多.就不讲解了.

pub fn sload&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn_top!([], index, context.interpreter);
    let spec_id = context.interpreter.runtime_flag.spec_id();
    let target = context.interpreter.input.target_address();

    if spec_id.is_enabled_in(BERLIN) {
        let additional_cold_cost = context
            .interpreter
            .gas_params
            .cold_storage_additional_cost();
        let skip_cold = context.interpreter.gas.remaining() &lt; additional_cold_cost;
        let res = context.host.sload_skip_cold_load(target, *index, skip_cold);
        match res {
            Ok(storage) => {
                if storage.is_cold {
                    gas!(context.interpreter, additional_cold_cost);
                }

                *index = storage.data;
            }
            Err(LoadError::ColdLoadSkipped) => context.interpreter.halt_oog(),
            Err(LoadError::DBError) => context.interpreter.halt_fatal(),
        }
    } else {
        let Some(storage) = context.host.sload(target, *index) else {
            return context.interpreter.halt_fatal();
        };
        *index = storage.data;
    };
}

KECCAK256

对内存中的数据进行哈希.
ETH 中的哈希算法,计算地址、哈希校验、Merkle 证明都会使用到它.

网站中执行到 461 步,会碰到它.
看下指令的源码.

要注意,popn_top!([offset], top, context.interpreter)这里 top 是要 hash 数据的长度,不是具体数据.

这里根据要 hash 的数据长度来计算 Gas 消耗.
REVM 中 word 指的是 32 个字节.keccak256_per_word = 6.
这里的计算公式是$$Gas = 6 \times \lceil \frac{len}{32} \rceil$$

gas!(
    context.interpreter,
    context.interpreter.gas_params.keccak256_cost(len)
);
// crates/interpreter/src/gas/params.rs
pub fn keccak256_cost(&self, len: usize) -> u64 {
        self.get(GasId::keccak256_per_word())
            .saturating_mul(num_words(len) as u64)
    }

resize_memory!(context.interpreter, from, len) 避免数据长度不足报错.
keccak256 内部就不深入了.最终调用的 C 写的库.

// crates/interpreter/src/instructions/system.rs
pub fn keccak256&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    popn_top!([offset], top, context.interpreter);
    let len = as_usize_or_fail!(context.interpreter, top);
    gas!(
        context.interpreter,
        context.interpreter.gas_params.keccak256_cost(len)
    );
    let hash = if len == 0 {
        KECCAK_EMPTY
    } else {
        let from = as_usize_or_fail!(context.interpreter, offset);
        resize_memory!(context.interpreter, from, len);
        primitives::keccak256(context.interpreter.memory.slice_len(from, len).as_ref())
    };
    *top = hash.into();
}

CALL

用于合约间的调用.

CALL 的时候会传递 7 个参数.从栈顶自上而下

  1. gas, 传递给被调用合约的 GasLimit,最大数值是当前 Frame剩余Gas 的 63 / 64.
  2. addr, 被调用合约的地址.
  3. value 发送给被调用合约的 ETH
  4. argsOffset 传递给被调用函数的参数在内存中的 offset.
  5. argsSize 传递给被调用函数的参数的 size.
  6. retOffset 调用函数返回结果写入内存的 offset.
  7. retSize 调用函数返回结果写入内存的 size

网站中跳到700步.这里对应的源码是

(bool s1, bytes memory r1) = target.call{value: 1 wei}(
    abi.encodeWithSignature("mockFunction(uint256)", 123)
);

栈中自上而下的数据是.

0x0000000000000000000000000000000000000000000000000000000000045417(gas)
0x0000000000000000000000000fb8f3bae4005f8ee539c20e6273b2aac25e5ad2(to)
0x0000000000000000000000000000000000000000000000000000000000000001(value)
0x0000000000000000000000000000000000000000000000000000000000000138(argsOffset)
0x0000000000000000000000000000000000000000000000000000000000000024(argsSize)
0x0000000000000000000000000000000000000000000000000000000000000138(retOffset)
0x0000000000000000000000000000000000000000000000000000000000000000(retSize)

前三个比较明显直接跳过.

argsOffset 数值 138, argsSize 数值0x24(36), 从内存 0x138 的位置 读取36个字节.
7b23bd32bf000000000000000000000000000000000000000000000000000000000000007b
7b23bd32bfmockFunction(uint256)abiencode.
后面的 7b 对应的是 123.

retOffset 的数值和 argsOffset 的数值是相同的,这是为了节省 内存Gas.
且两者阶段不一样,不会发生冲突.
retSize 的数值为 0 因为不确定返回的类型,这里直接返回的 bytes,并不能确定长度.

看下 Call 的源码.

在函数开始部分,你会看到这里也会判断 is_static. 正常来说只需要在 STATICCALL 中判断.
为什么在 Call 中也判断 static 的原因是 STATICCALL 的传染性.
如果A合约 STATICCALL B合约, B 合约 CALL C合约, 在整条调用链中执行环境都是 static .即使是 CALL.

get_memory_input_and_out_ranges 获取函数参数和返回值在 memory 中的范围,如果内存不足还会顺便对内存进行扩容,扩容的部分和前面说的 MSTORE 一样.

set_action(InterpreterAction::NewFrame(FrameInput::Call(Box::new())) 涉及到 CALLCREATE 都要返回一个新建 Frame 的动作给上层进行新的调用.

处理函数在 crates/handler/src/evm.rs -> frame_run
crates/handler/src/frame.rs -> process_next_action 中.

// crates/interpreter/src/instructions/contract.rs
pub fn call&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(
    mut context: InstructionContext&lt;'_, H, WIRE>,
) {
    popn!([local_gas_limit, to, value], context.interpreter);
    let to = to.into_address();
    let local_gas_limit = u64::try_from(local_gas_limit).unwrap_or(u64::MAX);
    let has_transfer = !value.is_zero();

    if context.interpreter.runtime_flag.is_static() && has_transfer {
        context
            .interpreter
            .halt(InstructionResult::CallNotAllowedInsideStatic);
        return;
    }

    let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(context.interpreter)
    else {
        return;
    };

    let Some((gas_limit, bytecode, bytecode_hash)) =
        load_acc_and_calc_gas(&mut context, to, has_transfer, true, local_gas_limit)
    else {
        return;
    };

    // Call host to interact with target contract
    context
        .interpreter
        .bytecode
        .set_action(InterpreterAction::NewFrame(FrameInput::Call(Box::new(
            CallInputs {
                input: CallInput::SharedBuffer(input),
                gas_limit,
                target_address: to,
                caller: context.interpreter.input.target_address(),
                bytecode_address: to,
                known_bytecode: Some((bytecode_hash, bytecode)),
                value: CallValue::Transfer(value),
                scheme: CallScheme::Call,
                is_static: context.interpreter.runtime_flag.is_static(),
                return_memory_offset,
            },
        ))));
}

load_acc_and_calc_gas 加载 bytecode 并计算需要本次 CALL 需要的 Limit.
load_account_delegated_handle_error 这里不深入,是加载账户和它的委托账户,不过要处理冷、热账户加载的不同 Gas 消耗

pub fn load_acc_and_calc_gas&lt;H: Host + ?Sized>(
    context: &mut InstructionContext&lt;'_, H, impl InterpreterTypes>,
    to: Address,
    transfers_value: bool,
    create_empty_account: bool,
    stack_gas_limit: u64,
) -> Option&lt;(u64, Bytecode, B256)> {
    // 如果有涉及到转账.A转账给B X ETH
    if transfers_value {
        gas!(
            context.interpreter,
            // 9000
            context.interpreter.gas_params.transfer_value_cost(),
            None
        );
    }

    // 加载账户和它的委托账户.
    let (gas, bytecode, code_hash) =
        load_account_delegated_handle_error(context, to, transfers_value, create_empty_account)?;
    let interpreter = &mut context.interpreter;

    // 记录加载账户和它的委托账户消耗的Gas
    gas!(interpreter, gas, None);

    let interpreter = &mut context.interpreter;

    // EIP-150 升级后CALL 的 GAS 处理
    let mut gas_limit = if interpreter.runtime_flag.spec_id().is_enabled_in(TANGERINE) {
        // 这段相当于 gaslimit - gaslimit /64
        // 就是前面说的将 63/64 的 Gas 传递给CALL
        // 每次CALL之前剩下1/64,是为了避免CALL用完所有的GAS
        // 导致CALLER没有GAS处理剩下的逻辑
        let reduced_gas_limit = interpreter
            .gas_params
            .call_stipend_reduction(interpreter.gas.remaining());
        min(reduced_gas_limit, stack_gas_limit)
    } else {
        stack_gas_limit
    };
    gas!(interpreter, gas_limit, None);

    // 系统对于转账的补贴.赠送给子合约让子合约可以打个Log或者啥.
    if transfers_value {
        gas_limit = gas_limit.saturating_add(interpreter.gas_params.call_stipend());
    }

    Some((gas_limit, bytecode, code_hash))
}

RETURN

函数结束的指令.还会涉及将当前执行帧(Frame)在内存中的返回结果,返回给父调用者.

源码的实现比较简单.
output 从内存中获取要返回的数据.
set_action(InterpreterAction::new_return()) 返回 new_return action.

和其他的 Opcode 不一样,其他大部分的 Opcode 逻辑都直接在对应的函数中写完.
Return 因为涉及到 Frame 之间的调用.所以处理逻辑分开的.

//crates/interpreter/src/instructions/control.rs
pub fn ret&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    return_inner(context.interpreter, InstructionResult::Return);
}

fn return_inner(
    interpreter: &mut Interpreter&lt;impl InterpreterTypes>,
    instruction_result: InstructionResult,
) {
    popn!([offset, len], interpreter);
    let len = as_usize_or_fail!(interpreter, len);
    // Important: Offset must be ignored if len is zeros
    let mut output = Bytes::default();
    if len != 0 {
        let offset = as_usize_or_fail!(interpreter, offset);
        if !interpreter.resize_memory(offset, len) {
            return;
        }
        output = interpreter.memory.slice_len(offset, len).to_vec().into()
    }

    interpreter
        .bytecode
        .set_action(InterpreterAction::new_return(
            instruction_result,
            output,
            interpreter.gas,
        ));
}

Return 在返回后具体的处理逻辑在 crates/handler/src/evm.rs->frame_return_result.
主要调用的 return_result,直接跳转进去查看具体逻辑.

free_child_context SharedMemory部分讲过.每个 frame 被创建的时候都会创建 checkpoint,指示 子Frame 的内存从这里开始.这里是将已使用内存的长度设置为 checkpoint.相当于释放了之前 子Frame 占用的内存.
这里只是告诉 SharedMemory 我不再需要那块内存,但是数据还没清理.

match core::mem::replace(ctx.error(), Ok(())) 这段是判断在处理交易过程中,有没有发生错误.

interpreter.return_data.set_buffer(outcome.result.output) 这里就是将 return 返回的数据保存到 interpreter.return_data 中.这里的 interpreter.return_data父frame 的.

let target_len = min(mem_length, returned_len); 这里有个注意的点,记得前面讲 CALL 指令的时候,retSize0 吗? 如果 retSize 为 0 , 即mem_length, target_len 也就为0.
retSize0 的情况一般是返回类型是返回动态类型,例如bytes之类的.
这些类型并不能提前知道返回的长度,所以直接设置为 0.
如果 retSize = 0 会导致这段中的 [..target_len][..0],实际并没有拷贝数据到 Frame中专门保存返回数据的内存中.
至于什么时候才会拷贝动态数据的返回类型,后面再说.

if ins_result.is_ok_or_revert() {
    interpreter.gas.erase_cost(out_gas.remaining());
    interpreter
        .memory
        .set(mem_start, &interpreter.return_data.buffer()[..target_len]);
                }
// crates/interpreter/src/gas.rs
pub fn erase_cost(&mut self, returned: u64) {
    self.remaining += returned;
}

interpreter.gas.erase_cost(out_gas.remaining()) 这里是返还未使用的Gas.虽然用的 erase,但是实际是加.看起来误导人所以说下.

create 的部分和 call 差不多,感兴趣的自己看下

// crates/handler/src/evm.rs
fn frame_return_result(
        &mut self,
        result: &lt;Self::Frame as FrameTr>::FrameResult,
    ) -> Result&lt;Option&lt;&lt;Self::Frame as FrameTr>::FrameResult>, ContextDbError&lt;Self::Context>> {
        if self.frame_stack.get().is_finished() {
            self.frame_stack.pop();
        }
        if self.frame_stack.index().is_none() {
            return Ok(Some(result));
        }
        self.frame_stack
            .get()
            .return_result::&lt;_, ContextDbError&lt;Self::Context>>(&mut self.ctx, result)?;
        Ok(None)
    }
// crates/handler/src/frame.rs
pub fn return_result&lt;CTX: ContextTr, ERROR: From&lt;ContextTrDbError&lt;CTX>> + FromStringError>(
        &mut self,
        ctx: &mut CTX,
        result: FrameResult,
    ) -> Result&lt;(), ERROR> {
        self.interpreter.memory.free_child_context();
        match core::mem::replace(ctx.error(), Ok(())) {
            Err(ContextError::Db(e)) => return Err(e.into()),
            Err(ContextError::Custom(e)) => return Err(ERROR::from_string(e)),
            Ok(_) => (),
        }

        // Insert result to the top frame.
        match result {
            FrameResult::Call(outcome) => {
                let out_gas = outcome.gas();
                let ins_result = *outcome.instruction_result();
                let returned_len = outcome.result.output.len();

                let interpreter = &mut self.interpreter;
                let mem_length = outcome.memory_length();
                let mem_start = outcome.memory_start();
                interpreter.return_data.set_buffer(outcome.result.output);

                let target_len = min(mem_length, returned_len);

                if ins_result == InstructionResult::FatalExternalError {
                    panic!("Fatal external error in insert_call_outcome");
                }

                let item = if ins_result.is_ok() {
                    U256::from(1)
                } else {
                    U256::ZERO
                };
                let _ = interpreter.stack.push(item);

                if ins_result.is_ok_or_revert() {
                    interpreter.gas.erase_cost(out_gas.remaining());
                    interpreter
                        .memory
                        .set(mem_start, &interpreter.return_data.buffer()[..target_len]);
                }

                if ins_result.is_ok() {
                    interpreter.gas.record_refund(out_gas.refunded());
                }
            }
            FrameResult::Create(outcome) => {
                let instruction_result = *outcome.instruction_result();
                let interpreter = &mut self.interpreter;

                if instruction_result == InstructionResult::Revert {
                    // Save data to return data buffer if the create reverted
                    interpreter
                        .return_data
                        .set_buffer(outcome.output().to_owned());
                } else {
                    // Otherwise clear it. Note that RETURN opcode should abort.
                    interpreter.return_data.clear();
                };

                assert_ne!(
                    instruction_result,
                    InstructionResult::FatalExternalError,
                    "Fatal external error in insert_eofcreate_outcome"
                );

                let this_gas = &mut interpreter.gas;
                if instruction_result.is_ok_or_revert() {
                    this_gas.erase_cost(outcome.gas().remaining());
                }

                let stack_item = if instruction_result.is_ok() {
                    this_gas.record_refund(outcome.gas().refunded());
                    outcome.address.unwrap_or_default().into_word().into()
                } else {
                    U256::ZERO
                };

                // Safe to push without stack limit check
                let _ = interpreter.stack.push(stack_item);
            }
        }

        Ok(())
    }

在网站中第一个 RETURN 指令在 934 步.
两个参数,分别是返回数据在内存中的 offse t和 size.组合起来对应的数据是 7c.

RETURNDATASIZE

将调用合约后 返回数据 的长度压入栈.

在上面我们说了,返回数据会被拷贝到 context.interpreter.return_data 中.
这里直接返回的这部分的数据长度,没从栈中获取.
因为 子Framereturn 的时候已经被释放,数据不存在了,这就是为什么拷贝到 return_data 的原因.

// crates/interpreter/src/instructions/system.rs
pub fn returndatasize&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    check!(context.interpreter, BYZANTIUM);
    push!(
        context.interpreter,
        U256::from(context.interpreter.return_data.buffer().len())
    );
}

在网站中执行 939 步就是对应的指令.

RETURNDATACOPY

拷贝合约调用返回数据到指定内存位置.

as_usize_saturated! 宏,将 U256 类型的值转换为 usize 类型,如果数值过大设置为最大值.
其他的源码比较明显,就不讲解了.

看下源码的功能,会感觉 RETURNDATACOPYRETURNDATASIZE 写到 return_result 里面也行.没必要加个 interpreter.return_data 再拷贝.
那为什么还要分出两个指令呢.

  • 对于返回的动态数据,我并不一定需要所有的数据,我可能只需要部分.分成两部分我可以按需拷贝数据.
  • 数据写入内存可能会涉及内存扩容费用,调用部分合约,返回的数据过长时,我可以丢弃,避免内存扩容消耗大量Gas

网站中执行到 968 步,就是对应的指令了.

// crates/interpreter/src/instructions/system.rs
pub fn returndatacopy&lt;WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    check!(context.interpreter, BYZANTIUM);
    popn!([memory_offset, offset, len], context.interpreter);

    let len = as_usize_or_fail!(context.interpreter, len);
    let data_offset = as_usize_saturated!(offset);

    let data_end = data_offset.saturating_add(len);
    if data_end > context.interpreter.return_data.buffer().len() {
        context.interpreter.halt(InstructionResult::OutOfOffset);
        return;
    }

    let Some(memory_offset) = copy_cost_and_memory_resize(context.interpreter, memory_offset, len)
    else {
        return;
    };

    context.interpreter.memory.set_data(
        memory_offset,
        data_offset,
        len,
        context.interpreter.return_data.buffer(),
    );
}

STATICCALL

合约间调用,和 CALL 功能差不多,但是它不能修改数据.

讲下和 CALL 不同的地方.
STATICCALL 不能传递发送 Value,参数只有相比 CALL 少了 Value.

因为没有 Value,也不能修改数据.
load_acc_and_calc_gas调用时, transfers_valuecreate_empty_account 直接传递 false.

CallInputsis_static 直接传递 true. Value 传递 0.

其他的和 CALL 差不多,就不解释了.

在网站中对应的步数是 1123.

pub fn static_call&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(
    mut context: InstructionContext&lt;'_, H, WIRE>,
) {
    check!(context.interpreter, BYZANTIUM);
    popn!([local_gas_limit, to], context.interpreter);
    let to = Address::from_word(B256::from(to));
    // Max gas limit is not possible in real ethereum situation.
    let local_gas_limit = u64::try_from(local_gas_limit).unwrap_or(u64::MAX);

    let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(context.interpreter)
    else {
        return;
    };

    let Some((gas_limit, bytecode, bytecode_hash)) =
        load_acc_and_calc_gas(&mut context, to, false, false, local_gas_limit)
    else {
        return;
    };

    // Call host to interact with target contract
    context
        .interpreter
        .bytecode
        .set_action(InterpreterAction::NewFrame(FrameInput::Call(Box::new(
            CallInputs {
                input: CallInput::SharedBuffer(input),
                gas_limit,
                target_address: to,
                caller: context.interpreter.input.target_address(),
                bytecode_address: to,
                known_bytecode: Some((bytecode_hash, bytecode)),
                value: CallValue::Transfer(U256::ZERO),
                scheme: CallScheme::StaticCall,
                is_static: true,
                return_memory_offset,
            },
        ))));
}

DELEGATECALL

合约间调用,和 CALL 功能差不多,但是它的上下文是 CALLER.
它读取修改的是调用发起者的Storage,而不是被调用者的Storage.

讲下和 CALL 不同的地方.
因为上下文是 CALLER 的,所以也不能发送 Value.

load_acc_and_calc_gas调用时, 传入的to 是被调用者的合约地址,而不是CALLER.
所以transfers_valuecreate_empty_account 直接传递 false.

最关键的部分就是 CallInputstarget_address, caller, value.

target_address 指示要读取修改Storage 的合约.
CALL 中, target_addressto, 比较容易理解.

在这里是 context.interpreter.input.target_address().
interpreter.input.target_address指的是 当前Frametarget_address,也就是 当前Frame 的要读取修改Storage的合约地址.
相当于保持不变,你在 当前Frame 读取修改的谁,在 子Frame 中也读取修改的谁.

caller 指示调用发起者.
CALL 中, caller 是 context.interpreter.input.target_address().
要注意这里用的是target_address.
如果是 当前Frame 是由 CALL 生成的容易理解,如果是 DELEGATECALL 生成的就不同了.
例如 A DELEGATECALL B, B CALL C, B 的 target_addressA, 那 Ccaller 实际是 A, 而不是 B.

DELEGATECALL 中, callercontext.interpreter.input.caller_address(), 也就是它直接传递 当前FrameCALLER,没进行修改.
例如 EOA 通过 RPC 调用 合约A,A DELEGATE B,Bcaller 应该是 EOA,而不是 A.
B CALL C, 因为 caller 是 context.interpreter.input.target_address(),也就是A,所以C的callerA

这段很绕,建议你们多看几遍,我自己看了几遍还是觉得绕.
最好画个图.我在纸上画了懒得在这里画.
分清楚 当前Frame 的 callertarget_address, 传递给 子Frame 时的 callertarget_address.
DELEGATECALL 还是 CALL.

在网站上对应的步数是 1490 步.

pub fn delegate_call&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(
    mut context: InstructionContext&lt;'_, H, WIRE>,
) {
    check!(context.interpreter, HOMESTEAD);
    popn!([local_gas_limit, to], context.interpreter);
    let to = Address::from_word(B256::from(to));
    // Max gas limit is not possible in real ethereum situation.
    let local_gas_limit = u64::try_from(local_gas_limit).unwrap_or(u64::MAX);

    let Some((input, return_memory_offset)) = get_memory_input_and_out_ranges(context.interpreter)
    else {
        return;
    };

    let Some((gas_limit, bytecode, bytecode_hash)) =
        load_acc_and_calc_gas(&mut context, to, false, false, local_gas_limit)
    else {
        return;
    };

    // Call host to interact with target contract
    context
        .interpreter
        .bytecode
        .set_action(InterpreterAction::NewFrame(FrameInput::Call(Box::new(
            CallInputs {
                input: CallInput::SharedBuffer(input),
                gas_limit,
                target_address: context.interpreter.input.target_address(),
                caller: context.interpreter.input.caller_address(),
                bytecode_address: to,
                known_bytecode: Some((bytecode_hash, bytecode)),
                value: CallValue::Apparent(context.interpreter.input.call_value()),
                scheme: CallScheme::DelegateCall,
                is_static: context.interpreter.runtime_flag.is_static(),
                return_memory_offset,
            },
        ))));
}

CREATE / CREATE2

CREATE 和 CREATE2 都是使用给定的代码创建合约.
两个的区别主要是地址的计算不同.
CREATE2 生成的地址是确定的,可以提前预测.

地址计算:
CREATE2: keccak256(0xff + sender_address + salt + keccak256(init_code))[12:]
CREATE: keccak256(rlp([sender_address, sender_nonce]))[12:]

两者调用的同一个函数,用 IS_CREATE2 参数区别是否 CREATE2.

require_non_staticcall!(context.interpreter) 判断是否 STITICCALL.
STITICCALL 不允许修改状态,自然也不允许创建合约.

CREATE 指令接受 3个函数,CREATE2 接受4个参数.
尾部多了一个 salt,用于生成地址.

前3个参数是 value, offset, len

  • value 发送给创建后的合约的 ETH
  • offset 合约的 initcode 在内存中的 offset.
  • len 合约的 initcode 在内存中的 len.

if len != 0 {xxx} 这段是读取 init code 到内存并计算要消耗的Gas.
Gas 按每 32字节 进行计算. 每 32字节 消耗 2 GasLimit.

刚才扣除的是 inticode 的Gas.

接下来还要扣除 Create 这个操作的基础Gas.
在上面的公式可以看到,CREATE 的地址计算参数类型都是静态参数.
所以 Gas 消耗也是固定消耗,create_cost = 32000.

CREATE2 的计算公式需要 saltinit_code. salt 是静态类型, init_code 是动态类型.
所以 Gas 消耗是计算出来的.在 32000 的基础上再加上 keccak256(init_code.len)的消耗.
32000 + 6 * ( len / 32)

call_stipend_reduction 之前说过, CALLCREATE 的时候必须要留 1/64 给 当前Frame处理后续的操作.

你会发现这里并没有计算地址和其他的合约创建操作.
CALL 一样,需要返回上层创建新的 Frame 进行操作.

在网站中对应的步数是 1811 步.

pub fn create&lt;WIRE: InterpreterTypes, const IS_CREATE2: bool, H: Host + ?Sized>(
    context: InstructionContext&lt;'_, H, WIRE>,
) {
    require_non_staticcall!(context.interpreter);

    // EIP-1014: Skinny CREATE2
    if IS_CREATE2 {
        check!(context.interpreter, PETERSBURG);
    }

    popn!([value, code_offset, len], context.interpreter);
    let len = as_usize_or_fail!(context.interpreter, len);

    let mut code = Bytes::new();
    if len != 0 {
        if context
            .interpreter
            .runtime_flag
            .spec_id()
            .is_enabled_in(SpecId::SHANGHAI)
        {
            if len > context.host.max_initcode_size() {
                context
                    .interpreter
                    .halt(InstructionResult::CreateInitCodeSizeLimit);
                return;
            }
            gas!(
                context.interpreter,
                context.interpreter.gas_params.initcode_cost(len)
            );
        }

        let code_offset = as_usize_or_fail!(context.interpreter, code_offset);
        resize_memory!(context.interpreter, code_offset, len);

        code = Bytes::copy_from_slice(
            context
                .interpreter
                .memory
                .slice_len(code_offset, len)
                .as_ref(),
        );
    }

    let scheme = if IS_CREATE2 {
        popn!([salt], context.interpreter);
        gas!(
            context.interpreter,
            context.interpreter.gas_params.create2_cost(len)
        );
        CreateScheme::Create2 { salt }
    } else {
        gas!(
            context.interpreter,
            context.interpreter.gas_params.create_cost()
        );
        CreateScheme::Create
    };

    let mut gas_limit = context.interpreter.gas.remaining();

    if context
        .interpreter
        .runtime_flag
        .spec_id()
        .is_enabled_in(SpecId::TANGERINE)
    {
        gas_limit = context
            .interpreter
            .gas_params
            .call_stipend_reduction(gas_limit);
    }
    gas!(context.interpreter, gas_limit);

    context
        .interpreter
        .bytecode
        .set_action(InterpreterAction::NewFrame(FrameInput::Create(Box::new(
            CreateInputs::new(
                context.interpreter.input.target_address(),
                scheme,
                value,
                code,
                gas_limit,
            ),
        ))));
}

下一步在crates/handler/src/frame.rs - > process_next_action中.
然后 match 这里匹配了 NewFrame, 再执行一遍新建 Frame, 走的 make_create_frame.
这部分在前面的流程讲过了,不讲了.

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

在上面新建的 Frame 执行完成后,又回到 process_next_action.
这次执行到后面的 return_create.
这部分不讲了,感兴趣自己看下.

pub fn return_create&lt;JOURNAL: JournalTr>(
    journal: &mut JOURNAL,
    checkpoint: JournalCheckpoint,
    interpreter_result: &mut InterpreterResult,
    address: Address,
    max_code_size: usize,
    is_eip3541_disabled: bool,
    spec_id: SpecId,
) {
    if !interpreter_result.result.is_ok() {
        journal.checkpoint_revert(checkpoint);
        return;
    }
    if !is_eip3541_disabled
        && spec_id.is_enabled_in(LONDON)
        && interpreter_result.output.first() == Some(&0xEF)
    {
        journal.checkpoint_revert(checkpoint);
        interpreter_result.result = InstructionResult::CreateContractStartingWithEF;
        return;
    }

    if spec_id.is_enabled_in(SPURIOUS_DRAGON) && interpreter_result.output.len() > max_code_size {
        journal.checkpoint_revert(checkpoint);
        interpreter_result.result = InstructionResult::CreateContractSizeLimit;
        return;
    }
    let gas_for_code = interpreter_result.output.len() as u64 * gas::CODEDEPOSIT;
    if !interpreter_result.gas.record_cost(gas_for_code) {
        if spec_id.is_enabled_in(HOMESTEAD) {
            journal.checkpoint_revert(checkpoint);
            interpreter_result.result = InstructionResult::OutOfGas;
            return;
        } else {
            interpreter_result.output = Bytes::new();
        }
    }
    journal.checkpoint_commit();
    let bytecode = Bytecode::new_legacy(interpreter_result.output.clone());
    journal.set_code(address, bytecode);
    interpreter_result.result = InstructionResult::Return;
}

LOG

用于广播链上事件.

逻辑还是比较清晰的,没啥要特别注意的.不讲解了,自己看看.

pub fn log&lt;const N: usize, H: Host + ?Sized>(
    context: InstructionContext&lt;'_, H, impl InterpreterTypes>,
) {
    require_non_staticcall!(context.interpreter);

    popn!([offset, len], context.interpreter);
    let len = as_usize_or_fail!(context.interpreter, len);
    gas!(
        context.interpreter,
        context.interpreter.gas_params.log_cost(N as u8, len as u64)
    );
    let data = if len == 0 {
        Bytes::new()
    } else {
        let offset = as_usize_or_fail!(context.interpreter, offset);
        // Resize memory to fit the data
        resize_memory!(context.interpreter, offset, len);
        Bytes::copy_from_slice(context.interpreter.memory.slice_len(offset, len).as_ref())
    };
    let Some(topics) = context.interpreter.stack.popn::&lt;N>() else {
        context.interpreter.halt_underflow();
        return;
    };

    let log = Log {
        address: context.interpreter.input.target_address(),
        data: LogData::new(topics.into_iter().map(B256::from).collect(), data)
            .expect("LogData should have &lt;=4 topics"),
    };

    context.host.log(log);
}

TSTORE / TLOAD

用于保存和读取瞬态存储.

都是直接调用的 context.host.tstore,这部分内容在前面都讲过,这里也略过.

pub fn tstore&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    check!(context.interpreter, CANCUN);
    require_non_staticcall!(context.interpreter);
    popn!([index, value], context.interpreter);

    context
        .host
        .tstore(context.interpreter.input.target_address(), index, value);
}
pub fn tload&lt;WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext&lt;'_, H, WIRE>) {
    check!(context.interpreter, CANCUN);
    popn_top!([], index, context.interpreter);

    *index = context
        .host
        .tload(context.interpreter.input.target_address(), *index);
}

结束

磨磨蹭蹭终于把文章写完了,写得不是很详细将就着看吧.
写这几篇文章的时候是一边看一边学习理解,看的是源码,还没从整体架构来了解.
讲的也都是正常流程,没深入讲异常处理的部分.
都有点像是在写文档和做翻译了.
后面闲下来继续从稍高点的维度来写写.

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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