200字范文,内容丰富有趣,生活中的好帮手!
200字范文 > 区块链 以太坊 合约 创建 执行 详解

区块链 以太坊 合约 创建 执行 详解

时间:2019-12-09 03:29:58

相关推荐

区块链 以太坊 合约 创建 执行 详解

一、合约的创建和赋值:

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 limitif evm.depth > int(params.CallCreateDepth) {//如果 call 的栈深度超过了预设值, 报错return nil, gas, ErrDepth}// Fail if we're trying to transfer more than the available balanceif !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 := PrecompiledContractsHomesteadif 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.gotype 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交易部分分析

/article/265557

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。