区块链 区块链技术 比特币公众号手机端

REVM源码阅读- Interpreter(2)

liumuhui 7个月前 (03-20) 阅读数 267 #区块链
文章标签 EVMRevmEthereum

前言

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

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

网站介绍

website.png

  1. 每次 Call 和 Create 都会创建一个 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 0x80、PUSH1 0x40、MSTORE.
这三个 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 弹出两个元素,赋值到 offset、value.

// 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

清晰明了,计算出 input 的 len 并 push 到 stack 中.
执行到 第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>. jumpdest 的 pc 对应的第几位(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 压入栈.

真实交易的 GasPrice 是2032443413 Wei
网站上112步模拟的交易入栈的是 0x79aadd5e(2041240926)
是因为模拟的时候可能 Block 的 Basefee 和 Tx 的 PriorityFee 会有不同.

// 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概念, Address 和 Storage 第一次访问属于 冷访问, 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 的部分就讲到这里吧,后面再进行总结.

在网站中接着往下运行,碰到了 MSTORE 和 MSTORE8.
MSTORE 我们在前面讲过了, MSTORE8 和 MSTORE 逻辑差不多.
不同的是 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
7b23bd32bf 是 mockFunction(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())) 涉及到 CALL 和 CREATE 都要返回一个新建 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 指令的时候,retSize为 0 吗? 如果 retSize 为 0 , 即mem_length, target_len 也就为0.
retSize为 0 的情况一般是返回类型是返回动态类型,例如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 中.
这里直接返回的这部分的数据长度,没从栈中获取.
因为 子Frame 在 return 的时候已经被释放,数据不存在了,这就是为什么拷贝到 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 类型,如果数值过大设置为最大值.
其他的源码比较明显,就不讲解了.

看下源码的功能,会感觉 RETURNDATACOPY 和 RETURNDATASIZE 写到 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_value 和 create_empty_account 直接传递 false.

CallInputs 中 is_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_value 和 create_empty_account 直接传递 false.

最关键的部分就是 CallInputs 中 target_address, caller, value.

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

在这里是 context.interpreter.input.target_address().
interpreter.input.target_address指的是 当前Frame 的 target_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_address 是 A, 那 C 的 caller 实际是 A, 而不是 B.

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

这段很绕,建议你们多看几遍,我自己看了几遍还是觉得绕.
最好画个图.我在纸上画了懒得在这里画.
分清楚 当前Frame 的 caller、target_address, 传递给 子Frame 时的 caller、target_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 的计算公式需要 salt 和 init_code. salt 是静态类型, init_code 是动态类型.
所以 Gas 消耗是计算出来的.在 32000 的基础上再加上 keccak256(init_code.len)的消耗.
32000 + 6 * ( len / 32)

call_stipend_reduction 之前说过, CALL 和 CREATE 的时候必须要留 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);
}

结束

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

版权声明

本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门