阅读(4696) (12)

存储中状态变量的布局

2022-05-24 11:31:44 更新

合约的状态变量以紧凑的方式存储在存储中,以便多个值有时使用相同的存储槽。除了动态大小的数组和映射(见下文)之外,数据以连续的方式存储在以第一个状态变量开头的项接一项地存储,该状态变量存储在插槽中。对于每个变量,大小(以字节为单位)根据其类型确定。如果可能,需要少于 32 个字节的多个连续项目将打包到单个存储槽中,具体取决于以下规则:0

  • 存储槽中的第一个项目以较低的顺序对齐存储。
  • 值类型仅使用存储它们所需的字节数。
  • 如果值类型不适合存储槽的剩余部分,则该值类型将存储在下一个存储槽中。
  • 结构和阵列数据总是启动一个新的槽,并且根据这些规则,它们的物品被紧密地包装起来。
  • 结构或数组数据后面的项始终启动新的存储槽。

对于使用继承的协定,状态变量的顺序由从最基本守合同开始的协定的 C3 线性化顺序确定。如果上述规则允许,来自不同合约的状态变量确实共享相同的存储槽。

结构和数组的元素彼此相邻存储,就像它们作为单个值给出一样。

警告

当使用小于 32 字节的元素时,合约的气体使用量可能会更高。这是因为 EVM 一次在 32 个字节上运行。因此,如果元素小于此值,则 EVM 必须使用更多操作,以便将元素的大小从 32 个字节减少到所需的大小。

如果要处理存储值,则使用减小大小的类型可能会有所帮助,因为编译器会将多个元素打包到一个存储槽中,从而将多个读取或写入合并到单个操作中。但是,如果不同时读取或写入槽中的所有值,则可能会产生相反的效果:当将一个值写入多值存储槽时,必须首先读取该存储槽,然后将其与新值组合,以便不会破坏同一槽中的其他数据。

在处理函数参数或内存值时,没有固有的好处,因为编译器不打包这些值。

最后,为了允许 EVM 对此进行优化,请确保尝试对存储变量和成员进行排序,以便它们可以紧密地打包。例如,按 的顺序声明存储变量,而不是 ,因为前者仅占用两个存储槽,而后者将占用三个。structuint128, uint128, uint256uint128, uint256, uint128

注意

存储中状态变量的布局被认为是 Solidity 外部接口的一部分,因为存储指针可以传递到库。这意味着对本节中概述的规则的任何更改都被视为语言的重大更改,并且由于其关键性,在执行之前应非常仔细地考虑。如果发生这样的重大更改,我们希望发布一种兼容模式,在该模式下,编译器将生成支持旧布局的字节码。

映射和动态数组

由于映射和动态大小的数组类型不可预测,因此不能存储在它们之前和之后的状态变量“之间”。相反,根据上述规则,它们被认为仅占用32个字节,并且它们包含的元素从使用Keccak-256哈希计算的不同存储槽开始存储。

假设映射或阵列的存储位置在应用存储布局规则后最终成为插槽。对于动态数组,此槽存储数组中的元素数(字节数组和字符串是例外,请参阅下文)。对于映射,该槽保持为空,但仍需要确保即使有两个映射彼此相邻,其内容最终也会位于不同的存储位置。p

数组数据从 开始,其布局方式与静态大小的数组数据相同:一个元素接一个,如果元素的长度不超过 16 个字节,则可能共享存储槽。动态数组的动态数组以递归方式应用此规则。元素的位置,其中类型为 ,计算如下(再次假设其本身存储在插槽中):插槽是,并且可以使用从插槽数据中获取元素。keccak256(p)x[i][j]xuint24[][]xpkeccak256(keccak256(p) + i) + floor(j / floor(256 / 24))v(v >> ((j % floor(256 / 24)) * 24)) & type(uint24).max

与映射键对应的值位于串联位置,并且是根据其类型应用于键的函数:kkeccak256(h(k) . p).h

  • 对于值类型,将值填充到 32 个字节,其方式与将值存储在内存中时相同。h

  • 对于字符串和字节数组,只是未填充的数据。h(k)

如果映射值是非值类型,则计算的槽将标记数据的开始。例如,如果值为结构类型,则必须添加与结构成员对应的偏移量才能到达该成员。

例如,请考虑以下合同:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;


contract C {
    struct S { uint16 a; uint16 b; uint256 c; }
    uint x;
    mapping(uint => mapping(uint => S)) data;
}

我们计算 的存储位置。映射本身的位置是(前面有 32 个字节的变量)。这意味着 存储在 。的类型再次是映射,并且 的数据从插槽 开始。结构内成员的槽偏移量是因为 并且被打包在单个槽中。这意味着 的插槽是 。该值的类型是 ,因此它使用单个插槽。data[4][9].c1xdata[4]keccak256(uint256(4) . uint256(1))data[4]data[4][9]keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)))cS1abdata[4][9].ckeccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1uint256

bytesstring

bytes并且编码相同。通常,编码类似于 ,因为数组本身有一个插槽,而数据区域是使用该插槽位置的哈希值计算的。但是,对于短值(短于 32 个字节),数组元素将与长度一起存储在同一个插槽中。stringbytes1[]keccak256

特别是:如果数据最多是字节长,则元素存储在高阶字节(左对齐)中,最低阶字节存储值。对于存储长度为或更多字节的数据的字节数组,主槽将存储,并且数据将照常存储在 中。这意味着您可以通过检查是否设置了最低位来区分短数组和长数组:短位(未设置)和长位(设置)。31length * 232plength * 2 + 1keccak256(p)

注意

目前不支持处理无效编码的插槽,但将来可能会添加。如果通过 IR 进行编译,则读取无效编码的插槽会导致错误。Panic(0x22)

JSON 输出

可以通过标准 JSON 接口请求合约的存储布局。输出是一个 JSON 对象,其中包含两个键和 。该对象是一个数组,其中每个元素具有以下形式:storagetypesstorage

{
    "astId": 2,
    "contract": "fileA:A",
    "label": "x",
    "offset": 0,
    "slot": "0",
    "type": "t_uint256"
}

上面的示例是源单元的存储布局,并且contract A { uint x; }fileA

  • astId是状态变量声明的 AST 节点的 ID

  • contract是合约的名称,包括其路径作为前缀

  • label是状态变量的名称

  • offset是存储槽内根据编码的偏移量(以字节为单位)

  • slot是状态变量驻留或启动的存储槽。此数字可能非常大,因此其 JSON 值表示为字符串。

  • type是用作变量类型信息键的标识符(如下所述)

给定的 ,在本例中表示 中的一个元素,其形式为:typet_uint256types

{
    "encoding": "inplace",
    "label": "uint256",
    "numberOfBytes": "32",
}

哪里

  • encoding数据在存储中的编码方式,其中可能的值为:

    • inplace:数据在存储中连续排列(见上文)。

    • mapping:基于Keccak-256哈希的方法(见上文)。

    • dynamic_array:基于Keccak-256哈希的方法(见上文)。

    • bytes:单插槽或基于Keccak-256哈希,具体取决于数据大小(见上文)。

  • label是规范类型名称。

  • numberOfBytes是已用字节数(作为十进制字符串)。请注意,如果这意味着使用了多个插槽。numberOfBytes > 32

除了上述四种类型之外,某些类型还有额外的信息。映射包含其和类型(再次引用此类型映射中的条目),数组具有其类型,结构以与顶级相同的格式列出它们(见上文)。keyvaluebasemembersstorage

注意

合约存储布局的 JSON 输出格式仍被视为实验性格式,并且可能会在 Solidity 的非中断版本中发生变化。

下面的示例演示协定及其存储布局,其中包含值和引用类型、编码打包的类型以及嵌套类型。

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract A {
    struct S {
        uint128 a;
        uint128 b;
        uint[2] staticArray;
        uint[] dynArray;
    }

    uint x;
    uint y;
    S s;
    address addr;
    mapping (uint => mapping (address => bool)) map;
    uint[] array;
    string s1;
    bytes b1;
}
{
  "storage": [
    {
      "astId": 15,
      "contract": "fileA:A",
      "label": "x",
      "offset": 0,
      "slot": "0",
      "type": "t_uint256"
    },
    {
      "astId": 17,
      "contract": "fileA:A",
      "label": "y",
      "offset": 0,
      "slot": "1",
      "type": "t_uint256"
    },
    {
      "astId": 20,
      "contract": "fileA:A",
      "label": "s",
      "offset": 0,
      "slot": "2",
      "type": "t_struct(S)13_storage"
    },
    {
      "astId": 22,
      "contract": "fileA:A",
      "label": "addr",
      "offset": 0,
      "slot": "6",
      "type": "t_address"
    },
    {
      "astId": 28,
      "contract": "fileA:A",
      "label": "map",
      "offset": 0,
      "slot": "7",
      "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))"
    },
    {
      "astId": 31,
      "contract": "fileA:A",
      "label": "array",
      "offset": 0,
      "slot": "8",
      "type": "t_array(t_uint256)dyn_storage"
    },
    {
      "astId": 33,
      "contract": "fileA:A",
      "label": "s1",
      "offset": 0,
      "slot": "9",
      "type": "t_string_storage"
    },
    {
      "astId": 35,
      "contract": "fileA:A",
      "label": "b1",
      "offset": 0,
      "slot": "10",
      "type": "t_bytes_storage"
    }
  ],
  "types": {
    "t_address": {
      "encoding": "inplace",
      "label": "address",
      "numberOfBytes": "20"
    },
    "t_array(t_uint256)2_storage": {
      "base": "t_uint256",
      "encoding": "inplace",
      "label": "uint256[2]",
      "numberOfBytes": "64"
    },
    "t_array(t_uint256)dyn_storage": {
      "base": "t_uint256",
      "encoding": "dynamic_array",
      "label": "uint256[]",
      "numberOfBytes": "32"
    },
    "t_bool": {
      "encoding": "inplace",
      "label": "bool",
      "numberOfBytes": "1"
    },
    "t_bytes_storage": {
      "encoding": "bytes",
      "label": "bytes",
      "numberOfBytes": "32"
    },
    "t_mapping(t_address,t_bool)": {
      "encoding": "mapping",
      "key": "t_address",
      "label": "mapping(address => bool)",
      "numberOfBytes": "32",
      "value": "t_bool"
    },
    "t_mapping(t_uint256,t_mapping(t_address,t_bool))": {
      "encoding": "mapping",
      "key": "t_uint256",
      "label": "mapping(uint256 => mapping(address => bool))",
      "numberOfBytes": "32",
      "value": "t_mapping(t_address,t_bool)"
    },
    "t_string_storage": {
      "encoding": "bytes",
      "label": "string",
      "numberOfBytes": "32"
    },
    "t_struct(S)13_storage": {
      "encoding": "inplace",
      "label": "struct A.S",
      "members": [
        {
          "astId": 3,
          "contract": "fileA:A",
          "label": "a",
          "offset": 0,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 5,
          "contract": "fileA:A",
          "label": "b",
          "offset": 16,
          "slot": "0",
          "type": "t_uint128"
        },
        {
          "astId": 9,
          "contract": "fileA:A",
          "label": "staticArray",
          "offset": 0,
          "slot": "1",
          "type": "t_array(t_uint256)2_storage"
        },
        {
          "astId": 12,
          "contract": "fileA:A",
          "label": "dynArray",
          "offset": 0,
          "slot": "3",
          "type": "t_array(t_uint256)dyn_storage"
        }
      ],
      "numberOfBytes": "128"
    },
    "t_uint128": {
      "encoding": "inplace",
      "label": "uint128",
      "numberOfBytes": "16"
    },
    "t_uint256": {
      "encoding": "inplace",
      "label": "uint256",
      "numberOfBytes": "32"
    }
  }
}