Solidity Storage Layout
Table of Contents
1. Storage Layout
EVM 中的每个智能合约都有自己的 Storage,其中的数据在合约调用 SELFDESTRUCT 前是一直保存的。
我们可以把 Storage 理解为一个非常巨大的数组,每个元素位置可以保存 32 字节(256 比特位)的信息,这个数组共有 2 的 256 次方个元素,如下所示:
0 1 2 3 2^{256}-1 +---+---+---+---+ ---+---+ | | | | | ...... | | +---+---+---+---+ ---+---+
这是概念上的模型,我们不用真的分配这么大的连续存储空间。比如,我们可以使用 key/value(key 是数组下标,vaule 是对应值)的方式来实现这个模型。
2. 固定大小的状态变量
除变长数组和 mapping 外,状态变量是固定大小的。下面介绍固定大小的状态变量是如何保存在 Storage 中的。
考虑下面合约:
contract MyContract { struct Entry { uint256 id; uint256 value; } uint256 a; uint256 b; uint256[2] c; Entry d; }
它们在 Storage 中的布局如下所示:
0 1 2 3 4 5 +---+---+---+---+---+---+ | | | | | | | ...... +---+---+---+---+---+---+ a b \_____/ \_____/ c d
可见, 对于固定大小的状态变量:按照合约中状态变量的定义顺序,从 Slot 0 开始占据 Storage。
对于固定大小的状态变量(除了映射,变长数组以外的所有类型)在 Storage 中是依次连续从位置 0 的 Slot 开始排列的,一般来说每个 Slot 是 32 字节。
2.1. 打包多个状态变量到单个 Slot
如果有多个连续变量占用的大小少于 32 字节,会尽可能的打包到单个 Slot 中,具体规则如下:
- 在 Storage 槽中第一项是按低位对齐存储(lower-order aligned);
- 基本类型存储时仅占用其实际需要的字节;
- 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位;
- 结构体和数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)。
考虑下面合约:
contract MyContract { uint128 a; uint64 b; uint32 c; uint32 d; // a,b,c,d 可以打包到一个 Slot 中,加起来占 32字节(256 比特位) uint256 e; function func1() public { a = 1; b = 2; c = 0x12345678; d = 0xFFFFFFFF; e = 5; } }
在 Remix 中执行完 func1 后,我们可以在 Remix 中看到 Storage 的情况如图 1 所示。
Figure 1: 打包多个状态变量到单个 Slot 中
我们可以看到,由于 a,b,c,d 加起来不超过 32 字节,所以它们可以打包到同一个 Slot 中。
合约只使用了 Storage 中两个 Slot,其内容分别为:
0xffffffff12345678000000000000000200000000000000000000000000000001 # Slot 0 0x0000000000000000000000000000000000000000000000000000000000000005 # Slot 1
2.2. 继承时的布局
如果合约使用了继承,则基合约和继承合约中状态变量在 Storage 中的顺序由 C3 linearization 规则决定。
3. 非固定大小的状态变量
3.1. 变长数组
考虑下面合约:
contract MyContract { uint256 a; uint256 b; uint256[] c; // c 是变长数组,在 Storage 中如何存储? uint256 d; function func1() public { a = 1; b = 2; c.push(0xAABB); c.push(0xCCDD); c.push(0xEEFF); c.push(0x1122); d = 5; } }
合约中状态变量 c
是变长数组,它存储在 Storage 中的规则为:
- 对应 Slot 中保存数组的长度。如这个例子中,执行完 func1 后,slot 2 中保存 4(变长数组 c 在执行完 func1 后的长度是 4);
- 数组中元素本身保存在 keccak_256(slot) 开始的位置。
变长数组 c
在 Storage 中的存储的示意图如下所示:
0 1 2 3 4 5 hash(2) +---+---+---+---+---+---+ ---+---+---+-- | | | 4 | | | | ...... | | | ...... +---+---+---+---+---+---+ ---+---+---+-- a b ^ d ^ ^ | | | ...... c.length c[0] c[1]
我们在 Remix 中验证一下,执行完 func1 后,Storage 的布局如下所示:
0x0000000000000000000000000000000000000000000000000000000000000001 # slot 0 0x0000000000000000000000000000000000000000000000000000000000000002 # slot 1 0x0000000000000000000000000000000000000000000000000000000000000004 # slot 2 0x0000000000000000000000000000000000000000000000000000000000000005 # slot 3 0x000000000000000000000000000000000000000000000000000000000000aabb # slot 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace 0x000000000000000000000000000000000000000000000000000000000000ccdd # slot 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5acf 0x000000000000000000000000000000000000000000000000000000000000eeff # slot 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ad0 0x0000000000000000000000000000000000000000000000000000000000001122 # slot 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ad1
数组 c
首元素的 Slot 编号是 keccak_256(slot),其具体计算过程如下:
>>> import sha3 # pip3 install pysha3 >>> sha3.keccak_256(bytes.fromhex('0000000000000000000000000000000000000000000000000000000000000002')).hexdigest() '405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace'
3.2. mapping
考虑下面合约:
contract MyContract { uint256 a; uint256 b; mapping(uint256 => uint256) c; uint256 d; function func1() public { a = 1; b = 2; c[3] = 0xAABB; c[9] = 0xCCDD; d = 5; } }
合约中状态变量 c
是 mapping,它存储在 Storage 中的规则为:
- 对应 Slot 空出来,什么也不保存,下一个状态变量直接跳过它;
- mapping 中元素保存在 keccak_256(k . slot) 位置(点号是 concatenation 的含义)。
mapping c
在 Storage 中的存储的示意图如下所示:
0 1 2 3 4 5 hash(3 . 2) hash(9 . 2) +---+---+---+---+---+---+ ---+---+---+-- ---+---+---+-- | | | | | | | ...... | | | ...... | | | ... +---+---+---+---+---+---+ ---+---+---+-- ---+---+---+-- a b ^ d ^ ^ | | | empty c[3] c[9]
我们在 Remix 中验证一下,执行完 func1 后,Storage 的布局如下所示:
0x0000000000000000000000000000000000000000000000000000000000000001 # slot 0 0x0000000000000000000000000000000000000000000000000000000000000002 # slot 1 0x0000000000000000000000000000000000000000000000000000000000000000 # slot 2 0x0000000000000000000000000000000000000000000000000000000000000005 # slot 3 0x000000000000000000000000000000000000000000000000000000000000aabb # slot 0x88601476d11616a71c5be67555bd1dff4b1cbf21533d2669b768b61518cfe1c3 0x000000000000000000000000000000000000000000000000000000000000ccdd # slot 0xf85cc6ffc513dc6cf7d199ef87b7a63cf9defe62251c1c247cd12f1eec7bff29
mapping c
两个元素的 Slot 编号的具体计算过程如下:
>>> import sha3 # pip3 install pysha3 >>> sha3.keccak_256(bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000002')).hexdigest() '88601476d11616a71c5be67555bd1dff4b1cbf21533d2669b768b61518cfe1c3' >>> sha3.keccak_256(bytes.fromhex('00000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000002')).hexdigest() 'f85cc6ffc513dc6cf7d199ef87b7a63cf9defe62251c1c247cd12f1eec7bff29'
4. 参考
https://solidity.readthedocs.io/en/latest/internals/layout_in_storage.html
https://mixbytes.io/blog/collisions-solidity-storage-layouts
https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/
https://medium.com/@hayeah/diving-into-the-ethereum-vm-the-hidden-costs-of-arrays-28e119f04a9b