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 中,具体规则如下:

  1. 在 Storage 槽中第一项是按低位对齐存储(lower-order aligned);
  2. 基本类型存储时仅占用其实际需要的字节;
  3. 如果基本类型不能放入某个槽位余下的空间,它将被放入下一个槽位;
  4. 结构体和数组总是使用一个全新的槽位,并占用整个槽(但在结构体内或数组内的每个项仍遵从上述规则)。

考虑下面合约:

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 所示。

solidity_storage_pack.gif

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 中的规则为:

  1. 对应 Slot 中保存数组的长度。如这个例子中,执行完 func1 后,slot 2 中保存 4(变长数组 c 在执行完 func1 后的长度是 4);
  2. 数组中元素本身保存在 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 中的规则为:

  1. 对应 Slot 空出来,什么也不保存,下一个状态变量直接跳过它;
  2. 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. 参考

Author: cig01

Created: <2020-08-01 Sat>

Last updated: <2020-09-26 Sat>

Creator: Emacs 27.1 (Org mode 9.4)