本文讨论了以太坊上的可升级智能合约及其存储选项。作者探讨了三种主要的数据存储方法,包括各版本独立的存储、将数据存储在单独的数据库合约中,以及通过代理合约存储数据。其中,每种方法都有其优缺点,文章提供了代码示例和解决方案,展示了可升级合同在动态环境中的实现挑战和潜力。最后,作者承诺在后续文章中进一步探讨相关主题。
作者:MixBytes 团队
这是系列文章《可升级智能合约:存储与挑战》的第一篇。
我们将仔细研究可升级的智能合约,开发者可用的功能和存储选项。
以太坊区块链上的智能合约是不可变的。一旦智能合约被部署,就无法更改合约地址上的代码。你可以完全删除一个合约,或者更确切地说,如果这样的功能最初被编写在代码中,智能合约可以自我销毁。一方面,这解决了信任问题,用户可以确保一切完全由算法控制。另一方面,修复错误现在变得不可能。
于是,可升级的以太坊智能合约救了我们。等等,什么?我们刚才说以太坊没有这样的合约(与 EOS 相比)。然而,可升级的智能合约是可以模拟的。其理念是智能合约地址和代码保持不变,代码将执行转发到另一个合约,然后返回结果。在这种情况下,主要的智能合约被称为 proxy。在将另一个合约地址保存在一个变量中后,我们可以像改变合约状态一样轻松地更改它,而代码依然保持不可变。因此,最终可能会有多个智能合约版本;迁移是通过记录新版本地址来完成的。
与任何其他软件类似,开发者在每次发布新版本时都必须处理数据迁移问题。在 proxy 的情况下,智能合约状态到底要存储在哪里?那么,我们有三种截然不同的方法。
第一种方法意味着每个版本分别在其自己的存储中存储其状态。这确保了最大程度的孤立和控制,排除了冲突,但增加了复杂性和迁移到存储的单独记录所产生的Gas费用。假设正在开发一个基本的代币合约。在这种情况下,核心数据是余额:
mapping (address => uint256) private _balances;
直接从新版本调用 _balances 是不可能的;为了允许这样做,数据必须先从先前版本迁移。注意,迁移必须仅执行一次。
mapping (address => uint256) private _balances;
// 代币智能合约的先前版本
ERC20 private _previous;
// 标志表示某个用户余额的迁移已执行
mapping (address => bool) private _migrated;
function balanceOf(address owner) public view returns (uint256) {
return _migrated[owner] ? _balances[owner] : _previous.balanceOf(owner);
}
function setBalance(address owner, uint256 new_balance) private {
_balances[owner] = new_balance;
if (!_migrated[owner])
_migrated[owner] = true;
}
此时出现了额外的问题:迁移不能在某个请求时即时进行,因为可能需要将数据写入存储,而在只读函数中不可用。因此,所有对余额的请求,甚至内部请求,都必须通过 balanceOf 和 setBalance 函数进行。模板代码较高,更不用说Gas消耗增加。
在最糟糕的情况下,对只读函数的调用将遍历整个代币版本链收集数据,并未能记录与最新版本相关的操作结果,因为它们没有修改权限。从最新版本以外的其他版本调用这些函数是可能的,但意义不大。
同时在最新的代币代码版本中迁移数据并记录当前用户的操作结果需要调用可以改变最新版本状态的函数。因此,后续对任何其他函数的调用都不会经过整个代币版本链。只有 proxy 合约被允许调用能够改变最新版本状态的函数;对于之前的版本,这些函数的访问必须完全拒绝。
另一种存储选项可以被建议。让我们看看传统程序如何解决这个问题。数据与代码是分开的!而且,当涉及复杂程序和系统时,数据通常存储在 SQL 或 NoSQL 存储中。
可以使用为此目的编写的临时智能合约作为存储。因此,数据总是保存在这个合约的存储中,而不论当前代币代码版本如何。这个合约的代码可以移到一个库中,但这不是当前的议程。不需要将数据从一个存储迁移到另一个存储;相反,存储访问权限从一个版本转移到下一个版本。然而,使用这种类型的存储并非没有问题。它需要定义一个对任何版本的代币智能合约可用的接口,例如类 SQL 或文档导向的。说到这种存储类型的例子,请看 EOS 表格。
让我们把结构、字段名称和数据类型统一为 data scheme。存储智能合约代码可以由静态部分(无论当前数据方案如何,代码都不会变化)和动态部分(依赖于方案的代码)组成。动态部分包含大量模板代码,因此自动生成它是有意义的,正如在 Protocol Buffers 或 Apache Thrift 中实现的那样。我曾在 ETHBerlin 黑客松上处理过类似的任务,开发了 以太坊列式数据存储原型。
数据项由以下结构描述:
struct Cafe {
string name;
uint32 latitude;
uint32 longitude;
address owner;
}
..之后我们为 GitHub 生成一个“驱动”。该驱动调用来自 GitHub 的静态代码,例如 `CDF.writeString`、`CDF.chunkDataPosition` 及其他函数。
正如我之前提到的, 解决方案 覆盖了其他问题,并作为外部存储操作的一个示例。目前,我所知没有在以太坊智能合约存储上工作的 SQL/NoSQL 存储实现。这似乎是一个有趣的话题,可能成为解决可变智能合约数据存储问题的有前途的解决方案。
状态存储在作为数据库使用的合约中,并通过调用 call 而非 delegatecall 指令进行调用。对写调用的访问应受到保护,仅限于 proxy 合约。这个数据库合约的公共代码可以移到一个库中。
在代理合约存储中存储数据的委托调用
最后,第三种选择是在 proxy 合约存储中存储数据。如果 proxy 是一个独立的智能合约,特定代码版本如何访问数据?EVM 的 delegatecall 功能使之成为可能。它在目标地址处执行代码,但使用执行 delegatecall 指令的合约的存储(详细见 Solidity)。
调用以前合约版本的函数毫无意义,因为这些不过是“代码片段”,所有状态被存储在 proxy 合约中。delegatecall 用于调用库合约。库代码通过指针轻松找到所需的数据。然而,该指令可能给 proxy 合约带来潜在威胁。不幸的是,官方 Solidity 文档几乎没有警示我们注意:“如果通过低级 delegatecall 访问状态变量,则两个合约的存储布局必须对齐,以便被调用的合约可以通过名称正确访问调用合约的存储变量。”
我们研究了可升级智能合约的开发,并检查了 3 种数据存储方法。下次我们将深入探讨 delegatecall 以及可能会出现的问题。祝你合约安全!
MixBytes 是一支专业的区块链审计师和安全研究团队,专注于为 EVM 兼容和 Substrate 基础项目提供全面的智能合约审计和技术咨询服务。请加入我们 X,以随时了解最新的行业趋势和见解。
- 原文链接: mixbytes.io/blog/storage...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!