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 消耗
- 指令本身消耗:0(RETURN 没有额外指令成本)
- 访问 memory 的成本:根据 offset+size 的访问范围来收取内存扩展费用(
memoryGasCost)
func memoryGasCost(memSize uint64) uint64 {
// linear + quadratic cost function
// linear: memSize * 3
// quadratic: (memSize^2) / 512
}💡 特点
- 调用成功返回数据(用于
CALL,DELEGATECALL等) - 常用于函数正常返回
🚨 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 消耗
- 指令本身:0
- 内存访问费用:同样受
memoryGasCost限制 - 保留剩余 gas,不销毁
💡 特点
- 引入自 Byzantium(EIP-140)
- 与
INVALID、ASSERT不同,它会“温柔地失败”,gas 不全耗尽,便于合约进行 try/catch
💣 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 消耗
- 固定成本:
5000(EIP-150 后) - 若首次销毁:+
24000(删除合约 refund) - 若 beneficiary 是新地址:再加
25000
// In gas_table.go (EIP150):
SelfDestruct: 5000,
SelfDestructRefund: 24000,💡 特点
- 合约立即从状态树中移除(实际上是标记为删除,block 结束后才真正移除)
- balance 强制转移,但无法返回自定义数据(不是直接返回 data)
🔁 总结对比表
| 指令名 | 功能描述 | 返回数据 | Gas 行为 | 出现版本 |
|---|---|---|---|---|
RETURN | 正常返回执行结果 | ✅ | memory + 0 | 初始版本 |
REVERT | 回滚并返回错误信息 | ✅ | memory + 0 | Byzantium (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 时,会设置:
contract.CallerAddress = caller(保留 msg.sender)contract.value = value(保留 msg.value)- 使用被调用者
toAddr的代码
✅ 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.go 与 core/vm/gas.go):
1. 通用机制
在这几条指令中,gas 的计算由 interpreter.evm.callGasTemp 暂存,实际 gas 是前置算好的,计算时考虑以下因素:
| 项 | 描述 |
|---|---|
| BaseCost | 固定开销(通常为 700) |
| Transfer | 如果转账了 value,增加 CallStipend = 2300(用于 fallback gas) |
| MemoryGas | 计算传入传出数据的 memory access 成本(线性+平方增长) |
| ColdAccess | EIP-2929 引入冷账户调用要额外加 gas |
2. 详细举例:CALL
gas = baseGas + memoryCost + (isColdAccount ? ColdAccessCost : 0)
if value != 0 {
gas += CallStipend
}3. STATICCALL 特别限制:
- 不允许非零 value(否则报错)
- 状态写入全部禁止(storage、selfdestruct 等都会报
ErrWriteProtection)
15.create、create2
CREATE指令
CREATE指令是EVM中用于创建新合约的基本指令,它使用调用者的地址和nonce作为合约地址的计算因子。
在go-ethereum中,CREATE指令的实现主要在core/vm/instructions.go文件中:
opCreate函数分析
主要逻辑流程
权限检查:
if interpreter.readOnly { return nil, ErrWriteProtection }检查EVM是否处于只读模式,如果是则不允许创建合约。
参数获取:
value := scope.Stack.pop() // 转账金额 offset, size := scope.Stack.pop(), scope.Stack.pop() // 内存位置和大小 input := scope.Memory.GetCopy(offset.Uint64(), size.Uint64()) // 初始化代码 gas := scope.Contract.Gas // 可用gasEIP150调整:
if interpreter.evm.chainRules.IsEIP150 { gas -= gas / 64 }根据EIP150规则保留1/64的gas以防调用深度过大。
执行创建:
res, addr, returnGas, suberr := interpreter.evm.Create(scope.Contract.Address(), input, gas, &value)调用底层Create方法实际执行合约创建。
结果处理:
- 根据不同链规则处理
ErrCodeStoreOutOfGas错误 - 将结果地址压入栈中
- 处理剩余的gas返还
- 根据不同链规则处理
返回数据设置:
- 如果执行被revert,保存返回数据
- 否则清空返回数据缓冲区
关键点
- 使用调用者地址和nonce计算新合约地址
- 处理Homestead和Frontier规则下的不同错误处理逻辑
- 实现了EIP150的gas调整规则
- 将创建结果(地址或错误)压入堆栈
CREATE指令的gas消耗包括以下几个部分:
基础创建费用:32,000 gas (
params.CreateGas)- 内存扩展费用:如果操作需要扩展内存,按内存扩展规则计算
- 代码存储费用:200 gas/字节(合约部署后存储的字节码)
- 初始化代码执行费用:执行初始化代码时消耗的gas
CREATE2指令
CREATE2是EVM中更灵活的合约创建指令,允许调用者控制合约地址的计算方式,通过添加"salt"参数使得地址不依赖于nonce。
Golang实现
同样在core/vm/instructions.go中:
opCreate2函数分析
主要逻辑流程
权限检查:
if interpreter.readOnly { return nil, ErrWriteProtection }同样检查是否只读模式。
参数获取:
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 // 可用gasEIP150调整:
gas -= gas / 64同样保留1/64的gas。
执行创建:
res, addr, returnGas, suberr := interpreter.evm.Create2(scope.Contract.Address(), input, gas, &endowment, &salt)调用Create2方法,传入盐值。
结果处理:
- 如果有错误清空栈值
- 否则将地址压入栈中
- 处理剩余的gas返还
返回数据设置:
- 处理revert情况下的返回数据
关键点
- 使用盐值(salt)参与地址计算,使地址可预测
- 不需要nonce,地址计算方式为
keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:] - 错误处理比CREATE简单,不区分特殊错误情况
- 同样实现EIP150的gas调整规则
CREATE2的Gas消耗
CREATE2的gas消耗与CREATE类似,但有以下区别:
- 基础创建费用:32,000 gas (与CREATE相同)
- 额外的哈希计算费用:CREATE2需要计算Keccak256哈希,每次哈希计算需要支付30 gas
- 内存扩展费用:与CREATE相同
- 代码存储费用:200 gas/字节(与CREATE相同)
- 初始化代码执行费用:与CREATE相同
关键区别
地址计算方式:
- CREATE:
keccak256(rlp([sender, nonce]))[12:] - CREATE2:
keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]
- CREATE:
可预测性:
- CREATE地址依赖于nonce,无法提前预测
- CREATE2地址可以通过salt和init_code预先计算
重放保护:
- CREATE自动增加nonce防止重放
- CREATE2需要应用层通过salt防止重放
最新变化
在最新的以太坊升级中(如伦敦升级、柏林升级等),CREATE和CREATE2的gas计算有一些调整:
- EIP-2929:增加了状态访问的gas成本,影响了合约创建时的状态访问操作
- EIP-3860:对init code长度收取额外gas,限制过长的init code
这些变化使得合约创建的gas计算更加复杂,但基本框架保持不变。
实际应用建议
- 如果需要可预测的合约地址(如状态通道、合约工厂等),使用CREATE2
- 如果不需要地址预测,使用CREATE更简单
- 在gas估算时,一定要考虑初始化代码的执行成本和最终的代码存储成本
- 对于复杂的合约部署,可以考虑使用代理模式减少部署成本
以上代码和分析基于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消耗由很多因素决定,并且每个以太坊版本都会有所改动,下面总结下:
calldata大小:calldata中的每个字节都需要花费gas,交易数据的大小越大,gas消耗就越高。calldata每个零字节花费4Gas,每个非零字节花费16Gas(伊斯坦布尔硬分叉之前为 64 个)。- 内在gas:每笔交易的内在成本为21000 Gas。除了交易成本之外,创建合约还需要 32000 Gas。该成本是在任何操作码执行之前从交易中支付的。
opcode固定成本:每个操作码在执行时都有固定的成本,以Gas为单位。对于所有执行,该成本都是相同的。比如每个ADD指令消耗3Gas。opcode动态成本:一些指令消耗更多的计算资源取决于其参数。因此,除了固定成本之外,这些指令还具有动态成本。比如SHA3指令消耗的Gas随参数长度增长。- 内存拓展成本:在EVM中,合约可以使用操作码访问内存。当首次访问特定偏移量的内存(读取或写入)时,内存可能会触发扩展,产生gas消耗。比如
MLOAD或RETURN。 - 访问集成本:对于每个外部交易,EVM会定义一个访问集,记录交易过程中访问过的合约地址和存储槽(slot)。访问成本根据数据是否已经被访问过(热)或是首次被访问(冷)而有所不同。
- Gas退款:
SSTORE的一些操作(比如清除存储)可以触发Gas退款。退款会在交易结束时执行,上限为总Gas消耗的20%(从伦敦硬分叉开始)。
GAS 指令的功能
- 作用:将当前合约调用剩余的 Gas 压入操作数栈,供后续指令使用。
- 操作码:
0x5A(参见 EVM 操作码列表)。 - Gas 消耗:固定 2 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
}关键组件:
scope.Contract.Gas
当前合约调用的剩余 Gas 值,存储在Contract结构体中,随指令执行动态更新。scope.Stack.push
将数据压入 EVM 的操作数栈(后进先出结构),栈的每个槽位为uint256类型。uint256.Int
以太坊自定义的 256 位无符号整数库,用于处理 EVM 的 256 位数据宽度。
执行流程:
- 读取剩余 Gas:从
scope.Contract.Gas获取当前剩余 Gas。 - 转换为
uint256:通过SetUint64将 Gas 值封装为 256 位整数。 - 压入栈顶:调用
scope.Stack.push将 Gas 值存入栈中。 - 返回空结果:
GAS指令不修改内存或存储,故返回nil。
EVM 执行上下文与 Gas 管理
Gas 的生命周期
- 交易初始化:用户为交易设置
GasLimit,例如 21000 Gas(简单转账)。 - 合约执行:每条指令消耗 Gas(如
ADD消耗 3 Gas,SSTORE动态计算)。 动态扣除:执行指令前,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字段:在合约调用开始时初始化为GasLimit - 已消耗 Gas,逐条指令递减。
为什么需要 GAS 指令?
应用场景
Gas 估算:合约可通过
GAS指令动态检查剩余 Gas,避免操作中途耗尽。// Solidity 示例:检查剩余 Gas function doSomething() public { uint256 remainingGas = gasleft(); // 编译为 `GAS` 指令 require(remainingGas > 1000, "Insufficient gas"); }- 优化 Gas 消耗:复杂逻辑中根据剩余 Gas 调整执行路径(如循环次数)。
与 gasleft() 的关系
- Solidity 的
gasleft()函数会被编译为GAS操作码,二者等价。
EVM 指令执行的全流程
以执行 GAS 指令为例:
- 指令调度:EVM 根据
pc(程序计数器)从合约字节码中读取0x5A。 - 调用
opGas:通过指令表跳转到对应的处理函数。 - 栈操作:将剩余 Gas 压入栈顶,更新
pc(*pc++)。 - Gas 扣除:执行前已扣除
GAS指令的基础成本(2 Gas)。
关键设计思想
- 显式 Gas 管理:EVM 要求所有操作明码标价,防止无限循环或资源滥用。
- 栈式虚拟机:通过操作数栈传递参数和结果,
GAS指令符合这一模式。 - 状态隔离:
GAS指令仅读取当前合约的 Gas,不影响其他上下文。
扩展:其他 Gas 相关指令
| 指令 | 操作码 | 功能 |
|---|---|---|
GASPRICE | 0x3A | 获取当前交易的 Gas 单价 |
GASLIMIT | 0x45 | 获取当前区块的 Gas 上限 |
总结
opGas的本质:将当前剩余 Gas 作为uint256压入栈顶,无副作用。- EVM 的 Gas 机制:每条指令执行前扣除 Gas,不足则回滚。
- 实用意义:智能合约可通过
gasleft()动态优化执行逻辑。
评论 (0)