Hello World

About the EVM Part III

13.Return指令

🚀 1. RETURN 指令

✅ 功能

终止执行,并返回内存中一段数据作为调用结果。

🔍 实现

源码路径:go-ethereum/core/vm/instructions.go

func opReturn(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) {
    offset, size := stack.pop(), stack.pop()
    ret := contract.GetMem(int64(offset.Uint64()), int64(size.Uint64()))

    return ret, nil
}
📦 Gas 消耗
func memoryGasCost(memSize uint64) uint64 {
    // linear + quadratic cost function
    // linear: memSize * 3
    // quadratic: (memSize^2) / 512
}
💡 特点

🚨 2. REVERT 指令

✅ 功能

回滚状态,同时返回错误数据(不会消耗所有 gas)。

🔍 实现
func opRevert(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) {
    offset, size := stack.pop(), stack.pop()
    ret := contract.GetMem(int64(offset.Uint64()), int64(size.Uint64()))

    evm.interpreter.returnData = ret
    return ret, ErrExecutionReverted
}
📦 Gas 消耗
💡 特点

💣 3. SELFDESTRUCT 指令(旧称 SUICIDE)

✅ 功能

销毁当前合约,并将余额发送给指定地址。

🔍 实现
func opSelfDestruct(pc *uint64, evm *EVM, contract *Contract, stack *Stack) ([]byte, error) {
    beneficiary := common.Address(stack.pop().Bytes20())

    // handle refund and deletion
    evm.StateDB.SelfDestruct(contract.Address())

    // transfer balance to beneficiary
    evm.Transfer(contract.Gas, contract.Address(), beneficiary, contract.Balance())

    contract.SelfDestruct = true
    return nil, nil
}
📦 Gas 消耗
// In gas_table.go (EIP150):
SelfDestruct:         5000,
SelfDestructRefund:  24000,
💡 特点

🔁 总结对比表

指令名功能描述返回数据Gas 行为出现版本
RETURN正常返回执行结果memory + 0初始版本
REVERT回滚并返回错误信息memory + 0Byzantium (EIP-140)
SELFDESTRUCT销毁合约,发送余额高消耗+可能 refund初始版本(EIP-6 优化)

14.CALL、DELEGATECALL、STATICCALL

📘 总览

指令名是否改变调用者是否转移 msg.value是否可变更 storage
CALL✅ 改变 msg.sender✅ 支持转账✅ 支持状态更改
DELEGATECALL❌ 保持调用上下文❌ 不支持转账✅ 支持状态更改
STATICCALL❌ 保持调用上下文❌ 不支持转账❌ 禁止状态更改

✅ 1. opCall —— 普通合约调用

核心逻辑
ret, returnGas, err := interpreter.evm.Call(
    scope.Contract.Address(), // caller
    toAddr,                   // callee
    args,                     // calldata
    gas,                      // gas
    &value,                   // msg.value
)
实际调用函数:
func (evm *EVM) Callcode(caller Address, addr Address, input []byte, gas uint64, value *big.Int) ([]byte, uint64, error)

实现入口:

core/vm/evm.go

func (evm *EVM) Call(...) {
    return evm.call(...) // 里面处理合约创建、状态快照、gas 计算、revert 回滚等
}

✅ 2. opDelegateCall —— 保留 caller/value 的“委托调用”

核心逻辑
ret, returnGas, err := interpreter.evm.DelegateCall(
    scope.Contract.Caller(), // 委托原始 caller
    scope.Contract.Address(), // 委托当前调用合约
    toAddr,                   // 被委托的目标合约
    args, gas,
    scope.Contract.value,     // 保留 value
)
实际函数:
func (evm *EVM) DelegateCall(caller, address Address, codeAddr Address, input []byte, gas uint64, value *big.Int) ([]byte, uint64, error)

call.go 中构造新的 Contract 时,会设置:


✅ 3. opStaticCall —— 只读调用

核心逻辑
ret, returnGas, err := interpreter.evm.StaticCall(
    scope.Contract.Address(), // caller
    toAddr,                   // callee
    args,
    gas,
)
实际函数:
func (evm *EVM) StaticCall(caller, addr Address, input []byte, gas uint64) ([]byte, uint64, error)

StaticCall 会在创建子合约时加上只读标记 contract.SetReadOnly(true),任何写状态的行为将触发 ErrWriteProtection 错误。


⛽️ 四、Gas 消耗机制

Gas 在 EVM 中高度复杂,调用相关的 gas 计算规则如下(均实现于 core/vm/gas_table.gocore/vm/gas.go):

1. 通用机制

在这几条指令中,gas 的计算由 interpreter.evm.callGasTemp 暂存,实际 gas 是前置算好的,计算时考虑以下因素:

描述
BaseCost固定开销(通常为 700)
Transfer如果转账了 value,增加 CallStipend = 2300(用于 fallback gas)
MemoryGas计算传入传出数据的 memory access 成本(线性+平方增长)
ColdAccessEIP-2929 引入冷账户调用要额外加 gas
2. 详细举例:CALL
gas = baseGas + memoryCost + (isColdAccount ? ColdAccessCost : 0)
if value != 0 {
    gas += CallStipend
}
3. STATICCALL 特别限制:

15.create、create2

CREATE指令

CREATE指令是EVM中用于创建新合约的基本指令,它使用调用者的地址和nonce作为合约地址的计算因子。

在go-ethereum中,CREATE指令的实现主要在core/vm/instructions.go文件中:

opCreate函数分析

主要逻辑流程
  1. 权限检查

    if interpreter.readOnly {
        return nil, ErrWriteProtection
    }

    检查EVM是否处于只读模式,如果是则不允许创建合约。

  2. 参数获取

    value := scope.Stack.pop()        // 转账金额
    offset, size := scope.Stack.pop(), scope.Stack.pop() // 内存位置和大小
    input := scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) // 初始化代码
    gas := scope.Contract.Gas         // 可用gas
  3. EIP150调整

    if interpreter.evm.chainRules.IsEIP150 {
        gas -= gas / 64
    }

    根据EIP150规则保留1/64的gas以防调用深度过大。

  4. 执行创建

    res, addr, returnGas, suberr := interpreter.evm.Create(scope.Contract.Address(), input, gas, &value)

    调用底层Create方法实际执行合约创建。

  5. 结果处理

    • 根据不同链规则处理ErrCodeStoreOutOfGas错误
    • 将结果地址压入栈中
    • 处理剩余的gas返还
  6. 返回数据设置

    • 如果执行被revert,保存返回数据
    • 否则清空返回数据缓冲区
关键点
CREATE指令的gas消耗包括以下几个部分:
  1. 基础创建费用:32,000 gas (params.CreateGas)
  2. 内存扩展费用:如果操作需要扩展内存,按内存扩展规则计算
  3. 代码存储费用:200 gas/字节(合约部署后存储的字节码)
  4. 初始化代码执行费用:执行初始化代码时消耗的gas

CREATE2指令

CREATE2是EVM中更灵活的合约创建指令,允许调用者控制合约地址的计算方式,通过添加"salt"参数使得地址不依赖于nonce。

Golang实现

同样在core/vm/instructions.go中:

opCreate2函数分析

主要逻辑流程
  1. 权限检查

    if interpreter.readOnly {
        return nil, ErrWriteProtection
    }

    同样检查是否只读模式。

  2. 参数获取

    endowment := scope.Stack.pop()    // 转账金额
    offset, size := scope.Stack.pop(), scope.Stack.pop() // 内存位置和大小
    salt := scope.Stack.pop()         // 盐值
    input := scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) // 初始化代码
    gas := scope.Contract.Gas         // 可用gas
  3. EIP150调整

    gas -= gas / 64

    同样保留1/64的gas。

  4. 执行创建

    res, addr, returnGas, suberr := interpreter.evm.Create2(scope.Contract.Address(), input, gas, &endowment, &salt)

    调用Create2方法,传入盐值。

  5. 结果处理

    • 如果有错误清空栈值
    • 否则将地址压入栈中
    • 处理剩余的gas返还
  6. 返回数据设置

    • 处理revert情况下的返回数据
关键点

CREATE2的Gas消耗

CREATE2的gas消耗与CREATE类似,但有以下区别:

  1. 基础创建费用:32,000 gas (与CREATE相同)
  2. 额外的哈希计算费用:CREATE2需要计算Keccak256哈希,每次哈希计算需要支付30 gas
  3. 内存扩展费用:与CREATE相同
  4. 代码存储费用:200 gas/字节(与CREATE相同)
  5. 初始化代码执行费用:与CREATE相同

关键区别

  1. 地址计算方式

    • CREATE: keccak256(rlp([sender, nonce]))[12:]
    • CREATE2: keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]
  2. 可预测性

    • CREATE地址依赖于nonce,无法提前预测
    • CREATE2地址可以通过salt和init_code预先计算
  3. 重放保护

    • CREATE自动增加nonce防止重放
    • CREATE2需要应用层通过salt防止重放

最新变化

在最新的以太坊升级中(如伦敦升级、柏林升级等),CREATE和CREATE2的gas计算有一些调整:

  1. EIP-2929:增加了状态访问的gas成本,影响了合约创建时的状态访问操作
  2. EIP-3860:对init code长度收取额外gas,限制过长的init code

这些变化使得合约创建的gas计算更加复杂,但基本框架保持不变。

实际应用建议

  1. 如果需要可预测的合约地址(如状态通道、合约工厂等),使用CREATE2
  2. 如果不需要地址预测,使用CREATE更简单
  3. 在gas估算时,一定要考虑初始化代码的执行成本和最终的代码存储成本
  4. 对于复杂的合约部署,可以考虑使用代理模式减少部署成本

以上代码和分析基于go-ethereum最新稳定版本,具体实现可能会随着版本更新而变化。

16.GAS

在EVM中,交易和执行智能合约需要消耗计算资源。为了防止用户恶意的滥用网络资源和补偿验证者所消耗的计算能源,以太坊引入了一种称为Gas的计费机制,使每一笔交易都有一个关联的成本。

在发起交易时,用户设定一个最大Gas数量(gasLimit)和每单位Gas的价格(gasPrice)。如果交易执行超出了gasLimit,交易会回滚,但已消耗的Gas不会退还。

以太坊上的Gas用gwei衡量,它是ETH的子单位,1 ETH = 10^9 gwei。一笔交易的Gas成本等于每单位gas价格乘以交易的gas消耗,即gasPrice * gasUsed。gas价格会随着时间的推移而变化,具体取决于当前对区块空间的需求。gas消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:

  1. calldata大小:calldata中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata每个零字节花费4 Gas,每个非零字节花费16 Gas(伊斯坦布尔硬分叉之前为 64 个)。
  2. 内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。
  3. opcode固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD指令消耗3 Gas。
  4. opcode动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3指令消耗的Gas随参数长度增长。
  5. 内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如MLOADRETURN
  6. 访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。
  7. Gas退款:SSTORE的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。

GAS 指令的功能


opGas 函数实现解析

func opGas(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
    scope.Stack.push(new(uint256.Int).SetUint64(scope.Contract.Gas))
    return nil, nil
}
关键组件:
执行流程:
  1. 读取剩余 Gas:从 scope.Contract.Gas 获取当前剩余 Gas。
  2. 转换为 uint256:通过 SetUint64 将 Gas 值封装为 256 位整数。
  3. 压入栈顶:调用 scope.Stack.push 将 Gas 值存入栈中。
  4. 返回空结果GAS 指令不修改内存或存储,故返回 nil

EVM 执行上下文与 Gas 管理

Gas 的生命周期
  1. 交易初始化:用户为交易设置 GasLimit,例如 21000 Gas(简单转账)。
  2. 合约执行:每条指令消耗 Gas(如 ADD 消耗 3 Gas,SSTORE 动态计算)。
  3. 动态扣除:执行指令前,EVM 检查剩余 Gas 是否足够,不足则回滚。

    // 伪代码:EVM 执行指令前的 Gas 检查
    if remainingGas < op.GasCost {
        return ErrOutOfGas
    }
    remainingGas -= op.GasCost
Contract 结构体中的 Gas
type Contract struct {
    Gas   uint64          // 剩余 Gas
    Code  []byte          // 合约字节码
    Scope *ScopeContext   // 当前执行上下文
    // ... 其他字段
}

为什么需要 GAS 指令?

应用场景
  1. Gas 估算:合约可通过 GAS 指令动态检查剩余 Gas,避免操作中途耗尽。

    // Solidity 示例:检查剩余 Gas
    function doSomething() public {
        uint256 remainingGas = gasleft(); // 编译为 `GAS` 指令
        require(remainingGas > 1000, "Insufficient gas");
    }
  2. 优化 Gas 消耗:复杂逻辑中根据剩余 Gas 调整执行路径(如循环次数)。
gasleft() 的关系

EVM 指令执行的全流程

以执行 GAS 指令为例:

  1. 指令调度:EVM 根据 pc(程序计数器)从合约字节码中读取 0x5A
  2. 调用 opGas:通过指令表跳转到对应的处理函数。
  3. 栈操作:将剩余 Gas 压入栈顶,更新 pc*pc++)。
  4. Gas 扣除:执行前已扣除 GAS 指令的基础成本(2 Gas)。

关键设计思想

  1. 显式 Gas 管理:EVM 要求所有操作明码标价,防止无限循环或资源滥用。
  2. 栈式虚拟机:通过操作数栈传递参数和结果,GAS 指令符合这一模式。
  3. 状态隔离GAS 指令仅读取当前合约的 Gas,不影响其他上下文。

扩展:其他 Gas 相关指令
指令操作码功能
GASPRICE0x3A获取当前交易的 Gas 单价
GASLIMIT0x45获取当前区块的 Gas 上限

总结

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »