区块链 以太坊 合约 创建、执行 详解
  DJsdk34H4Gbu 2023年11月02日 60 0


一、合约的创建和赋值:

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(),它用来处理(转帐)转入方地址为空的情况。

区块链 以太坊 合约 创建、执行 详解_预编译_02

与 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 关系图如下所示:

区块链 以太坊 合约 创建、执行 详解_预编译_03

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 对象的四个函数,流程示意图如下:

区块链 以太坊 合约 创建、执行 详解_数组_04

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交易部分分析


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
DJsdk34H4Gbu