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

Call 和 Create 都会创建一个 Frame.这里就是对应的 Frame.
Frame,实际可能调用的都是同一个地址的合约.Opcode 列表,也就是调用的合约的字节码对应的Opcode列表.左边是 PC, 右边是具体OpCode.Stack Interpreter 执行过程中的栈数据.Memory Interpreter 执行过程中的内存数据.Storage 合约的Storage数据,这里的数据我暂时没写.ReturnData 每一次合约 Call 后的返回数据.index 和 界面3的index 是相反的,这里的 index是从顶开始算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,在这位置之后的空间你可以使用.
0x60-0x7f 零值空间,不被使用,永远为0.当然,你在合约的 solidity 源码和 REVM 代码里都找不到关于这些的定义.
因为这部分写在编译器里了.
在将 solidity 源码编译成字节码的过程中,会自动进行内存指针的管理并写到字节码中.
REVM 只负责执行编译好的字节码.
在讲其他 Opcode 前,先讲下几个宏
功能很简单,就是从 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>() })
}
也是从 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() };
};
}
如果有需要的话会调整内存的大小.
$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消耗.
// 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
}
看下代码逻辑:
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>());
}
从内存中加载数据.
逻辑比较简单,就不解释了.
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()
}
清晰明了,计算出 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())
);
}
比较栈顶两个元素的大小.
为了方便理解,后面栈顶用 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);
}
条件跳转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<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
popn!([target, cond], context.interpreter);
if !cond.is_zero() {
jump_inner(context.interpreter, target);
}
}
fn jump_inner<WIRE: InterpreterTypes>(interpreter: &mut Interpreter<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 < self.len && unsafe { *self.table_ptr.add(pc >> 3) & (1 << (pc & 7)) != 0 }
}
offset_ptr 保存的是栈顶指针.
let offset = as_usize_saturated!(offset_ptr) 这里传入的是指针,但会自动解引用获取栈顶的元素,并转成 usize. offset 保存的是要读取 calldata 的偏移位置.
let count = 32.min(input_len - offset); 默认读取32字节,如果剩余不足则读取剩余的.
判断 input 类型
count 字节的数据到 word 中.*offset_ptr = word.into() 将 word 的数据写入到栈顶.0x769c02d500000000000000000000000000000000000000000000000000000000.// crates/interpreter/src/instructions/system.rs
pub fn calldataload<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, 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 < 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();
}
右移操作.将数据向右移动 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<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
check!(context.interpreter, CONSTANTINOPLE);
popn_top!([op1], op2, context.interpreter);
let shift = as_usize_saturated!(op1);
*op2 = if shift < 256 {
*op2 >> shift
} else {
U256::ZERO
}
}
一直执行到17步,前面那几步是为了找到 0x769c02d5(runFullOpcodeSuite) 函数对应的PC位置.
这里开始进行跳转.
跳转后指定函数后,还会进行一系列的检查和加载数据.
加载函数的参数、检测payable等等.我们的任务不是学习反编译.所以这部分都跳过.
直接跳到 104 步,这里是函数主体开始的地方.
将 target_address 地址入栈.
如果你去看网站对应的 solidity 源码,你要搞清楚.
这里 ADDRESS 出现是因为 this,而不是因为address(this) 中的 address.
solidity 中 address 只是类型转换. this 是获取 target_address.
// crates/interpreter/src/instructions/system.rs
pub fn address<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
push!(
context.interpreter,
context
.interpreter
.input
.target_address()
.into_word()
.into()
);
}
将 Transaction 发送时带的 Value 压入栈.
网站发送的交易我发送了 0.00001 ETH(10000000000000 WEI, 0x9184e72a000).
执行108步,将 0x9184e72a000 压入栈.
// crates/interpreter/src/instructions/system.rs
pub fn callvalue<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
push!(context.interpreter, context.interpreter.input.call_value());
}
将 Transacton 发送时带的 GasPrice 压入栈.
真实交易的 GasPrice 是2032443413 Wei
网站上112步模拟的交易入栈的是 0x79aadd5e(2041240926)
是因为模拟的时候可能 Block 的 Basefee 和 Tx 的 PriorityFee 会有不同.
// crates/interpreter/src/instructions/tx_info.rs
pub fn gasprice<WIRE: InterpreterTypes, H: Host + ?Sized>(
context: InstructionContext<'_, H, WIRE>,
) {
push!(context.interpreter, context.host.effective_gas_price());
}
将两个数相加并对另一个数取模
源码中是将 msg.value 和 函数参数进行相加,并对 100 就行取模.
网站中拖到 125 步. 此时栈顶自上而下是
0x0000000000000000000000000000000000000000000000000000000000000001(1)0x000000000000000000000000000000000000000000000000000009184e72a000(10000000000000)0x0000000000000000000000000000000000000000000000000000000000000064(100)0x0000000000000000000000000000000000000000000000000000000000000001.//crates/interpreter/src/instructions/arithmetic.rs
pub fn addmod<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
popn_top!([op1, op2], op3, context.interpreter);
*op3 = op1.add_mod(op2, *op3)
}
执行到 162 步会遇到 AND 操作.
比较简单就不解释了
// crates/interpreter/src/instructions/bitwise.rs
pub fn bitand<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
popn_top!([op1], op2, context.interpreter);
*op2 = op1 & *op2;
}
将值写入存储槽.
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<WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext<'_, 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() <= 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() < 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 写入的是单个字节.
可以自己去看下,这里就不展开了.
从Storage 中加载数据.
虽然功能不同,但实现和SSTORE差不多.就不讲解了.
pub fn sload<WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext<'_, 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() < 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;
};
}
对内存中的数据进行哈希.
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<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, 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 的时候会传递 7 个参数.从栈顶自上而下
gas, 传递给被调用合约的 GasLimit,最大数值是当前 Frame剩余Gas 的 63 / 64.addr, 被调用合约的地址.value 发送给被调用合约的 ETHargsOffset 传递给被调用函数的参数在内存中的 offset.argsSize 传递给被调用函数的参数的 size.retOffset 调用函数返回结果写入内存的 offset.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<WIRE: InterpreterTypes, H: Host + ?Sized>(
mut context: InstructionContext<'_, 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<H: Host + ?Sized>(
context: &mut InstructionContext<'_, H, impl InterpreterTypes>,
to: Address,
transfers_value: bool,
create_empty_account: bool,
stack_gas_limit: u64,
) -> Option<(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))
}
函数结束的指令.还会涉及将当前执行帧(Frame)在内存中的返回结果,返回给父调用者.
源码的实现比较简单.
output 从内存中获取要返回的数据.
set_action(InterpreterAction::new_return()) 返回 new_return action.
和其他的 Opcode 不一样,其他大部分的 Opcode 逻辑都直接在对应的函数中写完.
Return 因为涉及到 Frame 之间的调用.所以处理逻辑分开的.
//crates/interpreter/src/instructions/control.rs
pub fn ret<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
return_inner(context.interpreter, InstructionResult::Return);
}
fn return_inner(
interpreter: &mut Interpreter<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: <Self::Frame as FrameTr>::FrameResult,
) -> Result<Option<<Self::Frame as FrameTr>::FrameResult>, ContextDbError<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::<_, ContextDbError<Self::Context>>(&mut self.ctx, result)?;
Ok(None)
}
// crates/handler/src/frame.rs
pub fn return_result<CTX: ContextTr, ERROR: From<ContextTrDbError<CTX>> + FromStringError>(
&mut self,
ctx: &mut CTX,
result: FrameResult,
) -> Result<(), 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.
将调用合约后 返回数据 的长度压入栈.
在上面我们说了,返回数据会被拷贝到 context.interpreter.return_data 中.
这里直接返回的这部分的数据长度,没从栈中获取.
因为 子Frame 在 return 的时候已经被释放,数据不存在了,这就是为什么拷贝到 return_data 的原因.
// crates/interpreter/src/instructions/system.rs
pub fn returndatasize<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, H, WIRE>) {
check!(context.interpreter, BYZANTIUM);
push!(
context.interpreter,
U256::from(context.interpreter.return_data.buffer().len())
);
}
在网站中执行 939 步就是对应的指令.
拷贝合约调用返回数据到指定内存位置.
as_usize_saturated! 宏,将 U256 类型的值转换为 usize 类型,如果数值过大设置为最大值.
其他的源码比较明显,就不讲解了.
看下源码的功能,会感觉 RETURNDATACOPY 和 RETURNDATASIZE 写到 return_result 里面也行.没必要加个 interpreter.return_data 再拷贝.
那为什么还要分出两个指令呢.
网站中执行到 968 步,就是对应的指令了.
// crates/interpreter/src/instructions/system.rs
pub fn returndatacopy<WIRE: InterpreterTypes, H: ?Sized>(context: InstructionContext<'_, 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(),
);
}
合约间调用,和 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<WIRE: InterpreterTypes, H: Host + ?Sized>(
mut context: InstructionContext<'_, 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,
},
))));
}
合约间调用,和 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<WIRE: InterpreterTypes, H: Host + ?Sized>(
mut context: InstructionContext<'_, 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 都是使用给定的代码创建合约.
两个的区别主要是地址的计算不同.
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<WIRE: InterpreterTypes, const IS_CREATE2: bool, H: Host + ?Sized>(
context: InstructionContext<'_, 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<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;
}
用于广播链上事件.
逻辑还是比较清晰的,没啥要特别注意的.不讲解了,自己看看.
pub fn log<const N: usize, H: Host + ?Sized>(
context: InstructionContext<'_, 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::<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 <=4 topics"),
};
context.host.log(log);
}
用于保存和读取瞬态存储.
都是直接调用的 context.host.tstore,这部分内容在前面都讲过,这里也略过.
pub fn tstore<WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext<'_, 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<WIRE: InterpreterTypes, H: Host + ?Sized>(context: InstructionContext<'_, H, WIRE>) {
check!(context.interpreter, CANCUN);
popn_top!([], index, context.interpreter);
*index = context
.host
.tload(context.interpreter.input.target_address(), *index);
}
磨磨蹭蹭终于把文章写完了,写得不是很详细将就着看吧.
写这几篇文章的时候是一边看一边学习理解,看的是源码,还没从整体架构来了解.
讲的也都是正常流程,没深入讲异常处理的部分.
都有点像是在写文档和做翻译了.
后面闲下来继续从稍高点的维度来写写.
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!