CoinEx Chain开发团队:EVM组成-设计

.png

近些年来,基于区块链技术,出现了一些火爆的区块链游戏、去中心化金融(DeFi)等应用,而大部分这些应用,都是基于以太坊合约开发的。

以太坊被称为区块链2.0,号称永不停机的世界计算机(The Unstoppable World Computer),提供了强大的去中心运算能力。而以太坊协议的核心是以太坊虚拟机(简称EVM),是一个基于堆栈的准图灵完备的虚拟机,以沙箱模式嵌入在每个以太坊节点中,用来处理智能合约的部署和执行。在以太坊中,除了基础的外部账户间的转账,剩余所有操作都涉及到EVM,用来执行合约,更新对应账户的状态。用高级语言Solidity编写的以太坊合约,编译为EVM字节码后,就可以在EVM中执行。下面将介绍EVM的组成、以及设计。

EVM组成-设计

EVM主要由三部分组成:链的上下文StateDB环境、指令解释器InterpreterEnvironment Function;作为独立的三部分,各自在运行虚拟机中运行中起着不同的作用。

  • 链的上下文StateDB环境:向EVM虚拟机提供链上数据支持,同时可以将合约执行过程中的需要存储的数据持久化至链上;例如:合约执行过程中账户余额的更新、合约账户内部状态的更新等。类似与通用计算机中的硬盘。

  • 指令解释器Interpreter:解释执行编译后合约字节码,与一般虚拟机不同的是,EVM执行中有Gas的概念,用来解决停机和资源消耗问题,所以在解释器执行指令时,也会对计算相应指令的Gas消耗。解释器依据PC调用相应的指令,从堆栈、内存中获取该指令所需的操作数;如果属于简单指令(如:算术ADD、比较指令GT...),则解释器直接计算相应指令的结果;如果属于EVM语义的指令(如:SSTORECALL),则将操作数与StateDBEnviroment Function进行交互,计算指令结果,然后将结果存入堆栈。

  • Environment Function(简写ENV FUNC):提供EVM特有指令的执行逻辑,使用指令的操作数、StateDB进行交互,计算执行结果(如:对于CALL指令,EVM先从堆栈中获取指令所需的操作数,将操作数和StateDB传入ENV FUNCENV FUNC使用自身的执行逻辑和传入数据,计算结果,返回给解释器);部分代码展示如下

    func opCreate(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ... {
        //1. 从堆栈、内存获取指令操作数
        var (
            value        = callContext.stack.pop()
            offset, size = callContext.stack.pop(), callContext.stack.pop()
            input        = callContext.memory.GetCopy(int64(offset.Uint64()), int64(size.Uint64()))
            gas          = callContext.contract.Gas
        )   
        ....
        
        //2. 使用ENV FUNC计算指令结果
        res, addr, returnGas, suberr := interpreter.evm.Create(callContext.contract, input, gas, value.ToBig())
        ....
        
        //3. 将计算结果压回站上
        stackvalue.SetBytes(addr.Bytes())
        callContext.stack.push(&stackvalue)
        callContext.contract.Gas += returnGas
    }
    

从更宏观上来看,可以将EVM分为两层抽象,如下图所示。

image.png
  1. 从VM层进行抽象,从根本上来说,EVM只对外暴漏了两个方法调用CallCreate.

    type VM interface{
        Create(caller types.ContractRef, code []byte, gas uint64, val sdk.Int) (ret []byte, contractAddr sdk.AccAddress, gasLeft uint64, err error)
      
        Call(caller types.ContractRef, toAddr sdk.AccAddress, input []byte, gas uint64, val sdk.Int) (ret []byte, gasLeft uint64, err error)
    }
    
    • Create: 依据链上交易,来创建指定的合约;参数为创建者地址、原始合约字节码、gas、金额,返回创建的合约地址、合约存储字节码和剩余gas。
    • Call: 依据链上交易,来调用指定合约;参数为调用者地址、合约地址、调用输入、gas、金额,返回合约执行结果、剩余gas。
  2. 解释器层抽象,解释器的功能比较单纯,解释执行合约指令,并将计算压入栈。

    type Interpreter interface {
        Run(contract *Contract, input []byte, static bool) ([]byte, error)
        CanRun([]byte) bool
    }
    
    • Run: 用来解释执行合约;参数为合约信息、调用输入,返回执行结果。
    • CanRun: 解释器的额外控制逻辑,可以作为解释器的拓展点。
    • 当前的有一些解释器的开源实现和规范
      • go-ethereum项目自带的官方解释器
      • 遵循EVMC解释器规范实现的C++版解释器EVMONE,C++版解释器Hera,其中Hera是一个Wasm版字节码实现。
  3. EVM整体结构上,为如下组织

    type VM interface{  ...  }
    type EVM struct {
        ....
        
        StateDB StateDB
        interpreter  Interpreter
    }
    func (evm *EVM) Call(...)...
    func (evm *EVM) Create(...)...
    

    Go 的类型系统为Duck typing,不要求一个类型显式地声明它实现了某个接口。因此go-ethereum的EVM结构体实现了VM接口。

    • Duck typing 的准则是 “If you can do it, you can be used here”。
  4. EVM的整体执行流程如下图所示

image.png

小结

所以在一个准备兼容EVM的其它区块链项目中,可以通过下面几种方案,来引入EVM:

  1. 只引用现成的EVM字节码解释器,项目中自己实现ENV FUNC.

    • 例如使用遵从EVMC规范的解释器实现:EVMONE、Hera ...
    • 官方的go-ethereum解释器,并未拆分为一个单独的可导入组件,无法独立使用。
  2. 引用官方go-ethereum的EVM,然后替换其中的Interpreter实现。

    • 可以使用上述提到的那些解释器项目

但无论使用上述那种方式,都需要给虚拟机提供链的上下文StateDB,用于和链之间进行数据交互。

*本文由CoinEx Chain开发团队成员撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。