深入以太坊虚拟机 Part5 — 智能合约创建过程
原文:Diving Into The Ethereum VM Part 5 — The Smart Contract Creation Process | by Howard | Oct 24, 2017
在本系列的前几篇文章中,我们学习了 EVM 汇编的基础知识,以及 ABI 编码如何允许外部世界与合约进行通信。在本文中,我们将了解如何从无到有创建合约。
本系列的前几篇文章(按顺序)。
到目前为止,我们看到的 EVM 字节码很简单,只是 EVM 从上到下执行的指令,没有魔法。合约创建过程更有趣,因为它模糊了代码和数据之间的界限。
在学习如何创建合约时,我们会看到有时数据就是代码,有时代码就是数据。
戴上你最喜欢的巫师帽🎩
A Contract’s Birth Certificate
让我们创建一个简单(而且完全没用)的合约:
编译它:
字节码是:
要创建此合约,我们需要通过对以太坊节点进行 eth_sendtransaction RPC 调用来创建交易。您可以使用 Remix 或 Metamask 来执行此操作。
无论您使用什么部署工具,RPC 调用的参数都类似于:
没有特殊的 RPC 调用或交易类型来创建合约。相同的交易机制也用于其他目的:
- 将以太币转移到账户或合约。
- 使用参数调用合约的方法。
根据您指定的参数,以太坊对交易的解释不同。要创建合约,to
地址应为空(或省略)。
我用这个交易创建了示例合约:
https://rinkeby.etherscan.io/tx/0x58f36e779950a23591aaad9e4c3c3ac105547f942f221471bf6ffce1d40f8401
打开 Etherscan,您应该看到该交易的输入数据是 Solidity 编译器生成的字节码。
在处理此交易时,EVM 会将输入数据作为代码执行。Voila,合同诞生了。
What The Bytecode Is Doing
我们可以将上面的字节码分成三个单独的块:
- 部署代码在创建合约时运行。
- 合约代码在合约创建后其方法被调用时运行。
- (可选)辅助数据是源代码的加密指纹,用于验证。这只是数据,从未由 EVM 执行。
部署代码有两个主要目标:
- 运行构造函数,并设置初始存储变量(如合约所有者)。
- 计算合约代码,并将其返回给 EVM。
Solidity 编译器生成的部署代码将字节码 60606040525b600080fd00
加载到内存中,然后将其作为合约代码返回。在这个例子中,“计算”只是将一大块数据读入内存。原则上,我们可以通过编程方式生成合约代码。
构造函数的确切作用取决于语言,但任何 EVM 语言都必须在最后返回合约代码。
Contract Creation
那么在部署代码运行并返回合约代码之后会发生什么。以太坊如何根据返回的合约代码创建合约?
让我们一起深入研究源代码以了解详细信息。
我发现 Go-Ethereum 实现是查找所需信息的最简单参考。我们得到正确的变量名、静态类型信息和符号交叉引用。Try beating that, Yellow Paper!
相关的方法是 evm.Create,在 Sourcegraph 上阅读它(当您将鼠标悬停在变量上时会显示类型信息,非常棒)。让我们略读代码,省略一些错误检查和繁琐的细节。从上到下:
- 从调用者的地址生成(derive)新合约的地址(传入创建者账户的
nonce
):
- 使用生成的合约地址创建新的合约账户(更改“世界状态 (word state)”StateDB):
- 将初始 Ether 捐赠(endowment)从调用者转移到新合约:
- 将输入数据设置为合约的部署代码,然后使用 EVM 执行。
ret
变量是返回的合约代码:
- 检查错误。或者如果合约代码太大,则失败。收取用户 gas,然后设置合约代码:
Code That Deploys Code
现在让我们深入了解详细的汇编代码,看看在创建合约时“部署代码”如何返回“合约代码”。同样,我们将分析示例合约:
该合约的字节码分成不同的块:
部署代码的汇编是:
跟踪上述汇编以返回合约代码:
dataSize(sub_0)
和 dataOffset(sub_0)
不是真正的指令。它们实际上是将常量放入堆栈的 PUSH 指令。两个常量 0x1C
(28) 和 0x36
(54) 指定一个字节码子串作为合约代码返回。
部署代码汇编大致对应如下 Python3 代码:
结果内存内容是:
对应于汇编(加上 auxdata):
再次查看 Etherscan,这正是部署为合约代码的内容:Ethereum Account 0x2c7f561f1fc5c414c48d01e480fdaae2840b8aa2 Info
CODECOPY
部署代码使用 codecopy
从交易的输入数据复制到内存。
与其他更简单的指令相比,codecopy
指令的确切行为和参数不那么明显。如果我在黄皮书中查找它,我可能会更加困惑。相反,让我们参考 go-ethereum 源代码,看看它在做什么。
见 CODECOPY:
没有希腊字母!
evm.interpreter.intPool.put(memOffset, codeOffset, length)
行回收对象 (big integers) 以供后面使用。这只是一个效率优化。
Constructor Argument
除了返回合约代码外,部署代码的另一个目的是运行构造函数进行设置。如果有构造函数参数,部署代码需要以某种方式从某个地方加载参数数据。
传递构造函数参数的 Solidity 约定是在调用 eth_sendtransaction
时在字节码末尾附加 ABI 编码的参数值。 RPC 调用会将字节码和 ABI 编码参数一起作为输入数据传递,如下所示:
让我们看一个带有一个构造函数参数的示例合约:
我创建了这个合约,传入值 66
。 Etherscan 上的交易:https://rinkeby.etherscan.io/tx/0x2f409d2e186883bd3319a8291a345ddbc1c0090f0d2e182a32c9e54b5e3fdbd8
输入数据为:
我们可以在最后看到构造函数参数,即数字 66,但 ABI 编码为 32 字节数字:
为了处理构造函数中的参数,部署代码将 ABI 参数从 calldata
的末尾复制到内存中,然后从内存复制到堆栈中。
A Contract That Creats Contracts
FooFactory
合约可以通过调用 makeNewFoo
创建新的 Foo
实例:
该合约的完整汇编在 This Gist 中。编译器输出的结构比较复杂,因为有两组“install time”和“run time”字节码。它是这样组织的:
FooFactoryContractCode
基本上是复制 tag_8
中 Foo
的字节码,然后跳转回 tag_7
以执行 create
指令。
create
指令类似于 eth_sendtransaction
RPC 调用。它提供了一种在 EVM 内创建新合约的方法。
有关 go-ethereum 源代码,请参见 opCreate。该指令调用 evm.Create
来创建一个合约:
我们之前见过 evm.Create
,但这次调用者是智能合约,而不是人。
AUXDATA
如果您真的必须了解 auxdata 是什么,请阅读 Contract Metadata。它的要点是 auxdata
是一个哈希值,您可以使用它来获取有关已部署合约的元数据。
auxdata
的格式为:
解构我们之前看到的 auxdata 字节序列:
Conclusion
合约被创建的方式类似于自解压软件安装程序的工作方式。当安装程序运行时,它会配置系统环境,然后通过读取其程序包将目标程序提取到系统中。
- “install time”和“run time”之间存在强制分离。没有办法运行构造函数两次。
- 智能合约可以使用相同的过程来创建其他智能合约。
- 非 Solidity 语言很容易实现。
起初,我发现“智能合约安装程序”的不同部分在交易中作为 data
字节字符串打包在一起,这让我感到困惑:
从阅读 eth_sendtransaction
的文档来看,data
应该如何编码并不明显。我无法弄清楚构造函数参数是如何传递到交易中的,直到一个朋友告诉我它们是 ABI 编码然后附加到字节码的末尾。
另一种更清晰的设计可能是将这些部分作为交易中的单独属性发送:
不过,仔细想想,我认为 Transaction 对象如此简单实际上非常强大。对于交易来说,data
只是一个字节字符串,它并没有规定如何解释数据的语言模型。通过保持 Transaction 对象的简单性,语言实现者有一个用于设计和实验的空白画布(blank canvas)。
事实上,未来 data
甚至可以由不同的虚拟机解释。
Other Parts
在这一系列文章中,我翻译了 Howard 的 Diving Into The Ethereum VM 系列文章。译文链接如下: