evm源码分析分为3篇去讲解,所有的代码解析基于以太坊go-ethereum-1.8.23-release
源码结构
runtime 包下的文档在实际运行的geth客户端中并没有被调用到,只是作为开发人员测试使用。
-
core/vm/runtime/env.go
设置evm运行环境,并返回新的evm对象 -
core/vm/runtime/fuzz.go
fuzz使得开发者可以随机测试evm代码,详见go-fuzz
工具 -
core/vm/runtime/runtime.go
设置evm运行环境,并执行相应的evm代码
下面的文档为实际的使用到的evm的代码文档
-
core/vm/analysis.go
分析命令跳转目标 -
core/vm/common.go
存放常用工具方法 -
core/vm/contract.go
合约数据结构 -
core/vm/contracts.go
存放预编译好的合约 -
core/vm/errors.go
定义一些常见错误 -
core/vm/evm.go
evm对于解释器提供的一些操作接口 -
core/vm/gas.go
计算一级命令耗费gas -
core/vm/gas_table.go
各种操作的gas消耗计算表 -
core/vm/gen_structlog.go
生成structlog的 序列化json和反序列化方法 -
core/vm/instructions.go
所有命令集的实现函数 -
core/vm/interface.go
定义常用操作接口 -
core/vm/interpreter.go
evm 命令解释器 -
core/vm/intpool.go
常量池 -
core/vm/jump_table.go
命令跳转表 -
core/vm/logger.go
状态日志 -
core/vm/logger_json.go
json形式日志 -
core/vm/memory.go
evm 可操作内存 -
core/vm/memory_table.go
evm内存操作表,衡量一些操作耗费内存大小 -
core/vm/opcodes.go
定义操作码的名称和编号 -
core/vm/stacks.go
evm栈操作 -
core/vm/stack_table.go
evm栈验证函数
上述对要介绍的evm代码进行了一个简单的介绍,接下来将详细的分析其中的代码。
从交易提交说起
我们将会从一个交易的提交开始讲起,当一个geth客户端接收到其他节点提交的交易后,它会首先将这笔交易提交给evm进行处理。
1 |
func (w *worker) (tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) { |
一笔交易提交到EVM前的主要过程就是上述代码所描述的
- 创建当前stateDB的snapshot, 创建snapshot其实就是将leveldb的revisionId自增1,然后将这个revisionId加入到revisionId列表里,然后返回创建的id。
- 将交易发送到evm,执行交易, 这步骤后面会重点分析,这个就是我们这次文章主要分析的重点EVM的执行交易过程。
- 判断执行结果是否出错,如果出错,则回滚snapshot。 首先找到在revisionId列表里面找到需要回滚的revisionId, 然后将此revisionId里面的所有snapshot依次回滚。
- 将当前交易加入到交易列表
- 将交易收据加入到交易收据列表
接下来我们主要分析ApplyTransaction函数
1 |
|
AsMessage 函数
1 |
func (tx *Transaction) AsMessage(s Signer) (Message, error) { |
将tx 里面的数据填充到msg里面, 这个过程主要是将交易里面的form address 利用 ecrevoer函数恢复出来。
NewEVMContext 函数
1 |
// NewEVMContext creates a new context for use in the EVM. |
填充vm.Context的各项内容,并返回一个Context对象
NewEVM函数
1 |
func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM { |
这个函数主要是根据当前的区块号以及相关配置,设置EVM的解释器.这里可以看到以太坊已经在为后面EWASM 虚拟机做准备了。
ApplyMessage函数
1 |
// ApplyMessage 通过给定的message计算新的DB状态,继而改变旧的DB状态 |
这个函数的分为两个函数执行一个是NewStateTransition 函数,这个函数主要是设置一些交易执行的必要参数。
TransitionDb 这个函数则是主要负责执行交易,影响Db状态。
TransitionDb 函数
1 |
// TransitionDB 函数通过 apply message 将会改变state 并且返回 包含gas使用情况的结果。 |
-
preCheck 函数主要进行执行交易前的检查,目前包含下面两个步骤
1.1 检查msg 里面的nonce值与db里面存储的账户的nonce值是否一致。
1.2 buyGas方法主要是判断交易账户是否可以支付足够的gas执行交易,如果可以支付,则设置stateTransaction 的gas值 和 initialGas 值。并且从交易执行账户扣除相应的gas值。
-
先获取固定交易的基础费用,根据当前分叉版本和交易类型来决定基础费用,如果是创建合约则是至少是53000gas,如果是普通交易则至少是21000gas ,如果data部分不为空,则具体来说是按字节收费:字节为0的收4gas,字节不为0收68gas,所以你会看到很多做合约优化的,目的就是减少数据中不为0的字节数量,从而降低油费消耗。具体代码如下
1 |
//IntrinsicGas 计算给定数据的固定gas消耗 |
- 根据contractCreation这个变量判断这是一个普通交易还是一个合约创建交易,如果是合约创建交易,则会进入下面的代码
1 |
// Create creates a new contract using code as deployment code. |
crypto.CreateAddress 主要是利用账户地址和nonce进行rlp编码后取后20字节得到一个新的合约地址,因此合约地址其实是可以提前计算出来的,这也是很多合约地址是靓号的原因。
这一篇主要分析了交易执行前的一些准备工作,包括创建EVM,计算交易金额,设置交易对象,计算交易gas花销;下一篇主要是分析evm虚拟机解析器通过合约命令,执行智能合约代码的过程。
原文:大专栏 evm源码分析第一篇