一、合约的创建和赋值:
1. 合约
合约(Contract)是 EVM 用来执行(虚拟机)指令的结构体。
2. 合约的结构
Contract 的结构定义于:core/vm/contract.go 中,在这些成员变量里,
- caller 是转帐转出方地址(账户),
- self 是转入方地址,不过它们的类型都用接口 ContractRef 来表示;
- Code 是指令数组,其中每一个 byte 都对应于一个预定义的虚拟机指令;
- CodeHash 是 Code 的 RLP 哈希值;
- Input 是数据数组,是指令所操作的数据集合;
- Args 是参数。
3. self变量
有意思的是 self 这个变量,为什么转入方地址要被命名成 self 呢?
Contract 实现了ContractRef 接口,返回的恰恰就是这个 self 地址。
func (c *Contract) Address() common.Address {
return c.self.Address()
}
所以当 Contract 对象作为一个 ContractRef 接口出现时,它返回的地址就是它的 self地址。
那什么时候 Contract 会被类型转换成 ContractRef 呢?
当 Contract A 调用另一个Contract B 时,A 就会作为 B 的 caller 成员变量出现。
Contract 可以调用 Contract,这就为系统在业务上的潜在扩展,提供了空间。
创建一个 Contract 对象时,重点关注对 self 的初始化,以及对 Code, CodeAddr 和Input 的赋值。
另外,StateDB 提供
- 方法 SetCode(),可以将指令数组 Code 存储在某个 stateObject 对象中;
- 方法 GetCode(),可以从某个 stateObject 对象中读取已有的指令数组 Code。
func (self *StateDB) SetCode(addr common.Address, code []byte)
func (self *StateDB) GetCode(addr common.Address, code []byte)
4. stateObject
stateObject (core/state/state_object.go)是 Ethereum 里用来管理一个账户所有信息修改的结构体,它以一个 Address 类型变量为唯一标示符。
StateDB 在内部用一个巨大的map 结构来管理这些 stateObject 对象。
所有账户信息-包括 Ether 余额,指令数组 Code,该账户发起合约次数 nonce 等-它们发生的所有变化,会首先缓存到 StateDB 里的某个stateObject 里,然后在合适的时候,被 StateDB 一起提交到底层数据库。
5. 创建并执行 Contract
EVM(core/vm/evm.go)中 目前有五个函数可以创建并执行 Contract,按照作用和调用方式,可以分成两类:
- Create(), Call(): 二者均在 StateProcessor 的 ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐。
- CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐。
考虑到与执行交易的相关性,这里着重探讨 Create()和 Call()。
call()
先来看 Call(),它用来处理(转帐)转入方地址不为空的情况:
Call()函数的逻辑可以简单分为以上 6 步。
- 步骤(3)调用了转帐函数 Transfer(),转入账户 caller, 转出账户 addr;
- 步骤(4)创建一个 Contract 对象,并初始化其成员变量 caller, self(addr), value 和 gas;
- 步骤(5)赋值 Contract 对象的 Code, CodeHash, CodeAddr 成员变量;
- 步骤(6) 调用 run()函数执行该合约的指令,最后 Call()函数返回。
相关代码可见:
func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error)
{
if evm.vmConfig.NoRecursion && evm.depth > 0 {//如果设置了“禁用 call”,并且depth 正确,直接返回
return nil, gas, nil
}
// Fail if we're trying to execute above the call depth limit
if evm.depth > int(params.CallCreateDepth) {//如果 call 的栈深度超过了预设值, 报错
return nil, gas, ErrDepth
}
// Fail if we're trying to transfer more than the available balance
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {//检查发出账户是否有足够的钱(实际实现的函数定义在 core/evm.go/CanTransfer()中)但目前还不知道是怎么调用的
return nil, gas, ErrInsufficientBalance
}
var (
to = AccountRef(addr)
snapshot = evm.StateDB.Snapshot()
)
if !evm.StateDB.Exist(addr) {//建立账户
precompiles := PrecompiledContractsHomestead
if evm.ChainConfig().IsByzantium(evm.BlockNumber) {
precompiles = PrecompiledContractsByzantium
}
if precompiles[addr] == nil && evm.ChainConfig().IsEIP158(evm.BlockNumber) && value.Sign() == 0 {
return nil, gas, nil
}
evm.StateDB.CreateAccount(addr)
}
evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)//转移
// initialise a new contract and set the code that is to be used by the
// E The contract is a scoped environment for this execution context
// only.
contract := NewContract(caller, to, value, gas)//建立合约contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr),
evm.StateDB.GetCode(addr)
ret, err = run(evm, snapshot, contract, input)
// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors. if err != nil {
evm.StateDB.RevertToSnapshot(snapshot) if err != errExecutionReverted {
contract.UseGas(contract.Gas)
}
return ret, contract.Gas, err
}
因为此时(转帐)转入地址不为空,所以直接将入参 addr 初始化 Contract 对象的 self 地址,并可从 StateDB 中(其实是以 addr 标识的账户 stateObject 对象)读取出相关的 Code 和CodeHash 并赋值给 contract 的成员变量。
注意,此时转入方地址参数 addr 同时亦被赋值予 contract.CodeAddr。
create()
再来看看 EVM.Create(),它用来处理(转帐)转入方地址为空的情况。
与 Call()相比,Create()因为没有 Address 类型的入参 addr,其流程有几处明显不同:
- 步骤(3)中创建一个新地址 contractAddr,作为(转帐)转入方地址,亦作为Contract 的 self 地址;
- 步骤(6)由于 contracrAddr 刚刚新建,db 中尚无与该地址相关的 Code 信息, 所以会将类型为[]byte 的入参 code,赋值予 Contract 对象的 Code 成员;
- 步骤(8)将本次执行合约的返回结果,作为 contractAddr 所对应账户(stateObject 对象)的 Code 储存起来,以备下次调用。
还有一点隐藏的比较深,Call()有一个入参 input 类型为[]byte,而 Create()有一个入参code 类型同样为[]byte,没有入参 input,它们之间有无关系?
其实,它们来源都是Transaction 对象 tx 的成员变量 Payload!调用 EVM.Create()或 Call()的入口在StateTransition.TransitionDb()中,
- 当 tx.Recipent 为空时,tx.data.Payload 被当作所创建Contract 的 Code;
- 当 tx.Recipient 不为空时,tx.data.Payload 被当作 Contract 的 Input。
二、预编译合约
EVM 中执行合约(指令)的函数是 run(),在 core/vm/evm.go 中其实现代码如下:
- 可见如果待执行的 Contract 对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr 为匹配项-那么它可以直接运行;
- 没有经过预编译的 Contract,才会由Interpreter 解释执行。这里的”预编译”,可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要 Code,仅需 Input 即可。
在代码实现中,预编译合约只需实现两个方法 Required()和 Run()即可,这两方法仅需一个入参 input。
/core/vm/contracts.go
type PrecompiledContract interface {
RequiredGas(input []byte) uint64 Run(input []byte) ([]byte, error)
}
func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contrat) (ret []byte, err error) {
gas := p.RequiredGas(input) if contract.UseGas(gas) {
return p.Run(input)
}
return nil, ErrOutOfGas
}
目前,Ethereuem 代码中已经加入了多个预编译合约,功能覆盖了包括椭圆曲线密钥恢复,SHA-3(256bits)哈希算法,RIPEMD-160 加密算法等等。
相信基于自身业务的需求,二次开发者完全可以加入自己的预编译合约,大大加快合约的执行速度。
三、解释器执行合约的指令
解释器 Interpreter 用来执行(非预编译的)合约指令。
它的结构体 UML 关系图如下所示:
Interpreter 结构体通过一个 Config 类型的成员变量,间接持有一个包括 256 个operation 对象在内的数组 JumpTable。
operation 是做什么的呢?
每个 operation 对象正对 应 一 个 已 定 义 的 虚 拟 机 指 令 , 它 所 含 有 的 四 个 函 数 变 量 execute, gasCost, validateStack, memorySize 提供了这个虚拟机指令所代表的所有操作。
每个指令长度1byte,Contract 对象的成员变量 Code 类型为[]byte,就是这些虚拟机指令的任意集合,operation 对象的函数操作,主要会用到 Stack,Memory, IntPool 这几个自定义的数据结构。
这样一来,Interpreter 的 Run()函数就很好理解了,其核心流程就是逐个 byte 遍历入参 Contract 对象的 Code 变量,将其解释为一个已知的 operation,然后依次调用该operation 对象的四个函数,流程示意图如下:
operation 在操作过程中,会需要几个数据结构:
- Stack,实现了标准容器 -栈的行为;
- Memory,一个字节数组,可表示线性排列的任意数据;
- intPool,提供对big.Int 数据的存储和读取。
已定义的 operation,种类很丰富,包括:
- 算术运算:ADD,MUL,SUB,DIV,SDIV,MOD,SMOD,EXP…;
- 逻辑运算:LT,GT,EQ,ISZERO,AND,XOR,OR,NOT…;
- 业务功能:SHA3,ADDRESS,BALANCE,ORIGIN,CALLER,GASPRICE,LOG1,LOG2…等等需要特别注意的是 LOGn 指令操作,它用来创建 n 个 Log 对象,这里 n 最大是 4。还记得 Log 在何时被用到么?每个交易(Transaction,tx)执行完成后,会创建一个 Receipt 对象用来记录这个交易的执行结果。Receipt 携带一个 Log 数组,用来记录 tx 操作过程中的所有变动细节,而这些 Log,正是通过合适的 LOGn 指令-即合约指令数组(Contract.Code) 中的单个 byte,在其对应的 operation 里被创建出来的。每个新创建的 Log 对象被缓存在StateDB 中的相对应的 stateObject 里,待需要时从 StateDB 中读取。
准确的来说是从链上取二进制的代码指令,这个指令就是合约编译后产生的binary,binary中包含了每个方法对应的ID及指令集,发送一笔交易会先对方法进行编码,编码时会产生方法ID,然后签名,再发送交易,交易进入EVM后根据Id查找对应的指令集,根据输入数据和指令集进行交易执行。
以太坊探究:ETH交易部分分析
https://m.8btc.com/article/265557