CALL指令
详细设计
概述
CALL指令用于实现智能合约间的交互,即从当前智能合约调用另一个智能合约的函数。CALL指令从栈上弹出调用所需的操作数(如目标地址、调用的参数数据等),然后执行目标智能合约的代码;CALL指令在执行结束后会向栈上写入一个状态码标识目标合约的函数执行成功或失败,成功时向栈上写入1否则写入0。注:CALL指令不会引发回滚,即使调用失败当前智能合约仍会继续执行.
堆栈示例
// 栈顶 ---------------------> 栈底
CALL指令执行需要的操作数: |retLength|retOffset|argsLength|argsOffset|value|addr|gas|
CALL指令结束写入的操作数: |success|
下面文档中涉及到几个术语:
- 调用方/者:CALL指令的调用者合约
- 被调用方:CALL指令的被调用方合约(即栈上的操作数:addr)
ZKEVM中CORE指令的设计
在zkevm中CORE指令由多个gadget配合完成相应合约调用功能,每个gadget负责CORE指令的一部分逻辑,同时多个gadget之间有相应的执行顺序。
下列表格展示了CALL指令不同阶段使用到的gadget,以及gadget的执行顺序。
执行阶段 | gadget名称 | 执行顺序 |
---|---|---|
CALL指令调用前 | CALL_1 | 1 |
CALL指令调用前 | CALL_2 | 2 |
CALL指令调用前 | CALL_3 | 3 |
CALL指令调用前 | CALL_4 | 4 |
CALL指令调用前 | CALL_5 | 5 |
CALL指令调用前 | CALL_6 | 6 |
CALL指令调用前 | CALL_7 | 7 |
CALL指令调用中 | 被调用合约OPCODE的gadget(如:ADD,PUSH,CODESIZE) | 8 |
同上 | ... | 9 |
CALL指令调用结束 | STOP/RETURN/REVERT | 20 |
CALL指令调用结束后 | END_CALL | 21 |
CALL指令调用结束后 | POST_CALL_1 | 22 |
CALL指令调用结束后 | POST_CALL_2 | 23 |
各gadget的电路布局以及负责的功能
CORE_1 gadget
电路布局如下,core电路中使用3行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 2 | COPY | |
| 1 | STATE1| STATE2| STATE3|LEN_INV(1)|
| 0 | DYNA_SELECTOR | AUX | STATE_STAMP_INIT(1) |
+---+-------+-------+-------+----------+
call_1 从栈中读取CALL指令需要的argsOffset,argsLength两个操作数;并为CALL生成call_id,call_id基于state_stamp计算得出,将当前的state_stamp+1,在core电路中记录CALL ID对应的参数长度(即操作数argsLength),将CALL指令需要的参数数据从memory copy至CALL调用的calldata区域;Note: 此时gadget仍处于调用者的环境。
因此电路布局中,第0行为gadget标识和辅助状态的记录,第1行依次写入的三个state状态分别:argsOffset,argsLength,call id与argsLength对应关系,第2行写入copy的数据记录。
CORE_2 gadget
电路布局如下,core电路中使用2行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 1 | STATE1| STATE2| |
| 0 | DYNA_SELECTOR | AUX | STATE_STAMP_INIT(1) |
+---+-------+-------+-------+----------+
call_2 从栈中读取CALL指令需要的value操作数,记录新生成的call_id与value的对应关系;Note: 此时gadget仍处于调用者的环境。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行依次写入2个state状态分别为:栈上弹出的操作数value,新生成的call_id与value的对应关系。
CORE_3 gadget
电路布局如下,core电路中使用2行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 1 | STATE1| STATE2| STATE3| STATE4 |
| 0 | DYNA_SELECTOR | AUX | STATE_STAMP_INIT(1) |
+---+-------+-------+-------+----------+
call_3负责存储新生成的call_id调用者环境以便call指令执行结束后可以恢复调用者状态,不从栈上读取CALL指令的操作数;Note: 此时gadget仍处于调用者的环境。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的4个state状态为新call_id与调用者不同状态的对应关系,依次为调用者的call_id、调用者的pc、调用者的栈指针、调用者的合约地址。
CORE_4 gadget
电路布局如下:
+-----+----------------+-------------------------+-------------------------+----------------+---------------------+------------------+-----------------+
| cnt | | | | | | | |
+-----+----------------+-------------------------+-------------------------+----------------+---------------------+------------------+-----------------+
| 2 | U64Div(2..6) | MemoryExpansion(7..11) | MemoryExpansion(12..16) | U64Div(17..21) | U64Div(22..26) | args_len_inv(27) | ret_len_inv(28) |
| 1 | STATE0(0..7) | STATE1(8..15) | STATE2(16..23) | STATE3(24..31) | | | |
| 0 | DYNAMIC(0..17) | AUX(18..24) | STATE_STAMP_INIT(25) | MEMORY_GAS(26) | | | |
+-----+----------------+-------------------------+-------------------------+----------------+---------------------+------------------+-----------------+
call_4用于计算call过程中的memory gas,过程中需要四个栈元素。第0行计算出来的MEMORY_GAS提前预置在cnt == 0, vers_26的位置,方便下一个状态call_5调用时可以直接获取到该值的位置。第1行分别为栈里四个值args_offset, args_length, ret_offset, ret_length
。第2行为memory gas计算过程中所需要的算术电路。
CORE_5 gadget
电路布局:
+-----+-------------------+---------------------+---------------------+------------------------+---------------+
| cnt | | | | | |
+-----+-------------------+---------------------+---------------------+------------------------|----------+----+
| 3 | STORAGE_READ(0..11)| STORAGE_WRITE(12..23)| value_inv(24) | capped_gas_left(25) | |
| 2 | U64Div(2..6) | U64Overflow(7..11) | U64Overflow(12..16) | MemoryExpansion(17..21)| |
| 1 | STATE0(0..7) | STATE1(8..15) | STATE2(16..23) | | |
| 0 | dynamic(0..17) | AUX(18..24) | STAMP_INIT(25) | TRACE_GAS(26) | TRACE_GAS_COST(27) |
+-----+-------------------+---------------------+---------------------+---------------|-------------------+----+
call_5用于计算最终的call gas花费,在这一步计算出next_state的gas_left,以及call的gas_cost。第0行STAMP_INIT,TRACE_GAS,TRACE_GAS_COST
为预留的位置,提供给call_6计算使用。第1行为计算需要的stack中的值,分别对应gas,addr, value
。第2行为计算所需要的算术电路。第3行分别对应EIP2929中的is_warm read, write,value的乘法逆元,capped_gas_left为降低degree所需要的中间变量。
CORE_6 gadget
+-----+---------------------+---------------------+-----------------+
| cnt | | | |
+-----+---------------------+---------------------+-----------------+
| 1 | CALLCONTEXT_WRITE_0 | CALLCONTEXT_WRITE_1 | |
| 0 | DYNA_SELECTOR | AUX | STAMP_INIT (25) |
+-----+---------------------+---------------------+-----------------+
call_6用于在post_call时计算gas费用提供所需要的上下文信息。第0行STAMP_INIT为预留位置,供call_7使用。第1行分别代表trace.gas 和 trace.gas_cost的上下文写入数据操作。
CORE_7 gadget
电路布局如下,core电路中使用2行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 1 | STATE1| STATE2| STATE3| STATE4 |
| 0 | DYNA_SELECTOR | AUX |
+---+-------+-------+-------+----------+
call_4 从栈中读取CALL指令需要的gas,addr操作数,记录新call_id与调用方地址、被调用方合约地址(即弹出的addr操作数)对应关系,将stack_pointer置0、call_id与code_addr更新为新call对应被调用方状态;Note: 此时gadget执行完成后开始进行call指令的调用,进入新call指令的执行环境。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的4个state状态依次为:栈上弹出的gas操作数、栈上弹出的addr操作数、新call_id与被调用合约地址的对应关系、新call_id与调用方地址的对应关系;
RETURN/REVERT gadget
电路布局如下,core电路中使用3行。
+---+-------+-------+---------+---------+
|cnt| 8 col | 8 col | 8 col | 8col |
+---+-------+-------+---------+---------+
| 2 | Copy(11) | |
| 1 | STATE | STATE | STATE | STATE |
| 0 | DYNA_SELECTOR | AUX | ReturnDataSize(1)|
+---+-------+-------+---------+---------+
return/revert 标识一个call指令执行结束,此时从栈上弹出两个操作数:offset、length,并从call指令内存区域的指定位置读取return_data数据,记录call_id与returndata数据以及长度的对应关系。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的4个state状态依次为:栈上弹出的offset操作数、length操作数、返回数据的returndata_call_id、call_id与returndata数据长度的对应关系;第二行记录从memory copy至returndata区域的数据。
STOP gadget
电路布局如下,core电路中使用2行。
+---+-------+-------+-------+---------+
|cnt| 8 col | 8 col | 8 col | 8col |
+---+-------+-------+-------+---------+
| 1 | STATE1| STATE2| |
| 0 | DYNA_SELECTOR | AUX |RETURNDATASIZE(1) |
+---+-------+-------+-------+---------+
stop为表示CALL指令执行结束的另一种evm opcode,与RETURN/REVERT区别在于没有returndata数据,栈上不需要offset、length操作数,只需记录call_id与returndata的数据长度对应关系。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的2个state状态依次为:返回数据的returndata_call_id、call_id与returndata数据长度的对应关系。
END_CALL gadget
电路布局如下,core电路中使用2行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 1 | STATE1| STATE2| STATE3| STATE4 |
| 0 | DYNA_SELECTOR | AUX |SUCCESS(1)| PARENT_CALL_ID_INV(1)| RETURNDATA_SIZE(1)|
+---+-------+-------+-------+----------+
end_call 用于在call指令结束后恢复它对应的调用方的状态,当call指令调用方的call_id为0时标识当前执行的call为root call(即交易的所有指令执行结束),非0位交易的中间call调用此时恢复调用方的状态:stack_pointer、call_id、contract addr。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的4个state状态依次为: call指令对应的调用方call_id、对应的调用方pc、对应的调用方stack_pointer、对应的调用方contract addr。
POST_CALL_1 gadget
+-----+---------------------+---------------------+---------------------+----------------------+
| cnt | | | | |
+-----+---------------------+---------------------+---------------------+----------------------+
| 1 | CALLCONTEXT_READ_0 | CALLCONTEXT_READ_1 | | |
| 0 | DYNA_SELECTOR | AUX | RETURN_SUCCESS (25) | RETURNDATA_SIZE (27) |
+-----+---------------------+---------------------+---------------------+----------------------+
post_call_1用于处理之前在call_6写入的上下文信息,并计算post_call时的gas费用,第0行为预留位置给post_call_2使用,第1行分别为trace.gas和trace.gas_cost的读取操作。
POST_CALL_2 gadget
电路布局如下,core电路中使用3行。
+---+-------+-------+-------+----------+
|cnt| 8 col | 8 col | 8 col | 8 col |
+---+-------+-------+-------+----------+
| 2 | COPY(11) | COPY_LEN(1)| LEN_INV(1)| COPY_PADDING_LEN(1) |
| 1 | STATE1| STATE2| STATE3| STATE4 |
| 0 | DYNA_SELECTOR | AUX |
+---+-------+-------+-------+----------+
core_5 用于CALL指令结束后回到调用方上下文执行环境时的操作处理,首先从栈上读取CALL指令剩余的2个操作数retLength、retOffset,然后更新stack pointer将CALL指令的7个操作数弹出栈,将call指令的return data数据copy至调用方的memory区域。
因此在电路布局中,第0行为gadget标识和辅助状态的记录,第一行记录的4个state状态依次为: CALL指令的操作数retOffset、CALL指令的操作数retLength、CALL指令的returndata call_id、CALL指令的返回状态(0标识失败、1标识成功);第二行记录从returndata区域copy至memory区域的数据以及对应的属性。
其他类型的CALL
概述
-
CALL
- callee合约对象中过存储的callerAddress为caller的address
- callee合约对象的address为callee的address
- callee合约对象中的code为callee的code(toAddress对应的code)
-
STATICCALL
- callee合约对象中过存储的callerAddress为caller的address
- callee合约对象的address为callee的address
- callee合约对象中的code为callee的code(toAddress对应的code)
-
CALLCODE:
- callee合约对象中过存储的callerAddress为caller的地址
- callee合约对象的address为caller的address
- callee合约对象中的code为callee的code(toAddress对应的code)
-
DELEGATECALL:
- callee合约对象中过存储的callerAddress为caller.parent的地址
- callee合约对象的address为caller的address
- callee合约对象中的code为callee的code(toAddress对应的code)
因为每一次CALL的stack都是新的,memory是公用的,storage修改的是callee对象中的address对应的slot,所以CALLCODE和DELEGATECALL可以做到调用指定的合约但修改的是自己的存储状态数据
又因为DELEGATE中存储的callerAddress为caller.Parent.Address, 所以DELEGATE中操作msg.sender, msg.value和在caller中操作一样的效果
代码修改
- 添加opcode selector选择器
- CALL的stack操作数是7个,STATICCALL和DELEGATECALL的stack操作数是8个(需要修改栈的弹出位置以及stamp delta, stack pointer)
- DELEGATECALL中目标合约代码执行过程中的contract_addr为调用者的contract_addr, sender_addr为调用者的sender_addr
- LOG_TOPIC_NUM_ADDR中所使用的addr是contract_addr并不是code_addr
说明:
-
除了DELEGATECALL和CALLCODE的其他操作中,contract_addr和code_addr是相等的, msg.sender为caller的addr
-
DELEGATECALL中contract_addr为caller.addr, sender为caller.sender
-
CALLCODE中contract_addr为caller.addr, sender为caller
注:并未实现CALLCODE,原因参考:(https://docs.soliditylang.org/en/v0.8.26/050-breaking-changes.html,https://docs.soliditylang.org/zh/v0.8.17/050-breaking-changes.html)
参数比较
STATICCALL和DELEGATECALL比着CALL和CALLCODE少了一个value的参数
CALL
|gas|addr|value|argsOffset|argsLength|retOffset|retLength 返回值success
CALLCODE
|gas|addr|value|argsOffset|argsLength|retOffset|retLength 返回值success
DELEGATECALL
|gas|addr|argsOffset|argsLength|retOffset|retLength 返回值success
STATICCALL
|gas|addr|argsOffset|argsLength|retOffset|retLength 返回值success
指令行为比较
CALL
success,
memory[retOffset:retOffset+retLength] =
address(addr).call.gas(gas).value(value)
(memory[argsOffset:argsOffset+argsLength])
CALLCODE
success,
memory[retOffset:retOffset+retLength] =
address(addr).callcode.gas(gas).value(value)
(memory[argsOffset:argsOffset+argsLength])
DELETGATECALL
success,
memory[retOffset:retOffset+retLength] =
address(addr).delegatecall.gas(gas)
(memory[argsOffset:argsOffset+argsLength])
STATICCALL
success, memory[retOffset:retOffset+retLength] =
address(addr).staticcall.gas(gas)
(memory[argsOffset:argsOffset+argsLength])
evm指令代码比较
四个CALL调用逻辑一模一样
CALL
func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
stack := scope.Stack
// Pop gas. The actual gas in interpreter.evm.callGasTemp.
// We can use this as a temporary value
temp := stack.pop()
gas := interpreter.evm.callGasTemp
// Pop other call parameters.
addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr := common.Address(addr.Bytes20())
// Get the arguments from the memory.
args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
if interpreter.readOnly && !value.IsZero() {
return nil, ErrWriteProtection
}
if !value.IsZero() {
gas += params.CallStipend
}
ret, returnGas, err := interpreter.evm.Call(scope.Contract, toAddr, args, gas, &value)
if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas
interpreter.returnData = ret
return ret, nil
}
CALLCODE
func opCallCode(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
// Pop gas. The actual gas is in interpreter.evm.callGasTemp.
stack := scope.Stack
// We use it as a temporary value
temp := stack.pop()
gas := interpreter.evm.callGasTemp
// Pop other call parameters.
addr, value, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr := common.Address(addr.Bytes20())
// Get arguments from the memory.
args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
if !value.IsZero() {
gas += params.CallStipend
}
ret, returnGas, err := interpreter.evm.CallCode(scope.Contract, toAddr, args, gas, &value)
if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas
interpreter.returnData = ret
return ret, nil
}
DELEGATECALL
func opDelegateCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
stack := scope.Stack
// Pop gas. The actual gas is in interpreter.evm.callGasTemp.
// We use it as a temporary value
temp := stack.pop()
gas := interpreter.evm.callGasTemp
// Pop other call parameters.
addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr := common.Address(addr.Bytes20())
// Get arguments from the memory.
args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
ret, returnGas, err := interpreter.evm.DelegateCall(scope.Contract, toAddr, args, gas)
if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas
interpreter.returnData = ret
return ret, nil
}
STATICCALL
func opStaticCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
// Pop gas. The actual gas is in interpreter.evm.callGasTemp.
stack := scope.Stack
// We use it as a temporary value
temp := stack.pop()
gas := interpreter.evm.callGasTemp
// Pop other call parameters.
addr, inOffset, inSize, retOffset, retSize := stack.pop(), stack.pop(), stack.pop(), stack.pop(), stack.pop()
toAddr := common.Address(addr.Bytes20())
// Get arguments from the memory.
args := scope.Memory.GetPtr(int64(inOffset.Uint64()), int64(inSize.Uint64()))
ret, returnGas, err := interpreter.evm.StaticCall(scope.Contract, toAddr, args, gas)
if err != nil {
temp.Clear()
} else {
temp.SetOne()
}
stack.push(&temp)
if err == nil || err == ErrExecutionReverted {
scope.Memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
scope.Contract.Gas += returnGas
interpreter.returnData = ret
return ret, nil
}
evm调用代码比较
CALL
// callee的地址
addrCopy := addr
// NewContract(caller地址, callee地址,value, gas)
// c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}
contract := NewContract(caller, AccountRef(addrCopy), value, gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code)
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
CALLCODE
addrCopy := addr
// NewContract(caller地址, callee地址,value, gas)
// c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}
contract := NewContract(caller, AccountRef(caller.Address()), value, gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, false)
gas = contract.Gas
DELEGATECALL
addrCopy := addr
// NewContract(caller地址, callee地址,value, gas)
// c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}
// c.CallerAddress = parent.CallerAddress
contract := NewContract(caller, AccountRef(addrCopy), new(uint256.Int), gas)
contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), evm.StateDB.GetCode(addrCopy))
ret, err = evm.interpreter.Run(contract, input, true)
gas = contract.Gas
// CallerAddress: 默认是调用者的地址,
func NewContract(caller ContractRef, object ContractRef, value *uint256.Int, gas uint64) *Contract {
c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}
if parent, ok := caller.(*Contract); ok {
// Reuse JUMPDEST analysis from parent context if available.
c.jumpdests = parent.jumpdests
} else {
c.jumpdests = make(map[common.Hash]bitvec)
}
// Gas should be a pointer so it can safely be reduced through the run
// This pointer will be off the state transition
c.Gas = gas
// ensures a value is set
c.value = value
return c
}
func (c *Contract) AsDelegate() *Contract {
// NOTE: caller must, at all times be a contract. It should never happen
// that caller is something other than a Contract.
parent := c.caller.(*Contract)
// 被调用合约中记录的调用者的地址和调用者的调用者是同一个地址
c.CallerAddress = parent.CallerAddress
// value和调用者的value一致
c.value = parent.value
return c
}