EVM

EVM源码分析(二)

Posted by Thomas_Xu on 2023-02-23

EVM源码分析(二)


author:Thomas_Xu

在上一文中我们分析了EVM的代码架构以及主要的两个合约。而在交易交给EVM处理之前,其实还有一系列的数据转移操作。

这篇文章,我们将从交易入手,来看看以太坊究竟是怎么处理交易的。

从Geth客户端收到交易开始:

当一个geth客户端接收到其他节点提交的交易后,它会首先将这笔交易提交给evm进行处理。

commitTransaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  minner/worker.go

func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
snap := w.current.state.Snapshot()
//ApplyTransaction函数将一次交易的执行写入数据库,接下来会详细讲
receipt, _, err := core.ApplyTransaction(w.config, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig())
if err != nil {
w.current.state.RevertToSnapshot(snap)
return nil, err
}
w.current.txs = append(w.current.txs, tx)
w.current.receipts = append(w.current.receipts, receipt)

return receipt.Logs, nil
}

一笔交易提交到EVM前的主要过程就是上述代码所描述的

  1. 创建当前stateDB的snapshot, 创建snapshot其实就是将leveldbrevisionId自增1,然后将这个revisionId加入到revisionId列表里,然后返回创建的id。
  2. 将交易发送到evm,执行交易, 这步骤后面会重点分析,这个就是我们这次文章主要分析的重点EVM的执行交易过程。
  3. 判断执行结果是否出错,如果出错,则回滚snapshot。 首先找到在revisionId列表里面找到需要回滚的revisionId, 然后将此revisionId里面的所有snapshot依次回滚。
  4. 将当前交易加入到交易列表
  5. 将交易收据加入到交易收据列表

Process

Process是个入口函数,所有的交易都需要经过此函数来运行调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) {
var (
receipts types.Receipts
usedGas = new(uint64)
header = block.Header()
blockHash = block.Hash()
blockNumber = block.Number()
allLogs []*types.Log
gp = new(GasPool).AddGas(block.GasLimit()) //这里的gaspool时根据当前区块决定的,并且gp其实是一个指针
)
// Mutate the block and state according to any hard-fork specs
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb)
}
blockContext := NewEVMBlockContext(header, p.bc, nil)
/* 新建EVM实例
也创建了EVM解释器,解释器中会根据cgg的配置参数选择对应的jump table
*/
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg)
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
statedb.SetTxContext(tx.Hash(), i)
/*EVM入口*/
receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv)
/*注意在这里任何一个交易执行失败,都会直接返回err*/
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
}
// Fail if Shanghai not enabled and len(withdrawals) is non-zero.
withdrawals := block.Withdrawals()
if len(withdrawals) > 0 && !p.config.IsShanghai(block.Time()) {
return nil, nil, 0, fmt.Errorf("withdrawals before shanghai")
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles(), withdrawals)

return receipts, allLogs, *usedGas, nil
}

ApplyTransaction

接下来我们主要分析ApplyTransaction函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// core/state_processor.go

// ApplyTransaction 尝试将一次交易的执行写入数据库,并且为执行环境准备输入参数。它返回交易的收据。
// 如果gasg 使用完,或者交易执行出现error,则表示该块无效。
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, uint64, error) {
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil {
return nil, 0, err
}
// 创建一个新的evm执行上下文环境
context := NewEVMContext(msg, header, bc, author)
// 创建一个包含所有相关信息的新环境, 包括事务和调用机制
vmenv := vm.NewEVM(context, statedb, config, cfg)
/*
让EVM执行该交易
Params
@ vmenv:虚拟机实例
@ gp:gaspool

returns:
@ gas:该交易执行结束时使用了多少gas
*/
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)/* ApplyMessage 先生成了一个StateTransition对象,然后调用其TransitionDB方法开始交给虚拟机运行 */
if err != nil {
return nil, 0, err
}
// Update the state with pending changes
var root []byte
if config.IsByzantium(header.Number) {
statedb.Finalise(true)
} else {
root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
}
*usedGas += gas

//创建一个新的收据为这笔交易,存储中间状态根和gas使用情况
// based on the eip phase, we're passing whether the root touch-delete accounts.
receipt := types.NewReceipt(root, failed, *usedGas)
receipt.TxHash = tx.Hash()
receipt.GasUsed = gas
//如果这笔交易是创建一个合约, 存储合约地址在收据里面
if msg.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
}
// 设置收据日志和创建布隆过滤器
receipt.Logs = statedb.GetLogs(tx.Hash())
receipt.Bloom = types.CreateBloom(types.Receipts{receipt})

return receipt, gas, err
}

这里其实就出现了最核心的TransitionDb函数,我们后面会讲到

AsMessage 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// core/types/transactions.go
func (tx *Transaction) AsMessage(s Signer) (Message, error) {
msg := Message{
nonce: tx.data.AccountNonce,
gasLimit: tx.data.GasLimit,
gasPrice: new(big.Int).Set(tx.data.Price),
to: tx.data.Recipient,
amount: tx.data.Amount,
data: tx.data.Payload,
checkNonce: true,
}

var err error
msg.from, err = Sender(s, tx)
return msg, err
}

将tx 里面的数据填充到msg里面, 这个过程主要是将交易里面的form address 利用 ecrevoer函数恢复出来。

NewEVMContext 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//core/vm/evm.go

// NewEVMContext creates a new context for use in the EVM.
func NewEVMContext(msg Message, header *types.Header, chain ChainContext, author *common.Address) vm.Context {
//如果不能得到一个明确的author,那就从区块头里面解析author
var beneficiary common.Address
//如果函数参数里面的author为nil,则从区块头里面解析author, 这里不叫coinbase 主要是为了区别ehthash与clique引擎
if author == nil {
beneficiary, _ = chain.Engine().Author(header) // Ignore error, we're past header validation
} else {
beneficiary = *author
}
return vm.Context{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: GetHashFn(header, chain),
Origin: msg.From(),
Coinbase: beneficiary,
BlockNumber: new(big.Int).Set(header.Number),
Time: new(big.Int).Set(header.Time),
Difficulty: new(big.Int).Set(header.Difficulty),
GasLimit: header.GasLimit,
GasPrice: new(big.Int).Set(msg.GasPrice()),
}
}

填充vm.Context的各项内容,并返回一个Context对象

NewEVM函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func NewEVM(ctx Context, statedb StateDB, chainConfig *params.ChainConfig, vmConfig Config) *EVM {
evm := &EVM{
Context: ctx,
StateDB: statedb,
vmConfig: vmConfig,
chainConfig: chainConfig,
chainRules: chainConfig.Rules(ctx.BlockNumber),
interpreters: make([]Interpreter, 0, 1),
}

if chainConfig.IsEWASM(ctx.BlockNumber) {
// to be implemented by EVM-C and Wagon PRs.
// if vmConfig.EWASMInterpreter != "" {
// extIntOpts := strings.Split(vmConfig.EWASMInterpreter, ":")
// path := extIntOpts[0]
// options := []string{}
// if len(extIntOpts) > 1 {
// options = extIntOpts[1..]
// }
// evm.interpreters = append(evm.interpreters, NewEVMVCInterpreter(evm, vmConfig, options))
// } else {
// evm.interpreters = append(evm.interpreters, NewEWASMInterpreter(evm, vmConfig))
// }
panic("No supported ewasm interpreter yet.")
}

//vmConfig.EVMInterpreter 将会被使用在EVM-C, 这里不会使用。
//因为我们希望内置的EVM作为出错转移的备用选项
evm.interpreters = append(evm.interpreters, NewEVMInterpreter(evm, vmConfig))
evm.interpreter = evm.interpreters[0]

return evm
}

ApplyMessage函数

1
2
3
4
5
6
// ApplyMessage 通过给定的message计算新的DB状态,继而改变旧的DB状态
// ApplyMessage 返回EVM执行的返回结果和gas使用情况(包括gas refunds)和error(如果有错误出现)。
// 如果一个错误总是作为一个core error 出现,那么这个message将永远不会被这个区块所接受。
func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) {
return NewStateTransition(evm, msg, gp).TransitionDb()
}

这个函数的分为两个函数执行一个是NewStateTransition 函数,这个函数主要是设置一些交易执行的必要参数。

TransitionDb 这个函数则是主要负责执行交易,影响Db状态。

TransitionDb 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// core/state_transation.go

// TransitionDB 函数通过 apply message 将会改变state 并且返回 包含gas使用情况的结果。
// 如果执行失败,将会返回一个error, 这个error代表一个共识错误。
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
/*
检查nonce是否符合要求,检查账户是否足够支付gas fee
*/
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
/* msg.To() == ni 代表创建合约 */
contractCreation := msg.To() == nil

// Pay intrinsic gas
/* 计算固有成本gas */
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}

var (
evm = st.evm
// vm errors do not effect consensus and are therefor
// not assigned to err, except for insufficient balance
// error.
vmerr error
)
/* 判断合约类型
---接下来就是调用虚拟机的操作了
*/
if contractCreation {
/* 进行创建合约操作 */
/* st.data = message.data() = tx.txdata.payload */
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
// Increment the nonce for the next transaction
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
if vmerr != nil {
log.Debug("VM returned with error", "err", vmerr)
if vmerr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr
}
}
/* 退回多余的gas
在EIP3529之前,最多退还一半的gas
EIP3529之后最多退还1/5的gas
*/
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))

return ret, st.gasUsed(), vmerr != nil, err
}
  1. preCheck 函数主要进行执行交易前的检查,目前包含下面两个步骤

    1.1 检查msg 里面的nonce值与db里面存储的账户的nonce值是否一致。

    1.2 buyGas方法主要是判断交易账户是否可以支付足够的gas执行交易,如果可以支付,则设置stateTransaction 的gas值 和 initialGas 值。并且从交易执行账户扣除相应的gas值。

  2. 先获取固定交易的基础费用,根据当前分叉版本和交易类型来决定基础费用,如果是创建合约则是至少是53000gas,如果是普通交易则至少是21000gas ,如果data部分不为空,则具体来说是按字节收费:零字节收4gas,零字节0收68gas(在EIP2028之后是16gas),所以你会看到很多做合约优化的,目的就是减少数据中不为0的字节数量,从而降低油费消耗。具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//IntrinsicGas 计算给定数据的固定gas消耗
func IntrinsicGas(data []byte, contractCreation, homestead bool) (uint64, error) {
// Set the starting gas for the raw transaction
var gas uint64
if contractCreation && homestead {
gas = params.TxGasContractCreation
} else {
gas = params.TxGas
}
// 通过事务数据量增加所需的气体
if len(data) > 0 {
// 零字节和非零字节的定价不同
var nz uint64
//获取非零字节的个数
for _, byt := range data {
if byt != 0 {
nz++
}
}
// 防止所需gas超过最大限制
if (math.MaxUint64-gas)/params.TxDataNonZeroGas < nz {
return 0, vm.ErrOutOfGas
}
//计算非0字节的gas消耗
gas += nz * params.TxDataNonZeroGas

z := uint64(len(data)) - nz
if (math.MaxUint64-gas)/params.TxDataZeroGas < z {
return 0, vm.ErrOutOfGas
}
gas += z * params.TxDataZeroGas
}
return gas, nil
}

notice

true

This is copyright.

...

...

00:00
00:00