存储中状态变量的布局

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

  • 存储槽中的第一个项目按较低的顺序排列存储。

  • 值类型只使用存储它们所需的字节数。

  • 如果值类型不适合存储插槽的剩余部分,则会将其存储在下一个存储插槽中。

  • 结构和数组数据总是开始一个新的槽,它们的项按照这些规则被紧密打包。

  • 结构或数组数据后面的项总是开始一个新的存储槽。

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

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

警告

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

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

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

最后,为了让EVM对此进行优化,请确保尝试对存储变量进行排序,并 struct 使其能被紧紧地包装。例如,按以下顺序声明存储变量: uint128, uint128, uint256 而不是 uint128, uint256, uint128 因为前者只占用两个存储槽,而后者占用三个存储槽。

注解

存储中状态变量的布局被认为是solidity外部接口的一部分,因为存储指针可以传递给库。这意味着,本节所述规则的任何变更均被视为语言的重大变更,由于其关键性,在执行前应仔细考虑。

映射和动态数组

由于其不可预测的大小,映射和动态调整大小的数组类型不能存储在它们之前和之后的状态变量之间。相反,它们被认为只占用32个字节。 rules above 并且它们包含的元素从使用Keccak-256散列计算的不同存储槽开始存储。

假设映射或阵列的存储位置最终是一个插槽 p 申请后 the storage layout rules 。对于动态数组,此槽存储数组中的元素数(字节数组和字符串除外,请参见 below )。对于映射,槽保持为空,但仍然需要确保即使有两个相邻的映射,它们的内容最终也会出现在不同的存储位置。

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

对应于映射键的值 k is located at keccak256(h(k) . p) where . 是串联的,并且 h 是根据键的类型应用于键的函数:

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

  • 对于字符串和字节数组, h 计算 keccak256 未填充数据的哈希。

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

例如,请考虑以下合同:

// 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;
}

让我们计算一下的存储位置 data[4][9].c 。映射本身的位置为 1 (变量 x 其前面有32个字节)。这意味着 data[4] 存储在 keccak256(uint256(4) . uint256(1)) 。的类型 data[4] 也是一个映射,并且 data[4][9] 从插槽开始 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) 。成员的插槽偏移量 c 在结构内部 S1 因为 ab 都装在一个单独的槽里。这意味着 data[4][9].ckeccak256(uint256(9) . keccak256(uint256(4) . uint256(1))) + 1 。该值的类型为 uint256 ,因此它使用单个插槽。

bytes and string

bytesstring 编码相同。通常,编码类似于 bytes1[] 从这个意义上说,有一个用于数组本身的槽和一个使用 keccak256 该槽位置的散列。但是,对于短值(小于32字节),数组元素与长度一起存储在同一个槽中。

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

注解

当前不支持处理无效编码的时隙,但将来可能会添加。如果通过实验性的基于IR的编译器管道进行编译,则读取无效编码的槽会导致 Panic(0x22) 错误。

JSON输出

合同的存储布局可以通过 standard JSON interface . 输出是一个包含两个键的JSON对象, storagetypes . 这个 storage 对象是一个数组,其中每个元素具有以下形式:

{
    "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 用作变量类型信息键的标识符(如下所述)

给定的 type 在这种情况下 t_uint256 表示中的元素 types ,其形式为:

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

在哪里?

  • encoding 数据在存储器中是如何编码的,其中可能的值有:

    • inplace (请参阅存储中的数据) above

    • mapping :Keccak-256基于哈希的方法(请参见 above

    • dynamic_array :Keccak-256基于哈希的方法(请参见 above

    • bytes :单槽或Keccak-256散列,取决于数据大小(请参见 above

  • label 是规范类型名。

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

除上述四种类型外,有些类型还有其他信息。映射包含 keyvalue 类型(同样引用此类型映射中的条目),数组具有 base 类型,结构列出它们的 members 以与顶层相同的格式 storage (见 above

注解

JSON的输出格式仍被视为违反协议的实验性存储格式。

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

// 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"
    }
  }
}