以太坊EVM源码分析之数据结构
EVM代码整体结构
EVM相关的源码目录结构:
~/go-ethereum-master/core/vm# tree
.
├── analysis.go // 分析合约字节码,标记是否是跳转目标(jumpdest)
├── analysis_test.go
├── common.go // 一些公用的方法
├── contract.go // 智能合约数据结构
├── contracts.go // 预编译合约集
├── contracts_test.go
├── doc.go
├── eips.go // 一些EIP的实现
├── errors.go // 列出执行时错误
├── evm.go // 执行器 提供一些对外接口
├── gas.go // call gas花费计算 一级指令耗费gas级别
├── gas_table.go // 各个指令对应的计算耗费gas的函数
├── gas_table_test.go
├── gen_structlog.go
├── instructions.go // 指令对应的执行函数
├── instructions_test.go
├── interface.go // StateDB接口、EVM调用约定基本接口CallContext
├── interpreter.go // 解释器 调用核心
├── intpool.go // int值池 用来加速bit.Int的分配。 辅助对象
├── intpool_test.go
├── int_pool_verifier_empty.go
├── int_pool_verifier.go
├── jump_table.go // 指令和指令操作(操作,花费,验证)对应表
├── logger.go // logger、Tracer 辅助对象
├── logger_json.go
├── logger_test.go
├── memory.go // 内存模型及其访问函数
├── memory_table.go // EVM 内存操作表 计算指令所需内存大小
├── opcodes.go // EVM指令集
├── stack.go // 堆栈及其方法
└── stack_table.go // 一些栈的辅助函数, minStack、maxStack等
~/go-ethereum-master/core# tree
.
├── evm.go // EVM用到的一些函数
├── gaspool.go // GasPool实时记录区块在执行交易期间可用的gas量
├── state_processor.go // 处理状态转移
├── state_transition.go // 状态转换模型
└── types // 一些核心数据结构
├── block.go // block、blockHeader
├── log.go // log
├── receipt.go // receipt
├── transaction.go // transaction、message
└── transaction_signing.go
这是网上找到的一张EVM模块的整体结构图,有些已经发生变化。到目前为止(2020.02.25),EVM的指令集版本已经有7个了。operation的字段也有一些改动。
core/vm/jump_table.go
// 指令集, 下面定义了7种指令集,针对7种不同的以太坊版本
var (
frontierInstructionSet = newFrontierInstructionSet()
homesteadInstructionSet = newHomesteadInstructionSet()
tangerineWhistleInstructionSet = newTangerineWhistleInstructionSet()
spuriousDragonInstructionSet = newSpuriousDragonInstructionSet()
byzantiumInstructionSet = newByzantiumInstructionSet()
constantinopleInstructionSet = newConstantinopleInstructionSet()
istanbulInstructionSet = newIstanbulInstructionSet()
)
EVM的代码结构要比想象的简单。EVM涉及的核心对象有解释器Interpreter
、解释器的配置选项Config
、为EVM提供辅助信息的执行上下文Context
以及用于完整状态查询的EVM数据库stateDB
。
从上图可以看出,EVM通过解释器运行智能合约,而解释器依赖于config
的核心结构:JumpTable [256]operation
,JumpTable
的下标是操作码,JumpTable[opCode]
对应的operation
对象存储了指令对应的处理逻辑, gas计算函数, 堆栈验证方法, memory使用的大小以及一些flag。
以太坊的不同版本对应着不同的JumpTable
,只有frontierInstructionSet
的初始化函数中初始化了基本指令的operation
对象,之后的版本都是对前一个版本的修修补补:首先生成前一个版本的指令,然后应用一些EIP,增加自己特有的指令,或者改动某些指令。例如最新的Istanbul
版本:
core/vm/jump_table.go
// newIstanbulInstructionSet returns the frontier, homestead
// byzantium, contantinople and petersburg instructions.
// 先初始化前一个版本Constantinople的指令集,然后应用一些EIP.
func newIstanbulInstructionSet() JumpTable {
instructionSet := newConstantinopleInstructionSet()
enable1344(&instructionSet) // ChainID opcode - https://eips.ethereum.org/EIPS/eip-1344
enable1884(&instructionSet) // Reprice reader opcodes - https://eips.ethereum.org/EIPS/eip-1884
enable2200(&instructionSet) // Net metered SSTORE - https://eips.ethereum.org/EIPS/eip-2200
return instructionSet
}
Contract
EVM是智能合约的运行时环境,因此我们有必要了解一下合约的结构以及比较重要的方法。
core/vm/contract.go
// ContractRef is a reference to the contract's backing object
// Contrtref是对背后的合约对象的引用
type ContractRef interface {
Address() common.Address // Address方法返回合约地址
}
// Contract represents an ethereum contract in the state database. It contains
// the contract code, calling arguments. Contract implements ContractRef
// Contract在状态数据库中表示一个以太坊合约。它包含合约代码,调用参数。
// Contract 实现 ContractRef接口
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是初始化此合约的调用者的结果。
// 然而,当“调用方法”被委托时,需要将此值初始化为调用者的调用者的地址。
CallerAddress common.Address
caller ContractRef // 调用者
self ContractRef // 合约自身
// JUMPDEST分析结果聚合。
// 实际是合约字节码对应字节是指令还是普通数据的分析结果,若是指令,则可以作为jumpdest。
jumpdests map[common.Hash]bitvec // Aggregated result of JUMPDEST analysis.
// 本地保存本合约的代码JUMPDEST分析结果,不保存在调用者上下文中
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 // 携带的数据,如交易的数额
}
构造函数
core/vm/contract.go
// NewContract returns a new contract environment for the execution of EVM.
// NewContract 为EVM的执行返回一个新的合约环境
func NewContract(caller ContractRef, object ContractRef, value *big.Int, gas uint64) *Contract {
// 初始化Contract对象
c := &Contract{
CallerAddress: caller.Address(), caller: caller, self: object}
// 将ContractRef接口类转换为Contarct具体类型,当成功标志为真时,
// 表示成功将接口转换为具体类型,否则表示该接口不是具体类型的实例。
if parent, ok := caller.(*Contract); ok {
// Reuse JUMPDEST analysis from parent context if available.
// 重用调用者上下文中的JUMPDEST
c.jumpdests = parent.jumpdests
} else {
// 初始化新的jumpdests
c.jumpdests = make(map[common.Hash]bitvec)
}
// Gas should be a pointer so it can safely be reduced through the run
// Gas应为一个指针,这样它可以通过run方法安全地减少
// This pointer will be off the state transition
// 这个指针将脱离状态转换
c.Gas = gas
// ensures a value is set
// 确保value被设置
c.value = value
return c //返回合约指针
}
方法
core/vm/contract.go
// Contract结构方法
// 判断跳转目标是否有效
func (c *Contract) validJumpdest(dest *big.Int) bool {
udest := dest.Uint64() //将目标转换为Uint64类型
// PC cannot go beyond len(code) and certainly can't be bigger than 63bits.
// PC大小不能超过代码长度,并且位数不大于63位
// Don't bother checking for JUMPDEST in that case.
// 在这种情况下,不必检查JUMPDEST。
if dest.BitLen() >= 63 || udest >= uint64(len(c.Code)) {
return false
}
// Only JUMPDESTs allowed for destinations
// 只有JUMPDEST可以成为跳转目标
if OpCode(c.Code[udest]) != JUMPDEST {
return false
}
// 下面的代码检查目的地的值是否是指令,而非普通数据
// Do we have a contract hash already?
// 我们已经有一个合约hash了吗?
if c.CodeHash != (common.Hash{
}) {
//若不是空hash
// Does parent context have the analysis?
// 调用者上下文是否已经有一个分析,c.jumpdests = parent.jumpdests
// go 中map 是引用类型,因此在这里c.jumpdests与parent.jumpdests指向同一结构,同一片内存区域
analysis, exist := c.jumpdests[c.CodeHash] // 查看元素是否存在
if !exist {
//若不存在
// Do the analysis and save in parent context
// 进行分析并保存在调用者上下文中
// We do not need to store it in c.analysis
// 不需要在c.analysis存储结果
analysis = codeBitmap(c.Code)
//由于Map是引用类型,改变c.jumpdests等同于改变parent.jumpdests,键是各自的代码hash
c.jumpdests[c.CodeHash] = analysis
}
// 检查跳转位置是否在代码段中,并返回结果
return analysis.codeSegment(udest)
}
// We don't have the code hash, most likely a piece of initcode not already
// in state trie. In that case, we do an analysis, and save it locally, so
// we don't have to recalculate it for every JUMP instruction in the execution
// However, we don't save it within the parent context
// 我们还没有代码hash, 很可能是因为一部分初试代码还没有保存到状态树中。
// 在那种情况下,我们进行代码分析并局部保存,在执行过程中我们就不必为每条跳转指令重新进行代码分析
// 然而,我们并没有将分析结果保存在调用者上下文中
// 一般是因为新合约创建,还未将合约写入状态数据库。
if c.analysis == nil {
c.analysis = codeBitmap(c.Code)
}
return c.analysis.codeSegment(udest)
}
// AsDelegate sets the contract to be a delegate call and returns the current
// contract (for chaining calls)
// 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 //返回当前合约
}
Contract在EVM中的使用
Transaction
被转换成Message
后传入EVM
,在调用EVM.Call
或者EVM.Create
时,会将Message
转换为Contract
对象,以便后续执行。转换过程如图所示,合约代码从相应的状态数据库地址获取,然后加载到合约对象中。
EVM
core/vm/evm.go
// EVM is the Ethereum Virtual Machine base object and provides
// the necessary tools to run a contract on the given state with
// the provided context. It should be noted that any error
// generated through any of the calls should be considered a
// revert-state-and-consume-all-gas operation, no checks on
// specific errors should ever be performed. The interpreter makes
// sure that any errors generated are to be considered faulty code.
// EVM 是以太坊虚拟机的基本对象,并且提供必要的工具,以便在给定的状态下使用提供的上下文运行合约。
// 应该注意的是,通过任何调用产生的任何错误都会导致状态回滚并消耗掉所有gas,
// 不应该执行任何对特定错误的检查。解释器确保产生的任何错误都被认为是错误代码。
//
// The EVM should never be reused and is not thread safe.
// EVM不应该被重用,而且也不是线程安全的。
type EVM struct {
// Context provides auxiliary blockchain related information
// Context提供区块链相关的辅助信息 提供访问当前区块链数据和挖矿环境的函数和数据
Context
// StateDB gives access to the underlying state
// StateDB 以太坊状态数据库对象 提供对底层状态的访问
StateDB StateDB
// Depth is the current call stack
// Depth 是当前调用堆栈
depth int
// ch