从EVM 角度探究合约创建与部署
- 原文链接: https://blog.smlxl.io/evm-contract-construction-93c98cc4ca96
- 译文出自:登链翻译计划
- 译者:翻译小组 校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
本文我们探讨智能合约是如何创建和部署的。
当谈论以太坊上的合约构建时,我们必须区分在EVM层面上发生的事情和作为Solidity开发者,在编写我们想要部署的合约时看到的事情之间的区别。
在这份文件中,我们将探讨智能合约是如何在链上部署的,以及与EVM执行合约创建代码有关的微妙之处。
constants
)的使用成本较低,但不可变的变量(immutable
)有更大的灵活性,因为它们可以在构造函数中初始化,也可以运行时复制值。当Solidity开发者编写合约时,可以定义一个特殊的函数,称为constructor()
,其作用很像其他面向对象编程中的构造函数。就好像Solidity团队想让开发者有宾至如归的感觉,让他们觉得他们只是在定义一个类(合约)和它的构造函数。
constructor()
函数通常用于初始化状态变量:
pragma solidity ^0.8.13;
contract MyCoin {
uint public constant totalSupply = 1000000000000000000000000000;
mapping(address => uint256) balances;
constructor() {
balances[msg.sender] = totalSupply;
}
}
上面的例子是一个简单的构造函数,它将部署器的余额设置为totalSupply
值,在我们的例子中这是一个常数。
尽管构造函数在Solidity中是明确定义的,但它与普通函数的不同之处在于,它在合约部署后是不可访问的。我们将使用solc
的- abi
输出标志来观察合约的可访问函数列表:
[{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
}, {
"inputs": [],
"name": "totalSupply",
"outputs": [{
"internalType": "uint256",
"name": "",
"type": "uint256"
}],
"stateMutability": "view",
"type": "function"
}]
编译器为totalSupply
状态变量创建了一个公共函数,因为我们把这个变量声明为public
。
我们的 构造函数
在合约的ABI中具有一个特殊的 类型
,以表明它不是一个普通的函数。它没有name
字段,与 真正的
函数不同,暗示它是不可访问的。
让我们看看当我们部署合约时将会运行的实际代码:
solc --bin ~/Desktop/MyCoin.sol
======= Desktop/MyCoin.sol:MyCoin =======
Binary:
608060405234801561001057600080fd5b506b033b2e3c9fd0803ce80000006000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555060bd8061006e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806318160ddd14602d575b600080fd5b60336047565b604051603e9190606e565b60405180910390f35b6b033b2e3c9fd0803ce800000081565b6000819050919050565b6068816057565b82525050565b6000602082019050608160008301846061565b9291505056fea2646970667358221220edc6183cb296d3c2809859d4531deef0fb83c0ad90b772697ffaa375befe9c7664736f6c634300080d0033
这是 init code
,而不仅仅是将被部署到链上的代码。对于后者,我们使用solc
的-- bin-runtime
选项获得:
solc --bin-runtime ~/Desktop/MyCoin.sol
======= Desktop/MyCoin.sol:MyCoin =======
Binary of the runtime part:
6080604052348015600f57600080fd5b506004361060285760003560e01c806318160ddd14602d575b600080fd5b60336047565b604051603e9190606e565b60405180910390f35b6b033b2e3c9fd0803ce800000081565b6000819050919050565b6068816057565b82525050565b6000602082019050608160008301846061565b9291505056fea2646970667358221220edc6183cb296d3c2809859d4531deef0fb83c0ad90b772697ffaa375befe9c7664736f6c634300080d0033
-- bin
输出包含-- bin-runtime
输出,这不是巧合。-- bin
输出包含部署动作和要部署的代码,而-- bin-runtime
输出只显示要部署的代码:
让我们仔细看看init code
是做什么的,看一下我们例子合约的反汇编:
label_0000:
0000 60 PUSH1 0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 34 CALLVALUE
0006 80 DUP1
0007 15 ISZERO
0008 61 PUSH2 0x0010
000B 57 JUMPI
label_000C:
000C 60 PUSH1 0x00
000E 80 DUP1
000F FD REVERT
label_0010:
0010 5B JUMPDEST
0011 50 POP
0012 6B PUSH12 0x033b2e3c9fd0803ce8000000
001F 60 PUSH1 0x00
0021 80 DUP1
0022 33 CALLER
0023 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0038 16 AND
0039 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
004E 16 AND
004F 81 DUP2
0050 52 MSTORE
0051 60 PUSH1 0x20
0053 01 ADD
0054 90 SWAP1
0055 81 DUP2
0056 52 MSTORE
0057 60 PUSH1 0x20
0059 01 ADD
005A 60 PUSH1 0x00
005C 20 SHA3
005D 81 DUP2
005E 90 SWAP1
005F 55 SSTORE
0060 50 POP
0061 60 PUSH1 0xbd
0063 80 DUP1
0064 61 PUSH2 0x006e
0067 60 PUSH1 0x00
0069 39 CODECOPY
006A 60 PUSH1 0x00
006C F3 RETURN
006D FE ASSERT
为了完全理解这个例子的要点,建议读者尝试用调试器自己执行这个例子,比如使用evm.codes 。
这里发生了很多事情,但我们可以把它分解成2个基本部分:
运行构造函数(设置代码)
0000-005F
返回将被部署在链上的运行时字节码
0060-006C
对于构造函数部分,事情是非常简单的。请记住,我们将Solidity构造函数设置为 nonpayable
. 事实上,我们不需要做任何事情就可以使构造函数成为 nonpayable
,这是一个 "solc "默认值,除非指定 payable
。
因此,如果任何 value
与运行我们代码的交易一起被发送,0005-000F
行将简单地回退。
在第0012-005F
行,可以看到Solidity构造函数的反汇编,它包含了一行代码:
pragma solidity ^0.8.13;
...
balances[msg.sender] = totalSupply;
由于我们用一个常数来表示 totalSupply
值,编译器将该值植入EVM字节码。这可以在第0012
行看到,十六进制值0x033b2e3c9fd0803ce8000000
被推送到堆栈。这是1000000000000000000000
的十进制,也就是我们给totalSupply
赋值。
CALLER
操作码,在第0022行
检索Solidity的msg.sender
。
随后,在005F
行,我们可以看到SSTORE
,它实际上是在映射中存储常量。
在第0060-006C
行,CODECOPY
将当前代码从偏移量0x006E
复制到偏移量0x00
的内存中,该偏移量刚好在init code
的最后一行。RETURN
被执行。
这实际上是将 runtime code
完整地返回给EVM,以便它将其作为代码存储在账户的状态中。这种行为,类似于一个安装者(init code
),想要部署它所持有的一段代码(runtime code
)。
由于在以太坊中存储数据的成本很高,最好只存储我们真正打算多次使用的代码,即运行时代码(runtime code)
。这就是为什么初始化合约状态的一次性设置代码,即 constructor
,不是部署代码的一部分。进行一个疯狂类比,设置代码可以被看作是火箭飞船的助推器,在使用一次后就被处理掉。然而,当一次性助推器永远消失在海洋中时,设置代码作为合约部署交易的一部分永远记录在链上。保留部分对于分析新部署的合约往往是有用的。
我们可以用一幅漂亮的ASCII图来总结本节,它显示了 "初始代码(init code)" 是如何组成的。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ +
| setup_code |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +
| |
+ +
| |
+ +
| |
+ +
| |
+ +
| runtime_code |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+-+-+-+-+-+-+-+-+-+
我们有110字节的 "设置代码(setup code)",用于初始化合约的状态,附加189字节的 "运行时代码(runtime code)",将被部署到链上。
如果我们想在构造函数的参数中指定总的发行量呢?这在同一合约将以不同的初始化值被多次部署的情况下最为有用:
pragma solidity ^0.8.13;
contract MyCoin2 {
mapping(address => uint256) balances;
constructor(uint256 _totalSupply) {
balances[msg.sender] = _totalSupply;
}
}
为构造函数提供参数的能力允许 Solidity 开发人员改变合约的初始化状态,而不必重新编译合约。这与我们观察到的在字节码中把初始化参数设置成常量的以往方法不同。
对源码模式的小改变对 init code
有实质性的影响:
solc --bin ~/Desktop/MyCoin2.sol
======= Desktop/MyCoin2.sol:MyCoin2 =======
Binary: 6080604052348015600f57600080fd5b506040516101223803806101228339818101604052810190602f919060ad565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505060d5565b600080fd5b6000819050919050565b608d81607c565b8114609757600080fd5b50565b60008151905060a7816086565b92915050565b60006020828403121560c05760bf6077565b5b600060cc84828501609a565b91505092915050565b603f806100e36000396000f3fe6080604052600080fdfea2646970667358221220c96818e63eea5c37b6a86bd71c0e718fc8f036db10e8d071ade062040534d7a564736f6c634300080d0033
让我们检查一下代码的大小:
In [3]: hex(len(binascii.unhexlify('6080604052348015600f57600080fd5b506040516101223803806101228339818101604052810190602f919060ad565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffff
ffffffffffffff168152602001908152602001600020819055505060d5565b600080fd5b6000819050919050565b608d81607c565b8114609757600080fd5b50565b60008151905060a7816086565b92915050565b60006020828403121560c05760bf6077565b
5b600060cc84828501609a565b91505092915050565b603f806100e36000396000f3fe6080604052600080fdfea2646970667358221220c96818e63eea5c37b6a86bd71c0e718fc8f036db10e8d071ade062040534d7a564736f6c634300080d0033')))
Out[3]: '0x122'
这个链接 是反汇编的init code
及 除去runtime
后的代码 。
Solidity代码中的一个小变化引起了EVM字节码的巨大变化。当我们处理EVM字节码时,提供构造函数参数是相当复杂的。
我们建议读者用evm.codes 来执行每一步代码。
虽然也可能通过反汇编整个代码来了解发生了什么,但让我们查看反编译版本中的main()
函数:
pragma solidity ^ 0.8 .13;
...
var temp0 = memory[0x40: 0x60];
var temp1 = code.length - 0x0122;
memory[temp0: temp0 + temp1] = code[0x0122: 0x0122 + temp1];
memory[0x40: 0x60] = temp1 + temp0;
var0 = 0x2f;
var var2 = temp0;
var var1 = var2 + temp1;
var0 = func_00AD(var1, var2);
main()
函数抓取CODESIZE
操作码的结果,用于计算当前运行代码的大小,并从中减去0x0122
。记住,init code
是0x0122
字节。init code
在意它的大小,并期望在执行前有东西附加到它上面!
反编译中的其他函数用来验证附加在init code
上的东西不大于0x20
(32)字节,并获取它。
然后我们看到这个值在同一设置代码中使用,因为它被SSTORE
存储在合约的状态中。
总之,当EVM执行init code
时,构造函数参数会被附加到init code
上。负责正确追加参数的是部署者,而不是编译器。再次使用一个图表示:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+ +
| |
+ +
| |
+ +
| |
+ +
| |
+ +
| |
+ +
| setup_code |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | |
+-+-+-+ +
| runtime_code |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | constructor_arguments |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
+-+-+
我们可以看到这次的setup_code
非常笨重,因为它包含了辅助函数来检索附加在runtime_code
末尾的constructor_arguments
,而这次的runtime_code
出乎意料的小!
为什么这次的 runtime code
只有63字节?(提示:我们是否在合约中省略了任何可公开访问的函数?🙂)
我们已经看 到Solidity编译器是如何将定义为常量的值植入EVM字节码的。这并不是在Solidity中可以声明不可修改的变量的唯一形式, 我们也可以将状态变量声明为 immutable
。
与 constant
变量不同,immutable
变量可以在构造函数中被初始化:
pragma solidity ^ 0.8 .13;
contract MyCoin {
uint256 public immutable totalSupply;
mapping(address => uint256) balances;
constructor(uint256 _totalSupply) {
totalSupply = _totalSupply;
balances[msg.sender] = _totalSupply;
}
}
通过这样的设置,会在运行时字节码中看到一大块零:
solc --bin ~/Desktop/MyCoin3.sol
======= Desktop/MyCoin3.sol:MyCoin =======
Binary: 60a060405234801561001057600080fd5b506040516101d43803806101d4833981810160405281019061003291906100be565b8060808181525050806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550506100eb565b600080fd5b6000819050919050565b...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!