查看原文
其他

使用 EIP-2535 “钻石” 升级智能合约代码和存储布局

Guilherme Dantas CTSI中文社区 2022-12-17


照片由 Bas van den Eijkhof 在 Unsplash 上拍摄


介绍


以太坊智能合约代码是不可变的:部署后无法更改。这与区块链的去信任性密切相关,但阻碍了智能合约的开发和维护。除此之外,智能合约代码大小限制为 24 KB。


除了代码之外,每个智能合约都有自己的内部存储,即 256 位插槽阵列。Solidity 是以太坊智能合约最流行的编程语言,它将状态变量分配给存储槽并处理低级细节。但是,在许多情况下,开发人员可能希望在部署后更改存储布局,例如添加变量或优化存储读/写操作。


为了缓解这些问题,EIP-2535 提出了一种复杂的代理模式,它允许添加、替换和删除功能,并实际上消除了对代码大小的任何限制;以及创新的存储布局,抗碰撞且非常灵活。


在以下部分中,我们将介绍 EIP-2535 的基础知识,以及它如何实现代码可升级性。然后,我们将看到它是如何实现存储布局升级的。最后,我将提出一些与升级动态字段任务相关的注意事项,并提出缓解措施。


EIP-2535 及升级功能


尽管智能合约具有不可变的代码,但它们可以调用其他智能合约的函数。特别是,他们也可以这样做,同时通过委托调用保留存储上下文。此功能是启用高级代理合约的基础。


EIP-2535 中提出的代理合约,即 Diamond,代码很少,完全通用。同时,所有特定于应用程序的功能都由称为 Facets 的合约实现。在构造时,钻石只公开两个函数:一个 diamondCut 函数,它允许钻石所有者添加、替换和/或删除函数实现,以及一个 fallback 函数,它动态地将函数调用无缝地分派到适当的方面。


EIP 还指定了一个名为 Diamond Loupe 的接口,它允许对可以通过 fallback 函数调用的函数进行自省。这对于测试升级时的开发人员和使钻石的功能透明的用户非常有用。此外,可升级性是一项可选功能,因为可以随时删除 diamondCut 功能。


EIP-2535 还提出了一种称为 Diamond Storage 的存储布局,它将状态变量分组到结构中。为了避免冲突,这些结构存储在由非常具有描述性的字符串(如 mytoken.diamond.storage)的哈希值给出的位置。只有在 Solidity 0.6.4 允许引用任意存储位置的结构后,这种布局才成为可能。


diamondCut 函数还允许钻石所有者在添加、替换和/或删除函数实现之后指定要委托的额外函数调用。根据规范,“执行此操作是为了初始化数据或设置或删除任何需要或不再需要的东西”。


wu yi 在 Unsplash 上拍摄


升级存储布局


EIP-2535 允许以非常简单的方式升级功能,但很少提及升级存储布局。为了举例说明我们的意思,假设我们部署了一个采用菱形图案并利用以下结构的合约。


struct DiamondStorage1 { uint32 ts; uint32 a; uint64 b; uint128 c; mapping (address => uint32) m;}


请注意,ts、a、b 和 c 紧密封装在同一个 256 位存储槽中。假设 ts 存储了与合约的最后一次交互的时间戳。我们知道,一个无符号的 32 位整数只能保存 UNIX 时间戳,直到 2106 年左右。因此,如果我们想将此事件推迟数十亿年,例如,我们可以用 64 位表示 ts。考虑到这一点,向后兼容的结构如下所示。


struct DiamondStorage2 { uint32 _ts; // deprecated uint32 a; uint64 b; uint128 c; mapping (address => uint32) m; uint64 ts; // new field!}


需要注意的是,我们选择将 ts 移动到结构的末尾而不是在原地扩展它,因为这可能会改变 _ts 之后的某些字段的存储位置。特别是,如果将映射 m 移动到某个其他存储位置,则每个条目 m[k] 也会如此。要初始化新字段 ts,我们需要先部署一个具有以下功能的合约。假设 diamondStorage 返回指向正确存储位置中结构的指针。


function upgrade() external { DiamondStorage2 storage ds2 = diamondStorage(); ds2.ts = uint64(ds2._ts); // expand `ts` in a new field}


我们还需要将引用已弃用字段 (uint32 _ts) 的函数替换为引用新字段 (uint64 ts) 的较新版本。但是,如果有任何函数因为 _ts 而暴露了 uint32 类型的参数或返回值,我们可以选择保持与调用它们的合约的向后兼容性。


因为我们总是可以将新字段附加到结构的末尾并替换引用它们的函数实现,所以很容易看出添加和弃用字段,无论类型如何,都是与添加、替换和/或删除函数实现一样简单的操作 .


但是,更改字段类型并不总是那么容易。我们能够更改 ts 的类型,因为整数占据了恒定数量的存储槽,因此可以使用恒定数量的气体进行复制和扩展。但是,对于动态类型(如数组和映射)则不能这样说,因为它们可以占用任意数量的存储槽,并且迭代它们可能会消耗超过块 gas 限制,这可能会使这样的升级在一次执行中不切实际 交易。


以下列表有助于组织 EIP-2535 可能进行的某些类型的升级,同时注意以太坊的可扩展性限制。


  • 添加功能


指定要添加的函数选择器和构面的地址。


  • 删除函数


指定要删除的函数选择器。


  • 替换函数的构面地址


指定函数选择器和新构面的地址。


  • 添加字段


将字段附加到某些现有结构的末尾,或使用它们创建新结构。


  • 删除字段


弃用这些字段。如果结构中没有动态字段或动态字段足够小,您还可以创建一个没有此类字段的新结构并复制其余字段。


  • 更改静态字段的类型


(例如从 32 到 64 位整数)

弃用旧字段,将新字段附加到结构的末尾,并将旧字段的内容转换为新字段。


  • 更改数组元素的类型


(例如,从 32 位到 64 位整数数组)

如果数组元素的数量足够少,则弃用旧字段,将新字段附加到结构的末尾,然后逐个元素地将数组复制到新位置。


  • 更改映射键和/或值的类型


(例如从整数到结构)

如果映射条目的数量足够小,则弃用旧字段,将新字段附加到结构的末尾,然后逐个条目地将映射复制到新位置。


照片由Dawn McDonald在 Unsplash 上拍摄


注意事项和缓解措施

由于块气体限制,动态字段的操作并不总是可以在单个事务中执行。从这个角度来看,在超过块气体限制之前存储写入次数的过度乐观上限是 1500。可能的缓解方法是将任务分成几个独立的事务,每个不超过块气体限制。


如果动态字段没有被任何函数改变,升级很简单。例如,假设我们要更改前面示例中的映射字段 m。我们可以弃用它并将一个新字段附加到结构的末尾,如下面的代码片段所示:


struct DiamondStorage3 { uint32 _ts; uint32 a; uint64 b; uint128 c; mapping (address => uint32) _m; // deprecated uint64 ts; mapping (address => uint256) m; // new field!}

然后,我们可以一点一点地转换映射条目,而不影响合约的行为,合约的行为只考虑旧的字段。为了能够将升级拆分为多个事务,我们将映射具有有效条目的键集划分为几个不相交的子集。为了给这次升级提供一些动力,假设我们想将每个值乘以某个常数 c。


function upgrade(address[] calldata keys, uint256 c) external { DiamondStorage3 storage ds3 = diamondStorage(); for (uint i; i < keys.length; ++i) { address key = keys[i]; ds3.m[key] = c * uint256(ds3._m[key]); }}


在使用所有有效键初始化新映射字段 m 后,我们可以将引用旧字段的函数替换为引用新字段的较新版本。


另一方面,如果要更新的动态字段可以由用户更改,我们希望避免 read-after-write 并发。一个廉价而肮脏的解决方案是删除引用映射的函数,进行所有必要的更改,然后添加新版本的函数。但是,这会破坏升级期间依赖于钻石的合约,甚至可能被恶意钻石所有者用作针对此类合约的 DoS 攻击。


结论


EIP-2535 允许添加、删除和替换函数实现,以及在每次更新后在菱形的存储上下文上执行的任意代码。这允许以灵活的方式无限期地升级代码和存储布局。


但是,在升级动态字段时需要考虑一些警告,因为它们的大小可以任意大,并且遍历它们可能会超过块气体限制。一种可能的缓解措施是将升级动态字段的任务拆分为多个事务,每个事务的消耗都低于限制。此技术非常适用于用户无法更改的字段。然而,在相反的情况下,钻石所有者被迫禁止用户在升级期间更改这些字段,这可能会暂时破坏依赖钻石的合同和应用程序。



关于Cartesi

Blockchain OS 是一分布式的第 2 层基础设施,支持 Linux 和主流编程软件组件。使得开发人员可以第一次在Blockchain OS上使用丰富的传统软件工具、库和他们习惯的服务编写可扩展的智能合约,Cartesi 弥合了主流软件和区块链之间的差距。

Cartesi 正在引领数百万新创业公司及其开发人员加入并使用区块链操作系统,同时将 Linux 应用程序纳入其中。凭借开创性的虚拟机、Rollups和侧链,Cartesi 为所有开发人员铺平了道路,以帮助他们进入区块链的世界并构建下一代区块链应用程序。

Cartesi在此诚挚的邀请所有人,请和我们一起来到区块链操作系统的世界,一起探索未来。



友情提示FRIENDLY TIPS

本信息不构成任何投资建议,投资者不应以该等信息取代其独立判断或仅根据该等信息作出决策。我们力求本公众号信息准确可靠,但对这些新的准确性或完整性不作保证,亦不对因使用该等信息而引发的损失承担任何责任。

加密资产属于高风险资产,需要充分认识到其波动性



往期推荐

Webchefs一个开创性的区块链操作系统项目


CARTESI x 加密矿工 AMA回顾

Cartesi 2022 年 7 月回顾

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存