EVM

    • 待办清单
    • 结构与流程
      • 2020年版本的evm结构
      • 大致流程
    • opcodes.go
    • contract.go
    • analysis.go
    • stack.go
    • stack_table.go
    • Memory.go
    • Memory_table.go
    • EVM.go
      • 区块上下文
      • 交易上下文
      • EVM结构
      • 以太坊中的调用call、callcode和delegatecall
      • 创建合约
    • interpreter.go
    • jump_table.go
    • instructions.go
    • gas.go
    • gas_table.go
    • logger.go
    • contracts.go
    • common.go
    • eips.go
    • interface.go

待办清单

  • analysis.go
  • common.go
  • contract.go
  • contracts.go
  • doc.go
  • eips.go
  • errors.go
  • evm.go
  • gas.go
  • gas_table.go
  • instructions.go
  • interface.go
  • interpreter.go
  • jump_table.go
  • logger.go
  • memory.go
  • memory_table.go
  • opcodes.go
  • operations_acl.go
  • stack.go
  • stack_table.go

结构与流程

2020年版本的evm结构

大致流程

编写合约 > 生成abi > 解析abi得出指令集 > 指令通过opcode来映射成操作码集 > 生成一个operation[256]

以太坊虚拟机的工作流程:
由solidity语言编写的智能合约,通过编译器编译成bytecode,之后发到以太坊上,以太坊底层通过evm模块支持合约的执行和调用,调用时根据合约获取代码,即合约的字节码,生成环境后载入到 EVM 执行。


opcodes.go

OptionCode(操作码)
OpCode
文件opcodes.go中定义了所有的OpCode,该值是一个byte,合约编译出来的bytecode中,一个OpCode就是上面的一位。opcodes按功能分为9组,以第一位十六进制数来分类,例如0x1x,0x2x。
例如第一组为 算术 操作

// 0x0 range - arithmetic ops.const (STOP OpCode = 0x0ADDOpCode = 0x1MULOpCode = 0x2SUBOpCode = 0x3DIVOpCode = 0x4SDIV OpCode = 0x5MODOpCode = 0x6SMOD OpCode = 0x7ADDMOD OpCode = 0x8MULMOD OpCode = 0x9EXPOpCode = 0xaSIGNEXTEND OpCode = 0xb)

可以使用表格来总结

opCodeRange对应操作
0x0算术操作
0x10比较操作
0x20加密操作
0x30状态闭包
0x40区块操作
0x50存储和执行操作
0x60压栈操作
0x80克隆操作
0x90交换操作
0xa0日志操作
0xf0闭包

实现了判断能否压栈、操作码的byte类型和string类型互相转换的函数或接口。
func StringToOp(str string) OpCode
func (op OpCode) String() string
func (op OpCode) IsPush() bool


AddressLength = 20HashLength = 32type Address [AddressLength]bytetype bitvec [ ]byte// Hash represents the 32 byte Keccak256 hash of arbitrary data.type Hash [HashLength]byte

contract.go

该文件中包含了饭回合约的调用者信息和value、判断gas值是否足够运行合约执行、

合约的结构

type Contract struct {// CallerAddress is the result of the caller which initialised this// contract. However when the "call method" is delegated this value// needs to be initialised to that of the caller's caller.CallerAddress common.AddresscallerContractRefselfContractRefjumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis.analysisbitvec // Locally cached result of JUMPDEST analysisCode []byteCodeHash common.HashCodeAddr *common.AddressInput[]byteGas uint64value *big.Int}
func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uint64) *Contract

该函数构造了新的合约,且如果是被合约调用,则复用该合约的 jumpdests

func (c *Contract) validJumpdest(dest *uint256.Int) boolfunc (c *Contract) isCode(udest uint64) bool

存在两段校验的函数检验代码跳转是否合法以及


  • int类型的大小为 8 字节
  • int8类型大小为 1 字节
  • int16类型大小为 2 字节
  • int32类型大小为 4 字节
  • int64类型大小为 8 字节

analysis.go

func (c *Contract) AsDelegate() *Contract

AsDelegate将合约设置为委托调用并返回当前合同(用于链式调用)


stack.go

为了应对高并发情况下的栈资源问题,代码中创建了 栈池 来保存一些被创造但未使用的栈空间。

var stackPool = sync.Pool{New: func() interface{} {return &Stack{data: make([]uint256.Int, 0, 16)}},}

除了一些栈该有的基础操作以外,还有:

func (st *Stack) swap(n int) 将从栈顶开始数的第 n 个和栈顶元素交换

func (st *Stack) dup(n int) 复制栈顶元素,并将其压栈

func (st *Stack) Back(n int) *uint256.Int 返回栈底元素


stack_table.go

一些栈的辅助函数


Memory.go

type Memory struct {store []bytelastGasCost uint64}

为以太坊虚拟机提供一个简单存储的模型

func (m *Memory) Set(offset, size uint64, value []byte) func (m *Memory) Set32(offset uint64, val *uint256.Int) func (m *Memory) Resize(size uint64)func (m *Memory) GetCopy(offset, size int64) (cpy []byte)// 截取切片中的一段 (offset,offset+size)func (m *Memory) GetPtr(offset, size int64)// 返回切片中的一段的指针func (m *Memory) Len() intfunc (m *Memory) Data() []byte

Memory_table.go

衡量一些操作所消耗的内存大小同时判断是否会发生栈溢出,如keccak256、callDataCopy、MStore等


EVM.go

EVM机器位宽为256位,即32个字节,256位机器字宽不同于我们经常见到主流的64位的机器字宽

设计256位宽的原因:

  • 时间,智能合约是否能执行得更快
  • 空间,这样是否整体字节码的大小会有所减少gas成本

区块上下文

这里的Random same as difficulty(具体是什么还不知道)

前三个为函数类型,依次作用为 查询转账者账户是否有充足ether支持转账操作转账操作获取第n个区块的hash

其余为一些基础的区块信息,如币基交易地址、Gaslimit、区块高、时间戳、难度值和基础费用

区块一旦创建,区块信息不可以被修改

type BlockContext struct {// CanTransfer returns whether the account contains// sufficient ether to transfer the valueCanTransfer CanTransferFunc// Transfer transfers ether from one account to the otherTransfer TransferFunc// GetHash returns the hash corresponding to nGetHash GetHashFunc// Block informationCoinbasecommon.Address // Provides information for COINBASEGasLimituint64 // Provides information for GASLIMITBlockNumber *big.Int // Provides information for NUMBERTime*big.Int // Provides information for TIMEDifficulty*big.Int // Provides information for DIFFICULTYBaseFee *big.Int // Provides information for BASEFEERandom*common.Hash // Provides information for PREVRANDAO}

交易上下文

Origin是什么,就是第一个交易

type TxContext struct {// Message informationOrigin common.Address // Provides information for ORIGINGasPrice *big.Int // Provides information for GASPRICE}

EVM结构

evm是以太坊虚拟机基础对象,提供工具处理对应上下文中的交易。运行过程中一旦发生错误,状态会回滚并且不退还gas费用,运行中产生的任务错误都会被归结为代码错误。

type EVM struct // Context provides auxiliary blockchain related informationContext BlockContextTxContext// StateDB gives access to the underlying stateStateDB StateDB// Depth is the current call stackdepth int// chainconfig是决定区块链设置的核心配置。//chainconfig以块为单位存储在数据库中。这意味着//任何一个网络,通过它的起源块来识别,都可以有它自己的//一组配置选项。// 包含了chainId,该链什么时候发生硬分叉,该链难度总和到多少的时候发生更新等信息chainConfig *params.ChainConfig// chain rules contains the chain rules for the current epoch// rules包装了config信息,属于语法糖,是一次性接口,不应chainRules params.Rules// virtual machine configuration options used to initialise the// evm.// 解释器的配置信息Config Config// global (to this context) ethereum virtual machine// used throughout the execution of the tx.interpreter *EVMInterpreter// abort is used to abort the EVM calling operations// NOTE: must be set atomically// 能够终止evm调用操作abort int32// callGasTemp holds the gas available for the current call. This is needed because the// available gas is calculated in gasCall* according to the 63/64 rule and later// applied in opCall*.callGasTemp uint64}

创建evm,只能用一次

func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM 

reset EVM的交易上下文和状态数据库

func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) 

能够通过原子的修改abort使得取消任何evm操作

func (evm *EVM) Cancel()func (evm *EVM) Cancelled() bool 

合约预编译的作用

预编译合约是 EVM 中用于提供更复杂库函数(通常用于加密、散列等复杂操作)的一种折衷方法,这些函数不适合编写操作码。 它们适用于简单但经常调用的合约,或逻辑上固定但计算量很大的合约。 预编译合约是在使用节点客户端代码实现的,因为它们不需要 EVM,所以运行速度很快。 与使用直接在 EVM 中运行的函数相比,它对开发人员来说成本也更低。

evm调用深度 <= 1024

evm调用contract的步骤

  • 判断调用深度是否大于1024
  • 判断是否有充足的余额支持调用
  • 进行快照和预编译
  • 检查该地址是否在状态数据库中存在
  • 若不存在,调用一个不存在的帐户,不要做任何事情,只需ping跟踪程序,检查是否是debug模式,若不是则会创建账户
  • 判断是否预编译,若是调用节点客户端代码实现;反之,创建合约对象并加载被调用地址和地址的hash以及代码信息,后用解释器来运行
  • 若运行过程中有任何错误,则状态将会回滚到操作前快照处,并消耗gas

以太坊中的调用call、callcode和delegatecall

调用方式修改的storage调用者的msg.sender被调用者的msg.sender执行的上下文
call被调用者的storage交易发起者的地址调用者的地址被调用者
callcode调用者的storage调用者的地址调用者的地址调用者
delegatecall调用者的storage交易发起者的地址调用者的地址调用者

还有staticCall调用过程中不允许进行任何修改操作,可以用view来修饰,因此在函数实现中会给解释器的运行函数中的read-only参数传入true值。

创建合约

nonce值指定交易数,每发起一笔交易确认后nonce值+1


interpreter.go

解释器中会有一个配置结构体,能够选择debug模式,包含追踪操作码的evm日志,一些eip提议的配置,evm跳表

type Config struct {Debug bool// Enables debuggingTracerEVMLogger // Opcode loggerNoBaseFee bool// Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)EnablePreimageRecording bool// Enables recording of SHA3/keccak preimagesJumpTable *JumpTable // EVM instruction table, automatically populated if unsetExtraEips []int // Additional EIPS that are to be enabled}

范围上下文

解释器结构,包含evm指针,配置信息,hasher??,是否只读,返回数据信息

type EVMInterpreter struct {evm *EVMcfg Confighashercrypto.KeccakState // Keccak256 hasher instance shared across opcodeshasherBuf common.Hash// Keccak256 hasher result array shared aross opcodesreadOnly bool // Whether to throw on stateful modificationsreturnData []byte // Last CALL's return data for subsequent reuse}
func NewEVMInterpreter(evm *EVM, cfg Config) *EVMInterpreter

传入evm和配置信息构建新的解释器,根据配置信息设置该链的规则,如遵循eip158、eip150提议。

func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error)

会控制解释器堆栈调用的深度的加减,同时会用传入的合约、和栈池中调用一个栈来创建一个全新的上下文,并为它新建一个memory模型。

run函数中主要部分是处理一个死循环,只会在停止、返回、自毁和出错的时候停止。

通过循环一直执行合约中的操作,并且每次执行之前都要验证堆栈是否在限制范围之中,还要计算由于动态使用空间导致的动态gas费用,检查完这些之后才会由operation来执行操作。


jump_table.go

operation结构体

type operation struct {execute executionFuncconstantGas uint64dynamicGasgasFuncminStack int // 堆栈中需要已有多少项maxStack int //堆栈中最多能有多少项(否则执行这个操作的时候会溢出)memorySize memorySizeFunc// 返回该操作需要的内存大小}

其中 executionFunc 有四处实现

func makeLog(size int) executionFuncfunc makePush(size uint64, pushByteSize int) executionFuncfunc makeDup(size int64) executionFuncfunc makeSwap(size int64) executionFunc

memorySizeFunc 的实现在memory_table.go文件中

// memory_table.gofunc memoryMLoad(stack *Stack) (uint64, bool) {return calcMemSize64WithUint(stack.Back(0), 32)}func memoryMStore8(stack *Stack) (uint64, bool) {return calcMemSize64WithUint(stack.Back(0), 1)}func memoryMStore(stack *Stack) (uint64, bool) {return calcMemSize64WithUint(stack.Back(0), 32)}// common.gofunc calcMemSize64WithUint(off *uint256.Int, length64 uint64) (uint64, bool) 

我们可以看到这几个比较熟悉的操作,MLoad、MStore、MStore8 从栈中拿出 偏移量offset地址 + length

查看是否溢出 uint64

Solidity的内存布局将前4个32字节的插槽保留

  • 0x00 – 0x3f (64bytes): 暂存空间(Scratch space)
  • 0x40 – 0x5f (32bytes): 空闲内存指针
  • 0x60 – 0x7f (32bytes): 0 插槽值

他们的作用分别是

  • 用来给hash方法和内联汇编使用
  • 记录当前已经分配的内存大小,空闲内存的起始值为0x80
  • 用作动态内存的初始值,不会被使用

jumpTable包含指向操作的指针

type JumpTable [256]*operation
func validate(jt JumpTable) JumpTable

检查jumpTable中的操作是否为空

我们知道 opsCode是代码的解释器,这里的operation就是opsCode的解释器,interpreter中有一个jumptable,它包含了指向操作的指针,jumptable中的操作就是对应opscode的操作,但是在不同的config配置下,操作集合也会遵循不同的规则。

例如我们可以看看部分代码

// jump_table.gofunc newFrontierInstructionSet() JumpTable {tbl := JumpTable{STOP: {execute: opStop,constantGas: 0,minStack:minStack(0, 0),maxStack:maxStack(0, 0),},ADD: {execute: opAdd,constantGas: GasFastestStep,minStack:minStack(2, 1),maxStack:maxStack(2, 1),},......RETURN: {execute:opReturn,dynamicGas: gasReturn,minStack: minStack(2, 0),maxStack: maxStack(2, 0),memorySize: memoryReturn,},SELFDESTRUCT: {execute:opSelfdestruct,dynamicGas: gasSelfdestruct,minStack: minStack(1, 0),maxStack: maxStack(1, 0),},}// Fill all unassigned slots with opUndefined.// 将所有没有指定的插槽填充为 未定义操作for i, entry := range tbl {if entry == nil {tbl[i] = &operation{execute: opUndefined, maxStack: maxStack(0, 0)}}}return validate(tbl)}// opscode.go// 0x0 range - arithmetic ops.const (STOP OpCode = 0x0ADDOpCode = 0x1......EXPOpCode = 0xaSIGNEXTEND OpCode = 0xb)

instructions.go

指令集合,封装了操作指定过程中的堆栈操作。


gas.go

const ( GasQuickStep uint64 = 2 GasFastestStep uint64 = 3 GasFastStepuint64 = 5 GasMidStep uint64 = 8 GasSlowStepuint64 = 10 GasExtStep uint64 = 20)

根据是否遵循EIP150,返回实际的调用产生的费用


gas_table.go

func memoryGasCost(mem *Memory, newMemSize uint64) (uint64, error)

一些操作的gas值计算,如自毁、Call、Callcode、delegateCall、staticCall、内存存储等。


logger.go

EVM的日志接口,由不同的tracer或者logger实现。用于从EVM交易执行中收集执行时的信息,能够追踪交易级、调用级、opcode操作码级的信息。


contracts.go

存放预编译好的合约


common.go

存放常用工具方法

func calcMemSize64(off, l *uint256.Int) (uint64, bool) func getData(data []byte, start uint64, size uint64) []byte func toWordSize(size uint64) uint64func allZero(b []byte) bool

计算内存空间是否溢出、根据给的参数返回数据切片、 返回内存扩展所需的字的大小、判断是否全0


eips.go

实现了许多eip协议的配置函数,可以通过函数的方式使能跳转表,使其能够遵循某个eip规则。


interface.go

包含stateDBCallContext两种接口,


在evm上,evm字节码是可执行的代码,合约abi是与EVM字节码交互的接口。

ppt中首先介绍web3js , contract abi – json format , evm

contract function 用来支持外部调用,使得应用-合约能够交互,使得合约-合约之间可以联系。

evm bytecode 对应EVM中的一系列的opcode指令

前4个byte是函数名的keccak256的前4个byte 后32byte是十六进制参数 左边用0补齐

所以这个bytecode是4 + 32 = 36 byte