0%

StudyRecord-以太坊源码分析-通过EVM创建智能合约-Create()

Pre:

20220720154820
如图所示,在TransitionDb()函数中,执行交易前,会对交易的目的进行判断。

core/state_transition.go
1
2
// 0地址为创建合约,否则是交易或合约调用
contractCreation = msg.To() == nil

如果交易的接收者为空,则代表此条交易的目的是要创建一条合约,随后调用 evm.Create() 执行相关的功能。
现在我们就来看看evm.Create()方法是如何实现合约的创建的。

创建合约 evm.Create:

首先看一看创建一个合约所需要的参数:

core/vm/evm.go
1
2
3
4
5
// Create creates a new contract using code as deployment code.
func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) {
contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address()))
return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE)
}
  • caller:转出方地址

  • code:代码(input)

  • gas:当前交易的剩余gas

  • value:转账额度

Create()方法主要操作:

  1. 首先对发送者地址(caller.Address)和账户的nonce进行keccak256计算得到合约地址

  2. 然后将合约地址传入create()方法

  3. 调用是evm.create() 合约创建的真正函数。

create()方法主要操作:

  1. 交易执行前的检查

  2. 确保当前要创建的地址在世界状态中没有合约存在,如果存在,直接返回;

  3. StateDB创建一个新的合约账户,设置新账户为nonce为1;

  4. 给新合约账户进行转账

  5. 创建一个待执行的合约对象,并在解释器执行合约初始化字节码;

  6. 处理返回值,在StateDB存储合约的运行时字节码:

分几个部分看看create()方法的具体实现:

检查1-调用栈深度、用户余额:

首先,在执行交易之前需要进行检查:

  1. 深度判断

  2. 余额是否足够;

1
2
3
4
5
6
7
8
9
10
   // Depth check execution. Fail if we're trying to execute above the
// limit.
// 执行深度检查,如果超出设定的深度限制 创建失败
if evm.depth > int(params.CallCreateDepth) {
return nil, common.Address{}, gas, ErrDepth
}
// 账户余额不足,创建失败
if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) {
return nil, common.Address{}, gas, ErrInsufficientBalance
}

第一个 if 判断中的 evm.depth 记录者合约的递归调用次数。
在 solidity 语言中,允许在合约中通过 new 关键字创建新的合约对象,但这种「在合约中创建合约」的递归调用是有限制的,这也是这个 if 判断的意义。

检查2-是否有相同地址的合约

然后,给交易发送者的账户nonce加1(普通转账时,是在外面加1的,即在TransitionDb中),
接着判断当前要创建的地址在是世界状态中没有合约存在,如果存在直接返回。

需要注意的点:
由于用到了账户的 Nonce 值,所以同一份合约代码,每次创建合约时得到的合约地址都是不一样的(因为合约是通过发送交易创建,而每发送一次交易 Nonce 值都会改变)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nonce := evm.StateDB.GetNonce(caller.Address())

if nonce+1 < nonce {
return nil, common.Address{}, gas, ErrNonceUintOverflow
}
evm.StateDB.SetNonce(caller.Address(), nonce+1)
// We add this to the access list _before_ taking a snapshot. Even if the creation fails,
// the access-list change should not be rolled back
if evm.chainRules.IsBerlin {
evm.StateDB.AddAddressToAccessList(address)
}
// Ensure there's no existing contract already at the designated address
// 确保指定地址没有已存在的相同合约
contractHash := evm.StateDB.GetCodeHash(address)
if evm.StateDB.GetNonce(address) != 0 || (contractHash != (common.Hash{}) && contractHash != emptyCodeHash) {
return nil, common.Address{}, 0, ErrContractAddressCollision
}

创建新的合约账户:

第三步,如果上面两个检查都没有问题,那么我们就可以创建新的合约账户了。
先用合约地址在状态数据库中创建一个合约账户,然后给合约账户设置nonce为1。

1
2
3
4
5
6
7
// 先对当前StateDB进行快照
snapshot := evm.StateDB.Snapshot()
// 创建新合约并将合约的nonce设置为1
evm.StateDB.CreateAccount(address)
if evm.ChainConfig().IsEIP158(evm.BlockNumber) {
evm.StateDB.SetNonce(address, 1)
}

给新的合约账户转账:

第四步是进行转账,将我们创建合约交易时的的以太币数值value转入智能合约账户。
转账的过程很简单,就是sender的账户减减(- -),合约账户加加(++)。

1
2
// 转账操作
evm.Context.Transfer(evm.StateDB, caller.Address(), address, value)

创建合约对象:

第五步是创建合约Contract对象,并执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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, AccountRef(address), value, gas)
// 设置合约字节码、包含构造函数部分
contract.SetCodeOptionalHash(&address, codeAndHash)

if evm.Config.Debug {
if evm.depth == 0 {
evm.Config.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value)
} else {
evm.Config.Tracer.CaptureEnter(typ, caller.Address(), address, codeAndHash.code, gas, value)
}
}

start := time.Now()

使用caller地址 、合约地址、转账额和交易余额传入NewContract()方法。
然后执行contract.SetCodeOptionalHash(),将合约代码code(包含构造函数部分)设置到合约中:

core/vm/contract.go
1
2
3
4
5
6
7
// SetCodeOptionalHash can be used to provide code, but it's optional to provide hash.
// In case hash is not provided, the jumpdest analysis will not be saved to the parent context
func (c *Contract) SetCodeOptionalHash(addr *common.Address, codeAndHash *codeAndHash) {
c.Code = codeAndHash.code
c.CodeHash = codeAndHash.hash
c.CodeAddr = addr
}

Contract对象:

看看Contract对象的数据结构:

一个 Contract 对象包含和维护了合约在执行过程中的必要信息,比如

  • 合约创建者

  • 合约自身地址

  • 合约剩余 gas

  • 合约代码

  • 代码的 jumpdests 记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Contract represents an ethereum contract in the state database. It contains
// the contract code, calling arguments. Contract implements 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是初始化这个合约的人。 如果是delegate,这个值被设置为调用者的调用者。
CallerAddress common.Address
caller ContractRef
self ContractRef

// JUMPDEST指令分析的结果
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
}

解释器执行合约初始化代码:

1
2
// 执行合约的初始化
ret, err := evm.interpreter.Run(contract, nil, false)

run()函数将contract交给了evm解释器,返回interpreter.Run(contract, input, readOnly)的执行结果。
此时合约对象中存储的字节码是Deployment Bytecode(部署字节码),包含了:

  • 用户实际交易调用这个新合约时需要执行的字节码(即运行时字节码)

  • 合约的构造函数中进行初始化处理的代码

至于interpreter如何执行合约,另post文分析。

core/vm/interpreter.go
1
2
3
4
// 执行合约代码
func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {
...
}

StateDB存储合约的运行时字节码:

第六步,处理interpreter.Run返回值
interpreter.Run函数的两个返回值分别是ret(运行时字节码)err

  • 约定合约代码最大长度为24576,检查代码长度不超过24576

  • 如果执行没有报错:

    • 计算本次合约创建消耗的gas,每字节200gas, 扣除gas
    • 在StateDB存储合约的Runtime bytecode运行时字节码,
  • 如果报错:

    • 恢复之前的快照
    • 如果不是revert指令导致的错误,要扣除所有的gas

evm.Create()最后返回 合约代码、合约地址、gas余额和错误

core/vm/evm.go
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
// Check whether the max code size has been exceeded, assign err if the case.
// 检查初始化生成的代码长度是否超过限制,约定合约代码最大长度为24576
// 如果执行没有错且代码长度不超过24576
if err == nil && evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize {
err = ErrMaxCodeSizeExceeded
}

// Reject code starting with 0xEF if EIP-3541 is enabled.
if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon {
err = ErrInvalidCode
}

// if the contract creation ran successfully and no errors were returned
// calculate the gas required to store the code. If the code could not
// be stored due to not enough gas set an error and let it be handled
// by the error checking condition below.
// 合约创建成功
if err == nil {
// 计算本次合约创建消耗的gas,每字节200gas
createDataGas := uint64(len(ret)) * params.CreateDataGas
// 如果交易gas余额足够,则成功部署合约,将合约代码设置到账户储存中
if contract.UseGas(createDataGas) {
evm.StateDB.SetCode(address, ret) // !!!!!!! 注意点1
} else { // 否则返回余额不足
// 当前拥有的Gas不足以存储代码
err = ErrCodeStoreOutOfGas
}
}

// When an error was returned by the EVM or when setting the creation code
// above we revert to the snapshot and consume any gas remaining. Additionally
// when we're in homestead this also counts for code storage gas errors.
// 如果代码长度受限或执行错误,合约创建失败,借助上面创建的快照快速回滚
if err != nil && (evm.chainRules.IsHomestead || err != ErrCodeStoreOutOfGas) {
// 恢复之前的快照
evm.StateDB.RevertToSnapshot(snapshot)
// 如果不是revert指令导致的错误,要扣除所有的gas
if err != ErrExecutionReverted {
contract.UseGas(contract.Gas)
}
}

if evm.Config.Debug {
if evm.depth == 0 {
evm.Config.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
} else {
evm.Config.Tracer.CaptureExit(ret, gas-contract.Gas, err)
}
}
// 最后返回合约代码、合约地址、gas余额和错误
return ret, address, contract.Gas, err

注意点1:

为什么存储的合约代码是合约运行后的返回码,而不是原来交易中的数据(即 Transaction.data.Payload,或者说 EVM.Create 方法的 code 参数)?

这是因为合约源代码在编译成二进制数据时,除了合约原有的代码外,编译器还另外插入了一些代码,以便执行相关的功能。
对于创建来说,编译器插入了执行合约「构造函数」(即合约对象的 constructor 方法)的代码。
因此在将编译器编译后的二进制提交以太坊节点创建合约时,EVM 执行这段二进制代码,实际上主要执行了合约的 constructor 方法,然后将合约的其它字节码返回,所以才会有这里的 ret 变量作为合约的真正代码存储到状态数据库中

也就是说,创建合约交易的初始字节码可分成3个部分:

  1. Deployment Bytecode(部署字节码) :

    • 执行初始化新合约账户的所有操作
    • 包含 Runtime bytecode
    • 包含 构造函数的字节码
    • 并不存储在StateDB
  2. Runtime bytecode(运行时字节码):

    • 合约本身的代码
    • 当新合约被调用时所执行的所有字节码,不包含需要在部署中用来初始化合约的字节码。
    • 存储在StateDB
  3. Auxdata

注意点2:

值得注意的是,如果代码执行错误是revert错误,则不会收取gas,否则gas会被扣除。

那么这个revert是什么?
revert是evm中的一条指令,在我们高级编程语言(solidity)中有requirerevert这两个判断。
如果requirerevert判断错误,那么就会返回一个revert指令错误,此时就不会收取gas
这也就是为什么solidity中requirerevert执行不会扣除gas的原因。
当然,这个方法是在拜占庭分叉后出现的。

Summary:

最后,一张图看看智能合约创建的过程:

20220722155925

interpreter.Run()返回的是Runtime bytecode(运行时字节码):当新合约被调用时所执行的所有字节码,不包含需要在部署中用来初始化合约的字节码。

Refs: