2018年3月9日,史蒂夫马克思html
以太坊智能合约使用一种不常见的存储模式,这种模式一般会让新开发人员感到困惑。在这篇文章中,我将描述该存储模型并解释Solidity编程语言如何使用它。编程
每一个在以太坊虚拟机(EVM)中运行的智能合约的状态都在链上永久地存储着。这个存储能够被认为是每一个智能合约都保存着一个很是大的数组,初始化为全0。数组中的每一个值都是32字节宽,而且有2^256个这样的值。智能合约能够在任何位置读取或写入数值。这就是存储接口的大小。
我鼓励你坚持“天文数组”的思考模式,但要注意,这不是组成以太坊网络的物理计算机的实际存储方式。存储数组空间实际上很是稀疏,由于不须要存储零。将32字节密钥映射到32字节值的键/值存储将很好地完成这项工做。一个不存在的键被简单地定义为映射到零值。数组
因为零不占用任何空间,所以能够经过将值设置为零来回收存储空间。当您将一个值更改成零时,智能合约中内置的返还gas机制被激活。安全
在这个存模型中,到底是怎么样存储的呢?对于具备固定大小的已知变量,在内存中给予它们保留空间是合理的。Solidity编程语言就是这样作的。网络
contract StorageTest { uint256 a; uint256[2] b; struct Entry { uint256 id; uint256 value; } Entry c; }
在上面的代码中:app
这些下标位置是在编译时肯定的,严格基于变量出如今合同代码中的顺序。编程语言
使用保留下标的方法适用于存储固定大小的状态变量,但不适用于动态数组和映射(mapping
),由于没法知道须要保留多少个槽。函数
若是您想将计算机RAM或硬盘驱动器做为比喻,您可能会但愿有一个“分配”步骤来查找可用空间,而后执行“释放”步骤,将该空间放回可用存储池中。布局
可是这是没必要要的,由于智能合约存储是一个天文数字级别的规模。存储器中有2^256个位置可供选择,大约是已知可观察宇宙中的原子数。您能够随意选择存储位置,而不会遇到碰撞。您选择的位置相隔太远以致于您能够在每一个位置存储尽量多的数据,而无需进入下一个位置。ui
固然,随机选择地点不会颇有帮助,由于您没法再次查找数据。Solidity改成使用散列函数来统一并可重复计算动态大小值的位置。
动态数组须要一个地方来存储它的大小以及它的元素。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; }
在上面的代码中,动态大小的数组d存在下标5的位置,可是存储的惟一数据是数组的大小。数组d中的值从下标的散列值hash(5)开始连续存储。
下面的Solidity函数计算动态数组元素的位置:
function arrLocation(uint256 slot, uint256 index, uint256 elementSize) public pure returns (uint256) { return uint256(keccak256(slot)) + (index * elementSize); }
一个映射mapping须要有效的方法来找到与给定的键相对应的位置。计算键的哈希值是一个好的开始,但必须注意确保不一样的mappings产生不一样的位置。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; // slot 5 for length, keccak256(5)+ for data mapping(uint256 => uint256) e; mapping(uint256 => uint256) f; }
在上面的代码中,e的“位置” 是下标6,f的位置是下标7,但实际上没有任何内容存储在这些位置。(不知道多长须要存储,而且独立的值须要位于其余地方。)
要在映射中查找特定值的位置,键和映射存储的下标会一块儿进行哈希运算。
如下Solidity函数计算值的位置:
function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) { return uint256(keccak256(key, slot)); }
请注意,当keccak256
函数有多个参数时,在哈希运算以前先将这些参数链接在一块儿。因为下标和键都是哈希函数的输入,所以不一样mappings之间不会发生冲突。
动态大小的数组和mappings能够递归地嵌套在一块儿。当发生这种状况时,经过递归地应用上面定义的计算来找到值的位置。这听起来比它更复杂。
contract StorageTest { uint256 a; // slot 0 uint256[2] b; // slots 1-2 struct Entry { uint256 id; uint256 value; } Entry c; // slots 3-4 Entry[] d; // slot 5 for length, keccak256(5)+ for data mapping(uint256 => uint256) e; // slot 6, data at h(k . 6) mapping(uint256 => uint256) f; // slot 7, data at h(k . 7) mapping(uint256 => uint256[]) g; // slot 8 mapping(uint256 => uint256)[] h; // slot 9 }
要找到这些复杂类型中的项目,咱们可使用上面定义的函数。要找到g123:
// first find arr = g[123] arrLoc = mapLocation(8, 123); // g is at slot 8 // then find arr[0] itemLoc = arrLocation(arrLoc, 0, 1);
要找到h2:
// first find map = h[2] mapLoc = arrLocation(9, 2, 1); // h is at slot 9 // then find map[456] itemLoc = mapLocation(mapLoc, 456);
下表显示了如何计算不一样类型的存储位置。“下标”是指在编译时遇到状态变量时的下一个可用下标,而点表示二进制串联:
类 | 声明 | 值 | 位置 |
---|---|---|---|
简单的变量 | T v | v | v的下标 |
固定大小的数组 | T[10] v | v[n] | (v's slot)+ n *(T的大小) |
动态数组 | T[] v | v[n] | keccak256(v's slot)+ n *(T的大小) |
v.length | v的下标 | ||
制图 | mapping(T1 => T2) v | v[key] | keccak256(key。(v's slot)) |
若是您想了解更多信息,我推荐如下资源: