Pre:
如图所示,在TransitionDb()
函数中,执行交易前,会对交易的目的进行判断。
1 | // 0地址为创建合约,否则是交易或合约调用 |
如果交易的接收者为空,则代表此条交易的目的是要创建一条合约,随后调用 evm.Create()
执行相关的功能。
现在我们就来看看evm.Create()
方法是如何实现合约的创建的。
创建合约 evm.Create:
首先看一看创建一个合约所需要的参数:
1 | // Create creates a new contract using code as deployment code. |
-
caller
:转出方地址 -
code
:代码(input) -
gas
:当前交易的剩余gas -
value
:转账额度
Create()
方法主要操作:
-
首先对发送者地址(caller.Address)和账户的nonce进行
keccak256
计算得到合约地址 -
然后将合约地址传入
create()
方法 -
调用是
evm.create()
合约创建的真正函数。
create()
方法主要操作:
-
交易执行前的检查
-
确保当前要创建的地址在世界状态中没有合约存在,如果存在,直接返回;
-
在
StateDB
创建一个新的合约账户,设置新账户为nonce
为1; -
给新合约账户进行转账
-
创建一个待执行的合约对象,并在解释器执行合约初始化字节码;
-
处理返回值,在
StateDB
存储合约的运行时字节码:
分几个部分看看create()
方法的具体实现:
检查1-调用栈深度、用户余额:
首先,在执行交易之前需要进行检查:
-
深度判断
-
余额是否足够;
1 | // Depth check execution. Fail if we're trying to execute above the |
第一个 if 判断中的 evm.depth
记录者合约的递归调用次数。
在 solidity 语言中,允许在合约中通过 new
关键字创建新的合约对象,但这种「在合约中创建合约」的递归调用是有限制的,这也是这个 if 判断的意义。
检查2-是否有相同地址的合约
然后,给交易发送者的账户nonce
加1(普通转账时,是在外面加1的,即在TransitionDb
中),
接着判断当前要创建的地址在是世界状态中没有合约存在,如果存在直接返回。
需要注意的点:
由于用到了账户的 Nonce
值,所以同一份合约代码,每次创建合约时得到的合约地址都是不一样的(因为合约是通过发送交易创建,而每发送一次交易 Nonce
值都会改变)。
1 | nonce := evm.StateDB.GetNonce(caller.Address()) |
创建新的合约账户:
第三步,如果上面两个检查都没有问题,那么我们就可以创建新的合约账户了。
先用合约地址在状态数据库中创建一个合约账户,然后给合约账户设置nonce
为1。
1 | // 先对当前StateDB进行快照 |
给新的合约账户转账:
第四步是进行转账,将我们创建合约交易时的的以太币数值value
转入智能合约账户。
转账的过程很简单,就是sender的账户减减(- -),合约账户加加(++)。
1 | // 转账操作 |
创建合约对象:
第五步是创建合约Contract
对象,并执行。
1 | // Initialise a new contract and set the code that is to be used by the EVM. |
使用caller地址 、合约地址、转账额和交易余额传入NewContract()
方法。
然后执行contract.SetCodeOptionalHash()
,将合约代码code
(包含构造函数部分)设置到合约中:
1 | // SetCodeOptionalHash can be used to provide code, but it's optional to provide hash. |
Contract对象:
看看Contract
对象的数据结构:
一个 Contract 对象包含和维护了合约在执行过程中的必要信息,比如
-
合约创建者
-
合约自身地址
-
合约剩余 gas
-
合约代码
-
代码的 jumpdests 记录
1 | // Contract represents an ethereum contract in the state database. It contains |
解释器执行合约初始化代码:
1 | // 执行合约的初始化 |
run()
函数将contract
交给了evm解释器,返回interpreter.Run(contract, input, readOnly)
的执行结果。
此时合约对象中存储的字节码是Deployment Bytecode(部署字节码),包含了:
-
用户实际交易调用这个新合约时需要执行的字节码(即运行时字节码)
-
合约的构造函数中进行初始化处理的代码
至于interpreter
如何执行合约,另post文分析。
1 | // 执行合约代码 |
StateDB存储合约的运行时字节码:
第六步,处理interpreter.Run
返回值
interpreter.Run
函数的两个返回值分别是ret(运行时字节码)
和err
-
约定合约代码最大长度为24576,检查代码长度不超过24576
-
如果执行没有报错:
- 计算本次合约创建消耗的gas,每字节200gas, 扣除gas
- 在StateDB存储合约的
Runtime bytecode
运行时字节码,
-
如果报错:
- 恢复之前的快照
- 如果不是
revert
指令导致的错误,要扣除所有的gas
evm.Create()
最后返回 合约代码、合约地址、gas余额和错误
1 | // Check whether the max code size has been exceeded, assign err if the case. |
注意点1:
为什么存储的合约代码是合约运行后的返回码,而不是原来交易中的数据(即 Transaction.data.Payload
,或者说 EVM.Create
方法的 code
参数)?
这是因为合约源代码在编译成二进制数据时,除了合约原有的代码外,编译器还另外插入了一些代码,以便执行相关的功能。
对于创建来说,编译器插入了执行合约「构造函数」(即合约对象的 constructor
方法)的代码。
因此在将编译器编译后的二进制提交以太坊节点创建合约时,EVM 执行这段二进制代码,实际上主要执行了合约的 constructor
方法,然后将合约的其它字节码返回,所以才会有这里的 ret
变量作为合约的真正代码存储到状态数据库中。
也就是说,创建合约交易的初始字节码可分成3个部分:
-
Deployment Bytecode(部署字节码) :
- 执行初始化新合约账户的所有操作
- 包含
Runtime bytecode
- 包含 构造函数的字节码
- 并不存储在
StateDB
-
Runtime bytecode(运行时字节码):
- 合约本身的代码
- 当新合约被调用时所执行的所有字节码,不包含需要在部署中用来初始化合约的字节码。
- 存储在
StateDB
-
Auxdata
注意点2:
值得注意的是,如果代码执行错误是revert
错误,则不会收取gas
,否则gas
会被扣除。
那么这个revert
是什么?
revert是evm中的一条指令,在我们高级编程语言(solidity)中有require
和revert
这两个判断。
如果require
和revert
判断错误,那么就会返回一个revert
指令错误,此时就不会收取gas
。
这也就是为什么solidity中require
和revert
执行不会扣除gas的原因。
当然,这个方法是在拜占庭分叉后出现的。
Summary:
最后,一张图看看智能合约创建的过程:
interpreter.Run()
返回的是Runtime bytecode(运行时字节码)
:当新合约被调用时所执行的所有字节码,不包含需要在部署中用来初始化合约的字节码。