Pre:
深入探索EVM : 编译和部署智能合约 学习记录+Demo测试
导读:
以太坊虚拟机(Ethereum Virtual Machine)是以太坊的基础,它负责执行所有的交易(Transaction),并且根据这些 Transaction 来维护整个以太坊的账户状态,或者更准确的称之为 World State。
Transaction 分很多种,有最简单的以太币(Ether)交易,有部署或者调用智能合约的交易。智能合约(Smart Contract)是由虚拟机执行的代码,用以完成复杂的业务逻辑。
Solidity 是目前最流行的编写智能合约的高级语言。由 Solidity 编写的智能合约会先被编译成可被虚拟机直接接受的字节码,然后会被用户以 Transaction 的方式发送给以太坊从而进行智能合约部署。在这之后,用户便可以调用智能合约的函数来完成业务逻辑。
那么在整个流程中,
Solidity 代码是如何被编译成字节码的?
字节码在虚拟机中又是如何运行的?
编译字节码的时候,虚拟机如何对其进行优化?
图来源
图来源
从一个例子开始:
1 2 3 4 5 6 7 8 pragma solidity ^0.4.11; contract C { uint256 a; function C() { // 旧的构造函数写法 a = 1; } }
编译:
进行solc编译:
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 $ solc --bin --asm file_name.sol ======= file_name.sol:C ======= EVM assembly: /* "file_name.sol":26:93 contract C {... */ mstore(0x40, 0x60) /* "file_name.sol":58:91 function C() {... */ jumpi(tag_1, iszero(callvalue)) 0x0 dup1 revert tag_1: tag_2: /* "file_name.sol":83:84 1 */ 0x1 /* "file_name.sol":79:80 a */ 0x0 /* "file_name.sol":79:84 a = 1 */ dup2 swap1 sstore pop /* "file_name.sol":58:91 function C() {... */ tag_3: /* "file_name.sol":26:93 contract C {... */ tag_4: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "file_name.sol":26:93 contract C {... */ mstore(0x40, 0x60) tag_1: 0x0 dup1 revert auxdata: 0xa165627a7a72305820142c04ffb9430b805f0c95b45fe0ced073cc017dffca3c288c5c45dd913025080029 } Binary: 60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820142c04ffb9430b805f0c95b45fe0ced073cc017dffca3c288c5c45dd913025080029
使用remix编译:
1 2 3 4 5 6 { "linkReferences": {}, "object": "60606040523415600b57fe5b5b60016000819055505b5b60338060236000396000f30060606040525bfe00a165627a7a72305820ae4e46818f9f69832c210ff67b80210d9bc5c5863f97d91286c7278d4c5240d30029", "opcodes": "PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xB JUMPI INVALID JUMPDEST JUMPDEST PUSH1 0x1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP JUMPDEST JUMPDEST PUSH1 0x33 DUP1 PUSH1 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE JUMPDEST INVALID STOP LOG1 PUSH6 0x627A7A723058 SHA3 0xae 0x4e 0x46 DUP2 DUP16 SWAP16 PUSH10 0x832C210FF67B80210D9B 0xc5 0xc5 DUP7 0x3f SWAP8 0xd9 SLT DUP7 0xc7 0x27 DUP14 0x4c MSTORE BLOCKHASH 0xd3 STOP 0x29 ", "sourceMap": "26:67:0:-;;;58:33;;;;;;;83:1;79;:5;;;;58:33;26:67;;;;;;;" }
使用remix编译的opcodes输出格式看起来还不错
bytecode && OpCodes:
编译后的代码我们称之为字节码(bytecode)
,如下所示:
1 2 60606040523415600e57600080fd5b600160008190555060358060236000396000f3006060604052600080fd00a165627a7a72305820d315875f56b532ab371cf9aa86a62850e13eb6ab194847011dcd641b9a9d2f8d0029
在这段字节码中,每个字符代表一个 16 进制数,每两个字符代表一个字节。
这段字节码就是直接运行在虚拟机上的代码,虚拟机只需要按照事先定义好的规则,解释并且执行每个字节即可。但是对人类来说,直接阅读这些字节码太过繁琐,所以我们可以将其转换成对人类更友好的形式,操作码(OpCodes),如下所示:
1 2 PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP PUSH1 0x35 DUP1 PUSH1 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0xd3 ISZERO DUP8 0x5f JUMP 0xb5 ORIGIN 0xab CALLDATACOPY SHR 0xf9 0xaa DUP7 0xa6 0x28 POP 0xe1 RETURNDATACOPY 0xb6 0xab NOT 0x48 0x47 ADD SAR 0xcd PUSH5 0x1B9A9D2F8D STOP 0x29
上面的字节码或者操作码是等价的 ,它们都可以被分为三个部分:
TODO:换个新合约的bytecode和opcode要学会自己分part
1 2 3 60606040523415600e57600080fd5b600160008190555060358060236000396000f300 PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST PUSH1 0x1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP PUSH1 0x35 DUP1 PUSH1 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP
1 2 3 6060604052600080fd00 PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT STOP
1 2 3 a165627a7a72305820d315875f56b532ab371cf9aa86a62850e13eb6ab194847011dcd641b9a9d2f8d0029 LOG1 PUSH6 0x627A7A723058 KECCAK256 0xd3 ISZERO DUP8 0x5f JUMP 0xb5 ORIGIN 0xab CALLDATACOPY SHR 0xf9 0xaa DUP7 0xa6 0x28 POP 0xe1 RETURNDATACOPY 0xb6 0xab NOT 0x48 0x47 ADD SAR 0xcd PUSH5 0x1B9A9D2F8D STOP 0x29
下面让我们来逐步讲解每个部分,看看它们都是怎么工作的。
部署合约的流程:
部署智能合约的代码:
第一部分代码是事实上把智能合约部署到以太坊上的代码,也是我们重点讨论的部分。这段代码又可以被划分为三个部分:
1 2 3 60606040523415600e57600080fd PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1 REVERT
1 2 3 5b6001600081905550 JUMPDEST PUSH1 0x1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP
1 2 3 60358060236000396000f300 PUSH1 0x35 DUP1 PUSH1 0x23 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP
Payable 检查:
payable
是 Solidity
的一个关键字,如果一个函数被其标记,那么用户在调用该函数的同时还可以发送以太币到该智能合约。而这部分字节码的意义就在于阻止用户在调用没有被 payable
标记的函数时,向该智能合约发送以太币。下面这张图是对这段代码进一步演算,左边两列分别是字节码和操作码,最右边一列是执行完该条语句之后栈的状态。
自己演算一遍:
1 2 3 4 60606040523415600e57600080fd PUSH1 0x60 PUSH1 0x40 MSTORE CALLVALUE ISZERO PUSH1 0xE JUMPI PUSH1 0x0 DUP1 REV
↓ 每两个字符代表一个字节,逐个字节拆分
1 2 3 4 5 6 7 8 9 10 60 60 PUSH1 0x60 60 40 PUSH1 0x40 52 MSTORE 34 CALLVALUE 15 ISZERO 60 0e PUSH1 0xE 57 JUMPI 60 00 PUSH1 0x0 80 DUP1 fd REVERT
参考:
演算stack里的变化
原文的演算图:
在上图中,前三句是将内存中从0x40
开始往后 32 个字节
的地址赋上0x60
这个值,这是虚拟机保留的内存地址。后面的几句就是在通过查看发送的以太币是否为 0 来做 payable 检查。如果是 0 的话,那么虚拟机程序计数器(PC
)跳转到0xe
的位置继续执行,如果不是的话,终止程序。
在这里需要说明一下,stack 里面的每一个元素都是 32 字节长度,在这里为了方便,省略了高位的 0。
执行构造函数:
智能合约部署代码的第二部分是用来执行合约的构造函数的。如下图所示,在执行完这段字节码之后,heap 里面0x0的地址就被赋上了值0x1。
在上图中,JUMPDEST
对应上面的0xe
,它代表了如果通过上面的 payable 检查,我们应该跳转到这里继续执行代码。SSTORE
命令是用来将栈上的值存储到 World State
上的。
在图中用了 heap
来代表 World State
是因为它们俩有很多相似之处。
我们知道在 Java 里面,栈是用来存储函数运行时的临时变量的,而堆是用来存储生命周期更长的变量,比如成员变量。
栈上的数据会随着方法的执行完毕而被实时清空,而堆上的数据会在整个类实例的生命周期里面始终有效。
Java 虚拟机不会将堆中的成员变量清空,除非该类的实例被回收。而一个部署到以太坊上的智能合约可以被认为是永远活着的合约实例(当然一个合约也可以被杀死)。
所以用来存放智能合约状态的 World State 就可以被看做是以太坊的 heap
。在这里我之所以用 heap
来代指 World State
,第一是希望跟 stack
做一个呼应,第二是希望从另一个方面描述以太坊的本质:以太坊是一个计算机网络,它将整个网络里面的所有计算机连接起来形成一个单一计算机。在这个计算机中,它使用数据结构来模拟内存的工作机制从而实现图灵完备的编程语言。
在以太坊中,World State
是一个 key-value pair
。每一个 key 对应一个 32 字节长的数据块。所以在上图所示的情况里面,0x0
这个 key 所对应的数据块里面存储了 0x1
这个数(32 字节,高位补 0)。
复制代码:
智能合约部署代码的第三部分是将剩余的代码,既智能合约本身的代码和 Auxdata
从 Transaction
中复制到内存里面并返回之。
从上图可知,我们将0x23到0x58的字节码(总共 0x35 个字节码)复制到了内存中0x0到0x35的地址上。
智能合约本身的代码
整个字节码的第二部分是智能合约本身的代码,它们会在智能合约的函数被调用的时候执行。因为在我们当前的例子中,智能合约只有一个构造函数,而没有其他方法,所以下图所示的代码并没有做什么有实际意义的操作。
Auxdata:
第三部分 Auxdata 是有一个固定模板的:
1 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
我们将上述的字节码
1 a165627a7a72305820d315875f56b532ab371cf9aa86a62850e13eb6ab194847011dcd641b9a9d2f8d0029
带入该模板中,可以得到 swarm hash
为
1 d315875f56b532ab371cf9aa86a62850e13eb6ab194847011dcd641b9a9d2f8d
这个 Swarm Hash
可以用来校验智能合约的代码,也可以用来获取智能合约的元数据。
创建合约的合约
我们已经通过上面的讲解,了解了部署智能合约的整个流程。在这个流程中,字节码以 Transaction
的方式发送给以太坊从而完成对其的部署,不过智能合约不仅能被手动创建,也可以被其他已有的智能合约创建。
1 2 3 4 5 6 7 8 9 10 11 pragma solidity ^0.4 .11 ; contract Foo { } contract FooFactory { address fooInstance; function makeNewFoo ( ) { fooInstance = new Foo (); } }
solc编译
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 $ solc --bin --asm deployContractbyContract.sol ======= deployContractbyContract.sol:Foo ======= EVM assembly: /* "deployContractbyContract.sol":26:42 contract Foo {... */ mstore(0x40, 0x60) jumpi(tag_1, iszero(callvalue)) invalid tag_1: tag_2: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "deployContractbyContract.sol":26:42 contract Foo {... */ mstore(0x40, 0x60) tag_1: invalid } Binary: 60606040523415600b57fe5b5b60338060196000396000f30060606040525bfe00a165627a7a723058200b92d5c31520b4d21e734dab0cb60055bb92299f7b249b877ba77ba2cebe22610029 ======= deployContractbyContract.sol:FooFactory ======= EVM assembly: /* "deployContractbyContract.sol":44:149 contract FooFactory {... */ mstore(0x40, 0x60) jumpi(tag_1, iszero(callvalue)) invalid tag_1: tag_2: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "deployContractbyContract.sol":44:149 contract FooFactory {... */ mstore(0x40, 0x60) calldataload(0x0) 0x100000000000000000000000000000000000000000000000000000000 swap1 div 0xffffffff and dup1 0x4e3991af eq tag_2 jumpi tag_1: invalid /* "deployContractbyContract.sol":91:147 function makeNewFoo() {... */ tag_2: jumpi(tag_3, iszero(callvalue)) invalid tag_3: tag_4 jump(tag_5) tag_4: stop tag_5: /* "deployContractbyContract.sol":133:142 new Foo() */ tag_7 jump // in(tag_8) tag_7: dup1 swap1 pop mload(0x40) dup1 swap2 sub swap1 0x0 create dup1 iszero iszero tag_9 jumpi invalid tag_9: /* "deployContractbyContract.sol":119:130 fooInstance */ 0x0 0x0 /* "deployContractbyContract.sol":119:142 fooInstance = new Foo() */ 0x100 exp dup2 sload dup2 0xffffffffffffffffffffffffffffffffffffffff mul not and swap1 dup4 0xffffffffffffffffffffffffffffffffffffffff and mul or swap1 sstore pop /* "deployContractbyContract.sol":91:147 function makeNewFoo() {... */ tag_6: jump // out /* "deployContractbyContract.sol":44:149 contract FooFactory {... */ tag_8: mload(0x40) dataSize(sub_0) dup1 dataOffset(sub_0) dup4 codecopy add swap1 jump // out stop sub_0: assembly { /* "deployContractbyContract.sol":26:42 contract Foo {... */ mstore(0x40, 0x60) jumpi(tag_1, iszero(callvalue)) invalid tag_1: tag_2: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "deployContractbyContract.sol":26:42 contract Foo {... */ mstore(0x40, 0x60) tag_1: invalid } } } Binary: 6060604052341561000c57fe5b5b6101358061001c6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634e3991af1461003b575bfe5b341561004357fe5b61004b61004d565b005b6100556100ae565b809050604051809103906000f080151561006b57fe5b600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505b565b604051604c806100be83390190560060606040523415600b57fe5b5b60338060196000396000f30060606040525bfe00a165627a7a723058200b92d5c31520b4d21e734dab0cb60055bb92299f7b249b877ba77ba2cebe22610029a165627a7a723058204b341e0e274980968e26e50d5f362e68f368b18aff1545db99672b0be89301670029
在上面的代码里面我们可以看到两个合约,一个是 Foo,一个是用来创建 Foo 的 FooFactory。如果我们把上面的代码编译之后会得到如下的字节码:
不知道怎么得出下面的…
1 2 3 4 5 6 FooFactoryDeployCode FooFactoryContractCode FooDeployCode FooContractCode FooAUXData FooFactoryAUXData
不难看出,整个字节码分两层,每一层又和之前描述的一样,分为三个部分。最外层的字节码用来部署 FooFactory
,它的 Contract Code
部分是用来创建合约 Foo
的,所以在这一部分里面又嵌套了一套完整的用来部署合约的代码
增加一个成员变量:
在第一个例子中,我们在整个合约里面只创建了一个成员变量。现在让我们来把合约变的复杂一点,再增加一个成员变量,看看相应的字节码有什么变化。
1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.4 .11 ; contract C { uint256 a; uint256 b; function C ( ) { a = 1 ; b = 2 ; } }
在省略掉其余部分之后,运行构造函数的部分如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 5b JUMPDEST 60 01 PUSH1 0x1 60 00 PUSH1 0x0 81 DUP2 90 SWAP1 55 SSTORE // heap {0x0 => 0x1} 50 POP 60 02 PUSH1 0x2 60 01 PUSH1 0x1 81 DUP2 90 SWAP1 55 SSTORE // heap {0x0 => 0x1} {0x1 => 0x2} 50 POP
很容易看出,虚拟机依次为变量a
和b
在 World State 中分配了两个地址0x0
和0x1
,并且赋上了相应的值 1 和 2。事实上如果有更多的成员变量,虚拟机会依次的为它们分配存储地址。在这里我们分配的存储地址对应于该RPC里面的第二个参数。
从256位到128位:
在上面的例子中我们声明了两个 256 位(32 字节)的无符号整型数。在实际运用中我们可能根本不需要那么多的空间,比如在其他语言中常用的整型数只有 4 个字节。所以现在让我们来做一点优化,把这两个 32 字节的数变成两个 16 字节的整型数,看看会发生什么变化。
1 2 3 4 5 6 7 8 9 10 pragma solidity ^0.4.11; contract C { uint128 a; uint128 b; function C() { a = 1; b = 2; } }
同样的,将其余部分省略,运行构造函数的部分如下所示:
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 /********************** a = 1 ********************************/ 60 01 PUSH1 0x1 stack: [0x1] 60 00 PUSH1 0x0 stack: [0x0 0x1] 80 DUP1 stack: [0x0 0x0 0x1] 61 0100 PUSH2 0x100 stack: [0x100 0x0 0x0 0x1] 0a EXP(base, exponent) stack: [0x01 0x0 0x1] 81 DUP2 stack: [0x0 0x1 0x0 0x1] 54 SLOAD(location) stack: [0x0 0x1 0x0 0x1] 81 DUP2 stack: [0x1 0x0 0x1 0x0 0x1] 6f ffffffffffffffffffffffffffffffff PUSH16 stack: [0xffffffffffffffffffffffffffffffff 0x1 0x0 0x1 0x0 0x1] 02 MUL(x, y) stack: [0xffffffffffffffffffffffffffffffff 0x0 0x1 0x0 0x1] 19 NOT stack: [0xffffffffffffffffffffffffffffffff00000000000000000000000000000000 0x0 0x1 0x0 0x1] 16 AND(x, y) stack: [0x0 0x1 0x0 0x1] 90 SWAP1 stack: [0x1 0x0 0x0 0x1] 83 DUP4 stack: [0x1 0x1 0x0 0x0 0x1] 6f ffffffffffffffffffffffffffffffff PUSH16 stack: [0xffffffffffffffffffffffffffffffff 0x1 0x1 0x0 0x0 0x1] 16 AND stack: [0x1 0x1 0x0 0x0 0x1] 02 MUL stack: [0x1 0x0 0x0 0x1] 17 OR stack: [0x1 0x0 0x1] 90 SWAP1 stack: [0x0 0x1 0x1] 55 SSTORE(pos, val) stack: [0x1] heap: {0x0 => 0x1} 50 POP stack: [] /********************** b = 2 ********************************/ 60 02 PUSH1 0x2 stack: [0x2] 60 00 PUSH1 0x0 stack: [0x0 0x2] 60 10 PUSH1 0x10 stack: [0x10 0x0 0x2] 61 0100 PUSH2 0x100 stack: [0x100 0x10 0x0 0x2] 0a EXP stack: [0x100000000000000000000000000000000 0x0 0x2] 81 DUP2 stack: [0x0 0x100000000000000000000000000000000 0x0 0x2] 54 SLOAD(location) stack: [0x1 0x100000000000000000000000000000000 0x0 0x2] 81 DUP2 stack: [0x100000000000000000000000000000000 0x1 0x100000000000000000000000000000000 0x0 0x2] 6f ffffffffffffffffffffffffffffffff PUSH16 stack: [0xffffffffffffffffffffffffffffffff 0x100000000000000000000000000000000 0x1 0x100000000000000000000000000000000 0x0 0x2] 02 MUL stack: [0xffffffffffffffffffffffffffffffff00000000000000000000000000000000 0x1 0x100000000000000000000000000000000 0x0 0x2] 19 NOT stack: [0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff 0x1 0x100000000000000000000000000000000 0x0 0x2] 16 AND stack: [0x1 0x100000000000000000000000000000000 0x0 0x2] 90 SWAP1 stack: [0x100000000000000000000000000000000 0x1 0x0 0x2] 83 DUP4 stack: [0x2 0x100000000000000000000000000000000 0x1 0x0 0x2] 6f ffffffffffffffffffffffffffffffff PUSH16 stack: [0xffffffffffffffffffffffffffffffff 0x2 0x100000000000000000000000000000000 0x1 0x0 0x2] 16 AND stack: [0x2 0x100000000000000000000000000000000 0x1 0x0 0x2] 02 MUL stack: [0x200000000000000000000000000000000 0x1 0x0 0x2] 17 OR stack: [0x200000000000000000000000000000001 0x0 0x2] 90 SWAP1 stack: [0x0 0x200000000000000000000000000000001 0x2] 55 SSTORE stack: [0x2] heap: {0x0 => 0x200000000000000000000000000000001} 50 POP stack: []
总得来讲上面的代码分为两个部分第一部分对应a = 1
,这部分代码在地址0x0
的低 16 字节里存入0x1;第二部分对应b = 2
,它表示在 0x0
的高 16 字节里面存入 0x2.
所以在运行完上面的代码之后,我们只使用了 World State 里面的一个 key,即0x0
,完成了对两个变量的保存。 用更形象的方式可以表示成:
1 2 [ b ][ a ] [16 bytes / 128 bits][16 bytes / 128 bits]
打包存储:
那么问题来了,为什么虚拟机要做这个变动?这两个例子的 Solidity 代码几乎一样,我们只是改变了变量的类型而已,然而虚拟机为第二个例子编译出的字节码比之前例子的字节码长了不止一倍。要知道,这些增加的字节码可是会直接影响 Transaction
的大小的。所以虚拟机到底是出于何种目的来产生了如此多的字节码的呢?
其实对于上面的问题有一个简单的答案,那就是 gas。
我们知道执行、部署合约是需要消耗 gas
的,而具体到 EVM 的层面,那就是每个操作码都有其对应的需要消耗的 gas
。
下面是对一些操作码消耗 gas 的说明:
sstore
当使用这个操作码往一个新的地址中存入数据时消耗 20000 gas
sstore
当使用这个操作码往一个已有的地址中存入数据时消耗 5000 gas
sload
当使用这个操作码从 World State 中读取数据,消耗 500 gas
其余的操作码消耗 3 到 10 gas
所以在两个例子中我们消耗的 gas 分别为:
在打包存储的情况下,因为我们第二次使用sstore
时,只是往已有的地址中再次写入数据,所以我们省掉了 15000 的 gas。正是由于这个原因,虚拟机才宁愿编译出如此复杂的字节码,也不愿意直接使用来个存储地址。
编译优化:
其实上述字节码还是略显冗长,因为很容易想到,我们其实可以在内存里面先准备好a
和b
对应的数据,然后在一次性的存到 World State
里面,这样一来我们还可以再节省掉第二个sstore
所消耗的 5000gas。我们可以通过指示编译器优化字节码的方式来达到这个目的。
1 solc --bin --asm --optimize file_name.sol
我们现在再进行一次编译,看看结果会如何
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 60 00 PUSH 0x0 80 DUP1 54 SLOAD 70 0200000000000000000000000000000000 PUSH17 /* not(sub(exp(0x2, 0x80), 0x1)) 高16字节bitmask */ 60 01 PUSH 0x1 60 80 PUSH 0x80 60 02 PUSH 0x2 0a EXP 03 SUB 19 NOT 90 SWAP1 91 SWAP2 16 AND 60 01 PUSH 0x1 17 OR /* sub(exp(0x2, 0x80), 0x1) 低16字节bitmask */ 60 01 PUSH 0x1 60 80 PUSH 0x80 60 02 PUSH 0x02 0a EXP 03 SUB 16 AND 17 OR 90 SWAP1 55 SSTORE
从上面我们可以看出,虚拟机通过使用 bitmask
分别将高 16 字节和低 16 字节赋值,而且只使用了一个sstore
指令就像数据存入了 World State
里面。优化目的达成!
但是,等等,为什么要在字节码中直接嵌入0200000000000000000000000000000000
这 17 个字节?
要知道我们只需要做一个简单运算便能获得这个值:exp(0x2, 0x81)
。 换句话说,我们其实只需要用 3 个字节就能代表这 17 个字节,但是虚拟机为什么没有这么做呢?答案很简单,仍然是 gas
。让我们来看看每个字节消耗 gas
的规则:
每一个 0 字节消耗 4 个 gas
每一个非 0 字节消耗 68gas
根据这个规则,我们很容易计算出两种情况下消耗的 gas 的值:
68 + 16 x 4 = 132
68 x 3 = 20
所以直接嵌入0200000000000000000000000000000000
虽然显得笨拙,但是贵在便宜。虚拟机宁愿增加字节码的大小也想为用户节约每一个 gas。
总结:
智能合约的生命周期被严格的划分为两个阶段:部署时和运行时。
智能合约的构造函数在且仅在部署时运行,一旦被部署就不可能再次运行构造函数了。
World State
是一个键值对,每一个键对应一个 32 字节长的数据块。
因为上面一点,以太坊虚拟机是一个 256 位机,其天生就是用来对 32 字节长的数据做运算的。
往 World State
里面存数据是非常昂贵的。
以太坊虚拟机一切向钱看,所有的优化都是围绕减少所需 gas
而进行的。
Refs: