该提案旨在通过向用户收取在Unitrie节点中存储状态数据(例如,帐户状态、合约代码和合约存储)的“租金”来改善RSK中的资源利用率。存储租金可以减少存储垃圾信息的风险,并使存储付款更加公平。它还可以改善缓存并帮助保护网络免受某些IO攻击。每个交易的gas限制在执行成本和租金之间平均分配,根据新的RPC方法设置累计gas限制,收取的租金将传递给矿工。
RSKIP | 未分配 |
---|---|
标题 | 在 RSK 中实现存储租金 |
创建时间 | 2020 年 9 月 29 日 |
作者 | Sergio Demian Lerner, Diego Masini, Shreemoy Mishra |
目的 | Sca, Fair, Sec |
层级 | 核心 |
复杂度 | 2 |
状态 | 草案 |
本提案旨在通过向用户收取在 Unitrie 节点中存储状态数据(例如,账户状态、合约代码和合约存储)的“租金”来提高 RSK 中的资源利用率。存储租金可以降低存储垃圾信息的风险,并使存储支付更加公平。它还可以改善缓存,并帮助保护网络免受某些 IO 攻击。我们提出了一个系统,其中每个交易的 gas limit 在两个 gas 预算之间平均分配:一个用于常规执行成本,另一个用于租金。交易发送者可以基于新的和更新的 RPC 方法(例如 estimateGas
或 exactimateGas
)设置适当的累计 gas limit。就像执行 gas 一样,所有收取的租金都将传递给 REMASC,以作为交易费用分配给矿工。租金 不计入 区块级别的 gas limit。因此,对可以包含在一个区块中的交易数量没有不利影响。
目前,减少状态存储使用的激励措施是通过 SSTORE
、CREATE-DATA
等操作码的 gas 成本以及 SELF-DESTRUCT
和 CLEAR-SSTORE
的“退款”来提供的。但是,这些是一次性激励。存储租金是一种经常性成本,这为更明智地使用存储提供了持续的激励。
存储区块链状态信息并不便宜,尤其是在考虑到在许多完整节点上进行复制时。但是,存储也存在巨大的 机会成本,这些成本与磁盘 IO 性能有关。在更深的层次上,存储租金实际上是通过帮助优先考虑要保存在各种缓存中的信息来提供减少磁盘 IO 成本的激励。我们的主要注意力集中在 RAM 中的 trie 缓存上,而不是磁盘上的键值数据存储。对快速 IO 操作的需求是区块链节点使用昂贵的 SSD 而不是更便宜的 HDD 的原因。如果 RAM 变得如此便宜,以至于整个区块链状态可以常规地容纳在缓存中 - 以及交易处理所需的多个快照 - 那么像租金机制就不会有强大的好处。
除了通过缓存提高节点性能外,存储租金还可以作为一种机制来强制执行 IO 攻击的 惩罚。假设攻击者故意查询他们知道不存在于区块链状态中的节点。在这些情况下,我们可以当场为这些不存在的节点收取“一些”租金作为惩罚。对于诚实的错误,惩罚将很小(例如,相当于 6 个月的租金),但对于大规模攻击而言,惩罚可能很大。
实施存储租金的其他好处包括可以使用上次支付租金的时间戳来实施 节点休眠。
区块 gas limit 不适用于存储租金。租金是矿工的额外无上限收入来源。因此,以 gas 支付的交易费用(执行 + 存储租金)可能超过区块的 gas limit。
当前的 RSKIP 对存储租金采取了极简主义的方法。它代表了之前几个 RSKIP 的简化,包括 RSKIP21、RSKIP52、RSKIP61 和 RSKIP113。有关存储租金的动机、概念演变和影响的其他信息,请参见 [1]。
租金是在状态 Unitrie 中各个 包含值的(终端)节点的粒度上计算和跟踪的。租金取决于节点的大小以及数据存储的持续时间。节点的大小 (nodeSize
) 计算为节点的值长度加上 128 字节(用于存储开销)。因此,nodeSize
仅近似于实际消耗的空间,例如,它没有考虑嵌入式节点。除了合约的 存储根 之外,其他非终端 Unitrie 节点不包括在存储租金计算中。
每个 Unitrie 节点将有一个新字段 lastRentPaidTime
来存储租金支付时间戳(例如,以 Unix 秒为单位)。租金将遵循使用后付款或后付费模式(新节点除外,请参见下文)。
执行交易时,查询的所有 Unitrie 键/值对(例如,通过 SLOAD
、SSTORE
或 EXTCODE*
)都存储在缓存中。该交易创建的所有 新 trie 键/值对(新帐户、合约、存储单元)都存储在不同的缓存中。
在一个交易被完全处理后,两个缓存都会被迭代,并且会为每个节点收取存储租金。对于所有新节点,大约提前收取 6 个月的租金,并且它们的 lastrentpaidtime
时间戳被设置为从现在开始 6 x 30 x 24 x 3600 = 15552000
秒。
租金以给定费率 R 的 gas 单位计算。先前的 RSKIP(例如,RSKIP 52、61 和 113)中已建议值 R = 1/(2^21) 每字节每秒 gas。
计算示例: 设 SecondsAYear
为 31536000。每个字节每秒支付 1/2^21 gas。因此,一个存储字节每年支付 SecondsAYear/2^21=15.03
gas 单位。
nodeSize
为 138。这样的帐户每年将消耗 2075 单位的 gas 租金。为了减少 trie 或缓存 IO 操作的数量,仅当应付金额超过某些阈值时才收取租金。对于在交易期间修改的 先前存在的 键值对 - 例如,存储单元值已更改 - 租金收取触发器设置为 1000 gas。对于已访问但未修改的节点(例如,合约代码),则使用更高的阈值(10000 gas)。如果未达到这些阈值,则不会将节点添加到租金跟踪缓存中。因此,较大的阈值会降低与租金相关的 trie IO 操作的频率。
例如,如果一个简单的 EOA 帐户(值长度为 10 字节)经常使用,则租金将大约每年收取两次。如果它不活动,并且只有合约使用 BALANCE 操作码检查其余额,则租金将每 5 年收取一次。
计算和收取逻辑是完全自动的,并且是交易执行的一部分。一个交易 访问、修改 或 创建 的所有 trie 节点都会被自动检查并添加到租金收取缓存中,前提是满足收取阈值。用户无法选择或排除个别租金支付。
任何应付的租金都由交易的发送者支付。如前所述,我们提出了一个简单的系统,其中每个交易的 gas limit 在两个预算之间平均分配:一个用于常规执行 gas 成本,另一个用于租金成本。交易发送者可以根据执行和租金使用的 gas 估算值适当地设置 gaslimit。
在交易结束时,所有剩余的执行 gas 或租金 gas 都将退还到发送者的帐户。
// 计算预先存在的节点的租金(对于新节点,预付6个月的租金)
setRentDue(Trie node, boolean modifiedByTX){
lrpt = node.lastRentPaidTime;
currentTime = time.Now();
timeDelta = currentTime - lrpt; //自上次支付租金以来的时间
// 计算应付租金,但仅适用于过去应付租金的节点
if (timeDelta > 0) {
// 公式为 1/2^21 * (节点 valueLength + 128 字节开销) * timeDelta (秒)
rd = calculateStorageRent(node.valueLength, timeDelta);
}
// 如果租金超过高阈值(修改状态无关紧要)
if (rd > notModifiedThresh){
node.rentDue = rd;
node.lastrentpaidtime = currentTime; //更新时间戳
} else {
// 检查金额是否超过较低阈值,并且节点已被修改
if (modifiedByTX && rd > modifiedThresh){
node.rentDue = rd;
node.lastrentpaidtime = currentTime; //更新时间戳
} else {
// 不值得在这个时候收取租金/更新 trie
node.rentDue = 0L;
}
}
假设用户 Alice
向合约 gameOfLife
发送一个交易,该交易修改了 gameOfLife
的两个存储单元。将为包含 Alice 帐户信息的 trie 节点、gameOfLife
的帐户信息、存储根、包含其合约代码的节点以及最终修改的两个存储单元触发租金计算。在交易结束时,这些节点中的任何一个应付的任何租金都会从交易的 gaslimit(预留给存储租金的部分)中收取,并且 lastrentpaidtime
时间戳会更新。
我们希望用户通常最终为自己的节点以及用户与之交互的合约的某些存储单元支付租金。偶尔 - 偶然地 - 个别用户最终可能会为包含合约代码的节点支付租金。我们希望这种成本很小,并且会随着时间的推移在用户之间平均分配。在大多数情况下,用户可以在广播实时交易之前获得可靠的租金 gas 估算值。
如果交易因 OOG(执行 gas 或租金 gas 不足)异常或 REVERT 而结束,则 25% 的存储租金 gas 预算将作为 IO 成本的补偿而消耗。在这种情况下,不会更新租金时间戳。
CALL 操作码允许交易发送者为 CALL(“子”或“被调用者”)指定 gas limit。这为“父项”/“调用者”提供了在子 CALL OOG 或遇到其他异常时继续执行的机会。我们 不 将相同的机制扩展到租金 gas。所有可用的租金 gas 都会传递给 CALL,并且无法构建用户指定的租金 gas limit。我们假设用户在与他们不信任的合约交互时会小心谨慎,否则某些 CALL 可能会消耗掉他们所有的租金预算,即使 TX 剩下一些执行 gas 也无法继续。
我们开发了一个带有存储租金的 RSK(J) 节点的 研究实施。
使用实验节点(在 regtest 上)进行租金计算的 CREATE 交易示例
root@5429dcd447b9:~/code/rskj# ./gradlew test --tests BlockExecRentTest.executeBlockWithOneCreateTransaction
...
...
发送者: 1ddd173f36582f9de451b54553aa29eab2656a0b
接收者: null // 合约创建
合约: 27fcd2c5584134d8426e5028b35df6d4e26b2fd7 //tx.getContractAddr()
TX 数据: 0x600160026000600160026000 //仅用于测试 gas 使用的一些 PUSH 操作码
TX 值: 10 //合约捐赠
执行 GasLimit 100000 //TX gas limit 的 50:50 分割(设置为 200K)
执行 gas 使用 53706 //21K(基本)+ 32K(创建)+ 数据成本
执行 gas 退款 46294
租金 GasLimit 100000 //TX gas limit 的 50:50 分割(设置为 200K)
租金 gas 使用 8130 //1 个节点租金更新(发送者)+ 3 个新节点创建
租金 gas 退款 91870
交易费用(执行 + 租金):61836 // 租金 gas 也传递给矿工
具有“已更新”租金时间戳的 trie 节点数:1 // 新 Trie::putWithRent(key, value, timestamp)
创建的新 trie 节点数:3 // 来自 Program、TransactionExecutor、VM 的 trie“访问”的存储库更改
发送者余额 1938164 // 初始余额为 2_000_000
发送者 LRPT 1595968062 // 发送者帐户节点上次支付租金的时间戳 (LRPT)
合约捐赠 10
合约 LRPT 1611520062 // 新合约节点, LRPT 在 6 个月后
// (vm.program.RentData 中的租金收取逻辑)
区块交易费用:61836 // 租金与正常执行 gas 一起传递给 REMASC
通过 CC0 放弃版权和相关权利。
- 原文链接: github.com/rsksmart/RSKI...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!