什么是智能合约存储布局?

本指南将解释智能合约中存储的数据。合约存储布局是指控制合约存储变量在长期内存中排布的规则。

读者先决条件知识

以下一般先决条件有助于理解本文:

什么是合约存储布局,为什么它很重要?

  • 合约存储布局是指规定合约存储变量在长期内存中的排列方式的规则。几乎所有的智能合约都有需要长期存储的状态变量。

理解合约存储布局对以下方面很重要:

  • 编写高效的燃气合约,因为在区块链上将数据存储在长期内存中是昂贵的。在本文后面,我们将详细介绍如何使用存储布局规则来最大化燃气节省。
  • 处理使用代理或钻石模式或其他各种模式的合约。
  • 审计合约的安全性。不理解合约存储布局规则可能会使我们的合约容易受到攻击。

除我们定义的公共函数和变量之外,状态变量的布局也被认为是外部接口的一部分。

智能合约开发人员无法直接控制合约外部接口的这个方面,它由编译器控制。但是,如果编译器版本更改并且合约存储布局的规则发生变化,开发人员需要了解这一点。

内存如何在EVM中使用?

智能合约是在区块链上运行的计算机程序。程序包括函数和数据(也称为变量或参数),这些函数操作数据。函数使用的数据需要存储在计算机的内存中。在这种情况下,计算机是EVM。

Solidity内存类型

在Solidity中,有3种不同的内存类型,开发人员可以使用它们来指示EVM存储其变量的位置:memory,calldata和storage。

还有关于变量存储位置的有效期限以及变量使用方式的规定。例如,变量是否可以被读取?变量是否可以被写入?

1. Memory

开发人员会在函数中使用“memory”关键字来定义变量和参数。这些类型的变量只存在于函数执行期间。当函数运行结束时,存储在内存区域中的变量和参数会消失。

对于有编程背景的人来说,“memory”是最为熟悉的内存类型。

2. Calldata

calldata memory类型与memory类型非常相似,并且在声明组成函数签名的动态大小参数的外部函数时必须使用它。

memory变量和calldata变量之间的区别在于,calldata变量引用的是只读内存区域。

3. Storage

Solidity 的最终类型是存储类型。 存储内存 是合约的长期存储区域,在函数或事务执行完成后存储变量。

本文的重点是关于存储变量如何布局的 EVM 规则。

长期存储内存的概念与其他两种内存类型形成鲜明对比。合约的状态变量(即在合约内声明但不在函数内声明的变量)存储在存储内存区域中。

存储内存类型 的概念是区块链所特有的,因为在智能合约中工作时,通过区块链的加密封存属性,存储的数据是无法篡改的。在其他编程环境中,如果我们想要长期存储变量,通常会将这项工作转移到文件系统或数据库中。但在区块链上,智能合约的代码和数据都长期保留在区块链上。

什么是存储器?

每个合约都有自己的存储区域,这是一个持久的、可读写的内存区域。合约只能从自己的存储区读取和写入。合约的存储被分成2²⁵⁶个32字节大小的槽位。槽位是连续的,由索引引用,从0开始,到2²⁵⁶结束。所有槽位都初始化为0。

EVM存储器只能直接访问这些32字节大小的槽位。

2²⁵⁶个槽位!

每个合约的存储区域具有比宇宙中所有星星都多的槽位。我们在这里处理的是真正的天文数字。

由于存储容量巨大,合约的存储可以被认为是虚拟的。这意味着,如果您读取一个随机槽位,它很可能为空/未初始化。读取这样的槽位将返回一个值为0。EVM实际上并没有存储所有这些0,但它会跟踪哪些槽位正在使用,哪些没有。当您访问一个未使用的槽位时,EVM知道并将返回0。

为什么EVM的设计者会给合约一个如此大的存储区域?

合约存储区域如此之大的原因与动态大小的状态变量以及哈希如何用于计算状态变量的存储槽有关。

状态变量如何存储在智能合约存储槽中?

Solidity将自动将您合约定义的每个状态变量映射到存储槽中,按照声明状态变量的顺序,从槽0开始。

这个想法的简单可视化如下所示:

状态变量映射到存储槽的图示。
状态变量映射到存储槽的图示。

状态变量映射到存储槽的图示。

这里我们可以看到变量 a、b 和 c 如何从声明顺序映射到它们的存储槽。要了解存储变量实际上如何被编码并存储在二进制级别的槽中,我们需要深入挖掘并理解字节序、字节打包和字节填充的概念。

什么是字节序?

字节序是指计算机在内存中存储多字节值(例如:uint256、bytes32、address)的方式,有两种字节序:大端序和小端序。

大端序 → 数据类型的二进制表示的最后一个字节先存储

小端序 → 数据类型的二进制表示的第一个字节先存储

例如,取十六进制数0x01e8f7a1,这个十六进制表示的十进制数是32044961。这个值在内存中的存储方式是什么?视觉上,根据字节序的不同,它看起来像下面的其中一个图表。

计算机如何在内存中存储多字节值的图示。
计算机如何在内存中存储多字节值的图示。

计算机如何在内存中存储多字节值的图示。

Endian-ness在以太坊中的使用方式是怎样的?

  • 以太坊使用大端和小端两种格式,使用的格式取决于变量类型。
  • 大端仅用于字节和字符串类型。这两种类型在合约存储槽中的行为与其他变量不同。
  • 小端用于其他任何类型的变量。一些例子是:uint8,uint32,uint256,int8,boolean,address等等...

状态变量在智能合约存储槽中如何填充和打包?

为了将需要少于32字节内存的变量存储在存储器中,EVM将使用0填充值,直到使用了所有32字节的槽,然后存储填充值。

许多变量类型比32字节的槽大小要小,例如:bool,uint8,address。以下是当我们想要存储需要少于32字节内存的类型的状态变量时它是什么样子的图示:

*需要少于32字节内存的变量存储的图表。*
*需要少于32字节内存的变量存储的图表。*

需要少于32字节内存的变量存储的图表。

由于EVM填充,开发者可以直接访问状态变量a和c,但会浪费大量昂贵的存储内存。EVM以最小化读/写值的成本为代价来存储变量。

如果我们仔细考虑合约状态变量的大小和声明顺序,EVM将把变量打包到存储槽中,以减少使用的存储内存量。以上文中的PaddedContract示例为例,我们可以重新排列状态变量的声明顺序,让EVM紧密地将变量打包到存储槽中。

下面是一个示例,即PackedContract,它只是对PaddedContract示例中变量的重新排序:

https://img-blog.csdnimg.cn/img_convert/c518da2b602bdfc3363fd54074fb3a56.jpeg
https://img-blog.csdnimg.cn/img_convert/c518da2b602bdfc3363fd54074fb3a56.jpeg

在 EVM 中,变量将从存储槽的右侧开始打包,对于每个后续可以打包到同一槽中的状态变量,将向左移动。在这里,我们可以看到变量 a 和 c 已经被打包到存储槽 0 中。由于变量 b 的大小不能适应槽 0 中剩余的空间,EVM 将变量 b 分配给槽 1。

与填充变量相比,打包变量最大程度地减少了存储使用量,但是读取/写入这些变量的代价更高。额外的读写成本来自于需要进行额外位操作以读取 a 和 c。例如,要读取 a,EVM 需要读取存储槽 0,然后掩码除属于变量 a 的所有位。

紧密打包变量的存储气体节省可以显着增加读取/写入它们的成本,如果打包的变量通常不一起使用。例如,如果我们需要经常读取 c 而不读取 a,则最好不要紧密打包变量。这是开发人员编写合同时必须考虑的设计问题。

用户定义的类型(结构体)如何存储在合约内存中?

在我们有一组逻辑上属于一起的变量并且经常作为一个单元进行读写的情况下,我们可以通过Solidity的struct关键字定义一个用户定义的类型,并应用上述字节打包的知识,以获得最有效的燃气使用,以便在存储变量的使用和读写方面进行优化。

结构体在智能合约存储内存中如何打包和存储的图表。
结构体在智能合约存储内存中如何打包和存储的图表。

结构体在智能合约存储内存中如何打包和存储的图表。

现在我们可以受益于紧密字节打包和分组读写状态变量someStruct。

静态大小的状态变量存储在它们对应的槽位中。如果一个静态大小的变量占用2个槽位(64字节)并存储在槽位s中,那么下一个存储变量将存储在槽位s + 2处。

例如,在以下合约中,状态变量的存储布局如下:

静态大小状态变量如何存储在存储插槽中的图表。
静态大小状态变量如何存储在存储插槽中的图表。

静态大小状态变量如何存储在存储插槽中的图表。

动态大小的状态变量如何存储在智能合约内存中?

动态大小的状态变量与静态大小的状态变量分配的插槽相同,但分配给动态状态变量的插槽只是标记插槽。也就是说,插槽标记了动态数组或映射存在的事实,但插槽不存储变量的数据。

对于动态大小的变量,为什么不能像静态大小的变量一样直接存储其数据到分配的插槽中?

因为如果向变量添加新项,将需要更多的插槽来存储其数据,这意味着后续的状态变量将被推到更远的插槽。

通过使用标记插槽的keccak256哈希,我们可以利用巨大的虚拟存储区域来存储变量,而不会出现动态大小的变量增长并与其他状态变量重叠的风险。

对于动态大小的数组,标记插槽还存储数组的长度。标记插槽号的keccak256哈希是指向数组值在合同存储布局中存放位置的“指针”。

例如:

动态大小状态变量如何使用标记槽和keccak256哈希存储在存储槽中的图表。
动态大小状态变量如何使用标记槽和keccak256哈希存储在存储槽中的图表。

动态大小状态变量如何使用标记槽和keccak256哈希存储在存储槽中的图表。

映射存储在智能合约存储中的方式是怎样的?

对于mappings,标记槽只标记了有映射的事实。要找到给定键的值,使用公式keccak256(h(k).p),其中:

  • 符号.表示字符串连接
  • p是状态变量在智能合约中的声明位置
  • h()是应用于键的函数,取决于键的类型
  • 对于值类型,h()返回填充为32个字节的值
  • 对于字符串和字节数组,h()只返回未填充的数据

下面是一个描述映射如何存储在内存中的图示:

智能合约存储槽中映射如何存储的图表。
智能合约存储槽中映射如何存储的图表。

智能合约存储槽中映射如何存储的图表。

本文由 mdnice 多平台发布

猜你喜欢

转载自blog.csdn.net/weixin_40156487/article/details/130837500