以太坊虚拟机 - CSDN
精华内容
参与话题
  • 以太坊虚拟机的基本介绍

    万次阅读 2019-04-14 00:06:59
    以太坊虚拟机的基本介绍 作者:HPB团队整理 此文简要的介绍了以太坊虚拟机的基本要素,在以后的文章我们会向大家展示如何安装应用调试等基本技术。 1.1 概述  以太坊虚拟机(EVM)是以太网上智能合约的运行环境。...

    以太坊虚拟机的基本介绍

    作者:HPB团队整理

    此文简要的介绍了以太坊虚拟机的基本要素,在以后的文章我们会向大家展示如何安装应用调试等基本技术。

    1.1 概述

     以太坊虚拟机(EVM)是以太网上智能合约的运行环境。这不仅仅是个沙盒,更确实的是一个完全独立的环境,也就是说代码运行在EVM里是没有网络,文件系统或是其他进程的。智能合约甚至被限制访问其他的智能合约

    1.2 账号

    ​ 在以太坊中有两种账号共享地址空间:外部账号和合约账号。外部账号是由公钥和私钥控制的(如人),合约账号是由账号存储的代码所控制。

    外部账号的地址是由公钥决定的,而合约地址是在智能合约被创建的时候决定的(这个地址由创建者的地址和发送方发送过来的交易数字衍生而来,这个数字通常被叫做“nonce”)

    不管是否账号存有代码(合约账号存储了代码,而外部账号没有),对于EVM来说这两种账号是相等的。

    每一个账号都有持久化存储一个key和value长度都为256位字的键值对,被称为“storage”

    而且,在以太坊中,每个账号都有一个余额(确切的是用“Wei”来作为基本单位),该余额可以被发送方发送过来带有以太币的交易所更改。

    1.3 交易

       交易是一个账号和另外一个账号之间的信息交换。它包含了二进制数据(消费数据)和以太数据。如果目标账号包含了代码,这个代码一旦被执行,那么它的消费数据就会作为一个输入数据。如果目标账号是一个0账号(地址为0的账号),交易会生成一个新的合约。这个合约的地址不为0,但是是来源于发送方,之后这个账号的交易数据会被发送。这个合约消费会被编译为EVM的二进制代码,并执行。这次的执行会被作为这个合约的代码持久化。这就是说:为了创建一个合约,你不需要发送真正的代码到这个合约上,事实上是代码的返回作为合约代码。

    1.4 Gas

       以太坊上的每笔进行一笔交易都会被收取一定数量的Gas.这是为了限制交易的数量,同时对每一笔交易的进行支付额外费用。当EVM执行一个交易,交易发起方就会根据定义的规则消耗对应的Gas。

    交易的创造者定义了的Gas 价格。所以交易发起方每次需要支付 gas_price * gas 。如果有gas在执行后有剩余,会以同样的方法返回给交易发起方。如果gas在任何时候消耗完,out-of-gas 异常会被抛出,那当前的这边交易所执行的后的状态全部会被回滚到初始状态。

    1.5 存储,主存和栈

       每个账号都有持久化的内存空间叫做存储. 存储是一个key和value长度都为256位的key-value键值对。从一个合约里列举存储是不大可能的。读取存储里的内容是需要一定的代价的,修改storage里的内容代价则会更大。一个合约只能读取或是修改自己的存储内容。

    第二内存区域叫做主存。系统会为每个消息的调用分配一个新的,被清空的主存空间。主存是线性并且以字节粒度寻址。读的粒度为32字节(256位),写可以是1个字节(8位)或是32个字节(256字节)。当访问一个字(256位)内存时,主存会按照字的大小来扩展。主存扩展时候,消耗Gas也必须要支付,主存的开销会随着其增长而增大(指数增长)。

    EVM不是一个基于寄存器,而是基于栈的。所以所有的计算都是在栈中执行。最大的size为1024个元素,每个元素为256位的字。栈的访问限于顶端,按照如下方式:允许拷贝最上面的16个元素中的一个到栈顶或是栈顶和它下面的16个元素中的一个进行交换。所有其他操作会从栈中取出两个(有可能是1个,多个,取决于操作)元素,把操作结果在放回栈中。当然也有可能把栈中元素放入到存储或是主存中,但是不可能在没有移除上层元素的时候,随意访问下层元素。

    1.6 指令集

     为了避免错误的实现而导致的一致性问题,EVM的指令集保留最小集合。所有的指令操作都是基于256位的字。包含有常用的算术,位操作,逻辑操作和比较操作。条件跳转或是非条件跳转都是允许的。而且合约可以访问当前区块的相关属性比如编号和时间戳。

    1.7 消息调用

    合约可以通过消息调用来实现调用其他合约或是发送以太币到非合约账号。消息调用和交易类似,他们都有一个源,一个目标,数据负载,以太币,gas和返回的数据。事实上,每个交易都包含有一个顶层消息调用,这个顶层消息可以依次创建更多的消息调用。

    一个合约可以定义内部消息调用需要消耗多少gas,多少gas需要被保留。如果在内部消息调用中出现out-of-gas异常,合约会被通知,会在栈里用一个错误值来标记。这种情况只是这次调用的gas被消耗完。在Solidity,这种情况下调用合约会引起一个人为异常,这种异常会抛出栈的信息。

    上面提到,调用合约会被分配到一个新的,并且是清空的主存,并能访问调用的负载。调用负载时被称为calldata的一个独立区域。调用结束后,返回一个存储在调用主存空间里的数据。这个存储空间是被调用者预先分配好的。调用限制的深度为1024.对于更加复杂的操作,我们更倾向于使用循环而不是递归。

    1.8 代理调用/ 代码调用和库

    存在一种特殊的消息调用,叫做代理调用。除了目标地址的代码在调用方的上下文中被执行,而且msg.sender和msg.value不会改变他们的值,其他都和消息调用一样。这就意味着合约可以在运行时动态的加载其他地址的代码。存储,当前地址,余额都和调用合约有关系。只有代码是从被调用方中获取。这就使得我们可以在Solidity中使用库。比如为了实现复杂的数据结构,可重用的代码可以应用于合约存储中。

    1.9 日志

      我们可以把数据存储在一个特殊索引的数据结构中。这个结构映射到区块层面的各个地方。为了实现这个事件,在Solidity把这个特性称为日志。合约在被创建出来后是不可以访问日志数据的。但是他们可以从区块链外面有效的访问这些数据。因为日志的部分数据是存储在bloom filters上。我们可以用有效并且安全加密的方式来查询这些数据。即使不用下载整个区块链数据(轻客户端)也能找到这些日志

    1.10 创建

     合约可以通过特殊的指令来创建其他合约。这些创建调用指令和普通的消息调用唯一区别是:负载数据被执行,结果作为代码被存储,调用者在栈里收到了新合约的地址。

    1.11 自毁

    从区块链中移除代码的唯一方法是合约在它的地址上执行了selfdestruct操作。这个账号下剩余的以太币会发送给指定的目标,存储和代码从栈中删除。

    展开全文
  • EVM为以太坊虚拟机,以太坊底层通过EVM支持智能合约的执行和调用。调用智能合约时根据合约的地址获取合约代码,生成具体的执行环境,然后将代码载入到EVM虚拟机中运行。通常目前开发智能合约的高级语言为Solidity,...
    • 一. 概述

            EVM为以太坊虚拟机,以太坊底层通过EVM支持智能合约的执行和调用。调用智能合约时根据合约的地址获取合约代码,生成具体的执行环境,然后将代码载入到EVM虚拟机中运行。通常目前开发智能合约的高级语言为Solidity,在利用solidity实现智能合约逻辑后,通过编译器编译成元数据(字节码)最后发布到以太坊上。

    • 二. 架构设计

    • 1. 位宽设计

           EVM机器位宽为256位,即32个字节,256位机器位宽不同于经常见到主流的32/6位机器字宽,这就标明EVM设计上将考虑一套自己的关于操作,数据,逻辑控制的指令编码。目前主流的处理器原生的支持的计算数据类型有:8bits整数,16bits整数,32bits整数,64bits整数。一般情况下宽字节的计算将更加的快一些,因为它可能包含更多的指令被一次性加载到pc寄存器中,同时伴有内存访问次数的减少。从两个整形数相加来对比具体的操作时间消耗。

    1. 32bits相加的X86的汇编代码

    • mov eax, dword [9876ABCD] //将地址9876ABCD中的32位数据放入eax数据寄存器

    • add eax, dword [1234DCBA] //将1234DCBA地址指向32位数和eax相加,结果保存在eax中

    2. 64bits相加的X86汇编代码

    • mov rax, qword [123456789ABCDEF1] //将地址指向的64位数据放入64位寄存器

    • add rax, qword [1020304050607080] //计算相加的结果并将结果放入到64位寄存器中

    3. 64bits机器上完成256bits的加法汇编代码

    • mov rax, qword [9876ABCD]

    • add qword [1234DCBA], rax

    • mov rax, qword [9876ABCD+8]

    • adc qword [1234DCBA+8], rax//这里应用adc带进位的加法指令,影响进位标记CF

    • mov rax, qword [9876ABCD+16]

    • adc qword [1234DCBA+16], rax

    • mov rax, qword [9876ABCD+24]

    • adc qword [1234DCBA+24], rax

            从以上汇编指令可以看出256位操作要比系统原生支持的要复杂的多,从时间上考虑采用256位这样的字节宽度,实际的收益并不大。空间上,由上面的汇编操作可以看出,如果直接对地址进行操作似乎是一种快速的方式,并减少了操作数,进而操作码也有所减少,相应的智能合约的字节流大小就会小很多,gas花费也会有所下降。但是从另外一个层面来讲,支持宽字节的数据类型势必会造成在处理低字节宽度的数据时候带来存储上的浪费。从时间和空间角度来看,仅支持256字节宽度的选择有利有弊,EVM之所以设计为256位位宽可能是因为一下几方面的原因:

    1. 256位的宽度方便进行密码学方面的计算(sha256)

    2. 仅支持256位的比要支持其他类型的操作要少,单一,实现简单可控

    3. 和gas的计算相关,仅支持一种,方便计算,同时也考虑到了安全问题

    • 2. 结构体模型

    3. 代码结构

    代码结构  
    .  
    ├── analysis.go            //跳转目标判定  
    ├── common.go  
    ├── contract.go            //合约数据结构  
    ├── contracts.go           //预编译好的合约  
    ├── errors.go  
    ├── evm.go                 //执行器 对外提供一些外部接口     
    ├── gas.go                 //call gas花费计算 一级指令耗费gas级别  
    ├── gas_table.go           //指令耗费计算函数表  
    ├── gen_structlog.go         
    ├── instructions.go        //指令操作  
    ├── interface.go             
    ├── interpreter.go         //解释器 调用核心  
    ├── intpool.go             //int值池  
    ├── int_pool_verifier_empty.go  
    ├── int_pool_verifier.go  
    ├── jump_table.go           //指令和指令操作(操作,花费,验证)对应表  
    ├── logger.go               //状态日志  
    ├── memory.go               //EVM 内存  
    ├── memory_table.go         //EVM 内存操作表 主要衡量操作所需内存大小  
    ├── noop.go  
    ├── opcodes.go              //Op指令 以及一些对应关系       
    ├── runtime  
    │   ├── env.go              //执行环境   
    │   ├── fuzz.go  
    │   └── runtime.go          //运行接口 测试使用  
    ├── stack.go                //栈  
    └── stack_table.go          //栈验证  
    

     三.EVM设计

    1.解释器

    1.1 解释器结构体图

           解释器是以太坊的虚拟机的核心,主要用来执行智能合约。从上面的UML图可以清楚的看出,以太坊智能合约解释器主要由一个接口,一个实现类和一个配置类和其他两个组件组成。以下主要介绍接口和实现类:

    • Interpreter 接口
    • EVMInterpreter 接口的实现

    1.2  Interpreter接口

    Interpreter接口中主要包括两个函数:

    1. Run(contract *Contract, input []byte, static bool) 执行智能合约代码,参数为:智能合约对象、输入的参数,调用方式。其中智能合约调用参数(input)通常分两部分构成:

    • 前面4个字节被称为“4-byte signature”,是某个函数签名的Keccak哈希值的前4个字节,作为该函数的唯一标识。

    • 为调用该函数提供的参数,长度不定。

    例如:部署一个智能合约A,调用A中的add(1)方法,对应的input参数为:0x87db03b70000000000000000000000000000000000000000000000000000000000000001

    2. CanRun(code []byte) 判断当前合约代码是否能执行,暂时没有实现真正的逻辑

    1.3  EVMInterpreter 接口的实现类

    Interpreter 接口最终由EVMInterpreter结构体实现

    EVMInterpreter 主要包含了四种对象,分别是: intPool、GasTable、Config、EVM:

     

    1. intPool : 主要用于回收对象(大整数),这是一个高效的优化。里面存放的是栈里的数据。
    2. GasTable : 记录了不同时期的需要消耗的gas值 。
    3. Config : 包含了 EVMInterpreter 用到的配置选项。包含日志配置及操作码的表(不同的 bytecode 对应的不同的 opcode 码)。
    4. EVM虚拟机 : 一个基于对象并且可以运行智能合约的必要的工具。包含了 EVM虚拟机 的上下文、创建合约及四种部署合约的方式、把数据保存到状态库的内容。
      1. EVMInterpreter结构体:

    实现类结构体:

    type EVMInterpreter struct {
       evm      *EVM //EVM虚拟机对象
       cfg      Config //当前解释器的配置文件
       gasTable params.GasTable //代码执行的gas消耗
    
       intPool *intPool//数据回收对象
    
       hasher    keccakState // 签名算法接口
       hasherBuf common.Hash // 签名后的数值
    
       readOnly   bool   // 合约调用方式
       returnData []byte // 合约调用后的返回值
    }
    
       

    配置文件结构体:

    type Config struct {
       Debug                   bool   //是否允许Debug 调用
       Tracer                  Tracer // 操作码日志
       NoRecursion             bool   // Disables call, callcode, delegate call and create
       EnablePreimageRecording bool   // Enables recording of SHA3/keccak preimages
    
       JumpTable [256]operation // 当前阶段的操作码列表(Frontier,Homestead,Byzantium,Constantinople)
    
       EWASMInterpreter string // WASM虚拟机选项
       EVMInterpreter   string // EVM虚拟机选项
    }
    
    

     

     

    EVMInterpreter Run方法:

    EVM主要执行流程如下:

           首先PC会从合约代码中读取一个OpCode,然后从一个JumpTable中检索出对应的operation,也就是与其相关联的函数集合。接下来会计算该操作需要消耗的油费,如果油费耗光则执行失败,返回ErrOutOfGas错误。如果油费充足,则调用execute()执行该指令,根据指令类型的不同,会分别对Stack、Memory或者StateDB进行读写操作。

    调用合约函数执行流程如下:

           首先通过CALLDATALOAD指令将input字段中的前“4-byte signature”压入堆栈中,然后依次跟该合约中包含的函数进行比对,如果匹配则调用JUMPI指令跳入该段代码继续执行。最后根据执行过程中的指令不同,分别对Stack、Memory或者StateDB进行读写操作。

    Run方法主要部分源码解析:

    func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
         in.returnData = nil //返回值
       var (
          op    OpCode        // 当前操作码
          mem   = NewMemory() // 内存
          stack = newstack()  // 栈
          pc   = uint64(0) // 指令位置
          cost uint64 //gas花费
          pcCopy  uint64 // debug使用
          gasCopy uint64 // debug使用
          logged  bool   // debug使用
          res     []byte //当前操作码执行函数的返回值
       )
       contract.Input = input //函数入参
       for atomic.LoadInt32(&in.evm.abort) == 0 {
          //获取一条指令及指令对应的操作
          op = contract.GetOp(pc)
          operation := in.cfg.JumpTable[op]
          //valid校验
          if !operation.valid {
             return nil, fmt.Errorf("invalid opcode 0x%x", int(op))
          }
          // 栈校验
          if sLen := stack.len(); sLen < operation.minStack {
             return nil, fmt.Errorf("stack underflow (%d <=> %d)", sLen, operation.minStack)
          } else if sLen > operation.maxStack {
             return nil, fmt.Errorf("stack limit reached %d (%d)", sLen, operation.maxStack)
          }
           // 扣除固定静态操作gas值
          if !contract.UseGas(operation.constantGas) {
             return nil, ErrOutOfGas
          }
    
          var memorySize uint64
           //计算内存 按操作所需要的操作数来算
          if operation.memorySize != nil {
             memSize, overflow := operation.memorySize(stack)
             if overflow {
                return nil, errGasUintOverflow
             }
    
             if memorySize, overflow = math.SafeMul(toWordSize(memSize), 32); overflow {
                return nil, errGasUintOverflow
             }
          }
          if operation.dynamicGas != nil {
             // 校验cost 调用前面提到的costfunc 计算本次操作动态cost消耗
             cost, err = operation.dynamicGas(in.gasTable, in.evm, contract, stack, mem, memorySize)
             if err != nil || !contract.UseGas(cost) {
                return nil, ErrOutOfGas
             }
          }
          if memorySize > 0 {
             //如果本次操作需要消耗memory ,扩展memory
             mem.Resize(memorySize)
          }
          // 执行操作
          res, err = operation.execute(&pc, in, contract, mem, stack)
    
          if verifyPool {
             verifyIntegerPool(in.intPool)
          }
          // 如果遇到return 设置返回值
          if operation.returns {
             in.returnData = res
          }
    
          switch {
          case err != nil:
             return nil, err  //报错
          case operation.reverts: //出错回滚
             return res, errExecutionReverted
          case operation.halts:
             return res, nil  //停止
          case !operation.jumps:  //跳转
             pc++
          }
       }
       return nil, nil
    }

    执行流程:

    1. 从合约中取得第pc个指令,放⼊入当前opcode(op)中(下面简称op)

    2. 从JumpTable查到op对应的操作operation

    3. 验证operation的有效性

    4. 验证栈空间是否足够

    5. readOnly一直给它传的值是false

    6. 支付gas

    7. 计算多少内存可以适应operation

    8. 支付动态分配内存需要gas

    9. 分配内存

    10. 执行operation

    11. 返回结果放入返回数据的变量中

    2. EVM对象

    2.1 解释器结构体图

    以太坊的虚拟机主要有四部分组成:

    1. 实现了CallContext接口,该接口内定义了创建、调用合约的四种方法(Call,CallCode,DelegateCall,Create)
    2. Context 合约执行的上下文
    3. 集成解释器接口类,调运解释器执行智能合约
    4. 集成StateDB接口类,操作StateDB

    下面主要对虚拟机比较核心的代码进行分析:

    2.2. 核心机构体

    1. 虚拟机结构体

    type EVM struct {
       Context //上下文环境
       StateDB StateDB //stateDB 函数接口
       depth int //当前调用的深度
       chainConfig *params.ChainConfig //当前链配置
       chainRules params.Rules // 当前区块链所使用的版本
       vmConfig Config //虚拟机配置
       interpreters []Interpreter //解释器数组
       interpreter  Interpreter//当前使用的解释器
       abort int32 //用于中止EVM调用操作
       callGasTemp uint64 //保存当前可用的gas
    }
    
    

    2. Context结构体

    type Context struct {
       // CanTransfer returns whether the account contains
       // sufficient ether to transfer the value
       CanTransfer CanTransferFunc //返回账户是否有足够的ether用来转账
       // Transfer transfers ether from one account to the other
       Transfer TransferFunc //用来从一个账户给另一个账户转账
       // GetHash returns the hash corresponding to n
       GetHash GetHashFunc //用来返回入参n的对应的hash
    
       // Message information
       Origin   common.Address // 用来提供Origin的信息 sender的地址
       GasPrice *big.Int       // 用来提供GasPrice信息
    
       // Block information
       Coinbase    common.Address // Provides information for COINBASE
       GasLimit    uint64         // Provides information for GASLIMIT
       BlockNumber *big.Int       // Provides information for NUMBER
       Time        *big.Int       // Provides information for TIME
       Difficulty  *big.Int       // Provides information for DIFFICULTY
    }

     

    2.3. 核心函数设计

    2.3.1 合约创建函数create

             如果某一笔交易的to地址为nil,则表明该交易是用于创建智能合约的。首先需要创建合约地址,采用下面的计算公式:Keccak(RLP(call_addr, nonce))[12:]。也就是说,对交易发起人的地址和nonce进行RLP编码,再算出Keccak哈希值,取后20个字节作为该合约的地址。其次根据合约地址创建对应的stateObject,然后存储交易中包含的合约代码。该合约的所有状态变化会存储在一个storage trie中,最终以Key-Value的形式存储到StateDB中。代码一经存储则无法改变,而storage trie中的内容则是可以通过调用合约进行修改的,比如通过SSTORE指令。

    源码分析:

    func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) {
       //合约调用深度检查
       if evm.depth > int(params.CallCreateDepth) {
          return nil, common.Address{}, gas, ErrDepth
       }
       //balance检查
       if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
          return nil, common.Address{}, gas, ErrInsufficientBalance
       }
       //确保特定的地址没有合约存在
       nonce := evm.StateDB.GetNonce(caller.Address())
       evm.StateDB.SetNonce(caller.Address(), nonce+1)
       contractHash := evm.StateDB.GetCodeHash(address)
       if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
          return nil, common.Address{}, 0, ErrContractAddressCollision
       }
       //创建一个StateDB的快照,以便回滚
       snapshot := evm.StateDB.Snapshot()
       evm.StateDB.CreateAccount(address)//创建合约账号
       if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
          evm.StateDB.SetNonce(address, 1)
       }
       //转账
       evm.Transfer(evm.StateDB, caller.Address(), address, value)
       //创建一个新的合约
       contract := NewContract(caller, AccountRef(address), value, gas)
       contract.SetCodeOptionalHash(&address, codeAndHash)
       //如果是委托合约
       if evm.vmConfig.NoRecursion && evm.depth > 0 {
          return nil, address, gas, nil
       }
       if evm.vmConfig.Debug && evm.depth == 0 {
          evm.vmConfig.Tracer.CaptureStart(caller.Address(), address, true, codeAndHash.code, gas, value)
       }
       start := time.Now()
       ret, err := run(evm, contract, nil, false)//执行合约
       // 检查初始化生成的代码的长度不超过限制
       maxCodeSizeExceeded := evm.ChainConfig().IsEIP158(evm.BlockNumber) && len(ret) > params.MaxCodeSize
       //如果合同创建成功并且没有错误返回,则计算存储代码所需的GAS。
       // 如果由于没有足够的GAS而导致代码不能被存储设置错误,并通过下面的错误检查条件来处理。
       if err == nil && !maxCodeSizeExceeded {
          createDataGas := uint64(len(ret)) * params.CreateDataGas
          if contract.UseGas(createDataGas) {
             evm.StateDB.SetCode(address, ret)
          } else {
             err = ErrCodeStoreOutOfGas
          }
       }
       //当发生错误是回滚,但是gas不退回
       if maxCodeSizeExceeded || (err != nil && (evm.ChainConfig().IsHomestead(evm.BlockNumber) || err != ErrCodeStoreOutOfGas)) {
          evm.StateDB.RevertToSnapshot(snapshot)
          if err != errExecutionReverted {
             contract.UseGas(contract.Gas)
          }
       }
       // Assign err if contract code size exceeds the max while the err is still empty.
       if maxCodeSizeExceeded && err == nil {
          err = errMaxCodeSizeExceeded
       }
       if evm.vmConfig.Debug && evm.depth == 0 {
          evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
       }
       return ret, address, contract.Gas, err
    }

     

    执行流程:

    1. 判断虚拟机EVM的调用深度,不能超过限定值,默认是1024

    2. 根据 value 判断合约发布者是否有足额的以太币

    3. 合约部署者caller的 nonce 加1

    4. 根据 address 确保上面创建的合约地址没有被使用

    5. 创建当前状态的快照,用于后结出错的回滚

    6. 创建新的帐户 nonce设置为1

    7. 给该合约转帐,转帐值为 value

    8. 使用 caller address value gas 创建合约对象

    9. 把代码和哈希值 codeAndHash 放进去

    10. 运行虚拟机EVM,传⼊入参数有:evm contract input(nil)

    readOnly(false)

    11. 检查合约允许的最大字节码,即代码是否溢出

    12. 花费gas并保存合约代码

    13. 如果执行失败,回滚到快照的状态

     

    2.3.2 合约调用函数

    在以太坊合约调用一共有四种方法,分别为:

    1. Call
    2. CallCode
    3. StaticCall
    4. DelegateCall

    上面四种合约调用方法中,StaticCall实际没有被调用,所以在此处不再说它,下面主要说明另外三种方法的异同之处。

     

    1. Call和CallCode

            Call和CallCode的区别在于:代码执行的上下文环境不同。具体来说,Call修改的是被调用者的storage,而CallCode修改的是调用者的storage。

     

    2. CallCode 和DelegateCall

           CallCode和DelegateCall的区别在于:msg.sender不同。具体来说,DelegateCall会一直使用原始调用者的地址,而CallCode不会。

    3. Call函数分析

           Call 执行与给定的input作为参数与addr相关联的合约。处理所需的任何必要的转账操作,采取必要的步骤来创建帐户,在任意错误的情况下回滚所做的操作。

    源码分析:

    func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) {
       //  调用深度最多1024
       if evm.depth > int(params.CallCreateDepth) {
          return nil, gas, ErrDepth
       }
      //查看账户是否有足够钱
       if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
          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 {
             // Calling a non existing account, don't do anything, but ping the tracer
             if evm.vmConfig.Debug && evm.depth == 0 {
                evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value)
                evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil)
             }
             return nil, gas, nil
          }
          // 负责在本地状态创建addr
          evm.StateDB.CreateAccount(addr)
       }
       //执行转账
       evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value)
       //创建一个新的合约对象
       contract := NewContract(caller, to, value, gas)
       contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
       start := time.Now()
       //执行合约
       ret, err = run(evm, contract, input, false)
      if err != nil {
          evm.StateDB.RevertToSnapshot(snapshot)
          if err != errExecutionReverted {
             contract.UseGas(contract.Gas)
          }
       }
       return ret, contract.Gas, err
    }

    参数分析:

    1. caller ContractRef 合约部署者

    2. addr common.Address 合约的地址

    3. input []byte 合约的输⼊入,或者说传入的参数

    4. gas uint64支付的gas

    5. value *big.Int支付的以太币

    代码执行流程:

    1. 判断虚拟机EVM的调用深度,不能超过限定值,默认是1024

    2. 根据 value 判断合约发布者是否有⾜足额的以太币

    3. 根据 addr 设置合约地址

    4. 创建当前状态的快照,用于后结出错的回滚

    5. 根据 addr 判断该地址是否在状态库中已存在,如果不存在,则创建该帐户

    6. 给该合约地址转帐,转帐值为 value

    7. 使⽤用 caller to value gas 创建合约对象

    8. 根据 addr 把库中查到的合约的地下、代码和哈希值放进去

    9. 运行虚拟机EVM,传入参数有:evm contract input

    readOndy(false)

    10. 如果执行失败,回滚到快照的状态

    4. CallCode(已被代替掉)函数分析

    //CallCode与Call不同的地方在于它使用caller的context来执行给定地址的代码。
    func (evm *EVM) CallCode(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 {
          return nil, gas, nil
       }
    
       // Fail if we're trying to execute above the call depth limit
       if evm.depth > int(params.CallCreateDepth) {
          return nil, gas, ErrDepth
       }
       // Fail if we're trying to transfer more than the available balance
       if !evm.CanTransfer(evm.StateDB, caller.Address(), value) {
          return nil, gas, ErrInsufficientBalance
       }
       var (
          snapshot = evm.StateDB.Snapshot()
          //这里是不同的地方 to的地址被修改为caller的地址了 而且没有转账的行为
          to       = AccountRef(caller.Address())
       )
       // Initialise a new contract and set the code that is to be used by the EVM.
       // 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, contract, input, false)
       if err != nil {
          evm.StateDB.RevertToSnapshot(snapshot)
          if err != errExecutionReverted {
             contract.UseGas(contract.Gas)
          }
       }
       return ret, contract.Gas, err
    }

     

    5. DelegateCall函数分析

    //DelegateCall 和 CallCode不同的地方在于 caller被设置为 caller的caller
    func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) {
       if evm.vmConfig.NoRecursion && evm.depth > 0 {
          return nil, gas, nil
       }
       // Fail if we're trying to execute above the call depth limit
       if evm.depth > int(params.CallCreateDepth) {
          return nil, gas, ErrDepth
       }
       var (
          snapshot = evm.StateDB.Snapshot()
          to       = AccountRef(caller.Address())
       )
       // Initialise a new contract and make initialise the delegate values
       contract := NewContract(caller, to, nil, gas).AsDelegate()
       contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
       ret, err = run(evm, contract, input, false)
       if err != nil {
          evm.StateDB.RevertToSnapshot(snapshot)
          if err != errExecutionReverted {
             contract.UseGas(contract.Gas)
          }
       }
       return ret, contract.Gas, err
    }

     

    3. 合约对象 

         在EVM中,执行智能合约代码之前首先会先生成一个合约对象,合约对象里面定义了合约的调用者等属性。

     

    3.1 合约对象结构体:

    type Contract struct {
       CallerAddress common.Address//是初始化这个合约的人。 如果是delegate,这个值被设置为调用者的调用者
       caller        ContractRef //是转帐转出方地址(账户)
       self          ContractRef   //转入方地址
       jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis.
       analysis  bitvec                 // Locally cached result of JUMPDEST analysis
       Code     []byte //合约代码
       CodeHash common.Hash //合约代码hash值
       CodeAddr *common.Address //合约代码地址
       Input    []byte//输入参数
       Gas   uint64 //合约的gas值
       value *big.Int //转账值
    }

     

    3.2 初始化合约对象

    func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uint64) *Contract {
       c := &Contract{CallerAddress: caller.Address(), caller: caller, self: object}
    
       if parent, ok := caller.(*Contract); ok {
          // 如果 caller 是一个合约,说明是合约调用了我们。 jumpdests设置为caller的jumpdests
          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
    }

     

    3.3 设置委托调用

    //AsDelegate将合约设置为委托调用并返回当前合同(用于链式调用)
    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
       c.value = parent.value
    
       return c
    }
    
    

    四. 存储模型

            EVM中数据可以在三个地方进行存储,分别是栈,临时存储,永久存储。由于EVM是基于栈的虚拟机,因此基本上所有的操作都是在栈上进行的,并且EVM中没有寄存器的概念,这样EVM对栈的依赖就更大,虽然这样的设计使实现比较简单且易于理解,但是带来的问题就是需要更多数据的相关操作。在EVM中栈是唯一的免费(几乎是)存放数据的地方。栈自然有深度的限制,目前的限制是1024。因为栈的限制,因此栈上的临时变量的使用会受限制。临时内存存储在每个VM实例中,并在合约执行完后消失永久内存存储在区块链的状态层。 ​​​​​

    1. 栈存储

            EVM中栈用于保存操作数,每个操作数的类型是big.int。执行opcode的时候,从上往下弹出操作数,作为操作的参数。

    栈中的主要函数:

    1.    Data():返回栈中的数据

    2.    push():把一个元素放入栈中

    3.    pushN():把多个元素放入栈中

    4.    pop():取出栈顶元素

    5.    len():栈的长度

    6.    swap():第几个元素和栈顶元素交换

    7.    dup():复制第几个元素到栈顶

    8.    peek():偷看栈顶元素

    9.    Back():返回栈中的第几个元素

    10.  require():确定是否有该元素

    11.  Print():打印栈中的内容临时存储

    2. 临时存储

            内存用于一些内存操作(MLOAD,MSTORE,MSTORE8)及合约调用的参数拷贝(CALL,CALLCODE)。内存数据结构,维护了一个byte数组,MLOAD,MSTORE读取存入的时候都要指定位置及长度才能准确的读写。

    主要方法:

    1.   Set():把数据放入内存中

    2.   Set32():把32字节的数据放入内存中,不足部分用0补齐

    3.   Resize():扩展内存到指定大小

    4.   Get():从内存中获取数据,作为一个新的slice返回

    5.   GetPtr():从内存中获取数据

    6.   Len():返回内存的长度

    7.   Data():返回内存中的数据

    8.   Print():打印内存中的数据

    3. 持久存储

           合约及其调用类似于数据库的日志,保存了合约定义以及对他的一系列操作,只要将这些操作执行一遍就能获取当前的结果,但是如果每次都要去执行就太慢了,因而这部分数据是会持久化到stateDb里面的。code中定义了两条指令SSTORE SLOAD用于从db中读写合约当前的状态。

    五.GAS消耗模型

    以太坊中发送交易固定收取21000gas,除此之外gas收取主要分为两种:

    • 固定消耗的gas(例如:加减乘除消耗的gas)

    • 动态调整的gas(例如:扩展内容的gas大小根据内存大小而定) 

    1. 固定消耗的gas 

    const (
       GasQuickStep   uint64 = 2
       GasFastestStep uint64 = 3
       GasFastStep    uint64 = 5
       GasMidStep     uint64 = 8
       GasSlowStep    uint64 = 10
       GasExtStep     uint64 = 20
    )

    2. 动态调整的gas

    const (
       ExpByteGas            uint64 = 10    // Times ceil(log256(exponent)) for the EXP instruction.
       SloadGas              uint64 = 50    // Multiplied by the number of 32-byte words that are copied (round up) for any *COPY operation and added.
       CallValueTransferGas  uint64 = 9000  // Paid for CALL when the value transfer is non-zero.
       CallNewAccountGas     uint64 = 25000 // Paid for CALL when the destination address didn't exist prior.
       TxGas                 uint64 = 21000 // Per transaction not creating a contract. NOTE: Not payable on data of calls between transactions.
       TxGasContractCreation uint64 = 53000 // Per transaction that creates a contract. NOTE: Not payable on data of calls between transactions.
       TxDataZeroGas         uint64 = 4     // Per byte of data attached to a transaction that equals zero. NOTE: Not payable on data of calls between transactions.
       LogDataGas            uint64 = 8     // Per byte in a LOG* operation's data.
       Sha3Gas     uint64 = 30 // Once per SHA3 operation.
       Sha3WordGas uint64 = 6  // Once per word of the SHA3 operation's data.
       SstoreSetGas    uint64 = 20000 // Once per SLOAD operation.
       SstoreResetGas  uint64 = 5000  // Once per SSTORE operation if the zeroness changes from zero.
       SstoreClearGas  uint64 = 5000  // Once per SSTORE operation if the zeroness doesn't change.
       SstoreRefundGas uint64 = 15000 // Once per SSTORE operation if the zeroness changes to zero.
       NetSstoreNoopGas  uint64 = 200   // Once per SSTORE operation if the value doesn't change.
       NetSstoreInitGas  uint64 = 20000 // Once per SSTORE operation from clean zero.
       NetSstoreCleanGas uint64 = 5000  // Once per SSTORE operation from clean non-zero.
       NetSstoreDirtyGas uint64 = 200   // Once per SSTORE operation from dirty.
       JumpdestGas      uint64 = 1     // Once per JUMPDEST operation.
       CallGas          uint64 = 40    // Once per CALL operation & message call transaction.
       CreateDataGas    uint64 = 200   //
       ExpGas           uint64 = 10    // Once per EXP instruction
       LogGas           uint64 = 375   // Per LOG* operation.
       CopyGas          uint64 = 3     //
       TierStepGas      uint64 = 0     // Once per operation, for a selection of them.
       LogTopicGas      uint64 = 375   // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas.
       CreateGas        uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
       Create2Gas       uint64 = 32000 // Once per CREATE2 operation
       SuicideRefundGas uint64 = 24000 // Refunded following a suicide operation.
       MemoryGas        uint64 = 3     // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
       TxDataNonZeroGas uint64 = 68    // Per byte of data attached to a transaction that is not equal to zero. NOTE: Not payable on data of calls between transactions.
       EcrecoverGas            uint64 = 3000   // Elliptic curve sender recovery gas price
       Sha256BaseGas           uint64 = 60     // Base price for a SHA256 operation
       Sha256PerWordGas        uint64 = 12     // Per-word price for a SHA256 operation
       Ripemd160BaseGas        uint64 = 600    // Base price for a RIPEMD160 operation
       Ripemd160PerWordGas     uint64 = 120    // Per-word price for a RIPEMD160 operation
       IdentityBaseGas         uint64 = 15     // Base price for a data copy operation
       IdentityPerWordGas      uint64 = 3      // Per-work price for a data copy operation
       Bn256AddGas             uint64 = 500    // Gas needed for an elliptic curve addition
       Bn256ScalarMulGas       uint64 = 40000  // Gas needed for an elliptic curve scalar multiplication
       Bn256PairingBaseGas     uint64 = 100000 // Base price for an elliptic curve pairing check
       Bn256PairingPerPointGas uint64 = 80000  // Per-point price for an elliptic curve pairing check
    )

    六 . 指令集设计 

    1. 操作码分类

        操作码opcodes按功能分为9组(运算相关,块操作,加密相关等)

    1.1. 基础计算相关

    const (
       STOP OpCode = iota
       ADD
       MUL
       SUB
       DIV
       SDIV
       MOD
       SMOD
       ADDMOD
       MULMOD
       EXP
       SIGNEXTEND
    )
    
    

     

    1.2. 比较加密相关

    const (
       LT OpCode = iota + 0x10
       GT
       SLT
       SGT
       EQ
       ISZERO
       AND
       OR
       XOR
       NOT
       BYTE
       SHL
       SHR
       SAR
    
       SHA3 = 0x20
    )
    
    

     

    1.3 关闭当前状态相关

    const (
       ADDRESS OpCode = 0x30 + iota
       BALANCE
       ORIGIN
       CALLER
       CALLVALUE
       CALLDATALOAD
       CALLDATASIZE
       CALLDATACOPY
       CODESIZE
       CODECOPY
       GASPRICE
       EXTCODESIZE
       EXTCODECOPY
       RETURNDATASIZE
       RETURNDATACOPY
       EXTCODEHASH
    )
    
    

     

    1.4. 块操作相关

    const (
       BLOCKHASH OpCode = 0x40 + iota
       COINBASE
       TIMESTAMP
       NUMBER
       DIFFICULTY
       GASLIMIT
    )

    1.5. 存储操作相关

    const (
       POP OpCode = 0x50 + iota
       MLOAD
       MSTORE
       MSTORE8
       SLOAD
       SSTORE
       JUMP
       JUMPI
       PC
       MSIZE
       GAS
       JUMPDEST
    )

     

    1.6. 栈操作相关

    const (
       PUSH1 OpCode = 0x60 + iota
       PUSH2
       PUSH3
       PUSH4
       PUSH5
       PUSH6
       PUSH7
       PUSH8
       PUSH9
       PUSH10
       PUSH11
       PUSH12
       PUSH13
       PUSH14
       PUSH15
       PUSH16
       PUSH17
       PUSH18
       PUSH19
       PUSH20
       PUSH21
       PUSH22
       PUSH23
       PUSH24
       PUSH25
       PUSH26
       PUSH27
       PUSH28
       PUSH29
       PUSH30
       PUSH31
       PUSH32
       DUP1
       DUP2
       DUP3
       DUP4
       DUP5
       DUP6
       DUP7
       DUP8
       DUP9
       DUP10
       DUP11
       DUP12
       DUP13
       DUP14
       DUP15
       DUP16
       SWAP1
       SWAP2
       SWAP3
       SWAP4
       SWAP5
       SWAP6
       SWAP7
       SWAP8
       SWAP9
       SWAP10
       SWAP11
       SWAP12
       SWAP13
       SWAP14
       SWAP15
       SWAP16
    )
    
    

     

    1.7. 日志相关

    const (
       LOG0 OpCode = 0xa0 + iota
       LOG1
       LOG2
       LOG3
       LOG4
    )

     

    1.8. 执行合约相关

    const (
       CREATE OpCode = 0xf0 + iota
       CALL
       CALLCODE
       RETURN
       DELEGATECALL
       CREATE2
       STATICCALL = 0xfa
    
       REVERT       = 0xfd
       SELFDESTRUCT = 0xff
    )
    
    

     

    1.9. 其他非官方提供的操作     

    const (
       PUSH OpCode = 0xb0 + iota
       DUP
       SWAP
    )
    
    

    2. 操作指令集

            文件jump.table.go定义了四种指令集合,每个集合实质上是个256长度的数组,名字翻译过来是(前沿,家园,拜占庭,君士坦丁堡)估计是对应了EVM的四个发展阶段。指令集向前兼容。

    frontierInstructionSet       = newFrontierInstructionSet()
    homesteadInstructionSet      = newHomesteadInstructionSet()
    byzantiumInstructionSet      = newByzantiumInstructionSet()
    constantinopleInstructionSet = newConstantinopleInstructionSet()
    1. FrontierInstructionSet存放的是一堆基础指令。
    2. HomesteadInstructionSet以上一个指令集为基础,增加了 DELEGATECALL 指令。
    3. ByzantiumInstructionSet以上一个指令集为基础,增加了 STATICCALL、RETURNDATASIZE、RETURNDATACOPY、REVERT 指令。
    4. ConstantinopleInstructionSet 以上一个指令集为基础,增加了 SHL、SHR、SAR、EXTCODEHASH、CREATE2 指令。

     

    具体每条指令结构如下:

    type operation struct {
       execute     executionFunc  //对应的操作函数
       constantGas uint64 // 操作对应的gas消耗
       dynamicGas  gasFunc  //该指令动态调整后的gas值
       minStack int //该指令最小需要的栈空间大小
       maxStack int //该指令最大需要的栈空间大小
       memorySize memorySizeFunc  // 操作所需空间
    
       halts   bool // 运算中止
       jumps   bool // 跳转(for)
       writes  bool //是否写入
       valid   bool // 操作是否有效
       reverts bool // 出错回滚
       returns bool // d返回
    }

    3. 指令详解 

     

    1. 以 ADD指令为例,该指令是从栈中获取两个元素,然后把相加的结果再放进栈中。

    func opAdd(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
       x, y := stack.pop(), stack.peek()
       math.U256(y.Add(x, y))
    
       interpreter.intPool.put(x)
       return nil, nil
    }

         从栈中取出一个元素放进变量 x 中,再查看下一个元素并把值放进变量 y 中,把x和y相加并把结果赋值给 y,最后把 x 缓存起来。

    2. 以 MSTORE指令为例,该指令是从栈中获取两个元素,一个标识内存地址,一个表示保存到内存的值。(临时内存)

    func opMstore(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
       // pop value of the stack
       mStart, val := stack.pop(), stack.pop()
       memory.Set32(mStart.Uint64(), val)
    
       interpreter.intPool.put(mStart, val)
       return nil, nil
    }

           从栈中取第一个元素作为内存地址的偏移量,再取第二个元素作为内存要保存的值,根据这两个值保存到内存中,并且把这两个值缓存起来。

    3. 以 SSTORE指令为例,该指令是从栈中获取两个元素,一个标识内存地址,一个表示保存到内存的值。(永久内存)

    func opSstore(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
       loc := common.BigToHash(stack.pop())
       val := stack.pop()
       interpreter.evm.StateDB.SetState(contract.Address(), loc, common.BigToHash(val))
    
       interpreter.intPool.put(val)
       return nil, nil
    }

       永久内存是以 key-value 的形式存储的,key通常是从0开始,并依次增加。上述操作的意思是从栈中取第一个元素,哈希后作为内存的 key,取第二个元素,哈希后作为内存的 value,然后从上下文获取合约的地址,然后保存到永久内存中。最后把从栈中取出的 val 缓存起来。

    展开全文
  • 以太坊虚拟机及交易的执行

    千次阅读 2018-03-07 17:54:12
    最近在看以太坊(Ethereum)的源代码, 初初看出点眉目。 区块链是近年热点之一,面向大众读者介绍概念的文章无数,有兴趣的朋友可自行搜索。我会从源代码实现入手,较系统的介绍一下以太坊的系统设计和协议实现等...

    最近在看以太坊(Ethereum)的源代码, 初初看出点眉目。 区块链是近年热点之一,面向大众读者介绍概念的文章无数,有兴趣的朋友可自行搜索。我会从源代码实现入手,较系统的介绍一下以太坊的系统设计和协议实现等,希望能提供有一定深度的内容,欢迎有兴趣的朋友多多讨论。

    注:1.源代码在github上, 分C++和Golang两个版本,这里我选择的是Go语言版(github.com/ethereum/go-ethereum),以下文中提到的Ethereum 代码部分,如无特别说明,均指go-ethereum; 2.github 主干代码还在持续更新中,所以此文中摘录的代码将来可能会跟读者的本地版本有所不同,如有差异我会作相应修改。

    1. 基本概念

    1.1 SHA-3哈希加密,RLP编码

    Ethereum 代码里哈希(hash)无处不在,许许多多的类型对象通过给定的哈希算法,可以得到一个哈希值。注意,算法中所使用的哈希函数是不可逆的,即对于h = hash(x), 仅仅通过哈希运算的结果h 无法作逆运算得到输入x。哈希值在数学上的唯一性使得它可以用作某个对象的全局唯一标识符。

    Ethereum 中用到的哈希函数全部采用SHA-3(Secure Hash Algorithm 3,wikipedia)。SHA-3在2015年8月由美国标准技术协会(NIST)正式发布,作为Secure Hash Algorithm家族的最新一代标准,它相比于SHA-2和SHA-1,采用了完全不同的设计思路,性能也比较好。需要注意的是,SHA-2目前并没有出现被成功攻克的案例,SHA-3也没有要立即取代SHA-2的趋势,NIST只是考虑到SHA-1有过被攻克的案例,未雨绸缪的征选了采用全新结构和思路的SHA-3来作为一种最新的SHA方案。

    RLP(Recursive Length Prefix)编码,其定义可见wiki,它可以将一个任意嵌套的字节数组([]byte),编码成一个“展平”无嵌套的[]byte。1 byte取值范围0x00 ~ 0xff,可以表示任意字符,所以[]byte可以线性的表示任意的数据。最简单比如一个字符串,如果每个字符用ASCII码的二进制表示,整个字符串就变成一个[]byte。 RLP 编码其实提供了一种序列化的编码方法,无论输入是何种嵌套形式的元素或数组,编码输出形式都是[]byte。RLP是可逆的,它提供了互逆的编码、解码方法。

    Ethereum 中具体使用的哈希算法,就是对某个类型对象的RLP编码值做了SHA3哈希运算,可称为RLP Hash。 Ethereum 在底层存储中特意选择了专门存储和读取[k, v] 键值对的第三方数据库,[k, v] 中的v 就是某个结构体对象的RLP编码值([]byte),k大多数情况就是v的RLP编码后的SHA-3哈希值

    1.2 常用数据类型 哈希值和地址

    两个最常用的自定义数据类型common.Hash用来表示哈希值,common.Address表示地址

    [plain] view plain copy
    1. # /commons/types.go  
    2. const (  
    3.     HashLength = 32  
    4.     AddressLength = 20  
    5. )  
    6. type Hash [HashLength]byte  
    7. type Address [AddressLength]byte  

    在Ethereum 代码里,所有用到的哈希值,都使用该Hash类型,长度为32bytes,即256 bits;Ethereum 中所有跟帐号(Account)相关的信息,比如交易转帐的转出帐号(地址)和转入帐号(地址),都会用该Address类型表示,长度20bytes。

    big.Int是golang提供的数据类型,用来处理比较大的整型数,当然它也可以处理诸如64bit,32bit的常用整数。

    [plain] view plain copy
    1. # /go-1.x/src/math/big/int.go  
    2. package big  
    3. type Int struct {  
    4.     neg bool  // sign, whether negaive  
    5.     abs nat   // absolute value of integer  
    6. }  

    big.Int是一个结构体(struct),相当于C++中的class,所以每次新建big.Int时可以用 x := new(big.Int), 返回一个指针。注意对Int的算术操作,要使用该对象的成员函数,比如Add():

    [plain] view plain copy
    1. func (z *Int) Add(x, y *Int) *Int   // Add sets z to sum x+y and returns z  

     Ethereum 代码中, 很多整型变量的类型都选用big.Int,比如Gas和Ether。

    1.3 汽油(Gas)和以太币(Ether)

    Gas, 是Ethereum里对所有活动进行消耗资源计量的单位。这里的活动是泛化的概念,包括但不限于:转帐,合约的创建,合约指令的执行,执行中内存的扩展等等。所以Gas可以想象成现实中的汽油或者燃气。

    Ether, 是Ethereum世界中使用的数字货币,也就是常说的以太币。如果某个帐号,Address A想要发起一个交易,比如一次简单的转帐,即向 Address B 发送一笔金额H,那么Address A 本身拥有的Ether,除了转帐的数额H之外,还要有额外一笔金额用以支付交易所耗费的Gas。

    如果可以实现Gas和Ether之间的换算,那么Ethereum系统里所有的活动,都可以用Ether来计量。这样,Ether就有了点一般等价物,也就是货币的样子。

    1.4 区块是交易的集合

    区块(Block)是Ethereum的核心结构体之一。在整个区块链(BlockChain)中,一个个Block是以单向链表的形式相互关联起来的。Block中带有一个Header(指针), Header结构体带有Block的所有属性信息,其中的ParentHash 表示该区块的父区块哈希值, 亦即Block之间关联起来的前向指针。只不过要想得到父区块(parentBlock)对象,直接解析这个ParentHash是不够的, 而是要将ParentHash同其他字符串([]byte)组合成合适的key([]byte), 去kv数据库里查询相应的value才能解析得到。 Block和Header的部分成员变量定义如下:

    [plain] view plain copy
    1. # /core/types/block.go  
    2. type Block struct {  
    3.     header *Header  
    4.     transactions Transactions  // type Transactions []*Transaction  
    5.     ...  
    6. }  
    7. type Header struct {  
    8.     ParentHash common.Hash  
    9.     Number *big.Int  
    10.     ...  
    11. }   

    Header的整型成员Number表示该区块在整个区块链(BlockChain)中所处的位置,每一个区块相对于它的父区块,其Number值是+1。这样,整个区块链会存在一个原始区块,即创世块(GenesisBlock), 它的Number是0,由系统自然生成而不必去额外挖掘(mine)。Block和BlockChain的实现细节,之后会有更详细的讨论。

    Block中还有一个Tranction(指针)数组,这是我们这里关注的。Transaction(简称tx),是Ethereum里标示一次交易的结构体, 它的成员变量包括转帐金额,转入方地址等等信息。Transaction的完整声明如下:

    [plain] view plain copy
    1. # /core/types/transaction.go  
    2. type Transaction struct {  
    3.     data txdata  
    4.     hash, size, from atomic.Value  // for cache  
    5. }  
    6. type txdata struct {  
    7.     AccountNonce uint64  
    8.     Price *big.Int  
    9.     GasLimit *big.Int  
    10.     Recipient *common.Address  
    11.     Amount *big.Int  
    12.     Payload []byte  
    13.     V, R, S *big.Int   // for signature  
    14.     Hash *common.Hash  // for marshaling  
    15. }  
    每个tx都声明了自己的(Gas)Price 和 GasLimit。 Price指的是单位Gas消耗所折抵的Ether多少,它的高低意味着执行这个tx有多么昂贵。GasLimit 是该tx执行过程中所允许消耗资源的总上限,通过这个值,我们可以防止某个tx执行中出现恶意占用资源的问题,这也是Ethereum中有关安全保护的策略之一。拥有独立的Price和GasLimit, 也意味着每个tx之间都是相互独立的。

    转帐转入方地址Recipient可能为空(nil),这时在后续执行tx过程中,Ethereum 需要创建一个地址来完成这笔转帐。Payload是重要的数据成员,它既可以作为所创建合约的指令数组,其中每一个byte作为一个单独的虚拟机指令;也可以作为数据数组,由合约指令进行操作。合约由以太坊虚拟机(Ethereum Virtual Machine, EVM)创建并执行。

    细心的朋友在这里会有个疑问,为何交易的定义里没有声明转帐的转出方地址? 问的好,tx 的转帐转出方地址确实没有如转入方一样被显式的声明出来,而是被加密隐藏起来了,在Ethereum里这个转出方地址是机密,不能直接暴露。这个对tx加密的环节,在Ethereum里被称为签名(sign), 关于它的实现细节容后再述。

    2. 交易的执行

    Block 类型的基本目的之一,就是为了执行交易。狭义的交易可能仅仅是一笔转帐,而广义的交易同时还会支持许多其他的意图。Ethereum 中采用的是广义交易概念。按照其架构设计,交易的执行可大致分为内外两层结构第一层是虚拟机外,包括执行前将Transaction类型转化成Message,创建虚拟机(EVM)对象,计算一些Gas消耗,以及执行交易完毕后创建收据(Receipt)对象并返回等;第二层是虚拟机内,包括执行转帐,和创建合约并执行合约的指令数组。

    2.1 虚拟机外

    2.1.1 入口和返回值

    执行tx的入口函数是StateProcessor的Process()函数,其实现代码如下:

    [plain] view plain copy
    1. # /core/state_processor.go  
    2. func (p *StateProcessor) Process(block *Block, statedb *StateDB, cfg vm.Config) (types.Receipts, []*types.Log, *big.Int, error) {  
    3.     var {  
    4.         receipts     types.Receipts  
    5.         totalUsedGas = big.NewInt(0)  
    6.         header       = block.Header()  
    7.         allLogs      []*types.Log  
    8.         gp           = new(GasPool).AddGas(block.GasLimit())  
    9.     }  
    10.     ...  
    11.     for i, tx := range block.Transactions() {  
    12.         statedb.Prepare(tx.Hash(), block.Hash(), i)  
    13.         receipt, _, err := ApplyTransaction(p.config, p.bc, author:nil, gp, statedb, header, tx, totalUsedGas, cfg)  
    14.         if err != nil { return nil, nil, nil, err}  
    15.         receipts = append(receipts, receipt)  
    16.         allLogs = append(allLogs, receipt.Logs...)  
    17.     }  
    18.     p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles(), receipts)  
    19.     return receipts, allLogs, totalUsedGas, nil  
    20. }  

    GasPool 类型其实就是big.Int。在一个Block的处理过程(即其所有tx的执行过程)中,GasPool 的值能够告诉你,剩下还有多少Gas可以使用。在每一个tx执行过程中,Ethereum 还设计了偿退(refund)环节,所偿退的Gas数量也会加到这个GasPool里。

    Process()函数的核心是一个for循环,它将Block里的所有tx逐个遍历执行。具体的执行函数叫ApplyTransaction(),它每次执行tx, 会返回一个收据(Receipt)对象。Receipt结构体的声明如下:


    Receipt 中有一个Log类型的数组,其中每一个Log对象记录了Tx中一小步的操作。所以,每一个tx的执行结果一个Receipt对象来表示;更详细的内容,由一组Log对象来记录。这个Log数组很重要,比如在不同Ethereum节点(Node)的相互同步过程中,待同步区块的Log数组有助于验证同步中收到的block是否正确和完整,所以会被单独同步(传输)。

    Receipt的PostState保存了创建该Receipt对象时,整个Block内所有“帐户”的当时状态。Ethereum 里用stateObject来表示一个账户Account,这个账户可转帐(transfer value), 可执行tx, 它的唯一标示符是一个Address类型变量。 这个Receipt.PostState 就是当时所在Block里所有stateObject对象的RLP Hash值。

    Bloom类型是一个Ethereum内部实现的一个256bit长Bloom Filter。 Bloom Filter概念定义可见wikipedia,它可用来快速验证一个新收到的对象是否处于一个已知的大量对象集合之中。这里Receipt的Bloom,被用以验证某个给定的Log是否处于Receipt已有的Log数组中。

    2.1.2 消耗Gas,亦奖励Gas

    我们来看下StateProcessor.ApplyTransaction()的具体实现,它的基本流程如下图:


    ApplyTransaction()首先根据输入参数分别封装出一个Message对象和一个EVM对象,然后加上一个传入的GasPool类型变量,由TransitionDb()函数完成tx的执行,待TransitionDb()返回之后,创建一个收据Receipt对象,最后返回该Recetip对象,以及整个tx执行过程所消耗Gas数量。

    GasPool对象是在一个Block执行开始时创建,并在该Block内所有tx的执行过程中共享,对于一个tx的执行可视为“全局”存储对象; Message由此次待执行的tx对象转化而来,并携带了解析出的tx的(转帐)转出方地址,属于待处理的数据对象;EVM 作为Ethereum世界里的虚拟机(Virtual Machine),作为此次tx的实际执行者,完成转帐和合约(Contract)的相关操作。

    我们来细看下TransitioinDb()的执行过程(/core/state_transition.go)。假设有StateTransition对象st,  其成员变量initialGas表示初始可用Gas数量,gas表示即时可用Gas数量,初始值均为0,于是st.TransitionDb() 可由以下步骤展开:

    1. 购买Gas。首先从交易的(转帐)转出方账户扣除一笔Ether,费用等于tx.data.GasLimit * tx.data.Price;同时 st.initialGas = st.gas = tx.data.GasLimit;然后(GasPool) gp -= st.gas。
    2. 计算tx的固有Gas消耗 - intrinsicGas。它分为两个部分,每一个tx预设的消耗量,这个消耗量还因tx是否含有(转帐)转入方地址而略有不同;以及针对tx.data.Payload的Gas消耗,Payload类型是[]byte,关于它的固有消耗依赖于[]byte中非0字节和0字节的长度。最终,st.gas -= intrinsicGas
    3. EVM执行。如果交易的(转帐)转入方地址(tx.data.Recipient)为空,调用EVM的Create()函数;否则,调用Call()函数。无论哪个函数返回后,更新st.gas。
    4. 计算本次执行交易的实际Gas消耗: requiredGas = st.initialGas - st.gas
    5. 偿退Gas。它包括两个部分:首先将剩余st.gas 折算成Ether,归还给交易的(转帐)转出方账户;然后,基于实际消耗量requiredGas,系统提供一定的补偿,数量为refundGas。refundGas 所折算的Ether会被立即加在(转帐)转出方账户上,同时st.gas += refundGas,gp += st.gas,即剩余的Gas加上系统补偿的Gas,被一起归并进GasPool,供之后的交易执行使用。
    6. 奖励所属区块的挖掘者:系统给所属区块的作者,亦即挖掘者账户,增加一笔金额,数额等于 st.data,Price * (st.initialGas - st.gas)。注意,这里的st.gas在步骤5中被加上了refundGas, 所以这笔奖励金所对应的Gas,其数量小于该交易实际消耗量requiredGas。

    由上可见,除了步骤3中EVM 函数的执行,其他每个步骤都在围绕着Gas消耗量作文章(EVM 虚拟机的运行原理容后再述)。到这里,大家可以对Gas在以太坊系统里的作用有个初步概念,Gas就是Ethereum系统中的血液。

    步骤5的偿退机制很有意思,设立它的目的何在?目前为止我只能理解它可以避免交易执行过程中过快消耗Gas,至于对其全面准确的理解尚需时日。

    步骤6就更有趣了,正是这个奖励机制的存在才会吸引社会上的矿工(miner)去卖力“挖矿”(mining)。越大的运算能力带来越多的的区块(交易)产出,矿工也就能通过该奖励机制赚取越多的以太币。

    2.1.3 交易的数字签名

    Ethereum 中每个交易(transaction,tx)对象在被放进block时,都是经过数字签名的,这样可以在后续传输和处理中随时验证tx是否经过篡改。Ethereum 采用的数字签名是椭圆曲线数字签名算法(Elliptic Cure Digital Signature Algorithm,ECDSA)。ECDSA 相比于基于大质数分解的RSA数字签名算法,可以在提供相同安全级别(in bits)的同时,仅需更短的公钥(public key)。关于ECDSA的算法理论和实现细节,本系列会有另外一篇文章专门加以介绍。这里需要特别留意的是,tx的转帐转出方地址,就是对该tx对象作ECDSA签名计算时所用的公钥publicKey

    Ethereum中的数字签名计算过程所生成的签名(signature), 是一个长度为65bytes的字节数组,它被截成三段放进tx中,前32bytes赋值给成员变量R, 再32bytes赋值给S,末1byte赋给V,当然由于R、S、V声明的类型都是*big.Int, 上述赋值存在[]byte -> big.Int的类型转换。


    当需要恢复出tx对象的转帐转出方地址时(比如在需要执行该交易时),Ethereum 会先从tx的signature中恢复出公钥,再将公钥转化成一个common.Address类型的地址,signature由tx对象的三个成员变量R,S,V转化成字节数组[]byte后拼接得到。

    Ethereum 对此定义了一个接口Signer, 用来执行挂载签名,恢复公钥,对tx对象做哈希等操作。

    [plain] view plain copy
    1. // core/types/transaction_signing.go  
    2. type Signer innterface {  
    3.     Sender(tx *Transaction) (common.Address, error)  
    4.     SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error)  
    5.     Hash(tx *Transaction) common.Hash  
    6.     Equal(Signer) bool  
    7. }  

    生成数字签名的函数叫SignTx(),它会先调用其他函数生成signature, 然后调用tx.WithSignature()将signature分段赋值给tx的成员变量R,S,V。

    [plain] view plain copy
    1. func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error)  

    恢复出转出方地址的函数叫Sender(), 参数包括一个Signer, 一个Transaction,代码如下:

    [plain] view plain copy
    1. func Sender(signer Signer, tx *Transaction) (common.Address, error) {  
    2.     if sc := tx.from().Load(); sc != null {  
    3.         sigCache := sc.(sigCache)// cache exists,  
    4.         if sigCache.signer.Equal(signer) {  
    5.             return sigCache.from, nil  
    6.         }   
    7.     }  
    8.     addr, err := signer.Sender(tx)  
    9.     if err != nil {  
    10.         return common.Address{}, err  
    11.     }  
    12.     tx.from.Store(sigCache{signer: signer, from: addr}) // cache it  
    13.     return addr, nil  
    14. }  
    Sender()函数体中,signer.Sender()会从本次数字签名的签名字符串(signature)中恢复出公钥,并转化为tx的(转帐)转出方地址。

    在上文提到的ApplyTransaction()实现中,Transaction对象需要首先被转化成Message接口,用到的AsMessage()函数即调用了此处的Sender()。

    [plain] view plain copy
    1. // core/types/transaction.go  
    2. func (tx *Transaction) AsMessage(s Signer) (Message,error) {  
    3.     msg := Message{  
    4.         price: new(big.Int).Set(tx.data.price)  
    5.         gasLimit: new(big.Int).Set(tx.data.GasLimit)  
    6.         ...  
    7.     }  
    8.     var err error  
    9.     msg.from, err = Sender(s, tx)  
    10.     return msg, err  
    11. }  

    在Transaction对象tx的转帐转出方地址被解析出以后,tx 就被完全转换成了Message类型,可以提供给虚拟机EVM执行了。

    2.2 虚拟机内

    每个交易(Transaction)带有两部分内容需要执行:1. 转帐,由转出方地址向转入方地址转帐一笔以太币Ether; 2. 携带的[]byte类型成员变量Payload,其每一个byte都对应了一个单独虚拟机指令。这些内容都是由EVM(Ethereum Virtual Machine)对象来完成的。EVM 结构体是Ethereum虚拟机机制的核心,它与协同类的UML关系图如下:


    其中Context结构体分别携带了Transaction的信息(GasPrice, GasLimit),Block的信息(Number, Difficulty),以及转帐函数等,提供给EVM;StateDB 接口是针对state.StateDB 结构体设计的本地行为接口,可为EVM提供statedb的相关操作; Interpreter结构体作为解释器,用来解释执行EVM中合约(Contract)的指令(Code)。

    注意,EVM 中定义的成员变量Context和StateDB, 仅仅声明了变量名而无类型,而变量名同时又是其类型名,在Golang中,这种方式意味着宗主结构体可以直接调用该成员变量的所有方法和成员变量,比如EVM调用Context中的Transfer()。

    2.2.1 完成转帐

    交易的转帐操作由Context对象中的TransferFunc类型函数来实现,类似的函数类型,还有CanTransferFunc, 和GetHashFunc。

    [plain] view plain copy
    1. // core/vm/evm.go  
    2. type {  
    3.     CanTransferFunc func(StateDB, common.Address, *big.Int)  
    4.     TransferFunc func(StateDB, common.Address, common.Address, *big.Int)  
    5.     GetHashFunc func(uint64) common.Hash  
    6. }   

    这三个类型的函数变量CanTransfer, Transfer, GetHash,在Context初始化时从外部传入,目前使用的均是一个本地实现:

    [plain] view plain copy
    1. // core/evm.go  
    2. func NewEVMContext(msg Message, header *Header, chain ChainContext, author *Address){  
    3.     return vm.Context {  
    4.         CanTransfer: CanTransfer,  
    5.         Transfer: Transfer,  
    6.         GetHash: GetHash(header, chain),  
    7.         ...  
    8.     }  
    9. }  
    10.   
    11. func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) {  
    12.     return db.GetBalance(addr).Cmp(amount) >= 0  
    13. }  
    14. func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) {  
    15.     db.SubBalance(sender, amount)  
    16.     db.AddBalance(recipient, amount)  
    17. }  
    可见目前的转帐函数Transfer()的逻辑非常简单,转帐的转出账户减掉一笔以太币,转入账户加上一笔以太币。由于EVM调用的Transfer()函数实现完全由Context提供,所以,假设如果基于Ethereum平台开发,需要设计一种全新的“转帐”模式,那么只需写一个新的Transfer()函数实现,在Context初始化时赋值即可。

    有朋友或许会问,这里Transfer()函数中对转出和转入账户的操作会立即生效么?万一两步操作之间有错误发生怎么办?答案是不会立即生效。StateDB 并不是真正的数据库,只是一行为类似数据库的结构体。它在内部以Trie的数据结构来管理各个基于地址的账户,可以理解成一个cache;当该账户的信息有变化时,变化先存储在Trie中。仅当整个Block要被插入到BlockChain时,StateDB 里缓存的所有账户的所有改动,才会被真正的提交到底层数据库。

    2.2.2 合约的创建和赋值

    合约(Contract)是EVM用来执行(虚拟机)指令的结构体。先来看下Contract的定义:

    [plain] view plain copy
    1. // core/vm/contract.go  
    2. type ContractRef interface {  
    3.     Address() common.Address  
    4. }  
    5. type Contract struct {  
    6.     CallerAddress common.Address  
    7.     caller ContractRef  
    8.     self ContractRef  
    9.   
    10.     jumpdests destinations  
    11.     Code []byte  
    12.     CodeHash common.Hash  
    13.     CodeAddr *Address  
    14.     Input []byte  
    15.     Gas uint64  
    16.     value *big.Int  
    17.     Args []byte  
    18.     DelegateCall bool  
    19. }  
    在这些成员变量里,caller是转帐转出方地址(账户),self是转入方地址,不过它们的类型都用接口ContractRef来表示;Code是指令数组,其中每一个byte都对应于一个预定义的虚拟机指令;CodeHash 是Code的RLP哈希值;Input是数据数组,是指令所操作的数据集合;Args 是参数。

    有意思的是self这个变量,为什么转入方地址要被命名成self呢? Contract实现了ContractRef接口,返回的恰恰就是这个self地址。

    [plain] view plain copy
    1. func (c *Contract) Address() common.Address {  
    2.     return c.self.Address()  
    3. }  

    所以当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。

    [plain] view plain copy
    1. func (self *StateDB) SetCode(addr common.Address, code []byte)  
    2. func (self *StateDB) GetCode(addr common.Address) code []byte  

    stateObject 是Ethereum里用来管理一个账户所有信息修改的结构体,它以一个Address类型变量为唯一标示符。StateDB 在内部用一个巨大的map结构来管理这些stateObject对象。所有账户信息-包括Ether余额,指令数组Code, 该账户发起合约次数nonce等-它们发生的所有变化,会首先缓存到StateDB里的某个stateObject里,然后在合适的时候,被StateDB一起提交到底层数据库。注意,一个Contract所对应的stateObject的地址,是Contract的self地址,也就是转帐的转入方地址

    EVM 目前有五个函数可以创建并执行Contract,按照作用和调用方式,可以分成两类:

    • Create(), Call(): 二者均在StateProcessor的ApplyTransaction()被调用以执行单个交易,并且都有调用转帐函数完成转帐
    • CallCode(), DelegateCall(), StaticCall():三者由于分别对应于不同的虚拟机指令(1 byte)操作,不会用以执行单个交易,也都不能处理转帐

    考虑到与执行交易的相关性,这里着重探讨Create()和Call()。先来看Call(),它用来处理(转帐)转入方地址不为空的情况:


    Call()函数的逻辑可以简单分为以上6步。其中步骤(3)调用了转帐函数Transfer(),转入账户caller, 转出账户addr;步骤(4)创建一个Contract对象,并初始化其成员变量caller, self(addr), value和gas; 步骤(5)赋值Contract对象的Code, CodeHash, CodeAddr成员变量;步骤(6) 调用run()函数执行该合约的指令,最后Call()函数返回。相关代码可见:

    [plain] view plain copy
    1. // core/vm/evm.go  
    2. func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftGas *big.Int, error){  
    3.     ...  
    4.     var snapshot = evm.StateDB.Snapshot()  
    5.     contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))  
    6.     ret, err = run(evm, snapshot, contract, input)  
    7.     return ret, contract.Gas, err  
    8. }  
    因为此时(转帐)转入地址不为空,所以直接将入参addr初始化Contract对象的self地址,并可从StateDB中(其实是以addr标识的账户stateObject对象)读取出相关的Code和CodeHash并赋值给contract的成员变量。注意,此时转入方地址参数addr同时亦被赋值予contract.CodeAddr。

    再来看看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。

    2.2.3 预编译的合约

    EVM中执行合约(指令)的函数是run(),其实现代码如下:

    [plain] view plain copy
    1. // core/vm/evm.go  
    2. func run(evm *EVM, snapshot int, contract *Contract, input []byte) ([]byte, error) {  
    3.     if contract.CodeAddr != nil {  
    4.         precompiles := PrecompiledContractsHomestead  
    5.         ...  
    6.         if p := precompiles[*contract.CodeAddr]; p != nil {  
    7.             return RunPrecompiledContract(p, input, contract)  
    8.         }  
    9.     }  
    10.     return evm.interpreter.Run(snapshot, contract, input)  
    11. }  

    可见如果待执行的Contract对象恰好属于一组预编译的合约集合-此时以指令地址CodeAddr为匹配项-那么它可以直接运行;没有经过预编译的Contract,才会由Interpreter解释执行。这里的"预编译",可理解为不需要编译(解释)指令(Code)。预编译的合约,其逻辑全部固定且已知,所以执行中不再需要Code,仅需Input即可。

    在代码实现中,预编译合约只需实现两个方法Required()和Run()即可,这两方法仅需一个入参input。

    [plain] view plain copy
    1. // core/vm/contracts.go  
    2. type PrecompiledContract interface {  
    3.     RequiredGas(input []byte) uint64  
    4.     Run(input []byte) ([]byte, error)  
    5. }  
    6. func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {  
    7.     gas := p.RequiredGas(input)  
    8.     if contract.UseGas(gas) {  
    9.         return p.Run(input)  
    10.     }  
    11.     return nil, ErrOutOfGas  
    12. }  
    目前,Ethereuem 代码中已经加入了多个预编译合约,功能覆盖了包括椭圆曲线密钥恢复,SHA-3(256bits)哈希算法,RIPEMD-160加密算法等等。相信基于自身业务的需求,二次开发者完全可以加入自己的预编译合约,大大加快合约的执行速度。

    2.2.4 解释器执行合约的指令

    解释器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中读取。

    3. 小结

    以太坊的出现大大晚于比特币,虽然明显受到比特币系统的启发,但在整个功能定位和设计架构上却做了很多更广更深的思考和尝试。以太坊更像是一个经济活动平台,而并不局限一种去中心化数字代币的产生,分发和流转。本文从交易执行的角度切入以太坊的系统实现,希望能提供一点管中窥豹的作用。

    • Gas是Ethereum系统的血液。一切资源,活动,交互的开销,都以Gas为计量单元。如果定义了一个GasPrice,那么所有的Gas消耗亦可等价于以太币Ether。
    • Block是Transaction的集合。Block在插入BlockChain前,需要将所有Transaction逐个执行。Transaction的执行会消耗发起方的Ether,但系统在其执行完成时,会给予其作者(挖掘出这个Block的账户)一笔补偿,这笔补偿是“矿工”赚取收入的来源之一。
    • Ethereum 定义了自己的虚拟机EVM, 它与合约(Contract)机制相结合,能够在提供非常丰富的操作的同时,又能很好的控制存储空间和运行速度。Contract由Transaction转化得到。
    • Ethereum 里的哈希函数,用的是SHA-3,256 bits;数据(数组)的序列化,用的是RLP编码,所以所有对象,数组的哈希算法,实际用的RLP + SHA-3。数字签名算法,使用了椭圆曲线数字签名算法(ECDSA)
    展开全文
  • 以太坊虚拟机(EVM) 以太坊虚拟机 EVM 是智能合约的运行环境 作为区块验证协议的一部分,参与网络的每个节点都会运行EVM。他们会检查正在验证的块中列出的交易,并运行由EVM中的交易触发的代码 EVM不仅是沙盒封装...

    以太坊虚拟机(EVM)

    • 以太坊虚拟机 EVM 是智能合约的运行环境
    • 作为区块验证协议的一部分,参与网络的每个节点都会运行EVM。他们会检查正在验证的块中列出的交易,并运行由EVM中的交易触发的代码
    • EVM不仅是沙盒封装的,而且是完全隔离的,也就是说在EVM 中运行的代码是无法访问网络、文件系统和其他进程的,甚至智能合约之间的访问也是受限的
    • 合约以字节码的格式(EVM bytecode)存在于区块链上
    • 合约通常以高级语言(solidity)编写,通过EVM编译器编译为字节码,最终通过客户端上载部署到区块链网络中
    展开全文
  • 来源 | 区块链研究实验室封图 | CSDN 付费下载于视觉中国以太坊存储机制在EVM中允许执行智能合约代码。合约状态或内存存储在智能合约地址中。可以将这种存储视为位于智能合约地址的无限...
  • 以太坊虚拟机介绍

    千次阅读 2018-08-28 11:26:40
    以太坊虚拟机介绍 近期打算写一些关于以太坊虚拟机(后面简称EVM)的文章,这是其中的第一篇。这一系列文章想站在EVM指令集的角度,带领读者逐步理解EVM工作原理,进而理解以太坊区块链技术细节。由于网上介绍以太...
  • [以太坊源代码分析] I.区块和交易,合约和虚拟机

    万次阅读 多人点赞 2017-10-16 21:11:21
    打算写一个系列文章,基于源代码分析下以太...这是第一篇,介绍了区块,交易等基本概念,并从执行交易的角度,简单介绍了交易同合约的设计,执行合约的完整流程,以太坊虚拟机的指令集设计,以及用到的数字签名算法等。
  • go-ethereum以太坊源码解析完整版

    万次阅读 2018-07-23 14:17:54
    go-ethereum-code-analysis 目录 go-ethereum代码阅读环境搭建 以太坊黄皮书 符号索引 rlp源码解析 trie源码分析 ethdb源码分析 rpc源码分析 p2p源码分析 ...以太坊的trie树管理 回滚等操作 state源码...
  • 虚拟机作端口映射,下面以虚拟机内部的以太坊监控页面为例子 点击VMware虚拟机编辑->虚拟网络编辑器 点击更改设置,获得更改VMnet8适配器权限 选中VMnet8适配器,点击NAT设置 点击添加按钮 设置主机端口...
  • go-ethereum-code-analysis 以太坊源码分析

    万次阅读 2018-02-11 16:53:57
    目录go-ethereum代码阅读环境搭建以太坊黄皮书 符号索引rlp源码解析trie源码分析ethdb源码分析rpc源码分析p2p源码分析eth协议源码分析core源码分析区块链索引 chain_indexer源码分析布隆过滤器索引 bloombits源码...
  • linux搭建以太坊

    千次阅读 2018-03-01 13:47:55
    准备工具: 虚拟机:VMware Workstation Pro 14 Ubuntu 桌面版系统镜像:...以太坊钱包版本: geth-linux-amd64-1.7.3-4bb3c89d.tar 安装后ubuntu预先安装指令 红色字体为执行的指令! 1.更新包 root@ubuntu:/...
  • 以太经典ETC的客户端介绍(一)

    万次阅读 2018-02-26 11:04:21
    ETC客户端两个主要的客户端软件是Geth和Parity,目前开发团队正在开发一个全新的客户端及钱包Emerald,已经...ETC客户端通过运行EVM虚拟机,类似Java虚拟机和.NET平台环境,将你的计算机变成了控制绝大多数的ETC区...
  • EOS系列 - EVM和WASM的基本原理

    千次阅读 2019-11-26 23:30:25
    EVM是以太坊图灵完备的虚拟机(Ethereum Virtual Machine), 简称EVM 由程序翻译指令并执行 EVM出于所谓运算速度和效率方面考虑,采用了非主流的256bit整数 不支持浮点数 缺乏标准库支持,例如字符串拼接、切割、查找...
  • 参考:https://blog.csdn.net/fengzizhuang/article/details/12757389 编辑vmware虚拟机,在编辑下-虚拟网络编辑器-点击重置默认设置;
  • 图解以太坊虚拟机EVM

    千次阅读 2018-11-06 14:17:04
    今天聊一聊以太坊虚拟机的原理。 以太坊虚拟机,简称EVM,是用来执行以太坊上的交易的。业务流程参见下图: 输入一笔交易,内部会转换成一个Message对象,传入EVM执行。 如果是一笔普通转账交易,那么直接修改...
  • geth---搭建多节点私有链 1、动态加入节点 (1)先看本地网络配置...可以看到,虚拟机A的IP:192.168.209.133 虚拟机B的IP:192.168.209.134 (2)将上一文中私有链搭建中所新建genesis.json、keystore文...
  • 好巧合啊

    千次阅读 2017-03-17 21:41:30
    下图是我在8月31号发在巴比特网站的区块链技术详解...我用以太坊为例子讲解区块链2.0架构,因此在智能合约层用了一个以太坊专有的术语EVM(以太坊虚拟机),严格的说,这个架构是以太坊的架构,难道工信部白皮书对于区
  • 以太坊核心概念(一)

    千次阅读 2019-04-09 00:58:58
    以太坊虚拟机(EVM) 以太坊虚拟机(EVM)是以太坊中智能合约的运行环境。它不仅被沙箱封装起来,事实上它被完全隔离,也就是说运行在EVM内部的代码不能接触到网络、文件系统或者其它进程。甚至智能合约之间也只有...
  • EOS从入门到精通(四)

    千次阅读 2018-02-28 09:27:01
    大家好,非常感谢参加《EOS从入门到精通》系列课程,我是王巨,今天是EOS技术白皮书解读的第四讲。我们来解读EOS白皮书的最后几部分。今天的内容相对于上一节课会简单一些...在我们熟悉的区块链产品比特币和以太坊中...
  • 什么是以太币?如何获取?

    万次阅读 2019-04-11 09:44:43
    以太币是以太坊中使用的货币名称,用于在以太坊虚拟机内支付计算。这通过为了以太币购买gas间接实现,在gas中有所解释。 面额 以太坊有一个面额的度量体系,用作以太币单位。每个面额都有自己独特的名字(有的是在...
1 2 3 4 5 ... 20
收藏数 5,322
精华内容 2,128
热门标签
关键字:

以太坊虚拟机